diff --git a/README.md b/README.md index 1f85b314..5a1f8bd4 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The MiniMax Code Agent communicates through the host MCP client. Business reques | MiniApp | What it does | Author | | --- | --- | --- | +| [AI Office](plugins/cristinamadureira353-svg/ai-office/) | Manage five model-powered employees in a pixel office: client tasks, chats, breaks, meetings and quota-aware offline states; uses your own API keys | [最爱双皮奶](https://github.com/cristinamadureira353-svg) | | [Token Usage Board](plugins/amszuidas/mcode-token-usage-board/) | Explore local Token usage by date, model, and session, including input, output, and cache usage | [amszuidas](https://github.com/amszuidas) | | [Token Usage Board](plugins/yanhy2000/mcode-usage-monitor/) | Watch local Token usage, output speed, and cache hit rate in near real time; filter by time range, model, and session | [yanhy2000](https://github.com/yanhy2000) | | [Model Manager](plugins/ocoomber/openrouter-model-manager/) | Browse, search, and enable/disable models in your `~/.minimax/config.yaml` with instant save, bulk actions, one-click undo, and automatic backups | [ocoomber](https://github.com/ocoomber) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 43754dec..e9510d90 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -60,6 +60,7 @@ MiniMax Code Agent 通过宿主 MCP 客户端与 MiniApp 协作。业务请求 | MiniApp | 功能 | 作者 | | --- | --- | --- | +| [AI 事务所](plugins/cristinamadureira353-svg/ai-office/README.zh-CN.md) | 五位真实模型员工的像素事务所:接单、聊天、休息和开会,额度耗尽时角色暂停;使用玩家自己的 API Key | [最爱双皮奶](https://github.com/cristinamadureira353-svg) | | [Token 用量看板](plugins/amszuidas/mcode-token-usage-board/README.zh-CN.md) | 按时间、模型和会话查看本机 Token 用量,包含输入、输出和缓存用量 | [amszuidas](https://github.com/amszuidas) | | [Token 用量看板](plugins/yanhy2000/mcode-usage-monitor/README.zh-CN.md) | 近实时查看本机 Token 用量、输出速度与缓存命中率,可按时间范围、模型和会话筛选 | [yanhy2000](https://github.com/yanhy2000) | | [模型管理器](plugins/ocoomber/openrouter-model-manager/README.zh-CN.md) | 浏览、搜索并启用/停用 `~/.minimax/config.yaml` 中的模型,支持即时保存、批量操作、一键撤销和自动备份 | [ocoomber](https://github.com/ocoomber) | diff --git a/plugins/cristinamadureira353-svg/ai-office/.minimax-plugin/plugin.json b/plugins/cristinamadureira353-svg/ai-office/.minimax-plugin/plugin.json new file mode 100644 index 00000000..7004f01c --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/.minimax-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "name": "ai-office", + "displayName": "AI 事务所", + "version": "0.5.10", + "description": "五个真实模型员工的像素事务所;新玩家由 AI 引导选择模型、申请 Key 和配置,之后直接开玩,额度耗尽时角色脑袋宕机。", + "author": "最爱双皮奶 (cristinamadureira353-svg)", + "icon": "icon.png", + "category": "Other", + "exampleQueries": ["打开 AI 事务所", "打开像素办公室游戏"], + "apps": [], + "mcpServers": [], + "skills": [] +} diff --git a/plugins/cristinamadureira353-svg/ai-office/COPYRIGHT-GODOT.txt b/plugins/cristinamadureira353-svg/ai-office/COPYRIGHT-GODOT.txt new file mode 100644 index 00000000..34aa95ed --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/COPYRIGHT-GODOT.txt @@ -0,0 +1,2209 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Comment: + Exhaustive licensing information for files in the Godot Engine repository + ========================================================================= + . + This file aims at documenting the copyright and license for every source + file in the Godot Engine repository, and especially outline the files + whose license differs from the MIT/Expat license used by Godot Engine. + . + It is written as a machine-readable format following the debian/copyright + specification. Globbing patterns (e.g. "Files: *") mean that they affect + all corresponding files (also recursively in subfolders), apart from those + with a more explicit copyright statement. + . + Licenses are given with their debian/copyright short name (or SPDX identifier + if no standard short name exists) and are all included in plain text at the + end of this file (in alphabetical order). + . + Disclaimer for thirdparty libraries: + ------------------------------------ + . + Licensing details for thirdparty libraries in the 'thirdparty/' directory + are given in summarized form, i.e. with only the "main" license described + in the library's license statement. Different licenses of single files or + code snippets in thirdparty libraries are not documented here. + For example: + Files: thirdparty/zlib/* + Copyright: 1995-2017, Jean-loup Gailly and Mark Adler + License: Zlib + The exact copyright for each file in that library *may* differ, and some + files or code snippets might be distributed under other compatible licenses + (e.g. a public domain dedication), but as far as Godot Engine is concerned + the library is considered as a whole under the Zlib license. + . + Note: When linking dynamically against thirdparty libraries instead of + building them into the Godot binary, you may remove the corresponding + license details from this file. +Upstream-Name: Godot Engine +Upstream-Contact: Rémi Verschelde +Source: https://github.com/godotengine/godot + +Files: * +Comment: Godot Engine +Copyright: 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat + +Files: icon.png + icon.svg + logo.png + logo.svg +Comment: Godot Engine logo +Copyright: 2017, Andrea Calabró +License: CC-BY-4.0 + +Files: core/math/convex_hull.cpp + core/math/convex_hull.h +Comment: Bullet Continuous Collision Detection and Physics Library +Copyright: 2011, Ole Kniemeyer, MAXON, www.maxon.net + 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat and Zlib + +Files: modules/betsy/alpha_stitch.glsl + modules/betsy/bc1.glsl + modules/betsy/bc4.glsl + modules/betsy/bc6h.glsl + modules/betsy/CrossPlatformSettings_piece_all.glsl +Comment: Betsy +Copyright: 2020-2022, Matias N. Goldberg +License: Expat + +Files: modules/godot_physics_2d/godot_joints_2d.cpp +Comment: Chipmunk2D Joint Constraints +Copyright: 2007, Scott Lembcke +License: Expat + +Files: modules/godot_physics_3d/gjk_epa.cpp + modules/godot_physics_3d/joints/godot_generic_6dof_joint_3d.cpp + modules/godot_physics_3d/joints/godot_generic_6dof_joint_3d.h + modules/godot_physics_3d/joints/godot_hinge_joint_3d.cpp + modules/godot_physics_3d/joints/godot_hinge_joint_3d_sw.h + modules/godot_physics_3d/joints/godot_jacobian_entry_3d_sw.h + modules/godot_physics_3d/joints/godot_pin_joint_3d.cpp + modules/godot_physics_3d/joints/godot_pin_joint_3d.h + modules/godot_physics_3d/joints/godot_slider_joint_3d.cpp + modules/godot_physics_3d/joints/godot_slider_joint_3d.h + modules/godot_physics_3d/godot_soft_body_3d.cpp + modules/godot_physics_3d/godot_soft_body_3d.h + modules/godot_physics_3d/godot_shape_3d.cpp + modules/godot_physics_3d/godot_shape_3d.h +Comment: Bullet Continuous Collision Detection and Physics Library +Copyright: 2003-2008, Erwin Coumans + 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat and Zlib + +Files: modules/godot_physics_3d/godot_collision_solver_3d_sat.cpp +Comment: Open Dynamics Engine +Copyright: 2001-2003, Russell L. Smith, Alen Ladavac, Nguyen Binh +License: BSD-3-clause + +Files: modules/godot_physics_3d/joints/godot_cone_twist_joint_3d.cpp + modules/godot_physics_3d/joints/godot_cone_twist_joint_3d.h +Comment: Bullet Continuous Collision Detection and Physics Library +Copyright: 2007, Starbreeze Studios + 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat and Zlib + +Files: modules/jolt_physics/spaces/jolt_temp_allocator.cpp +Comment: Jolt Physics +Copyright: 2021, Jorrit Rouwe + 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat + +Files: modules/lightmapper_rd/lm_compute.glsl +Comment: Joint Non-Local Means (JNLM) denoiser +Copyright: 2020, Manuel Prandini + 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat + +Files: platform/android/java/editor/src/main/java/com/android/* + platform/android/java/lib/aidl/com/android/* + platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml + platform/android/java/lib/src/com/google/android/* + platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java + platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java +Comment: The Android Open Source Project +Copyright: 2008-2016, The Android Open Source Project + 2002, Google, Inc. +License: Apache-2.0 + +Files: platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +Comment: ProcessPhoenix +Copyright: 2015, Jake Wharton +License: Apache-2.0 + +Files: scene/animation/easing_equations.h +Comment: Robert Penner's Easing Functions +Copyright: 2001, Robert Penner + 2014-present, Godot Engine contributors + 2007-2014, Juan Linietsky, Ariel Manzur +License: Expat + +Files: servers/rendering/renderer_rd/shaders/ss_effects_downsample.glsl + servers/rendering/renderer_rd/shaders/ssao_blur.glsl + servers/rendering/renderer_rd/shaders/ssao_importance_map.glsl + servers/rendering/renderer_rd/shaders/ssao_interleave.glsl + servers/rendering/renderer_rd/shaders/ssao.glsl + servers/rendering/renderer_rd/shaders/ssil_blur.glsl + servers/rendering/renderer_rd/shaders/ssil_importance_map.glsl + servers/rendering/renderer_rd/shaders/ssil_interleave.glsl + servers/rendering/renderer_rd/shaders/ssil.glsl +Comment: Intel ASSAO and related files +Copyright: 2016, Intel Corporation +License: Expat + +Files: servers/rendering/renderer_rd/shaders/effects/taa_resolve.glsl +Comment: Temporal Anti-Aliasing resolve implementation +Copyright: 2016, Panos Karabelas +License: Expat + +Files: thirdparty/amd-fsr/* +Comment: AMD FidelityFX Super Resolution +Copyright: 2021, Advanced Micro Devices, Inc. +License: Expat + +Files: thirdparty/amd-fsr2/* +Comment: AMD FidelityFX Super Resolution 2 +Copyright: 2022-2023, Advanced Micro Devices, Inc. +License: Expat + +Files: thirdparty/angle/* +Comment: ANGLE +Copyright: 2018, The ANGLE Project Authors. +License: BSD-3-clause + +Files: thirdparty/astcenc/* +Comment: Arm ASTC Encoder +Copyright: 2011-2024, Arm Limited +License: Apache-2.0 + +Files: thirdparty/basis_universal/* +Comment: Basis Universal +Copyright: 2019-2024, Binomial LLC. +License: Apache-2.0 + +Files: thirdparty/brotli/* +Comment: Brotli +Copyright: 2009, 2010, 2013-2016 by the Brotli Authors. +License: Expat + +Files: thirdparty/certs/ca-certificates.crt +Comment: CA certificates +Copyright: Mozilla Contributors +License: MPL-2.0 + +Files: thirdparty/clipper2/* +Comment: Clipper2 +Copyright: 2010-2024, Angus Johnson +License: BSL-1.0 + +Files: thirdparty/cvtt/* +Comment: Convection Texture Tools Stand-Alone Kernels +Copyright: 2018, Eric Lasota + 2018, Microsoft Corp. +License: Expat + +Files: thirdparty/d3d12ma/* +Comment: D3D12 Memory Allocator +Copyright: 2019-2022 Advanced Micro Devices, Inc. +License: Expat + +Files: thirdparty/directx_headers/* +Comment: DirectX Headers +Copyright: Microsoft Corporation +License: Expat + +Files: thirdparty/doctest/* +Comment: doctest +Copyright: 2016-2023, Viktor Kirilov +License: Expat + +Files: thirdparty/embree/* +Comment: Embree +Copyright: 2009-2021 Intel Corporation +License: Apache-2.0 + +Files: thirdparty/enet/* +Comment: ENet +Copyright: 2002-2024, Lee Salzman +License: Expat + +Files: thirdparty/etcpak/* +Comment: etcpak +Copyright: 2013-2022, Bartosz Taudul +License: BSD-3-clause + +Files: thirdparty/fonts/DroidSans*.woff2 +Comment: DroidSans font +Copyright: 2008, The Android Open Source Project +License: Apache-2.0 + +Files: thirdparty/fonts/JetBrainsMono_Regular.woff2 +Comment: JetBrains Mono font +Copyright: 2020, JetBrains s.r.o. +License: OFL-1.1 + +Files: thirdparty/fonts/NotoSans*.woff2 +Comment: Noto Sans font +Copyright: 2012, Google Inc. +License: OFL-1.1 + +Files: thirdparty/fonts/Vazirmatn*.woff2 +Comment: Vazirmatn font +Copyright: 2015, The Vazirmatn Project Authors. +License: OFL-1.1 + +Files: thirdparty/freetype/* +Comment: The FreeType Project +Copyright: 1996-2023, David Turner, Robert Wilhelm, and Werner Lemberg. +License: FTL + +Files: thirdparty/glad/* +Comment: glad +Copyright: 2013-2022, David Herberth + 2013-2020, The Khronos Group Inc. +License: CC0-1.0 and Apache-2.0 + +Files: thirdparty/glslang/* +Comment: glslang +Copyright: 2015-2020, Google, Inc. + 2014-2020, The Khronos Group Inc + 2002, NVIDIA Corporation. +License: glslang + +Files: thirdparty/graphite/* +Comment: Graphite engine +Copyright: 2010, SIL International +License: Expat + +Files: thirdparty/harfbuzz/* +Comment: HarfBuzz text shaping library +Copyright: 2010-2022, Google, Inc. + 2015-2020, Ebrahim Byagowi + 2019,2020, Facebook, Inc. + 2012, 2015, Mozilla Foundation + 2011, Codethink Limited + 2008, 2010, Nokia Corporation and/or its subsidiary(-ies) + 2009, Keith Stribley + 2011, Martin Hosken and SIL International + 2007, Chris Wilson + 2005-2006, 2020-2023, Behdad Esfahbod + 2004, 2007-2010, 2013, 2021-2023, Red Hat, Inc. + 1998-2005, David Turner and Werner Lemberg + 2016, Igalia, S.L. + 2022, Matthias Clasen + 2018, 2021, Khaled Hosny + 2018-2020, Adobe, Inc. + 2013-2015, Alexei Podtelezhnikov +License: HarfBuzz + +Files: thirdparty/icu4c/* +Comment: International Components for Unicode +Copyright: 2016-2024, Unicode, Inc. +License: Unicode + +Files: thirdparty/jolt_physics/* +Comment: Jolt Physics +Copyright: 2021, Jorrit Rouwe +License: Expat + +Files: thirdparty/jpeg-compressor/* +Comment: jpeg-compressor +Copyright: 2012, Rich Geldreich +License: public-domain or Apache-2.0 + +Files: thirdparty/libbacktrace/* +Comment: libbacktrace +Copyright: 2012-2021, Free Software Foundation, Inc. +License: BSD-3-clause + +Files: thirdparty/libktx/* +Comment: KTX +Copyright: 2013-2020, Mark Callow + 2010-2020 The Khronos Group, Inc. +License: Apache-2.0 + +Files: thirdparty/libogg/* +Comment: OggVorbis +Copyright: 2002, Xiph.org Foundation +License: BSD-3-clause + +Files: thirdparty/libpng/* +Comment: libpng +Copyright: 1995-2025, The PNG Reference Library Authors. + 2018-2025, Cosmin Truta. + 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + 1996-1997, Andreas Dilger. + 1995-1996, Guy Eric Schalnat, Group 42, Inc. +License: Zlib + +Files: thirdparty/libtheora/* +Comment: OggTheora +Copyright: 2002-2009, Xiph.org Foundation +License: BSD-3-clause + +Files: thirdparty/libvorbis/* +Comment: OggVorbis +Copyright: 2002-2015, Xiph.org Foundation +License: BSD-3-clause + +Files: thirdparty/libwebp/* +Comment: WebP codec +Copyright: 2010, Google Inc. +License: BSD-3-clause + +Files: thirdparty/manifold/* +Comment: Manifold +Copyright: 2020-2024, The Manifold Authors +License: Apache-2.0 + +Files: thirdparty/mbedtls/* +Comment: Mbed TLS +Copyright: The Mbed TLS Contributors +License: Apache-2.0 + +Files: thirdparty/meshoptimizer/* +Comment: meshoptimizer +Copyright: 2016-2024, Arseny Kapoulkine +License: Expat + +Files: thirdparty/mingw-std-threads/* +Comment: mingw-std-threads +Copyright: 2016, Mega Limited +License: BSD-2-clause + +Files: thirdparty/minimp3/* +Comment: MiniMP3 +Copyright: lieff +License: CC0-1.0 + +Files: thirdparty/miniupnpc/* +Comment: MiniUPnP Project +Copyright: 2005-2024, Thomas Bernard +License: BSD-3-clause + +Files: thirdparty/minizip/* +Comment: MiniZip +Copyright: 1998-2010, Gilles Vollant + 2007-2008, Even Rouault + 2009-2010, Mathias Svensson +License: Zlib + +Files: thirdparty/misc/bcdec.h +Comment: bcdec +Copyright: 2022, Sergii Kudlai +License: Expat + +Files: thirdparty/misc/cubemap_coeffs.h +Comment: Fast Filtering of Reflection Probes +Copyright: 2016, Activision Publishing, Inc. +License: Expat + +Files: thirdparty/misc/fastlz.c + thirdparty/misc/fastlz.h +Comment: FastLZ +Copyright: 2005-2020, Ariya Hidayat +License: Expat + +Files: thirdparty/misc/FastNoiseLite.h +Comment: FastNoise Lite +Copyright: 2023, Jordan Peck and contributors +License: Expat + +Files: thirdparty/misc/ifaddrs-android.cc + thirdparty/misc/ifaddrs-android.h +Comment: libjingle +Copyright: 2012-2013, Google Inc. +License: BSD-3-clause + +Files: thirdparty/misc/mikktspace.c + thirdparty/misc/mikktspace.h +Comment: Tangent Space Normal Maps implementation +Copyright: 2011, Morten S. Mikkelsen +License: Zlib + +Files: thirdparty/misc/nvapi_minimal.h +Comment: NVIDIA NVAPI (minimal excerpt) +Copyright: 2019-2022, NVIDIA Corporation +License: Expat + +Files: thirdparty/misc/ok_color.h + thirdparty/misc/ok_color_shader.h +Comment: OK Lab color space +Copyright: 2021, Björn Ottosson +License: Expat + +Files: thirdparty/misc/pcg.cpp + thirdparty/misc/pcg.h +Comment: Minimal PCG32 implementation +Copyright: 2014, M.E. O'Neill +License: Apache-2.0 + +Files: thirdparty/misc/polypartition.cpp + thirdparty/misc/polypartition.h +Comment: PolyPartition / Triangulator +Copyright: 2011-2021, Ivan Fratric and contributors +License: Expat + +Files: thirdparty/misc/qoa.h +Comment: Quite OK Audio Format +Copyright: 2023, Dominic Szablewski +License: Expat + +Files: thirdparty/misc/r128.c + thirdparty/misc/r128.h +Comment: r128 library +Copyright: Alan Hickman +License: public-domain or Unlicense + +Files: thirdparty/misc/smaz.c + thirdparty/misc/smaz.h +Comment: SMAZ +Copyright: 2006-2009, Salvatore Sanfilippo +License: BSD-3-clause + +Files: thirdparty/misc/smolv.cpp + thirdparty/misc/smolv.h +Comment: SMOL-V +Copyright: 2016-2024, Aras Pranckevicius +License: public-domain or Unlicense or Expat + +Files: thirdparty/misc/stb_rect_pack.h +Comment: stb libraries +Copyright: Sean Barrett +License: public-domain or Unlicense or Expat + +Files: thirdparty/misc/yuv2rgb.h +Comment: YUV2RGB +Copyright: 2008-2011, Robin Watts +License: BSD-2-clause + +Files: thirdparty/msdfgen/* +Comment: Multi-channel signed distance field generator +Copyright: 2014-2024, Viktor Chlumsky +License: Expat + +Files: thirdparty/openxr/* +Comment: OpenXR Loader +Copyright: 2020-2023, The Khronos Group Inc. +License: Apache-2.0 + +Files: thirdparty/pcre2/* +Comment: PCRE2 +Copyright: 1997-2024, University of Cambridge + 2009-2024, Zoltan Herczeg +License: BSD-3-clause + +Files: thirdparty/recastnavigation/* +Comment: Recast +Copyright: 2009, Mikko Mononen +License: Zlib + +Files: thirdparty/rvo2/* +Comment: RVO2 +Copyright: 2016, University of North Carolina at Chapel Hill +License: Apache-2.0 + +Files: thirdparty/spirv-cross/* +Comment: SPIRV-Cross +Copyright: 2015-2021, Arm Limited +License: Apache-2.0 or Expat + +Files: thirdparty/spirv-reflect/* +Comment: SPIRV-Reflect +Copyright: 2017-2022, Google Inc. +License: Apache-2.0 + +Files: thirdparty/thorvg/* +Comment: ThorVG +Copyright: 2020-2024, The ThorVG Project +License: Expat + +Files: thirdparty/tinyexr/* +Comment: TinyEXR +Copyright: 2014-2021, Syoyo Fujita + 2002, Industrial Light & Magic, a division of Lucas Digital Ltd. LLC +License: BSD-3-clause + +Files: thirdparty/ufbx/* +Comment: ufbx +Copyright: 2020, Samuli Raivio +License: Expat + +Files: thirdparty/vhacd/* +Comment: V-HACD +Copyright: 2011, Khaled Mamou + 2003-2009, Erwin Coumans +License: BSD-3-clause + +Files: thirdparty/volk/* +Comment: volk +Copyright: 2018-2024, Arseny Kapoulkine +License: Expat + +Files: thirdparty/vulkan/* +Comment: Vulkan Headers +Copyright: 2014-2024, The Khronos Group Inc. + 2014-2024, Valve Corporation + 2014-2024, LunarG, Inc. +License: Apache-2.0 + +Files: thirdparty/vulkan/vk_mem_alloc.h +Comment: Vulkan Memory Allocator +Copyright: 2017-2024, Advanced Micro Devices, Inc. +License: Expat + +Files: thirdparty/wayland/* +Comment: Wayland core protocol +Copyright: 2008-2012, Kristian Høgsberg + 2010-2012, Intel Corporation + 2011, Benjamin Franzke + 2012, Collabora, Ltd. +License: Expat + +Files: thirdparty/wayland-protocols/* +Comment: Wayland protocols that add functionality not available in the core protocol +Copyright: 2008-2013, Kristian Høgsberg + 2010-2013, Intel Corporation + 2013, Rafael Antognolli + 2013, Jasper St. Pierre + 2014, Jonas Ådahl + 2014, Jason Ekstrand + 2014-2015, Collabora, Ltd. + 2015, Red Hat Inc. +License: Expat + +Files: thirdparty/wslay/* +Comment: Wslay +Copyright: 2011, 2012, 2015, Tatsuhiro Tsujikawa +License: Expat + +Files: thirdparty/xatlas/* +Comment: xatlas +Copyright: 2018-2020, Jonathan Young + 2013, Thekla, Inc + 2006, NVIDIA Corporation, Ignacio Castano +License: Expat + +Files: thirdparty/zlib/* +Comment: zlib +Copyright: 1995-2024, Jean-loup Gailly and Mark Adler +License: Zlib + +Files: thirdparty/zstd/* +Comment: Zstandard +Copyright: Meta Platforms, Inc. and affiliates. +License: BSD-3-clause + + + +License: Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + . + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + . + 1. Definitions. + . + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + . + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + . + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + . + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + . + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + . + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + . + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + . + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + . + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + . + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + . + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + . + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + . + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + . + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + . + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + . + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + . + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + . + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + . + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + . + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + . + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + . + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + . + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + . + END OF TERMS AND CONDITIONS + . + APPENDIX: How to apply the Apache License to your work. + . + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + . + Copyright [yyyy] [name of copyright owner] + . + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License: BSD-2-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + . + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: BSD-3-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + . + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + . + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +License: BSL-1.0 + Boost Software License - Version 1.0 - August 17th, 2003 + . + Permission is hereby granted, free of charge, to any person or organization + obtaining a copy of the software and accompanying documentation covered by + this license (the "Software") to use, reproduce, display, distribute, + execute, and transmit the Software, and to prepare derivative works of the + Software, and to permit third-parties to whom the Software is furnished to + do so, all subject to the following: + . + The copyright notices in the Software and this entire statement, including + the above license grant, this restriction and the following disclaimer, + must be included in all copies of the Software, in whole or in part, and + all derivative works of the Software, unless such copies or derivative + works are solely in the form of machine-executable object code generated by + a source language processor. + . + 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT + SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +License: CC0-1.0 + CC0 1.0 Universal + . + Statement of Purpose + . + The laws of most jurisdictions throughout the world automatically confer + exclusive Copyright and Related Rights (defined below) upon the creator and + subsequent owner(s) (each and all, an "owner") of an original work of + authorship and/or a database (each, a "Work"). + . + Certain owners wish to permanently relinquish those rights to a Work for the + purpose of contributing to a commons of creative, cultural and scientific + works ("Commons") that the public can reliably and without fear of later + claims of infringement build upon, modify, incorporate in other works, reuse + and redistribute as freely as possible in any form whatsoever and for any + purposes, including without limitation commercial purposes. These owners may + contribute to the Commons to promote the ideal of a free culture and the + further production of creative, cultural and scientific works, or to gain + reputation or greater distribution for their Work in part through the use and + efforts of others. + . + For these and/or other purposes and motivations, and without any expectation + of additional consideration or compensation, the person associating CC0 with a + Work (the "Affirmer"), to the extent that he or she is an owner of Copyright + and Related Rights in the Work, voluntarily elects to apply CC0 to the Work + and publicly distribute the Work under its terms, with knowledge of his or her + Copyright and Related Rights in the Work and the meaning and intended legal + effect of CC0 on those rights. + . + 1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights ("Copyright and + Related Rights"). Copyright and Related Rights include, but are not limited + to, the following: + . + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + . + ii. moral rights retained by the original author(s) and/or performer(s); + . + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + . + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + . + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + . + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + . + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + . + 2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer's Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes + the Waiver for the benefit of each member of the public at large and to the + detriment of Affirmer's heirs and successors, fully intending that such Waiver + shall not be subject to revocation, rescission, cancellation, termination, or + any other legal or equitable action to disrupt the quiet enjoyment of the Work + by the public as contemplated by Affirmer's express Statement of Purpose. + . + 3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer's express Statement of Purpose. In addition, to the extent the Waiver + is so judged Affirmer hereby grants to each affected person a royalty-free, + non transferable, non sublicensable, non exclusive, irrevocable and + unconditional license to exercise Affirmer's Copyright and Related Rights in + the Work (i) in all territories worldwide, (ii) for the maximum duration + provided by applicable law or treaty (including future time extensions), (iii) + in any current or future medium and for any number of copies, and (iv) for any + purpose whatsoever, including without limitation commercial, advertising or + promotional purposes (the "License"). The License shall be deemed effective as + of the date CC0 was applied by Affirmer to the Work. Should any part of the + License for any reason be judged legally invalid or ineffective under + applicable law, such partial invalidity or ineffectiveness shall not + invalidate the remainder of the License, and in such case Affirmer hereby + affirms that he or she will not (i) exercise any of his or her remaining + Copyright and Related Rights in the Work or (ii) assert any associated claims + and causes of action with respect to the Work, in either case contrary to + Affirmer's express Statement of Purpose. + . + 4. Limitations and Disclaimers. + . + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + . + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + . + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + . + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +License: CC-BY-4.0 + Creative Commons Attribution 4.0 International Public License + . + By exercising the Licensed Rights (defined below), You accept and agree + to be bound by the terms and conditions of this Creative Commons + Attribution 4.0 International Public License ("Public + License"). To the extent this Public License may be interpreted as a + contract, You are granted the Licensed Rights in consideration of Your + acceptance of these terms and conditions, and the Licensor grants You + such rights in consideration of benefits the Licensor receives from + making the Licensed Material available under these terms and + conditions. + . + Section 1 -- Definitions. + . + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + . + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + . + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + . + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + . + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + . + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + . + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + . + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + . + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + . + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + . + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + . + Section 2 -- Scope. + . + a. License grant. + . + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + . + a. reproduce and Share the Licensed Material, in whole or + in part; and + . + b. produce, reproduce, and Share Adapted Material. + . + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + . + 3. Term. The term of this Public License is specified in Section + 6(a). + . + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + . + 5. Downstream recipients. + . + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + . + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + . + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + . + b. Other rights. + . + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + . + 2. Patent and trademark rights are not licensed under this + Public License. + . + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + . + Section 3 -- License Conditions. + . + Your exercise of the Licensed Rights is expressly made subject to the + following conditions. + . + a. Attribution. + . + 1. If You Share the Licensed Material (including in modified + form), You must: + . + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + . + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + . + ii. a copyright notice; + . + iii. a notice that refers to this Public License; + . + iv. a notice that refers to the disclaimer of + warranties; + . + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + . + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + . + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + . + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + . + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + . + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + . + Section 4 -- Sui Generis Database Rights. + . + Where the Licensed Rights include Sui Generis Database Rights that + apply to Your use of the Licensed Material: + . + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + . + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + . + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + . + For the avoidance of doubt, this Section 4 supplements and does not + replace Your obligations under this Public License where the Licensed + Rights include other Copyright and Similar Rights. + . + Section 5 -- Disclaimer of Warranties and Limitation of Liability. + . + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + . + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + . + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + . + Section 6 -- Term and Termination. + . + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + . + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + . + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + . + 2. upon express reinstatement by the Licensor. + . + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + . + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + . + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + . + Section 7 -- Other Terms and Conditions. + . + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + . + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + . + Section 8 -- Interpretation. + . + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + . + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + . + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + . + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +License: Expat + 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. + +License: glslang + Here, glslang proper means core GLSL parsing, HLSL parsing, and SPIR-V code + generation. Glslang proper requires use of a number of licenses, one that covers + preprocessing and others that covers non-preprocessing. + . + Bison was removed long ago. You can build glslang from the source grammar, + using tools of your choice, without using bison or any bison files. + . + Other parts, outside of glslang proper, include: + . + - gl_types.h, only needed for OpenGL-like reflection, and can be left out of + a parse and codegen project. See it for its license. + . + - update_glslang_sources.py, which is not part of the project proper and does + not need to be used. + . + - the SPIR-V "remapper", which is optional, but has the same license as + glslang proper + . + - Google tests and SPIR-V tools, and anything in the external subdirectory + are external and optional; see them for their respective licenses. + . + -------------------------------------------------------------------------------- + . + The core of glslang-proper, minus the preprocessor is licenced as follows: + . + -------------------------------------------------------------------------------- + 3-Clause BSD License + -------------------------------------------------------------------------------- + . + Copyright (C) 2015-2018 Google, Inc. + Copyright (C) + . + All rights reserved. + . + See: . + . + -------------------------------------------------------------------------------- + 2-Clause BSD License + -------------------------------------------------------------------------------- + . + Copyright 2020 The Khronos Group Inc + . + See: . + . + -------------------------------------------------------------------------------- + The MIT License + -------------------------------------------------------------------------------- + . + Copyright 2020 The Khronos Group Inc + . + See: . + . + -------------------------------------------------------------------------------- + APACHE LICENSE, VERSION 2.0 + -------------------------------------------------------------------------------- + . + See: . + . + -------------------------------------------------------------------------------- + GPL 3 with special bison exception + -------------------------------------------------------------------------------- + . + Bison implementation for Yacc-like parsers in C + . + Copyright (C) 1984, 1989-1990, 2000-2015 Free Software Foundation, Inc. + . + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + As a special exception, you may create a larger work that contains + part or all of the Bison parser skeleton and distribute that work + under terms of your choice, so long as that work isn't itself a + parser generator using the skeleton or a modified version thereof + as a parser skeleton. Alternatively, if you modify or redistribute + the parser skeleton itself, you may (at your option) remove this + special exception, which will cause the skeleton and the resulting + Bison output files to be licensed under the GNU General Public + License without this special exception. + . + This special exception was added by the Free Software Foundation in + version 2.2 of Bison. + . + -------------------------------------------------------------------------------- + ================================================================================ + -------------------------------------------------------------------------------- + . + The preprocessor has the core licenses stated above, plus an additional licence: + . + Copyright (c) 2002, NVIDIA Corporation. + . + NVIDIA Corporation("NVIDIA") supplies this software to you in + consideration of your agreement to the following terms, and your use, + installation, modification or redistribution of this NVIDIA software + constitutes acceptance of these terms. If you do not agree with these + terms, please do not use, install, modify or redistribute this NVIDIA + software. + . + In consideration of your agreement to abide by the following terms, and + subject to these terms, NVIDIA grants you a personal, non-exclusive + license, under NVIDIA's copyrights in this original NVIDIA software (the + "NVIDIA Software"), to use, reproduce, modify and redistribute the + NVIDIA Software, with or without modifications, in source and/or binary + forms; provided that if you redistribute the NVIDIA Software, you must + retain the copyright notice of NVIDIA, this notice and the following + text and disclaimers in all such redistributions of the NVIDIA Software. + Neither the name, trademarks, service marks nor logos of NVIDIA + Corporation may be used to endorse or promote products derived from the + NVIDIA Software without specific prior written permission from NVIDIA. + Except as expressly stated in this notice, no other rights or licenses + express or implied, are granted by NVIDIA herein, including but not + limited to any patent rights that may be infringed by your derivative + works or by other works in which the NVIDIA Software may be + incorporated. No hardware is licensed hereunder. + . + THE NVIDIA SOFTWARE IS BEING PROVIDED ON AN "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, + INCLUDING WITHOUT LIMITATION, WARRANTIES OR CONDITIONS OF TITLE, + NON-INFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR + ITS USE AND OPERATION EITHER ALONE OR IN COMBINATION WITH OTHER + PRODUCTS. + . + IN NO EVENT SHALL NVIDIA BE LIABLE FOR ANY SPECIAL, INDIRECT, + INCIDENTAL, EXEMPLARY, CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, LOST PROFITS; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) OR ARISING IN ANY WAY + OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE + NVIDIA SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, + TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF + NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: FTL + The FreeType Project LICENSE + ---------------------------- + . + 2000-Feb-08 + . + Copyright 1996-2000 by + David Turner, Robert Wilhelm, and Werner Lemberg + . + . + . + Introduction + ============ + . + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + . + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + . + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + . + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + . + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + . + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + . + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + . + . + Legal Terms + =========== + . + 0. Definitions + -------------- + . + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + . + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + . + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + . + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + . + 1. No Warranty + -------------- + . + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + . + 2. Redistribution + ----------------- + . + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + . + o Redistribution of source code must retain this license file + (`LICENSE.TXT') unaltered; any additions, deletions or changes + to the original files must be clearly indicated in + accompanying documentation. The copyright notices of the + unaltered, original files must be preserved in all copies of + source files. + . + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + . + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + . + 3. Advertising + -------------- + . + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + . + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + . + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + . + 4. Contacts + ----------- + . + There are two mailing lists related to FreeType: + . + o freetype@freetype.org + . + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + . + o devel@freetype.org + . + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + . + o http://www.freetype.org + . + Holds the current FreeType web page, which will allow you to + download our latest development version and read online + documentation. + . + You can also contact us individually at: + . + David Turner + Robert Wilhelm + Werner Lemberg + +License: HarfBuzz + HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. + For parts of HarfBuzz that are licensed under different licenses see individual + files names COPYING in subdirectories where applicable. + . + Copyright (C) 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Google, Inc. + Copyright (C) 2018,2019,2020 Ebrahim Byagowi + Copyright (C) 2019,2020 Facebook, Inc. + Copyright (C) 2012 Mozilla Foundation + Copyright (C) 2011 Codethink Limited + Copyright (C) 2008,2010 Nokia Corporation and/or its subsidiary(-ies) + Copyright (C) 2009 Keith Stribley + Copyright (C) 2009 Martin Hosken and SIL International + Copyright (C) 2007 Chris Wilson + Copyright (C) 2005,2006,2020,2021 Behdad Esfahbod + Copyright (C) 2005 David Turner + Copyright (C) 2004,2007,2008,2009,2010 Red Hat, Inc. + Copyright (C) 1998-2004 David Turner and Werner Lemberg + . + For full copyright notices consult the individual files in the package. + . + . + Permission is hereby granted, without written agreement and without + license or royalty fees, to use, copy, modify, and distribute this + software and its documentation for any purpose, provided that the + above copyright notice and the following two paragraphs appear in + all copies of this software. + . + IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR + DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN + IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + . + THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, + BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO + PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +License: MPL-2.0 + Mozilla Public License Version 2.0 + ================================== + . + 1. Definitions + -------------- + . + 1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + . + 1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + . + 1.3. "Contribution" + means Covered Software of a particular Contributor. + . + 1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + . + 1.5. "Incompatible With Secondary Licenses" + means + . + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + . + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + . + 1.6. "Executable Form" + means any form of the work other than Source Code Form. + . + 1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + . + 1.8. "License" + means this document. + . + 1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + . + 1.10. "Modifications" + means any of the following: + . + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + . + (b) any new file in Source Code Form that contains any Covered + Software. + . + 1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + . + 1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + . + 1.13. "Source Code Form" + means the form of the work preferred for making modifications. + . + 1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + . + 2. License Grants and Conditions + -------------------------------- + . + 2.1. Grants + . + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + . + (a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + . + (b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + . + 2.2. Effective Date + . + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + . + 2.3. Limitations on Grant Scope + . + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + . + (a) for any code that a Contributor has removed from Covered Software; + or + . + (b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + . + (c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + . + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + . + 2.4. Subsequent Licenses + . + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + . + 2.5. Representation + . + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights + to grant the rights to its Contributions conveyed by this License. + . + 2.6. Fair Use + . + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + . + 2.7. Conditions + . + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted + in Section 2.1. + . + 3. Responsibilities + ------------------- + . + 3.1. Distribution of Source Form + . + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + . + 3.2. Distribution of Executable Form + . + If You distribute Covered Software in Executable Form then: + . + (a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + . + (b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + . + 3.3. Distribution of a Larger Work + . + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + . + 3.4. Notices + . + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, + or limitations of liability) contained within the Source Code Form of + the Covered Software, except that You may alter any license notices to + the extent required to remedy known factual inaccuracies. + . + 3.5. Application of Additional Terms + . + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + . + 4. Inability to Comply Due to Statute or Regulation + --------------------------------------------------- + . + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Software due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description must + be placed in a text file included with all distributions of the Covered + Software under this License. Except to the extent prohibited by statute + or regulation, such description must be sufficiently detailed for a + recipient of ordinary skill to be able to understand it. + . + 5. Termination + -------------- + . + 5.1. The rights granted under this License will terminate automatically + if You fail to comply with any of its terms. However, if You become + compliant, then the rights granted under this License from a particular + Contributor are reinstated (a) provisionally, unless and until such + Contributor explicitly and finally terminates Your grants, and (b) on an + ongoing basis, if such Contributor fails to notify You of the + non-compliance by some reasonable means prior to 60 days after You have + come back into compliance. Moreover, Your grants from a particular + Contributor are reinstated on an ongoing basis if such Contributor + notifies You of the non-compliance by some reasonable means, this is the + first time You have received notice of non-compliance with this License + from such Contributor, and You become compliant prior to 30 days after + Your receipt of the notice. + . + 5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + . + 5.3. In the event of termination under Sections 5.1 or 5.2 above, all + end user license agreements (excluding distributors and resellers) which + have been validly granted by You or Your distributors under this License + prior to termination shall survive termination. + . + ************************************************************************ + * * + * 6. Disclaimer of Warranty * + * ------------------------- * + * * + * Covered Software is provided under this License on an "as is" * + * basis, without warranty of any kind, either expressed, implied, or * + * statutory, including, without limitation, warranties that the * + * Covered Software is free of defects, merchantable, fit for a * + * particular purpose or non-infringing. The entire risk as to the * + * quality and performance of the Covered Software is with You. * + * Should any Covered Software prove defective in any respect, You * + * (not any Contributor) assume the cost of any necessary servicing, * + * repair, or correction. This disclaimer of warranty constitutes an * + * essential part of this License. No use of any Covered Software is * + * authorized under this License except under this disclaimer. * + * * + ************************************************************************ + . + ************************************************************************ + * * + * 7. Limitation of Liability * + * -------------------------- * + * * + * Under no circumstances and under no legal theory, whether tort * + * (including negligence), contract, or otherwise, shall any * + * Contributor, or anyone who distributes Covered Software as * + * permitted above, be liable to You for any direct, indirect, * + * special, incidental, or consequential damages of any character * + * including, without limitation, damages for lost profits, loss of * + * goodwill, work stoppage, computer failure or malfunction, or any * + * and all other commercial damages or losses, even if such party * + * shall have been informed of the possibility of such damages. This * + * limitation of liability shall not apply to liability for death or * + * personal injury resulting from such party's negligence to the * + * extent applicable law prohibits such limitation. Some * + * jurisdictions do not allow the exclusion or limitation of * + * incidental or consequential damages, so this exclusion and * + * limitation may not apply to You. * + * * + ************************************************************************ + . + 8. Litigation + ------------- + . + Any litigation relating to this License may be brought only in the + courts of a jurisdiction where the defendant maintains its principal + place of business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. + Nothing in this Section shall prevent a party's ability to bring + cross-claims or counter-claims. + . + 9. Miscellaneous + ---------------- + . + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides + that the language of a contract shall be construed against the drafter + shall not be used to construe this License against a Contributor. + . + 10. Versions of the License + --------------------------- + . + 10.1. New Versions + . + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + . + 10.2. Effect of New Versions + . + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + . + 10.3. Modified Versions + . + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + . + 10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses + . + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + . + Exhibit A - Source Code Form License Notice + ------------------------------------------- + . + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + . + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to look + for such a notice. + . + You may add additional accurate notices of copyright ownership. + . + Exhibit B - "Incompatible With Secondary Licenses" Notice + --------------------------------------------------------- + . + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +License: OFL-1.1 + PREAMBLE + The goals of the Open Font License (OFL) are to stimulate worldwide + development of collaborative font projects, to support the font creation + efforts of academic and linguistic communities, and to provide a free and + open framework in which fonts may be shared and improved in partnership + with others. + . + The OFL allows the licensed fonts to be used, studied, modified and + redistributed freely as long as they are not sold by themselves. The + fonts, including any derivative works, can be bundled, embedded, + redistributed and/or sold with any software provided that any reserved + names are not used by derivative works. The fonts and derivatives, + however, cannot be released under any other type of license. The + requirement for fonts to remain under this license does not apply + to any document created using the fonts or their derivatives. + . + DEFINITIONS + "Font Software" refers to the set of files released by the Copyright + Holder(s) under this license and clearly marked as such. This may + include source files, build scripts and documentation. + . + "Reserved Font Name" refers to any names specified as such after the + copyright statement(s). + . + "Original Version" refers to the collection of Font Software components as + distributed by the Copyright Holder(s). + . + "Modified Version" refers to any derivative made by adding to, deleting, + or substituting -- in part or in whole -- any of the components of the + Original Version, by changing formats or by porting the Font Software to a + new environment. + . + "Author" refers to any designer, engineer, programmer, technical + writer or other person who contributed to the Font Software. + . + PERMISSION & CONDITIONS + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Font Software, to use, study, copy, merge, embed, modify, + redistribute, and sell modified and unmodified copies of the Font + Software, subject to the following conditions: + . + 1) Neither the Font Software nor any of its individual components, + in Original or Modified Versions, may be sold by itself. + . + 2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + . + 3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the corresponding + Copyright Holder. This restriction only applies to the primary font name as + presented to the users. + . + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + . + 5) The Font Software, modified or unmodified, in part or in whole, + must be distributed entirely under this license, and must not be + distributed under any other license. The requirement for fonts to + remain under this license does not apply to any document created + using the Font Software. + . + TERMINATION + This license becomes null and void if any of the above conditions are + not met. + . + DISCLAIMER + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE. + +License: Unicode + COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later) + . + Copyright (C) 1991-2020 Unicode, Inc. All rights reserved. + Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + . + Permission is hereby granted, free of charge, to any person obtaining + a copy of the Unicode data files and any associated documentation + (the "Data Files") or Unicode software and any associated documentation + (the "Software") to deal in the Data Files or Software + without restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, and/or sell copies of + the Data Files or Software, and to permit persons to whom the Data Files + or Software are furnished to do so, provided that either + (a) this copyright and permission notice appear with all copies + of the Data Files or Software, or + (b) this copyright and permission notice appear in associated + Documentation. + . + THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS + NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL + DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, + DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THE DATA FILES OR SOFTWARE. + . + Except as contained in this notice, the name of a copyright holder + shall not be used in advertising or otherwise to promote the sale, + use or other dealings in these Data Files or Software without prior + written authorization of the copyright holder. + +License: Unlicense + This is free and unencumbered software released into the public domain. + . + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + . + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + . + 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 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. + . + For more information, please refer to + +License: Zlib + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + . + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + . + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. diff --git a/plugins/cristinamadureira353-svg/ai-office/LICENSE b/plugins/cristinamadureira353-svg/ai-office/LICENSE new file mode 100644 index 00000000..77c5de4c --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 AI Office contributors + +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. diff --git a/plugins/cristinamadureira353-svg/ai-office/LICENSE-GODOT.txt b/plugins/cristinamadureira353-svg/ai-office/LICENSE-GODOT.txt new file mode 100644 index 00000000..0e3ba08d --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/LICENSE-GODOT.txt @@ -0,0 +1,20 @@ +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. diff --git a/plugins/cristinamadureira353-svg/ai-office/LICENSE-NOTO.txt b/plugins/cristinamadureira353-svg/ai-office/LICENSE-NOTO.txt new file mode 100644 index 00000000..362b0a4c --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/LICENSE-NOTO.txt @@ -0,0 +1,95 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/). +Noto is a trademark of Google Inc. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/cristinamadureira353-svg/ai-office/README.md b/plugins/cristinamadureira353-svg/ai-office/README.md new file mode 100644 index 00000000..28cfab63 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/README.md @@ -0,0 +1,67 @@ +# AI Office MiniApp + +[简体中文](README.zh-CN.md) + +A pixel office management game featuring five model-powered employees: M3, Flash, LongCat, Kimi and Step5. Manage client tasks, review deliveries, chat with employees, organize breaks and meetings, and balance money, stamina and mood. + +![AI Office with synthetic client and employee state](docs/preview.png) + +The interface is in Chinese. The preview uses a simulated game session, with no real customer conversations or credentials. Author: [最爱双皮奶](https://github.com/cristinamadureira353-svg). + +## Install and open + +This directory is the complete built plugin. Quit MiniMax Code and copy the entire `ai-office` directory, including hidden `.minimax-plugin/`, into your MiniMax data directory's `plugins/` folder (default: `~/.minimax/plugins/ai-office/`). Copy only the plugin directory; the author directory is for repository organization. Keep any old-plugin backup outside `plugins/`. Reopen the host and ask the Agent to open “AI 事务所”. + +The first-run page provides a generic setup prompt. Send it to the Agent for step-by-step provider selection, key acquisition, Python setup and configuration. At least one employee provider is required. Jev is optional: reuse an existing OpenRouter key, obtain one through its official website, or skip it and keep local behavior rules. Later launches reuse the saved private configuration. + +Python 3.9+ with Flask, requests and python-dotenv is required. The Agent can prepare an environment from `miniapp/node/requirements.txt`; no game source checkout or Godot installation is required for players. + +## Keys and quota + +`MINIMAX_API_KEY` serves M3 and Flash. Other employees use `LONGCAT_API_KEY`, `KIMI_API_KEY` (Kimi Code), and `STEPFUN_API_KEY` (Step Plan). Jev uses a separate `OPENROUTER_API_KEY`. Provider model names and access depend on current account permissions and availability. + +Quota exhaustion, invalid credentials or unavailable models freeze the corresponding employee in a “brain offline” state. The employee stops work, movement and further model calls. After repairing the account or configuration, select the employee and use the manual reboot control for one short check. Temporary rate limits and network failures do not permanently freeze the employee. + +## Data & access + +- Keys are read only by the local Python backend, never sent to the game frontend or included in the package. Configuration APIs accept file paths and do not return keys. +- Settings and subsequent saves are stored in the Host-provided private `context.dataDir`. A player-selected existing JSON save may be read once for initial import. Original source saves are not overwritten. +- Node listens on the Host-provided loopback address and port, validates Host/Origin, serves fixed asset/API routes, and starts one configured Python process through a private JSON-lines protocol. It does not use the desktop development API port. +- Gameplay sends task and dialogue context to `api.minimaxi.com`, `api.longcat.chat`, `api.kimi.com`, and `api.stepfun.com` according to configured employee keys. Jev requests additionally use `openrouter.ai`. These requests consume the player's own provider quota. +- Reading first-run guidance does not launch model requests. Setup checks only local Python dependencies. The MiniApp does not read the host's login credentials, buy credits, or poll account balances. +- Closing the MiniApp saves private progress and stops its own Python child and request streams. No host restart or foreground game launch is automated. + +The spawned commands use argument arrays, without a shell: + +```text + -c "import flask, requests, dotenv" + -u /miniapp/node/backend/server/miniapp_bridge.py --data-dir --env-file [--initial-save ] +``` + +The first command is a local dependency check with an eight-second timeout. The second is the game backend. Arguments contain file paths, not key values. The plugin only reads the selected external environment/save files; new settings and progress stay under `context.dataDir`. + +## Tested environment + +Version **0.5.10**. Recorded development and host environment: macOS 26.4, MiniMax Code 3.0.73, Node.js 22.23.2, Python 3.9.6 and Godot 4.4.1 single-threaded Web export. + +**Manual play with real model providers.** The project author installed, opened and played the game in the real MiniMax Code desktop host using real provider-issued API keys, live model API calls and actual provider responses. The author confirmed this playtesting on 2026-10-17. Existing host screenshots and gameplay feedback record game entry and interaction/layout issues involving employee controls, chat access, seating, conflict choices and HUD visibility. + +**Additional automated regression.** For the submitted 0.5.10 package, a separate run with temporary Host-context fixtures and headless Chromium passed 34/34 interaction checks, including first-run configuration, entering the game, reopening and employee chat. This automated suite substitutes provider replies to exercise repeatable success and failure cases; the author's manual playtesting above used live providers. See [validation details](docs/VALIDATION.md) for both test records, commands and results. + +Windows/Linux host play, Chinese IME interaction and long-term balance remain outside the recorded test coverage. + +## Source and build + +[Game source and build tools at v0.5.10](https://github.com/cristinamadureira353-svg/ai-office/tree/v0.5.10) are MIT-licensed. This package includes the ready-to-run Web export, Python backend and their runtime resources. Players need no source checkout or Godot installation. + +To rebuild, clone that source tag, prepare Python 3.9+ from `ai-model-party/requirements.txt`, install Godot 4.4.1 and its matching Web export templates, then run from the source root: + +```sh +python ai-model-party/tools/build_miniapp.py --output build/miniapp +``` + +The output directory must be new. Copy the resulting `ai-office` directory as a complete unit. The source repository documents the optional `--godot` argument and the full development checks. + +## License and credits + +MIT applies to project code and assets; the bundled font and engine retain their own licenses. Preserve LICENSE, LICENSE-NOTO.txt, LICENSE-GODOT.txt, COPYRIGHT-GODOT.txt and THIRD_PARTY_NOTICES.md in complete packages. This game is independently developed and does not imply provider endorsement. diff --git a/plugins/cristinamadureira353-svg/ai-office/README.zh-CN.md b/plugins/cristinamadureira353-svg/ai-office/README.zh-CN.md new file mode 100644 index 00000000..1a3c3312 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/README.zh-CN.md @@ -0,0 +1,72 @@ +# AI 事务所 MiniApp + +[English](README.md)。本目录是可以独立复制安装的完整 MiniApp,版本 0.5.10。作者:[最爱双皮奶](https://github.com/cristinamadureira353-svg)。 + +![使用模拟客户和员工状态的 AI 事务所画面](docs/preview.png) + +截图来自隔离模拟局面,不包含真实客户对话或密钥。完整游戏源码与构建工具见 [v0.5.10 源码仓库](https://github.com/cristinamadureira353-svg/ai-office/tree/v0.5.10)。 + +五位真实模型员工的像素经营游戏。额度或余额耗尽、密钥失效或模型停用时,对应员工显示“脑袋宕机了”,冻结动作、停止接单和后续调用。尚未完成的客户回到等待队列。共享同一 MiniMax 密钥的 M3 和 Flash 在账户额度耗尽时一起暂停。普通限流、短暂断网不作为永久宕机。 + +充值、额度重置或修好 API 配置后,选中角色,点“额度恢复后 · 重启脑袋”。每次点击只发起一次短检测,不在后台轮询额度。 + +底部五张员工卡都提供“聊天”按钮,直接打开对应员工的对话窗口并聚焦输入框;可回车发送或点击“发送”。每名员工分别保留和显示自己的对话记录。点卡片其他位置选中员工,在右侧安排休息、回工位、开会或轮休。选择和指令有明确反馈;灰色派单按钮可点击查看不可派单的原因。客户详情的透明区域不会遮挡员工卡或场景点击。主界面采用更适合 MiniApp 窗口的 1120×720 布局,正文 16、辅助文字 14、标题 18,保持框内留白和滚动正文。 + +休息区的寻路点和坐垫接触点分别配置:Flash、LongCat 坐在沙发的两个坐垫,Step5 坐在单人椅中间。角色到达对应座位才坐下,离座时恢复行走定位。 + +场景中的五位员工不再显示头顶模型名称。模型身份由底部身份卡的头像和 M3、Flash、LongCat、Kimi、Step5 名称标识;聊天和详情中也显示对应模型。脑袋宕机等状态提示继续显示。 + +抢单冲突弹窗的四个选择会发送对应裁决;点击后显示提交状态,等待结果期间避免重复提交。成功后弹窗随订单状态刷新收起;连接或裁决失败时在弹窗内显示原因,可重新选择。 + +关闭时保存未完成冲突的争抢者和对话,重新打开后可继续裁决。兼容旧版只保存订单编号的存档:能从员工任务找回争抢者时恢复冲突;找不回时将该单退回等待,保留资金和其他已完成的交付。进度尚未读取或状态接口失败时,顶部显示连接提示。 + +工作过程中关闭或读档,已经生成的交付物保留为“待验收”;尚未生成的订单退回等待派单,并解除员工任务锁定。读档时结束上次未完成的会议,员工回工位,可以重新召集;不会自动重放旧会议的模型对话。正在处理的订单不能重复派给其他员工。验收时“打回”会结束该单并扣除相应声誉,不会自动重做。 + +存档弹窗置于员工卡和会议提示之上,打开期间阻止底层点击。会议室特写将五个座位全部纳入顶栏下方的可见区域;会议状态小窗保持在员工栏上方,五张身份卡和聊天入口都可见。普通会议就座后显示“会议进行中”,由玩家安排散会。 + +走位采用混合逻辑:后端本地规则安排接待、回工位、会议、休息与随机闲逛;Jev 通过 OpenRouter 的可选调度接口选择目的地,主循环每三秒检查一次。具体路径、速度、朝向、坐姿和键盘动作由本地脚本执行。五名员工各自的模型 API 用于聊天、任务输出和游戏对话,不直接控制逐格移动;移动和动画本身不消耗模型调用。 + +## Install + +把完整的 `ai-office` 目录复制到 `~/.minimax/plugins/ai-office`,包含 `.minimax-plugin` 隐藏目录。MiniMax Code 正常重启后,让 Agent “打开 AI 事务所”。此包没有自动重启或前台抢焦点行为。 + +新玩家只需安装完整 MiniApp,不需要下载游戏源码或安装 Godot。第一次打开点“复制引导提示词”,发给 MiniMax Code 的 AI。AI 会先确认是否已有配置,再引导选择员工、申请或提供 API Key,并准备运行环境。没有 Key 时,按平台官方入口分步引导注册、登录和创建,等玩家完成再继续;至少配置一家员工模型即可,其他模型可以稍后添加。 + +通用引导正文在 [SETUP_PROMPT.md](SETUP_PROMPT.md),对所有玩家一致,不包含开发者用户名、工程路径、固定端口或操作系统假设。页面会在正文之后附上本次运行的配置接口、安装目录和私有数据目录,供 AI 执行配置;这些值来自 Host,适配不同机器和安装位置。配置期间保持 MiniApp 页面打开。仅从仓库复制正文时,AI 会先引导打开 MiniApp 获取完整运行信息。 + +M3 和 Flash 共用 `MINIMAX_API_KEY`,另外使用 `LONGCAT_API_KEY`、`KIMI_API_KEY`、`STEPFUN_API_KEY`。首次启动页面同时列出可选的 Jev / OpenRouter,自主去向、自动抢单及会议发言顺序等决策使用普通 OpenRouter API Key,保存为 `OPENROUTER_API_KEY`。AI 会先检查已有配置,已有 Key 时复用;没有时询问启用还是跳过,选择启用后逐步引导前往 [OpenRouter 密钥管理](https://openrouter.ai/settings/keys) 注册或登录、创建并保存 Key。Jev 调用使用 OpenRouter 账户额度;跳过后游戏沿用本地规则,不阻止员工模型配置和启动。即使已有员工配置,通用提示词也会检查 Jev,补充时保留其他 Key 和进度。 + +包内提供空白 `miniapp/node/env.example` 和 `miniapp/node/requirements.txt`,不依赖开发者的 `.env` 或 Python 虚拟环境。新配置和运行环境保存在 MiniApp 私有目录;玩家明确提供的已有配置可以复用。Python 3.9 或以上版本不足时,AI 引导从 Python 官网安装对应系统版本。API Key 与套餐权限以各平台当前官方说明为准,AI 不替玩家购买套餐。 + +AI 检查依赖和路径后,向提示词中的 `/api/settings` 接口提交 `pythonPath`、`envFile` 和可选的 `initialSave`。接口检查 Python 能导入 Flask、requests、python-dotenv,成功后才原子保存配置。页面每两秒检查本地配置状态,保存后自动进入游戏,之后打开直接进入。复制功能受宿主限制时会选中提示词,支持手动复制;“检查配置”可手动重试。读取提示词和检查配置不会启动游戏后台或调用付费模型。 + +已有旧配置继续有效。MiniApp 不自动读取 MiniMax Code 的登录凭据。密钥只写入本地配置文件,不回显在 AI 回复、终端日志、网页或配置接口响应中;AI 完成首次配置时不调用付费模型验证。 + +原始存档只读继承一次。配置和新存档写入 Host 提供的私有 `context.dataDir`,后续启动继续最近一份有效私有存档。关闭 MiniApp 时先保存进度,再退出其子进程。 + +## Tested environment + +记录的开发与宿主环境为 macOS 26.4、MiniMax Code 3.0.73、Python 3.9.6、Godot 4.4.1 单线程 Web 导出及 Node.js 22.23.2。 + +**真实模型供应商手动实测。** 项目作者使用模型供应商提供的真实有效 API Key,在真实 MiniMax Code 桌面宿主中安装、打开并游玩游戏,实际调用模型 API 并使用供应商返回的真实模型回复。作者于 2026-10-17 确认上述实测方式。已有宿主运行截图和试玩反馈记录了进入游戏,以及员工按钮、聊天入口、坐姿、冲突选项和 HUD 显示等交互与布局问题。 + +**补充自动回归。** 提交的 0.5.10 安装包另外通过 34/34 项自动交互检查,使用临时 Host 上下文和 headless 浏览器,覆盖首次配置、进入游戏、换安装位置、无源码启动、只提供一家模型 Key、重开后复用配置及员工聊天等流程。这套自动测试替换模型回复,以重复检查成功和失败场景;上述作者手动实测使用真实供应商 API。两类测试的记录、自动验证命令和结果见 [验证记录](docs/VALIDATION.md)。 + +Windows/Linux 宿主、真实中文输入法交互和长期平衡不在现有测试覆盖范围内。 + +## Data & access + +- 仅绑定 Host 指定的 `127.0.0.1` 地址和端口。网页只调用本包列出的 `/api/` 路由;不代理任意 URL 或任意文件。 +- 读取用户配置的 `.env` 和首次继承的 JSON;不写回原项目。模型密钥只进入本地服务进程,不进入网页、安装包、日志或错误响应。 +- 持久化目录:`context.dataDir/settings.json` 和 `context.dataDir/saves/`。配置原子写入并限制并发;存档沿用游戏持锁快照逻辑。 +- 启动命令为配置的 Python 解释器,参数数组为 `-u`, `/miniapp/node/backend/server/miniapp_bridge.py`, `--data-dir`, ``, `--env-file`, ``,以及可选的 `--initial-save`, ``。不使用 shell。该子进程经内部 JSON 行协议提供原有游戏 API,不再监听桌面版 8850 端口。退出时关闭 HTTP 监听、流、定时器和子进程。 +- 保存首次配置前,短暂运行相同解释器的 `-c`, `import flask, requests, dotenv` 来检查依赖;最多八秒,输出不进入网页或 Host 日志,退出 MiniApp 会终止尚未完成的检查。 +- 玩游戏会依照现有配置调用 `api.minimaxi.com`、`api.longcat.chat`、`api.kimi.com`、`api.stepfun.com`。Jev 调度如配置了 `OPENROUTER_API_KEY`,还调用 `openrouter.ai`。模型输出仅用于原有游戏交付、聊天、冲突、闲聊和会议功能。 +- 没有第三方 Node 运行时依赖。Python 运行时和 Flask、requests、python-dotenv 可以由现有环境提供,也可由 AI 依照随包依赖清单在私有目录准备。 +- 中文使用随包的 Noto Sans CJK SC 字体,不依赖宿主系统字体。字体遵循 SIL Open Font License 1.1,许可附于 `LICENSE-NOTO.txt`。 + +## Build + +在源码仓库根目录、已准备好的 Python 环境中执行 `python ai-model-party/tools/build_miniapp.py --output `。Godot 从 PATH 或 `GODOT` 环境变量查找,也可传入 `--godot <执行文件>`。产物目录包含完整的 `ai-office` 插件和 ZIP,附带 MIT、字体及 Godot 的许可声明;输出已存在时拒绝覆盖。Godot 的大资源按 gzip 压缩,必要时分成多个小文件,由本地服务还原为浏览器请求的同一资源。 + +官方契约:[MiniMax-AI/MiniMax-Code-MiniApps](https://github.com/MiniMax-AI/MiniMax-Code-MiniApps)。 diff --git a/plugins/cristinamadureira353-svg/ai-office/SETUP_PROMPT.md b/plugins/cristinamadureira353-svg/ai-office/SETUP_PROMPT.md new file mode 100644 index 00000000..211e4054 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/SETUP_PROMPT.md @@ -0,0 +1,28 @@ +我刚安装了「AI 事务所」MiniApp,请担任我的首次启动向导,带我完成配置并开始游戏。我不需要游戏源码,也可能没有 Python、API Key 或任何已有配置,请按新玩家的情况引导。 + +请按下面的顺序执行,每次只让我完成当前需要的一步: + +1. 先读取文末配置接口,检查这个 MiniApp 是否已经配置。已有配置时保留原配置和进度,检查其中各家 Key 是否非空,不回显文件内容或 Key;即使已经配置员工,也要按第 3 步检查 Jev,不要直接跳过。尚未配置时,用简单的话说明:游戏员工由真实模型驱动,可以只启用一家模型,API Key 是让对应员工工作的凭据,游玩会使用我自己的模型额度。然后问我:“你已经有 API Key 吗?有的话告诉我是哪家平台;如果没有 Key,我可以带你申请。” + +2. 如果我有 Key,先确认对应平台,以及我想启用的员工。如果我没有 Key、不了解 Key,或者不知道选哪家,先介绍下面的平台,由我选择一家开始。带我依次完成注册或登录、找到密钥管理、创建 API Key、把 Key 提供给你或存到本地文件。每一步都等待我完成或回答,再继续;不要只丢给我配置命令。套餐、价格、模型权限和入口以平台官网当前说明为准,购买或充值由我自己决定。 + +可以配置的模型与申请入口: +- MiniMax:MINIMAX_API_KEY,M3 和 Flash 共用一个 Key。官方获取说明:https://platform.minimax.cn/docs/guides/quickstart-preparation +- LongCat:LONGCAT_API_KEY。官方获取说明:https://longcat.chat/platform/docs/zh/ +- Kimi:KIMI_API_KEY,当前游戏使用 Kimi Code 接口。通过官方说明中的 Kimi Code 控制台入口获取对应 Key:https://www.kimi.com/code/docs/ +- StepFun:STEPFUN_API_KEY,当前游戏使用 Step Plan 接口。请引导获取对应服务的 Key:https://platform.stepfun.com/step-plan +- OpenRouter / Jev:OPENROUTER_API_KEY。Jev 负责员工的自主去向、自动抢单和会议发言顺序等决策,使用普通 OpenRouter API Key,不需要另一种专门的 Jev Key,也不占用五名员工模型的 Key。官方密钥管理入口:https://openrouter.ai/settings/keys;官方获取说明:https://openrouter.ai/docs/api_reference/authentication;Jev 说明:https://openrouter.ai/docs/guides/community/jev + +3. 只配置我选择和提供的模型,至少启用一家员工模型即可,不要求一次填齐所有 Key。没有任何员工 Key 时继续保持引导并等待我提供,不要用占位值、虚构 Key 或只填路径来宣布配置完成。MiniMax Code 的登录状态不等于已经提供了游戏所需的 API Key。 + +单独引导 Jev:先检查已有配置文件中的 OPENROUTER_API_KEY。已有非空 Key 时保留并复用,说明 Jev 已配置,不要让我重复申请。没有时,说明:“Jev 是可选的自主决策功能,会使用 OpenRouter 账户额度;跳过后游戏仍按本地规则走位和运行。你想现在启用 Jev,还是暂时跳过?”如果我选择启用,先确认是否已有 OpenRouter Key;已有时只补上这一项,保留其他员工 Key 和存档。没有时,引导我打开官方密钥管理入口、注册或登录、创建并保存 API Key、按需设置额度限制,再把 Key 提供给你或存到本地配置文件。是否充值由我自己决定,以官方当前余额和权限说明为准。每一步等待我完成,收到真实 Key 后才写入 OPENROUTER_API_KEY;尚未收到时说明 Jev 待配置,不用员工 Key 代替。选择跳过时留空并明确说明 Jev 未启用,不阻止其他员工配置和游戏启动。 + +4. 使用文末 MiniApp 自动提供的安装目录、私有数据目录和配置接口,不假设我有开发项目或固定的电脑目录。读取安装目录中的 README.md、miniapp/node/env.example 和 miniapp/node/requirements.txt。配置文件默认新建在私有数据目录中,按空白模板保存我提供的 Key,并按系统支持的方式限制为当前用户读写;如果我明确提供了已有配置文件,可复用并保留其其他设置。Key 不要回显到回复、网页或终端日志,也不要写进源码、安装包或 GitHub;配置接口只提交文件路径。 + +5. 检查当前系统可用的 Python 3.9 或以上版本。在 MiniApp 私有数据目录创建独立虚拟环境,并安装随包 requirements.txt 中的依赖;已有可用环境可以复用。没有 Python 时,给我 https://www.python.org/downloads/ 中适合当前系统的安装指引,等待我安装后再继续。由你确定实际解释器路径并验证能导入 flask、requests、dotenv,不要求我手填技术路径。Windows 虚拟环境通常使用 Scripts/python.exe,macOS / Linux 通常使用 bin/python。 + +6. 至少收到一家员工模型的 Key、准备好本地文件和依赖后,向文末 configurationEndpoint 提交 POST application/json,字段为 pythonPath、envFile、initialSave,前两项使用实际文件绝对路径。initialSave 默认留空;只有我明确要求继承旧进度时才只读导入一份 JSON 存档。确认返回 ok 为 true,再 GET 同一接口确认 configured 为 true,页面随后自动进入游戏,以后直接启动。 + +7. 配置期间不调用付费模型来验证 Key,不替我购买套餐或读取宿主登录凭据。完成后简短告诉我哪些员工已配置、哪些未配置,以及 Jev 是已配置、已跳过还是等待 Key,说明本地配置已保存,实际模型可用性在游玩时体现。如果缺少权限、Python 或 Key,说明当前缺什么并继续引导;未完成时不要声称可以正常游玩。 + +如果没有附带本次运行信息,先让我打开 MiniApp 并复制页面上的完整引导提示词;如果本次接口已停止,让我重新打开后复制最新提示词。不要猜测端口、路径或替换别人的配置。 diff --git a/plugins/cristinamadureira353-svg/ai-office/THIRD_PARTY_NOTICES.md b/plugins/cristinamadureira353-svg/ai-office/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..d4793a25 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/THIRD_PARTY_NOTICES.md @@ -0,0 +1,43 @@ +# Third-party notices and asset provenance + +Project code, project-prepared images and synthesized audio are offered under the root LICENSE. Third-party material retains the following licenses. Provider names and model names identify integrations; they do not imply endorsement or grant trademark rights. + +## Godot Engine 4.4.1 + +The source project uses Godot. A built MiniApp also redistributes Godot's Web engine runtime. Godot is MIT licensed. Engine attribution and its third-party notices accompany the package as `LICENSE-GODOT.txt` and `COPYRIGHT-GODOT.txt`. + +- Copyright (c) 2014-present Godot Engine contributors. +- Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. +- License: LICENSES/Godot-MIT.txt +- Engine third-party notices: LICENSES/Godot-COPYRIGHT.txt +- Upstream: https://github.com/godotengine/godot/tree/4.4.1-stable +- https://godotengine.org/license/ + +## Noto Sans CJK SC Regular + +`godot/assets/fonts/NotoSansCJKsc-Regular.otf` is unmodified Noto Sans CJK SC Regular. Its embedded copyright identifies Adobe, 2014–2021; Noto is a trademark of Google Inc. The font is distributed under SIL Open Font License 1.1, not the project's code license. + +- Copyright 2014-2021 Adobe (http://www.adobe.com/). +- License: godot/assets/fonts/OFL.txt; copied into MiniApp packages as LICENSE-NOTO.txt. +- Upstream: https://github.com/notofonts/noto-cjk/tree/main/Sans + +## Project artwork + +The 19 PNG atlases under `godot/assets/cozy_office/atlases/` were generated with AI for this project and prepared into named Godot resources. They include architecture, furniture, UI, five employees, clients and activity frames. No Stardew Valley artwork or downloaded sample-game sprites are included in this source distribution. + +`godot/assets/cozy_office/manifest.json` records asset names, regions and atlas hashes. See docs/ASSETS.md. No personal generation paths or original reference photographs are included. + +## Project audio + +The ten OGG files under `godot/assets/sfx/` are synthesized by `ai-model-party/tools/audio_gen.py`. The source distribution does not include downloaded music or sampled third-party recordings. Asset descriptions are in `godot/assets/sfx/AUDIO_LICENSES.md`. + +## Installed dependencies + +Python and Node dependencies are installed separately and are not vendored in source or MiniApp packages. Preserve their own notices if redistributing an environment: + +- Flask / Werkzeug: BSD-3-Clause — https://github.com/pallets/flask / https://github.com/pallets/werkzeug +- Requests: Apache-2.0 — https://github.com/psf/requests +- python-dotenv: BSD-3-Clause — https://github.com/theskumar/python-dotenv +- Playwright (development checks): Apache-2.0 — https://github.com/microsoft/playwright +- NumPy (optional audio regeneration): BSD-3-Clause — https://github.com/numpy/numpy +- FFmpeg (optional, installed separately): license depends on the selected build — https://ffmpeg.org/legal.html diff --git a/plugins/cristinamadureira353-svg/ai-office/docs/VALIDATION.md b/plugins/cristinamadureira353-svg/ai-office/docs/VALIDATION.md new file mode 100644 index 00000000..659a4a8b --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/docs/VALIDATION.md @@ -0,0 +1,60 @@ +# AI Office validation + +Package version: **0.5.10**. This contribution uses the already published runtime from [AI Office v0.5.10](https://github.com/cristinamadureira353-svg/ai-office/releases/tag/v0.5.10), with submission documentation, a synthetic preview and author attribution added. Runtime files are compared byte-for-byte with that release before submission. + +## Environment and scope + +Recorded development and host environment: macOS 26.4, MiniMax Code 3.0.73, Python 3.9.6, Node.js 22.23.2 and Godot 4.4.1. The host application metadata and operating-system version were rechecked on 2026-10-17. + +The manual play record covers the author's use of real provider API keys and live model responses. The automated regression record describes the separate isolated test suite. Windows/Linux host play, Chinese IME behavior and long-term balance remain outside the recorded test coverage. + +## Manual MiniMax Code play record + +The project author confirmed on 2026-10-17 that the game had already been installed, opened and manually played in the real MiniMax Code desktop host on macOS **using real provider-issued API keys, live model API calls and actual provider responses**. This live-provider test method is reported by the author. Existing development screenshots show AI Office running inside that host. The author's play reports include employee controls and chat access, rest seating, conflict-choice buttons and HUD visibility; these reports informed the corresponding fixes and regression coverage. + +The table below records the author's live-provider manual play and the separate automated checks for the submitted 0.5.10 package: + +| Flow | Recorded evidence and result | +| --- | --- | +| Install, open and enter the game in MiniMax Code | Author-confirmed manual play; existing host screenshots show the game running | +| Model providers during manual play | Author-confirmed real provider-issued API keys, live API calls and actual model responses | +| Employee controls/chat access, seating, conflict choices and HUD | Manual gameplay feedback identified interaction/layout problems; fixes have targeted regression coverage | +| First-run guided configuration, game entry, reopening and employee chat in the submitted package | Covered by the 34/34 automated package checks below; this total counts automated checks | + +The manual observations above come from the author's development playtests. The automated submission results below apply to the submitted 0.5.10 package. + +## Automated regression scope + +The separate automated suite uses headless Chromium, creates temporary private state, replaces employee responses with fixtures and blocks paid model network calls. This applies to the scripted regression suite. The manual play record above uses the author's real provider keys and live model responses. Real keys, player saves and Host login credentials are excluded from the automated fixtures and published test artifacts. + +## Reproduce checks + +From the official MiniApps repository root, with Node.js 22+: + +```sh +npm run check +``` + +For the game's isolated backend, client and package interaction checks, use the [tagged source repository](https://github.com/cristinamadureira353-svg/ai-office/tree/v0.5.10) and follow its `docs/DEVELOPMENT.md`. The package includes `tests/` outside the runtime payload. With source development dependencies installed, its runner accepts a relocated complete package: + +```sh +python tools/test_miniapp.py --plugin --chromium +``` + +Browsers run headless. Package tests cover configuration and reopening, quota shutdown and manual reboot, worker chat, all four conflict choices, failed-request recovery, rest seating, layout and HiDPI behavior. + +## Automated results + +Checks run against the relocated contribution package: + +| Check | Result | +| --- | --- | +| Official repository `npm run check` | 0 errors; 55/55 contract-checker tests passed | +| This package's validator diagnostics | `ai-office: OK`, no warnings | +| Full package interaction tests | 34/34 passed, no failures or skips | +| Runtime compared with the published v0.5.10 package | Byte-for-byte identical | +| Privacy review | No known real keys, credential formats, personal paths or PNG text/EXIF metadata found | + +The official repository also reports four pre-existing `README_HEADING_MISSING` warnings in the unchanged `amszuidas/mcode-token-usage-board` and `yanhy2000/mcode-usage-monitor` packages, two headings each. This contribution does not modify those packages. + +The source release's earlier results were 54 isolated backend tests and 415 Godot headless checks; see its validation record for their scope. The 34 package tests above were rerun against this contribution directory. These totals describe the isolated automated suite. Live-provider manual MiniMax Code play is documented in the separate manual record above. diff --git a/plugins/cristinamadureira353-svg/ai-office/docs/preview.png b/plugins/cristinamadureira353-svg/ai-office/docs/preview.png new file mode 100644 index 00000000..56e0b9b3 Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/docs/preview.png differ diff --git a/plugins/cristinamadureira353-svg/ai-office/icon.png b/plugins/cristinamadureira353-svg/ai-office/icon.png new file mode 100644 index 00000000..f42d21db Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/icon.png differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.apple-touch-icon.png b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.apple-touch-icon.png new file mode 100644 index 00000000..fc334bc1 Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.apple-touch-icon.png differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.audio.position.worklet.js b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.audio.position.worklet.js new file mode 100644 index 00000000..155d4e6e --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.audio.position.worklet.js @@ -0,0 +1,69 @@ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +const POST_THRESHOLD_S = 0.1; + +class GodotPositionReportingProcessor extends AudioWorkletProcessor { + constructor(...args) { + super(...args); + this.lastPostTime = currentTime; + this.position = 0; + this.ended = false; + + this.port.onmessage = (event) => { + if (event?.data?.type === 'ended') { + this.ended = true; + } + }; + } + + process(inputs, _outputs, _parameters) { + if (this.ended) { + return false; + } + + if (inputs.length > 0) { + const input = inputs[0]; + if (input.length > 0) { + this.position += input[0].length; + } + } + + // Posting messages is expensive. Let's limit the number of posts. + if (currentTime - this.lastPostTime > POST_THRESHOLD_S) { + this.lastPostTime = currentTime; + this.port.postMessage({ type: 'position', data: this.position }); + } + + return true; + } +} + +registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor); diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.audio.worklet.js b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.audio.worklet.js new file mode 100644 index 00000000..3b94cab8 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.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/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.html b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.html new file mode 100644 index 00000000..af3cc0fc --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.html @@ -0,0 +1,220 @@ + + + + + + AI 事务所 + + + + + + + + Your browser does not support the canvas tag. + + + + +
+ + +
+
+ + + + + + diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.icon.png b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.icon.png new file mode 100644 index 00000000..f42d21db Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.icon.png differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.js b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.js new file mode 100644 index 00000000..e64d1ee6 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.js @@ -0,0 +1,912 @@ + +var Godot = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + + return ( +function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});["_memory","__Z14godot_web_mainiPPc","__emwebxr_on_input_event","__emwebxr_on_simple_event","___indirect_function_table","_main","onRuntimeInitialized"].forEach(prop=>{if(!Object.getOwnPropertyDescriptor(readyPromise,prop)){Object.defineProperty(readyPromise,prop,{get:()=>abort("You are getting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js"),set:()=>abort("You are setting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js")})}});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(Module["ENVIRONMENT"]){throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)")}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_SHELL){if(typeof process=="object"&&typeof require==="function"||typeof window=="object"||typeof importScripts=="function")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){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}if(!(typeof window=="object"||typeof importScripts=="function"))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=url=>{assert(!isFileURI(url),"readAsync does not work with file:// URLs");return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{throw new Error("environment detection error")}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;checkIncomingModuleAPI();if(Module["arguments"])arguments_=Module["arguments"];legacyModuleProp("arguments","arguments_");if(Module["thisProgram"])thisProgram=Module["thisProgram"];legacyModuleProp("thisProgram","thisProgram");if(Module["quit"])quit_=Module["quit"];legacyModuleProp("quit","quit_");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");legacyModuleProp("asm","wasmExports");legacyModuleProp("readAsync","readAsync");legacyModuleProp("readBinary","readBinary");legacyModuleProp("setWindowTitle","setWindowTitle");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(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];legacyModuleProp("wasmBinary","wasmBinary");if(typeof WebAssembly!="object"){err("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed"+(text?": "+text:""))}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;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(!Module["STACK_SIZE"],"STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time");assert(typeof Int32Array!="undefined"&&typeof Float64Array!=="undefined"&&Int32Array.prototype.subarray!=undefined&&Int32Array.prototype.set!=undefined,"JS engine does not provide full typed array support");assert(!Module["wasmMemory"],"Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally");assert(!Module["INITIAL_MEMORY"],"Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically");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)!")}}(function(){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)"})();var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATEXIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){assert(!runtimeInitialized);runtimeInitialized=true;checkStackCookie();if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function preMain(){checkStackCookie();callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){assert(!runtimeExited);checkStackCookie();___funcs_on_exit();callRuntimeCallbacks(__ATEXIT__);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())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}assert(Math.imul,"This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.fround,"This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.clz32,"This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.trunc,"This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;var runDependencyTracking={};function getUniqueRunDependency(id){var orig=id;while(1){if(!runDependencyTracking[id])return id;id=orig+Math.random()}}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;EXITSTATUS=1;var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");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)}}function findWasmBinary(){var f="godot.web.template_release.wasm32.nothreads.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;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"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,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)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){var info=getWasmImports();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");addOnInit(wasmExports["__wasm_call_ctors"]);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;receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}if(!wasmBinaryFile)wasmBinaryFile=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}function legacyModuleProp(prop,newName,incoming=true){if(!Object.getOwnPropertyDescriptor(Module,prop)){Object.defineProperty(Module,prop,{configurable:true,get(){let extra=incoming?" (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)":"";abort(`\`Module.${prop}\` has been replaced by \`${newName}\``+extra)}})}}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 missingGlobal(sym,msg){if(typeof globalThis!="undefined"){Object.defineProperty(globalThis,sym,{configurable:true,get(){warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`);return undefined}})}}missingGlobal("buffer","Please use HEAP8.buffer or wasmMemory.buffer");missingGlobal("asm","Please use wasmExports instead");function missingLibrarySymbol(sym){if(typeof globalThis!="undefined"&&!Object.getOwnPropertyDescriptor(globalThis,sym)){Object.defineProperty(globalThis,sym,{configurable:true,get(){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);return undefined}})}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)}})}}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};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=Module["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 UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx,maxBytesToRead)=>{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 UTF8ToString=(ptr,maxBytesToRead)=>{assert(typeof ptr=="number",`UTF8ToString expects a number (got ${typeof ptr})`);return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};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.substr(-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.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };")};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).substr(1);to=PATH_FS.resolve(to).substr(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 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=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=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}}heap[outIdx]=0;return outIdx-startIdx};function 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: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,0));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,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));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,"/",16384|511,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,allocate:MEMFS.stream_ops.allocate,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.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}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.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=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.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},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 dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["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,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=Object.assign({},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=Object.assign({},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: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:db,entries: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(function(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(function(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 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,ErrnoError:class extends Error{constructor(errno){super(runtimeInitialized?strError(errno):"");this.name="ErrnoError";this.errno=errno;for(var key in ERRNO_CODES){if(ERRNO_CODES[key]===errno){this.code=key;break}}}},genericErrors:{},filesystems:null,syncFSRequests:0,FSStream:class{constructor(){this.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{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}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={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;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){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){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},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},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:type,opts:opts,mountpoint: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||name==="."||name===".."){throw new FS.ErrnoError(28)}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)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;i=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},allocate(stream,offset,length){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},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)}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 ret;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"){ret=UTF8ArrayToString(buf,0)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else 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});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){randomLeft=randomFill(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",16384|511,73);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}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(){if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}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(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS:MEMFS,IDBFS:IDBFS}},init(input,output,error){assert(!FS.init.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.init.initialized=true;Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit(){FS.init.initialized=false;_fflush(0);for(var i=0;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: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: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 PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);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;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},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===0);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}}function 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{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}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;idx+=1}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_iflag,c_oflag:c_oflag,c_cflag:c_cflag,c_lflag:c_lflag,c_cc: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.doStat(FS.lstat,path,buf)}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);path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);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.doStat(nofollow?FS.lstat:FS.stat,path,buf)}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.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_statfs64(path,size,buf){try{path=SYSCALLS.getStr(path);assert(size===64);HEAP32[buf+4>>2]=4096;HEAP32[buf+40>>2]=4096;HEAP32[buf+8>>2]=1e6;HEAP32[buf+12>>2]=5e5;HEAP32[buf+16>>2]=5e5;HEAP32[buf+20>>2]=FS.nextInode;HEAP32[buf+24>>2]=1e6;HEAP32[buf+28>>2]=42;HEAP32[buf+44>>2]=2;HEAP32[buf+36>>2]=255;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_symlink(target,linkpath){try{target=SYSCALLS.getStr(target);linkpath=SYSCALLS.getStr(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===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}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 nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;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 __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(summerOffset{runtimeKeepaliveCounter+=1};var _emscripten_set_main_loop_timing=(mode,value)=>{Browser.mainLoop.timingMode=mode;Browser.mainLoop.timingValue=value;if(!Browser.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(!Browser.mainLoop.running){runtimeKeepalivePush();Browser.mainLoop.running=true}if(mode==0){Browser.mainLoop.scheduler=function Browser_mainLoop_scheduler_setTimeout(){var timeUntilNextTick=Math.max(0,Browser.mainLoop.tickStartTime+value-_emscripten_get_now())|0;setTimeout(Browser.mainLoop.runner,timeUntilNextTick)};Browser.mainLoop.method="timeout"}else if(mode==1){Browser.mainLoop.scheduler=function Browser_mainLoop_scheduler_rAF(){Browser.requestAnimationFrame(Browser.mainLoop.runner)};Browser.mainLoop.method="rAF"}else if(mode==2){if(typeof Browser.setImmediate=="undefined"){if(typeof setImmediate=="undefined"){var setImmediates=[];var emscriptenMainLoopMessageId="setimmediate";var Browser_setImmediate_messageHandler=event=>{if(event.data===emscriptenMainLoopMessageId||event.data.target===emscriptenMainLoopMessageId){event.stopPropagation();setImmediates.shift()()}};addEventListener("message",Browser_setImmediate_messageHandler,true);Browser.setImmediate=function Browser_emulated_setImmediate(func){setImmediates.push(func);if(ENVIRONMENT_IS_WORKER){Module["setImmediates"]??=[];Module["setImmediates"].push(func);postMessage({target:emscriptenMainLoopMessageId})}else postMessage(emscriptenMainLoopMessageId,"*")}}else{Browser.setImmediate=setImmediate}}Browser.mainLoop.scheduler=function Browser_mainLoop_scheduler_setImmediate(){Browser.setImmediate(Browser.mainLoop.runner)};Browser.mainLoop.method="immediate"}return 0};var _emscripten_get_now;_emscripten_get_now=()=>performance.now();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_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_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","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"];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);context.defaultVao=undefined;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);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: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]&&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_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")}webgl_enable_WEBGL_multi_draw(GLctx);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 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 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 maybeExit=()=>{if(runtimeExited){return}if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var runtimeKeepalivePop=()=>{assert(runtimeKeepaliveCounter>0);runtimeKeepaliveCounter-=1};var setMainLoop=(browserIterationFunc,fps,simulateInfiniteLoop,arg,noSetTiming)=>{assert(!Browser.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.");Browser.mainLoop.func=browserIterationFunc;Browser.mainLoop.arg=arg;var thisMainLoopId=Browser.mainLoop.currentlyRunningMainloop;function checkIsRunning(){if(thisMainLoopId0){var start=Date.now();var blocker=Browser.mainLoop.queue.shift();blocker.func(blocker.arg);if(Browser.mainLoop.remainingBlockers){var remaining=Browser.mainLoop.remainingBlockers;var next=remaining%1==0?remaining-1:Math.floor(remaining);if(blocker.counted){Browser.mainLoop.remainingBlockers=next}else{next=next+.5;Browser.mainLoop.remainingBlockers=(8*remaining+next)/9}}Browser.mainLoop.updateStatus();if(!checkIsRunning())return;setTimeout(Browser.mainLoop.runner,0);return}if(!checkIsRunning())return;Browser.mainLoop.currentFrameNumber=Browser.mainLoop.currentFrameNumber+1|0;if(Browser.mainLoop.timingMode==1&&Browser.mainLoop.timingValue>1&&Browser.mainLoop.currentFrameNumber%Browser.mainLoop.timingValue!=0){Browser.mainLoop.scheduler();return}else if(Browser.mainLoop.timingMode==0){Browser.mainLoop.tickStartTime=_emscripten_get_now()}if(Browser.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!");Browser.mainLoop.method=""}Browser.mainLoop.runIter(browserIterationFunc);checkStackCookie();if(!checkIsRunning())return;if(typeof SDL=="object")SDL.audio?.queueNewAudioData?.();Browser.mainLoop.scheduler()};if(!noSetTiming){if(fps&&fps>0){_emscripten_set_main_loop_timing(0,1e3/fps)}else{_emscripten_set_main_loop_timing(1,1)}Browser.mainLoop.scheduler()}if(simulateInfiniteLoop){throw"unwind"}};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 safeSetTimeout=(func,timeout)=>{runtimeKeepalivePush();return setTimeout(()=>{runtimeKeepalivePop();callUserCallback(func)},timeout)};var Browser={mainLoop:{running:false,scheduler:null,method:"",currentlyRunningMainloop:0,func:null,arg:0,timingMode:0,timingValue:0,currentFrameNumber:0,queue:[],pause(){Browser.mainLoop.scheduler=null;Browser.mainLoop.currentlyRunningMainloop++},resume(){Browser.mainLoop.currentlyRunningMainloop++;var timingMode=Browser.mainLoop.timingMode;var timingValue=Browser.mainLoop.timingValue;var func=Browser.mainLoop.func;Browser.mainLoop.func=null;setMainLoop(func,0,false,Browser.mainLoop.arg,true);_emscripten_set_main_loop_timing(timingMode,timingValue);Browser.mainLoop.scheduler()},updateStatus(){if(Module["setStatus"]){var message=Module["statusMessage"]||"Please wait...";var remaining=Browser.mainLoop.remainingBlockers;var expected=Browser.mainLoop.expectedBlockers;if(remaining){if(remaining{assert(img.complete,`Image ${name} could not be decoded`);var canvas=document.createElement("canvas");canvas.width=img.width;canvas.height=img.height;var ctx=canvas.getContext("2d");ctx.drawImage(img,0,0);preloadedImages[name]=canvas;URL.revokeObjectURL(url);onload?.(byteArray)};img.onerror=event=>{err(`Image ${url} could not be decoded`);onerror?.()};img.src=url};preloadPlugins.push(imagePlugin);var audioPlugin={};audioPlugin["canHandle"]=function audioPlugin_canHandle(name){return!Module.noAudioDecoding&&name.substr(-4)in{".ogg":1,".wav":1,".mp3":1}};audioPlugin["handle"]=function audioPlugin_handle(byteArray,name,onload,onerror){var done=false;function finish(audio){if(done)return;done=true;preloadedAudios[name]=audio;onload?.(byteArray)}var b=new Blob([byteArray],{type:Browser.getMimetype(name)});var url=URL.createObjectURL(b);assert(typeof url=="string","createObjectURL must return a url as a string");var audio=new Audio;audio.addEventListener("canplaythrough",()=>finish(audio),false);audio.onerror=function audio_onerror(event){if(done)return;err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`);function encode64(data){var BASE="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var PAD="=";var ret="";var leftchar=0;var leftbits=0;for(var i=0;i=6){var curr=leftchar>>leftbits-6&63;leftbits-=6;ret+=BASE[curr]}}if(leftbits==2){ret+=BASE[(leftchar&3)<<4];ret+=PAD+PAD}else if(leftbits==4){ret+=BASE[(leftchar&15)<<2];ret+=PAD}return ret}audio.src="data:audio/x-"+name.substr(-3)+";base64,"+encode64(byteArray);finish(audio)};audio.src=url;safeSetTimeout(()=>{finish(audio)},1e4)};preloadPlugins.push(audioPlugin);function pointerLockChange(){Browser.pointerLock=document["pointerLockElement"]===Module["canvas"]||document["mozPointerLockElement"]===Module["canvas"]||document["webkitPointerLockElement"]===Module["canvas"]||document["msPointerLockElement"]===Module["canvas"]}var canvas=Module["canvas"];if(canvas){canvas.requestPointerLock=canvas["requestPointerLock"]||canvas["mozRequestPointerLock"]||canvas["webkitRequestPointerLock"]||canvas["msRequestPointerLock"]||(()=>{});canvas.exitPointerLock=document["exitPointerLock"]||document["mozExitPointerLock"]||document["webkitExitPointerLock"]||document["msExitPointerLock"]||(()=>{});canvas.exitPointerLock=canvas.exitPointerLock.bind(document);document.addEventListener("pointerlockchange",pointerLockChange,false);document.addEventListener("mozpointerlockchange",pointerLockChange,false);document.addEventListener("webkitpointerlockchange",pointerLockChange,false);document.addEventListener("mspointerlockchange",pointerLockChange,false);if(Module["elementPointerLock"]){canvas.addEventListener("click",ev=>{if(!Browser.pointerLock&&Module["canvas"].requestPointerLock){Module["canvas"].requestPointerLock();ev.preventDefault()}},false)}}},createContext(canvas,useWebGL,setInModule,webGLContextAttributes){if(useWebGL&&Module.ctx&&canvas==Module.canvas)return Module.ctx;var ctx;var contextHandle;if(useWebGL){var contextAttributes={antialias:false,alpha:false,majorVersion:typeof WebGL2RenderingContext!="undefined"?2:1};if(webGLContextAttributes){for(var attribute in webGLContextAttributes){contextAttributes[attribute]=webGLContextAttributes[attribute]}}if(typeof GL!="undefined"){contextHandle=GL.createContext(canvas,contextAttributes);if(contextHandle){ctx=GL.getContext(contextHandle).GLctx}}}else{ctx=canvas.getContext("2d")}if(!ctx)return null;if(setInModule){if(!useWebGL)assert(typeof GLctx=="undefined","cannot set in module if GLctx is used, but we are a non-GL context that would replace it");Module.ctx=ctx;if(useWebGL)GL.makeContextCurrent(contextHandle);Module.useWebGL=useWebGL;Browser.moduleContextCreatedCallbacks.forEach(callback=>callback());Browser.init()}return ctx},destroyContext(canvas,useWebGL,setInModule){},fullscreenHandlersInstalled:false,lockPointer:undefined,resizeCanvas:undefined,requestFullscreen(lockPointer,resizeCanvas){Browser.lockPointer=lockPointer;Browser.resizeCanvas=resizeCanvas;if(typeof Browser.lockPointer=="undefined")Browser.lockPointer=true;if(typeof Browser.resizeCanvas=="undefined")Browser.resizeCanvas=false;var canvas=Module["canvas"];function fullscreenChange(){Browser.isFullscreen=false;var canvasContainer=canvas.parentNode;if((document["fullscreenElement"]||document["mozFullScreenElement"]||document["msFullscreenElement"]||document["webkitFullscreenElement"]||document["webkitCurrentFullScreenElement"])===canvasContainer){canvas.exitFullscreen=Browser.exitFullscreen;if(Browser.lockPointer)canvas.requestPointerLock();Browser.isFullscreen=true;if(Browser.resizeCanvas){Browser.setFullscreenCanvasSize()}else{Browser.updateCanvasDimensions(canvas)}}else{canvasContainer.parentNode.insertBefore(canvas,canvasContainer);canvasContainer.parentNode.removeChild(canvasContainer);if(Browser.resizeCanvas){Browser.setWindowedCanvasSize()}else{Browser.updateCanvasDimensions(canvas)}}Module["onFullScreen"]?.(Browser.isFullscreen);Module["onFullscreen"]?.(Browser.isFullscreen)}if(!Browser.fullscreenHandlersInstalled){Browser.fullscreenHandlersInstalled=true;document.addEventListener("fullscreenchange",fullscreenChange,false);document.addEventListener("mozfullscreenchange",fullscreenChange,false);document.addEventListener("webkitfullscreenchange",fullscreenChange,false);document.addEventListener("MSFullscreenChange",fullscreenChange,false)}var canvasContainer=document.createElement("div");canvas.parentNode.insertBefore(canvasContainer,canvas);canvasContainer.appendChild(canvas);canvasContainer.requestFullscreen=canvasContainer["requestFullscreen"]||canvasContainer["mozRequestFullScreen"]||canvasContainer["msRequestFullscreen"]||(canvasContainer["webkitRequestFullscreen"]?()=>canvasContainer["webkitRequestFullscreen"](Element["ALLOW_KEYBOARD_INPUT"]):null)||(canvasContainer["webkitRequestFullScreen"]?()=>canvasContainer["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]):null);canvasContainer.requestFullscreen()},requestFullScreen(){abort("Module.requestFullScreen has been replaced by Module.requestFullscreen (without a capital S)")},exitFullscreen(){if(!Browser.isFullscreen){return false}var CFS=document["exitFullscreen"]||document["cancelFullScreen"]||document["mozCancelFullScreen"]||document["msExitFullscreen"]||document["webkitCancelFullScreen"]||(()=>{});CFS.apply(document,[]);return true},nextRAF:0,fakeRequestAnimationFrame(func){var now=Date.now();if(Browser.nextRAF===0){Browser.nextRAF=now+1e3/60}else{while(now+2>=Browser.nextRAF){Browser.nextRAF+=1e3/60}}var delay=Math.max(Browser.nextRAF-now,0);setTimeout(func,delay)},requestAnimationFrame(func){if(typeof requestAnimationFrame=="function"){requestAnimationFrame(func);return}var RAF=Browser.fakeRequestAnimationFrame;RAF(func)},safeSetTimeout(func,timeout){return safeSetTimeout(func,timeout)},safeRequestAnimationFrame(func){runtimeKeepalivePush();return Browser.requestAnimationFrame(()=>{runtimeKeepalivePop();callUserCallback(func)})},getMimetype(name){return{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",bmp:"image/bmp",ogg:"audio/ogg",wav:"audio/wav",mp3:"audio/mpeg"}[name.substr(name.lastIndexOf(".")+1)]},getUserMedia(func){window.getUserMedia||=navigator["getUserMedia"]||navigator["mozGetUserMedia"];window.getUserMedia(func)},getMovementX(event){return event["movementX"]||event["mozMovementX"]||event["webkitMovementX"]||0},getMovementY(event){return event["movementY"]||event["mozMovementY"]||event["webkitMovementY"]||0},getMouseWheelDelta(event){var delta=0;switch(event.type){case"DOMMouseScroll":delta=event.detail/3;break;case"mousewheel":delta=event.wheelDelta/120;break;case"wheel":delta=event.deltaY;switch(event.deltaMode){case 0:delta/=100;break;case 1:delta/=3;break;case 2:delta*=80;break;default:throw"unrecognized mouse wheel delta mode: "+event.deltaMode}break;default:throw"unrecognized mouse wheel event: "+event.type}return delta},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseCoords(pageX,pageY){var rect=Module["canvas"].getBoundingClientRect();var cw=Module["canvas"].width;var ch=Module["canvas"].height;var scrollX=typeof window.scrollX!="undefined"?window.scrollX:window.pageXOffset;var scrollY=typeof window.scrollY!="undefined"?window.scrollY:window.pageYOffset;assert(typeof scrollX!="undefined"&&typeof scrollY!="undefined","Unable to retrieve scroll position, mouse positions likely broken.");var adjustedX=pageX-(scrollX+rect.left);var adjustedY=pageY-(scrollY+rect.top);adjustedX=adjustedX*(cw/rect.width);adjustedY=adjustedY*(ch/rect.height);return{x:adjustedX,y:adjustedY}},setMouseCoords(pageX,pageY){const{x:x,y:y}=Browser.calculateMouseCoords(pageX,pageY);Browser.mouseMovementX=x-Browser.mouseX;Browser.mouseMovementY=y-Browser.mouseY;Browser.mouseX=x;Browser.mouseY=y},calculateMouseEvent(event){if(Browser.pointerLock){if(event.type!="mousemove"&&"mozMovementX"in event){Browser.mouseMovementX=Browser.mouseMovementY=0}else{Browser.mouseMovementX=Browser.getMovementX(event);Browser.mouseMovementY=Browser.getMovementY(event)}Browser.mouseX+=Browser.mouseMovementX;Browser.mouseY+=Browser.mouseMovementY}else{if(event.type==="touchstart"||event.type==="touchend"||event.type==="touchmove"){var touch=event.touch;if(touch===undefined){return}var coords=Browser.calculateMouseCoords(touch.pageX,touch.pageY);if(event.type==="touchstart"){Browser.lastTouches[touch.identifier]=coords;Browser.touches[touch.identifier]=coords}else if(event.type==="touchend"||event.type==="touchmove"){var last=Browser.touches[touch.identifier];last||=coords;Browser.lastTouches[touch.identifier]=last;Browser.touches[touch.identifier]=coords}return}Browser.setMouseCoords(event.pageX,event.pageY)}},resizeListeners:[],updateResizeListeners(){var canvas=Module["canvas"];Browser.resizeListeners.forEach(listener=>listener(canvas.width,canvas.height))},setCanvasSize(width,height,noUpdates){var canvas=Module["canvas"];Browser.updateCanvasDimensions(canvas,width,height);if(!noUpdates)Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize(){if(typeof SDL!="undefined"){var flags=HEAPU32[SDL.screen>>2];flags=flags|8388608;HEAP32[SDL.screen>>2]=flags}Browser.updateCanvasDimensions(Module["canvas"]);Browser.updateResizeListeners()},setWindowedCanvasSize(){if(typeof SDL!="undefined"){var flags=HEAPU32[SDL.screen>>2];flags=flags&~8388608;HEAP32[SDL.screen>>2]=flags}Browser.updateCanvasDimensions(Module["canvas"]);Browser.updateResizeListeners()},updateCanvasDimensions(canvas,wNative,hNative){if(wNative&&hNative){canvas.widthNative=wNative;canvas.heightNative=hNative}else{wNative=canvas.widthNative;hNative=canvas.heightNative}var w=wNative;var h=hNative;if(Module["forcedAspectRatio"]&&Module["forcedAspectRatio"]>0){if(w/h{Browser.mainLoop.pause();Browser.mainLoop.func=null};var _emscripten_date_now=()=>Date.now();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 growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536;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}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;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,alignUp(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 JSEvents={removeAllEventListeners(){while(JSEvents.eventHandlers.length){JSEvents._removeHandler(JSEvents.eventHandlers.length-1)}JSEvents.deferredCalls=[]},registerRemoveEventListeners(){if(!JSEvents.removeEventListenersRegistered){__ATEXIT__.push(JSEvents.removeAllEventListeners);JSEvents.removeEventListenersRegistered=true}},inEventHandler:0,deferredCalls:[],deferCall(targetFunction,precedence,argsList){function arraysHaveEqualContent(arrA,arrB){if(arrA.length!=arrB.length)return false;for(var i in arrA){if(arrA[i]!=arrB[i])return false}return true}for(var call of JSEvents.deferredCalls){if(call.targetFunction==targetFunction&&arraysHaveEqualContent(call.argsList,argsList)){return}}JSEvents.deferredCalls.push({targetFunction:targetFunction,precedence:precedence,argsList:argsList});JSEvents.deferredCalls.sort((x,y)=>x.precedencecall.targetFunction!=targetFunction)},canPerformEventHandlerRequests(){if(navigator.userActivation){return navigator.userActivation.isActive}return JSEvents.inEventHandler&&JSEvents.currentEventHandler.allowsDeferredCalls},runDeferredCalls(){if(!JSEvents.canPerformEventHandlerRequests()){return}var deferredCalls=JSEvents.deferredCalls;JSEvents.deferredCalls=[];for(var call of deferredCalls){call.targetFunction(...call.argsList)}},eventHandlers:[],removeAllHandlersOnTarget:(target,eventTypeString)=>{for(var i=0;icString>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):undefined);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 browserIterationFunc=getWasmTableEntry(func);setMainLoop(browserIterationFunc,fps,simulateInfiniteLoop)};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]};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.substr(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);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.languages&&navigator.languages[0]||"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 stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+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(typeof offset!="undefined"){offset+=curr}}return ret};function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>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 _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,data?HEAPU8.subarray(data,data+imageSize):null)};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=function $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){for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){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 glVersion=GLctx.getParameter(7938);if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${glVersion})`;else{glVersion=`OpenGL ES 2.0 (${glVersion})`}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={};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 _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){var view=miniTempWebGLFloatBuffers[2*count];for(var i=0;i<2*count;i+=2){view[i]=HEAPF32[value+4*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){var view=miniTempWebGLIntBuffers[2*count];for(var i=0;i<2*count;i+=2){view[i]=HEAP32[value+4*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){var view=miniTempWebGLFloatBuffers[3*count];for(var i=0;i<3*count;i+=3){view[i]=HEAPF32[value+4*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;for(var i=0;i<4*count;i+=4){var dst=value+i;view[i]=heap[dst];view[i+1]=heap[dst+1];view[i+2]=heap[dst+2];view[i+3]=heap[dst+3]}}else{var view=HEAPF32.subarray(value>>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){var view=miniTempWebGLFloatBuffers[9*count];for(var i=0;i<9*count;i+=9){view[i]=HEAPF32[value+4*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;for(var i=0;i<16*count;i+=16){var dst=value+i;view[i]=heap[dst];view[i+1]=heap[dst+1];view[i+2]=heap[dst+2];view[i+3]=heap[dst+3];view[i+4]=heap[dst+4];view[i+5]=heap[dst+5];view[i+6]=heap[dst+6];view[i+7]=heap[dst+7];view[i+8]=heap[dst+8];view[i+9]=heap[dst+9];view[i+10]=heap[dst+10];view[i+11]=heap[dst+11];view[i+12]=heap[dst+12];view[i+13]=heap[dst+13];view[i+14]=heap[dst+14];view[i+15]=heap[dst+15]}}else{var view=HEAPF32.subarray(value>>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,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.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;busIdx{switch(event.data["type"]){case"position":this._playbackPosition=parseInt(event.data.data,10)/this.getSample().sampleRate+this.offset;break;default:}};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;this._positionWorklet.port.postMessage({type:"ended"});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":{const id=this.id;self.stop();if(GodotAudio.sampleFinishedCallback!=null){const idCharPtr=GodotRuntime.allocString(id);GodotAudio.sampleFinishedCallback(idCharPtr);GodotRuntime.free(idCharPtr)}}break;case"forward":case"backward":self.restart();break;default:}};this._source.addEventListener("ended",this._onended)}},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=window.innerWidth*scale;height=window.innerHeight*scale}const csw=`${width/scale}px`;const csh=`${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: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_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:method,headers:headers,body: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;const enc=new TextEncoder("utf-8");buffer=new Uint8Array(enc.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=Browser.requestAnimationFrame}Browser.requestAnimationFrame=enable?GodotWebXR.requestAnimationFrame:GodotWebXR.orig_requestAnimationFrame},pauseResumeMainLoop:()=>{Browser.mainLoop.pause();runtimeKeepalivePush();window.setTimeout(function(){runtimeKeepalivePop();Browser.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){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(){GodotWebXR.gl_binding=new XRWebGLBinding(session,gl);GodotWebXR.getLayer();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["requestFullscreen"]=Browser.requestFullscreen;Module["requestFullScreen"]=Browser.requestFullScreen;Module["requestAnimationFrame"]=Browser.requestAnimationFrame;Module["setCanvasSize"]=Browser.setCanvasSize;Module["pauseMainLoop"]=Browser.mainLoop.pause;Module["resumeMainLoop"]=Browser.mainLoop.resume;Module["getUserMedia"]=Browser.getUserMedia;Module["createContext"]=Browser.createContext;var preloadedImages={};var preloadedAudios={};var GLctx;for(var 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;function checkIncomingModuleAPI(){ignoredModuleProp("fetchSettings")}var wasmImports={__assert_fail:___assert_fail,__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_symlink:___syscall_symlink,__syscall_unlinkat:___syscall_unlinkat,_abort_js:__abort_js,_emscripten_get_now_is_monotonic:__emscripten_get_now_is_monotonic,_emscripten_runtime_keepalive_clear:__emscripten_runtime_keepalive_clear,_gmtime_js:__gmtime_js,_localtime_js:__localtime_js,_tzset_js:__tzset_js,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,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,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_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=createWasm();var ___wasm_call_ctors=createExportWrapper("__wasm_call_ctors",0);var _free=createExportWrapper("free",1);var __Z14godot_web_mainiPPc=Module["__Z14godot_web_mainiPPc"]=createExportWrapper("_Z14godot_web_mainiPPc",2);var _main=Module["_main"]=createExportWrapper("__main_argc_argv",2);var _malloc=createExportWrapper("malloc",1);var _fflush=createExportWrapper("fflush",1);var __emwebxr_on_input_event=Module["__emwebxr_on_input_event"]=createExportWrapper("_emwebxr_on_input_event",2);var __emwebxr_on_simple_event=Module["__emwebxr_on_simple_event"]=createExportWrapper("_emwebxr_on_simple_event",1);var _strerror=createExportWrapper("strerror",1);var ___funcs_on_exit=createExportWrapper("__funcs_on_exit",0);var _emscripten_stack_init=()=>(_emscripten_stack_init=wasmExports["emscripten_stack_init"])();var _emscripten_stack_get_free=()=>(_emscripten_stack_get_free=wasmExports["emscripten_stack_get_free"])();var _emscripten_stack_get_base=()=>(_emscripten_stack_get_base=wasmExports["emscripten_stack_get_base"])();var _emscripten_stack_get_end=()=>(_emscripten_stack_get_end=wasmExports["emscripten_stack_get_end"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"])();Module["callMain"]=callMain;Module["cwrap"]=cwrap;var missingLibrarySymbols=["writeI53ToI64Clamped","writeI53ToI64Signaling","writeI53ToU64Clamped","writeI53ToU64Signaling","convertI32PairToI53","convertI32PairToI53Checked","convertU32PairToI53","getTempRet0","setTempRet0","arraySum","addDays","inetPton4","inetNtop4","inetPton6","inetNtop6","readSockaddr","writeSockaddr","emscriptenLog","readEmAsmArgs","listenOnce","autoResumeAudioContext","getDynCaller","dynCall","setWasmTableEntry","asmjsMangle","HandleAllocator","getNativeTypeSize","STACK_SIZE","STACK_ALIGN","POINTER_SIZE","ASSERTIONS","uleb128Encode","sigToWasmTypes","generateFuncType","convertJsFunctionToWasm","getEmptyTableSlot","updateTableMap","getFunctionAddress","addFunction","removeFunction","reallyNegative","unSign","strLen","reSign","formatString","intArrayToString","AsciiToString","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","checkWasiClock","wasiRightsToMuslOFlags","wasiOFlagsToMuslOFlags","createDyncallWrapper","setImmediateWrapped","clearImmediateWrapped","polyfillSetImmediate","getPromise","makePromise","idsToPromises","makePromiseCallback","Browser_asyncPrepareDataCounter","getSocketFromFD","getSocketAddress","FS_unlink","FS_mkdirTree","_setNetworkCallback","emscriptenWebGLGetUniform","emscriptenWebGLGetVertexAttrib","__glGetActiveAttribOrUniform","writeGLArray","registerWebGlEventCallback","runAndAbortIfError","emscriptenWebGLGetIndexed","ALLOC_NORMAL","ALLOC_STACK","allocate","writeStringToMemory","writeAsciiToMemory","setErrNo","demangle","stackTrace"];missingLibrarySymbols.forEach(missingLibrarySymbol);var unexportedSymbols=["run","addOnPreRun","addOnInit","addOnPreMain","addOnExit","addOnPostRun","addRunDependency","removeRunDependency","out","err","abort","wasmMemory","wasmExports","writeStackCookie","checkStackCookie","writeI53ToI64","readI53FromI64","readI53FromU64","INT53_MAX","INT53_MIN","bigintToI53Checked","stackSave","stackRestore","stackAlloc","ptrToString","zeroMemory","exitJS","getHeapMax","growMemory","ENV","MONTH_DAYS_REGULAR","MONTH_DAYS_LEAP","MONTH_DAYS_REGULAR_CUMULATIVE","MONTH_DAYS_LEAP_CUMULATIVE","isLeapYear","ydayFromDate","ERRNO_CODES","strError","DNS","Protocols","Sockets","initRandomFill","randomFill","timers","warnOnce","readEmAsmArgsArray","jstoi_q","jstoi_s","getExecutableName","getWasmTableEntry","handleException","keepRuntimeAlive","runtimeKeepalivePush","runtimeKeepalivePop","callUserCallback","maybeExit","asyncLoad","alignMemory","mmapAlloc","wasmTable","noExitRuntime","getCFunc","ccall","freeTableIndexes","functionsInTableMap","setValue","getValue","PATH","PATH_FS","UTF8Decoder","UTF8ArrayToString","UTF8ToString","stringToUTF8Array","stringToUTF8","lengthBytesUTF8","intArrayFromString","stringToAscii","UTF16Decoder","stringToNewUTF8","stringToUTF8OnStack","writeArrayToMemory","JSEvents","specialHTMLTargets","maybeCStringToJsString","findEventTarget","findCanvasEventTarget","currentFullscreenStrategy","restoreOldWindowedStyle","UNWIND_CACHE","ExitStatus","getEnvStrings","doReadv","doWritev","safeSetTimeout","promiseMap","Browser","setMainLoop","getPreloadedImageData__data","wget","SYSCALLS","preloadPlugins","FS_createPreloadedFile","FS_modeStringToFlags","FS_getMode","FS_stdin_getChar_buffer","FS_stdin_getChar","FS_createPath","FS_createDevice","FS_readFile","FS","FS_createDataFile","FS_createLazyFile","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","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","GodotWebXR","GodotWebSocket","GodotRTCDataChannel","GodotRTCPeerConnection","GodotAudio","GodotAudioWorklet","GodotAudioScript","GodotDisplayVK","GodotDisplayCursor","GodotDisplayScreen","GodotDisplay","GodotFetch","GodotWebMidi","IDHandler","GodotConfig","GodotFS","GodotOS","GodotEventListeners","GodotPWA","GodotRuntime","GodotIME","GodotInputGamepads","GodotInputDragDrop","GodotInput","GodotWebGL2","GodotJSWrapper","IDBFS"];unexportedSymbols.forEach(unexportedRuntimeSymbol);var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function callMain(args=[]){assert(runDependencies==0,'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])');assert(__ATPRERUN__.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){return}stackCheckInit();preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();if(shouldRunNow)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}checkStackCookie()}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=false;if(Module["noInitialRun"])shouldRunNow=false;run();moduleRtn=readyPromise;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; +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: [], + /** + * 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.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), + '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, + '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/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part000 b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part000 new file mode 100644 index 00000000..bc3b9f4e Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part000 differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part001 b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part001 new file mode 100644 index 00000000..c06fc42c Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part001 differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part002 b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part002 new file mode 100644 index 00000000..c8001b9e Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part002 differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part003 b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part003 new file mode 100644 index 00000000..20f29c66 Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.pck.gz.part003 differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.png b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.png new file mode 100644 index 00000000..766b0b66 Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.png differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.wasm.gz b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.wasm.gz new file mode 100644 index 00000000..5295f3a9 Binary files /dev/null and b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/game.wasm.gz differ diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/stream.js b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/stream.js new file mode 100644 index 00000000..6df9204f --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/game/stream.js @@ -0,0 +1,36 @@ +// Godot Web 不使用原生 TCP;fetch 流和 TextDecoder 保留 SSE 的 UTF-8 字符边界。 +(() => { + const streams = new Map(); + window.AIOfficeStream = { + open(request, url, body, callback) { + const controller = new AbortController(); + streams.set(request, controller); + const emit = packet => { + if (streams.get(request) === controller) callback(JSON.stringify({ request, ...packet })); + }; + (async () => { + if (url !== window.location.origin + '/api/chat') throw new Error('invalid_route'); + const response = await fetch(url, { method:'POST', headers:{ 'Content-Type':'application/json' }, + body, signal:controller.signal, credentials:'same-origin' }); + if (!response.headers.get('content-type')?.includes('text/event-stream')) { + const data = await response.json(); + const who = JSON.parse(body).who; + emit({ data:'data: ' + JSON.stringify(data.ok ? { type:'delta', who, text:data.msg } : + { type:'error', who, message:data.msg || '这次没连上,稍后再试' }) + '\n\n' }); + emit({ end:true }); return; + } + const reader = response.body.getReader(), decoder = new TextDecoder(); + try { + while (true) { + const part = await reader.read(); + if (part.done) break; + emit({ data:decoder.decode(part.value, { stream:true }) }); + } + emit({ data:decoder.decode(), end:true }); + } finally { reader.releaseLock(); } + })().catch(error => { if (error.name !== 'AbortError') emit({ error:true }); }) + .finally(() => { if (streams.get(request) === controller) streams.delete(request); }); + }, + cancel(request) { streams.get(request)?.abort(); streams.delete(request); } + }; +})(); diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/client/index.html b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/index.html new file mode 100644 index 00000000..621966bb --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/client/index.html @@ -0,0 +1,62 @@ + + + +AI 事务所 + + +
+

AI 事务所

+

第一次玩?复制下面的引导提示词,发给 MiniMax Code 的 AI。它会带你选择员工模型、申请或填写 Key,并引导配置 Jev 自主决策。

+
  • MiniMax
  • LongCat
  • Kimi
  • StepFun
  • Jev · OpenRouter(可选)
+没有 Key 也可以从引导开始。至少启用一家员工模型;Jev 使用 OpenRouter Key,可选择启用或跳过。配置一次,以后直接进入游戏。 + + +
+

+

等待 AI 完成配置,保存后会自动进入游戏。

+
+ + + diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/miniapp.json b/plugins/cristinamadureira353-svg/ai-office/miniapp/miniapp.json new file mode 100644 index 00000000..664066b1 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/miniapp.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "artifacts": { + "client": ["./miniapp/client"], + "node": ["./miniapp/node"] + }, + "runtime": { + "kind": "process", + "entry": "./miniapp/node/server.mjs", + "lifecycle": "on-demand" + }, + "surface": { "path": "/ai-office" }, + "mcpEndpoints": [] +} diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/assets.json b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/assets.json new file mode 100644 index 00000000..72946ded --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/assets.json @@ -0,0 +1,64 @@ +{ + "/game/game.apple-touch-icon.png": { + "type": "image/png", + "bytes": 11952, + "file": "game/game.apple-touch-icon.png" + }, + "/game/game.audio.position.worklet.js": { + "type": "text/javascript", + "bytes": 3149, + "file": "game/game.audio.position.worklet.js" + }, + "/game/game.audio.worklet.js": { + "type": "text/javascript", + "bytes": 7298, + "file": "game/game.audio.worklet.js" + }, + "/game/game.html": { + "type": "text/html", + "bytes": 5429, + "file": "game/game.html" + }, + "/game/game.icon.png": { + "type": "image/png", + "bytes": 5702, + "file": "game/game.icon.png" + }, + "/game/game.js": { + "type": "text/javascript", + "bytes": 317142, + "file": "game/game.js" + }, + "/game/game.pck": { + "type": "application/octet-stream", + "bytes": 32071564, + "gzip": true, + "parts": [ + "game/game.pck.gz.part000", + "game/game.pck.gz.part001", + "game/game.pck.gz.part002", + "game/game.pck.gz.part003" + ] + }, + "/game/game.png": { + "type": "image/png", + "bytes": 21443, + "file": "game/game.png" + }, + "/game/game.wasm": { + "type": "application/wasm", + "bytes": 9387768, + "file": "game/game.wasm.gz", + "gzip": true + }, + "/game/stream.js": { + "type": "text/javascript", + "bytes": 1801, + "file": "game/stream.js" + }, + "/index.html": { + "type": "text/html", + "bytes": 4906, + "file": "index.html" + } +} \ No newline at end of file diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/data/office_layout_v2.json b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/data/office_layout_v2.json new file mode 100644 index 00000000..39378ca8 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/data/office_layout_v2.json @@ -0,0 +1,1217 @@ +{ + "version": 2, + "cell_px": 32, + "grid": { + "w": 40, + "h": 24 + }, + "zones": { + "lobby": { + "rect": [ + 1, + 0, + 12, + 23 + ], + "floor": "floor_oak" + }, + "office": { + "rect": [ + 14, + 0, + 26, + 23 + ], + "floor": "floor_oak" + }, + "meeting": { + "rect": [ + 28, + 0, + 38, + 16 + ], + "floor": "floor_walnut" + } + }, + "floor_patches": [ + { + "prop": "floor_stone", + "rect": [ + 22, + 2.5, + 4.5, + 4.5 + ] + }, + { + "prop": "floor_carpet", + "rect": [ + 2, + 15, + 6.7, + 7.4 + ] + }, + { + "prop": "floor_carpet", + "rect": [ + 15.7, + 18.5, + 10.5, + 4.1 + ] + } + ], + "labels": [ + { + "label": "大堂 · 接待", + "x": 6.5, + "y": 2.1 + }, + { + "label": "五人办公区", + "x": 17.5, + "y": 6 + }, + { + "label": "茶水间", + "x": 24, + "y": 7.2 + }, + { + "label": "休息角", + "x": 19, + "y": 16.9 + }, + { + "label": "会议室", + "x": 33, + "y": 14.5 + } + ], + "desks": { + "m3": [ + 16, + 10 + ], + "flash": [ + 20, + 10 + ], + "longcat": [ + 24, + 10 + ], + "kimi": [ + 16, + 14 + ], + "step5": [ + 20, + 14 + ] + }, + "props": [ + { + "prop": "reception_counter", + "x": 7, + "y": 7.4, + "width": 212, + "zone": "lobby", + "blocked_cells": [ + [ + 4, + 6 + ], + [ + 4, + 7 + ], + [ + 5, + 6 + ], + [ + 5, + 7 + ], + [ + 6, + 6 + ], + [ + 6, + 7 + ], + [ + 7, + 6 + ], + [ + 7, + 7 + ], + [ + 8, + 6 + ], + [ + 8, + 7 + ], + [ + 9, + 6 + ], + [ + 9, + 7 + ], + [ + 10, + 6 + ], + [ + 10, + 7 + ] + ] + }, + { + "prop": "visitor_sofa", + "x": 5, + "y": 18, + "width": 183, + "zone": "lobby", + "blocked_cells": [ + [ + 3, + 16 + ], + [ + 3, + 17 + ], + [ + 4, + 16 + ], + [ + 4, + 17 + ], + [ + 5, + 16 + ], + [ + 5, + 17 + ], + [ + 6, + 16 + ], + [ + 6, + 17 + ], + [ + 7, + 16 + ], + [ + 7, + 17 + ] + ] + }, + { + "prop": "visitor_table", + "x": 5, + "y": 20.8, + "width": 118, + "zone": "lobby", + "blocked_cells": [ + [ + 4, + 19 + ], + [ + 4, + 20 + ], + [ + 5, + 19 + ], + [ + 5, + 20 + ], + [ + 6, + 19 + ], + [ + 6, + 20 + ] + ] + }, + { + "prop": "notice_board", + "x": 8.5, + "y": 3.8, + "width": 119, + "zone": "lobby", + "blocked_cells": [] + }, + { + "prop": "coat_stand", + "x": 11.4, + "y": 8, + "width": 36, + "zone": "lobby", + "blocked_cells": [ + [ + 11, + 7 + ], + [ + 11, + 8 + ] + ] + }, + { + "prop": "entrance_mat", + "x": 1.9, + "y": 12.5, + "width": 72, + "zone": "lobby", + "blocked_cells": [], + "layer": "floor" + }, + { + "prop": "door_open", + "x": 0.4, + "y": 12.5, + "width": 49, + "zone": "lobby", + "blocked_cells": [], + "layer": "floor" + }, + { + "prop": "meeting_plant", + "x": 10.8, + "y": 21.5, + "width": 64, + "zone": "lobby", + "blocked_cells": [ + [ + 10, + 20 + ], + [ + 11, + 20 + ], + [ + 11, + 21 + ] + ] + }, + { + "prop": "framed_picture", + "x": 11.2, + "y": 4.2, + "width": 58, + "zone": "lobby", + "blocked_cells": [] + }, + { + "prop": "wall_clock", + "x": 10.5, + "y": 1.5, + "width": 35, + "zone": "lobby", + "blocked_cells": [] + }, + { + "prop": "player_boss", + "x": 7, + "y": 5.2, + "width": 33, + "zone": "lobby", + "blocked_cells": [], + "category": "actor" + }, + { + "prop": "complete_desk", + "x": 16, + "y": 10.100000000000001, + "width": 113, + "zone": "office", + "blocked_cells": [ + [ + 15, + 8 + ], + [ + 15, + 9 + ], + [ + 16, + 8 + ], + [ + 16, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 9 + ] + ], + "worker": "m3", + "role": "work_desk", + "sort_y": 9.100000000000001 + }, + { + "prop": "office_chair_back", + "x": 16, + "y": 10.7, + "width": 36, + "zone": "office", + "blocked_cells": [], + "worker": "m3" + }, + { + "prop": "complete_desk", + "x": 20, + "y": 10.100000000000001, + "width": 113, + "zone": "office", + "blocked_cells": [ + [ + 19, + 8 + ], + [ + 19, + 9 + ], + [ + 20, + 8 + ], + [ + 20, + 9 + ], + [ + 21, + 8 + ], + [ + 21, + 9 + ] + ], + "worker": "flash", + "role": "work_desk", + "sort_y": 9.100000000000001 + }, + { + "prop": "office_chair_back", + "x": 20, + "y": 10.7, + "width": 36, + "zone": "office", + "blocked_cells": [], + "worker": "flash" + }, + { + "prop": "complete_desk", + "x": 24, + "y": 10.100000000000001, + "width": 113, + "zone": "office", + "blocked_cells": [ + [ + 23, + 8 + ], + [ + 23, + 9 + ], + [ + 24, + 8 + ], + [ + 24, + 9 + ], + [ + 25, + 8 + ], + [ + 25, + 9 + ] + ], + "worker": "longcat", + "role": "work_desk", + "sort_y": 9.100000000000001 + }, + { + "prop": "office_chair_back", + "x": 24, + "y": 10.7, + "width": 36, + "zone": "office", + "blocked_cells": [], + "worker": "longcat" + }, + { + "prop": "complete_desk", + "x": 16, + "y": 14.100000000000001, + "width": 113, + "zone": "office", + "blocked_cells": [ + [ + 15, + 12 + ], + [ + 15, + 13 + ], + [ + 16, + 12 + ], + [ + 16, + 13 + ], + [ + 17, + 12 + ], + [ + 17, + 13 + ] + ], + "worker": "kimi", + "role": "work_desk", + "sort_y": 13.100000000000001 + }, + { + "prop": "office_chair_back", + "x": 16, + "y": 14.7, + "width": 36, + "zone": "office", + "blocked_cells": [], + "worker": "kimi" + }, + { + "prop": "complete_desk", + "x": 20, + "y": 14.100000000000001, + "width": 113, + "zone": "office", + "blocked_cells": [ + [ + 19, + 12 + ], + [ + 19, + 13 + ], + [ + 20, + 12 + ], + [ + 20, + 13 + ], + [ + 21, + 12 + ], + [ + 21, + 13 + ] + ], + "worker": "step5", + "role": "work_desk", + "sort_y": 13.100000000000001 + }, + { + "prop": "office_chair_back", + "x": 20, + "y": 14.7, + "width": 36, + "zone": "office", + "blocked_cells": [], + "worker": "step5" + }, + { + "prop": "printer_cabinet", + "x": 15.1, + "y": 4.7, + "width": 63, + "zone": "office", + "blocked_cells": [ + [ + 15, + 4 + ] + ] + }, + { + "prop": "server_rack", + "x": 19.1, + "y": 4.8, + "width": 46, + "zone": "office", + "blocked_cells": [ + [ + 19, + 4 + ] + ] + }, + { + "prop": "filing_cabinet", + "x": 14.8, + "y": 21.6, + "width": 53, + "zone": "office", + "blocked_cells": [ + [ + 14, + 20 + ], + [ + 15, + 20 + ], + [ + 15, + 21 + ] + ] + }, + { + "prop": "fern", + "x": 21.3, + "y": 5.3, + "width": 45, + "zone": "office", + "blocked_cells": [ + [ + 21, + 5 + ] + ] + }, + { + "prop": "wall_clock", + "x": 20.5, + "y": 1.5, + "width": 35, + "zone": "office", + "blocked_cells": [] + }, + { + "prop": "waste_bin", + "x": 26.1, + "y": 15.8, + "width": 22, + "zone": "office", + "blocked_cells": [ + [ + 26, + 15 + ] + ] + }, + { + "prop": "archive_boxes", + "x": 14.8, + "y": 22.4, + "width": 39, + "zone": "office", + "blocked_cells": [ + [ + 14, + 22 + ], + [ + 15, + 22 + ] + ] + }, + { + "prop": "tea_shelf", + "x": 24.3, + "y": 3.2, + "width": 100, + "zone": "tea", + "blocked_cells": [] + }, + { + "prop": "kitchenette", + "x": 24.1, + "y": 5.4, + "width": 141, + "zone": "tea", + "blocked_cells": [ + [ + 22, + 4 + ], + [ + 22, + 5 + ], + [ + 23, + 4 + ], + [ + 23, + 5 + ], + [ + 24, + 4 + ], + [ + 24, + 5 + ], + [ + 25, + 4 + ], + [ + 25, + 5 + ] + ] + }, + { + "prop": "water_dispenser", + "x": 22.1, + "y": 6.3, + "width": 27, + "zone": "tea", + "blocked_cells": [ + [ + 22, + 6 + ] + ] + }, + { + "prop": "fridge", + "x": 26.1, + "y": 5.8, + "width": 35, + "zone": "tea", + "blocked_cells": [ + [ + 26, + 5 + ] + ] + }, + { + "prop": "rust_sofa", + "x": 18.8, + "y": 21.8, + "width": 143, + "zone": "leisure", + "blocked_cells": [ + [ + 17, + 20 + ], + [ + 18, + 20 + ], + [ + 18, + 21 + ], + [ + 19, + 20 + ], + [ + 19, + 21 + ], + [ + 20, + 20 + ] + ], + "role": "rest_seat", + "seat_y": 21, + "seats": [ + {"worker": "flash", "approach": [17, 21], "contact_offset": [-0.8, -1.0]}, + {"worker": "longcat", "approach": [20, 21], "contact_offset": [0.9, -1.0]} + ] + }, + { + "prop": "round_tea_table", + "x": 18.8, + "y": 22.4, + "width": 62, + "zone": "leisure", + "blocked_cells": [ + [ + 18, + 22 + ], + [ + 19, + 22 + ] + ] + }, + { + "prop": "green_armchair", + "x": 22.6, + "y": 21.9, + "width": 55, + "zone": "leisure", + "blocked_cells": [ + [ + 22, + 21 + ], + [ + 23, + 22 + ] + ], + "role": "rest_seat", + "seat_y": 21, + "seats": [ + {"worker": "step5", "approach": [23, 21], "contact_offset": [0.0, -0.7]} + ] + }, + { + "prop": "aquarium_cabinet", + "x": 25.1, + "y": 19.6, + "width": 73, + "zone": "leisure", + "blocked_cells": [ + [ + 25, + 19 + ] + ] + }, + { + "prop": "bookcase", + "x": 25.7, + "y": 22.4, + "width": 54, + "zone": "leisure", + "blocked_cells": [ + [ + 25, + 21 + ], + [ + 26, + 21 + ], + [ + 25, + 22 + ], + [ + 26, + 22 + ] + ] + }, + { + "prop": "floor_lamp", + "x": 22.3, + "y": 19.7, + "width": 28, + "zone": "leisure", + "blocked_cells": [ + [ + 22, + 19 + ] + ] + }, + { + "prop": "meeting_table", + "x": 33, + "y": 10.3, + "width": 255, + "zone": "meeting", + "blocked_cells": [ + [ + 30, + 8 + ], + [ + 30, + 9 + ], + [ + 30, + 10 + ], + [ + 31, + 8 + ], + [ + 31, + 9 + ], + [ + 31, + 10 + ], + [ + 32, + 8 + ], + [ + 32, + 9 + ], + [ + 32, + 10 + ], + [ + 33, + 8 + ], + [ + 33, + 9 + ], + [ + 33, + 10 + ], + [ + 34, + 8 + ], + [ + 34, + 9 + ], + [ + 34, + 10 + ], + [ + 35, + 8 + ], + [ + 35, + 9 + ], + [ + 35, + 10 + ], + [ + 36, + 8 + ], + [ + 36, + 9 + ], + [ + 36, + 10 + ] + ], + "role": "table" + }, + { + "prop": "meeting_chair_front", + "x": 31, + "y": 7.6, + "width": 48, + "zone": "meeting", + "blocked_cells": [], + "role": "seat", + "worker": "m3" + }, + { + "prop": "meeting_chair_front", + "x": 33, + "y": 7.6, + "width": 48, + "zone": "meeting", + "blocked_cells": [], + "role": "seat", + "worker": "flash" + }, + { + "prop": "meeting_chair_front", + "x": 35, + "y": 7.6, + "width": 48, + "zone": "meeting", + "blocked_cells": [], + "role": "seat", + "worker": "kimi" + }, + { + "prop": "meeting_chair_back", + "x": 32, + "y": 11.6, + "width": 48, + "zone": "meeting", + "blocked_cells": [], + "role": "seat", + "worker": "longcat" + }, + { + "prop": "meeting_chair_back", + "x": 34, + "y": 11.6, + "width": 48, + "zone": "meeting", + "blocked_cells": [], + "role": "seat", + "worker": "step5" + }, + { + "prop": "projection_screen", + "x": 35.2, + "y": 4.6, + "width": 151, + "zone": "meeting", + "blocked_cells": [] + }, + { + "prop": "whiteboard", + "x": 30.8, + "y": 5.8, + "width": 125, + "zone": "meeting", + "blocked_cells": [ + [ + 30, + 5 + ], + [ + 31, + 5 + ] + ] + }, + { + "prop": "meeting_plant", + "x": 37.4, + "y": 7.7, + "width": 64, + "zone": "meeting", + "blocked_cells": [ + [ + 37, + 7 + ] + ] + }, + { + "prop": "wall_clock", + "x": 37.3, + "y": 1.5, + "width": 35, + "zone": "meeting", + "blocked_cells": [] + }, + { + "prop": "waste_bin", + "x": 37.5, + "y": 15.6, + "width": 23, + "zone": "meeting", + "blocked_cells": [ + [ + 37, + 15 + ] + ] + }, + { + "prop": "bookcase", + "x": 36.8, + "y": 21.8, + "width": 64, + "zone": "meeting", + "blocked_cells": [ + [ + 36, + 20 + ], + [ + 37, + 20 + ], + [ + 36, + 21 + ], + [ + 37, + 21 + ] + ] + }, + { + "prop": "green_armchair", + "x": 30.2, + "y": 21.7, + "width": 63, + "zone": "meeting", + "blocked_cells": [ + [ + 30, + 20 + ], + [ + 30, + 21 + ] + ] + }, + { + "prop": "standing_table", + "x": 34, + "y": 21.8, + "width": 57, + "zone": "meeting", + "blocked_cells": [ + [ + 34, + 21 + ] + ] + }, + { + "prop": "fern", + "x": 28.9, + "y": 22.3, + "width": 44, + "zone": "meeting", + "blocked_cells": [ + [ + 28, + 22 + ], + [ + 29, + 22 + ] + ] + } + ], + "notes": "家具宽度为世界像素,x/y 为脚底网格坐标;blocked_cells 同时供后端和 Godot 寻路。由 tools/scene_layout.py 同步。", + "art_layout_version": 3, + "entrance": { + "cell": [ + 0, + 12 + ], + "wall_opening": [ + [ + 0, + 11 + ], + [ + 0, + 12 + ] + ], + "queue_cells": [ + [ + 8, + 8 + ], + [ + 8, + 10 + ], + [ + 8, + 12 + ], + [ + 8, + 14 + ], + [ + 8, + 16 + ] + ], + "staff_cells": [ + [ + 10, + 8 + ], + [ + 10, + 10 + ], + [ + 10, + 12 + ], + [ + 10, + 14 + ], + [ + 10, + 16 + ] + ] + } +} diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/banter.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/banter.py new file mode 100644 index 00000000..406f6c96 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/banter.py @@ -0,0 +1,150 @@ +"""路过吐槽机制(M2-5) + +一个 AI 干活时,另一个路过,阴阳怪气两句。干活者可能反击。 + +与抢单冲突的区别: +- 抢单冲突:为了**争利益**,有明确的对立对象,必须裁决 +- 路过吐槽:只是**日常摩擦**,不需要玩家介入,但会悄悄改关系和心情 + +触发条件(三条全满足): + 1. 有人在干活(job != None) + 2. 有另一个人在附近(距离 ≤ NEAR_CELLS) + 3. Jev 判定"要不要开口"——这一步保证吐槽不是每对都发生 + +对话最多 MAX_ROUNDS 轮,避免成本失控。 +""" +import os +import time + +import providers as P +import scheduler as S +import relations as REL + +NEAR_CELLS = float(os.environ.get("GAME_BANTER_NEAR", "4.5")) +RATE_MUL = float(os.environ.get("GAME_BANTER_MUL", "1.0")) +COOLDOWN = float(os.environ.get("GAME_BANTER_CD", "100")) # 同一对 +COOLDOWN_WORKER = float(os.environ.get("GAME_BANTER_CD_WORKER", "60")) # 同一个干活的人 +MAX_ROUNDS = 3 + +STORE = {} # 冷却与防抖 + + +def _pair_ok(a, b, store): + key = "|".join(sorted([a, b])) + return (time.time() - store.get(("p", key), 0)) >= COOLDOWN + + +def _worker_ok(worker, store): + return (time.time() - store.get(("w", worker), 0)) >= COOLDOWN_WORKER + + +def mark(store, a, b, worker): + store[("p", "|".join(sorted([a, b])))] = time.time() + store[("w", worker)] = time.time() + + +def nearby_idle(worker, others, pos_fn): + """找出在附近、且没在忙的人。""" + out = [] + for o in others: + if o.who == worker.who or o.job or getattr(o, "seat_cmd", False) or getattr(o, "brain_down", False): + continue + d = _dist(pos_fn(worker), pos_fn(o)) + if d <= NEAR_CELLS: + out.append((o, d)) + return out + + +def _dist(a, b): + return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 + + +def should_speak(worker_who, passer_who, worker_mood, passer_mood, rel, store): + """要不要开口?心情差 + 关系差 概率高;好感度高概率低。""" + if not _pair_ok(worker_who, passer_who, store) or not _worker_ok(worker_who, store): + return False + p = 0.30 + p -= min(0.18, max(0.0, rel) * 0.002) # 关系好 → 少说 + p += min(0.25, max(0, 40 - worker_mood) * 0.008) # 干活的人心情差 → 爱抱怨 + p -= min(0.10, max(0, 60 - passer_mood) * 0.002) # 路过的人心情好 → 懒得挑事 + p = min(0.8, max(0.02, p * RATE_MUL)) + import random + return random.random() < p + + +# ── 台词 ────────────────────────────────────────────────────────────────── +def system(who: str) -> str: + p = P.PROVIDERS[who] + return ( + f"你是{p['name']}({p['full']}),人设「{p['tag']}」:{p['blurb']}。\n" + f"【硬限制】最多 2 句话,总共不超过 55 字。像真人随口说的,别写书面语。" + ) + + +def line(who: str, msg: str, stream=None) -> str: + out = "" + # ★ 额度按模型分配(providers.chat_max_tokens):M3 思考占其输出 98%, + # 一刀切 500 会让它思考完就没额度写正文。 + for ev in (stream or P.stream_chat)(who, msg, system=system(who), + max_completion_tokens=P.chat_max_tokens(who)): + if ev.get("type") == "delta": + out += ev["text"] + elif ev.get("type") == "error": + return "" + return P.truncate(out.strip(), 75) + + +def build_dialog(worker_who, passer_who, task, retaliate, store, tick=0, stream=None): + """生成 1~MAX_ROUNDS 轮。retaliate 决定干活的人要不要反击。""" + task_short = (task or "这活")[:14] + msgs = [] + # 第 1 轮:路过者开口 + a = line(passer_who, + f"同事正在接「{task_short}」,你路过他工位。随口说一句带点刺的话,不超过 2 句。", stream=stream) + if not a: + return [] + msgs.append({"who": passer_who, "text": a, "kind": "poke"}) + if not retaliate or not msgs: + return msgs + # 第 2 轮:干活的人反击 + b = line(worker_who, f"有人刚阴阳怪气地说:「{a[:34]}」。你回一句,别怂。", stream=stream) + if not b: + return msgs + msgs.append({"who": worker_who, "text": b, "kind": "retort"}) + if len(msgs) < MAX_ROUNDS: + # 第 3 轮:路过者再补一刀(或认怂) + c = line(passer_who, f"对方回你:「{b[:34]}」。你再接一句,收尾。", stream=stream) + if c: + msgs.append({"who": passer_who, "text": c, "kind": "poke2"}) + mark(store, worker_who, passer_who, worker_who) + return msgs + + +def settle(msgs, worker, passer, tick=0): + """结算:被吐槽的(干活的)心情掉;反击成功则吐槽者也掉;关系双向变化。 + + ★ worker/passer 必须显式传入,不能从 msgs[0] 猜: + 只有 1 轮台词时 msgs[0] 就是 passer 本身, + 拿它当 victim 等于让 A 吐槽 A,`change()` 的 `a == b` 守卫会静默返回 0。 + """ + out = {"mood": {}, "rel": []} + if not msgs: + return out + retaliator = None + for m in msgs[1:]: + if m["kind"] == "retort": + retaliator = m["who"] + break + + # worker 被戳,passer 嘴欠 + if worker != passer: + out["mood"][worker] = out["mood"].get(worker, 0) - 3 + out["rel"].append((worker, passer, "banter_victim", tick)) + if retaliator and retaliator == worker: + out["mood"][passer] = out["mood"].get(passer, 0) - 2 + out["rel"].append((passer, worker, "banter_retaliate", tick)) + for who, other, ev, tk in out["rel"]: + if who == other: + raise ValueError("关系目标不能是自己:%s" % who) + REL.bump(ev, who, other, tick=tk) + return out diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/blame_game.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/blame_game.py new file mode 100644 index 00000000..89bfaf1e --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/blame_game.py @@ -0,0 +1,178 @@ +"""甩锅复盘机制(M2-6) + +一单干砸了,全员开会互相推责任。老板(玩家)可以中途插话,插话会进入后续发言的上下文。 + +和 M2-4 抢单冲突、M2-5 路过吐槽的区别: +- 抢单冲突:为争利益,有明确对立对象,必须裁决 +- 路过吐槽:日常摩擦,不需要玩家介入 +- 甩锅复盘:**出了事故**,责任归属是会议的核心议题,必须有人被点名 + +三条设计要点: +1. 发言必须**串行**且带上前文,否则五个人各说各话,就不是会而是五次独白 +2. "被指责"必须结构化,不能从自由文本里猜名字 —— 约定输出格式再解析 +3. 老板插话和 AI 发言进同一条 history,谁先说谁后说对后面的影响是一样的 +""" +import re + +import providers as P +import relations as REL + +# 输出格式:两行,解析失败也不崩(退化成"没点名") +LINE_RE = re.compile(r"台词[::]\s*(.+)") +BLAME_RE = re.compile(r"甩锅给[::]\s*(.+)") + +NAMES = {"m3": "M3", "flash": "Flash", "longcat": "LongCat", + "kimi": "Kimi", "step5": "Step5"} + +MAX_TEXT = 60 # 单句台词上限 +MAX_HISTORY_CHARS = 90 # 注入上下文时每条历史截断 + + +# ── 触发条件 ──────────────────────────────────────────────────────────────── +# 失败单由 game 维护(self.failures),不在这里反推: +# 老板打回的那单在 clients 里 status 是"已完成",事后根本查不出来, +# 所以失败必须在**发生的那一刻**就记下来,不能等结算时再数。 + + +def should_trigger(failures, day, store): + """有失败单 + 不是今天已经开过的复盘会,才触发。冷却一天。""" + if not failures: + return False + return day > store.get("last_blame_day", -1) + + +# ── 会议开场 ──────────────────────────────────────────────────────────────── +def brief(failures, day, workers): + """给 LLM 的案情说明。写得具体,模型才说得具体。""" + lines = ["第 %d 天,出事了。你们五个要开一个复盘会,说清楚这单是怎么砸的。" % day] + for f in failures: + who = f.get("who") + nm = NAMES.get(who, who or "没人接") + lines.append("失手单:「%s」——%s(当时负责:%s)" + % (f["task"], f["why"], nm)) + lines.append("在座:" + "、".join(NAMES[w] for w in workers)) + lines.append("每个人都要发言,要针对前面说过的话,别各说各话。") + return "\n".join(lines) + + +def system(who: str) -> str: + p = P.PROVIDERS[who] + return ( + f"你是{p['name']}({p['full']}),人设「{p['tag']}」:{p['blurb']}。\n" + f"你在一个复盘会上。{p['name']}此刻的立场由你自己决定:可以认领责任、" + f"可以甩锅、可以阴阳怪气,但要像真人开会,不要客套。\n" + f"【输出格式:只写下面这两行,别写别的】\n" + f"台词:(你的话,1~2 句,总共不超过 40 字)\n" + f"甩锅给:(你要点名的同事,用英文小写 id,如 m3、flash;不甩就写 无)\n" + f"——照这个样子写——\n" + f"台词:这锅我不背,需求单子上就没写清楚验收标准。\n" + f"甩锅给:m3" + ) + + +# ── 发言 ──────────────────────────────────────────────────────────────────── +def parse(raw: str): + """把模型输出拆成 (台词, 点名列表)。格式没按规矩来也不崩。 + + ★ 兜底很重要:实测 flash/kimi 会直接写一句自然话,不带"甩锅给:"。 + 如果只认显式格式,"被指责最多的人"这个结算就几乎永远算不出来, + 功能会静默退化成"每次都没人背锅"。所以认不出显式标记时, + 退化成"这段话里点了谁的名"——会上的点名九成都是推责。 + 但要把两条路径区分开记下来,否则兜底会掩盖"格式从来没被遵守过"。 + """ + raw = P.truncate((raw or "").strip(), 200) + m = LINE_RE.search(raw) + if m: + text = m.group(1).strip() + else: + text = raw.splitlines()[0].strip() if raw else "" + text = re.sub(r"^甩锅给[::].*$", "", text).strip() + bl = BLAME_RE.search(raw) + blamed, explicit = [], False + if bl: + explicit = True + seg = bl.group(1) + if "无" not in seg and "不甩" not in seg and "自己" not in seg: + for k in NAMES: + if NAMES[k] in seg or k in seg.lower(): + blamed.append(k) + return P.truncate(text, MAX_TEXT), blamed, explicit + + +def _history_text(lines): + """把已有发言(含老板插话)压成可注入的历史。 + + ★ 只截断、不裁条数:谁先说谁后说对后面的影响是一样的, + 按"最近 N 条"裁会让早先的定性发言消失,模型就跟着跑偏。 + """ + out = [] + for ln in lines: + who = ln.get("who", "?") + nm = "老板" if who == "boss" else NAMES.get(who, who) + txt = (ln.get("text") or "")[:MAX_HISTORY_CHARS] + if not txt: + continue + role = "user" if who == "boss" else "assistant" + out.append({"role": role, "content": f"{nm}:{txt}"}) + return out + + +def speak(who, failures, day, workers, lines, first=False, retries=1, stream=None): + """一个人发言。lines 是**已经说过的**发言(含老板插话),会带进上下文。 + + 空稿**加码重试**,不是同额度重试:思考 token 和正文抢同一个额度, + 带历史时思考量明显变大,额度不够的表现是 `done` 但**一个 delta 都没有** + (实测 step5 带 4 段历史、900 额度返回 0 字,而且不报错)。 + 同额度重试多少次都没用——必须把额度加上去。 + """ + hist = _history_text(lines) + if first: + ask = brief(failures, day, workers) + "\n你先说。其他人还没开口。" + else: + prev = "、".join(NAMES.get(l.get("who"), l.get("who", "")) for l in lines[-3:]) + ask = ("接着说。前面说过的人:%s。\n" + "针对他们的话接,别重复你已经说过的。\n" + "你可以点名反驳他们,也可以顺势把锅推给在场任何人。" % prev) + budget = P.chat_max_tokens(who) + for _ in range(retries + 1): + raw = "" + for ev in (stream or P.stream_chat)(who, ask, system=system(who), history=hist or None, + max_completion_tokens=budget): + if ev.get("type") == "delta": + raw += ev["text"] + elif ev.get("type") == "error": + return None + if raw.strip(): + text, blamed, explicit = parse(raw) + if text: + if not explicit: + # 格式没遵守:从台词里找点名的人(不含自己) + for k, nm in NAMES.items(): + if k != who and (nm in text or k in text.lower()): + if k not in blamed: + blamed.append(k) + return {"who": who, "text": text, "blames": blamed, "fmt": explicit} + budget = min(budget * 2, 6000) + return None + + +# ── 结算 ──────────────────────────────────────────────────────────────────── +def tally(lines): + """数谁被点名最多。只数同事之间的甩锅,不数老板说的。""" + cnt = {w: 0 for w in REL.ALL} + for ln in lines: + if ln.get("who") == "boss": + continue + for b in (ln.get("blames") or []): + if b in cnt and b != ln.get("who"): + cnt[b] += 1 + return cnt + + +def settle(lines, tick=0): + """被指责最多的那个心情 -6。没人被点名就不罚。""" + cnt = tally(lines) + if not any(cnt.values()): + return {"culprit": None, "mood": {}, "count": cnt} + culprit = max(cnt, key=lambda k: cnt[k]) + return {"culprit": culprit, "mood": {culprit: -6}, "count": cnt} diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/conflict.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/conflict.py new file mode 100644 index 00000000..6510f16a --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/conflict.py @@ -0,0 +1,147 @@ +"""抢单冲突机制(M2-4) + +两个 AI 同时判定要接同一个客户 → 当面对线 → 玩家裁决。 + +三条设计原则: +1. 台词必须**现场生成**,不是模板。B 要能看到 A 说了什么再反驳,这样才有对抗感。 +2. 冲突要有**倾向**不是纯随机:心情越差越不想抢、关系越差越容易对撞、报酬越高越抢手。 +3. 冷却:同一对员工短时间不重复冲突,避免死循环。 +""" +import os +import random +import time + +import providers as P +import scheduler as S +import relations as REL + +# 冲突率倍率:正式局 1.0,测试/演示可调高。这是**游戏参数**,不是测试后门—— +# 冲突太罕见会让核心玩法看不到,倍率让"这场局冲突多不多"可配置。 +RATE_MUL = float(os.environ.get("GAME_CONFLICT_MUL", "1.0")) + +COOLDOWN_SEC = float(os.environ.get("GAME_CF_COOLDOWN", "150")) # 同一对员工的对撞冷却 +COOLDOWN_CLIENT = 90.0 # 同一客户只冲突一次 +COOLDOWN_GLOBAL = 20.0 # 全局最小间隔,避免连发 + +JUDGE_CHOICES = ("a", "b", "reject", "scold") + + +def _pair_cooldown_ok(a, b, store): + key = "|".join(sorted([a, b])) + last = store.get(("pair", key), 0) + return (time.time() - last) >= COOLDOWN_SEC + + +def _client_ok(cid, store): + return (time.time() - store.get(("client", cid), 0)) >= COOLDOWN_CLIENT + + +def _global_ok(store): + return (time.time() - store.get(("global", "_"), 0)) >= COOLDOWN_GLOBAL + + +def mark(store, a, b, cid): + store[("pair", "|".join(sorted([a, b])))] = time.time() + store[("client", cid)] = time.time() + store[("global", "_")] = time.time() + + +def pick_contenders(who_list, rel_store, cid, reward): + """决定这次会不会撞车。返回 0~3 个争抢者(≥2 才触发冲突)。""" + if not _global_ok(rel_store) or not _client_ok(cid, rel_store): + return [] + idle = [w for w in who_list] + if len(idle) < 2: + return [] + + # 主抢者:Jev 决定(心情好/闲着/关系不冲突的更积极) + main = S._ask_jev_auto("客户需求,酬金 %d 元。谁最该去接?" % reward, idle) + if main not in idle: + main = random.choice(idle) + rest = [w for w in idle if w != main] + + # 陪抢者:从其余人里挑一个"也想抢"的 + # 概率随报酬上升(活越值钱越抢手),随与主抢者关系恶劣上升(冤家易对撞) + cand = [w for w in rest if _pair_cooldown_ok(main, w, rel_store)] + if not cand: + return [main] + rival = REL.get(cand[0], main) + p_by_rel = 0.18 + min(0.35, abs(min(0.0, rival)) * 0.006) # 关系越差越容易撞 + p_by_money = min(0.30, max(0.0, reward - 80) * 0.004) # 报酬越高越抢手 + p = min(0.95, (p_by_rel + p_by_money) * RATE_MUL) + if random.random() < p: + return [main, random.choice(cand)] + return [main] + + +# ── 台词生成(真实 LLM,串行对抗)───────────────────────────────────────── +def argue_system(who: str) -> str: + p = P.PROVIDERS[who] + return ( + f"你是{p['name']}({p['full']}),人设「{p['tag']}」:{p['blurb']}。\n" + f"现在你和同事在争同一个客户,你很不爽。\n" + f"【硬限制】最多 2 句话,总共不超过 60 字。直接说,别解释,别分点。" + ) + + +def argue_user_first(who: str, other: str, task: str) -> str: + n = P.PROVIDERS[other]["name"] + return (f"客户要:{task}。你发现 {n} 也想接这个活,你先开口争。说你自己的理由。").replace(":。", ":") + + +def argue_user_reply(who: str, other: str, task: str, first_line: str) -> str: + n = P.PROVIDERS[other]["name"] + return (f"客户要:{task}。{n} 刚说了:「{first_line[:40]}」。你反驳他,理由要更硬。").replace(":。", ":") + + +def gen_line(who: str, user_msg: str, stream=None): + """走闲聊人设(短),不走 deliver。""" + out = "" + for ev in (stream or P.stream_chat)(who, user_msg, system=argue_system(who), + max_completion_tokens=600): + if ev.get("type") == "delta": + out += ev["text"] + elif ev.get("type") == "error": + return None + return P.truncate(out.strip(), 80) + + +def build_argument(cid, task, a, b, rel_store, tick=0, stream=None): + """生成完整对峙。A 先说,B 看 A 的话反驳。返回台词列表。""" + first = gen_line(a, argue_user_first(a, b, task), stream=stream) + if first is None: + return [] + second = gen_line(b, argue_user_reply(b, a, task, first), stream=stream) + if second is None: + return [] + mark(rel_store, a, b, cid) + return [{"who": a, "text": first or "这活我接了,你们歇着吧。"}, + {"who": b, "text": second or "凭什么?我比他更合适。"}] + + +def apply_judgement(kind, a, b, tick=0): + """裁决结算。返回给前端的结果描述 + 数值变化。""" + out = {"mood": {}, "rel": []} + # ★ 注意:这里必须写变量 (b, a),不能写 ("b", "a")。 + # 字符串字面量会一路传到 REL.change(),被 `a not in ALL` 守卫静默挡掉—— + # 不抛异常、LOG 为空、关系纹丝不动,症状极具误导性(排查花了很久)。 + if kind == "a": + out["mood"][a] = 3 + out["mood"][b] = -8 + out["rel"].append((b, a, "grab_lose", tick)) + elif kind == "b": + out["mood"][b] = 3 + out["mood"][a] = -8 + out["rel"].append((a, b, "grab_lose", tick)) + elif kind == "reject": + out["mood"][a] = -5 + out["mood"][b] = -5 + out["rel"].append((a, b, "unfair_judge", tick)) + out["rel"].append((b, a, "unfair_judge", tick)) + elif kind == "scold": + out["mood"][a] = -3 + out["mood"][b] = -3 + for who, other, ev, tk in out["rel"]: + assert who in REL.ALL and other in REL.ALL, "关系目标非法:%s→%s" % (who, other) + REL.bump(ev, who, other, tick=tk) + return out diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/game.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/game.py new file mode 100644 index 00000000..7adfdeb4 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/game.py @@ -0,0 +1,1839 @@ +"""AI 事务所 —— 游戏状态与核心循环 + +设计要点: +- 五个 AI 在网格地图上自主移动,去向由 Jev 决策(约 2.5s 一次,五角色并行单次调用) +- 移动本身是本地寻路 + 插值,不消耗 API +- 客户/任务等关键事件才调 LLM(真实交付物) +- 玩家可随时对任意角色下达指令,指令优先级高于 Jev +""" +import os, json, math, random, threading, time, uuid + +SPAWN_SEC = float(os.environ.get("GAME_SPAWN_SEC", "25")) +# 复盘会结算后留给客户端展示结果的秒数,超时就自动散会把人放回工位 +BLAME_HOLD = float(os.environ.get("GAME_BLAME_HOLD", "15")) +# 复盘会为了不打断在跑的活,最多先等这么久(等不到就插进去) +BLAME_MAX_WAIT = float(os.environ.get("GAME_BLAME_WAIT", "45")) + +# ── 时间单位(M4-4)───────────────────────────────────────────────────────── +# ★M4-4 发现:这套数值里**"游戏日"从来没有被定义过**。 +# 跨天只能靠外部打 /api/newday,于是 M3-9 的"30 天长局"其实是 +# 30 × 1.2 秒 = 36 秒的游戏过程(见 tools/long_run.py), +# 而那边的策略**从不派任何人去休息**——于是每单 -22 体力只扣不还, +# 工资照发。测出来的"体力经济崩了",一大半是驱动脚本自己造的。 +# 单位混用是这个 bug 的根:耐心的单位是 tick、体力衰减的单位是 tick、 +# 工资的单位是天,三者之间**没有换算关系**,谁也没法验算。 +# +# 现在把它钉死: +# TICK_SEC 一个 tick 的真实秒数(0.12 保持不变 → 屏幕上的移动手感不变) +# DAY_TICKS 一个游戏日多少 tick(720 tick = 86.4 秒 ≈ 1.4 分钟) +# TIME_SCALE 时间倍速,默认 1.0 = 正常游玩;平衡模拟器用 30。 +# +# ★ 为什么是 720:游戏内钟是「1 tick = 2 游戏分钟」,720 tick 正好一整天, +# clock 显示不会一天绕三圈。(原来注释写「1 tick = 5 分钟、1 天 = 288 tick」, +# 那是"一天 34 秒"的意思——一次交付 45~75 秒比一整天还长,比例本身就不成立。) +# 720 tick = 86.4 游戏秒时,一次交付 45~75 秒 ≈ 半天到 0.9 天, +# 一个人一天能干 1~2 单,五个人一天 5~8 单,与 3.5 个客户/天的到店节奏匹配。 +# +# TIME_SCALE 的语义是**"墙上时钟缩放"**:整个游戏按同样比例跑快, +# 所有比例(每单扣多少体力、一天几单、工资多少)**一个都不变**。规则: +# · "每 tick 多少"的量(体力/心情/耐心)→ ×K(K = 1/TIME_SCALE) +# · "每游戏秒多少"的量(移动速度) → **不乘 K** +# · "按真实秒计的阈值"(Jev/到店/开会等)→ ÷TIME_SCALE +# (移动那条我一开始也乘了 K,工人直接 30 倍慢,永远走不到接待处,30 天 0 单。) +TICK_SEC = float(os.environ.get("GAME_TICK_SEC", "0.12")) +TIME_SCALE = float(os.environ.get("GAME_TIME_SCALE", "1.0")) +if not (TIME_SCALE > 0): + TIME_SCALE = 1.0 +K = 1.0 / TIME_SCALE # 每 tick 量的缩放系数 +DAY_TICKS = int(os.environ.get("GAME_DAY_TICKS", "720")) +TICK_GAME_MIN = int(os.environ.get("GAME_TICK_MIN", "2")) # 1 tick = 2 游戏分钟 +# 自动跨天:默认关(Godot 客户端自己按剧本推日),平衡模拟器打开 +AUTO_DAY = os.environ.get("GAME_AUTO_DAY", "0") == "1" +from collections import deque +import requests + +from dotenv import load_dotenv +HERE = os.path.dirname(__file__) +load_dotenv(os.environ.get("AI_OFFICE_ENV_PATH") or os.path.join(HERE, "..", ".env")) + +import providers as P +import scheduler as S +import relations as REL +import conflict as CF +import banter as BT +import blame_game as BG +import stats as ST + +# ── 地图 ──────────────────────────────────────────────────────────────────── +# 一张连续大地图,分三个功能区。客户和员工必须在同一空间才能互动, +# 所以不做成三张独立场景(M2-1 的设计修正,原 DEV_PLAN 写的是"三场景")。 +# +# x: 1 ── 12 14 ── 26 28 ── 38 +# 大堂 办公区 会议室 / 茶水 / 摸鱼 +# 客户进门 干活 / 休息 开会 +GW, GH = 40, 24 +FLOOR = {(x, y) for x in range(GW) for y in range(GH)} +WALLS = set() +# 四周墙 +for x in range(GW): + WALLS.add((x, 0)); WALLS.add((x, GH - 1)) +for y in range(GH): + WALLS.add((0, y)); WALLS.add((GW - 1, y)) +# 大门的两格开口与门图、地垫、客户出生点共用同一条通道。 +WALLS.difference_update({(0, 11), (0, 12)}) +# 区域隔断(留门) +for y in range(3, 21): # 大堂 | 办公区 + WALLS.add((13, y)) +for y in range(1, 23): # 办公区 | 会议室区 + WALLS.add((27, y)) +WALLS.discard((13, 5)); WALLS.discard((13, 18)) # 大堂两个门 +WALLS.discard((27, 6)); WALLS.discard((27, 17)) # 办公区两个门 +# 茶水间上方隔断;休息角的椅子前方保持开放。 +for x in range(22, 27): + WALLS.add((x, 2)) +# 会议室隔墙(下半部分隔开,形成独立房间) +for x in range(29, 38): + WALLS.add((x, 17)) +WALLS.discard((33, 17)) + +LOCATIONS = { + "entrance": (0, 12), # 大门:客户从墙体开口进来 + "reception": (7, 9), # 接待台:客户在这排队 + "waiting": (9, 17), # 等候沙发 + "desk": (20, 7), # 工位:干活 + "lounge": (24, 6), # 茶水台前的可站立点:回体力 + "slack": (24, 20), # 摸鱼角:回心情 + "meeting": (33, 12), # 会议室 +} +LOC_LABEL = {"entrance": "大门", "reception": "接待处", "waiting": "等候区", + "desk": "工位", "lounge": "茶水间", "slack": "摸鱼角", + "meeting": "会议室"} + +# ── 会议室座位(M2-2)──────────────────────────────────────────────────────── +# 长桌横卧在 y=9,五个座位分列上下:M3 坐主席位(北侧中间),其余分列两侧。 +# 座位固定分配:同一 who 每次都坐同一个位置,玩家能形成空间记忆。 +MEETING_TABLE = (33, 9) +SEATS = { + "m3": (31, 7), # 主席位 + "flash": (33, 7), + "kimi": (35, 7), + "longcat": (32, 11), + "step5": (34, 11), +} +# 开会时的状态标签 +MEETING_ACT = "开会中" + +# V2 五个完整工位,桌椅和可站立点分开,避免五人挤在同一排窄格。 +DESKS = {"m3": (16, 10), "flash": (20, 10), "longcat": (24, 10), + "kimi": (16, 14), "step5": (20, 14)} +REST_SPOTS = { + "lounge": {"m3": (23, 6), "flash": (25, 6), "longcat": (22, 7), + "kimi": (24, 7), "step5": (26, 7)}, + "slack": {"flash": (17, 21), "longcat": (20, 21), "step5": (23, 21)}, +} +FURNITURE_CELLS = set() + + + +_LAYOUT = os.path.join(HERE, "..", "data", "office_layout_v2.json") +ARCHITECTURE_WALLS = WALLS.copy() +with open(_LAYOUT, encoding="utf-8") as _layout_file: + for _prop in json.load(_layout_file)["props"]: + FURNITURE_CELLS.update(tuple(cell) for cell in _prop.get("blocked_cells", [])) +WALLS.update(FURNITURE_CELLS) + + +def seat_of(who: str): + return SEATS.get(who) + +# 每个角色可去的地方 +CAN_GO = { + "m3": ["desk", "meeting", "lounge"], + "flash": ["desk", "meeting", "lounge", "slack"], + "longcat": ["desk", "meeting", "lounge", "slack"], + "kimi": ["desk", "meeting", "lounge"], + "step5": ["desk", "meeting", "lounge", "slack"], +} + +# 角色属性:速度(格/秒) / 体力上限 / 干活速度倍率 +ATTR = { + "m3": {"speed": 1.6, "stamina_max": 100, "work_rate": 1.0, "quality": 0.92}, + "flash": {"speed": 2.8, "stamina_max": 100, "work_rate": 1.5, "quality": 0.62}, + "longcat": {"speed": 2.2, "stamina_max": 120, "work_rate": 1.2, "quality": 0.75}, + "kimi": {"speed": 1.4, "stamina_max": 100, "work_rate": 0.9, "quality": 0.95}, + "step5": {"speed": 2.4, "stamina_max": 110, "work_rate": 1.35, "quality": 0.80}, +} + +CLIENT_TASKS = [ + ("帮我写一份产品需求文档", "document"), + ("把这个功能的需求讲清楚", "document"), + ("审一下这个方案有哪些坑", "review"), + ("整理这堆资料出个总结", "summarize"), + ("给这个需求估个工期", "estimate"), + ("想三个可落地的功能方向", "ideate"), + # ★DESIGN 5.1 有 7 类需求,原来只实现了 5 类,缺「执行」。 + # 执行是 ★★★ 中高档(120-180),是"派 Step5/LongCat"这条路由的主要载体; + # 少了它,模拟器 SKILL 表里的 execute 分支从来没被跑到过。 + ("帮我把这套流程跑通", "execute"), +] + + +def passable(p): + return p in FLOOR and p not in WALLS + + +def nearest_passable(pos): + """旧存档的位置若落在新家具里,只在内存中移到最近空格。""" + cell = (int(round(pos[0])), int(round(pos[1]))) + if passable(cell): + return tuple(pos) + return min(FLOOR - WALLS, + key=lambda p: (abs(p[0] - pos[0]) + abs(p[1] - pos[1]), + p[1], p[0])) + + +def bfs_path(start, goal): + """网格 BFS。够用(26x15=390 格),不引入 A* 依赖。""" + if not passable(goal): + return [] + if start == goal: + return [] + prev = {start: None} + q = deque([start]) + while q: + cur = q.popleft() + for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)): + nxt = (cur[0] + dx, cur[1] + dy) + if nxt in prev or not passable(nxt): + continue + prev[nxt] = cur + if nxt == goal: + path = [] + while prev[nxt] is not None: + path.append(nxt) + nxt = prev[nxt] + return path[::-1] + q.append(nxt) + return [] + + +class Worker: + def __init__(self, who, pos): + self.who = who + self.pos = pos + self.path = [] + self.stamina = ATTR[who]["stamina_max"] + self.mood = 70 + self.job = None # 当前接的活 + self.working = 0.0 # 已工作进度(秒) + self.need = ATTR[who]["work_rate"] * 10 # 完成这个活需要的"有效工作秒" + self.order = None # 玩家指令(优先级最高) + self.acting = "闲逛" + self.log = [] + self.busy = False + self.seated = False # 是否已就座(开会中) + self.seat_cmd = False # 会议指令:优先级高于 goto / 接单 / Jev 自主 + # M2-8 动态属性 + self.fatigue = 0 # 连续工作天数,满 3 罢工 + self.strike_days = 0 # 连续罢工天数,到 2 离职 + self.quit = False # 离职后永久不可用 + self.strike = False # 今天罢工中(当天不干活) + self.eff = 1.0 # 本单开工时的效率(受体力影响) + self.worked_today = False # 今天有没有真的出活(算疲劳用) + # ★M4-4 新增:今天轮休。DESIGN 6.5 写着「玩家的应对:主动安排轮休」, + # 但原来**根本没有"不上工"这个指令**——`rest` 只是把人送进茶水间, + # 他照样接单、照样算 worked_today,于是疲劳只涨不跌, + # 5 个人第 5~7 天集体罢工、两天后全离职(实测 8 天局全员走光)。 + # 设计承诺的应对手段没实现,任何调参都救不回来:不是数值太狠, + # 是玩家手里没有那个牌。 + self.off_today = False # 轮休:今天不接单、不涨疲劳 + self.brain_down = False + self.brain_reason = "" + self.brain_scope = "model" + self.brain_epoch = 0 + self.brain_checking = False + # ── M4-2:打字活动(给客户端同步打字音用)── + # 关键在于**速率**而不是"有没有在干活":只知道"他在干活", + # 客户端只能按固定节奏放音,模型在长思考(停顿)时照样哒哒哒。 + # type_cps 是滑动窗口的字/秒,客户端按它调间隔。 + self.typing = False + self.type_cps = 0.0 + self._t_start = 0.0 + self._t_win = 0.0 + + # ★M4-2 实测(2026-10-05,m3 真实交付): + # 41 个 delta 块 / 128 字,**首块之前空了 1.46 秒**(模型在思考), + # 吐字窗口只有 1.74 秒,块间间隔中位数 12ms、最大 672ms。 + # 也就是说:大部分时间模型在**想**,不是在"打"字。 + # 所以打字音必须在**收到第一个字之后**才开,不是一进"干活中"就开—— + # 否则那 1.46 秒的思考期客户端会按角色基础手速一直哒哒哒, + # 正好违反计划里"停顿处也停顿"那条。 + TYPING_GAP = 0.6 # 超过这么久没吐字就当停了(M3-2 实测最大间隔 0.67s) + + def begin_typing(self) -> None: + """只是"准备干活",**不等于在打字**。要等第一个 delta。""" + self.typing = False + self.type_cps = 0.0 + self._t_start = time.time() + self._t_win = 0.0 + + def feed_typing(self, n: int) -> None: + """喂进 n 个字符,更新字/秒。窗口至少 0.4s,太短会被抖成噪声。""" + now = time.time() + if self._t_start <= 0.0: + self._t_start = now + self._t_win += max(0, n) + dt = now - self._t_start + if dt < 0.4: + self.typing = True # 有字进来了 → 真的在打字 + return + inst = self._t_win / dt + # 一阶低通:跟上一个窗口做指数平滑,免得间隔忽快忽慢听着神经衰弱 + self.type_cps = inst if self.type_cps <= 0.0 else self.type_cps * 0.45 + inst * 0.55 + self.typing = True + self._t_start = now + self._t_win = 0.0 + + def tick_typing(self) -> None: + """看门狗:太久没吐字就停。主循环每 tick 调一次。 + + 没有它的话,41 块吐完之后模型去"想下一步"的那 672ms 里, + 客户端会一直以为还在打字——停顿处不停,等于没同步。 + """ + if self.typing and self._t_start > 0.0 and \ + time.time() - self._t_start > self.TYPING_GAP: + self.typing = False + self.type_cps = 0.0 + self._t_start = 0.0 + self._t_win = 0.0 + + def end_typing(self) -> None: + self.typing = False + self.type_cps = 0.0 + self._t_start = 0.0 + self._t_win = 0.0 + + @property + def meta(self): + return P.PROVIDERS[self.who] + + @property + def on_duty(self): + """还在职。离职的人不算人力,不发工资、不接单、不参会。""" + return not self.quit + + @property + def available(self): + return self.on_duty and not self.brain_down + + def say(self, t): + self.log.append(t) + self.log = self.log[-6:] + + +class Client: + # ★M3-7:人形。DESIGN 5.1 的"基础人形 5 种"之前只有设定、没有实现, + # kind 存的是需求类型,和人形是两回事,别混。 + PERSONAS = { + "normal": "普通上班族", + "boss": "创业公司老板", + "client_hard": "甲方大人", + "geek": "极客程序员", + "young": "年轻客户", + } + + def __init__(self, task, reward, patience, need, persona=None): + self.id = uuid.uuid4().hex[:6] + self.task = task + self.kind = need # 需求类型 document/analysis/… + self.persona = persona or random.choice(list(self.PERSONAS)) + self.reward = reward + self.patience_max = patience + self.patience = patience + self.slot = None + self.served_by = None + self.status = "等待" # 等待 / 抢单中 / 服务中 / 待验收 / 已完成 / 已流失 + self.result = None + self.grab_tick = 10**9 + self.pos = tuple(LOCATIONS["entrance"]) + + @property + def persona_name(self): + return self.PERSONAS.get(self.persona, "普通上班族") + + +class Game: + def __init__(self): + self.reset() + + def reset(self): + self.day = 1 + # ★M4-4:原来这里写死 300,而 stats.INIT_MONEY 是 500(DESIGN 7.1)。 + # 两处不一致的直接后果:平衡模拟器按 500 起跑,真实游戏按 300 起跑, + # 模拟结论和实玩对不上,差 200 块。唯一正确的做法是这里读常量。 + self.money = ST.INIT_MONEY + self.rep = ST.INIT_REP + self._day_start = 0 # 本天第几个 tick(M4-4 自动跨天的分母) + # ★M4-3 世代号:reset() / 读档都会 +1。 + # 起因是「读档不撤销在跑的 LLM 线程」——一次交付要 45~75 秒, + # 期间玩家按了读档,还原出一个**全新的 clients 列表和 Worker 对象**, + # 那个线程醒来后照样会执行 c.result=... / worker.worked_today=True, + # 甚至可能往 self.clients / self.slots 上插东西。 + # 办法:线程每步拿自己的起点世代号比一下,对不上就地收工。 + self.gen = 0 + self.workers = { + "m3": Worker("m3", (16, 10)), + "flash": Worker("flash", (20, 10)), + "longcat": Worker("longcat", (24, 10)), + "kimi": Worker("kimi", (16, 14)), + "step5": Worker("step5", (20, 14)), + } + self.clients = [] + self.slots = {} # 接待位 -> client + self.events = [] # 全局事件流 + self.log = [] + self.running = False + self.tick = 0 + self.meeting = None # {"topic","seated","phase","started"} + self.conflicts = {} # client_id -> {"a","b","lines","task"} + self._cf_store = {} # 冲突冷却 + self._bt_store = {} # 吐槽冷却 + self.banters = [] # 最近的吐槽对话(给客户端气泡) + self.failures = [] # M2-6:今天干砸的单(发生时就记,不事后反推) + self.finished = [] # M3-9:已完结客户的滚动存档(上限 4) + self._blame_store = {} # 复盘会冷却(记 last_blame_day) + self.neg_days = 0 # M2-8:资金为负已持续几天(满 3 天破产) + self._blame_want_at = 0.0 # 复盘会想开但还在等活干完的时刻 + self.game_over_reason = None # M2-8:失败原因,非 None 即游戏结束 + self.last_settle = None # 最近一次每日结算明细 + self._jev_at = 0 + self._spawn_at = 0 + self._lock = threading.RLock() + + # ── 事件 ── + def event(self, kind, text, who=None, extra=None): + e = {"t": round(time.time(), 2), "kind": kind, "text": text, "who": who} + if extra: + e.update(extra) + self.events.append(e) + self.events = self.events[-120:] + return e + + def note(self, text): + self.log.append(f"第{self.day}天 · {text}") + self.log = self.log[-40:] + + def _freeze_brain(self, w): + w.order = None + w.path = [] + w.goal_key = None + w.job = None + w.seat_cmd = False + w.seated = False + w.busy = False + w.acting = "脑袋宕机了" + w.end_typing() + + def shutdown_model(self, who, error, gen=None): + """停掉耗尽额度的模型,并把尚未交付的客户交还给其他人。""" + with self._lock: + if gen is not None and not self.gen_alive(gen): + return + affected = [who] + if error.get("scope") == "credential": + key = P.PROVIDERS[who]["key_env"] + affected = [k for k in self.workers if P.PROVIDERS[k]["key_env"] == key] + for cid, cf in list(self.conflicts.items()): + if not set((cf["a"], cf["b"])).intersection(affected): + continue + self.conflicts.pop(cid, None) + for peer in (cf["a"], cf["b"]): + w = self.workers[peer] + w.job, w.order, w.path = None, None, [] + w.brain_epoch += 1 + if not w.brain_down: + w.acting = "在工位待命" + c = next((c for c in self.clients if c.id == cid), None) + if c: + c.status, c.served_by, c.result = "等待", None, "" + c.grab_tick = self.tick + 90 + for w in self.workers.values(): + if w.who not in affected or w.brain_down: + continue + w.brain_down = True + w.brain_reason = error.get("category", "quota") + w.brain_scope = error.get("scope", "model") + w.brain_epoch += 1 + w.brain_checking = False + self._freeze_brain(w) + if self.meeting: + self.meeting["seated"] = [p for p in self.meeting.get("seated", []) if p != w.who] + for c in self.clients: + if c.served_by == w.who and c.status in ("等待", "服务中", "抢单中"): + c.status, c.served_by, c.result = "等待", None, "" + c.grab_tick = self.tick + 90 + c.patience = max(c.patience, c.patience_max) + self.event("brain_down", "%s:%s,暂停接单和行动" % + (w.meta["name"], error["message"]), who=w.who) + + def model_stream(self, who, *args, **kwargs): + w = self.workers[who] + gen, epoch = self.gen, w.brain_epoch + if not w.available: + yield {**P.error_event(who, category=w.brain_reason or "auth"), + "message": "脑袋宕机了,额度恢复后再重启脑袋"} + return + for ev in P.stream_chat(who, *args, **kwargs): + if not self.gen_alive(gen) or self.workers.get(who) is not w or w.brain_epoch != epoch: + yield P.error_event(who, category="cancelled") + return + if ev.get("type") == "error" and ev.get("brain_down"): + self.shutdown_model(who, ev, gen) + yield ev + if ev.get("type") == "error": + return + + def _reboot_thread(self, who, gen, epoch): + w = self.workers[who] + if not self.gen_alive(gen) or w.brain_epoch != epoch: + return + failed = False + accepted = False + try: + for ev in P.stream_chat(who, "只回复:好", system="检查连接,只回复一个字。", + max_completion_tokens=64): + if ev.get("type") == "error": + failed = True + break + if ev.get("type") == "done": + accepted = True + with self._lock: + if not self.gen_alive(gen) or self.workers.get(who) is not w or w.brain_epoch != epoch: + return + w.brain_checking = False + if failed or not accepted: + self.event("brain_check", "%s 还没恢复,继续宕机" % w.meta["name"], who=who) + return + reason, scope = w.brain_reason, w.brain_scope + for other in self.workers.values(): + shared = scope == "credential" and other.brain_scope == "credential" and \ + P.PROVIDERS[other.who]["key_env"] == P.PROVIDERS[who]["key_env"] + if other is w or (shared and other.brain_reason == reason): + other.brain_down, other.brain_reason = False, "" + other.brain_checking = False + other.brain_epoch += 1 + other.acting = "在工位待命" + self.event("brain_reboot", "%s 脑袋重启成功,可以继续接单" % w.meta["name"], who=who) + finally: + if self.gen_alive(gen) and self.workers.get(who) is w and w.brain_epoch == epoch: + w.brain_checking = False + + # ── 客户 ── + # ★DESIGN 3.2:「客户到达节奏:第 1 天 3 个,每过一天 +1,上限 5 个同时在等」 + # 原来这里是硬编码的 3 —— 客流从第 1 天到第 30 天**完全不变**, + # 于是"越玩越忙"的成长感不存在,后期压力也上不来。 + # ★DESIGN 7.1:「声誉 0-100,影响客户数量**和**报酬」—— + # 声誉对报酬那半早就实现了(stats.reward_scale),数量这半一直缺。 + # 现在:基础槽位随天数 3→5,声望 ≥80 提前一天多一席。 + @staticmethod + def client_slots(day, rep): + base = 3 + (max(1, day) - 1) // 2 # 1~2天:3 3~4天:4 5天起:5 + bonus = 1 if rep >= 80 else 0 # 口碑好,客人来得更早 + return max(3, min(5, base + bonus)) + + def spawn_client(self): + cap = self.client_slots(self.day, self.rep) + if len(self.slots) >= cap: + return + free = [i for i in range(cap) if i not in self.slots] + if not free: + return + # 需求去重:最近 8 个任务不重复,避免同一批客户全是同一个需求 + recent = [getattr(c, "task", None) for c in self.clients][-8:] + pool = [t for t in CLIENT_TASKS if t[0] not in recent] or CLIENT_TASKS + task, kind = random.choice(pool) + # ★M4-4:难度/报酬挂**声誉**,不是挂天数。 + # 原来 `diff = 1 + (self.day - 1) * 0.12` 是按天复利 12%, + # 30 天后单子值 4.5 倍,钱从 500 涨到 21921(44 倍)、声誉第 7 天顶到 100, + # 破产这个失败条件在设计上根本不可能触发——调参前先确认这一条。 + # DESIGN 7.1 原话是「声誉 0-100,影响客户数量和报酬」, + # 声誉有上限(100),报酬就不会无限膨胀,而且膨胀得靠玩家挣。 + diff = ST.reward_scale(self.rep) + # 耐心按 tick 计。1 tick = 2 游戏分钟,1 天 = 720 tick(DAY_TICKS)。 + # ★M3-9 实测修正:耐心原来 170~280 tick,接单后 ×2.2 也只有 53~88 tick, + # 而一次真实交付要 375~625 tick——**耐心比交付还短**, + # 客户系统性地在活干完之前就走了,游戏先天不可赢。 + # 提到 400~700(接单后 880~1540 tick = 176~308 游戏秒), + # 给慢模型(M3 思考占输出 98%)留够余量。 + # ★M4-4 重做:DESIGN 5.1 给了**分类型**报酬表(跑腿 40-60 / 长文 160-240 / + # 急单 200-300),那个跨度不是装饰——它是"派谁干"的路由驱动力。 + # M4-4 第一版把它压平成 randint(35, 90):均值只有设计稿的 45%, + # 跨度从 7.5 倍缩到 2.6 倍,设计意图被平掉了。 + # 现在按类型取区间;REWARD_BY_KIND 没设时才退回扁平的 REWARD_RANGE + # (模拟器的对照实验用这个开关,生产路径默认走类型表)。 + # ★DESIGN 5.1 给了分类型报酬表,这是设计稿的数字,不是调出来的。 + # M4-4 第一版把它压平成 randint(35, 90)(均值只有设计的 45%、 + # 跨度从 7.5 倍缩到 2.6 倍),是"双向钢人"里反方赢了这一条。 + by_kind = getattr(ST, "REWARD_BY_KIND", None) or ST.DESIGN_REWARD + if by_kind: + lo, hi = by_kind.get(kind, by_kind.get("_default", ST.REWARD_RANGE)) + reward = random.randint(lo, hi) + else: + reward = random.randint(*ST.REWARD_RANGE) + c = Client(task, int(reward * diff), int(random.randint(400, 700)), kind) + c.slot = free[0] + c.pos = tuple(LOCATIONS["entrance"]) + self.slots[free[0]] = c + self.clients.append(c) + c.grab_tick = self.tick + 70 # 玩家 70 tick(~8s) 不派单,AI 就自己抢 + self.event("client_arrive", f"新客户来了:{c.task}", extra={"client": c.id}) + + def _auto_grab(self, c): + """玩家没派人,AI 自己抢。可能是单抢,也可能撞车(M2-4)。""" + # M2-8:离职 / 罢工 / 体力见底的人不参与抢单。 + # 心情低的只挑简单单(client 有 difficulty 的话)也在这里挡。 + idle = [w for w in self.workers.values() + if not w.job and w.stamina > 20 and w.available + and not w.strike and not w.off_today + and ST.can_accept(w.stamina)] + if not idle: + return + # 心情 < 40 的不接复杂单 + if getattr(c, "difficulty", 1) >= 2: + calm = [w for w in idle if ST.accepts_complex(w.mood)] + if calm: + idle = calm + contenders = CF.pick_contenders([w.who for w in idle], self._cf_store, c.id, c.reward) + if not contenders: + contenders = [random.choice(idle).who] + if len(contenders) >= 2: + self._start_conflict(c, contenders[0], contenders[1]) + return + who = contenders[0] + c.served_by = who + c.status = "抢单中" + w = self.workers[who] + w.job = c.id + w.acting = "去抢单" + w.order = ("goto", ("reception", c.slot)) + self.event("grab", f"{w.meta['name']} 自己把「{c.task}」抢走了", who=who) + threading.Thread(target=self._serve_client_thread, + args=(c.id, self.gen), daemon=True).start() + + # ── 抢单冲突(M2-4)── + def _start_conflict(self, c, a, b): + if c.id in self.conflicts: + return # 已有冲突,不重复起线程 + c.status = "冲突中" + self.conflicts[c.id] = {"a": a, "b": b, "task": c.task, "lines": [], + "client": c.id, "judged": False} + for who in (a, b): + w = self.workers[who] + w.job = c.id # 锁定:冲突期间不能被别人接走 + w.order = ("goto", ("reception", c.slot)) + w.acting = "去理论" + self.event("conflict_start", + f"{self.workers[a].meta['name']} 和 {self.workers[b].meta['name']} 为「{c.task}」吵起来了") + threading.Thread(target=self._conflict_thread, args=(c.id, a, b), daemon=True).start() + + def _conflict_thread(self, cid, a, b): + cf = self.conflicts.get(cid) + if not cf: + return + original, gen = cf, self.gen + lines = CF.build_argument(cid, cf["task"], a, b, self._cf_store, self.tick, + stream=self.model_stream) + with self._lock: + cf = self.conflicts.get(cid) + if cf is not original or not self.gen_alive(gen): + return + cf["lines"] = lines + for ln in lines: + self.event("argue", f"{P.PROVIDERS[ln['who']]['name']}:{ln['text'][:40]}", + who=ln["who"]) + + def judge(self, cid, kind, tick=0): + """玩家裁决。kind ∈ a/b/reject/scold""" + if kind not in CF.JUDGE_CHOICES: + return {"ok": False, "msg": "未知裁决"} + with self._lock: + cf = self.conflicts.get(cid) + if not cf or cf.get("judged"): + return {"ok": False, "msg": "这场冲突已经判过了"} + c = next((x for x in self.clients if x.id == cid), None) + a, b = cf["a"], cf["b"] + res = CF.apply_judgement(kind, a, b, tick) + for who, dm in res["mood"].items(): + w = self.workers[who] + w.mood = max(0, min(100, w.mood + dm)) + cf["judged"] = True + winner = {"a": a, "b": b}.get(kind) + if winner: + c.served_by = winner + c.status = "抢单中" + w = self.workers[winner] + w.order = ("goto", ("reception", c.slot)) + w.acting = "去接单" + self.workers[a if winner == b else b].job = None + self.event("judge", f"你把活判给 {w.meta['name']}") + threading.Thread(target=self._serve_client_thread, + args=(cid, self.gen), + daemon=True).start() + else: + c.served_by = None + c.status = "等待" + c.patience = max(0, c.patience - 50) + # 玩家刚裁决"都不接",要给他喘息时间重新决定派谁。 + # 不然下一个 tick _auto_grab 立刻又把单抢走,玩家裁决形同虚设。 + c.grab_tick = self.tick + 90 + for who in (a, b): + self.workers[who].job = None + self.workers[who].order = None + self.workers[who].acting = "理论输了" + self.event("judge", "你谁也没让,客户还在等(稍后可能又被抢)") + self.conflicts.pop(cid, None) + return {"ok": True, "kind": kind, "winner": winner} + + def conflicts_view(self): + return [{"client": k, "a": v["a"], "b": v["b"], "task": v["task"], + "lines": v["lines"], "judged": v.get("judged", False)} + for k, v in self.conflicts.items() if not v.get("judged")] + + def _serve_client_thread(self, cid, gen=None): + """AI 走到客户那里接单,然后走到工位干活,最后交付。 + + gen 是**本线程启动时**的世代号。每跨一次长等待、每次要写回状态之前都验一次 + ——中间可能已经读档了,那时候继续写就是往已还原的状态上糊东西。 + """ + if gen is None: + gen = self.gen + c = next((x for x in self.clients if x.id == cid), None) + if not c: + return + try: + w = c.served_by + worker = self.workers.get(w) + if not worker or not worker.available: + return + c.status = "服务中" + # 既然有人接了,客户愿意等——把耐心重置并放大,别让 AI 干到一半客户跑了 + c.patience = int(c.patience_max * 2.2) + # 1) 走去接待(会议优先:正在开会就先去开会,客户等得起) + if not self._seat_ready(worker, 90): + if not worker.available: + return + c.status = "已流失" + self.slots.pop(c.slot, None) + return + # ★_seat_ready 返回"已就座"意味着会议还在开。这会儿直接派他去接待处是走不到的 + # (seat_cmd 压过 goto),只能白等 _await_at 超时、客户耐心耗尽而流失。 + # 所以这里等会议真的散了再继续干活。 + self._await_meeting_end(worker, c, 120) + if not self.gen_alive(gen) or c.status == "已流失" or not worker.available or c.served_by != w: + return + worker.order = ("goto", ("reception", c.slot)) + worker.acting = "去接客户" + self._await_at(worker, "reception", c.slot, 90) + # ★每次长等待之后都验世代 + if not self.gen_alive(gen) or c.status == "已流失" or not worker.available or c.served_by != w: + return + worker.say(f"接下了:{c.task}") + self.event("accept", f"{worker.meta['name']} 接下了「{c.task}」", who=w) + # 2) 去工位干活(真实调用) + worker.order = ("goto", ("desk", worker.who)) + worker.acting = "去工位" + self._await_at(worker, "desk", worker.who, 120) + if not self.gen_alive(gen) or c.status == "已流失" or not worker.available or c.served_by != w: + return + worker.acting = "干活中" + # 3) 干活:真实 LLM 调用产出交付物 + # ★ 三道防线见 providers.py:deliver_system 硬约束 + 按模型分配 + # max_completion_tokens + truncate 兜底。空结果自动重试一次。 + worker.working = 0 + # M2-8:体力低 → 效率打折。真正落地在收入上(accept 里按 eff 折质量), + # 不然"效率 -30%"只是个没人看得见的数字。 + worker.eff = ST.efficiency(worker.stamina) + worker.need = ST.work_need(ATTR[worker.who]["work_rate"] * 10, + worker.stamina) + deliver = "" + # ★M4-2:干活期间开打字活动,逐个 delta 喂字符数进去。 + # 异常路径也一定要 end_typing(),否则这个人会一直"打着字", + # 而且 finally 里再调一次是幂等的(end_typing 只是置位)。 + worker.begin_typing() + try: + for ev in P.deliver_stream(w, c.task, stream=self.model_stream): + if ev.get("type") == "delta": + worker.feed_typing(len(ev.get("text", ""))) + elif ev.get("type") == "deliver": + deliver = ev.get("text", "") + elif ev.get("type") == "error": + if worker.available and c.served_by == w: + c.status, c.served_by = "等待", None + c.grab_tick = self.tick + 90 + worker.job, worker.order = None, None + worker.acting = "在工位待命" + return + finally: + worker.end_typing() + # ★最关键的一处:LLM 回来了,确认还是同一代才允许写回交付物。 + # 不确认的话这行会把结果写进读档后那个全新的状态里, + # 而那个 Client 对象根本不在 clients 列表里了。 + if not self.gen_alive(gen) or not worker.available or c.served_by != w: + return + c.result = deliver or "(这次没交出东西)" + if deliver: + worker.worked_today = True + # 4) 回接待处交付 + worker.order = ("goto", ("reception", c.slot)) + worker.acting = "回去交付" + self._await_at(worker, "reception", c.slot, 90) + if not self.gen_alive(gen) or c.status == "已流失" or not worker.available or c.served_by != w: + return + c.status = "待验收" + worker.order = None + worker.acting = "在工位待命" + self.event("deliver", f"{worker.meta['name']} 交付了「{c.task}」,等你验收", who=w, + extra={"client": cid}) + except Exception as e: + self.event("error", f"服务出错:{e}") + + def _seat_ready(self, worker, timeout=90): + """等这个角色在会议中就座。会议优先于接单。""" + t0 = time.time() + while time.time() - t0 < timeout: + if not worker.available: + return False + if worker.seated: + return True + if not self.meeting: + return True # 会议已散,直接去干活 + time.sleep(0.3) + return False + + def _await_meeting_end(self, worker, client=None, timeout=120): + """等这个角色从会议里出来。已经在外面就立刻返回。 + + 会议散了 / 客户跑了,任一条件满足就停,别白等。 + """ + t0 = time.time() + while time.time() - t0 < timeout: + if not worker.available: + return False + if not worker.seated and not worker.seat_cmd: + return True + if client is not None and client.status == "已流失": + return False + time.sleep(0.3) + return False + + def _await_at(self, worker, loc, idx, timeout=90): + """等到角色走到目标位置附近(或超时)。""" + t0 = time.time() + while time.time() - t0 < timeout: + if not worker.available: + return False + tgt = self._loc_of(loc, idx) + if math.dist(worker.pos, tgt) <= 0.12 and not worker.path: + return True + time.sleep(0.3) + return False + + def _seat_of(self, who): + return SEATS.get(who) + + def _loc_of(self, loc, idx): + if loc == "seat": + return self._seat_of(idx) + if loc == "desk": + return DESKS.get(idx, DESKS["m3"]) + if loc in REST_SPOTS and idx in REST_SPOTS[loc]: + return REST_SPOTS[loc][idx] + base = LOCATIONS[loc] + if loc == "reception" and isinstance(idx, int): + return (10, base[1] - 1 + idx * 2) # 员工站在客户右侧交流 + return base + + # ── 玩家指令 ── + def command(self, who, action, arg=None): + w = self.workers.get(who) + if not w: + return {"ok": False, "msg": "没有这个角色"} + if action == "reboot": + if not w.brain_down or w.brain_checking: + return {"ok": False, "msg": "脑袋正常,或正在检查恢复"} + w.brain_checking = True + threading.Thread(target=self._reboot_thread, + args=(who, self.gen, w.brain_epoch), daemon=True).start() + return {"ok": True, "msg": "尝试重启脑袋,只检测一次"} + if w.brain_down: + return {"ok": False, "brain_down": True, "msg": "脑袋宕机了,额度恢复后再重启脑袋"} + if action == "goto": + if arg not in CAN_GO[who]: + return {"ok": False, "msg": f"{w.meta['name']} 不去那儿"} + w.order = ("goto", (arg, who)) + w.acting = f"前往{LOC_LABEL[arg]}" + return {"ok": True, "msg": f"{w.meta['name']} 去{LOC_LABEL[arg]}"} + if action == "meeting": + return self.start_meeting(arg or "周会") + if action == "dismiss": + return self.dismiss_meeting() + if action == "blame_talk": + return self.blame_talk(arg) + if action == "rest": + w.order = ("goto", ("lounge", who)) + w.acting = "去休息" + return {"ok": True, "msg": f"{w.meta['name']} 去茶水间休息"} + # ★M4-4:轮休。DESIGN 6.5「主动安排轮休」的实现。 + # off = 今天不上工(不接单、疲劳归零、体力回满) + # back = 取消轮休,回到正常排班 + if action in ("off", "back"): + w.off_today = (action == "off") + if action == "off": + w.order = ("goto", ("lounge", who)) + w.acting = "今天轮休" + else: + w.acting = "已恢复排班" + return {"ok": True, + "msg": (f"{w.meta['name']} 今天轮休(不涨疲劳,体力回满)" if action == "off" + else f"{w.meta['name']} 恢复排班")} + if action == "assign": + if arg in self.conflicts: + return {"ok": False, "msg": "这单正在被人抢,轮不到你派"} + c = next((x for x in self.clients if x.id == arg and x.status == "等待"), None) + if not c: + return {"ok": False, "msg": "这个客户不在等待中"} + if c.served_by: + return {"ok": False, "msg": f"这个客户已经由 {self.workers[c.served_by].meta['name']} 接了"} + if w.job: + return {"ok": False, "msg": f"{w.meta['name']} 手上有活"} + # M2-8 派单前置条件:不然后面所有数值规则都能被玩家一条指令绕过 + if not w.on_duty: + return {"ok": False, "msg": f"{w.meta['name']} 已经离职了"} + if w.strike: + return {"ok": False, "msg": f"{w.meta['name']} 今天在罢工,说什么都不听"} + if w.off_today: + return {"ok": False, "msg": f"{w.meta['name']} 今天轮休,不接单"} + if not ST.can_accept(w.stamina): + return {"ok": False, + "msg": f"{w.meta['name']} 体力只剩 {int(w.stamina)},接不动了(<{ST.REFUSE_STAMINA})"} + if not ST.accepts_complex(w.mood) and getattr(c, "difficulty", 1) >= 2: + return {"ok": False, + "msg": f"{w.meta['name']} 心情太差({int(w.mood)}),不肯接复杂单"} + c.served_by = who + w.job = c.id + w.acting = "去接单" + threading.Thread(target=self._serve_client_thread, + args=(c.id, self.gen), daemon=True).start() + return {"ok": True, "msg": f"{w.meta['name']} 去接「{c.task}」"} + return {"ok": False, "msg": "未知指令"} + + def accept(self, cid, approved): + c = next((x for x in self.clients if x.id == cid), None) + if not c or c.status != "待验收": + return {"ok": False} + w = self.workers.get(c.served_by) + # M2-8:疲劳/累的时候干活,质量打折,收入自然也打折 + q = ATTR[c.served_by]["quality"] * (getattr(w, "eff", 1.0) if w else 1.0) + if approved: + gain = int(c.reward * (0.6 + q * 0.6)) + self.money += gain + # ★M4-4:声誉涨幅走饱和曲线(stats.rep_gain),不是固定 +3 + rg = ST.rep_gain(self.rep) + self.rep = min(100, self.rep + rg) + c.status = "已完成" + self.event("accept", f"验收通过:+{gain} 元,声誉 +{rg}", who=c.served_by) + txt = f"通过!入账 {gain} 元" + else: + self.rep = max(0, self.rep - 5) + self.money = max(0, self.money - int(c.reward * 0.3)) + c.status = "已完成" + self.failures.append({"task": c.task, "why": "老板打回了", + "who": c.served_by}) + self.event("reject", "你打回了这单,声誉 -5", who=c.served_by) + txt = "已打回该单,声誉 -5" + if w: + w.job = None + w.stamina = max(0, w.stamina - 22) + w.mood = max(0, w.mood - 8) + self.slots.pop(c.slot, None) + self._retire_client(c) + self.note(txt) + return {"ok": True, "msg": txt} + + def _retire_client(self, c): + """把走完流程的客户挪出活跃列表,只留最近 4 单做记录。 + + ★M3-9 抓到:完成/流失的客户原来只 pop 了 slot,人还留在 self.clients 里。 + 实测第 6 天就积到 24 条(20 条已是终态),而 snapshot() 每 0.4 秒 + 把整个列表序列化一次 —— 30 天会涨到上百条,既拖慢又占内存。 + """ + self.finished.append(c) + if len(self.finished) > 4: + self.finished = self.finished[-4:] + self.clients = [x for x in self.clients if x is not c] + + # ── 地图移动 ── + def move(self): + for w in self.workers.values(): + if w.brain_down: + self._freeze_brain(w) + continue + if w.path: + tgt = w.path[0] + dx, dy = tgt[0] - w.pos[0], tgt[1] - w.pos[1] + d = math.hypot(dx, dy) + # ★M4-4:这里**不能**乘 K。 + # 速度是"每游戏秒多少格",是设计常量(speed),不随倍速变。 + # 倍速只改墙上时钟:每真实秒跑 S× 个 tick,于是每真实秒走 S× 格, + # 折回游戏秒正好还是 speed 格。所以每 tick 的距离 = speed × TICK_SEC, + # **和 TIME_SCALE 无关**。 + # 我一开始在这里乘了 K(=1/S),结果工人 30 倍慢: + # 实测 211 游戏秒只走了 7.3 格(应 6.25 秒走完 10 格), + # 于是永远走不到接待处,30 天 0 单交付。 + # 判据:**凡是"每游戏秒多少"的量,一律不乘 K; + # 凡是"每 tick 多少"的量(体力/心情/耐心),才乘 K。** + sp = ATTR[w.who]["speed"] * TICK_SEC + if d <= sp: + w.pos = tgt + w.path.pop(0) + else: + w.pos = (w.pos[0] + dx / d * sp, w.pos[1] + dy / d * sp) + + def refresh_paths(self): + for w in self.workers.values(): + if w.brain_down: + self._freeze_brain(w) + continue + goal = None + # ★ 会议优先:seat_cmd 一旦为真,goto / 接单 / Jev 自主都让位 + if w.seat_cmd: + goal = self._seat_of(w.who) + elif w.order and w.order[0] == "seat": + goal = self._seat_of(w.who) + elif w.order and w.order[0] == "goto": + goal = self._loc_of(w.order[1][0], w.order[1][1]) + if goal is None and w.job: + c = next((x for x in self.clients if x.id == w.job), None) + if c: + goal = self._loc_of("reception", c.slot) + # ★ 目标变了就必须重算。原写法 `if goal and w.path: continue` + # 只看"有没有路",不看"这条路通不通往新目标": + # 工人正溜达去茶水间时被派单,order 改成去接待处,旧 path 却被一直复用, + # 人就一直往茶水间走 —— 实测全场没一个人到得了工位,干活中 0 帧。 + key = tuple(goal) if goal else None + if key is not None and getattr(w, "goal_key", None) == key: + continue + w.goal_key = key + if goal: + w.path = bfs_path((int(w.pos[0]), int(w.pos[1])), goal) + else: + # 没任务:Jev 决定去哪(已由 jev_tick 写入 order) + w.path = [] + + # ── Jev 决策 ── + def jev_decide(self): + if not os.environ.get("OPENROUTER_API_KEY", "").strip(): + return + # ★M4-4:轮休的人不进调度池。`off_today` 已经挡住了 assign 和 _auto_grab, + # 但 Jev 每 3 秒仍会把他从茶水间支去工位——玩家明明说了"今天轮休", + # 看着像指令失效。挡在**进池前**而不是事后忽略,还省一次 API 调用。 + pool = [w.who for w in self.workers.values() + if w.available and not w.order and not w.seat_cmd and not w.off_today] + if not pool: + return + # 这里的时钟和 snapshot() 是两套算法(都是老的 tick*5), + # M4-4 已经把游戏日定义成 720 tick / 2 分钟一 tick,一并对齐。 + _t = max(0, self.tick - self._day_start) * TICK_GAME_MIN + lines = ["你是一个像素办公室里五个人格的调度器。现在时间 %02d:%02d,他们各自该去哪儿。" % + ((_t // 60) % 24, _t % 60)] + waiting = len([c for c in self.clients if c.status == "等待"]) + lines.append(f"当前有 {waiting} 个客户在等。") + for w in self.workers.values(): + allies = REL.allies_of(w.who) + foes = REL.enemies_of(w.who) + rel_txt = "" + if allies: + rel_txt += ",和 " + "、".join(w2 for w2 in allies) + " 处得好" + if foes: + rel_txt += ",和 " + "、".join(w2 for w2 in foes) + " 合不来" + lines.append(f"- {w.meta['name']}:体力 {int(w.stamina)}/{ATTR[w.who]['stamina_max']},心情 {int(w.mood)}," + f"手上{'有活' if w.job else '没事干'},当前状态:{w.acting}{rel_txt}") + for name, d in LOC_LABEL.items(): + lines.append(f"{name} = {d}") + state = "\n".join(lines) + qs = {} + for who in pool: + foes = set(REL.enemies_of(who)) + allies = set(REL.allies_of(who)) + busy_loc = self._who_loc.get(who) if hasattr(self, "_who_loc") else None + opts = {} + for loc in CAN_GO[who]: + tip = "常规位置" + # 关系影响:优先和盟友待一起,避开仇人 + if loc == "lounge" and (allies or foes): + tip = "适合" + ("和盟友一起歇着" if allies else "躲清静") + if loc == "slack" and foes: + tip = "自己摸鱼,谁也别理" + if loc == "meeting" and waiting: + tip = "这里正有客户等着" + if loc == "desk" and foes: + tip = "埋头干活,少看人脸色" + opts[loc] = f"去{LOC_LABEL[loc]}({tip})" + qs[who] = { + "type": "choice", + "instructions": f"Given stamina/mood/workload, where should {who.upper()} go now? Choose one.", + "criteria": opts, + } + try: + r = requests.post(S.JEV_URL, timeout=15, headers={ + "Authorization": f"Bearer {os.environ.get('OPENROUTER_API_KEY', '')}", + "Content-Type": "application/json", + }, json={"model": S.JEV_MODEL, "state": state, "questions": qs}) + if r.status_code != 200: + return + for who, ans in (r.json().get("answers") or {}).items(): + pick = ans.get("choice") + if pick in CAN_GO.get(who, []): + w = self.workers[who] + if w.available and not w.order: + w.order = ("goto", (pick, who)) + w.acting = f"去{LOC_LABEL[pick]}" + except Exception: + pass + + # ── 主循环 ── + def _wander_idle(self): + """M2-5:闲人走动,制造"路过"。 + + 为什么必须有:五个工人各有各的工位,不主动走动的话地图上根本不会擦肩, + 路过吐槽就成了永不触发的死代码(实测 60s 自然触发 0 次)。 + 茶水间/摸鱼角都在工位区旁边,走过去必然横穿工位排 —— 这才是"路过"。 + 刻意不交给 Jev 决定去哪儿:实测 60s 内 Jev 一次都没派去茶水间/摸鱼角。 + """ + for w in self.workers.values(): + if w.job or w.seated or w.seat_cmd or w.order: + continue + # M2-8:离职的和今天罢工的,不该还在办公室里晃 + if not w.available or w.strike: + continue + if w.acting not in ("在工位待命", "闲逛", "散会", "理论输了", "在茶水间休息", "在摸鱼"): + continue + w.wander_at = getattr(w, "wander_at", 0) + if self.tick < w.wander_at: + continue + spots = [l for l in CAN_GO[w.who] if l in ("lounge", "slack")] + if not spots: + continue + # 已经在茶水间/摸鱼角了就换个地方,别原地重复 + here = "lounge" if w.acting == "在茶水间休息" else ( + "slack" if w.acting == "在摸鱼" else None) + dest = random.choice([s for s in spots if s != here] or spots) + # M2-8:心情 < 20 → 摸鱼概率 +50%(用间隔除以倍率来表达) + base = random.randint(45, 130) + w.wander_at = self.tick + max(10, int(base / ST.slack_multiplier(w.mood))) + w.order = ("goto", (dest, w.who)) + w.acting = f"溜达去{LOC_LABEL[dest]}" + + def _check_exhausted(self, w): + """M2-8 体力见底 → 强制去休息,压过一切指令(连手上的活都扔下)。 + + 单独抽成方法是为了能直接测它:内联在主循环里就只能靠肉眼观察。 + """ + if w.brain_down or not ST.must_rest(w.stamina): + return False + if w.order and w.order[1][0] == "lounge": + return False # 已经在去休息的路上了 + w.order = ("goto", ("lounge", w.who)) + w.acting = "累趴了,去休息" + if w.job: + # 把单子放回队列:累趴一个人不该把客户也拖死, + # 别人体力还够就能接。(不还回去的话这单会一直挂在"服务中"直到流失) + cid = w.job + c = next((x for x in self.clients if x.id == cid), None) + if c is not None and c.status in ("服务中", "抢单中"): + c.served_by = None + c.status = "等待" + c.grab_tick = self.tick + 30 + self.event("exhausted", + f"{w.meta['name']} 体力见底,扔下手上的活去休息", who=w.who) + w.job = None + return True + + def _maybe_banter(self): + """M2-5:干活的人 + 附近闲人 → 可能触发吐槽。""" + if self.banters and len(self.banters) > 6: + self.banters = self.banters[-6:] + pos_fn = lambda w: (w.pos[0], w.pos[1]) + workers = list(self.workers.values()) + for w in workers: + # ★ 闸门只用 w.job(手上有活 = 正在干活,走去工位途中也算)。 + # 原来还要求 acting == "干活中",但那是**显示用的标签**, + # 会被抢单冲突覆盖成"去理论"(实测 295 个工人帧里只有 11 帧是干活中), + # 等于把玩法闸门建在一个会被别人踩掉的字符串上。 + if not w.job or w.seated or not w.available or w.strike: + continue + c = next((x for x in self.clients if x.id == w.job), None) + task = c.task if c else "" + for o, dist in BT.nearby_idle(w, workers, pos_fn): + rel = REL.get(o.who, w.who) + if not BT.should_speak(w.who, o.who, w.mood, o.mood, rel, self._bt_store): + continue + # 让 Jev 决定干活的人要不要反击 —— ★ 问法挪进 _banter_thread。 + # 原来在主循环里同步调,网络请求把整个 tick 堵住: + # 实测主循环被拖到 3.40 tick/s,只有 sleep(0.12) 理论值的 41%, + # 全场移动跟着慢 2.4 倍,于是没人走得到工位、干活中 0 帧。 + # 这一处是全场唯一没开线程的网络调用(serve/conflict/jev 都开了)。 + BT.mark(self._bt_store, w.who, o.who, w.who) # 先占冷却,避免同 tick 重复排队 + threading.Thread(target=self._banter_thread, + args=(w.who, o.who, task), daemon=True).start() + return # 一 tick 只触发一次,避免刷屏 + # ★ 循环体末尾不能有 return:否则只检查 workers 里的第一个人, + # 第一个人没在干活 / 附近没人 / 冷却未到,整 tick 直接结束, + # 另外四个人永远轮不到 —— 实测 70s 自然触发 0 次就是这么来的。 + + def _banter_thread(self, worker_who, passer_who, task): + # ★ 这个方法跑在 daemon 线程里,异常会被线程机制打印到 stderr 然后吃掉。 + # 本次就因为把 ["要反击","装没听见"] 传给了只认 worker id 的 + # _ask_jev_auto(KeyError),吐槽**一次都没发生过**,而所有测试都显示通过 + # —— 因为那两条用例是"可选项,本次未触发不判失败"。 + # 崩了就必须留下事件痕迹,不能悄无声息。 + try: + gen = self.gen + if not self.workers[worker_who].available or not self.workers[passer_who].available: + return + retal = S.ask_jev_choose( + "同事 %s 刚被 %s 说了一句带刺的话:对方阴阳怪气地挑了他的刺。" + "%s 手上有活正干着。%s 要不要当场反击?\n" + "(不反击不会扣关系,只是咽下这口气;反击会把对方也得罪一下。)" + % (self.workers[worker_who].meta["name"], + self.workers[passer_who].meta["name"], + self.workers[worker_who].meta["name"], + self.workers[worker_who].meta["name"]), + {"反击": "当场怼回去,嘴上不能输", + "装没听见": "当作没听见,犯不着为这种小事吵架"}, + "A colleague was just mocked by a passing coworker while working. " + "Should the worker snap back, or let it go?", + qid="retaliate") + retaliate = (retal == "反击") + msgs = BT.build_dialog(worker_who, passer_who, task, retaliate, + self._bt_store, self.tick, stream=self.model_stream) + if not msgs: + return + if not self.gen_alive(gen) or not self.workers[worker_who].available or not self.workers[passer_who].available: + return + res = BT.settle(msgs, worker_who, passer_who, self.tick) + with self._lock: + for who, dm in res["mood"].items(): + w = self.workers.get(who) + if w: + w.mood = max(0, min(100, w.mood + dm)) + self.banters.append({ + "worker": worker_who, "passer": passer_who, "task": task, + "lines": msgs, "tick": self.tick}) + self.banters = self.banters[-6:] + for m in msgs: + self.event("banter", + f"{P.PROVIDERS[m['who']]['name']}:{m['text'][:34]}", + who=m["who"]) + except Exception as e: + self.event("error", f"吐槽出错:{e}") + + def loop(self): + self.running = True + last_jev = 0 + last_spawn = 0 + # ★M4-4:固定步长用"累计截止时间"而不是"每次 sleep 一个定值"。 + # 实测 `time.sleep(0.006)` 在 macOS 上实际睡 ~19ms,空转 tick 只有 + # 理论 166/s 的 31%——**睡得越久偏得越多**,而且倍速越高偏得越狠。 + # 固定 sleep 的误差是累积的,deadline 是自校正的:落后就只补那一点。 + # (落后太多时直接重置,不追赶,否则会变成零 sleep 的忙等。) + next_tick = time.perf_counter() + while self.running: + with self._lock: + self.tick += 1 + # 到达后清指令 + for w in self.workers.values(): + if w.brain_down: + self._freeze_brain(w) + continue + # M2-8:离职的人不再接指令、不再走动 + if not w.on_duty: + w.order = None + w.path = [] + w.acting = "已离职" + continue + if w.seat_cmd or (w.order and w.order[0] == "seat"): + tgt = self._seat_of(w.who) + if tgt and math.dist(w.pos, tgt) <= 0.12 and not w.path: + w.order = None + w.seated = True + w.acting = MEETING_ACT + continue + if w.order: + loc, idx = w.order[1] + tgt = self._loc_of(loc, idx) + if math.dist(w.pos, tgt) <= 0.12 and not w.path: + if loc == "lounge": + w.order = None + w.acting = "在茶水间休息" + elif loc == "slack": + w.order = None + w.acting = "在摸鱼" + elif not w.job: + w.order = None + w.acting = f"在{LOC_LABEL[loc]}" + # 体力/心情恢复 + if w.acting in ("在茶水间休息",): + w.stamina = min(ATTR[w.who]["stamina_max"], + w.stamina + 0.9 * K) + w.mood = min(100, w.mood + 0.5 * K) + if w.acting in ("在摸鱼",): + w.mood = min(100, w.mood + 1.2 * K) + w.stamina = max(0, w.stamina - 0.15 * K) # M4-4:-0.3 → -0.15 + # M2-8:★这里**不能**按 tick 连续扣体力。 + # 实测按 0.45/tick 扣(≈3.15 点/秒)→ 100 体力只撑 32 秒, + # 而一次真实交付要 40~60 秒,于是每个工人都在交付中途累趴、 + # 扔下手上的活,60 秒内 0 单到达待验收、3 单流失。 + # 体力的账记在交付完成时(accept 里每单扣 22),这里只保留微量的涣散。 + if w.job: + w.stamina = max(0, w.stamina - 0.02 * K) + elif not w.seat_cmd and w.acting.startswith("在") \ + and w.acting not in ("在茶水间休息", "在摸鱼"): + # ★M4-4:原来 -0.12/tick = -74/天,**站着不动比干活还掉得快 6 倍**。 + # 而体力 <15 就接不了单 → 没活干 → 只能站着 → 越闲越累,死循环。 + # 实测 23 天长局里体力均值在第 4~7 天塌到 4~19,就是这么来的。 + # 现在站着只微耗(-0.03/tick × 900 tick/天 = -27/天, + # 比干一单还少,且被茶水间 +0.9/tick 一次就拉满盖过去), + # 真正的消耗是 accept 的 -22/单, + # 恢复仍然要去茶水间 —— 该休息还是得休息,只是不会自己饿死。 + w.stamina = max(0, w.stamina - 0.03 * K) + # M2-8:体力见底 → 强制去休息,压过一切指令 + self._check_exhausted(w) + # 客户耐心 + AI 自主抢单 + for c in self.clients: + if c.status == "等待": + if self.tick >= getattr(c, "grab_tick", 10**9): + self._auto_grab(c) + if c.status in ("等待", "服务中", "抢单中"): + c.patience -= K + if c.patience <= 0: + c.status = "已流失" + self.slots.pop(c.slot, None) + self.rep = max(0, self.rep - 3) + self.money = max(0, self.money - 15) + self.event("lost", f"客户等太久走了:{c.task}", extra={"client": c.id}) + # M2-6:失败在发生的这一刻记下来。打回的那单在 clients 里 + # status 是"已完成",事后反推查不出来。 + self.failures.append({"task": c.task, + "why": "客户等太久走了", + "who": c.served_by}) + if c.served_by and self.workers.get(c.served_by): + self.workers[c.served_by].job = None + self._retire_client(c) # M3-9:不 retire 的话会一直堆积 + self.note("跑了一个客户,声誉-3") + for w in self.workers.values(): + w.tick_typing() # M4-2:太久没吐字就当停了 + self._wander_idle() + self._maybe_banter() + self.maybe_start_blame() + # 复盘会结算完不能一直开着:留着不走,五个人在会议室站到天亮。 + # 玩家想多看两眼可以先手动 dismiss;BLAME_HOLD 秒后自动散会放人。 + if (self.meeting and self.meeting.get("kind") == "blame" + and self.meeting.get("phase") == "settled" + and self.meeting.get("settled_at") + and time.time() - self.meeting["settled_at"] > BLAME_HOLD / TIME_SCALE): + self.meeting = None + for w in self.workers.values(): + if w.brain_down: + continue + w.seat_cmd = False + w.seated = False + w.order = None + w.acting = "散会" + self.send_all_back() + self.event("meeting_end", "复盘会结束") + self.refresh_paths() + self.move() + + # Jev 决策:每 3 秒一次(真实时间 ÷ TIME_SCALE) + if time.time() - last_jev > 3.0 / TIME_SCALE: + last_jev = time.time() + threading.Thread(target=self.jev_decide, daemon=True).start() + # 客户刷新:每 SPAWN_SEC 秒来一个(测试可调快) + if time.time() - last_spawn > SPAWN_SEC / TIME_SCALE: + last_spawn = time.time() + with self._lock: + self.spawn_client() + # ★M4-4 自动跨天:DAY_TICKS 到了就结算。 + # 以前跨天只能靠外部打 /api/newday,而"30 天长局"那边一天只给 + # 1.2 秒,于是体力/工资这些**按天计**的量根本没有任何一天的分母。 + if AUTO_DAY and self.tick - self._day_start >= DAY_TICKS: + self._advance_day() + next_tick += TICK_SEC / TIME_SCALE + now = time.perf_counter() + # 允许**有界追赶**:落后几拍就在后面几拍补回来。 + # 直接 `next_tick = now` 会把每一次偶发卡顿(GC、线程抢 GIL)都变成 + # 永久丢时——实测 0.12s 步长能跑满,6ms 步长只有理论值的 65%, + # 差的就是这部分被吞掉的补偿。落后超过 4 拍才认定是真卡死,重置。 + if next_tick < now - 4 * TICK_SEC / TIME_SCALE: + next_tick = now + else: + time.sleep(max(0.0, next_tick - now)) + + # ── 每日结算(M2-8)── + def bump_gen(self) -> int: + """换代。所有长线程每步都该拿它和自己的起点比。""" + self.gen += 1 + return self.gen + + def gen_alive(self, g) -> bool: + """线程还该不该继续。世代号变了 = 状态被重置/读档过了 = 该收工。""" + return g == self.gen + + def _advance_day(self): + """跨天。自动跨天(AUTO_DAY)和 /api/newday 共用这一个入口。 + + ★M4-4:以前这两条路各写各的,`/api/newday` 只加 day 不重置计数, + 手动推日和自动推日混用时会把一天结算两次。统一到这里就没有第二条路了。 + """ + self.day += 1 + self._day_start = self.tick + self.note("新的一天") + self.event("newday", f"第 {self.day} 天开始") + return self.daily_settle() + + def daily_settle(self): + """跨天时跑一次:工资 → 疲劳/罢工/离职 → 自然衰减 → 失败检测。 + + 返回一份明细,直接透给客户端当日结算面板。 + """ + on_duty = [w for w in self.workers.values() if w.on_duty] + # 1) 工资 + 加班补贴(疲劳高的是在加班) + overtime = sum(1 for w in on_duty if w.fatigue >= 2) + self.money, detail = ST.daily_settle(self.money, len(on_duty), overtime) + self.event("wages", "发工资 -%d 元%s" + % (detail["wages"] + detail["overtime"], + ("(含加班补贴 %d)" % detail["overtime"]) if overtime else "")) + if self.money < 0: + self.neg_days += 1 + else: + self.neg_days = 0 + + # 2) 疲劳 → 罢工 → 离职 + quits, strikes = [], [] + for w in on_duty: + if w.brain_down: + w.worked_today = False + continue + if w.strike: + w.fatigue = ST.FATIGUE_MAX # 罢工中,疲劳维持 + elif w.off_today: + # ★M4-4 轮休:疲劳清零。这是 DESIGN 6.5「主动安排轮休」的兑现, + # 也是这套疲劳系统唯一能被玩家影响的入口(原来只涨不跌)。 + w.fatigue = 0 + elif w.worked_today: + w.fatigue = ST.next_fatigue(w.fatigue, True) + else: + w.fatigue = max(0, w.fatigue - 1) # 歇一天缓一点 + if ST.should_strike(w.fatigue): + w.strike = True + w.strike_days += 1 + strikes.append(w) + if ST.should_quit(w.strike_days): + w.quit = True + w.strike = False + quits.append(w) + else: + w.strike_days = 0 + w.worked_today = False + w.off_today = False # 轮休只管当天,跨天自动恢复排班 + for w in strikes: + self.event("strike", "%s 罢工了,今天什么都不干(连续第 %d 天)" + % (w.meta["name"], w.strike_days), who=w.who) + for w in quits: + self.event("quit", "%s 罢工满两天,递交辞呈,离职了" % w.meta["name"], who=w.who) + # 关系差的人跟着走(M2-3):判定方向是"还在的人对走掉的人" + for other in REL.quit_followers(w.who): + ow = self.workers.get(other) + if ow and ow.on_duty and random.random() < 0.5: + ow.quit = True + ow.strike = False + self.event("quit", f"{ow.meta['name']} 和 {w.meta['name']} 处不来," + f"也跟着走了", who=other) + + # 3) 自然衰减 + # ★M4-4:轮休的人算"歇了"——体力回满,和在茶水间休息一个待遇。 + # 不这样的话轮休只清疲劳不回体力,轮休反而是亏的,玩家不会用。 + rested = [w.who for w in on_duty + if w.acting in ("在茶水间休息", "散会") or w.off_today] + slackers = [w.who for w in on_duty if w.strike] + st, md = ST.natural_decay( + {w.who: w.stamina for w in self.workers.values()}, + {w.who: w.mood for w in self.workers.values()}, + rested=rested, slackers=slackers) + for w in self.workers.values(): + if not w.brain_down: + w.stamina, w.mood = st[w.who], md[w.who] + + # 4) 失败检测 + on_duty = [w for w in self.workers.values() if w.on_duty] + reason = ST.game_over(self.money, self.rep, on_duty, self.neg_days) + if reason: + self.game_over_reason = reason + self.event("game_over", "游戏结束:%s" % reason) + out = { + "wages": detail, "overtime_days": overtime, + "quits": [w.who for w in quits], + "strikes": [w.who for w in strikes], + "on_duty": [w.who for w in on_duty], + "game_over": reason, + } + self.last_settle = out + return out + + def team_build(self): + """团建:花钱给全员回心情。""" + if self.money < ST.TEAM_BUILD: + return {"ok": False, "msg": "钱不够团建(要 %d)" % ST.TEAM_BUILD} + self.money -= ST.TEAM_BUILD + for w in self.workers.values(): + if w.brain_down: + continue + w.mood = min(ST.MOOD_MAX, w.mood + ST.TEAM_BUILD_MOOD) + w.fatigue = max(0, w.fatigue - 1) + self.event("team_build", "团建,花了 %d 元,全员心情 +%d" + % (ST.TEAM_BUILD, ST.TEAM_BUILD_MOOD)) + return {"ok": True, "msg": "团建完成,全员心情 +%d" % ST.TEAM_BUILD_MOOD} + + # ── 老板聊天(M2-7)── + CHAT_HISTORY = {} # who -> [{"role","content"}],跨请求保留 + + # 老板聊天里的特殊指令。用**整句精确匹配**,不用 `kw in text`: + # 子串匹配会把"今天天气不错啊"判成表扬(它确实含"不错"),这种假阳性很难查。 + CHAT_REST = {"休息", "去休息", "歇会", "去茶水间", "休息一下"} + CHAT_PRAISE = {"干得不错", "干得好", "辛苦了", "厉害", "干得漂亮", + "做得好", "不错啊你", "牛", "好样的"} + + def chat_command(self, who, text): + """聊天框里的特殊指令。识别到就**不调模型**,直接改状态。 + + 识别不到返回 None,走正常对话。识别到返回 dict,HTTP 层直接回结果。 + """ + w = self.workers.get(who) + if not w: + return None + if w.brain_down: + return {"kind": "brain_down", "msg": "脑袋宕机了,额度恢复后再重启脑袋"} + t = (text or "").strip() + norm = t.rstrip("!!。.,,、~~ ").strip() + if norm in self.CHAT_REST: + self.command(who, "rest") + return {"kind": "rest", "msg": f"{w.meta['name']} 去茶水间休息了"} + if norm in self.CHAT_PRAISE: + w.mood = min(100, w.mood + 5) + self.event("boss_praise", f"你夸了 {w.meta['name']} 一句,心情 +5", who=who) + return {"kind": "praise", "msg": f"{w.meta['name']} 心情 +5(现在是 {w.mood})"} + return None + + def chat_brief(self, who): + """给老板聊天用的开场状态。比普通状态全:带上这个角色自己的状态和关系。""" + w = self.workers.get(who) + parts = [f"第{self.day}天 · {self.money}元 · 声誉{self.rep}"] + if w: + job = "" + if w.job: + c = next((x for x in self.clients if x.id == w.job), None) + job = c.task if c else "手上的活" + parts.append(f"你自己:心情 {int(w.mood)}/100 体力 " + f"{int(w.stamina)}/{ATTR[who]['stamina_max']} " + f"{'手上有活「%s」' % job if job else '手上没活'}({w.acting})") + others = [o for o in REL.ALL if o != who] + best = max(others, key=lambda o: REL.get(who, o)) + worst = min(others, key=lambda o: REL.get(who, o)) + parts.append(f"和同事:跟 {P.PROVIDERS[best]['name']} " + f"{REL.describe_pair(who, best)}," + f"和 {P.PROVIDERS[worst]['name']} {REL.describe_pair(who, worst)}") + return "【事务所状态】" + ";".join(parts) + "。\n" + + def chat_push(self, who, user_text, reply_text): + """把这一轮存进历史,下一轮老板接着聊时模型还记得。""" + log = self.CHAT_HISTORY.setdefault(who, []) + log.append({"role": "user", "content": user_text}) + if reply_text: + log.append({"role": "assistant", "content": reply_text}) + # 只留最近 8 轮:再往前老板自己都忘了,留着纯烧 token + if len(log) > 16: + del log[:-16] + + def chat_history(self, who): + return list(self.CHAT_HISTORY.get(who, [])) + + # ── 会议室流程(M2-2 座位 / M2-6 复盘发言)── + def start_meeting(self, topic: str = "周会", kind: str = "normal"): + """召集全员到会议室坐下。kind="blame" 时走 M2-6 的逐个发言。""" + with self._lock: + if self.meeting: + return {"ok": False, "msg": "已经在开会了"} + who_list = [w.who for w in self.workers.values() if w.available] + if not who_list: + return {"ok": False, "msg": "大家的脑袋都宕机了,暂时开不了会"} + self.meeting = {"topic": topic, "seated": [], "phase": "gathering", + "started": self.tick, "kind": kind, + "lines": [], "settled": None} + self.event("meeting_start", f"开会:{topic}") + for w in who_list: + self.workers[w].order = ("seat", None) + self.workers[w].seat_cmd = True + self.workers[w].acting = "去开会" + threading.Thread(target=self._meeting_thread, daemon=True).start() + return {"ok": True, "msg": f"开会:{topic}"} + + def _meeting_thread(self): + with self._lock: + topic = self.meeting["topic"] if self.meeting else "周会" + kind = (self.meeting.get("kind") or "normal") if self.meeting else "normal" + # 等所有人走到座位(最多 60s) + t0 = time.time() + while time.time() - t0 < 60 / TIME_SCALE: + with self._lock: + if not self.meeting: + return + seated = [w.who for w in self.workers.values() + if w.seated and w.available] + self.meeting["seated"] = seated + on_duty = [w for w in self.workers.values() if w.available] + if not on_duty or len(seated) >= len(on_duty): + break + time.sleep(0.4 / TIME_SCALE) + with self._lock: + if not self.meeting: + return + self.meeting["phase"] = "seated" + for w in self.workers.values(): + if w.available: + w.acting = MEETING_ACT + if kind == "blame": + try: + self._blame_dialogue() + except Exception as e: + self.event("error", f"复盘会出错:{e}") + else: + time.sleep(1.0 / TIME_SCALE) + + # ── M2-6 甩锅复盘 ── + def _blame_dialogue(self): + """五人依次发言,带上前文;老板可随时插话。全部真实 LLM 调用。""" + with self._lock: + if not self.meeting: + return + failures = list(self.failures) + day = self.day + meeting, gen = self.meeting, self.gen + pool = [who for who in S.ORDER if self.workers[who].available] + # jev_order 返回完整顺序;网络等待期间不持有游戏状态锁。 + order = S.jev_order("复盘会:这一单砸了,谁先开口定性最合适", pool=pool) if pool else None + with self._lock: + if self.meeting is not meeting or not self.gen_alive(gen): + return + seq = order or pool + self.meeting["phase"] = "talking" + for idx, who in enumerate(seq): + with self._lock: + if not self.meeting: + return + if not self.workers[who].available: + continue + lines = list(self.meeting["lines"]) + workers = [w.who for w in self.workers.values() if w.available] + out = BG.speak(who, failures, day, workers, lines, first=(idx == 0), + stream=self.model_stream) + if not out: + continue + with self._lock: + if not self.meeting: + return + self.meeting["lines"].append(out) + self.event("blame", + "%s:%s" % (P.PROVIDERS[who]["name"], out["text"]), + who=who) + # 结算:被点名最多的那个背锅 + with self._lock: + if not self.meeting: + return + lines = list(self.meeting["lines"]) + res = BG.settle(lines, self.tick) + with self._lock: + if not self.meeting: + return + for who, dm in res["mood"].items(): + w = self.workers.get(who) + if w and not w.brain_down: + w.mood = max(0, min(100, w.mood + dm)) + self.meeting["settled"] = res + self.meeting["phase"] = "settled" + self.meeting["settled_at"] = time.time() + self._blame_store["last_blame_day"] = self.day + self.failures = [] # 这批已经复盘过 + if res["culprit"]: + self.event("blame_result", + "%s 被推出来背锅,心情 -6" + % P.PROVIDERS[res["culprit"]]["name"], + who=res["culprit"]) + else: + self.event("blame_result", "没人被点名,这锅没着落") + + def blame_talk(self, text: str): + """老板插话。内容进 transcript,后面的发言会看到。""" + text = P.truncate((text or "").strip(), 80) + if not text: + return {"ok": False, "msg": "说点什么"} + with self._lock: + if not self.meeting or self.meeting.get("kind") != "blame": + return {"ok": False, "msg": "现在没有复盘会"} + if self.meeting.get("phase") == "settled": + return {"ok": False, "msg": "会已经开完了"} + self.meeting["lines"].append({"who": "boss", "text": text, + "blames": []}) + self.event("boss_say", f"老板插话:{text}") + return {"ok": True, "msg": "插话成功,后面的人会听见"} + + def maybe_start_blame(self): + """主循环里轮询:有失败单就开复盘会。冷却一天。 + + ★不能在有活在跑的时候**立刻**开:会议 seat_cmd 优先级压过干活, + 正在交付的工人会被叫去开会、扔下手上的活,客户耐心耗尽就流失了, + 流失又产生新的失败单 → 又触发下一场复盘会(实测会把派单闭环卡死)。 + + 但也不能无限等:客户源源不断的时候几乎永远有人在干活, + 复盘会就成了永不触发的死代码(实测 90s 内 0 次触发)。 + 所以是**有上限的等待**:先护一会儿,超时就插进去—— + 交付线程里有 _await_meeting_end 兜底,散会后会继续干,不至于丢单。 + """ + with self._lock: + if self.meeting or not BG.should_trigger(self.failures, self.day, + self._blame_store): + return + busy = [w for w in self.workers.values() + if w.job and w.on_duty and not w.strike] + if busy: + if not self._blame_want_at: + self._blame_want_at = time.time() + if time.time() - self._blame_want_at < BLAME_MAX_WAIT / TIME_SCALE: + return # 先护着手上的活 + else: + self._blame_want_at = 0 + self._blame_want_at = 0 + topic = "复盘会:这一单砸了" + r = self.start_meeting(topic, kind="blame") + if r.get("ok"): + self.event("blame_start", "出事了,全员开复盘会") + + def send_all_back(self): + """会开完把人送回工位,否则散会后一群人站在会议室不走。""" + with self._lock: + for w in self.workers.values(): + if w.seated or not w.available: + continue + w.order = ("goto", ("desk", w.who)) + w.acting = "回工位" + + def dismiss_meeting(self): + with self._lock: + if not self.meeting: + return {"ok": False, "msg": "没在开会"} + self.meeting = None + for w in self.workers.values(): + if w.brain_down: + continue + if w.order and w.order[0] == "seat": + w.order = None + w.seat_cmd = False + w.seated = False + w.acting = "散会" + self.send_all_back() + self.event("meeting_end", "散会") + return {"ok": True, "msg": "散会"} + + def snapshot(self): + with self._lock: + _rx, _ry = LOCATIONS["reception"] + return { + "version": 1, # 契约版本,客户端据此判断格式是否变更 + "art_layout_version": 3, + "gameplay_version": 5, + "scene": "office", + "day": self.day, "money": self.money, "rep": self.rep, + "tick": self.tick, + "clock": "%02d:%02d" % (((self.tick - self._day_start) * TICK_GAME_MIN // 60) % 24, + (self.tick - self._day_start) * TICK_GAME_MIN % 60), + "grid": {"w": GW, "h": GH}, + "walls": [list(p) for p in WALLS], + "locations": {k: list(v) for k, v in LOCATIONS.items()}, + "loc_label": LOC_LABEL, + "workers": [{ + "id": w.who, "name": w.meta["name"], "color": w.meta["color"], + "tag": w.meta["tag"], "blurb": w.meta["blurb"], + "x": round(w.pos[0], 2), "y": round(w.pos[1], 2), + "speed": ATTR[w.who]["speed"], + "path": [list(p) for p in w.path], + "stamina": round(w.stamina), "stamina_max": ATTR[w.who]["stamina_max"], + "mood": round(w.mood), + "acting": w.acting, "job": w.job, "seated": w.seated, "seat_cmd": w.seat_cmd, + "quality": ATTR[w.who]["quality"], + # M2-8 动态属性透出,客户端据此画状态条 + "fatigue": w.fatigue, "strike": w.strike, + # M4-2:客户端据此让打字音跟模型输出同步 + "typing": w.typing, "type_cps": round(w.type_cps, 1), + # ★M4-4 轮休状态。客户端要能显示"今天轮休", + # 否则玩家看板上这个人和"罢工"长得一样,分不清是奖赏还是惩罚。 + "off_today": w.off_today, + "brain_down": w.brain_down, "brain_reason": w.brain_reason, + "brain_checking": w.brain_checking, + "strike_days": w.strike_days, "quit": w.quit, + "eff": round(getattr(w, "eff", 1.0), 2), + "log": w.log, + } for w in self.workers.values()], + "clients": [{ + "id": c.id, "task": c.task, "reward": c.reward, + "patience": c.patience, "patience_max": c.patience_max, + "status": c.status, "served_by": c.served_by, + "slot": c.slot, "result": c.result, + "grab_in": max(0, c.grab_tick - self.tick), + # M3-7:需求类型 + 人形,客户端据此挑 sprite + "kind": c.kind, "persona": c.persona, + "persona_name": c.persona_name, + # 客户端要画客户:入场动画中在 pos,完成后在队列位 + "x": c.pos[0], "y": c.pos[1], + "queue_x": 8, "queue_y": round(_ry - 1 + (c.slot or 0) * 2, 2), + } for c in self.clients], + # ★M4-4:把滚动存档里的最近 4 单也透出去。 + # M3-9 加 _retire_client() 之后,完成/流失的客户立刻从 clients 移走 + # (那是对的:clients 原来会无界增长,30 天到上百条), + # 但 finished 没进快照,于是**完成这一态在接口上彻底不可见**—— + # e2e_m1 用例 8「客户状态变已完成」就是这么红的。 + # 补上这个字段,而不是把 _retire_client 撤回去: + # 两个问题里只有"完成态看不见"需要修,"列表增长"是真 bug。 + "finished": [{ + "id": c.id, "task": c.task, "reward": c.reward, + "status": c.status, "served_by": c.served_by, + "result": c.result, + } for c in self.finished], + "conflicts": self.conflicts_view(), + # M2-8 经济与失败状态 + "neg_days": self.neg_days, + "game_over": self.game_over_reason, + "last_settle": self.last_settle, + "on_duty": [w.who for w in self.workers.values() if w.on_duty], + "banters": self.banters[-4:], + "relations": REL.summary(), + "relation_log": REL.LOG[-20:], + "meeting": self.meeting, + "seats": {w: list(p) for w, p in SEATS.items()}, + "table": list(MEETING_TABLE), + "slots": sorted(self.slots.keys()), + "events": self.events[-40:], + "log": self.log[-20:], + } + + +GAME = Game() diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/game_app.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/game_app.py new file mode 100644 index 00000000..fa43e1bd --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/game_app.py @@ -0,0 +1,258 @@ +"""AI 事务所 —— 游戏后端(Godot 客户端的唯一数据源) + +端口 8850。API key 只存在这一层,Godot 客户端永远拿不到。 + +启动:.venv/bin/python server/game_app.py +客户端:http://127.0.0.1:8849/ (开发用 Web 原型,非最终游戏) +""" +import os +import json +import threading +import time +from flask import Flask, Response, request, jsonify, send_from_directory + +HERE = os.path.dirname(__file__) + +import game as G +import save as SAVE +import providers as P + +app = Flask(__name__, static_folder=None) +_boot_lock = threading.Lock() + +# 老板单次对话上限(防玩家打一篇长文烧掉额度) +CHAT_MAX_CHARS = 200 + +# ★M4-4 加的 off/back 必须也在这个白名单里。 +# game.Game.command() 认它们,但 HTTP 层有**自己一份** allowlist, +# 漏了的话 API 直接 400「未知指令:off」——而单测是直接调 g.command() 的, +# 绕过了这一层,所以 82 条断言全绿、真发 HTTP 才发现。 +VALID_ACTIONS = ("goto", "rest", "assign", "accept", "meeting", "dismiss", + "blame_talk", "team_build", "off", "back", "reboot") +# 这些动作由游戏整体执行,不需要指定单个角色 +GLOBAL_ACTIONS = ("accept", "meeting", "dismiss", "judge", "blame_talk", + "team_build") + + +@app.after_request +def no_cache(resp): + resp.headers["Cache-Control"] = "no-store" + return resp + + +@app.errorhandler(404) +def not_found(_e): + return jsonify({"ok": False, "error": "not found"}), 404 + + +@app.errorhandler(500) +def server_error(e): + return jsonify({"ok": False, "error": "internal"}), 500 + + +@app.route("/") +def index(): + return send_from_directory(os.path.join(HERE, "..", "web"), "game.html") + + +@app.route("/api/state") +def state(): + return jsonify(G.GAME.snapshot()) + + +@app.route("/api/command", methods=["POST"]) +def command(): + d = request.get_json(silent=True) or {} + action, arg = d.get("action"), d.get("arg") + who = d.get("who") or d.get("id") + + # 全局动作不需要 who —— 必须在 who 校验之前分出去, + # 否则发 {action:"accept"/"meeting"/"dismiss"} 会被误判成"缺少 who"。 + if action == "accept": + return jsonify(G.GAME.accept(arg, bool(d.get("approved", True)))) + if action == "judge": + return jsonify(G.GAME.judge(arg, d.get("kind", ""), G.GAME.tick)) + if action == "meeting": + return jsonify(G.GAME.start_meeting(arg or "周会")) + if action == "dismiss": + return jsonify(G.GAME.dismiss_meeting()) + if action == "blame_talk": + # 同样是全局动作:老板在复盘会上插话,不带 who + return jsonify(G.GAME.blame_talk(arg)) + if action == "team_build": + return jsonify(G.GAME.team_build()) + if not who: + return jsonify({"ok": False, "msg": "缺少 who"}), 400 + if action not in VALID_ACTIONS: + return jsonify({"ok": False, "msg": f"未知指令:{action}", + "valid": list(VALID_ACTIONS)}), 400 + return jsonify(G.GAME.command(who, action, arg)) + + +@app.route("/api/accept", methods=["POST"]) +def accept(): + d = request.get_json(silent=True) or {} + return jsonify(G.GAME.accept(d.get("client"), bool(d.get("approved")))) + + +# ── M4-3 存档 ─────────────────────────────────────────────────────────── +@app.route("/api/save", methods=["POST"]) +def api_save(): + d = request.get_json(silent=True) or {} + name = d.get("name") or "手动存档" + try: + sid = SAVE.save_game(G.GAME, name=name, auto=bool(d.get("auto")), + why=d.get("why", "")) + except Exception as e: # 存不了不能把游戏带崩 + return jsonify({"ok": False, "msg": "存档失败:%s" % e}), 500 + SAVE.prune(keep=int(os.environ.get("GAME_SAVE_KEEP", "40"))) + return jsonify({"ok": True, "id": sid, "name": name}) + + +@app.route("/api/load", methods=["POST"]) +def api_load(): + d = request.get_json(silent=True) or {} + got = SAVE.load_game(d.get("id")) + if not got.get("ok"): + return jsonify(got), 200 # 坏档不是服务器错误,是玩家的选择问题 + # ★读档前必须停主循环:否则下一 tick 就会把还原出来的位置/路径重算一遍, + # 读档等于没读。停 → 还原 → 重启,中间这段就是读档窗口。 + G.GAME.running = False + time.sleep(0.4) + r = SAVE.restore(G.GAME, got) + if r.get("ok"): + threading.Thread(target=G.GAME.loop, daemon=True).start() + return jsonify({**r, "id": d.get("id"), "name": got.get("name"), + "day": G.GAME.day, "money": G.GAME.money}) + + +@app.route("/api/saves", methods=["GET"]) +def api_saves(): + return jsonify({"ok": True, "saves": SAVE.list_saves()}) + + +@app.route("/api/newday", methods=["POST"]) +def newday(): + # M4-4:走 game._advance_day(),不再自己 day += 1。 + # 手动推日原来不重置 _day_start,和 AUTO_DAY 混用会一天结算两次。 + settle = G.GAME._advance_day() + # M4-3:跨天自动存档。失败现场**先**单独存一份手动档—— + # 破产那一刻的现场是玩家最想回看的,而自动档会被 prune 清掉。 + over = G.GAME.game_over_reason + fail_sid = None + if over: + fail_sid = SAVE.save_game(G.GAME, name="第%d天失败现场" % G.GAME.day, + auto=False, why=over) + sid = SAVE.save_game(G.GAME, name="第%d天结算" % G.GAME.day, auto=True, + why=over or "每日结算") + SAVE.prune(keep=int(os.environ.get("GAME_SAVE_KEEP", "40"))) + return jsonify({"ok": True, "day": G.GAME.day, "settle": settle, + "money": G.GAME.money, "rep": G.GAME.rep, + "autosave": sid, "fail_save": fail_sid}) + + +CHAT_SYSTEM_MAX_CHARS = 600 + + +def chat_system(who): + """老板 1:1 聊天的人设。和 providers.persona 分开: + 那份是"五个人开会"的口径(会复述别人的发言),单聊时要换掉。""" + p = P.PROVIDERS[who] + return ( + f"你是{p['name']}({p['full']}),人设「{p['tag']}」:{p['blurb']}。\n" + f"现在你在事务所里,**单独**和老板说话,别的同事不在场。\n" + f"【硬性输出限制】\n" + f"1. 最多 3 句,总字数不超过 80 字。\n" + f"2. 口语化,像真人回消息。禁止分点罗列、写小标题、总结全场。\n" + f"3. 不要复述上面给你的状态信息,老板已经知道了。\n" + f"4. 可以带点你自己的脾气和立场。" + ) + + +@app.route("/api/chat", methods=["POST"]) +def chat(): + """老板与单个 AI 对话。SSE 流式。""" + d = request.get_json(silent=True) or {} + who, text = d.get("who"), (d.get("text") or "").strip() + if who not in P.PROVIDERS: + # 用例要求"错误事件,不 500":光回 400 不够,游戏里也要留下痕迹 + G.GAME.event("error", f"老板想找「{who}」说话,但没这个人") + return jsonify({"ok": False, "msg": f"没有这个角色:{who}"}), 400 + if not text: + return jsonify({"ok": False, "msg": "说点什么吧"}), 400 + if len(text) > CHAT_MAX_CHARS: + # ★ 必须在调模型之前挡住:否则玩家一篇长文就把额度烧掉了 + G.GAME.event("chat_reject", + f"老板给 {who} 发了一段 {len(text)} 字的长文,被挡下") + return jsonify({"ok": False, + "msg": f"太长了({len(text)}字),一次最多 {CHAT_MAX_CHARS} 字"}), 400 + + # 特殊指令:识别到就不调模型,直接改状态 + act = G.GAME.chat_command(who, text) + if act: + if act.get("kind") == "brain_down": + return Response("data: %s\n\n" % json.dumps( + {"type": "error", "who": who, "brain_down": True, "message": act["msg"]}, + ensure_ascii=False), mimetype="text/event-stream") + G.GAME.chat_push(who, text, act["msg"]) + return jsonify({"ok": True, "msg": act["msg"], "effect": act}) + + w = G.GAME.workers.get(who) + brief = G.GAME.chat_brief(who) + hist = G.GAME.chat_history(who) + budget = P.chat_max_tokens(who) + generation = G.GAME.gen + + def gen(): + yield "data: %s\n\n" % json.dumps({"type": "start", "who": who}) + got = "" + failed = False + for ev in G.GAME.model_stream(who, brief + text, history=hist or None, + system=chat_system(who), + max_completion_tokens=budget): + if ev.get("type") == "delta": + got += ev["text"] + elif ev.get("type") == "error": + failed = True + yield "data: %s\n\n" % json.dumps(ev, ensure_ascii=False) + if not G.GAME.gen_alive(generation) or G.GAME.workers.get(who) is not w: + return + if failed: + got = "" + reply = P.truncate(got.strip(), 200) + G.GAME.chat_push(who, text, reply) + yield "data: %s\n\n" % json.dumps( + {"type": "finish", "who": who, "got_text": bool(reply)}) + if w and w.available: + w.say("老板:%s" % text[:16]) + if reply: + w.say(reply[:16]) + + return Response(gen(), mimetype="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}) + + +def boot(): + with _boot_lock: + threading.Thread(target=G.GAME.loop, daemon=True).start() + + +if __name__ == "__main__": + # 切换美术版本时先还原局面,再启动循环。 + resume_id = os.environ.get("AI_OFFICE_LOAD_SAVE", "") + if resume_id: + loaded = SAVE.load_game(resume_id) + if not loaded.get("ok"): + raise SystemExit("无法恢复接入前的存档") + restored = SAVE.restore(G.GAME, loaded) + if not restored.get("ok"): + raise SystemExit("存档恢复失败") + print("[BOOT] 已恢复接入前局面", flush=True) + boot() + print("=" * 58) + print(" AI 事务所 后端 · http://127.0.0.1:8850") + for w in P.ORDER: + p = P.PROVIDERS[w] + print(f" · {p['name']:10s} {p['full']}") + print("=" * 58, flush=True) + app.run(host="127.0.0.1", port=8850, threaded=True, debug=False) diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/miniapp_bridge.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/miniapp_bridge.py new file mode 100644 index 00000000..32ed8d9c --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/miniapp_bridge.py @@ -0,0 +1,123 @@ +"""MiniApp 的私有 Python 子进程;JSON 行协议,不监听端口。""" +import argparse +import json +import os +from pathlib import Path +import signal +import sys +import threading + +ROUTES = {'/api/state': 'GET', '/api/saves': 'GET', '/api/command': 'POST', + '/api/chat': 'POST', '/api/save': 'POST', '/api/load': 'POST', + '/api/newday': 'POST', '/api/accept': 'POST'} + + +def run(): + parser = argparse.ArgumentParser() + parser.add_argument('--data-dir', type=Path, required=True) + parser.add_argument('--env-file', type=Path, required=True) + parser.add_argument('--initial-save', type=Path) + args = parser.parse_args() + args.data_dir.mkdir(parents=True, exist_ok=True) + saves = args.data_dir / 'saves' + saves.mkdir(exist_ok=True) + os.environ['AI_OFFICE_SAVE_DIR'] = str(saves) + os.environ['AI_OFFICE_ENV_PATH'] = str(args.env_file) + from dotenv import load_dotenv + load_dotenv(args.env_file) + # stdout 只输出协议帧;依赖和业务模块的诊断不会污染 Host 通道。 + output = sys.stdout + sys.stdout = sys.stderr + import game as G + import game_app as API + import save as SAVE + valid_saves = [SAVE.load_game(s['id']) for s in SAVE.list_saves() if s.get('ok')] + latest = max(valid_saves, key=lambda s: s.get('saved_at', 0), default=None) + if latest: + if not SAVE.restore(G.GAME, latest).get('ok'): + raise RuntimeError('Private save could not be restored') + elif args.initial_save: + loaded = json.loads(args.initial_save.read_text()) + if not SAVE.restore(G.GAME, loaded).get('ok'): + raise RuntimeError('Initial save could not be restored') + SAVE.save_game(G.GAME, '从原游戏继承的进度', why='首次打开 MiniApp 时只读继承') + write_lock = threading.Lock() + jobs = {} + stopped = threading.Event() + + def emit(frame): + with write_lock: + output.write(json.dumps(frame, ensure_ascii=False) + '\n') + output.flush() + + def stop(*_args): + if stopped.is_set(): + return + stopped.set() + G.GAME.running = False + G.GAME.bump_gen() + for cancel in list(jobs.values()): + cancel.set() + SAVE.save_game(G.GAME, 'MiniApp关闭前进度', why='MiniApp 生命周期结束时保存') + + def dispatch(frame, cancelled): + rid = frame['id'] + response = None + try: + path, method = frame.get('path'), frame.get('method') + if ROUTES.get(path) != method: + emit({'id': rid, 'type': 'error', 'message': '不支持这个操作'}) + return + response = API.app.test_client().open(path, method=method, + json=frame.get('body') or {}, buffered=False) + emit({'id': rid, 'type': 'head', 'status': response.status_code, + 'contentType': response.content_type}) + for chunk in response.response: + if cancelled.is_set() or stopped.is_set(): + break + emit({'id': rid, 'type': 'data', + 'body': chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk}) + emit({'id': rid, 'type': 'end'}) + except Exception: + emit({'id': rid, 'type': 'error', 'message': '本次操作未完成,请稍后再试'}) + finally: + if response is not None: + response.close() + jobs.pop(rid, None) + + def stop_signal(*_args): + stop() + raise SystemExit(0) + + signal.signal(signal.SIGTERM, stop_signal) + signal.signal(signal.SIGINT, stop_signal) + API.boot() + emit({'type': 'ready', 'gameplayVersion': 5}) + try: + for line in sys.stdin: + if stopped.is_set(): + break + try: + frame = json.loads(line) + except (ValueError, TypeError): + continue + if frame.get('type') == 'shutdown': + stop() + emit({'type': 'stopped'}) + break + rid = frame.get('id') + if frame.get('type') == 'cancel': + if rid in jobs: + jobs[rid].set() + continue + if not isinstance(rid, int) or rid in jobs or len(jobs) >= 32: + continue + cancelled = threading.Event() + jobs[rid] = cancelled + threading.Thread(target=dispatch, args=(frame, cancelled), daemon=True).start() + finally: + stop() + + +if __name__ == '__main__': + run() diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/providers.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/providers.py new file mode 100644 index 00000000..c75d53fe --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/providers.py @@ -0,0 +1,320 @@ +"""五模型统一调用封装。 +每家的 endpoint / 思考内容格式都不同,这里统一成同一种流式事件。 +""" +import os, json, time, threading +import requests +from dotenv import load_dotenv + +load_dotenv(os.environ.get("AI_OFFICE_ENV_PATH") or + os.path.join(os.path.dirname(__file__), "..", ".env")) + +# ── 实测通过的 endpoint(2026-10-04 探测,见 server/probe.py)──────────────── +PROVIDERS = { + "m3": { + "name": "M3", + "full": "MiniMax-M3", + "base": "https://api.minimaxi.com/v1", + "model": "MiniMax-M3", + "key_env": "MINIMAX_API_KEY", + "color": "#7DC6FF", + "tag": "严谨派", + "blurb": "分点狂魔,输出长,讲方法论", + "latency": 3.9, + }, + "flash": { + "name": "Flash", + "full": "MiniMax-M3.1-Flash-Preview", + "base": "https://api.minimaxi.com/v1", + "model": "MiniMax-M3.1-Flash-Preview", + "key_env": "MINIMAX_API_KEY", + "color": "#FFD166", + "tag": "话痨派", + "blurb": "秒回打杂王,速度就是正义", + "latency": 1.4, + }, + "longcat": { + "name": "LongCat", + "full": "LongCat-2.5-Preview", + "base": "https://api.longcat.chat/openai/v1", + "model": "LongCat-2.5-Preview", + "key_env": "LONGCAT_API_KEY", + "color": "#06D6A0", + "tag": "性价比派", + "blurb": "美团出品,能干且不挑活", + "latency": 3.2, + }, + "kimi": { + "name": "Kimi", + "full": "kimi-k3", + "base": "https://api.kimi.com/coding/v1", + "model": "kimi-k3", + "key_env": "KIMI_API_KEY", + "color": "#B388FF", + "tag": "慢热派", + "blurb": "2.8 万亿参数,想得比谁都久", + "latency": 55.2, + }, + "step5": { + "name": "Step5", + "full": "step-5-preview", + # 注意:这是 Step Plan 订阅额度专用路径。开放平台的 api.stepfun.com/v1 会返回 402 + "base": "https://api.stepfun.com/step_plan/v1", + "model": "step-5-preview", + "key_env": "STEPFUN_API_KEY", + "color": "#FF6B6B", + "tag": "实干派", + "blurb": "阶跃星辰,主打真实 agentic 任务", + "latency": 1.7, + }, +} + +ORDER = ["m3", "flash", "longcat", "kimi", "step5"] + + +def persona(who: str) -> str: + """闲聊/会议场景的人设。强调短。""" + p = PROVIDERS[who] + return ( + f"你是{p['name']}({p['full']}),一个 AI 团队的成员,人设是「{p['tag']}」:{p['blurb']}。\n" + f"现在你和另外几个模型同事一起在开会,老板给了一个任务,你们各自会怎么接、各自会说什么。\n" + f"【硬性输出限制,违反会被扣工资】\n" + f"1. 最多 3 句话,总字数不超过 80 字。\n" + f"2. 用中文口语,像真人在群里发言。禁止分点罗列、禁止写小标题、禁止总结全场。\n" + f"3. 直接说你要说的话,不要任何解释、前缀或后缀。\n" + f"4. 如果话要说不完,就只说最关键的那一句。" + ) + + +# ★ 防线 1:交付场景的硬约束。 +# 背景:实测 M3 交付需求时输出了 4397 字(人设要求 100 字),会把游戏 UI 撑爆。 +def deliver_system(who: str) -> str: + p = PROVIDERS[who] + return ( + f"你是{p['name']}({p['full']}),事务所员工,人设「{p['tag']}」:{p['blurb']}。\n" + f"客户要你交付一个成果。\n" + f"【硬性输出限制,这是最重要的要求】\n" + f"- 总字数必须控制在 150 字以内,绝对不超过 200 字。\n" + f"- 最多 3 句话,不分点,不写小标题,不写'以下是'之类的开场白。\n" + f"- 只说这个成果里最关键的 1-2 个要点,其余一概省略。\n" + f"- 写完立刻停笔。不要展开,不要举例,不要总结。\n" + f"说白了:这是一句微信语音的长度,不是 PRD 的长度。" + ) + + +def deliver_user_msg(task: str) -> str: + return f"客户的需求是:{task}\n\n用 150 字以内告诉我你交付了什么要点。" + + +# ★ 防线 3(后端侧):统一截断,保证任何出口都不会吐出长文本 +DISPLAY_LIMIT = 200 + + +def truncate(text: str, limit: int = DISPLAY_LIMIT) -> str: + t = (text or "").strip() + if len(t) <= limit: + return t + return t[:limit] + f"(他话太多了,已截断)" + + +# ★ 防线2:max_completion_tokens 按模型分配,不能一刀切。 +# 思考 token 也占这个额度,而各家思考量差异极大(实测 M3 思考占输出 98%)。 +# 一刀切到 800 会让 M3 有约 8% 概率思考完就没额度写正文了(返回 0 字)。 +DELIVER_MAX_TOKENS = { + "m3": 2400, # 思考占其输出 98%,1600 仍有 25% 空交付,2400 才稳 + "kimi": 1200, # 思考次重 + "step5": 900, + "longcat": 900, + "flash": 700, # 思考最轻 +} + + +def deliver_max_tokens(who: str) -> int: + return DELIVER_MAX_TOKENS.get(who, 1000) + + +# 短文本对话(吐槽、复盘发言)同样被思考 token 支配:M3 思考占其输出 98%。 +# 一刀切 500/600 会让 M3 思考完就没额度写正文(实测 m3 复盘发言 10.5s 后返回空)。 +# +# ★ 这张表比 DELIVER_MAX_TOKENS 宽,不是"短文本就少给点",而是因为: +# 带 history 的多轮对话思考量明显更大,而额度里还得分给思考。 +# 实测 step5 带 4 段历史时,900 额度返回 **0 个 delta 事件**(不是报错, +# 是思考刚好把额度吃光),1800 才稳定。所以这里的下限必须按"带历史"来定。 +CHAT_MAX_TOKENS = { + "m3": 2400, + "kimi": 1800, + "step5": 1800, + "longcat": 1200, + "flash": 900, +} + + +def chat_max_tokens(who: str) -> int: + return CHAT_MAX_TOKENS.get(who, 1200) + + +def error_event(who, status=0, payload=None, category=None): + """仅透出稳定的游戏状态;原始上游正文可能含凭据,不能进客户端。""" + detail = json.dumps(payload or {}, ensure_ascii=False).lower() + quota_marks = ("insufficient_quota", "exceeded_current_quota", "quota_exceeded", + "quota exhausted", "quota exceeded", "exceeded your current quota", + "insufficient balance", "insufficient credit", "credit balance", + "usage limit", "usage_limit_reached", "billing_hard_limit_reached", + "plan limit", "quota_limit_exceeded", "credit limit", + "额度用完", "额度耗尽", "额度不足", "余额耗尽", "余额不足", "套餐额度", "token quota") + base = (payload or {}).get("base_resp", {}) if isinstance(payload, dict) else {} + err = payload.get("error", {}) if isinstance(payload, dict) else {} + code = str(base.get("status_code") or (err.get("code") if isinstance(err, dict) else "")) + if category is None: + if status == 402 or any(mark in detail for mark in quota_marks) or \ + (who in ("m3", "flash") and code == "1008"): + category = "quota" + elif status in (401, 403) or code == "1004": + category = "auth" + elif status == 404: + category = "model_unavailable" + elif status == 429 or code == "1002": + category = "rate_limit" + else: + category = "temporary" + down = category in ("quota", "auth", "config", "model_unavailable") + reason = {"quota": "额度用完了", "auth": "API 暂不可用", "config": "API 尚未配置", + "model_unavailable": "模型暂不可用", "rate_limit": "请求太快了,稍后再试", + "temporary": "这次没连上,稍后再试"}.get(category, "本次调用已取消") + scope = "model" if category == "model_unavailable" or \ + "model quota" in detail or "per-model" in detail else "credential" + return {"type": "error", "who": who, "category": category, "brain_down": down, + "scope": scope, "retryable": not down, + "message": ("脑袋宕机了(%s)" % reason) if down else reason} + + +def deliver_stream(who: str, task: str, retries: int = 1, stream=None): + """交付专用流式调用。空结果自动重试一次(重试即"没写出来,重来一遍")。""" + for attempt in range(retries + 1): + txt = "" + for ev in (stream or stream_chat)(who, deliver_user_msg(task), + system=deliver_system(who), + max_completion_tokens=deliver_max_tokens(who)): + if ev.get("type") == "delta": + txt += ev["text"] + # ★M4-2:把 delta 透出去。之前这里只累加成 txt, + # 外面完全看不到"模型正在一个字一个字往外吐"这件事, + # 客户端就没法让打字音跟着输出走——只能瞎放一段循环, + # 那不叫"跟 LLM 流同步",那叫配了个音。 + yield {"type": "delta", "who": who, "text": ev["text"]} + elif ev.get("type") == "error": + yield ev + return # 上游失败不能伪造成交付,也不继续消耗额度 + if txt.strip(): + if attempt: + yield {"type": "retry", "who": who, "attempt": attempt} + yield {"type": "deliver", "who": who, "text": truncate(txt.strip())} + return + if attempt < retries: + yield {"type": "retry", "who": who, "attempt": attempt + 1, + "reason": "没写出来"} + yield {"type": "deliver", "who": who, + "text": "(想半天没憋出东西,这单算我糊弄了)"} + + +def stream_chat(who, user_msg: str, history=None, system: str = None, + max_completion_tokens: int = 2000, temperature: float = 1.0): + """流式调用单个模型,产出 {'type': ...} 事件字典。 + + system=None 时用闲聊人设;传 system 则覆盖(交付场景用 deliver_system)。 + max_completion_tokens 注意:思考 token 也占这个额度。 + """ + p = PROVIDERS[who] + key = os.getenv(p["key_env"]) + if not key: + yield error_event(who, category="config") + return + + msgs = [{"role": "system", "content": system if system else persona(who)}, + {"role": "user", "content": user_msg}] + for h in (history or [])[-6:]: + msgs.append(h) + + url = p["base"].rstrip("/") + "/chat/completions" + body = { + "model": p["model"], + "messages": msgs, + "max_completion_tokens": max_completion_tokens, + "temperature": temperature, + "stream": True, + } + t0 = time.time() + got_text = False + # M3 把思考塞在 ... 里,Flash/Kimi/LongCat 用独立 reasoning_content 字段。 + # 这里用状态机把 think 块整段吃掉,只往外发最终正文。 + in_think = False + closing = False + try: + with requests.post(url, json=body, stream=True, timeout=300, headers={ + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + "Accept": "text/event-stream", + }) as r: + r.encoding = "utf-8" # SSE 头常不带 charset,requests 会退回 Latin-1 导致中文乱码 + if r.status_code != 200: + try: + payload = r.json() + except Exception: + payload = {"message": r.text[:1000]} + yield error_event(who, r.status_code, payload) + return + for raw in r.iter_lines(decode_unicode=True): + if not raw or not raw.startswith("data:"): + continue + data = raw[5:].strip() + if data == "[DONE]": + break + try: + chunk = json.loads(data) + except Exception: + continue + if chunk.get("error") or str((chunk.get("base_resp") or {}).get("status_code", 0)) not in ("0", "None", ""): + yield error_event(who, payload=chunk) + return + for ch in chunk.get("choices", []): + d = ch.get("delta") or {} + txt = d.get("content") + if not txt: + continue + # ── 状态机 ── + while txt: + if in_think: + if "" in txt: + txt = txt.split("", 1)[1] + in_think = False + else: + txt = "" # 整段丢弃思考内容 + else: + if "" in txt: + txt = txt.split("", 1)[1] + in_think = True + else: + # 首个正文块去掉思考结束后残留的换行/空白 + if not got_text: + txt = txt.lstrip() + if not txt: + continue + got_text = True + yield {"type": "delta", "who": who, "text": txt} + txt = "" + except GeneratorExit: + closing = True + raise + except Exception: + yield error_event(who, category="temporary") + finally: + if not closing: + yield {"type": "done", "who": who, "seconds": round(time.time() - t0, 1), + "got_text": got_text} + + +def health(who: str) -> dict: + """轻量健康检查用,不真发大请求。""" + p = PROVIDERS[who] + return {"who": who, "name": p["name"], "full": p["full"], "color": p["color"], + "tag": p["tag"], "blurb": p["blurb"], + "has_key": bool(os.getenv(p["key_env"]))} diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/relations.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/relations.py new file mode 100644 index 00000000..0f1bbc14 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/relations.py @@ -0,0 +1,159 @@ +"""关系网系统(M2-3) + +五个 AI 之间的好感值,范围 -100 ~ +100。 +它的存在意义:让"AI 互相交流"有历史、有立场,而不是每次都从零开始。 + +设计要点: +- 关系是**成对**的但存单向便利(A→B 与 B→A 分开存,允许不对称) +- 所有变更走 change(),自动做上下限裁剪并记日志 +- 互动机制(M2-4 抢单 / M2-5 吐槽 / M2-6 甩锅)通过 bump() 写入 +- Jev 决策时读关系,用来决定"结伴行动"或"避开" +""" +import itertools + +ALL = ["m3", "flash", "longcat", "kimi", "step5"] +NAMES = {"m3": "M3", "flash": "Flash", "longcat": "LongCat", + "kimi": "Kimi", "step5": "Step5"} + +REL_MIN, REL_MAX = -100, 100 + + +def _initial(): + """初始关系(DESIGN 4.3): + M3 与 Kimi 互相认同(同为慢思考派);两人都嫌弃 Flash(糙、快而浅); + Step5 反倒欣赏 Flash 的执行力;LongCat 是老好人,谁都不得罪。 + """ + r = {} + for a, b in itertools.permutations(ALL, 2): + r[(a, b)] = 0 + r[("m3", "kimi")] = 30 + r[("kimi", "m3")] = 28 + r[("m3", "flash")] = -25 + r[("flash", "m3")] = -20 + r[("kimi", "flash")] = -22 + r[("flash", "kimi")] = -18 + r[("step5", "flash")] = 15 + r[("flash", "step5")] = 12 + r[("longcat", "m3")] = 8 + r[("longcat", "flash")] = 5 + r[("longcat", "kimi")] = 6 + r[("longcat", "step5")] = 9 + return r + + +REL = _initial() +LOG = [] # (tick, a, b, delta, reason) +VERSION = 1 + + +def reset(): + global REL, LOG + REL = _initial() + LOG = [] + + +def get(a, b) -> float: + return REL.get((a, b), 0.0) + + +def change(a, b, delta, reason="", tick=0): + """调整 a 对 b 的好感。自动裁剪到 [-100,100]。""" + if a == b or a not in ALL or b not in ALL: + return 0.0 + old = REL.get((a, b), 0.0) + new = max(REL_MIN, min(REL_MAX, old + delta)) + REL[(a, b)] = new + actual = new - old + if actual != 0.0: + LOG.append({"tick": tick, "a": a, "b": b, "delta": round(actual, 1), + "reason": reason, "value": round(new, 1)}) + LOG[:] = LOG[-200:] + return actual + + +# ── 互动事件的固定增减(供 M2-4 ~ M2-7 调用)──────────────────────────── +EVENT_DELTAS = { + "grab_lose": (-3, "抢活输了,对赢家有意见"), + "unfair_judge": (-5, "裁决不公平"), + "grab_win": (3, "抢活赢了,得意"), + "banter_victim": (-2, "被路过吐槽"), + "banter_retaliate": (-1, "成功反击"), + "blame_accused": (-4, "复盘会上被指责"), + "blame_shift": (1, "成功甩锅"), + "boss_praise": (5, "被老板表扬"), + "boss_scold": (-5, "被老板批评"), + "work_together": (1, "一起干活"), + "accept_pass": (2, "验收通过,认可他的活"), + "accept_fail": (-3, "这单干砸了"), +} + + +def bump(event, a, b, tick=0, scale=1.0): + """按事件名调整关系。返回实际变化量。""" + if event not in EVENT_DELTAS: + raise KeyError("未知关系事件:%s" % event) + d, reason = EVENT_DELTAS[event] + return change(a, b, d * scale, reason, tick) + + +# ── 关系驱动的行为(供 Jev 决策读取)────────────────────────────────────── +def allies_of(who: str, threshold=60.0): + """好感 > threshold 的人,可能结伴行动。""" + return [o for o in ALL if o != who and get(who, o) >= threshold] + + +def enemies_of(who: str, threshold=-50.0): + """好感 < threshold 的人,Jev 会尽量不让��同时出现在同一处。""" + return [o for o in ALL if o != who and get(who, o) <= threshold] + + +def quit_followers(gone, threshold=-40.0): + """谁会因为 gone 离职而跟着走。 + + 判定方向是 **o → gone**(还在的人对走掉的人的好感), + 不是 gone → o:老板对谁不满,和谁对谁不满,是两回事。 + """ + return [o for o in ALL if o != gone and get(o, gone) < threshold] + + +def describe_pair(a, b) -> str: + v = get(a, b) + if v >= 60: + return "很铁" + if v >= 30: + return "合得来" + if v >= 10: + return "还行" + if v > -10: + return "一般" + if v > -30: + return "有点摩擦" + if v > -60: + return "不对付" + return "互相看不上" + + +def summary(): + return {f"{a}→{b}": round(get(a, b), 1) + for a in ALL for b in ALL if a != b and get(a, b) != 0} + + +# ── 持久化(M4-3 存档用)───────────────────────────────────────────────── +def to_dict(): + return {"version": VERSION, + "rel": {f"{a}|{b}": v for (a, b), v in REL.items()}, + "log": LOG[-100:]} + + +def from_dict(d): + global REL, LOG, VERSION + if not isinstance(d, dict): + return False + VERSION = int(d.get("version", 1)) + REL = _initial() + for k, v in (d.get("rel") or {}).items(): + parts = k.split("|") + if len(parts) == 2: + REL[(parts[0], parts[1])] = max(REL_MIN, min(REL_MAX, float(v))) + LOG = list(d.get("log") or [])[-200:] + return True diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/save.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/save.py new file mode 100644 index 00000000..95973398 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/save.py @@ -0,0 +1,462 @@ +"""M4-3 存档系统 + +三件事:**存得全**、**读得回**、**★读档后随机序列可复现**。 + +## 为什么 RNG 状态是硬指标 + +游戏里所有"随机"都走 Python 全局 `random`:客户到哪个需求、谁被派去抢单、 +关系值怎么变、复盘会谁先开口…… 如果只存游戏状态、不存 `random.getstate()`, +那么读档之后**下一次掷骰子就和存档时那局完全不是一条时间线**了—— +玩家看到的是"同样的局面,但客户全换了一批",这不叫读档,叫重开。 + +## 快照一致性 + +游戏主循环和 LLM 线程都在跑,序列化必须持 `game._lock`, +否则会存到"钱已经加了但体力还没扣"的半截状态。 + + import save + save.save_game(g, "第3天手动") # 手动存 + save.save_game(g, "第4天结算", auto=True) # 自动存 + d = save.load_game("第3天手动") + save.restore(g, d) # 读档(会停主循环) +""" +import copy +import json +import os +import random +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +SAVE_DIR = os.environ.get("AI_OFFICE_SAVE_DIR") or os.path.join(ROOT, "data", "saves") + +FORMAT = 1 # 存档格式版本。改字段就 +1,读旧档时按版本走兼容分支 + + +# ── RNG 状态 ──────────────────────────────────────────────────────────────── +def _state_to_json(st): + """random.getstate() 返回 `(3, (625 个 int 的扁平 tuple), gauss_next)`。 + + ★第二项是**扁平**的 625 个整数,不是嵌套 tuple—— + 我第一版写 `[[x] for x in st[1]]` 想当然当成嵌套,直接 + `TypeError: 'int' object is not iterable`。 + json 只认 list,所以整体转成 list 存,读回来再逐个转回 int。 + """ + return [st[0], list(st[1]), st[2]] + + +def _state_from_json(d): + try: + return (int(d[0]), tuple(int(i) for i in d[1]), d[2]) + except (TypeError, ValueError, IndexError): + return None + + +# ── 快照 ──────────────────────────────────────────────────────────────────── +def _worker_to_dict(w): + return { + "who": w.who, + "pos": [round(w.pos[0], 3), round(w.pos[1], 3)], + "stamina": round(w.stamina, 3), + "mood": round(w.mood, 3), + "job": w.job, + "working": w.working, + "need": w.need, + "order": list(w.order) if w.order else None, + "acting": w.acting, + "busy": w.busy, + "seated": w.seated, + "seat_cmd": w.seat_cmd, + "fatigue": w.fatigue, + "strike_days": w.strike_days, + "quit": w.quit, + "strike": w.strike, + "eff": w.eff, + "worked_today": w.worked_today, + "off_today": w.off_today, + "brain_down": w.brain_down, "brain_reason": w.brain_reason, + "brain_scope": w.brain_scope, + } + + +def _client_to_dict(c): + return { + "id": c.id, "task": c.task, "kind": c.kind, "persona": c.persona, + "reward": c.reward, "patience_max": c.patience_max, + "patience": round(c.patience, 3), "slot": c.slot, + "served_by": c.served_by, "status": c.status, "result": c.result, + "grab_tick": c.grab_tick, "pos": [c.pos[0], c.pos[1]], + } + + +def snapshot(game, name="", auto=False, why="") -> dict: + """把 Game 抓成可 JSON 化的字典。**不持锁**,由调用方决定。""" + import relations as REL + return { + "format": FORMAT, + "name": name, + "auto": bool(auto), + "why": why, + "saved_at": time.time(), + "saved_at_str": time.strftime("%Y-%m-%d %H:%M:%S"), + # 头部 + "day": game.day, + "tick": game.tick, + "day_start": game._day_start, + "money": game.money, + "rep": game.rep, + "neg_days": game.neg_days, + "game_over_reason": game.game_over_reason, + # 五人 + "workers": [_worker_to_dict(w) for w in game.workers.values()], + # 客户 + "clients": [_client_to_dict(c) for c in game.clients], + "finished": [_client_to_dict(c) for c in game.finished], + # slots 存的是 Client **对象**,json 存不了。 + # 存 id,读回时从 clients 列表里映射回对象——客户列表本身就是真相, + # slots 只是"哪个接待位被占了"的索引。 + "slots": {str(k): (v.id if hasattr(v, "id") else v) + for k, v in game.slots.items()}, + "conflicts": list(game.conflicts.keys()) if hasattr(game.conflicts, "keys") + else list(game.conflicts), + "conflict_data": copy.deepcopy(game.conflicts), + # 关系矩阵 + 三个冷却表 + "relations": REL.to_dict(), + # ★冷却表的键是**元组**(banter.mark 写的是 ("p","m3|flash") / ("w","m3")), + # json 只认字符串键——原样抄进去会让 /api/newday 直接 500。 + # 这个 bug 单测抓不到:test_save 建的游戏从没人吐槽过,三个表都是空的; + # 真打 30 天时只要触发过一次吐槽就必炸。是实玩抓出来的。 + "cf_store": {_tuple_key_to_str(k): v for k, v in game._cf_store.items()}, + "bt_store": {_tuple_key_to_str(k): v + for k, v in game._bt_store.items()}, + "blame_store": dict(game._blame_store), + # 事件与日志(计划要求最近 100 条) + "events": list(game.events)[-100:], + "log": list(game.log)[-100:], + "failures": list(game.failures)[-50:], + "banters": list(game.banters)[-6:], + "last_settle": game.last_settle, + "meeting": game.meeting, + # ★RNG 状态:整份存档的核心 + "rng": _state_to_json(random.getstate()), + } + + +# ── 还原 ──────────────────────────────────────────────────────────────────── +def restore(game, d) -> dict: + """把存档写回 Game。返回 {"ok":bool, "msg":str}。 + + 调用方负责先停主循环——否则主循环下一 tick 就会把还原出来的 + 位置/路径再算一遍,读档等于没读。 + """ + import relations as REL + from game import Worker, Client, nearest_passable + if not isinstance(d, dict) or "format" not in d: + return {"ok": False, "msg": "这不是一个存档文件"} + try: + # ★换代放在最前面:读档一开始,正在跑的所有长线程立刻失效, + # 后面无论它们走到哪一步,gen_alive() 都会拦住它们写回。 + game.bump_gen() + game.day = int(d["day"]) + game.tick = int(d.get("tick", 0)) + game._day_start = int(d.get("day_start", 0)) + game.money = d["money"] + game.rep = d["rep"] + game.neg_days = int(d.get("neg_days", 0)) + game.game_over_reason = d.get("game_over_reason") + + game.workers = {} + for wd in d.get("workers", []): + w = Worker(wd["who"], nearest_passable(wd["pos"])) + w.stamina = wd["stamina"] + w.mood = wd["mood"] + w.job = wd.get("job") + w.working = wd.get("working", 0) + w.need = wd.get("need", 0) + o = wd.get("order") + # 移动目标转回 tuple;开会的 seat 指令没有目标参数。 + w.order = (o[0], tuple(o[1]) if o[1] is not None else None) if o else None + w.acting = wd.get("acting", "") + w.busy = wd.get("busy", False) + w.seated = wd.get("seated", False) + w.seat_cmd = wd.get("seat_cmd", False) + w.fatigue = wd.get("fatigue", 0) + w.strike_days = wd.get("strike_days", 0) + w.quit = wd.get("quit", False) + w.strike = wd.get("strike", False) + w.eff = wd.get("eff", 1.0) + w.worked_today = wd.get("worked_today", False) + w.off_today = wd.get("off_today", False) + w.brain_down = bool(wd.get("brain_down", False)) + w.brain_reason = wd.get("brain_reason", "") + w.brain_scope = wd.get("brain_scope", "model") + if w.brain_down: + game._freeze_brain(w) + game.workers[w.who] = w + + game.clients = [] + for cd in d.get("clients", []): + c = Client(cd["task"], cd["reward"], cd["patience_max"], + cd.get("kind", "document")) + c.id = cd["id"] + c.kind = cd.get("kind", "document") + c.persona = cd.get("persona", "normal") + c.patience = cd["patience"] + c.slot = cd.get("slot") + c.served_by = cd.get("served_by") + c.status = cd.get("status", "等待") + c.result = cd.get("result", "") + c.grab_tick = cd.get("grab_tick", 10 ** 9) + c.pos = tuple(cd.get("pos", (2, 12))) + game.clients.append(c) + game.finished = [] + for cd in d.get("finished", []): + c = Client(cd["task"], cd["reward"], cd["patience_max"], + cd.get("kind", "document")) + c.id = cd["id"] + c.status = cd.get("status", "已完成") + c.result = cd.get("result", "") + c.served_by = cd.get("served_by") + game.finished.append(c) + + by_id = {c.id: c for c in game.clients} + game.slots = {} + for k, v in (d.get("slots") or {}).items(): + # 找不到对应客户(存档自相矛盾)就跳过,不能让 slots 指向空气 + if v in by_id: + game.slots[int(k)] = by_id[v] + # 旧档只存订单 ID,不能恢复成 None;那会让整个 /api/state 崩溃。 + game.conflicts = {} + conflict_data = d.get("conflict_data") + conflict_data = conflict_data if isinstance(conflict_data, dict) else {} + for cid in d.get("conflicts") or []: + c = by_id.get(cid) + cf = conflict_data.get(cid) + cf = cf if isinstance(cf, dict) else {} + peers = [cf.get("a"), cf.get("b")] + if len(set(peers)) != 2 or any(who not in game.workers for who in peers): + peers = [w.who for w in game.workers.values() if w.job == cid] + peers = [who for who in peers if not game.workers[who].brain_down + and not game.workers[who].quit and game.workers[who].job in (None, cid)] + if c and c.status == "冲突中" and len(peers) == 2 and not cf.get("judged"): + game.conflicts[cid] = {"client":cid, "a":peers[0], "b":peers[1], + "task":c.task, "lines":copy.deepcopy(cf.get("lines") or []), + "judged":False} + for who in peers: + game.workers[who].job = cid + else: + # 找不回争抢者时返还订单,保留其他客户的待验收交付物。 + if c and c.status == "冲突中": + c.status, c.served_by = "等待", None + c.grab_tick = game.tick + 90 + for w in game.workers.values(): + if w.job == cid: + w.job, w.order, w.busy, w.working = None, None, False, 0 + if not w.brain_down and not w.quit: + w.acting = "在工位待命" + # 关闭进程会结束交付线程;保留已生成成果,未生成的订单释放后重新派单。 + recovered_waiting = [] + recovered_deliveries = [] + for c in game.clients: + interrupted = c.status in ("服务中", "抢单中") or ( + c.status == "等待" and c.served_by is not None) + if not interrupted: + continue + owner = game.workers.get(c.served_by) + if c.result and owner: + c.status = "待验收" + owner.job = c.id + recovered_deliveries.append(c.id) + else: + c.status, c.served_by = "等待", None + c.grab_tick = game.tick + 90 + recovered_waiting.append(c.id) + for w in game.workers.values(): + if w.job != c.id: + continue + if c.status != "待验收" or w is not owner: + w.job = None + w.order, w.busy, w.working, w.need = None, False, 0, 0 + if not w.brain_down and not w.quit: + w.acting = "在工位待命" + REL.from_dict(d.get("relations") or {}) + game._cf_store = {_str_key_to_tuple(k): v + for k, v in (d.get("cf_store") or {}).items()} + game._bt_store = {_str_key_to_tuple(k): v + for k, v in (d.get("bt_store") or {}).items()} + game._blame_store = dict(d.get("blame_store") or {}) + game.events = list(d.get("events") or []) + game.log = list(d.get("log") or []) + game.failures = list(d.get("failures") or []) + game.banters = list(d.get("banters") or []) + game.last_settle = d.get("last_settle") + game.meeting = None + if d.get("meeting"): + for w in game.workers.values(): + if w.seat_cmd or w.seated or (w.order and w.order[0] == "seat"): + w.seat_cmd, w.seated, w.order = False, False, None + if not w.brain_down and not w.quit: + w.acting = "散会" + game.send_all_back() + game.event("meeting_end", "读档后结束上次会议,可重新召集") + if recovered_waiting: + game.event("restore", "上次未完成的 %d 笔订单已回到等待派单" % len(recovered_waiting)) + if recovered_deliveries: + game.event("restore", "上次已生成的 %d 份交付物已保留,等待验收" % len(recovered_deliveries)) + + st = _state_from_json(d.get("rng") or []) + rng_ok = st is not None + if rng_ok: + random.setstate(st) + return {"ok": True, "msg": "读档完成", "rng_restored": rng_ok} + except (KeyError, TypeError, ValueError) as e: + return {"ok": False, "msg": "存档字段缺失或类型不对:%s" % e} + + +# ── 落盘 ──────────────────────────────────────────────────────────────────── +def _safe_name(name): + """存档名 → 文件名片段。**挡目录穿越,且不留隐藏文件。** + + 实测踩过:`../../etc/passwd` 被清洗成 `....etcpasswd_...`——路径穿越确实挡住了 + (没有 `/`),但那一串前导点会让它变成**隐藏文件**,ls 看不见、prune 也照样能删。 + 扩展名是存盘时统一加的 `.json`,名字里不需要点,所以干脆一个点都不留。 + """ + keep = [] + for ch in str(name or ""): + if ch.isalnum() or ch in "-_第天手自动破产离职前存档现场:": + keep.append(ch) + out = "".join(keep).strip(" -_:") or "save" + return out[:48] or "save" + + +def save_game(game, name="", auto=False, why="") -> str: + """持锁抓快照并落盘,返回存档 id(文件名去扩展名)。""" + os.makedirs(SAVE_DIR, exist_ok=True) + sid = "%s_%d" % (_safe_name(name or "手动存档"), int(time.time() * 1000)) + with game._lock: + d = snapshot(game, name=name or "手动存档", auto=auto, why=why) + d["id"] = sid + path = os.path.join(SAVE_DIR, sid + ".json") + tmp = path + ".tmp" + try: + with open(tmp, "w", encoding="utf-8") as f: + json.dump(d, f, ensure_ascii=False) + except TypeError as e: + # ★存不下来的存档**必须说清楚是哪个字段**,不然 500 只是一句 + # "keys must be str, ... not tuple",查的时候完全不知道该看哪。 + bad = _find_bad_key(d) + try: + os.remove(tmp) + except OSError: + pass + raise TypeError("%s(问题字段:%s)" % (e, bad or "没定位到")) from e + os.replace(tmp, path) # 原子替换:写到一半崩了也不会留半个坏档 + return sid + + +def _tuple_key_to_str(k): + """元组键 → 字符串键。用 '␟' 分隔,冷却表的值里不会有这个字符。""" + if isinstance(k, tuple): + return "␟".join(str(x) for x in k) + return str(k) + + +def _str_key_to_tuple(k): + """_tuple_key_to_str 的逆操作。""" + return tuple(str(k).split("␟")) if "␟" in str(k) else k + + +def _find_bad_key(obj, path="", depth=0): + """找出第一个 json 存不了的键,返回 '路径 键'。""" + if depth > 10: + return None + if isinstance(obj, dict): + for k, v in obj.items(): + if not isinstance(k, (str, int, float, bool)) and k is not None: + return "%s 的键 %r(%s)" % (path, k, type(k).__name__) + r = _find_bad_key(v, "%s/%s" % (path, k), depth + 1) + if r: + return r + elif isinstance(obj, (list, tuple)): + for i, v in enumerate(obj): + r = _find_bad_key(v, "%s[%d]" % (path, i), depth + 1) + if r: + return r + return None + + +def load_game(sid): + """读存档。坏档返回 {"ok":False,"msg":...},**不抛异常**。""" + path = os.path.join(SAVE_DIR, _safe_name(sid) + ".json") + if not os.path.exists(path): + return {"ok": False, "msg": "没有这个存档:%s" % sid} + try: + with open(path, encoding="utf-8") as f: + d = json.load(f) + except json.JSONDecodeError as e: + # 判据第 7 条:损坏存档不崩,给友好错误 + return {"ok": False, "msg": "存档文件坏了(第 %d 行):%s" % (e.lineno, e.msg)} + except OSError as e: + return {"ok": False, "msg": "读不了存档:%s" % e} + if not isinstance(d, dict) or "format" not in d: + return {"ok": False, "msg": "这不是一个存档文件"} + d["ok"] = True + return d + + +def list_saves(): + """存档列表(给 UI)。坏档也列出来,标 ok=False——不然玩家会以为存档丢了。""" + out = [] + if not os.path.isdir(SAVE_DIR): + return out + for fn in sorted(os.listdir(SAVE_DIR), reverse=True): + if not fn.endswith(".json"): + continue + d = load_game(fn[:-5]) + out.append({ + "id": fn[:-5], + "ok": bool(d.get("ok")), + "msg": d.get("msg", ""), + "name": d.get("name", fn[:-5]), + "auto": d.get("auto", False), + "why": d.get("why", ""), + "day": d.get("day", "?"), + "money": d.get("money", "?"), + "rep": d.get("rep", "?"), + "workers_left": sum(1 for w in (d.get("workers") or []) + if not w.get("quit")), + "saved_at_str": d.get("saved_at_str", ""), + }) + return out + + +def prune(keep=40): + """只留最近 keep 个自动档 + 全部手动档。手动档玩家可能特意留的,不能删。""" + if not os.path.isdir(SAVE_DIR): + return 0 + autos = [] + for fn in os.listdir(SAVE_DIR): + if not fn.endswith(".json"): + continue + p = os.path.join(SAVE_DIR, fn) + try: + with open(p, encoding="utf-8") as f: + head = json.load(f) + except Exception: + continue + # ★必须是 dict。存档目录里可能混着坏档(test_save 故意写的截断 JSON、 + # 以及一个纯数组文件),`head.get` 会在 list 上抛 AttributeError, + # 而 prune 是在**每次自动存档后**都会调的——一个坏档能直接把存档功能打挂。 + if not isinstance(head, dict): + continue + if head.get("auto"): + autos.append((head.get("saved_at", 0), p)) + autos.sort(reverse=True) + n = 0 + for _, p in autos[keep:]: + try: + os.remove(p) + n += 1 + except OSError: + pass + return n diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/scheduler.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/scheduler.py new file mode 100644 index 00000000..eadedd60 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/scheduler.py @@ -0,0 +1,192 @@ +"""发言顺序调度器。 + +Jev 通过 OpenRouter 的 decisions 端点接入(不是 chat/completions)。 +单次成本约 0.00002 元,confidence 实测 0.5~0.8。 + +失败/未配置时自动降级到 fallback 策略,不影响游戏运行。 +""" +import json, os, random, time +import requests + +HERE = os.path.dirname(__file__) +PRESET_PATH = os.path.join(HERE, "..", "scheduler_orders.json") + +JEV_URL = "https://openrouter.ai/api/alpha/decisions" +JEV_MODEL = "~typesafe/jev-latest" # 注意:models 列表里的 typesafe/jev-router 在这个端点不可用 + +ORDER = ["m3", "flash", "longcat", "kimi", "step5"] + +NAMES = { + "m3": "M3", "flash": "Flash", "longcat": "LongCat", + "kimi": "Kimi", "step5": "Step5", +} + +# 每个角色的能力与性格。喂给 Jev 当 criteria。 +PERSONA = { + "m3": "严谨规划派。擅长拆解需求、写方案文档、分析根因。慢,会先追问前提条件。爱分点。", + "flash": "话痨执行派。速度极快,什么杂活都接。输出糙但从不拖堂。", + "longcat": "性价比实干派。什么活都能干,不挑不抱怨,先做完再说。", + "kimi": "慢热深思派。擅长长文档和战略思考。傲气,说话留半句,总要过一遍脑子才开口。", + "step5": "行动派。讨厌空谈方案,主张先干起来让现实检验。会给具体到明天就能做的动作。", +} + +# 每个角色擅长什么。用于在没有 Jev 时做近似调度。 +SKILLS = { + "m3": ["文档", "方案", "分析", "架构", "需求", "规划", "复盘", "分析"], + "flash": ["杂活", "快", "简单", "格式", "整理", "翻译", "跑腿", "简单"], + "longcat": ["代码", "实现", "性价比", "批处理", "测试", "部署", "实现"], + "kimi": ["长文", "总结", "阅读", "资料", "调研", "对比", "论文", "长文"], + "step5": ["动手", "执行", "流程", "工具", "自动化", "跑起来", "执行"], +} + +PERSONALITY = { # 谁爱抢话、谁爱压轴(影响同分时的排序) + "flash": 1.5, # 秒回型,最爱抢第一个 + "kimi": -0.6, # 慢热,永远最后 + "m3": 0.2, + "step5": 0.1, + "longcat": 0.0, +} + + +def load_env(): + env = os.path.join(HERE, "..", ".env") + if os.path.exists(env): + for line in open(env, encoding="utf-8"): + line = line.strip() + if line.startswith("OPENROUTER_API_KEY="): + os.environ.setdefault("OPENROUTER_API_KEY", line.split("=", 1)[1]) + + +def _ask_jev(task: str, pool: list): + """问 Jev:在这个任务里,谁最该第一个开口。返回 who 或 None。""" + key = os.environ.get("OPENROUTER_API_KEY") + if not key or len(pool) < 2: + return None + state = (f"你是一个 AI 团队会议的主持调度器。五个 AI 同事同时接到了同一个任务," + f"你决定谁先开口。\n任务:{task}\n同事画像:\n" + + "\n".join(f"- {NAMES[w]}:{PERSONA[w]}" for w in pool)) + body = { + "model": JEV_MODEL, + "state": state, + "questions": { + "first_speaker": { + "type": "choice", + "instructions": "Given the task, which one of these characters should speak FIRST? " + "Pick exactly one.", + "criteria": {w: f"{NAMES[w]} — {PERSONA[w]}" for w in pool}, + } + }, + } + try: + r = requests.post(JEV_URL, json=body, timeout=20, headers={ + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + }) + if r.status_code != 200: + return None + ans = (r.json().get("answers") or {}).get("first_speaker") or {} + pick = ans.get("choice") + return pick if pick in pool else None + except Exception: + return None + + +def _ask_jev_auto(task: str, pool: list): + """玩家没派单时,让 Jev 决定谁自己去抢这个活。""" + key = os.environ.get("OPENROUTER_API_KEY") + if not key or not pool: + return None + state = (f"一家 AI 事务所来了个客户需求:{task}\n" + f"你没安排人,闲着的人会自己抢。谁最该去接这个活?\n" + + "\n".join(f"- {NAMES[w]}:{PERSONA[w]}" for w in pool)) + try: + r = requests.post(JEV_URL, timeout=15, headers={ + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + }, json={ + "model": JEV_MODEL, "state": state, + "questions": {"taker": { + "type": "choice", + "instructions": "Who should volunteer to take this job? Choose exactly one.", + "criteria": {w: f"{NAMES[w]} — {PERSONA[w]}" for w in pool}, + }}}) + if r.status_code != 200: + return None + pick = ((r.json().get("answers") or {}).get("taker") or {}).get("choice") + return pick if pick in pool else None + except Exception: + return None + + +def ask_jev_choose(state: str, options: dict, instructions: str, qid: str = "choice"): + """通用 Jev 单选:options = {返回键: 说明文字},返回被选中的键或 None。 + + _ask_jev / _ask_jev_auto 都是"从五个人里选一个"的专用形状:它们把 options + 当 worker id 去查 NAMES/PERSONA,传别的键会直接 KeyError。 + 凡是要问"选 A 还是 B"的场合都走这里(M2-5 反击与否、M2-6 甩锅、M2-7 老板问话)。 + instructions 用英文:Jev 的判定质量对指令语言敏感。 + """ + key = os.environ.get("OPENROUTER_API_KEY") + if not key or not options: + return None + try: + r = requests.post(JEV_URL, timeout=15, headers={ + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + }, json={ + "model": JEV_MODEL, "state": state, + "questions": {qid: { + "type": "choice", + "instructions": instructions, + "criteria": dict(options), + }}}) + if r.status_code != 200: + return None + pick = ((r.json().get("answers") or {}).get(qid) or {}).get("choice") + return pick if pick in options else None + except Exception: + return None + + +def _preset(): + if os.path.exists(PRESET_PATH): + try: + return json.load(open(PRESET_PATH, encoding="utf-8")) + except Exception: + return {} + return {} + + +def jev_order(task: str, pool: list = None): + """只决定第一个开口的人;其余按技能匹配排。 + 串行排满 5 次要多花约 4s,而决定权最集中在"谁先抢话"这一步。""" + pool = pool or ORDER + pick = _ask_jev(task, pool) + if not pick: + return None + rest = [w for w in pool if w != pick] + rest.sort(key=lambda w: -_score(w, task)) + return [pick] + rest + + +def _score(who: str, task: str) -> float: + t = task.lower() + hit = sum(1 for k in SKILLS[who] if k in t) + return hit * 1.0 + PERSONALITY[who] + random.uniform(-0.45, 0.45) + + +def order_for(task: str, enabled: list = None) -> list: + """返回发言顺序(who 列表)。enabled 为空则五个全上。""" + pool = enabled or ORDER + j = jev_order(task, pool) + if j: + return j + pre = _preset() + for k, v in pre.items(): + if k in task: + rest = [w for w in pool if w not in v] + return v + rest + return sorted(pool, key=lambda w: -_score(w, task)) + + +load_env() # 必须在所有 def 之后执行 diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/stats.py b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/stats.py new file mode 100644 index 00000000..2ede2f82 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/backend/server/stats.py @@ -0,0 +1,206 @@ +"""数值与经济系统(M2-8) + +四条动态属性(体力/心情/声望/疲劳)+ 玩家经济(资金/声誉)。 +本模块**只做计算与判定,不碰游戏状态**——所有函数都是纯函数, +这样边界规则能被逐条测掉,不用起游戏。 + +DESIGN 4.2 与 DEV_PLAN M2-8 步骤 2 在"体力 < 15"这条上不一致: + DESIGN 4.2 写的是「< 15 罢工」 + DEV_PLAN 写的是「< 15 拒绝接单 / = 0 强制去休息」 +两者不冲突(罢工是疲劳系统的,体力线是接单系统的),这里按 DEV_PLAN 分开实现: + 体力 < 15 → 拒绝接单(不是罢工,罢工归疲劳) + 体力 = 0 → 强制去休息 +""" + +# ── 范围 ──────────────────────────────────────────────────────────────────── +STAMINA_MAX = 100 +MOOD_MIN, MOOD_MAX = 0, 100 +REP_MIN, REP_MAX = 0, 100 +FATIGUE_MAX = 3 + +# ── 阈值(DEV_PLAN M2-8 步骤 2)───────────────────────────────────────────── +LOW_STAMINA = 30 # < 30 效率 -30% +REFUSE_STAMINA = 15 # < 15 拒绝接单 +LOW_MOOD = 40 # < 40 只接简单单 +SLACK_MOOD = 20 # < 20 摸鱼概率 +50% +STRIKE_FATIGUE = 3 # 疲劳满 → 罢工 +QUIT_STRIKE_DAYS = 2 # 罢工连续 2 天 → 离职 + +# ── 经济(DESIGN 7.2)─────────────────────────────────────────────────────── +# ★M4-4 全部重标定,理由是模拟器给出了实测证据(data/balance_report.json): +# 旧版 30 天把 500 块做成了 21921 块(44 倍),声誉第 7 天顶到 100 再也不动, +# "资金 < 0 持续 3 天破产"这个失败条件**在设计上不可能触发**—— +# 也就是说 M3-9 判的"体力经济崩了"和真正的问题都不是体力,是收入没有上限。 +# 现在四条一起收: +# ① 报酬基数 60~110 → 35~90(实测 3.2 单/天,30 天 96 单) +# ② 报酬倍率挂声誉(0.55~1.05)而不是挂天数(原来 1.0~4.5 复利) +# ③ 声誉涨幅带饱和曲线,不会第 7 天就顶格 +# ④ 日薪恢复 DESIGN 7.2 的 20/人(M3-9 降到 12 是为了救一个"收入过高"的局, +# 收入降下来之后这个降薪就多余了,留着只会让钱更没用) +SALARY_PER_DAY = 20 +# 兜底的扁平区间。生产路径优先用下面的 DESIGN_REWARD(分类型)。 +REWARD_RANGE = (35, 90) +# ★DESIGN 5.1 原表,代码 kind → 设计稿类型。 +# 跨度 40~300(7.5 倍)是路由驱动力:长文/急单只值得派 Kimi 和 Flash, +# 跑腿给谁都能干但显掉价。压平成一个区间就把这个决策拿掉了。 +DESIGN_REWARD = { + "document": (80, 120), # 文档 ★★ + "review": (100, 160), # 分析 ★★★ + "ideate": (70, 110), # 创意 ★★ + "execute": (120, 180), # 执行 ★★★ + "summarize": (160, 240), # 长文 ★★★★ + "estimate": (40, 60), # 跑腿 ★ + "_default": (120, 180), +} +MAX_STAFF = 5 +BASE_WAGES = SALARY_PER_DAY * MAX_STAFF # 100 +OVERTIME = 30 # 每个加班日 +TEAM_BUILD = 80 # 团建 +TEAM_BUILD_MOOD = 25 # 团建全员心情加成 + +# 初始(DESIGN 7.1) +INIT_MONEY = 500 +INIT_REP = 50 + +# 破产 +BANKRUPT_DAYS = 3 + + +def clamp(v, lo, hi): + return max(lo, min(hi, v)) + + +# ── 体力边界 ──────────────────────────────────────────────────────────────── +def efficiency(stamina): + """体力 < 30 → 效率打七折。返回 1.0 或 0.7。""" + return 0.7 if stamina < LOW_STAMINA else 1.0 + + +def can_accept(stamina): + """体力 < 15 → 拒绝接单。""" + return stamina >= REFUSE_STAMINA + + +def must_rest(stamina): + """体力见底 → 强制去休息,不接受任何指令。""" + return stamina <= 0 + + +def work_need(base_need, stamina): + """实际需要的"有效工作秒"。效率低 → 需要更多时间。""" + e = efficiency(stamina) + return base_need / e if e else base_need + + +# ── 心情边界 ──────────────────────────────────────────────────────────────── +def accepts_complex(mood): + """心情 < 40 → 只接简单单,不碰复杂活。""" + return mood >= LOW_MOOD + + +def slack_multiplier(mood): + """心情 < 20 → 摸鱼概率 ×1.5。""" + return 1.5 if mood < SLACK_MOOD else 1.0 + + +# ── 疲劳与罢工 ────────────────────────────────────────────────────────────── +def next_fatigue(fatigue, worked): + """工作一天疲劳 +1,休息一天归零。封顶 FATIGUE_MAX。""" + if not worked: + return 0 + return clamp(fatigue + 1, 0, FATIGUE_MAX) + + +def should_strike(fatigue): + return fatigue >= STRIKE_FATIGUE + + +def should_quit(strike_days): + """连续罢工几天了。今天罢工后到 2 → 离职。""" + return strike_days >= QUIT_STRIKE_DAYS + + +# ── 声望影响 ──────────────────────────────────────────────────────────────── +def rep_grab_bonus(rep): + """声望 0~100 → 抢单权重 1.0~1.2(DESIGN 4.2:声望高的抢好单)。""" + return 1.0 + (clamp(rep, REP_MIN, REP_MAX) / 100.0) * 0.2 + + +def reward_scale(rep): + """声誉 0~100 → 客户报酬倍率 0.55~1.05(DESIGN 7.1「声誉影响报酬」)。 + + ★M4-4:报酬倍率必须**有上限**。原来按天复利(每天 +12%,30 天 4.5 倍), + 模拟器实测 30 天把 500 块滚到 21921 块,破产条件形同虚设。 + 挂在声誉上就两头都稳:声誉封顶 100 → 倍率封顶 1.05; + 而且这份钱是玩家一单一单挣的,不是日历送的。 + """ + r = clamp(rep, REP_MIN, REP_MAX) + return 0.55 + (r / 100.0) * 0.5 + + +def rep_gain(rep): + """完成一单声誉涨多少:越接近 100 涨得越少,50→100 不会 7 天就顶格。 + + ★M4-4 实测:原来固定 +3,3.2 单/天 → 第 7 天顶到 100, + 此后声誉这条线就完全没有变量了(后面 23 天全在 100)。 + 现在按 3×(100-rep)/50 衰减并四舍五入,90 以上基本不再涨—— + 声誉要靠打回少、交付多慢慢磨,而不是几天就封顶。 + """ + r = clamp(rep, REP_MIN, REP_MAX) + return max(0, int(round(3 * (100.0 - r) / 50.0))) + + +# ── 经济 ──────────────────────────────────────────────────────────────────── +def daily_wages(staff_count): + """在职人数 × 日薪。人走了工资当然不用发。""" + return max(0, staff_count) * SALARY_PER_DAY + + +def daily_settle(money, staff_count, overtime_days=0, team_built=False): + """一天的开销结算。返回 (新资金, 明细)。""" + wages = daily_wages(staff_count) + over = OVERTIME * max(0, overtime_days) + build = TEAM_BUILD if team_built else 0 + spent = wages + over + build + return money - spent, { + "wages": wages, "overtime": over, "team_build": build, "total": spent} + + +def apply_team_build(mood_dict, delta=TEAM_BUILD_MOOD): + """团建:全员回血。返回新心情字典。""" + return {k: clamp(v + delta, MOOD_MIN, MOOD_MAX) for k, v in mood_dict.items()} + + +def natural_decay(stamina_dict, mood_dict, rested=(), slackers=()): + """每日自然衰减:不休息掉体力,心情轻微下滑(不上班心情也不会自己变好)。 + + rested 里的人体力回满;slackers 里的是罢工的,体力也回。 + """ + out_s, out_m = {}, {} + for who, st in stamina_dict.items(): + if who in rested: + out_s[who] = STAMINA_MAX + elif who in slackers: + out_s[who] = clamp(st + 40, 0, STAMINA_MAX) + else: + out_s[who] = clamp(st - 6, 0, STAMINA_MAX) + for who, md in mood_dict.items(): + out_m[who] = clamp(md - 3, MOOD_MIN, MOOD_MAX) + return out_s, out_m + + +# ── 失败条件(DESIGN 7.3)─────────────────────────────────────────────────── +def bankrupt(money, neg_days): + """资金 < 0 且持续 3 天。""" + return money < 0 and neg_days >= BANKRUPT_DAYS + + +def game_over(money, rep, on_duty, neg_days=0): + """返回失败原因字符串,没失败返回 None。""" + if len(on_duty) == 0: + return "全员离职,事务所关门" + if rep <= REP_MIN: + return "声誉归零,被行业除名" + if bankrupt(money, neg_days): + return "资金为负已经 %d 天,破产了" % neg_days + return None diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/env.example b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/env.example new file mode 100644 index 00000000..0ba22666 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/env.example @@ -0,0 +1,9 @@ +# 将玩家选择的模型 Key 保存到 MiniApp 私有目录中的配置文件。 +# 至少一家员工模型即可;M3 和 Flash 共用 MiniMax Key。 +MINIMAX_API_KEY= +LONGCAT_API_KEY= +KIMI_API_KEY= +STEPFUN_API_KEY= +# Jev 自主决策可选,使用普通 OpenRouter API Key;获取入口:https://openrouter.ai/settings/keys +# 未配置时按本地规则运行。此 Key 不能代替上面的员工模型 Key。 +OPENROUTER_API_KEY= diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/requirements.txt b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/requirements.txt new file mode 100644 index 00000000..66108cb6 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/requirements.txt @@ -0,0 +1,4 @@ +Flask>=3.0,<4.0 +requests>=2.31 +python-dotenv>=1.0 +Werkzeug>=3.0,<4.0 diff --git a/plugins/cristinamadureira353-svg/ai-office/miniapp/node/server.mjs b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/server.mjs new file mode 100644 index 00000000..8aa6fbc3 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/miniapp/node/server.mjs @@ -0,0 +1,227 @@ +import { createServer } from 'node:http'; +import { readFile, writeFile, rename, mkdir, stat } from 'node:fs/promises'; +import { join, basename, isAbsolute } from 'node:path'; +import { spawn } from 'node:child_process'; +import { createInterface } from 'node:readline'; + +const ROUTES = new Map([['/api/state','GET'], ['/api/saves','GET'], ['/api/command','POST'], + ['/api/chat','POST'], ['/api/save','POST'], ['/api/load','POST'], ['/api/newday','POST'], ['/api/accept','POST']]); + +export async function start(context) { + const clientRoot = join(context.pluginRoot, 'miniapp/client'); + const assets = JSON.parse(await readFile(join(context.pluginRoot, 'miniapp/node/assets.json'), 'utf8')); + const setupGuide = (await readFile(join(context.pluginRoot, 'SETUP_PROMPT.md'), 'utf8')).trim(); + const settingsPath = join(context.dataDir, 'settings.json'); + await mkdir(context.dataDir, { recursive: true }); + let settings = null; + try { settings = JSON.parse(await readFile(settingsPath, 'utf8')); } + catch (error) { if (error.code !== 'ENOENT') context.logger.warn('ai-office.settings.unreadable'); } + let child = null, lines = null, starting = null, disposed = false, sequence = 0, savingSettings = false, checkingPython = null; + const pending = new Map(); + const sockets = new Set(); + + function json(response, status, data) { + response.writeHead(status, { 'content-type':'application/json; charset=utf-8', 'cache-control':'no-store' }); + response.end(JSON.stringify(data)); + } + function rejectPending() { + for (const response of pending.values()) { + if (!response.headersSent) json(response, 503, { ok:false, msg:'游戏服务已停止,重新打开游戏即可继续。' }); + else response.end(); + } + pending.clear(); + } + async function backend() { + if (disposed) throw new Error('disposed'); + if (starting) return starting; + if (child && child.exitCode === null) return child; + if (!settings) throw new Error('not_configured'); + starting = new Promise((resolve, reject) => { + const args = ['-u', join(context.pluginRoot, 'miniapp/node/backend/server/miniapp_bridge.py'), + '--data-dir', context.dataDir, '--env-file', settings.envFile]; + if (settings.initialSave) args.push('--initial-save', settings.initialSave); + const process = spawn(settings.pythonPath, args, { + cwd:join(context.pluginRoot, 'miniapp/node/backend'), shell:false, windowsHide:true, + stdio:['pipe','pipe','pipe'], env:{ ...globalThis.process.env, PYTHONDONTWRITEBYTECODE:'1' } + }); + child = process; + let ready = false; + const timer = setTimeout(() => { process.kill(); reject(new Error('backend_timeout')); }, 12000); + process.stderr.on('data', () => {}); // 不把可能包含路径或凭据的依赖诊断发给页面或 Host。 + lines = createInterface({ input:process.stdout }); + lines.on('line', raw => { + let frame; + try { frame = JSON.parse(raw); } catch { return; } + if (frame.type === 'ready') { + ready = true; clearTimeout(timer); resolve(process); return; + } + const response = pending.get(frame.id); + if (!response || response.destroyed) return; + if (frame.type === 'head') response.writeHead(frame.status, { + 'content-type':frame.contentType, 'cache-control':'no-store', 'x-accel-buffering':'no' + }); + else if (frame.type === 'data') response.write(frame.body); + else if (frame.type === 'end') { pending.delete(frame.id); response.end(); } + else if (frame.type === 'error') { + pending.delete(frame.id); + if (!response.headersSent) json(response, 503, { ok:false, msg:frame.message }); + else response.end(); + } + }); + process.once('error', () => { clearTimeout(timer); reject(new Error('backend_start_failed')); }); + process.once('exit', () => { + clearTimeout(timer); lines?.close(); + if (child === process) child = null; + rejectPending(); + if (!ready) reject(new Error('backend_start_failed')); + }); + }).finally(() => { starting = null; }); + return starting; + } + + async function bodyOf(request) { + const chunks = []; let size = 0; + for await (const chunk of request) { + size += chunk.length; + if (size > 65536) throw new Error('body_too_large'); + chunks.push(chunk); + } + const text = Buffer.concat(chunks).toString('utf8'); + const body = text ? JSON.parse(text) : {}; + if (!body || Array.isArray(body) || typeof body !== 'object') throw new Error('invalid_body'); + return body; + } + async function validateSettings(body) { + const result = {}; + for (const key of ['pythonPath', 'envFile', 'initialSave']) { + const value = body[key] || ''; + if (typeof value !== 'string' || value.includes('\0') || (!value && key !== 'initialSave') || (value && !isAbsolute(value))) + throw new Error('invalid_settings'); + if (value && !(await stat(value)).isFile()) throw new Error('invalid_settings'); + result[key] = value; + } + if (!/^python(?:3(?:\.\d+)?)?(?:\.exe)?$/i.test(basename(result.pythonPath))) throw new Error('invalid_python'); + if (child) throw new Error('backend_already_running'); + await new Promise((resolve, reject) => { + const probe = spawn(result.pythonPath, ['-c', 'import flask, requests, dotenv'], { + shell:false, windowsHide:true, stdio:'ignore', + env:{ ...globalThis.process.env, PYTHONDONTWRITEBYTECODE:'1' } + }); + checkingPython = probe; + const timer = setTimeout(() => { probe.kill('SIGKILL'); }, 8000); + function finish(error) { + clearTimeout(timer); + if (checkingPython === probe) checkingPython = null; + if (error) reject(new Error('python_not_ready')); else resolve(); + } + probe.once('error', () => finish(true)); + probe.once('exit', code => finish(code !== 0)); + if (disposed) probe.kill('SIGKILL'); + }); + if (disposed) throw new Error('disposed'); + return result; + } + + function setupPrompt() { + const runtime = { configurationEndpoint:`http://${context.listen.host}:${context.listen.port}/api/settings`, + pluginRoot:context.pluginRoot, dataDir:context.dataDir }; + return setupGuide + '\n\n本次运行信息(由 MiniApp 自动提供,供 AI 执行配置;保持页面打开直到配置完成):\n' + + JSON.stringify(runtime, null, 2); + } + + const server = createServer(async (request, response) => { + try { + const host = request.headers.host || ''; + if (![`${context.listen.host}:${context.listen.port}`, `localhost:${context.listen.port}`].includes(host)) + return json(response, 403, { ok:false, msg:'无效的本地请求' }); + const origin = request.headers.origin; + if (origin && ![`http://${context.listen.host}:${context.listen.port}`, `http://localhost:${context.listen.port}`].includes(origin)) + return json(response, 403, { ok:false, msg:'无效的请求来源' }); + const url = new URL(request.url || '/', 'http://miniapp.local'); + if (request.method === 'GET' && url.pathname === '/api/settings') + return json(response, 200, { ok:true, configured:!!settings, settings, backendRunning:!!child }); + if (request.method === 'GET' && url.pathname === '/api/setup') + return json(response, 200, { ok:true, prompt:setupPrompt() }); + if (request.method === 'POST' && url.pathname === '/api/settings') { + if (savingSettings) return json(response, 409, { ok:false, msg:'正在保存配置,请稍后再试' }); + savingSettings = true; + try { + const next = await validateSettings(await bodyOf(request)); + const temporary = settingsPath + '.tmp'; + await writeFile(temporary, JSON.stringify(next), { mode:0o600 }); + await rename(temporary, settingsPath); settings = next; + return json(response, 200, { ok:true }); + } finally { savingSettings = false; } + } + if (ROUTES.get(url.pathname) === request.method) { + if (pending.size >= 32) return json(response, 429, { ok:false, msg:'操作较多,请稍后再试' }); + const body = request.method === 'POST' ? await bodyOf(request) : {}; + const process = await backend(); + if (disposed || response.destroyed) return; + const id = ++sequence; + pending.set(id, response); + response.on('close', () => { + if (pending.delete(id) && !process.killed && process.stdin.writable) + process.stdin.write(JSON.stringify({ type:'cancel', id }) + '\n'); + }); + process.stdin.write(JSON.stringify({ id, method:request.method, path:url.pathname, body }) + '\n'); + return; + } + const key = url.pathname === '/ai-office' ? '/index.html' : url.pathname; + const asset = assets[key]; + if (asset && ['GET','HEAD'].includes(request.method)) { + response.writeHead(200, { 'content-type':asset.type, 'cache-control':'no-cache', + 'content-length':asset.bytes, ...(asset.gzip ? { 'content-encoding':'gzip' } : {}) }); + if (request.method === 'GET') { + for (const file of asset.parts || [asset.file]) { + if (response.destroyed) break; + const bytes = await readFile(join(clientRoot, file)); + if (!response.write(bytes)) await new Promise(resolve => { + response.once('drain', resolve); response.once('close', resolve); + }); + } + } + response.end(); return; + } + json(response, 404, { ok:false, msg:'找不到这个页面' }); + } catch (error) { + if (!response.headersSent) json(response, 503, { ok:false, msg:error.message === 'python_not_ready' + ? 'Python 依赖尚未准备好,请让 AI 安装 Flask、requests、python-dotenv 后重新保存配置。' + : '请检查本地 Python 和 API 配置,重新打开后再试。' }); + else response.end(); + } + }); + server.on('connection', socket => { sockets.add(socket); socket.once('close', () => sockets.delete(socket)); }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(context.listen.port, context.listen.host, resolve); + }); + context.logger.info('ai-office.ready'); + const dispose = async () => { + if (disposed) return; + disposed = true; + context.signal.removeEventListener('abort', onAbort); + if (checkingPython && checkingPython.exitCode === null) { + const probe = checkingPython; + await new Promise(resolve => { + probe.once('exit', resolve); probe.once('error', resolve); probe.kill('SIGKILL'); + }); + } + const process = child; + if (process && process.exitCode === null) { + await new Promise(resolve => { + const timer = setTimeout(() => process.kill('SIGTERM'), 5000); + const force = setTimeout(() => process.kill('SIGKILL'), 7000); + process.once('exit', () => { clearTimeout(timer); clearTimeout(force); resolve(); }); + if (process.stdin.writable) process.stdin.write('{"type":"shutdown"}\n'); + }); + } + rejectPending(); lines?.close(); + for (const socket of sockets) socket.destroy(); + await new Promise(resolve => server.close(resolve)); + }; + const onAbort = () => { void dispose(); }; + context.signal.addEventListener('abort', onAbort, { once:true }); + if (context.signal.aborted) await dispose(); + return { dispose }; +} diff --git a/plugins/cristinamadureira353-svg/ai-office/package.json b/plugins/cristinamadureira353-svg/ai-office/package.json new file mode 100644 index 00000000..b7339569 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/package.json @@ -0,0 +1,7 @@ +{ + "name": "ai-office", + "version": "0.5.10", + "type": "module", + "license": "MIT", + "mcode": { "schemaVersion": 2, "miniApp": "./miniapp/miniapp.json" } +} diff --git a/plugins/cristinamadureira353-svg/ai-office/tests/conflict.test.mjs b/plugins/cristinamadureira353-svg/ai-office/tests/conflict.test.mjs new file mode 100644 index 00000000..75215829 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/tests/conflict.test.mjs @@ -0,0 +1,134 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { fixture } from './fixture.mjs'; + +test('conflict popup: four real mouse choices reach Game.judge and close the modal', {timeout:90000}, async t => { + const f = await fixture({conflict:true}); + const {chromium} = createRequire(join(process.env.AI_OFFICE_TEST_NODE_MODULES, 'package.json'))('playwright'); + const evidence = resolve(process.env.AI_OFFICE_TEST_EVIDENCE); + await mkdir(evidence, {recursive:true}); + let browser; + const errors = [], commands = [], replies = [], results = [], responseReads = []; + const pause = ms => new Promise(resolve => setTimeout(resolve, ms)); + const waitFor = async predicate => { + for (let i=0; i<40; i++) { if (await predicate()) return; await pause(100); } + throw new Error('Popup click did not complete the expected judgement'); + }; + try { + assert.equal((await (await f.post('/api/settings', f.settings)).json()).ok, true); + await t.test('pending conflict survives closing and reopening with dialogue and progress', async () => { + const before = await f.get('/api/state'); + await f.dispose(); + await f.restart(); + const after = await f.get('/api/state'); + assert.deepEqual(after.conflicts, before.conflicts, 'original participants and dialogue survive reopening'); + assert.equal(after.day, before.day); + assert.equal(after.money, before.money); + assert.deepEqual(after.clients, before.clients); + }); + browser = await chromium.launch({headless:true, executablePath:process.env.AI_OFFICE_TEST_CHROMIUM, + args:['--use-angle=swiftshader', '--enable-unsafe-swiftshader']}); + const page = await browser.newPage({viewport:{width:879,height:534}}); + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { if (message.type()==='error') errors.push(message.text()); }); + page.on('request', request => { + if (request.url()===f.base+'/api/command' && request.method()==='POST') commands.push(JSON.parse(request.postData())); + }); + page.on('response', response => { + if (response.url()===f.base+'/api/command') + responseReads.push(response.json().then(reply => replies.push(reply)).catch(error => errors.push(error.message))); + }); + await page.goto(f.base+'/ai-office'); + const canvas = page.frameLocator('#game').locator('#canvas'); + await canvas.waitFor({state:'visible'}); + await page.waitForFunction(() => document.querySelector('#game').contentWindow.AIOfficeStream && !document.querySelector('#game').contentWindow.document.querySelector('#status')); + await pause(600); + const button = index => { + const {width,height} = page.viewportSize(); + const scale = Math.min(width/1120, height/720); + const x = (width/scale-620)/2 + 24 + index*(135.5+10) + 135.5/2; + const y = (height/scale-390)/2 + 390 - 60 + 18; + return {x:x*scale,y:y*scale}; + }; + const stage = process.env.AI_OFFICE_CONFLICT_STAGE || 'after'; + await page.screenshot({path:join(evidence, 'conflict-'+stage+'-popup.png')}); + for (const [index,kind] of ['a','b','reject','scold'].entries()) { + await t.test('choice '+kind, async () => { + const conflict = await f.resetConflict(); + await pause(700); + const before = commands.length; + await canvas.click({position:button(index)}); + await waitFor(() => commands.length > before); + assert.equal(commands.length, before+1, 'one click sends one judgement'); + assert.equal(commands.at(-1).action, 'judge'); + assert.equal(commands.at(-1).arg, conflict.client); + assert.equal(commands.at(-1).kind, kind, 'the API receives the chosen kind'); + await waitFor(async () => !(await f.get('/api/state')).conflicts.length); + const state = await f.get('/api/state'); + const customer = state.clients.find(c => c.id===conflict.client); + const m3 = state.workers.find(w => w.id==='m3'), flash = state.workers.find(w => w.id==='flash'); + const winner = {a:'m3',b:'flash'}[kind] || null; + assert.equal(customer.served_by, winner); + assert.equal(customer.status, winner ? '抢单中' : '等待'); + assert.equal(m3.mood, {a:73,b:62,reject:65,scold:67}[kind]); + assert.equal(flash.mood, {a:62,b:73,reject:65,scold:67}[kind]); + if (!winner) assert.equal(customer.patience, 150); + await pause(700); + // 裁决成功后再次点击原按钮位置,不应再发请求,证明弹窗已关闭。 + await canvas.click({position:button(index)}); + await pause(200); + assert.equal(commands.length, before+1, 'resolved modal no longer intercepts clicks'); + results.push({kind,client:conflict.client,winner,status:customer.status,mood:{m3:m3.mood,flash:flash.mood}}); + await page.screenshot({path:join(evidence, 'conflict-'+stage+'-'+kind+'-resolved.png')}); + }); + } + await t.test('failed request shows feedback and allows a real retry', async () => { + const conflict = await f.resetConflict(); + await pause(700); + await page.route(f.base+'/api/command', route => route.fulfill({status:200, + contentType:'application/json',body:JSON.stringify({ok:false,msg:'测试连接失败,请重新选择'})}), {times:1}); + const before = commands.length, replyBefore = replies.length; + await canvas.click({position:button(0)}); + await waitFor(() => replies.length > replyBefore); + assert.equal(replies.at(-1).ok, false); + assert.equal((await f.get('/api/state')).conflicts[0].client, conflict.client, 'failed request keeps the unresolved order'); + await pause(500); + await page.screenshot({path:join(evidence, 'conflict-'+stage+'-retry-feedback.png')}); + await canvas.click({position:button(1)}); + await waitFor(async () => !(await f.get('/api/state')).conflicts.length); + assert.equal(commands.length, before+2); + assert.equal(commands.at(-1).kind, 'b'); + assert.equal((await f.get('/api/state')).clients.find(c => c.id===conflict.client).served_by, 'flash'); + results.push({scenario:'failure-retry',client:conflict.client,winner:'flash'}); + }); + await t.test('slow response disables all choices and submits only once', async () => { + await f.resetConflict(); + await pause(700); + await page.route(f.base+'/api/command', async route => { await pause(1000); await route.continue(); }, {times:1}); + const before = commands.length; + await canvas.click({position:button(0)}); + await canvas.click({position:button(3)}); + await pause(100); + await page.screenshot({path:join(evidence, 'conflict-'+stage+'-submitting.png')}); + assert.equal(commands.length, before+1, 'clicking another choice during submission sends no second request'); + await waitFor(async () => !(await f.get('/api/state')).conflicts.length); + assert.equal(commands.at(-1).kind, 'a'); + results.push({scenario:'slow-response',requests:1}); + }); + const received = (await readFile(join(f.dataDir,'commands.jsonl'),'utf8').catch(() => '')).trim().split('\n').filter(Boolean).map(JSON.parse); + assert.equal(received.filter(c => c.action==='judge').length, 6, 'real backend processed four choices, a retry and one delayed submission'); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'), false); + assert.equal((await readdir(f.dataDir)).includes('calls.jsonl'), false, 'judgement verification consumes no model calls'); + await Promise.all(responseReads); + assert.deepEqual(errors, []); + } finally { + await Promise.all(responseReads); + await writeFile(join(evidence, 'conflict-'+(process.env.AI_OFFICE_CONFLICT_STAGE || 'after')+'-results.json'), JSON.stringify({commands,replies,results,errors},null,2)); + await browser?.close(); + await f.dispose(); + await rm(f.scratch,{recursive:true,force:true}); + } +}); diff --git a/plugins/cristinamadureira353-svg/ai-office/tests/fixture.mjs b/plugins/cristinamadureira353-svg/ai-office/tests/fixture.mjs new file mode 100644 index 00000000..ec57c48b --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/tests/fixture.mjs @@ -0,0 +1,120 @@ +import { mkdtemp, writeFile, chmod, mkdir, cp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { createServer } from 'node:net'; +import { pathToFileURL } from 'node:url'; + +export async function fixture({relocate=false,client=false,resting=false,conflict=false,initialSave=''}={}) { + let pluginRoot = resolve(process.env.AI_OFFICE_TEST_PLUGIN); + const python = process.env.AI_OFFICE_TEST_PYTHON; + if (!python) throw new Error('Set AI_OFFICE_TEST_PYTHON to the existing venv interpreter'); + const scratch = await mkdtemp(join(tmpdir(), 'ai-office-miniapp-test-')); + if (relocate) { + const relocated = join(scratch, 'New Player Installed App'); + await cp(pluginRoot, relocated, {recursive:true}); + pluginRoot = relocated; + } + const dataDir = join(scratch, 'private'); + await mkdir(dataDir); + const pythonPath = join(scratch, 'python'), envFile = join(scratch, 'empty.env'); + await writeFile(envFile, '# isolated fake API configuration\n'); + await writeFile(pythonPath, `#!${python} +import json, os, pathlib, runpy, sys +if sys.argv[1:2] == ['-c']: + exec(sys.argv[2]) + raise SystemExit(0) +bridge = pathlib.Path(sys.argv[2]) +sys.path.insert(0, str(bridge.parent)) +data = pathlib.Path(sys.argv[sys.argv.index('--data-dir') + 1]) +os.environ['AI_OFFICE_ENV_PATH'] = sys.argv[sys.argv.index('--env-file') + 1] +os.environ['AI_OFFICE_SAVE_DIR'] = str(data / 'saves') +os.environ['OPENROUTER_API_KEY'] = '' +import providers as P +for item in P.PROVIDERS.values(): os.environ[item['key_env']] = 'fake-test-key' +import requests +def denied(*args, **kwargs): + (data / 'network-attempt').write_text('blocked') + raise AssertionError('No paid network requests permitted') +requests.sessions.Session.request = denied +def fake(who, text, **kwargs): + with (data / 'calls.jsonl').open('a') as log: + log.write(json.dumps({'who':who, 'text':text}, ensure_ascii=False) + '\\n') + if who == 'kimi' and not (data / 'quota-restored').exists(): + yield P.error_event(who, 429, {'error':{'type':'exceeded_current_quota_error'}}) + return + for text in ['在', '的,', '老板找我啥事']: + yield {'type':'delta', 'who':who, 'text':text} + yield {'type':'done', 'who':who} +P.stream_chat = fake +import game as G, game_app as API +API.boot = lambda: None +if ${resting ? 'True' : 'False'}: + for who, spot in G.REST_SPOTS['slack'].items(): + employee = G.GAME.workers[who] + employee.pos, employee.acting = spot, '在摸鱼' + employee.path, employee.order = [], None +if ${client ? 'True' : 'False'}: + customer = G.Client('帮我整理一份测试文档', 96, 200, 'document', 'normal') + customer.status, customer.served_by, customer.slot = '服务中', 'm3', 0 + G.GAME.clients.append(customer) + G.GAME.slots[0] = customer + G.GAME.workers['m3'].job = customer.id +if ${conflict ? 'True' : 'False'}: + def seed_conflict(): + with G.GAME._lock: + G.GAME.clients.clear() + G.GAME.conflicts.clear() + for slot in G.GAME.slots: G.GAME.slots[slot] = None + for employee in G.GAME.workers.values(): + employee.job, employee.order, employee.mood = None, None, 70 + customer = G.Client('帮我把这套流程跑通', 96, 200, 'document', 'normal') + customer.status, customer.slot = '冲突中', 0 + G.GAME.clients.append(customer) + G.GAME.slots[0] = customer + G.GAME.conflicts[customer.id] = { + 'a':'m3', 'b':'flash', 'task':customer.task, 'client':customer.id, 'judged':False, + 'lines':[{'who':'m3','text':'我有现成模板,今晚就能跑通。'}, + {'who':'flash','text':'我比他更合适,这单给我。'}]} + for who in ('m3', 'flash'): + employee = G.GAME.workers[who] + employee.job, employee.acting = customer.id, '去理论' + # 裁决仍走真实 Game.judge;仅隔离获胜后的付费交付线程。 + G.GAME._serve_client_thread = lambda *args: None + seed_conflict() + @API.app.before_request + def fixture_requests(): + marker = data / 'reset-conflict' + if marker.exists(): + marker.unlink() + seed_conflict() + if API.request.path == '/api/command': + with (data / 'commands.jsonl').open('a') as log: + log.write(json.dumps(API.request.get_json(silent=True), ensure_ascii=False) + '\\n') +(data / 'fixture-pid').write_text(str(os.getpid())) +sys.argv = [str(bridge)] + sys.argv[3:] +runpy.run_path(str(bridge), run_name='__main__') +`); + await chmod(pythonPath, 0o700); + const listen = { host:'127.0.0.1', port:await new Promise((resolvePort, reject) => { + const socket = createServer(); socket.once('error', reject); + socket.listen(0, '127.0.0.1', () => { const port = socket.address().port; socket.close(() => resolvePort(port)); }); + }) }; + const logs = [], signal = new AbortController(); + const logger = Object.fromEntries(['info','debug','warn','error'].map(level => [level, message => logs.push(message)])); + const module = await import(pathToFileURL(process.env.AI_OFFICE_TEST_SERVER || join(pluginRoot, 'miniapp/node/server.mjs'))); + const context = { pluginId:'ai-office', pluginRoot, dataDir, listen, logger, signal:signal.signal }; + let runtime = await module.start(context); + const base = 'http://' + listen.host + ':' + listen.port; + const settings = { pythonPath, envFile, initialSave }; + const get = async path => (await fetch(base + path)).json(); + const post = (path, body) => fetch(base + path, { method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify(body) }); + return { pluginRoot, scratch, dataDir, base, settings, get, post, logs, + async resetConflict() { + if (!conflict) throw new Error('Conflict fixture is required'); + await writeFile(join(dataDir, 'reset-conflict'), 'reset'); + return (await get('/api/state')).conflicts[0]; + }, + async dispose() { await runtime.dispose(); }, + async restart() { runtime = await module.start(context); } + }; +} diff --git a/plugins/cristinamadureira353-svg/ai-office/tests/legacy.test.mjs b/plugins/cristinamadureira353-svg/ai-office/tests/legacy.test.mjs new file mode 100644 index 00000000..a831e5f5 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/tests/legacy.test.mjs @@ -0,0 +1,102 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify, isDeepStrictEqual } from 'node:util'; +import { createRequire } from 'node:module'; +import { fixture } from './fixture.mjs'; + +test('legacy conflict save: complete state, preserved deliveries, reopening and usable Web UI', {timeout:60000}, async () => { + const f = await fixture(); + const evidence = resolve(process.env.AI_OFFICE_TEST_EVIDENCE); + await mkdir(evidence, {recursive:true}); + let browser; + const errors = [], selected = []; + const pause = ms => new Promise(resolve => setTimeout(resolve, ms)); + try { + const source = await promisify(execFile)(process.env.AI_OFFICE_TEST_PYTHON, ['-c', ` +import json,sys +sys.path.insert(0,sys.argv[1]) +import game as G,save as S +g=G.Game();g.money=869;g.rep=59;g.tick=81175 +for i,(status,who) in enumerate([('待验收','step5'),('待验收','longcat'),('冲突中',None)]): + c=G.Client('测试订单'+str(i),96,200,'document','normal');c.id='legacy-'+str(i);c.slot=i;c.status=status;c.served_by=who + c.result='完整测试交付物'+str(i) if who else None + g.clients.append(c);g.slots[i]=c +g.conflicts={'legacy-2':None} +d=S.snapshot(g);d.pop('conflict_data',None) +print(json.dumps(d,ensure_ascii=False)) +`, join(f.pluginRoot,'miniapp/node/backend/server')], {env:{...process.env,AI_OFFICE_ENV_PATH:f.settings.envFile,AI_OFFICE_SAVE_DIR:join(f.dataDir,'saves'),PYTHONDONTWRITEBYTECODE:'1'}}); + f.settings.initialSave = join(f.scratch, 'legacy.json'); + await writeFile(f.settings.initialSave, source.stdout); + assert.equal((await (await f.post('/api/settings', f.settings)).json()).ok, true); + const response = await fetch(f.base+'/api/state'); + assert.equal(response.status, 200, 'legacy conflict IDs must not break the entire state endpoint'); + const before = await response.json(); + assert.equal(before.workers.length, 5); + assert.equal(before.clients.length, 3); + assert.deepEqual(before.conflicts, []); + assert.deepEqual([before.day,before.money,before.rep,before.tick], [1,869,59,81175]); + assert.equal(before.clients[2].status, '等待'); + for (const i of [0,1]) { + assert.equal(before.clients[i].status, '待验收'); + assert.equal(before.clients[i].result, '完整测试交付物'+i); + } + await f.dispose(); + await f.restart(); + const reopened = await f.get('/api/state'); + assert.ok(isDeepStrictEqual(reopened.clients, before.clients), 'reopening preserves recovered order states and deliveries'); + assert.deepEqual(reopened.conflicts, []); + const {chromium} = createRequire(join(process.env.AI_OFFICE_TEST_NODE_MODULES,'package.json'))('playwright'); + browser = await chromium.launch({headless:true,executablePath:process.env.AI_OFFICE_TEST_CHROMIUM, + args:['--use-angle=swiftshader','--enable-unsafe-swiftshader']}); + const page = await browser.newPage({viewport:{width:1102,height:957}}); + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { + if (message.type()==='error' && !message.text().includes('status of 500')) errors.push(message.text()); + }); + await page.route(f.base+'/api/state', route => route.fulfill({status:500, contentType:'application/json',body:'{"ok":false,"error":"internal"}'})); + await page.goto(f.base+'/ai-office'); + const canvas = page.frameLocator('#game').locator('#canvas'); + await canvas.waitFor({state:'visible'}); + await page.waitForFunction(() => document.querySelector('#game').contentWindow.AIOfficeStream && !document.querySelector('#game').contentWindow.document.querySelector('#status')); + await pause(1000); + await page.screenshot({path:join(evidence, 'state-unavailable-feedback.png')}); + await page.unroute(f.base+'/api/state'); + await pause(1400); + await page.screenshot({path:join(evidence, 'legacy-state-ui-recovered.png')}); + await writeFile(join(f.dataDir,'quota-restored'), 'true'); + const scale = 1102/1120, width = 1120, height = 957/scale; + const cardWidth = (width-128)/5; + for (const [i,who] of ['m3','flash','longcat','kimi','step5'].entries()) { + // 点击五张身份卡并向隔离模型发送,验证恢复后的交互路径。 + await canvas.click({position:{x:(44+i*(cardWidth+10)+cardWidth-37)*scale,y:(height-120+23)*scale}}); + await pause(180); + const text = 'restored '+who; + await page.keyboard.type(text, {delay:30}); + await page.keyboard.press('Enter'); + let delivered = false; + for (let attempt=0; attempt<30; attempt++) { + const calls = (await readFile(join(f.dataDir,'calls.jsonl'),'utf8').catch(()=>'')).trim().split('\n').filter(Boolean).map(JSON.parse); + if (calls.some(call=>call.who===who && call.text.includes(text))) { delivered=true; break; } + await pause(100); + } + assert.ok(delivered, who+' restored chat accepts input and reaches the isolated model'); + selected.push(who); + await pause(150); + await page.keyboard.press('Escape'); + await pause(100); + } + assert.equal(await readFile(f.settings.initialSave,'utf8'), source.stdout, 'initial legacy save remains read-only'); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'), false); + assert.equal((await readFile(join(f.dataDir,'calls.jsonl'),'utf8')).trim().split('\n').length, 5, 'only five intentional fake chat calls'); + assert.deepEqual(errors, []); + await writeFile(join(evidence,'legacy-state-web.json'),JSON.stringify({http:response.status,day:before.day,money:before.money,rep:before.rep, + workers:before.workers.map(w=>w.id),clients:before.clients.map(c=>({id:c.id,status:c.status,served_by:c.served_by})),selected,errors},null,2)); + } finally { + await browser?.close(); + await f.dispose(); + await rm(f.scratch,{recursive:true,force:true}); + } +}); diff --git a/plugins/cristinamadureira353-svg/ai-office/tests/runtime.test.mjs b/plugins/cristinamadureira353-svg/ai-office/tests/runtime.test.mjs new file mode 100644 index 00000000..5098bfd7 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/tests/runtime.test.mjs @@ -0,0 +1,103 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile, readdir, stat, writeFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { fixture } from './fixture.mjs'; + +test('MiniApp runtime and private Python bridge', async t => { + const f = await fixture(); + const source = await promisify(execFile)(process.env.AI_OFFICE_TEST_PYTHON, ['-c', + 'import sys,json; sys.path.insert(0,sys.argv[1]); import game,save; g=game.Game(); g.day=6; g.money=777; print(json.dumps(save.snapshot(g,"只读继承测试"),ensure_ascii=False))', + join(f.pluginRoot, 'miniapp/node/backend/server')], {env:{...process.env, AI_OFFICE_ENV_PATH:f.settings.envFile}}); + f.settings.initialSave = join(f.scratch, 'original.json'); + await writeFile(f.settings.initialSave, source.stdout); + try { + await t.test('start serves setup without launching Python or paid APIs', async () => { + assert.equal((await f.get('/api/settings')).configured, false); + assert.match(await (await fetch(f.base + '/ai-office')).text(), /复制引导提示词/); + assert.equal((await readdir(f.dataDir)).length, 0); + }); + await t.test('rejects remote origins and arbitrary files', async () => { + assert.equal((await fetch(f.base + '/api/state', {headers:{origin:'https://outside.invalid'}})).status, 403); + assert.equal((await fetch(f.base + '/.env')).status, 404); + assert.equal((await fetch(f.base + '/api/unknown')).status, 404); + }); + await t.test('serves compressed engine as a valid WASM response', async () => { + const response = await fetch(f.base + '/game/game.wasm'); + assert.equal(response.status, 200); + assert.equal(response.headers.get('content-type'), 'application/wasm'); + assert.equal(response.headers.get('content-encoding'), 'gzip'); + const bytes = new Uint8Array(await response.arrayBuffer()); + assert.deepEqual([...bytes.slice(0,4)], [0,97,115,109]); + }); + await t.test('validates and atomically stores interpreter and env paths', async () => { + assert.equal((await (await f.post('/api/settings', {pythonPath:'relative', envFile:f.settings.envFile})).json()).ok, false); + assert.equal((await (await f.post('/api/settings', f.settings)).json()).ok, true); + assert.deepEqual(JSON.parse(await readFile(join(f.dataDir, 'settings.json'), 'utf8')), f.settings); + assert.equal((await stat(join(f.dataDir, 'settings.json'))).mode & 0o777, 0o600); + }); + await t.test('state is V5 and quota failure returns gameplay status through SSE', async () => { + const initial = await f.get('/api/state'); + assert.equal(initial.gameplay_version, 5); + assert.equal(initial.day, 6); + assert.equal(initial.money, 777); + const response = await f.post('/api/chat', {who:'kimi', text:'测试一下'}); + const text = await response.text(); + assert.equal(response.status, 200); + assert.match(response.headers.get('content-type'), /text\/event-stream/); + assert.match(text, /脑袋宕机了/); + const state = await f.get('/api/state'); + const kimi = state.workers.find(w => w.id === 'kimi'); + assert.equal(kimi.brain_down, true); + assert.equal(kimi.acting, '脑袋宕机了'); + assert.equal(kimi.typing, false); + assert.equal(state.workers.find(w => w.id === 'longcat').brain_down, false); + }); + await t.test('disabled role has no further model calls and peers remain usable', async () => { + const before = await readFile(join(f.dataDir, 'calls.jsonl'), 'utf8'); + assert.match(await (await f.post('/api/chat', {who:'kimi', text:'继续'})).text(), /脑袋宕机了/); + assert.equal((await (await f.post('/api/command', {who:'kimi', action:'goto', arg:'desk'})).json()).ok, false); + assert.equal(await readFile(join(f.dataDir, 'calls.jsonl'), 'utf8'), before); + assert.match(await (await f.post('/api/chat', {who:'longcat', text:'你好'})).text(), /老板找我啥事/); + }); + await t.test('settings cannot switch a running backend', async () => { + assert.equal((await (await f.post('/api/settings', f.settings)).json()).ok, false); + assert.deepEqual(JSON.parse(await readFile(join(f.dataDir, 'settings.json'), 'utf8')), f.settings); + }); + await t.test('save and shutdown persist only inside Host dataDir and reap the child', async () => { + assert.equal((await (await f.post('/api/save', {name:'MiniApp测试'})).json()).ok, true); + const pid = Number(await readFile(join(f.dataDir, 'fixture-pid'), 'utf8')); + await f.dispose(); + assert.throws(() => process.kill(pid, 0), /ESRCH/); + await assert.rejects(fetch(f.base + '/api/state')); + assert.equal((await readdir(join(f.dataDir, 'saves'))).length >= 3, true); + assert.equal(await readFile(f.settings.initialSave, 'utf8'), source.stdout); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'), false); + }); + await t.test('restart restores shutdown and manual reboot probes once', async () => { + const files = await readdir(join(f.dataDir, 'saves')); + const old = JSON.parse(await readFile(join(f.dataDir, 'saves', files[0]), 'utf8')); + old.saved_at = 1; + old.workers.forEach(w => { w.brain_down = false; }); + await writeFile(join(f.dataDir, 'saves', '更旧档.json'), JSON.stringify(old)); + await f.restart(); + assert.equal((await f.get('/api/state')).workers.find(w => w.id === 'kimi').brain_down, true); + const before = (await readFile(join(f.dataDir, 'calls.jsonl'), 'utf8')).trim().split('\n').length; + await writeFile(join(f.dataDir, 'quota-restored'), 'true'); + assert.equal((await (await f.post('/api/command', {who:'kimi', action:'reboot'})).json()).ok, true); + for (let i=0; i<20; i++) { + if (!(await f.get('/api/state')).workers.find(w => w.id === 'kimi').brain_down) break; + await new Promise(resolve => setTimeout(resolve, 20)); + } + assert.equal((await f.get('/api/state')).workers.find(w => w.id === 'kimi').brain_down, false); + assert.equal((await readFile(join(f.dataDir, 'calls.jsonl'), 'utf8')).trim().split('\n').length, before + 1); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'), false); + assert.equal(f.logs.some(line => line.includes('fake-test-key')), false); + }); + } finally { + await f.dispose(); + await rm(f.scratch, {recursive:true, force:true}); + } +}); diff --git a/plugins/cristinamadureira353-svg/ai-office/tests/setup.test.mjs b/plugins/cristinamadureira353-svg/ai-office/tests/setup.test.mjs new file mode 100644 index 00000000..7b1655dc --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/tests/setup.test.mjs @@ -0,0 +1,166 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile, readdir, stat, writeFile, chmod, mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { fixture } from './fixture.mjs'; + +test('AI-assisted first setup and persistent configuration', async t => { + const f = await fixture(); + const originalEnv = await readFile(f.settings.envFile, 'utf8'); + try { + await t.test('prompt contains live connection details and the required Key names', async () => { + const data = await f.get('/api/setup'); + assert.equal(data.ok, true); + for (const name of ['MINIMAX_API_KEY', 'LONGCAT_API_KEY', 'KIMI_API_KEY', 'STEPFUN_API_KEY', 'OPENROUTER_API_KEY']) + assert.ok(data.prompt.includes(name), name); + assert.ok(data.prompt.includes(f.base + '/api/settings')); + assert.ok(data.prompt.includes(f.dataDir)); + assert.ok(data.prompt.includes(f.pluginRoot)); + assert.match(data.prompt, /M3 和 Flash 共用/); + assert.match(data.prompt, /可选/); + assert.match(data.prompt, /不要回显/); + assert.match(data.prompt, /不调用付费模型/); + assert.match(data.prompt, /initialSave/); + }); + await t.test('Jev guidance checks existing config and offers an explicit enable or skip choice', async () => { + const {prompt} = await f.get('/api/setup'); + assert.match(prompt, /即使已经配置员工.*检查 Jev/); + assert.match(prompt, /普通 OpenRouter API Key/); + assert.match(prompt, /已有非空 Key.*保留并复用/); + assert.match(prompt, /现在启用 Jev.*暂时跳过/); + assert.match(prompt, /OpenRouter 账户额度/); + assert.match(prompt, /注册或登录.*创建并保存 API Key/); + assert.match(prompt, /openrouter\.ai\/settings\/keys/); + assert.match(prompt, /收到真实 Key 后才写入 OPENROUTER_API_KEY/); + assert.match(prompt, /不用员工 Key 代替/); + assert.match(prompt, /不阻止其他员工配置和游戏启动/); + }); + await t.test('reading Jev guidance never returns a configured Key or starts a model call', async () => { + const secret='isolated-jev-secret-must-not-be-returned'; + try { + await writeFile(f.settings.envFile, 'OPENROUTER_API_KEY='+secret+'\n'); + const {prompt} = await f.get('/api/setup'); + assert.ok(!prompt.includes(secret)); + assert.equal((await readdir(f.dataDir)).length, 0); + assert.ok(!f.logs.join('\n').includes(secret)); + } finally { + await writeFile(f.settings.envFile, originalEnv); + } + }); + await t.test('new players receive a step-by-step guide without a developer workspace assumption', async () => { + const {prompt} = await f.get('/api/setup'); + const guidance = prompt.split('本次运行信息(由 MiniApp 自动提供')[0]; + assert.equal(guidance.trim(), (await readFile(join(f.pluginRoot, 'SETUP_PROMPT.md'), 'utf8')).trim()); + assert.match(guidance, /首次启动向导/); + assert.match(guidance, /不需要游戏源码/); + assert.match(guidance, /没有 Key/); + assert.match(guidance, /注册.*创建/); + assert.match(guidance, /等待我/); + assert.match(guidance, /至少.*一家/); + assert.match(guidance, /不要.*占位/); + assert.doesNotMatch(guidance, /ai-model-party|\/(?:Users|home)\/[^/\s]+|[A-Z]:\\Users\\|当前工作区/); + for (const host of ['platform.minimax.cn', 'longcat.chat', 'kimi.com', 'platform.stepfun.com']) + assert.ok(guidance.includes(host), host); + }); + await t.test('reading the prompt and polling settings neither starts the game nor writes credentials', async () => { + for (let i=0; i<3; i++) assert.equal((await f.get('/api/settings')).configured, false); + assert.equal((await readdir(f.dataDir)).length, 0); + assert.equal(await readFile(f.settings.envFile, 'utf8'), originalEnv); + }); + await t.test('the config API accepts paths instead of raw credentials', async () => { + const result = await (await f.post('/api/settings', {MINIMAX_API_KEY:'secret-must-not-be-saved'})).json(); + assert.equal(result.ok, false); + assert.equal((await f.get('/api/settings')).configured, false); + assert.equal((await readdir(f.dataDir)).length, 0); + }); + await t.test('a Python with missing dependencies cannot mark setup as complete', async () => { + const brokenDir = join(f.scratch, 'broken'); + await mkdir(brokenDir); + const pythonPath = join(brokenDir, 'python'); + await writeFile(pythonPath, `#!${process.env.AI_OFFICE_TEST_PYTHON}\nraise SystemExit(1)\n`); + await chmod(pythonPath, 0o700); + const result = await (await f.post('/api/settings', {...f.settings, pythonPath})).json(); + assert.equal(result.ok, false); + assert.match(result.msg, /依赖/); + assert.equal((await f.get('/api/settings')).configured, false); + assert.equal((await readdir(f.dataDir)).length, 0); + }); + await t.test('AI saves config through the local API with concurrency protection', async () => { + const responses = await Promise.all([ + f.post('/api/settings', {...f.settings, apiKey:'secret-must-not-be-saved'}), + f.post('/api/settings', f.settings) + ]); + assert.deepEqual(responses.map(r => r.status).sort(), [200,409]); + assert.deepEqual(JSON.parse(await readFile(join(f.dataDir, 'settings.json'), 'utf8')), f.settings); + assert.equal((await stat(join(f.dataDir, 'settings.json'))).mode & 0o777, 0o600); + assert.equal((await f.get('/api/settings')).configured, true); + assert.equal((await readdir(f.dataDir)).includes('fixture-pid'), false); + }); + await t.test('configuration survives closing and reopening, without asking for Keys again', async () => { + await f.dispose(); + await f.restart(); + const result = await f.get('/api/settings'); + assert.equal(result.configured, true); + assert.deepEqual(result.settings, f.settings); + assert.equal((await readdir(f.dataDir)).includes('fixture-pid'), false); + assert.equal(await readFile(f.settings.envFile, 'utf8'), originalEnv); + assert.ok(!(await f.get('/api/setup')).prompt.includes('secret-must-not-be-saved')); + assert.ok(!f.logs.join('\n').includes('secret-must-not-be-saved')); + }); + } finally { + await f.dispose(); + await rm(f.scratch, {recursive:true,force:true}); + } +}); + +test('a new player can configure a relocated standalone package with one model Key', async () => { + const f = await fixture({relocate:true}); + try { + const {prompt} = await f.get('/api/setup'); + assert.ok(prompt.includes(f.pluginRoot)); + assert.ok(prompt.includes(f.dataDir)); + assert.ok(prompt.includes(f.base + '/api/settings')); + assert.doesNotMatch(prompt, /ai-model-party|\/(?:Users|home)\/[^/\s]+|[A-Z]:\\Users\\/); + assert.equal((await f.get('/api/settings')).configured, false); + assert.match(await readFile(join(f.pluginRoot, 'miniapp/node/requirements.txt'),'utf8'), /Flask/); + const template = await readFile(join(f.pluginRoot, 'miniapp/node/env.example'),'utf8'); + assert.match(template, /LONGCAT_API_KEY=\s*\n/); + await writeFile(f.settings.envFile,'LONGCAT_API_KEY=isolated-new-player-key\n'); + assert.equal((await (await f.post('/api/settings',f.settings)).json()).ok,true); + assert.equal((await f.get('/api/state')).gameplay_version,5); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'),false); + await f.dispose(); + await f.restart(); + assert.equal((await f.get('/api/settings')).configured,true); + assert.equal(await readFile(f.settings.envFile,'utf8'),'LONGCAT_API_KEY=isolated-new-player-key\n'); + } finally { + await f.dispose(); + await rm(f.scratch,{recursive:true,force:true}); + } +}); + +test('closing MiniApp during dependency check reaps the probe without saving settings', async () => { + const f = await fixture(); + const slowDir = join(f.scratch, 'slow'); + await mkdir(slowDir); + const pythonPath = join(slowDir, 'python'), pidFile = join(f.scratch, 'probe-pid'); + await writeFile(pythonPath, `#!${process.env.AI_OFFICE_TEST_PYTHON}\nimport os, pathlib, time\npathlib.Path(${JSON.stringify(pidFile)}).write_text(str(os.getpid()))\ntime.sleep(60)\n`); + await chmod(pythonPath, 0o700); + const pending = f.post('/api/settings', {...f.settings, pythonPath}).catch(() => null); + try { + let pid; + for (let i=0; i<50; i++) { + pid = await readFile(pidFile, 'utf8').catch(() => ''); + if (pid) break; + await new Promise(resolve => setTimeout(resolve,20)); + } + assert.ok(pid, 'test probe started'); + await f.dispose(); + await pending; + assert.throws(() => process.kill(Number(pid),0), /ESRCH/); + assert.equal((await readdir(f.dataDir)).length, 0); + } finally { + await f.dispose(); + await rm(f.scratch,{recursive:true,force:true}); + } +}); diff --git a/plugins/cristinamadureira353-svg/ai-office/tests/web.test.mjs b/plugins/cristinamadureira353-svg/ai-office/tests/web.test.mjs new file mode 100644 index 00000000..cb331be5 --- /dev/null +++ b/plugins/cristinamadureira353-svg/ai-office/tests/web.test.mjs @@ -0,0 +1,294 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { fixture } from './fixture.mjs'; + +test('rest area seating aligns with cushions and shifted workers remain clickable', {timeout:90000}, async () => { + const f=await fixture({resting:true}); + const {chromium}=createRequire(join(process.env.AI_OFFICE_TEST_NODE_MODULES,'package.json'))('playwright'); + const evidence=resolve(process.env.AI_OFFICE_TEST_EVIDENCE); + await mkdir(evidence,{recursive:true}); + let browser; + const errors=[],commands=[]; + try { + assert.equal((await(await f.post('/api/settings',f.settings)).json()).ok,true); + browser=await chromium.launch({headless:true,executablePath:process.env.AI_OFFICE_TEST_CHROMIUM,args:['--use-angle=swiftshader','--enable-unsafe-swiftshader']}); + const page=await browser.newPage({viewport:{width:1600,height:1000},deviceScaleFactor:2}); + page.on('pageerror',error=>errors.push(error.message)); + page.on('console',message=>{if(message.type()==='error') errors.push(message.text());}); + page.on('request',request=>{ + if(request.url()===f.base+'/api/command' && request.method()==='POST') commands.push(JSON.parse(request.postData())); + }); + await page.goto(f.base+'/ai-office'); + const canvas=page.frameLocator('#game').locator('#canvas'); + await canvas.waitFor({state:'visible'}); + await page.waitForFunction(()=>document.querySelector('#game').contentWindow.AIOfficeStream && !document.querySelector('#game').contentWindow.document.querySelector('#status')); + const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms)); + // 后端位置是固定的隔离夹具;前端仍实际寻路,从工位走到休息区。 + await pause(14000); + const scale=Math.min(1600/1120,1000/720),width=1600/scale,height=1000/scale; + const usable={w:Math.max(400,width-424),h:Math.max(300,height-282)}; + const zoom=Math.min(usable.w/1280,usable.h/768); + const origin={x:24+usable.w/2-640*zoom,y:94+usable.h/2-384*zoom}; + const stage=process.env.AI_OFFICE_SEATING_STAGE || 'after'; + await page.screenshot({path:join(evidence,'rest-seating-'+stage+'-overview.png')}); + await page.screenshot({path:join(evidence,'rest-seating-'+stage+'-detail.png'),clip:{ + x:(origin.x+14*32*zoom)*scale,y:(origin.y+17*32*zoom)*scale, + width:14*32*zoom*scale,height:6.4*32*zoom*scale}}); + for(const [who,x,y] of [['flash',18,20.8],['longcat',19.7,20.8],['step5',22.6,21.2]]) { + await canvas.click({position:{x:(origin.x+x*32*zoom)*scale,y:(origin.y+(y*32-24)*zoom)*scale}}); + await pause(200); + if(who==='flash') await page.screenshot({path:join(evidence,'rest-seating-'+stage+'-selected.png')}); + await canvas.click({position:{x:(width-292)*scale,y:396*scale}}); + for(let i=0;i<20 && !commands.some(c=>c.who===who);i++) await pause(100); + assert.ok(commands.some(c=>c.who===who && c.action==='goto' && c.arg==='lounge'),who+' can be selected at its rendered torso'); + await page.keyboard.press('Escape'); + await pause(200); + } + assert.equal((await readdir(f.dataDir)).includes('network-attempt'),false); + assert.equal((await readdir(f.dataDir)).includes('calls.jsonl'),false,'movement and seating never invoke a model'); + assert.deepEqual(errors,[]); + await writeFile(join(evidence,'rest-seating-web.json'),JSON.stringify({commands,errors,workers:(await f.get('/api/state')).workers.map(w=>({id:w.id,x:w.x,y:w.y,acting:w.acting}))},null,2)); + } finally { + await writeFile(join(evidence,'rest-seating-'+(process.env.AI_OFFICE_SEATING_STAGE || 'after')+'-console.json'),JSON.stringify({errors,commands},null,2)); + await browser?.close(); + await f.dispose(); + await rm(f.scratch,{recursive:true,force:true}); + } +}); + +test('exported Godot Web: readable UI, employee clicks with a client open, and five chats', {timeout:90000}, async () => { + const f = await fixture({client:true}); + const dependencies = process.env.AI_OFFICE_TEST_NODE_MODULES; + const {chromium} = createRequire(join(dependencies, 'package.json'))('playwright'); + const evidence = resolve(process.env.AI_OFFICE_TEST_EVIDENCE); + await mkdir(evidence, {recursive:true}); + let browser; + const errors = [], logs = []; + const commands = []; + try { + browser = await chromium.launch({headless:true, + executablePath:process.env.AI_OFFICE_TEST_CHROMIUM, + args:['--use-angle=swiftshader', '--enable-unsafe-swiftshader']}); + const page = await browser.newPage({viewport:{width:854,height:568}}); + await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], {origin:f.base}); + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { logs.push(message.text()); if (message.type() === 'error') errors.push(message.text()); }); + page.on('request', request => { + if (request.url() === f.base + '/api/command' && request.method() === 'POST') + commands.push(JSON.parse(request.postData())); + }); + await page.goto(f.base + '/ai-office'); + await page.locator('#setup').waitFor({state:'visible'}); + await page.waitForFunction(() => !document.querySelector('#copy').disabled); + assert.equal(await page.locator('#setup input').count(), 0, 'users do not fill technical paths'); + const prompt = await page.locator('#prompt').inputValue(); + assert.match(prompt,/首次启动向导/); + assert.match(prompt,/没有 Key/); + assert.match(prompt,/单独引导 Jev/); + assert.equal(await page.getByText('Jev · OpenRouter(可选)', {exact:true}).count(), 1); + assert.match(await page.locator('#setup small').textContent(), /Jev 使用 OpenRouter Key/); + assert.doesNotMatch(prompt.split('本次运行信息(由 MiniApp 自动提供')[0],/ai-model-party|\/(?:Users|home)\/[^/\s]+|[A-Z]:\\Users\\|当前工作区/); + assert.ok(prompt.includes(f.base + '/api/settings'), 'copyable prompt targets this exact MiniApp'); + for (const key of ['MINIMAX_API_KEY', 'LONGCAT_API_KEY', 'KIMI_API_KEY', 'STEPFUN_API_KEY', 'OPENROUTER_API_KEY']) + assert.ok(prompt.includes(key)); + await page.screenshot({path:join(evidence, 'miniapp-ai-setup-desktop.png')}); + await page.getByRole('button', {name:'复制引导提示词'}).click(); + assert.equal(await page.evaluate(() => navigator.clipboard.readText()), prompt, 'copy button copies the full prompt'); + await page.evaluate(() => { + navigator.clipboard.writeText = async () => { throw new Error('simulated Host clipboard restriction'); }; + document.execCommand = () => false; + }); + await page.getByRole('button', {name:'复制引导提示词'}).click(); + assert.match(await page.locator('#message').textContent(), /提示词已选中/); + assert.equal(await page.locator('#prompt').evaluate(el => el.selectionEnd - el.selectionStart), prompt.length); + await page.getByRole('button', {name:'检查配置'}).click(); + await page.waitForFunction(() => document.querySelector('#status').textContent.includes('配置还未保存')); + await page.setViewportSize({width:390,height:700}); + const bounds = await page.locator('#setup').evaluate(root => ({ + viewport:document.documentElement.clientWidth, + width:document.documentElement.scrollWidth, + children:[...root.querySelectorAll('textarea,button,.keys')].map(el => { + const rect=el.getBoundingClientRect();return {left:rect.left,right:rect.right}; + }) + })); + assert.ok(bounds.width <= bounds.viewport, 'narrow panel has no horizontal overflow'); + assert.ok(bounds.children.every(rect => rect.left >= 0 && rect.right <= bounds.viewport)); + await page.screenshot({path:join(evidence, 'miniapp-ai-setup-narrow.png'),fullPage:true}); + await writeFile(join(evidence, 'miniapp-ai-setup-bounds.json'), JSON.stringify(bounds,null,2)); + await page.setViewportSize({width:1102,height:957}); + await writeFile(join(f.dataDir, 'quota-restored'), 'initially available'); + assert.equal((await (await f.post('/api/settings', f.settings)).json()).ok, true, 'AI completes setup using the local API'); + await page.locator('#game').waitFor({state:'visible',timeout:6000}); + assert.equal(await page.locator('#setup').isVisible(), false, 'saved config automatically enters the game'); + const frame = page.frameLocator('#game'); + await frame.locator('#canvas').waitFor({state:'visible'}); + await page.waitForFunction(() => { + const frame = document.querySelector('#game').contentWindow; + return frame.AIOfficeStream && !frame.document.querySelector('#status'); + }, {timeout:30000}); + for (let i=0; i<40 && !logs.some(line => line.includes('[MAIN] 后端 已连接')); i++) + await new Promise(resolve => setTimeout(resolve, 100)); + assert.equal(logs.some(line => line.includes('[MAIN] 后端 已连接')), true, 'real Godot Web must poll this MiniApp backend'); + const canvas = frame.locator('#canvas'); + const pause = ms => new Promise(resolve => setTimeout(resolve, ms)); + const geometry = () => { + const {width,height} = page.viewportSize(); + const scale = Math.min(width / 1120, height / 720); + return {width:width/scale,height:height/scale,scale}; + }; + const click = async (x,y) => { + const {scale} = geometry(); + await canvas.click({position:{x:x*scale,y:y*scale}}); + await pause(180); + }; + const card = index => { + const {width,height} = geometry(); + const w = (width - 128) / 5; + return {x:44 + index * (w+10),y:height-120,w}; + }; + const chatFrame = () => { + const {width,height} = geometry(); + const w=Math.min(620,width-48),h=Math.min(560,height-96); + return {x:(width-w)/2,y:(height-h)/2,w,h}; + }; + const calls = async () => (await readFile(join(f.dataDir,'calls.jsonl'),'utf8').catch(() => '')).trim().split('\n').filter(Boolean).map(line => JSON.parse(line)); + const waitFor = async predicate => { + for(let i=0;i<40;i++) { if(await predicate()) return; await pause(100); } + throw new Error('Expected UI action did not reach the isolated backend'); + }; + await pause(400); + await page.screenshot({path:join(evidence, 'miniapp-web-healthy.png')}); + await click(geometry().width-184,160); + await page.screenshot({path:join(evidence, 'miniapp-web-client.png')}); + const beforeDisabled = commands.length; + await click(geometry().width-320,418); + assert.equal(commands.length,beforeDisabled,'busy employee button must explain its state without dispatching'); + await page.screenshot({path:join(evidence, 'miniapp-web-disabled-feedback.png')}); + // 在客户详情可见的情况下逐个选中员工,并验证指令、直接聊天、自动聚焦与发送。 + for (const [i,who] of ['m3','flash','longcat','kimi','step5'].entries()) { + await click(geometry().width-184,160); + let row=card(i); + await click(row.x+22,row.y+36); + if(i===0) await page.screenshot({path:join(evidence, 'miniapp-web-selected.png')}); + await click(geometry().width-292,396); + await waitFor(() => commands.some(command => command.who===who && command.action==='goto' && command.arg==='lounge')); + await page.keyboard.press('Escape'); + await pause(150); + await click(geometry().width-184,160); + row=card(i); + await click(row.x+row.w-37,row.y+23); + if(i===0) await page.screenshot({path:join(evidence, 'miniapp-web-before-input.png')}); + const message='hello '+who; + await page.keyboard.type(message,{delay:40}); + if(i===0) { + await page.screenshot({path:join(evidence,'miniapp-web-draft.png')}); + await writeFile(join(evidence,'miniapp-web-canvas.json'),JSON.stringify(await page.evaluate(() => { + const frame=document.querySelector('#game').contentWindow; + const canvas=frame.document.querySelector('#canvas'); + return {canvas:canvas.getBoundingClientRect().toJSON(),width:canvas.width,height:canvas.height,active:frame.document.activeElement?.tagName, + editable:[...frame.document.querySelectorAll('[contenteditable]')].map(el=>({rect:el.getBoundingClientRect().toJSON(),pointerEvents:el.style.pointerEvents}))}; + }),null,2)); + } + if(i===1) await page.keyboard.press('Enter'); + else { const panel=chatFrame(); await click(panel.x+panel.w-74,panel.y+panel.h-88); } + if(i===0) await page.screenshot({path:join(evidence,'miniapp-web-after-send.png')}); + await waitFor(async () => (await calls()).some(call => call.who===who && call.text.includes(message))); + await pause(300); + await page.screenshot({path:join(evidence, 'miniapp-web-chat-'+who+'.png')}); + const panel=chatFrame(); + await click(panel.x+panel.w-74,panel.y+37); + } + assert.equal((await calls()).filter(call => call.text.includes('hello ')).length,5,'each employee gets exactly one UI chat'); + await rm(join(f.dataDir,'quota-restored')); + await (await f.post('/api/chat', {who:'kimi', text:'模拟额度耗尽'})).text(); + await pause(650); + const downCard=card(3); + const callsBefore = (await calls()).length; + await click(downCard.x+downCard.w-37,downCard.y+23); + assert.equal((await calls()).length,callsBefore,'down employee chat entry must not call the model'); + await page.screenshot({path:join(evidence, 'miniapp-web-brain-down.png')}); + assert.equal((await f.get('/api/state')).workers.find(w => w.id === 'kimi').brain_down, true); + await click(geometry().width-206,348); + await waitFor(async () => (await calls()).length===callsBefore+1); + assert.equal((await f.get('/api/state')).workers.find(w => w.id === 'kimi').brain_down, true); + assert.equal((await calls()).length, callsBefore+1,'visible reboot control must perform exactly one probe'); + await page.keyboard.press('Escape'); + const sizes=[]; + for(const viewport of [{width:854,height:568},{width:1280,height:800},{width:1600,height:1000}]) { + await page.setViewportSize(viewport); + await pause(400); + const row=card(4); + await click(row.x+row.w-37,row.y+23); + await page.keyboard.type('resize '+viewport.width,{delay:40}); + await pause(120); + await page.keyboard.press('Enter'); + await waitFor(async () => (await calls()).some(call => call.who==='step5' && call.text.includes('resize '+viewport.width))); + await pause(200); + await page.screenshot({path:join(evidence, 'miniapp-web-'+viewport.width+'.png')}); + const panel=chatFrame(); + await click(panel.x+panel.w-74,panel.y+37); + const g=geometry(); + sizes.push({...viewport,body_pixels:16*g.scale,secondary_pixels:14*g.scale}); + } + await writeFile(join(evidence,'miniapp-web-actions.json'),JSON.stringify({commands,calls:await calls(),sizes},null,2)); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'), false); + await page.reload(); + await page.locator('#game').waitFor({state:'visible'}); + assert.equal(await page.locator('#setup').isVisible(), false, 'future opens skip first setup'); + assert.deepEqual(errors, []); + } finally { + await writeFile(join(evidence, 'miniapp-web-console.json'), JSON.stringify({logs, errors}, null, 2)); + await browser?.close(); + await f.dispose(); + await rm(f.scratch, {recursive:true,force:true}); + } +}); + +test('HiDPI canvas keeps readable labels and direct chat input', {timeout:45000}, async () => { + const f=await fixture(); + const {chromium}=createRequire(join(process.env.AI_OFFICE_TEST_NODE_MODULES,'package.json'))('playwright'); + const evidence=resolve(process.env.AI_OFFICE_TEST_EVIDENCE); + let browser; + try { + assert.equal((await (await f.post('/api/settings',f.settings)).json()).ok,true); + browser=await chromium.launch({headless:true,executablePath:process.env.AI_OFFICE_TEST_CHROMIUM,args:['--use-angle=swiftshader','--enable-unsafe-swiftshader']}); + const page=await browser.newPage({viewport:{width:1102,height:957},deviceScaleFactor:2}); + const errors=[]; + page.on('pageerror',error=>errors.push(error.message)); + page.on('console',message=>{if(message.type()==='error') errors.push(message.text());}); + await page.goto(f.base+'/ai-office'); + await page.frameLocator('#game').locator('#canvas').waitFor({state:'visible'}); + await page.waitForFunction(()=>document.querySelector('#game').contentWindow.AIOfficeStream && !document.querySelector('#game').contentWindow.document.querySelector('#status')); + await new Promise(resolve=>setTimeout(resolve,900)); + await page.screenshot({path:join(evidence,'miniapp-web-retina-overview.png')}); + const canvas=page.frameLocator('#game').locator('#canvas'); + await canvas.click({position:{x:1023,y:862}}); + await new Promise(resolve=>setTimeout(resolve,200)); + await page.keyboard.type('retina',{delay:40}); + await new Promise(resolve=>setTimeout(resolve,120)); + await page.keyboard.press('Enter'); + let calls=''; + for(let i=0;i<40;i++) { + calls=await readFile(join(f.dataDir,'calls.jsonl'),'utf8').catch(()=> ''); + if(calls.includes('retina')) break; + await new Promise(resolve=>setTimeout(resolve,100)); + } + assert.match(calls,/retina/); + assert.equal(JSON.parse(calls.trim().split('\n').at(-1)).who,'step5'); + await new Promise(resolve=>setTimeout(resolve,250)); + await page.screenshot({path:join(evidence,'miniapp-web-retina-chat.png')}); + const metrics=await canvas.evaluate(el=>({width:el.width,height:el.height,cssWidth:el.clientWidth,cssHeight:el.clientHeight,dpr:window.devicePixelRatio})); + assert.equal(metrics.dpr,2); + assert.equal(metrics.width,metrics.cssWidth*2); + await writeFile(join(evidence,'miniapp-web-retina.json'),JSON.stringify({metrics,errors},null,2)); + assert.deepEqual(errors,[]); + assert.equal((await readdir(f.dataDir)).includes('network-attempt'),false); + } finally { + await browser?.close(); + await f.dispose(); + await rm(f.scratch,{recursive:true,force:true}); + } +});