diff --git a/CHANGELOG b/CHANGELOG index 7277c8971..099542da3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,11 @@ SINGE 3.00 API Changes ----------- +- On Windows, Singe started with its output already going somewhere -- an + IDE, a launcher that keeps a log, Singe.exe > log.txt -- leaves it there + instead of opening a console window of its own that took it away, and + only waits for a key before closing a console it opened itself. + - A game restyles its subtitles, its banners, and the score panel with its own copies of subtitle.rml and scoreBezel.rml beside its script, each linking a style sheet of its own; Singe draws from the game's copy when @@ -254,8 +259,8 @@ API Changes the command line keep their values. Every game in a library therefore behaves the same whether it was started from the menu or from a shell. -- The name now expands to "SINGE Is Not a Game Emulator", in the banner, - the manual, INSTALL, and the Windows file description. +- The name now expands to "Scott's Increasingly Nifty Game Engine", in the + banner, the menu, the manual, INSTALL, and the Windows file description. - Framework.singe applies the legacy sprite argument order once even when a game loads it more than once. @@ -996,7 +1001,10 @@ API Changes game-music-emu (NSF, NSFE, SPC, VGM, GBS, AY, GYM, HES, KSS and SAP), and the tracker formats libxmp was already reading -- MOD, S3M, XM, IT and fifty-odd - others -- are now documented rather than merely present. + others -- are now documented rather than merely present. A chiptune + that does not say how long it is plays until it falls silent, which + can be never, so it is decoded as it plays rather than all at once. + The same goes for anything that would decode to more than 512 MB. - MIDI files play, synthesized with TinySoundFont. Singe ships no sound bank, because a good one is tens of megabytes for a format @@ -1160,6 +1168,37 @@ API Changes New Features ------------ +- Windows on ARM: a native ARM64 build, Singe-v3.00-Windows-aarch64.exe, + for Windows PCs with ARM processors. The x86_64 build still runs on them, + emulated by Windows 11, but more slowly; Menu.bat starts the native one + when both are there. + +- --debug connects a game to ZeroBrane Studio's debugger, with nothing added + to the game: MobDebug is built into Singe now, and the game connects as + it starts, again after every --reload, and for every game the menu + launches. Breakpoints, stepping, and the Stack and Watch windows all + work, inside singeMain too. While the IDE has the game stopped, the disc, + the videos, and the sounds hold still, as they do for the pause key. An + error that is about to end the game stops in the IDE first, in + Singe/Debugger.singe, with the script's own frames and their variables + still there to look at; continue and the game ends as before, with a + traceback whose lines the IDE opens. --debug=HOST:PORT reaches an IDE on + another machine, and a cabinet can say debug = "HOST:PORT" in its + settings file. With nothing listening, the game plays as usual. + +- The ZeroBrane Studio package is new. Singe unpacks it into + Singe/ZeroBrane/, and one line in ZeroBrane Studio's user.lua loads it + from there: Singe becomes an interpreter, so F6 runs a game and F5 + debugs it, with no dialog to fill in, no video to name (the game's + games.dat entry supplies it, so a game without a disc runs too), and + Singe found beside the game or asked for once. Autocompletion and + tooltips cover the whole API, in English, French, Italian, German, or + Spanish to match ZeroBrane Studio's language, a right click looks a name + up in the Reference, and Analyze knows the API, so it flags a misspelled + call rather than every call. The toolbar button is the dragon. .singe + files open as Lua, .rml as XML, and .rcss as CSS. It replaces zbstudio/packages/singetoolbar.lua; remove + that one from ZeroBrane Studio's packages folder if you installed it. + - A microphone, for singing games and anything that listens. micOpen starts listening to the machine's default microphone or one named by micList; micPitch says which note is being sung, as its frequency and how clearly it @@ -1342,7 +1381,8 @@ New Features returned after three seconds, Singe stops it and shuts down as usual, your onShutdown included. The log shows where the script was when it was stopped. A script that returns promptly never notices any of this, and one - catching errors with pcall can't hold the stop off. + catching errors with pcall can't hold the stop off. A script under a + debugger is the debugger's: the watchdog stays out of its way. - --program traces where each frame's time went, every five seconds: per loop, the disc, the script, the scene, the GUIs, the overlay upload and the diff --git a/CMakeLists.txt b/CMakeLists.txt index a782585a9..d42f52adf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # @@ -224,6 +224,7 @@ add_custom_command( # Support files extracted at first run. singeEmbed(${CMAKE_SOURCE_DIR}/assets/Framework.singe ${GENERATED_DIR}/Framework_singe.h "") +singeEmbed(${CMAKE_SOURCE_DIR}/assets/Debugger.singe ${GENERATED_DIR}/Debugger_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "") @@ -296,6 +297,25 @@ foreach(language ${SINGE_DOC_LANGUAGES}) singeEmbed(${MANUAL_DIR}/${language}/Manual.pdf ${GENERATED_DIR}/${language}_Manual_pdf.h "${language}_") endforeach() +# The ZeroBrane Studio package and its API, one per documentation language (util/zbstudioApi.py). +singeEmbed(${CMAKE_SOURCE_DIR}/zbstudio/packages/singe.lua ${GENERATED_DIR}/zbstudio_singe_lua.h "zbstudio_") +singeEmbed(${CMAKE_SOURCE_DIR}/zbstudio/packages/singe/api.lua ${GENERATED_DIR}/zbstudio_api_lua.h "zbstudio_") +foreach(language ${SINGE_DOC_LANGUAGES}) + singeEmbed(${CMAKE_SOURCE_DIR}/zbstudio/packages/singe/api-${language}.lua ${GENERATED_DIR}/zbstudio_api_${language}_lua.h "zbstudio_") +endforeach() +# Its toolbar button is the dragon, at the sizes ZeroBrane Studio's toolbar comes in, cut from the +# same square render as the window's icon. +foreach(size 16 24 32 48) + add_custom_command( + OUTPUT ${GENERATED_DIR}/icon-${size}.png + COMMAND ${IMAGEMAGICK} ${GENERATED_DIR}/iconSquare.png -resize ${size}x${size} ${GENERATED_DIR}/icon-${size}.png + DEPENDS ${GENERATED_DIR}/iconSquare.png + COMMENT "Rendering icon-${size}.png" + VERBATIM + ) + singeEmbed(${GENERATED_DIR}/icon-${size}.png ${GENERATED_DIR}/zbstudio_icon_${size}_png.h "zbstudio_") +endforeach() + # The license Singe is under and the notices its third-party libraries and assets require. Both # have to reach whoever gets the binary, so they travel inside it like the manual does. singeEmbed(${CMAKE_SOURCE_DIR}/COPYING ${GENERATED_DIR}/COPYING.h "") @@ -331,6 +351,7 @@ singeEmbedLua(thirdparty/middleclass/middleclass.lua "") singeEmbedLua(thirdparty/lume/lume.lua "") singeEmbedLua(thirdparty/inspect.lua/inspect.lua "") singeEmbedLua(thirdparty/bump.lua/bump.lua "") +singeEmbedLua(thirdparty/MobDebug/src/mobdebug.lua "") # Optional HTML for browsing: cmake --build . --target docs. The user manual is the one book the # binary carries; the reference and the beginner's course are the optional @@ -363,6 +384,8 @@ set(SINGE_SOURCE src/collide.c src/collide.h src/common.h + src/debugger.c + src/debugger.h src/decode.c src/decode.h src/embedded.h diff --git a/CMakePresets.json b/CMakePresets.json index 26f8360d7..701369992 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -53,6 +53,18 @@ "SINGE_SUFFIX": ".exe" } }, + { + "name": "windows-aarch64", + "displayName": "Windows on ARM64 (zig)", + "inherits": "base", + "binaryDir": "${sourceDir}/.builddir/windows/aarch64/superbuild", + "toolchainFile": "${sourceDir}/cmake/zig/aarch64-windows-gnu.cmake", + "cacheVariables": { + "SINGE_TRIPLE": "aarch64-w64-mingw32", + "SINGE_CROSS_OS": "mingw32", + "SINGE_SUFFIX": ".exe" + } + }, { "name": "linux-aarch64", "displayName": "ARM64 Linux (zig)", @@ -103,6 +115,10 @@ "name": "windows-x86_64", "configurePreset": "windows-x86_64" }, + { + "name": "windows-aarch64", + "configurePreset": "windows-aarch64" + }, { "name": "linux-aarch64", "configurePreset": "linux-aarch64" diff --git a/INSTALL b/INSTALL index 090db7e50..c200bdf8a 100644 --- a/INSTALL +++ b/INSTALL @@ -5,7 +5,7 @@ SINGE 3.00 The full manual is extracted to Singe/Manual.pdf on first run, beside Singe/COPYING and Singe/LICENSES.) -Welcome to Singe! SINGE Is Not a Game Emulator! +Welcome to Singe! Scott's Increasingly Nifty Game Engine! INSTALLATION & UPGRADE @@ -85,6 +85,12 @@ VA-API, VDPAU, DRM); for Windows zig carries the headers and import libraries itself, so nothing else is installed: ./build-all.sh windows x86_64 + ./build-all.sh windows aarch64 Windows on ARM64 + +The ARM64 Windows build needs llvm-nm (Debian's llvm package) for FFmpeg's +configure, as the macOS build does, since binutils cannot read ARM64 Windows +objects. OpenSSL has no MinGW target for ARM64 of its own; +cmake/opensslMingwArm64.conf gives its Configure one. A developer build with the host's own gcc, glibc and symbols is the linux-x86_64-gcc preset (binary Singe-v3.00-Linux-x86_64-gcc). @@ -141,5 +147,5 @@ Without the wrapper: cmake --preset linux-x86_64 && cmake --build --preset linux-x86_64 -Presets: linux-x86_64, linux-x86_64-gcc, windows-x86_64, linux-aarch64, -macos-aarch64 (CMakePresets.json). +Presets: linux-x86_64, linux-x86_64-gcc, windows-x86_64, windows-aarch64, +linux-aarch64, macos-aarch64, macos-x86_64 (CMakePresets.json). diff --git a/LICENSES b/LICENSES index a0a28edde..0dd381a2a 100644 --- a/LICENSES +++ b/LICENSES @@ -39,6 +39,7 @@ luasocket 3.1.0 MIT https://lunarmodules.github.io lume 2.3.0 MIT https://github.com/rxi/lume manymouse 0.0.3 Zlib https://icculus.org/manymouse middleclass 4.1.1 MIT https://github.com/kikito/middleclass +MobDebug 0.805 MIT https://github.com/pkulchenko/MobDebug openssl 3.5.8 Apache-2.0 https://www.openssl.org opus 1.4 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer; also Singe's voice codec) opusfile 0.12 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer) diff --git a/assets/Backdrop.singe b/assets/Backdrop.singe index 7c88a81f7..e526fed9f 100644 --- a/assets/Backdrop.singe +++ b/assets/Backdrop.singe @@ -145,7 +145,7 @@ local STRIKE_ANGLE = -14.0 -- and forward through the strike. local DRAGON_YAW = -148.0 -- The dragon faces left, turned toward the camera so the wingbeat is -- seen three quarters on rather than edge on; the name stays square -- to the camera because it has to be read. -local TAGLINE = "SINGE Is Not a Game Emulator" +local TAGLINE = "Scott's Increasingly Nifty Game Engine" local backdropStarted = 0 -- singeGetTicks() when the backdrop's clock began. diff --git a/assets/Debugger.singe b/assets/Debugger.singe new file mode 100644 index 000000000..0d9c7db06 --- /dev/null +++ b/assets/Debugger.singe @@ -0,0 +1,36 @@ +--[[ + * + * Singe 3 + * Copyright (C) 2006-2026 Scott Duensing + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * +--]] + + +-- Singe stopped here because an error is about to end your game. This only happens under --debug, +-- with a debugger such as ZeroBrane Studio attached. +-- +-- What went wrong is in "message": hover over it, or look in the Stack window. Where it went wrong +-- is in the Stack window too, a frame or two below this one -- pick your own function there to see +-- its local variables as they were. Continue, and the game ends just as it would have. + + +local message = ... + +require("mobdebug").pause() +return message diff --git a/assets/menuBackground.mkv b/assets/menuBackground.mkv index fdb0cb929..82ff685c1 100644 --- a/assets/menuBackground.mkv +++ b/assets/menuBackground.mkv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05ca8170de08670f4c8d0a5d2e05b861b0f024efa9b3022df6545ff7ea0f525d -size 938199 +oid sha256:fb86e91fa21e388ff5a655821567fb29f730de74a25312e12bff023d8b6938da +size 953078 diff --git a/assets/settings.cfg b/assets/settings.cfg index cf8300b29..a7e0b834d 100644 --- a/assets/settings.cfg +++ b/assets/settings.cfg @@ -122,6 +122,9 @@ -- Working on a game ------------------------------------------------------ -- reload = false -- rerun the game when one of its loose script files changes +-- debug = false -- connect every game to ZeroBrane Studio's debugger; true for this +-- machine's, or "HOST:PORT" for one elsewhere, as a cabinet would. +-- With nothing listening, each start waits up to two seconds -- program = false -- trace engine activity to trace.txt -- perfsync = false -- with program, show the GPU's time in each stage of the perf lines; slows everything -- trace = false -- trace every Lua call to trace.txt diff --git a/build-all.sh b/build-all.sh index 15ff6a0a8..34e563f5f 100755 --- a/build-all.sh +++ b/build-all.sh @@ -5,7 +5,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # @@ -227,4 +227,5 @@ else buildAll macos universal 2>&1 | tee ${G_BUILDDIR}/macos-universal.log buildAll linux aarch64 2>&1 | tee ${G_BUILDDIR}/linux-aarch64.log buildAll windows x86_64 2>&1 | tee ${G_BUILDDIR}/windows-x86_64.log + buildAll windows aarch64 2>&1 | tee ${G_BUILDDIR}/windows-aarch64.log fi diff --git a/build-docs.sh b/build-docs.sh index 9714a04f5..4f488c23d 100755 --- a/build-docs.sh +++ b/build-docs.sh @@ -7,14 +7,16 @@ # books sit together, so the links between them work), and then packs the two books that do not # ship in the binary, with the lesson files and the license, into .builddir/Singe-Docs-v.zip # and .builddir/Singe-Docs-v-.zip, the optional documentation downloads. The -# version comes from CMakeLists.txt. +# version comes from CMakeLists.txt. Last, the ZeroBrane Studio package's API in every language, +# zbstudio/packages/singe/api*.lua, is made again from the Reference (util/zbstudioApi.py). # # ./build-docs.sh the books as they ship # ./build-docs.sh preview the same, with PREVIEW across every page, for proofreaders, all of it # under .builddir/preview so neither a release nor the binary's own # copies of the manual (rendered by CMake into .builddir) are touched; # the downloads are named -preview so they cannot be mistaken for a -# release. +# release, and they carry the Manual too, which a release download +# leaves to the binary. set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) @@ -93,19 +95,37 @@ for language in "${languages[@]}"; do built+=("$dest/$book.html" "$dest/$book.pdf") done - # The documentation download: everything but the user manual, which the binary carries. Built - # in a staging folder so the zip has one top-level directory and no stray build files. + # The documentation download: everything but the user manual, which the binary carries, except + # in a preview, where the Manual goes to the proofreaders with the rest. The README's lines marked + # @PREVIEW@ are kept for a preview and dropped for a release. Built in a staging folder so the zip + # has one top-level directory and no stray build files. stage=$out/$name$suffix rm -rf "$stage" "$stage.zip" mkdir -p "$stage/learn" cp "$dest/Reference.pdf" "$dest/Reference.html" "$dest/Learn.pdf" "$dest/Learn.html" "$stage/" + previewLines=/^@PREVIEW@/d + if [[ -n $preview ]]; then + cp "$dest/Manual.pdf" "$dest/Manual.html" "$stage/" + previewLines=s/^@PREVIEW@// + fi cp -r "$here/docs/learn/." "$stage/learn/" cp "$here/COPYING" "$stage/" - sed "s/@VERSION@/$version/" "$src/downloadReadme.txt" > "$stage/README.txt" + sed -e "s/@VERSION@/$version/" -e "$previewLines" "$src/downloadReadme.txt" > "$stage/README.txt" (cd "$out" && zip -q -r "$name$suffix.zip" "$name$suffix") rm -rf "$stage" built+=("$stage.zip") done +# ZeroBrane Studio's autocompletion and tooltips are made from the Reference, so they are made again +# with it, from the names a Singe built here defines; the next build embeds them. Without that +# Singe or python3 they stay as they are, and this says so. A preview leaves them alone. +if [[ -z $preview ]]; then + if command -v python3 > /dev/null && [[ -x $here/.builddir/Singe-v$version-Linux-x86_64 ]]; then + python3 "$here/util/zbstudioApi.py" + else + echo "note: ZeroBrane Studio API left as it was (needs python3 and .builddir/Singe-v$version-Linux-x86_64)" + fi +fi + echo "built:" ls -la "${built[@]}" diff --git a/cmake/Superbuild.cmake b/cmake/Superbuild.cmake index 3d6c65bc0..f35300855 100644 --- a/cmake/Superbuild.cmake +++ b/cmake/Superbuild.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # @@ -222,8 +222,16 @@ ExternalProject_Add_Step(SDL3_image pngHeaders COMMENT "Installing the libpng headers" ) +# Opus's NEON code on ARM comes with a run time check for NEON, and on Windows that check is written +# for MSVC only; MinGW gets "no CPU detection method available". Opus's CMake cannot presume NEON +# without the check either (it declares the NEON functions only alongside it), so on ARM64 Windows +# Opus is built from its plain C, which voice and .opus music need little of. +set(mixerOpusOptions "") +if((KANGAROO_OS STREQUAL "windows") AND (KANGAROO_ARCH STREQUAL "aarch64")) + set(mixerOpusOptions ";-DOPUS_DISABLE_INTRINSICS=ON") +endif() singeCmakeProject(SDL3_mixer ${SB_THIRDPARTY}/SDL3_mixer "SDL3" - "-DBUILD_SHARED_LIBS=off;-DSDLMIXER_DEPS_SHARED=off;-DSDLMIXER_VENDORED=on;-DSDLMIXER_EXAMPLES=off;-DSDLMIXER_TESTS=off;-DSDLMIXER_FLAC_LIBFLAC=off;-DSDLMIXER_MP3_MPG123=off;-DSDLMIXER_VORBIS_STB=on;-DSDLMIXER_VORBIS_VORBISFILE=off;-DSDLMIXER_VORBIS_TREMOR=off;-DSDLMIXER_GME=on;-DSDLMIXER_GME_SHARED=off;-DGME_BUILD_EXAMPLES=off;-DGME_BUILD_TESTING=off;-DGME_YM2612_EMU=MAME;-DSDLMIXER_MIDI_FLUIDSYNTH=off;${sdl3Dir};-DWAVPACK_ENABLE_ASM=no" + "-DBUILD_SHARED_LIBS=off;-DSDLMIXER_DEPS_SHARED=off;-DSDLMIXER_VENDORED=on;-DSDLMIXER_EXAMPLES=off;-DSDLMIXER_TESTS=off;-DSDLMIXER_FLAC_LIBFLAC=off;-DSDLMIXER_MP3_MPG123=off;-DSDLMIXER_VORBIS_STB=on;-DSDLMIXER_VORBIS_VORBISFILE=off;-DSDLMIXER_VORBIS_TREMOR=off;-DSDLMIXER_GME=on;-DSDLMIXER_GME_SHARED=off;-DGME_BUILD_EXAMPLES=off;-DGME_BUILD_TESTING=off;-DGME_YM2612_EMU=MAME;-DSDLMIXER_MIDI_FLUIDSYNTH=off;${sdl3Dir};-DWAVPACK_ENABLE_ASM=no${mixerOpusOptions}" "${SB_ENV}") singeCmakeProject(SDL3_ttf ${SB_THIRDPARTY}/SDL3_ttf "SDL3;zlib;brotli;SDL3_image" @@ -256,6 +264,9 @@ singeCmakeProject(RmlUi ${SB_THIRDPARTY}/RmlUi "SDL3_ttf" "${rmluiOptions}" "${S if(KANGAROO_OS STREQUAL "windows") if(KANGAROO_ARCH STREQUAL "x86") set(opensslTarget mingw) + elseif(KANGAROO_ARCH STREQUAL "aarch64") + # OpenSSL has no MinGW target for ARM64 Windows; Singe's own configuration file adds one. + set(opensslTarget --config=${CMAKE_SOURCE_DIR}/cmake/opensslMingwArm64.conf mingw-arm64) else() set(opensslTarget mingw64) endif() @@ -410,11 +421,12 @@ if(SINGE_ZIG_TARGET) set(ffmpegTools --enable-cross-compile --cc=${SB_CC} --cxx=${SB_CXX} --ar=${CMAKE_AR} --ranlib=${SB_RANLIB} --windres=${CMAKE_RC_COMPILER}) # FFmpeg only uses nm to learn the symbol prefix. The host's binutils nm reads x86 ELF and PE # objects but not aarch64 or Mach-O ones; the prefix is empty on Linux either way, and on macOS - # (where it is an underscore) llvm-nm from Debian's llvm package reads Mach-O. - if(KANGAROO_OS STREQUAL "macos") + # (where it is an underscore) and on ARM64 Windows llvm-nm from Debian's llvm package reads the + # objects. + if((KANGAROO_OS STREQUAL "macos") OR ((KANGAROO_OS STREQUAL "windows") AND (KANGAROO_ARCH STREQUAL "aarch64"))) find_program(SINGE_LLVM_NM NAMES llvm-nm llvm-nm-21 llvm-nm-20 llvm-nm-19 llvm-nm-18 llvm-nm-17 llvm-nm-16 llvm-nm-15) if(NOT SINGE_LLVM_NM) - message(FATAL_ERROR "llvm-nm is needed for the macOS FFmpeg build (apt-get install llvm).") + message(FATAL_ERROR "llvm-nm is needed for the ${KANGAROO_OS} ${KANGAROO_ARCH} FFmpeg build (apt-get install llvm).") endif() list(APPEND ffmpegTools --nm=${SINGE_LLVM_NM}) elseif(KANGAROO_ARCH STREQUAL "x86_64") diff --git a/cmake/embed.cmake b/cmake/embed.cmake index 10f2c64c0..b56f68e5b 100644 --- a/cmake/embed.cmake +++ b/cmake/embed.cmake @@ -4,7 +4,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/linux-x86_64.cmake b/cmake/linux-x86_64.cmake index c4396dbfe..cd27044ef 100644 --- a/cmake/linux-x86_64.cmake +++ b/cmake/linux-x86_64.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/opensslMingwArm64.conf b/cmake/opensslMingwArm64.conf new file mode 100644 index 000000000..96d6d3fc1 --- /dev/null +++ b/cmake/opensslMingwArm64.conf @@ -0,0 +1,34 @@ +# +# Singe 3 +# Copyright (C) 2006-2026 Scott Duensing +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +# + +# OpenSSL's Configure, given this with --config, knows MinGW on 64-bit ARM Windows. OpenSSL has no +# such target of its own: its "mingw64" is x86_64 and passes -m64, which an ARM64 compiler refuses. +# This is that target without the flag, with the integer layout of OpenSSL's own ARM64 Windows target +# (VC-WIN64-ARM). The Singe build turns the assembly off for every zig target, so the C paths serve. + +my %targets = ( + "mingw-arm64" => { + inherit_from => [ "mingw-common" ], + sys_id => "MINGW64", + bn_ops => add("SIXTY_FOUR_BIT RC4_CHAR"), + uplink_arch => undef, + multilib => "64", + }, +); diff --git a/cmake/runToFile.cmake b/cmake/runToFile.cmake index 810a7d18f..f83557249 100644 --- a/cmake/runToFile.cmake +++ b/cmake/runToFile.cmake @@ -4,7 +4,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/aarch64-linux-gnu.cmake b/cmake/zig/aarch64-linux-gnu.cmake index e9e789fca..4fee6b1bb 100644 --- a/cmake/zig/aarch64-linux-gnu.cmake +++ b/cmake/zig/aarch64-linux-gnu.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/aarch64-macos.cmake b/cmake/zig/aarch64-macos.cmake index 66b5f55d0..df28e16f7 100644 --- a/cmake/zig/aarch64-macos.cmake +++ b/cmake/zig/aarch64-macos.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/aarch64-windows-gnu.cmake b/cmake/zig/aarch64-windows-gnu.cmake new file mode 100644 index 000000000..51d0e8015 --- /dev/null +++ b/cmake/zig/aarch64-windows-gnu.cmake @@ -0,0 +1,34 @@ +# +# Singe 3 +# Copyright (C) 2006-2026 Scott Duensing +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +# + +# Windows on 64-bit ARM with zig as the cross compiler: zig ships the mingw-w64 headers and import +# libraries, so nothing else has to be installed. + +include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake) + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR aarch64) +singeZigToolchain(aarch64-windows-gnu "") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(KANGAROO_OS windows) +set(KANGAROO_ARCH aarch64) diff --git a/cmake/zig/macos.cmake b/cmake/zig/macos.cmake index 88689d7e4..8d7f42575 100644 --- a/cmake/zig/macos.cmake +++ b/cmake/zig/macos.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/macosUniversal.cmake b/cmake/zig/macosUniversal.cmake index eafaec429..6caa09dd4 100644 --- a/cmake/zig/macosUniversal.cmake +++ b/cmake/zig/macosUniversal.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # @@ -21,12 +21,23 @@ # Run as: cmake -P cmake/zig/macosUniversal.cmake (after both macOS presets have built) get_filename_component(buildDir ${CMAKE_CURRENT_LIST_DIR}/../../.builddir ABSOLUTE) -file(GLOB armBinary ${buildDir}/Singe-v*-Macos-aarch64) -file(GLOB intelBinary ${buildDir}/Singe-v*-Macos-x86_64) -if(NOT armBinary OR NOT intelBinary) +# The binaries by name, from the project() line as CMakeLists.txt makes SINGE_VERSION_FULL (patch 1 +# is a beta and tweak says which), so one an older version left in .builddir is never joined. +file(STRINGS ${CMAKE_CURRENT_LIST_DIR}/../../CMakeLists.txt projectLine REGEX "^project\\(singe2 VERSION ") +if(NOT projectLine MATCHES "VERSION ([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)") + message(FATAL_ERROR "CMakeLists.txt has no four-number project() version.") +endif() +if(CMAKE_MATCH_3 EQUAL 1) + set(version "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}b${CMAKE_MATCH_4}") +else() + set(version "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}") +endif() +set(armBinary ${buildDir}/Singe-v${version}-Macos-aarch64) +set(intelBinary ${buildDir}/Singe-v${version}-Macos-x86_64) +if(NOT EXISTS ${armBinary} OR NOT EXISTS ${intelBinary}) message(FATAL_ERROR "Build the macos-aarch64 and macos-x86_64 presets first.") endif() -string(REPLACE "-aarch64" "-universal" universalBinary ${armBinary}) +set(universalBinary ${buildDir}/Singe-v${version}-Macos-universal) find_program(SINGE_LIPO NAMES llvm-lipo llvm-lipo-21 llvm-lipo-20 llvm-lipo-19 llvm-lipo-18 llvm-lipo-17 llvm-lipo-16 llvm-lipo-15) if(NOT SINGE_LIPO) message(FATAL_ERROR "llvm-lipo is needed to make the universal binary (apt-get install llvm).") diff --git a/cmake/zig/x86_64-linux-gnu.cmake b/cmake/zig/x86_64-linux-gnu.cmake index 3cebfa7c4..685c34e37 100644 --- a/cmake/zig/x86_64-linux-gnu.cmake +++ b/cmake/zig/x86_64-linux-gnu.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/x86_64-macos.cmake b/cmake/zig/x86_64-macos.cmake index c51e43d93..2a340d2f6 100644 --- a/cmake/zig/x86_64-macos.cmake +++ b/cmake/zig/x86_64-macos.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/x86_64-windows-gnu.cmake b/cmake/zig/x86_64-windows-gnu.cmake index e394241b8..24df67a8f 100644 --- a/cmake/zig/x86_64-windows-gnu.cmake +++ b/cmake/zig/x86_64-windows-gnu.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/cmake/zig/zig.cmake b/cmake/zig/zig.cmake index 0685a2d86..ab64cf278 100644 --- a/cmake/zig/zig.cmake +++ b/cmake/zig/zig.cmake @@ -3,7 +3,7 @@ # Copyright (C) 2006-2026 Scott Duensing # # This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public Licens +# 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. # diff --git a/docs/Manual.adoc b/docs/Manual.adoc index f0203b828..c9bbca50c 100644 --- a/docs/Manual.adoc +++ b/docs/Manual.adoc @@ -14,7 +14,7 @@ Scott Duensing == About Singe -Singe (SINGE Is Not a Game Emulator, and named after the dragon in +Singe (Scott's Increasingly Nifty Game Engine, and named after the dragon in Dragon's Lair) is a Lua-based scripting system that allows for rapid prototyping of new laserdisc games, or the creation of entirely new games. The language is easy to learn, very powerful, and fast. All the features needed to develop your @@ -38,8 +38,8 @@ interesting ones are: * Analog and digital input device support, including light guns * Wide support for video and audio formats -Singe runs on Windows, macOS, and Linux, including the Raspberry Pi and -handhelds like the R36S. +Singe runs on Windows, macOS, and Linux, on ARM processors as well as Intel and +AMD ones: Windows on ARM laptops, the Raspberry Pi, and handhelds like the R36S. For players, Singe allows many unemulated and new games to be enjoyed using any desired front end or via the included menu system. @@ -59,7 +59,8 @@ before it. As of the writing of this manual, the latest release is To install Singe, simply download the appropriate binary for your system. Place it in a new folder by itself and run it. On the first run, Singe will create all the necessary directories and support files needed. You are now ready to -install some games. +install some games. On a Windows PC with an ARM processor, take the `Windows-aarch64` build: the +`Windows-x86_64` one runs there too, emulated, but more slowly. === Upgrading Singe @@ -878,6 +879,7 @@ See <> for the traces. [cols="1,2",options="header"] |=== | Option | Purpose +| `--debug[=HOST:PORT]` | Connect the game to a debugger listening at `HOST:PORT`, normally ZeroBrane Studio, which can then stop it at breakpoints, step through it, and show what is in its variables; see xref:Reference.adoc#zerobrane[Debugging in ZeroBrane Studio]. `HOST` defaults to `localhost` and `PORT` to `8172`. With nothing listening, the game plays as usual once the attempt gives up, within two seconds. Default: off. | `--deterministic[=MS]` | For testing only: ignore real time and run the whole engine on a virtual clock that moves `MS` milliseconds every frame, so the same frame number always means the same moment. The disc follows the virtual clock too, its frames lasting the time they are worth, and the random generators are seeded from the same number, so a run repeats to the pixel. The value is optional and is both the step and the seed, `1` to `1000`. Default when given without one: `15`, the frame time the engine's own rate implies. Pacing is meaningless in this mode, and so is the sound unless `--record` is also given, when it is generated in step with the picture; see xref:Reference.adoc#deterministic[Deterministic Test Mode]. Default: off. | `-P`, `--pack=DIRECTORY` | Pack the game in `DIRECTORY` into the database named after the options, then exit. See xref:Reference.adoc#singlefile[Single-File Games]. | `-T`, `--patch=GAME.game` | Replace files in the packed game from the directory or patch database named after the options, then exit. diff --git a/docs/Reference.adoc b/docs/Reference.adoc index f4feafb34..026ff4aae 100644 --- a/docs/Reference.adoc +++ b/docs/Reference.adoc @@ -76,6 +76,113 @@ empty until the next reload fixes it; the file stays watched, so saving the fix is enough. Because the reload runs the same teardown that quitting does, anything a script leaks shows up here first. +[[zerobrane]] +=== Debugging in ZeroBrane Studio + +https://studio.zerobrane.com[ZeroBrane Studio] is a free Lua IDE, and Singe +carries what it takes to run and debug a game from it: breakpoints, stepping, +the Stack and Watch windows, a game that holds still while it is stopped, a +stop in the IDE when an error is about to end the game, and autocompletion and +tooltips for the whole API in English, French, Italian, German, or Spanish. The +debugger half is MobDebug, built into Singe and started by `--debug`, so a game +needs no line of its own; anything else that speaks MobDebug's protocol can +connect the same way. + +[[zerobranesetup]] +==== Setting It Up + +Singe unpacks a ZeroBrane Studio package into its support folder, as +`Singe/ZeroBrane/singe.lua`, and keeps it current from one release to the next. +Tell ZeroBrane Studio where it is with one line in its `user.lua` (Edit > +Preferences > Settings: User), then restart it: + +[source,lua] +---- +package "/home/me/Singe/Singe/ZeroBrane/singe.lua" +---- + +Singe then appears under Project > Lua Interpreter, with Project > Debug With +Singe and a Singe button on the toolbar to start a game under the debugger in +one step. `.singe` files open as Lua, `.rml` documents as XML, and `.rcss` style +sheets as CSS. Make the game's folder the project (Project > Project Directory > +Choose). The package looks for Singe in that folder and the three above it, +taking the newest build for your machine, and then on the `PATH`; when it finds +none it asks the first time you run a game, and remembers. Project > Choose +Singe Executable picks another, and `path.singe = "/path/to/Singe"` in +`user.lua` overrides both. + +[[zerobranerun]] +==== Running and Debugging + +Project > Run (`F6`) starts the game, and Project > Start Debugging (`F5`) +starts it under the debugger. The script is the project's start file when one is +set (right click it in the project tree and choose Set As Start File), and the +file being edited otherwise; when that is one of the game's modules rather than +a `.singe` file, the project's only `.singe` file runs instead. Singe starts in +the folder holding its executable, as the menu would start it, so +`Singe/Framework.singe` and every name relative to that folder resolve as they +always do, and the game's `games.dat` entry supplies its video and settings. +Project > Command Line Parameters adds options of your own, `-v` and a video for +a game without an entry among them. + +Click in the margin to set a breakpoint. The game stops there with its locals in +the Stack window, the Watch window evaluates expressions, and Step Into (`F10`), +Step Over (`Shift+F10`), and Step Out (`Ctrl+F10`) behave as in any Lua program, +inside `singeMain` too. Project > Break stops a game wherever it is. While the +game is stopped, the disc, every video, and every sound pause just as for the +pause key, and go on from the same place afterwards. + +When an error is about to end the game -- a Lua error, or an engine call handed +something it cannot use -- the IDE stops first, in the Singe folder's +`Debugger.singe`, and the Output window says what went wrong. The Stack window +keeps the script's own frames below that file, with their locals as they were. +Continue, and the game ends as it would have, with the error and a traceback in +the Output window; double click a line of the traceback to open the file there. + +The game runs with `--reload`, so saving a script starts the game again and the +debugger with it, breakpoints and all. Project > Reload Singe When a Script +Changes turns that off. + +[[zerobranecompletion]] +==== Autocompletion and the Reference + +Autocompletion offers every function, callback, and constant of the API, and a +tooltip gives the arguments and the first paragraph of the entry. +Project > Analyze (`Shift+F7`) knows the API as well, so it flags a misspelled call rather than every call. ZeroBrane +Studio's own language setting picks the tooltips' language: `language = "fr"` +in `user.lua` gives French, and the same goes for Italian (`it`), German +(`de`), and Spanish (`es`). Right click a name and choose Look Up in Singe +Reference to open its entry in a browser; Help > Singe Reference opens the +start. Both need the documentation download unpacked beside Singe, as +`Singe-Docs-v3.00` or `Singe-Docs-v3.00-fr` and so on, the one in ZeroBrane +Studio's language taken first; +`singe = { reference = "/path/to/Reference.html" }` in `user.lua` names another. + +[[zerobranecabinet]] +==== Debugging a Cabinet + +The debugger works across a network. On the machine with ZeroBrane Studio, open +the game's folder as the project and choose Project > Start Debugger Server. On +the cabinet, start the game with `--debug=HOST`, where `HOST` is that machine's +name or address; the port is `8172` unless `--debug=HOST:PORT` says otherwise, +and the first machine has to accept connections on it. A cabinet that starts +its games from the menu can carry `debug = "HOST:PORT"` in its settings file +instead, and every game it starts connects, the menu among them. The IDE stops +each one at its first line, where Continue (`F5`) sets it going; +`debugger.runonstart = true` in `user.lua` lets it run straight on to the first +breakpoint. The cabinet reports its own paths, which ZeroBrane Studio maps onto +the project's files the first time it stops, so the same game folder on both +machines is all it needs. With nothing listening, a game plays as usual once the +attempt to connect gives up, which takes no more than two seconds. + +[[zerobranelimits]] +==== Limits + +* Debug a game loose, from its folder. A packed `.game` has no files on disk for the IDE to open. +* An error inside `singeMain` stops in `Framework.singe`, which runs `singeMain` as a coroutine; the message names the line of your script. +* While the IDE has the game stopped, the window does not redraw, and closing it waits until you continue. Stop Process (`Shift+F5`) ends the game at once. +* Singe stops a script that never returns when its window is closed, but not under the debugger, which owns the script while it runs; Break finds where it is stuck. + [[deterministic]] === Deterministic Test Mode @@ -2575,6 +2682,7 @@ install any additional software: | lume | `require("lume")` for the small things every game needs: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice` | inspect | `require("inspect")` to print a table in a shape a person can read, for when `debugPrint` is the debugger | bump | `require("bump")` for moving things that must not pass through each other -- it answers where a mover ends up, which <> deliberately do not +| MobDebug | `require("mobdebug")`, the debugger <> talks to; `--debug` starts it, so a game needs no line of its own | RmlUi | The GUI's own object model, the `rmlui` global (see <>) |=== @@ -2698,7 +2806,7 @@ it turns down, so between them they cover: * Tracker modules through libxmp: MOD, S3M, XM and IT, and fifty-odd others including 669, AMF, MED, OKT, PTM, STM and ULT. * Chiptunes through game-music-emu: NSF and NSFE, SPC, VGM, GBS, AY, GYM, HES, - KSS and SAP. The gzipped VGZ form is not read; ungzip it to VGM first. + KSS and SAP. The gzipped VGZ form is not read; ungzip it to VGM first. One that does not say how long it is plays until it falls silent, which can be never, so it is decoded as it plays rather than up front. * MIDI, synthesized from a SoundFont. See <>. The `Audio:` line of the trace header (`--program` or `--trace`) lists the @@ -4481,7 +4589,7 @@ Hypseus extension, accepted and ignored. See <> stops the game at a breakpoint, shows what is in its variables, and steps through it a line at a time. [[traceheader]] ==== The Trace Header @@ -7593,6 +7701,38 @@ end nodeSetMesh(playerCar, carMesh, teamLooks[1]) ---- +[#materialsetcutoff] +==== materialSetCutoff + +[source,text] +---- +materialSetCutoff(material, cutoff) +---- + +Discards every texel whose alpha falls below `cutoff` and draws the rest fully opaque, in the lit pass and the shadow pass alike, so a leaf card casts the shadow of its leaves rather than of its quad. This is glTF's alpha masking: unlike <> it needs no sorting, but a texel is kept whole or dropped, never blended. The cutoff is clamped to `0` through `1`, and `0`, the default, turns masking off. A model loaded from glTF arrives with the cutoff its file gives a `MASK` material. + +*Parameters:* + +* `material` -- integer material handle. +* `cutoff` -- number; the alpha, `0` to `1`, a texel needs to be drawn. + +*Since:* 3.00. +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- A chain-link fence: one textured quad, the holes cut out, both sides drawn. +fence = materialNew() +materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png")) +materialSetCutoff(fence, 0.5) +materialSetDoubleSided(fence, true) +panel = nodeNew() +nodeSetMesh(panel, meshPlane(4, 2), fence) +nodeSetPosition(panel, 0, 1, -5) +nodeSetRotation(panel, 90, 0, 0) +---- + [#materialsetdoublesided] ==== materialSetDoubleSided @@ -9589,7 +9729,7 @@ end id = musicLoad(filename) ---- -Hypseus extension. Loads a music track through the virtual file system and returns its handle. The whole file is decoded up front, as a sound is, so a long track costs memory; the handle is valid until <> or a reload. +Hypseus extension. Loads a music track through the virtual file system and returns its handle. The whole file is decoded up front, as a sound is, so a long track costs memory, unless it is a chiptune that does not say how long it is, or would decode to more than 512 MB, either of which is decoded as it plays; the handle is valid until <> or a reload. A file that is missing, or that no compiled-in decoder can read, terminates the script, as <> does. @@ -16867,6 +17007,9 @@ xref:Manual.adoc#onlineservice[Online: accounts, games and scores]). A game need | `scoreUpdate()` | Once a frame, from `onOverlayUpdate`. Nothing progresses without it. + +| `scoreGameId()` +| The game's `GAME_ID` from its `games.dat` entry, or `nil` when it has none. A game without one can be played but not ranked. |=== **A score is queued, not sent.** It is written to disk and handed to the service @@ -19406,6 +19549,23 @@ processor = voiceProcessorNew([frame [, rate [, tailMs]]]) *Since:* 3.00. +[#frameworkhelpers] +=== Framework Helpers + +`Framework.singe` defines five small helpers of its own, global like the rest +of the API, which games written for earlier versions use. New code may prefer +`lume` and `inspect`, from <<_included_libraries,the included libraries>>. + +[cols="1,2",options="header"] +|=== +| Function | What it does +| `utilDeepCopy(value)` | A copy of `value` all the way down: a table is copied key by key, its keys, values, and metatable copied in turn; anything else comes back as it is. +| `utilDump(value)` | `value` as text to print. A table becomes `{ ["key"] = value,}`, nested tables in braces of their own; anything else goes through `tostring`. For a table worth reading, `require("inspect")` does better. +| `utilGetTableSize(t)` | How many keys `t` holds, every one of them, not just the sequence `#t` measures. +| `utilOffsetTable(source, offset)` | A deep copy of a table shaped like `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, with `offset` added to every `value`. +| `utilTrim(s)` | `s` without the whitespace at either end. +|=== + [#enginecallbacks] === Engine Callbacks diff --git a/docs/TRANSLATING.md b/docs/TRANSLATING.md index 71c56c187..78dde5c74 100644 --- a/docs/TRANSLATING.md +++ b/docs/TRANSLATING.md @@ -84,7 +84,7 @@ names only); German capitalises its nouns as usual. A heading that is a name f * A decimal in running prose takes the language's comma (1,5 seconds); a number the reader types, or one in code or inline code, never does. -**Names:** Singe, its expansion "SINGE Is Not a Game Emulator" (which may be glossed in +**Names:** Singe, its expansion "Scott's Increasingly Nifty Game Engine" (which may be glossed in parentheses), Hypseus, Daphne, Lua, SDL, and every other product name stay as they are. The books name no games except where the English does, for Singe's own name; a translation adds none. Other programs and keys go by the names the reader's own system shows (Notepad is Bloc-notes on a diff --git a/docs/de/Manual.adoc b/docs/de/Manual.adoc index 3067001e9..9cbd16ec4 100644 --- a/docs/de/Manual.adoc +++ b/docs/de/Manual.adoc @@ -18,12 +18,12 @@ include::locale.adoc[] [#_about_singe] == Über Singe -Singe (SINGE Is Not a Game Emulator, benannt nach dem Drachen aus -Dragon's Lair) ist ein Lua-basiertes Skriptsystem, mit dem sich schnell Prototypen -neuer Laserdisc-Spiele bauen oder ganz neue Spiele erschaffen lassen. Die Sprache ist -leicht zu lernen, sehr mächtig und schnell. Alle Funktionen, die du für dein -eigenes Spiel brauchst, stehen über eine einfache Programmierschnittstelle -(API) zur Verfügung. +Singe (Scott's Increasingly Nifty Game Engine, „Scotts zunehmend pfiffige +Spiel-Engine“, benannt nach dem Drachen aus Dragon's Lair) ist ein Lua-basiertes +Skriptsystem, mit dem sich schnell Prototypen neuer Laserdisc-Spiele bauen oder +ganz neue Spiele erschaffen lassen. Die Sprache ist leicht zu lernen, sehr +mächtig und schnell. Alle Funktionen, die du für dein eigenes Spiel brauchst, +stehen über eine einfache Programmierschnittstelle (API) zur Verfügung. Singe bietet dem Spieleentwickler zahlreiche Funktionen. Einige der interessanteren sind: @@ -42,7 +42,8 @@ interessanteren sind: * Unterstützung für analoge und digitale Eingabegeräte, einschließlich Lightguns * Breite Unterstützung für Video- und Audioformate -Singe läuft unter Windows, macOS und Linux, einschließlich des Raspberry Pi und +Singe läuft unter Windows, macOS und Linux, auf ARM-Prozessoren ebenso wie auf +Intel- und AMD-Prozessoren: auf Windows-on-ARM-Laptops, dem Raspberry Pi und Handhelds wie dem R36S. Spielern ermöglicht Singe, viele nicht emulierte und neue Spiele mit einem @@ -65,7 +66,8 @@ davor kamen. Zu dem Zeitpunkt, als dieses Handbuch geschrieben wurde, ist die ne Um Singe zu installieren, lade einfach die passende Binärdatei für dein System herunter. Leg sie allein in einen neuen Ordner und starte sie. Beim ersten Start legt Singe alle nötigen Verzeichnisse und Hilfsdateien an. Jetzt kannst du -Spiele installieren. +Spiele installieren. Auf einem Windows-PC mit ARM-Prozessor nimm die Version `Windows-aarch64`: Die +Version `Windows-x86_64` läuft dort auch, emuliert, aber langsamer. [#_upgrading_singe] === Singe aktualisieren @@ -897,6 +899,7 @@ Siehe <> für die Traces. [cols="1,2",options="header"] |=== | Option | Zweck +| `--debug[=HOST:PORT]` | Das Spiel mit einem Debugger verbinden, der auf `HOST:PORT` lauscht, normalerweise ZeroBrane Studio, der es dann an Haltepunkten anhalten, Schritt für Schritt ausführen und zeigen kann, was in seinen Variablen steht; siehe xref:Reference.adoc#zerobrane[Debuggen in ZeroBrane Studio]. `HOST` ist ohne Angabe `localhost` und `PORT` ist `8172`. Hört niemand zu, spielt sich das Spiel wie gewohnt, sobald der Versuch aufgibt, nach höchstens zwei Sekunden. Standard: aus. | `--deterministic[=MS]` | Nur zum Testen: die Echtzeit ignorieren und die ganze Engine auf einer virtuellen Uhr laufen lassen, die jeden Frame um `MS` Millisekunden weiterrückt, sodass dieselbe Frame-Nummer immer denselben Moment bedeutet. Auch die Disc folgt der virtuellen Uhr, ihre Frames dauern so lange, wie sie wert sind, und die Zufallsgeneratoren werden mit derselben Zahl initialisiert, sodass sich ein Lauf pixelgenau wiederholt. Der Wert ist optional und ist zugleich der Schritt und der Startwert, `1` bis `1000`. Standard, wenn ohne Wert angegeben: `15`, die Frame-Zeit, die sich aus der eigenen Rate der Engine ergibt. Die Taktung ist in diesem Modus bedeutungslos, ebenso der Ton, es sei denn, `--record` ist auch angegeben; dann wird er im Gleichschritt mit dem Bild erzeugt; siehe xref:Reference.adoc#deterministic[Deterministischer Testmodus]. Standard: aus. | `-P`, `--pack=DIRECTORY` | Das Spiel in `DIRECTORY` in die nach den Optionen angegebene Datenbank packen, dann beenden. Siehe xref:Reference.adoc#singlefile[Spiele in einer Datei]. | `-T`, `--patch=GAME.game` | Dateien im gepackten Spiel aus dem Verzeichnis oder der Patch-Datenbank ersetzen, die nach den Optionen angegeben ist, dann beenden. diff --git a/docs/de/Reference.adoc b/docs/de/Reference.adoc index bc9240e54..73ae5b0b6 100644 --- a/docs/de/Reference.adoc +++ b/docs/de/Reference.adoc @@ -45,6 +45,53 @@ dofile("Singe/Framework.singe") Starte das Spiel mit `--reload` (`-R`), und Singe überwacht jede ungepackte Skriptdatei, die das Spiel geladen hat. Speichere eine Änderung an einer davon, und das Spiel beginnt einen Moment später wieder bei seinem Skript: Alles, was das Skript angelegt hat, verschwindet (Sprites, Sounds, Schriftarten, Videos, GUIs, die 3D-Szene, die Physik, Emitter, das Overlay kehrt zu seiner Standardgröße zurück, die Effektlautstärke und die Maus zu ihrem Zustand beim Start), die Engine bleibt (das Fenster, die GPU, die Controller), die Disc steht wie beim Start wieder auf ihrem ersten Frame, und das Skript läuft von vorn, sodass ein 52-MB-Modell in der Zeit wieder auf dem Bildschirm ist, die sein Laden dauert, statt in der Zeit, die der Start von Singe dauert. Eine Taste oder ein Knopf, die über das Neuladen hinweg gedrückt bleiben, werden ignoriert, bis sie losgelassen werden, wie bei einem frischen Start. `F5` lädt auf Wunsch neu, und ein Skript kann es mit `singeReload()` anfordern. Gepackte Spiele haben nichts, was sich überwachen ließe; die Option ist für ein Spiel in einem Ordner. Ein Skriptfehler beim Neuladen wird ausgegeben, und das Spiel bleibt leer, bis das nächste Neuladen ihn behebt; die Datei bleibt überwacht, das Speichern der Korrektur genügt also. Weil das Neuladen denselben Abbau durchläuft wie das Beenden, zeigt sich jedes Leck eines Skripts zuerst hier. +[[zerobrane]] +=== Debuggen in ZeroBrane Studio + +https://studio.zerobrane.com[ZeroBrane Studio] ist eine kostenlose Lua-IDE, und Singe bringt mit, was es braucht, um ein Spiel darin zu starten und zu debuggen: Haltepunkte, Einzelschritte, die Fenster Stack und Beobachtungspunkte, ein Spiel, das stillhält, solange es angehalten ist, einen Halt in der IDE, wenn ein Fehler das Spiel gleich beenden würde, und Autovervollständigung mit Tooltips für die ganze API, auf Englisch, Französisch, Italienisch, Deutsch oder Spanisch. Die Debugger-Hälfte ist MobDebug, in Singe eingebaut und von `--debug` gestartet, sodass ein Spiel keine eigene Zeile dafür braucht; alles andere, was das Protokoll von MobDebug spricht, kann sich auf dieselbe Weise verbinden. + +[[zerobranesetup]] +==== Einrichten + +Singe entpackt ein Paket für ZeroBrane Studio in seinen Support-Ordner, als `Singe/ZeroBrane/singe.lua`, und hält es von einer Version zur nächsten aktuell. Sag ZeroBrane Studio mit einer einzigen Zeile in dessen `user.lua` (Bearbeiten > Einstellungen > Einstellungen: Nutzer), wo es liegt, und starte es dann neu: + +[source,lua] +---- +package "/home/me/Singe/Singe/ZeroBrane/singe.lua" +---- + +Singe erscheint dann unter Projekt > Lua Interpreter, dazu Projekt > Mit Singe debuggen und ein Singe-Knopf in der Werkzeugleiste, die ein Spiel in einem Schritt unter dem Debugger starten. `.singe`-Dateien öffnen sich als Lua, `.rml`-Dokumente als XML und `.rcss`-Stylesheets als CSS. Mach den Ordner des Spiels zum Projekt (Projekt > Projektverzeichnis > Wählen...). Das Paket sucht Singe in diesem Ordner und den drei darüber, nimmt den neuesten Build für deinen Rechner und sucht danach im `PATH`; findet es keinen, fragt es beim ersten Start eines Spiels und merkt sich die Antwort. Projekt > Singe-Programm wählen... nimmt ein anderes, und `path.singe = "/path/to/Singe"` in `user.lua` geht beidem vor. + +[[zerobranerun]] +==== Starten und debuggen + +Projekt > Starten (`F6`) startet das Spiel, und Projekt > Debugging starten (`F5`) startet es unter dem Debugger. Das Skript ist die Startdatei des Projekts, wenn eine festgelegt ist (klick sie im Projektbaum mit rechts an und wähl Als Startdatei definieren), und sonst die gerade bearbeitete Datei; ist das eines der Module des Spiels statt einer `.singe`-Datei, startet stattdessen die einzige `.singe`-Datei des Projekts. Singe startet in dem Ordner, in dem sein Programm liegt, so wie das Menü es starten würde, sodass `Singe/Framework.singe` und jeder Name relativ zu diesem Ordner sich auflösen wie immer, und der Eintrag des Spiels in `games.dat` liefert sein Video und seine Einstellungen. Projekt > Kommandozeilenparameter... fügt eigene Optionen hinzu, darunter `-v` und ein Video für ein Spiel ohne Eintrag. + +Klick in den Rand, um einen Haltepunkt zu setzen. Das Spiel hält dort an, seine lokalen Variablen im Fenster Stack, das Fenster Beobachtungspunkte wertet Ausdrücke aus, und Schritt hinein (`F10`), Überspringen (`Shift+F10`) und Schritt raus (`Ctrl+F10`) verhalten sich wie in jedem Lua-Programm, auch innerhalb von `singeMain`. Projekt > Unterbrechung hält ein Spiel an, wo immer es gerade ist. Solange das Spiel angehalten ist, pausieren die Disc, jedes Video und jeder Sound genau wie bei der Pause-Taste und laufen danach an derselben Stelle weiter. + +Wenn ein Fehler das Spiel gleich beenden würde -- ein Lua-Fehler, oder ein Engine-Aufruf, der etwas bekommt, womit er nichts anfangen kann --, hält die IDE vorher an, in der `Debugger.singe` des Singe-Ordners, und das Fenster Ausgabe sagt, was schiefgegangen ist. Das Fenster Stack behält unter dieser Datei die eigenen Frames des Skripts, mit ihren lokalen Variablen, wie sie waren. Mach weiter, und das Spiel endet, wie es geendet hätte, mit dem Fehler und einem Traceback im Fenster Ausgabe; ein Doppelklick auf eine Zeile des Tracebacks öffnet die Datei an dieser Stelle. + +Das Spiel läuft mit `--reload`, sodass das Speichern eines Skripts das Spiel neu startet und den Debugger mit ihm, Haltepunkte und alles. Projekt > Singe neu laden, wenn sich ein Skript ändert schaltet das ab. + +[[zerobranecompletion]] +==== Autovervollständigung und die Referenz + +Die Autovervollständigung bietet jede Funktion, jeden Callback und jede Konstante der API an, und ein Tooltip zeigt die Argumente und den ersten Absatz des Eintrags. +Projekt > Analyseroutine (`Shift+F7`) kennt die API ebenfalls und meldet deshalb einen falsch geschriebenen Aufruf statt jedes Aufrufs. Die Spracheinstellung von ZeroBrane Studio wählt die Sprache der Tooltips: `language = "fr"` in `user.lua` ergibt Französisch, und ebenso geht es mit Italienisch (`it`), Deutsch (`de`) und Spanisch (`es`). Klick einen Namen mit rechts an und wähl In der Singe-Referenz nachschlagen, um seinen Eintrag in einem Browser zu öffnen; Hilfe > Singe-Referenz öffnet den Anfang. Beides braucht den Dokumentations-Download neben Singe entpackt, als `Singe-Docs-v3.00` oder `Singe-Docs-v3.00-fr` und so weiter, wobei der in der Sprache von ZeroBrane Studio zuerst genommen wird; `singe = { reference = "/path/to/Reference.html" }` in `user.lua` nennt einen anderen. + +[[zerobranecabinet]] +==== Einen Automaten debuggen + +Der Debugger arbeitet über ein Netzwerk hinweg. Öffne auf dem Rechner mit ZeroBrane Studio den Ordner des Spiels als Projekt und wähl Projekt > Debugserver starten. Starte auf dem Automaten das Spiel mit `--debug=HOST`, wobei `HOST` der Name oder die Adresse dieses Rechners ist; der Port ist `8172`, sofern `--debug=HOST:PORT` nichts anderes sagt, und der erste Rechner muss Verbindungen auf ihm annehmen. Ein Automat, der seine Spiele aus dem Menü startet, kann stattdessen `debug = "HOST:PORT"` in seiner Einstellungsdatei tragen, und jedes Spiel, das er startet, verbindet sich, das Menü eingeschlossen. Die IDE hält jedes an seiner ersten Zeile an, wo Fortsetzen (`F5`) es loslaufen lässt; `debugger.runonstart = true` in `user.lua` lässt es gleich bis zum ersten Haltepunkt laufen. Der Automat meldet seine eigenen Pfade, die ZeroBrane Studio beim ersten Halt den Dateien des Projekts zuordnet, sodass derselbe Spielordner auf beiden Rechnern alles ist, was es braucht. Hört niemand zu, spielt sich ein Spiel wie gewohnt, sobald der Verbindungsversuch aufgibt, was nicht länger als zwei Sekunden dauert. + +[[zerobranelimits]] +==== Grenzen + +* Debugge ein Spiel ungepackt, aus seinem Ordner. Ein gepacktes `.game` hat keine Dateien auf der Platte, die die IDE öffnen könnte. +* Ein Fehler in `singeMain` hält in `Framework.singe` an, das `singeMain` als Koroutine ausführt; die Meldung nennt die Zeile deines Skripts. +* Solange die IDE das Spiel angehalten hält, zeichnet sich das Fenster nicht neu, und das Schließen wartet, bis du weitermachst. Prozess anhalten (`Shift+F5`) beendet das Spiel sofort. +* Singe hält ein Skript, das nie zurückkehrt, an, wenn sein Fenster geschlossen wird, aber nicht unter dem Debugger, dem das Skript gehört, solange es läuft; Unterbrechung findet heraus, wo es festhängt. + [[deterministic]] === Deterministischer Testmodus @@ -1892,6 +1939,7 @@ installiert werden muss: | lume | `require("lume")` für die kleinen Dinge, die jedes Spiel braucht: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice` | inspect | `require("inspect")`, um eine Tabelle in einer Form auszugeben, die ein Mensch lesen kann, für den Fall, dass `debugPrint` der Debugger ist | bump | `require("bump")` für Dinge in Bewegung, die nicht durcheinander hindurchgehen dürfen -- es beantwortet, wo ein bewegtes Ding landet, was <> absichtlich nicht tun +| MobDebug | `require("mobdebug")`, der Debugger, mit dem <> spricht; `--debug` startet ihn, sodass ein Spiel keine eigene Zeile dafür braucht | RmlUi | Das eigene Objektmodell der GUI, die globale Variable `rmlui` (siehe <>) |=== @@ -2019,7 +2067,7 @@ sodass beide zusammen Folgendes abdecken: * Tracker-Module über libxmp: MOD, S3M, XM und IT sowie rund fünfzig weitere, darunter 669, AMF, MED, OKT, PTM, STM und ULT. * Chiptunes über game-music-emu: NSF und NSFE, SPC, VGM, GBS, AY, GYM, HES, - KSS und SAP. Die gzip-komprimierte Form VGZ wird nicht gelesen; entpacke sie vorher zu VGM. + KSS und SAP. Die gzip-komprimierte Form VGZ wird nicht gelesen; entpacke sie vorher zu VGM. Ein Chiptune, der seine Länge nicht angibt, spielt, bis er verstummt, und das muss nie geschehen; deshalb wird er beim Abspielen dekodiert statt vorab. * MIDI, aus einem SoundFont synthetisiert. Siehe <>. Die Zeile `Audio:` im Kopf des Trace (`--program` oder `--trace`) listet die Decoder @@ -3804,7 +3852,7 @@ Hypseus-Erweiterung, angenommen und ignoriert. Warum es den Offset, den sie verl [#debug] === Debugging -Zwei Aufrufe, um zu sehen, was ein Skript tut: `debugPrint` schreibt Text, und `lineDraw` zeichnet für einen Frame eine Linie über die 3D-Szene (`physicsSetDebug` und `navDraw` zeichnen über dieselben Linien). Ausgegebenes geht an die Konsole, von der aus Singe gestartet wurde, ist also während der Entwicklung sichtbar und in einem Automaten harmlos, wo `--noconsole` es stummschaltet. Läuft Singe mit `--trace`, wird außerdem jeder API-Aufruf, auch dieser, mit seinen Argumenten in `trace.txt` im Datenordner geschrieben; so bekommst du eine dauerhafte Aufzeichnung. Die Befehlszeilenoptionen stehen im Kapitel über das Starten von Singe; die Integration von ZeroBrane Studio in `zbstudio/` bietet einen Debugger, wenn Ausgeben nicht reicht. +Zwei Aufrufe, um zu sehen, was ein Skript tut: `debugPrint` schreibt Text, und `lineDraw` zeichnet für einen Frame eine Linie über die 3D-Szene (`physicsSetDebug` und `navDraw` zeichnen über dieselben Linien). Ausgegebenes geht an die Konsole, von der aus Singe gestartet wurde, ist also während der Entwicklung sichtbar und in einem Automaten harmlos, wo `--noconsole` es stummschaltet. Läuft Singe mit `--trace`, wird außerdem jeder API-Aufruf, auch dieser, mit seinen Argumenten in `trace.txt` im Datenordner geschrieben; so bekommst du eine dauerhafte Aufzeichnung. Die Befehlszeilenoptionen stehen im Kapitel über das Starten von Singe. Reicht Ausgeben nicht, hält <> das Spiel an einem Haltepunkt an, zeigt, was in seinen Variablen steht, und geht es Zeile für Zeile durch. [[traceheader]] ==== Der Trace-Kopf @@ -6916,6 +6964,38 @@ end nodeSetMesh(playerCar, carMesh, teamLooks[1]) ---- +[#materialsetcutoff] +==== materialSetCutoff + +[source,text] +---- +materialSetCutoff(material, cutoff) +---- + +Verwirft jedes Texel, dessen Alpha unter `cutoff` liegt, und zeichnet den Rest vollständig deckend, im beleuchteten Durchgang wie im Schattendurchgang, sodass eine Blätterkarte den Schatten ihrer Blätter wirft statt den ihres Quads. Das ist das Alpha-Masking von glTF: Anders als <> braucht es keine Sortierung, aber ein Texel wird ganz behalten oder verworfen, nie überblendet. Der Schwellwert wird auf `0` bis `1` begrenzt, und `0`, der Standard, schaltet das Masking ab. Ein aus glTF geladenes Modell kommt mit dem Schwellwert, den seine Datei einem `MASK`-Material gibt. + +*Parameter:* + +* `material` -- ganzzahliges Material-Handle. +* `cutoff` -- Zahl; das Alpha, `0` bis `1`, das ein Texel braucht, um gezeichnet zu werden. + +*Seit:* 3.00. +*Siehe auch:* <>, <>, <> + +.Beispiel +[source,lua] +---- +-- Ein Maschendrahtzaun: ein einziges texturiertes Quad, die Löcher ausgeschnitten, beide Seiten gezeichnet. +fence = materialNew() +materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png")) +materialSetCutoff(fence, 0.5) +materialSetDoubleSided(fence, true) +panel = nodeNew() +nodeSetMesh(panel, meshPlane(4, 2), fence) +nodeSetPosition(panel, 0, 1, -5) +nodeSetRotation(panel, 90, 0, 0) +---- + [#materialsetdoublesided] ==== materialSetDoubleSided @@ -8936,7 +9016,7 @@ end id = musicLoad(filename) ---- -Hypseus-Erweiterung. Lädt ein Musikstück über das virtuelle Dateisystem und gibt sein Handle zurück. Die ganze Datei wird vorab dekodiert, wie bei einem Sound, ein langes Stück kostet also Speicher; das Handle gilt bis <> oder zu einem Neuladen. +Hypseus-Erweiterung. Lädt ein Musikstück über das virtuelle Dateisystem und gibt sein Handle zurück. Die ganze Datei wird vorab dekodiert, wie bei einem Sound, ein langes Stück kostet also Speicher -- außer einem Chiptune, der seine Länge nicht angibt, oder etwas, das dekodiert mehr als 512 MB hätte, die beide beim Abspielen dekodiert werden; das Handle gilt bis <> oder zu einem Neuladen. Eine Datei, die fehlt oder die kein einkompilierter Decoder lesen kann, beendet das Skript, wie bei <>. @@ -16255,6 +16335,9 @@ xref:Manual.adoc#onlineservice[Online: Konten, Spiele und Punkte]). Ein Spiel br | `scoreUpdate()` | Einmal pro Frame, aus `onOverlayUpdate`. Ohne es geht nichts voran. + +| `scoreGameId()` +| Die `GAME_ID` des Spiels aus seinem Eintrag in `games.dat`, oder `nil`, wenn es keine hat. Ein Spiel ohne sie lässt sich spielen, aber nicht in eine Rangliste eintragen. |=== **Eine Punktzahl wird eingereiht, nicht gesendet.** Sie wird auf die Festplatte geschrieben und dem Dienst @@ -18799,6 +18882,21 @@ processor = voiceProcessorNew([frame [, rate [, tailMs]]]) *Seit:* 3.00. +[#frameworkhelpers] +=== Hilfsfunktionen des Frameworks + +`Framework.singe` definiert fünf kleine Hilfsfunktionen eigener Art, global wie der Rest der API, die Spiele für frühere Versionen benutzen. Neuer Code kann `lume` und `inspect` aus <<_included_libraries,den mitgelieferten Bibliotheken>> vorziehen. + +[cols="1,2",options="header"] +|=== +| Funktion | Was sie tut +| `utilDeepCopy(value)` | Eine Kopie von `value` bis ganz nach unten: Eine Tabelle wird Schlüssel für Schlüssel kopiert, ihre Schlüssel, Werte und ihre Metatabelle ihrerseits kopiert; alles andere kommt unverändert zurück. +| `utilDump(value)` | `value` als Text zum Ausgeben. Eine Tabelle wird zu `{ ["key"] = value,}`, verschachtelte Tabellen in eigenen Klammern; alles andere geht durch `tostring`. Für eine Tabelle, die sich zu lesen lohnt, macht es `require("inspect")` besser. +| `utilGetTableSize(t)` | Wie viele Schlüssel `t` enthält, alle, nicht nur die Folge, die `#t` misst. +| `utilOffsetTable(source, offset)` | Eine tiefe Kopie einer Tabelle in der Form von `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, mit `offset` zu jedem `value` addiert. +| `utilTrim(s)` | `s` ohne die Leerzeichen an beiden Enden. +|=== + [#enginecallbacks] === Engine-Callbacks diff --git a/docs/de/downloadReadme.txt b/docs/de/downloadReadme.txt index 19723eabe..3c6279ae7 100644 --- a/docs/de/downloadReadme.txt +++ b/docs/de/downloadReadme.txt @@ -1,5 +1,6 @@ Singe @VERSION@ Dokumentation +@PREVIEW@ Manual.pdf / .html Das Benutzerhandbuch, hier zum Korrekturlesen; eine Veröffentlichung trägt es in Singe selbst. Reference.pdf / .html Alles, was ein Spiel kann: das Programmiermodell und jede Funktion. Learn.pdf / .html Programmieren lernen mit Singe: einunddreißig Lektionen, von null bis zum fertigen Spiel. learn/ Das fertige Skript jeder Lektion und das Grafik-Kit, das die Lektionen verwenden diff --git a/docs/downloadReadme.txt b/docs/downloadReadme.txt index a1c17b5f4..7a337ac2d 100644 --- a/docs/downloadReadme.txt +++ b/docs/downloadReadme.txt @@ -1,5 +1,6 @@ Singe @VERSION@ documentation +@PREVIEW@ Manual.pdf / .html The user manual, here for proofreading; a release carries it inside Singe. Reference.pdf / .html Everything a game can do: the programming model and every function. Learn.pdf / .html Learn to Program with Singe: thirty-one lessons from nothing to a finished game. learn/ The finished script of every lesson, and the art kit the lessons use. diff --git a/docs/es/Manual.adoc b/docs/es/Manual.adoc index aa8c89fab..85ee542c6 100644 --- a/docs/es/Manual.adoc +++ b/docs/es/Manual.adoc @@ -18,12 +18,13 @@ include::locale.adoc[] [#_about_singe] == Acerca de Singe -Singe (SINGE Is Not a Game Emulator, «SINGE no es un emulador de juegos», que -debe su nombre al dragón de Dragon's Lair) es un sistema de scripts basado en -Lua que permite crear rápidamente prototipos de nuevos juegos de laserdisc, o -crear juegos totalmente nuevos. El lenguaje es fácil de aprender, muy potente y -rápido. Todo lo necesario para desarrollar tu propio juego está disponible a -través de una sencilla interfaz de programación de aplicaciones (API). +Singe (Scott's Increasingly Nifty Game Engine, «el motor de juegos cada vez más +ingenioso de Scott», que debe su nombre al dragón de Dragon's Lair) es un +sistema de scripts basado en Lua que permite crear rápidamente prototipos de +nuevos juegos de laserdisc, o crear juegos totalmente nuevos. El lenguaje es +fácil de aprender, muy potente y rápido. Todo lo necesario para desarrollar tu +propio juego está disponible a través de una sencilla interfaz de programación +de aplicaciones (API). Singe ofrece numerosas funciones al desarrollador de juegos. Algunas de las más interesantes son: @@ -42,8 +43,9 @@ interesantes son: * Compatibilidad con dispositivos de entrada analógicos y digitales, incluidas las pistolas de luz * Amplia compatibilidad con formatos de vídeo y audio -Singe funciona en Windows, macOS y Linux, incluidas la Raspberry Pi y consolas -portátiles como la R36S. +Singe funciona en Windows, macOS y Linux, con procesadores ARM además de Intel y +AMD: portátiles con Windows en ARM, la Raspberry Pi y consolas portátiles como la +R36S. Para los jugadores, Singe permite disfrutar de muchos juegos nuevos y no emulados con el front-end que prefieras o con el sistema de menús incluido. @@ -65,7 +67,8 @@ antes. En el momento de escribir este manual, la última versión es la Para instalar Singe, basta con descargar el binario adecuado para tu sistema. Ponlo solo en una carpeta nueva y ejecútalo. La primera vez que se ejecuta, Singe crea todas las carpetas y archivos de soporte necesarios. Ya puedes -instalar algunos juegos. +instalar algunos juegos. En un PC con Windows y procesador ARM, elige la versión `Windows-aarch64`: la +versión `Windows-x86_64` también funciona allí, emulada, pero más despacio. [#_upgrading_singe] === Actualizar Singe @@ -944,6 +947,7 @@ Véase <> para las trazas. [cols="1,2",options="header"] |=== | Opción | Función +| `--debug[=HOST:PORT]` | Conecta el juego a un depurador que escucha en `HOST:PORT`, normalmente ZeroBrane Studio, que entonces puede detenerlo en puntos de ruptura, ejecutarlo paso a paso y mostrar qué hay en sus variables; consulta xref:Reference.adoc#zerobrane[Depurar en ZeroBrane Studio]. `HOST` es `localhost` si no se indica y `PORT` es `8172`. Si nadie escucha, el juego se juega como siempre una vez que el intento se rinde, en dos segundos como mucho. Por defecto: desactivado. | `--deterministic[=MS]` | Solo para pruebas: ignora el tiempo real y ejecuta todo el motor con un reloj virtual que avanza `MS` milisegundos cada fotograma, así que el mismo número de fotograma siempre significa el mismo momento. El disco también sigue el reloj virtual, y sus fotogramas duran el tiempo que valen, y los generadores aleatorios se inicializan con el mismo número, así que una ejecución se repite píxel a píxel. El valor es opcional y es a la vez el paso y la semilla, de `1` a `1000`. Por defecto, si se da sin valor: `15`, el tiempo de fotograma que implica la tasa propia del motor. El ritmo no tiene sentido en este modo, y tampoco el sonido salvo que se dé también `--record`, en cuyo caso se genera al paso de la imagen; véase xref:Reference.adoc#deterministic[Modo de prueba determinista]. Por defecto: desactivada. | `-P`, `--pack=DIRECTORY` | Empaqueta el juego de `DIRECTORY` en la base de datos que se nombra tras las opciones y sale. Véase xref:Reference.adoc#singlefile[Juegos de un solo archivo]. | `-T`, `--patch=GAME.game` | Sustituye archivos del juego empaquetado a partir de la carpeta o la base de datos de parche que se nombra tras las opciones y sale. diff --git a/docs/es/Reference.adoc b/docs/es/Reference.adoc index 9dd7577b1..594705d77 100644 --- a/docs/es/Reference.adoc +++ b/docs/es/Reference.adoc @@ -86,6 +86,121 @@ hasta que la siguiente recarga lo arregla; el archivo sigue vigilado, así que basta con guardar la corrección. Como la recarga hace el mismo desmontaje que al salir, cualquier fuga de un script aparece aquí primero. +[[zerobrane]] +=== Depurar en ZeroBrane Studio + +https://studio.zerobrane.com[ZeroBrane Studio] es un IDE de Lua gratuito, y Singe +trae lo que hace falta para ejecutar y depurar un juego desde él: puntos de +ruptura, ejecución paso a paso, las ventanas Pila e Inspeccionar, un juego que +se queda quieto mientras está detenido, una parada en el IDE cuando un error +está a punto de terminar el juego, y autocompletado con tooltips para toda la +API, en inglés, francés, italiano, alemán o español. La parte del depurador es +MobDebug, integrado en Singe y arrancado por `--debug`, así que un juego no +necesita ninguna línea propia; cualquier otra herramienta que hable el +protocolo de MobDebug puede conectarse de la misma manera. + +[[zerobranesetup]] +==== Prepararlo + +Singe descomprime un paquete para ZeroBrane Studio en su carpeta de soporte, +como `Singe/ZeroBrane/singe.lua`, y lo mantiene al día de una versión a la +siguiente. Dile a ZeroBrane Studio dónde está con una sola línea en su +`user.lua` (Editar > Preferencias > Configuración: Usuario) y luego reinícialo: + +[source,lua] +---- +package "/home/me/Singe/Singe/ZeroBrane/singe.lua" +---- + +Singe aparece entonces en Proyecto > Intérprete Lua, con Proyecto > Depurar con +Singe y un botón de Singe en la barra de herramientas para arrancar un juego +bajo el depurador en un solo paso. Los archivos `.singe` se abren como Lua, los +documentos `.rml` como XML y las hojas de estilo `.rcss` como CSS. Haz de la +carpeta del juego el proyecto (Proyecto > Directorio de proyecto > Elegir...). +El paquete busca Singe en esa carpeta y en las tres de encima, tomando la +versión más reciente para tu máquina, y después en el `PATH`; si no encuentra +ninguno, te lo pregunta la primera vez que ejecutas un juego, y lo recuerda. +Proyecto > Elegir el ejecutable de Singe... elige otro, y +`path.singe = "/path/to/Singe"` en `user.lua` se impone a los dos. + +[[zerobranerun]] +==== Ejecutar y depurar + +Proyecto > Ejecutar (`F6`) arranca el juego, y Proyecto > Comenzar depuración +(`F5`) lo arranca bajo el depurador. El script es el fichero de inicio del +proyecto cuando hay uno (haz clic derecho sobre él en el árbol del proyecto y +elige Establecer como fichero de inicio), y si no, el archivo que se está +editando; cuando ese es uno de los módulos del juego en lugar de un archivo +`.singe`, se ejecuta el único archivo `.singe` del proyecto. Singe arranca en +la carpeta que contiene su ejecutable, como lo arrancaría el menú, así que +`Singe/Framework.singe` y todo nombre relativo a esa carpeta se resuelven como +siempre, y la entrada del juego en `games.dat` aporta su vídeo y sus ajustes. +Proyecto > Parámetros de línea de comandos... añade opciones tuyas, entre ellas +`-v` y un vídeo para un juego sin entrada. + +Haz clic en el margen para poner un punto de ruptura. El juego se detiene allí +con sus variables locales en la ventana Pila, la ventana Inspeccionar evalúa +expresiones, y Paso dentro (`F10`), Paso sin entrar (`Shift+F10`) y Paso fuera +(`Ctrl+F10`) se comportan como en cualquier programa de Lua, también dentro de +`singeMain`. Proyecto > Ruptura detiene un juego dondequiera que esté. Mientras +el juego está detenido, el disco, cada vídeo y cada sonido se pausan igual que +con la tecla de pausa, y después siguen desde el mismo punto. + +Cuando un error está a punto de terminar el juego -- un error de Lua, o una +llamada del motor que recibe algo que no puede usar --, el IDE se detiene antes, +en el `Debugger.singe` de la carpeta Singe, y la ventana Salida dice qué ha ido +mal. La ventana Pila conserva debajo de ese archivo los propios marcos del +script, con sus variables locales como estaban. Continúa, y el juego termina +como habría terminado, con el error y una traza en la ventana Salida; haz doble +clic en una línea de la traza para abrir el archivo en ese punto. + +El juego se ejecuta con `--reload`, así que guardar un script vuelve a arrancar +el juego y el depurador con él, puntos de ruptura incluidos. Proyecto > Recargar +Singe cuando cambie un script lo desactiva. + +[[zerobranecompletion]] +==== Autocompletado y la Referencia + +El autocompletado ofrece cada función, callback y constante de la API, y un +tooltip muestra los argumentos y el primer párrafo de la entrada. +Proyecto > Analizar (`Shift+F7`) también conoce la API, así que señala una llamada mal escrita en lugar de cada llamada. El ajuste de +idioma de ZeroBrane Studio elige el idioma de los tooltips: +`language = "fr"` en `user.lua` da el francés, y lo mismo vale para el +italiano (`it`), el alemán (`de`) y el español (`es`). Haz clic derecho en un nombre y +elige Buscar en la Referencia de Singe para abrir su entrada en un navegador; +Ayuda > Referencia de Singe abre el principio. Los dos necesitan la descarga de +la documentación descomprimida junto a Singe, como `Singe-Docs-v3.00` o +`Singe-Docs-v3.00-fr` y así sucesivamente, tomando primero la del idioma de +ZeroBrane Studio; `singe = { reference = "/path/to/Reference.html" }` en +`user.lua` indica otra. + +[[zerobranecabinet]] +==== Depurar una recreativa + +El depurador funciona a través de una red. En la máquina con ZeroBrane Studio, +abre la carpeta del juego como proyecto y elige Proyecto > Lanzar servidor de +depuración. En la recreativa, arranca el juego con `--debug=HOST`, donde `HOST` +es el nombre o la dirección de esa máquina; el puerto es `8172` salvo que +`--debug=HOST:PORT` diga otro, y la primera máquina tiene que aceptar +conexiones en él. Una recreativa que arranca sus juegos desde el menú puede +llevar en su lugar `debug = "HOST:PORT"` en su archivo de ajustes, y cada juego +que arranca se conecta, el menú incluido. El IDE detiene cada uno en su primera +línea, donde Continuar (`F5`) lo pone en marcha; `debugger.runonstart = true` en +`user.lua` lo deja seguir derecho hasta el primer punto de ruptura. La +recreativa informa de sus propias rutas, que ZeroBrane Studio hace corresponder +con los archivos del proyecto la primera vez que se detiene, así que basta con +la misma carpeta del juego en las dos máquinas. Si nadie escucha, un juego se +juega como siempre una vez que el intento de conectarse se rinde, lo que no +tarda más de dos segundos. + +[[zerobranelimits]] +==== Límites + +* Depura un juego sin empaquetar, desde su carpeta. Un `.game` empaquetado no tiene archivos en disco que el IDE pueda abrir. +* Un error dentro de `singeMain` se detiene en `Framework.singe`, que ejecuta `singeMain` como una corrutina; el mensaje indica la línea de tu script. +* Mientras el IDE tiene el juego detenido, la ventana no se redibuja, y cerrarla espera hasta que continúes. Parar proceso (`Shift+F5`) termina el juego al instante. +* Singe detiene un script que nunca devuelve el control cuando se cierra su ventana, pero no bajo el depurador, que es dueño del script mientras se ejecuta; Ruptura encuentra dónde está atascado. + [[deterministic]] === Modo de prueba determinista @@ -2682,6 +2797,7 @@ instalar ningún software adicional: | lume | `require("lume")` para las cosas pequeñas que necesita todo juego: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice` | inspect | `require("inspect")` para imprimir una tabla con una forma que una persona pueda leer, para cuando `debugPrint` es el depurador | bump | `require("bump")` para mover cosas que no deben atravesarse unas a otras -- responde dónde acaba algo que se mueve, cosa que <> no hacen a propósito +| MobDebug | `require("mobdebug")`, el depurador con el que habla <>; `--debug` lo arranca, así que un juego no necesita ninguna línea propia | RmlUi | El modelo de objetos propio de la GUI, la variable global `rmlui` (véase <>) |=== @@ -2805,7 +2921,7 @@ este rechaza, así que entre los dos cubren: * Módulos de tracker mediante libxmp: MOD, S3M, XM e IT, y otros cincuenta y tantos, entre ellos 669, AMF, MED, OKT, PTM, STM y ULT. * Chiptunes mediante game-music-emu: NSF y NSFE, SPC, VGM, GBS, AY, GYM, HES, - KSS y SAP. La forma VGZ, comprimida con gzip, no se lee; descomprímela antes a VGM. + KSS y SAP. La forma VGZ, comprimida con gzip, no se lee; descomprímela antes a VGM. Un chiptune que no dice cuánto dura suena hasta quedarse en silencio, lo que puede no ocurrir nunca, así que se decodifica mientras suena en lugar de por adelantado. * MIDI, sintetizado a partir de una SoundFont. Véase <>. La línea `Audio:` de la cabecera de la traza (`--program` o `--trace`) enumera los @@ -4591,7 +4707,7 @@ Extensión de Hypseus, se acepta y se ignora. Consulta <> detiene el juego en un punto de ruptura, muestra qué hay en sus variables y lo recorre línea a línea. [[traceheader]] ==== La cabecera de la traza @@ -7704,6 +7820,38 @@ end nodeSetMesh(playerCar, carMesh, teamLooks[1]) ---- +[#materialsetcutoff] +==== materialSetCutoff + +[source,text] +---- +materialSetCutoff(material, cutoff) +---- + +Descarta cada texel cuyo alfa queda por debajo de `cutoff` y dibuja el resto totalmente opaco, en la pasada iluminada y en la de sombras por igual, así que una tarjeta de hojas proyecta la sombra de sus hojas en lugar de la de su quad. Es el enmascarado alfa de glTF: a diferencia de <> no necesita ordenación, pero un texel se conserva entero o se descarta, nunca se mezcla. El umbral se limita a entre `0` y `1`, y `0`, el valor por defecto, desactiva el enmascarado. Un modelo cargado desde glTF llega con el umbral que su archivo da a un material `MASK`. + +*Parámetros:* + +* `material` -- handle entero de material. +* `cutoff` -- número; el alfa, de `0` a `1`, que necesita un texel para dibujarse. + +*Desde:* 3.00. +*Véase también:* <>, <>, <> + +.Ejemplo +[source,lua] +---- +-- Una valla de malla: un solo quad con textura, los huecos recortados, las dos caras dibujadas. +fence = materialNew() +materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png")) +materialSetCutoff(fence, 0.5) +materialSetDoubleSided(fence, true) +panel = nodeNew() +nodeSetMesh(panel, meshPlane(4, 2), fence) +nodeSetPosition(panel, 0, 1, -5) +nodeSetRotation(panel, 90, 0, 0) +---- + [#materialsetdoublesided] ==== materialSetDoubleSided @@ -9701,7 +9849,7 @@ end id = musicLoad(filename) ---- -Extensión de Hypseus. Carga una pista de música mediante el sistema de archivos virtual y devuelve su handle. El archivo entero se decodifica de antemano, como un sonido, así que una pista larga cuesta memoria; el handle es válido hasta <> o una recarga. +Extensión de Hypseus. Carga una pista de música mediante el sistema de archivos virtual y devuelve su handle. El archivo entero se decodifica de antemano, como un sonido, así que una pista larga cuesta memoria, salvo un chiptune que no dice cuánto dura o algo que decodificado pasaría de 512 MB, que se decodifican mientras suenan; el handle es válido hasta <> o una recarga. Un archivo que falta, o que ningún decodificador incluido en la compilación puede leer, termina el script, como hace <>. @@ -16986,6 +17134,9 @@ xref:Manual.adoc#onlineservice[En línea: cuentas, juegos y puntuaciones]). Un j | `scoreUpdate()` | Una vez por fotograma, desde `onOverlayUpdate`. Sin ella no avanza nada. + +| `scoreGameId()` +| El `GAME_ID` del juego de su entrada en `games.dat`, o `nil` si no tiene. Un juego sin él se puede jugar, pero no clasificar. |=== **Una puntuación se pone en cola, no se envía.** Se escribe en el disco y se entrega al @@ -19533,6 +19684,24 @@ processor = voiceProcessorNew([frame [, rate [, tailMs]]]) *Desde:* 3.00. +[#frameworkhelpers] +=== Funciones auxiliares del framework + +`Framework.singe` define cinco pequeñas funciones auxiliares propias, globales +como el resto de la API, que usan los juegos escritos para versiones +anteriores. El código nuevo puede preferir `lume` e `inspect`, de +<<_included_libraries,las bibliotecas incluidas>>. + +[cols="1,2",options="header"] +|=== +| Función | Qué hace +| `utilDeepCopy(value)` | Una copia de `value` hasta el fondo: una tabla se copia clave por clave, y sus claves, sus valores y su metatabla se copian a su vez; cualquier otra cosa vuelve tal cual. +| `utilDump(value)` | `value` como texto para imprimir. Una tabla se vuelve `{ ["key"] = value,}`, las tablas anidadas entre sus propias llaves; cualquier otra cosa pasa por `tostring`. Para una tabla que merezca leerse, `require("inspect")` lo hace mejor. +| `utilGetTableSize(t)` | Cuántas claves tiene `t`, todas, no solo la secuencia que mide `#t`. +| `utilOffsetTable(source, offset)` | Una copia profunda de una tabla con la forma de `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, con `offset` sumado a cada `value`. +| `utilTrim(s)` | `s` sin los espacios en blanco de sus dos extremos. +|=== + [#enginecallbacks] === Callbacks del motor diff --git a/docs/es/downloadReadme.txt b/docs/es/downloadReadme.txt index 0a6bf9cf5..616e7fa95 100644 --- a/docs/es/downloadReadme.txt +++ b/docs/es/downloadReadme.txt @@ -1,5 +1,6 @@ Documentación de Singe @VERSION@ +@PREVIEW@ Manual.pdf / .html El manual de usuario, aquí para la revisión; una versión publicada lo lleva dentro de Singe. Reference.pdf / .html Todo lo que puede hacer un juego: el modelo de programación y cada función. Learn.pdf / .html Aprende a programar con Singe: treinta y una lecciones, de cero a un juego terminado. learn/ El script terminado de cada lección, y el kit gráfico que usan las lecciones diff --git a/docs/fr/Manual.adoc b/docs/fr/Manual.adoc index 73c018c99..634a8f948 100644 --- a/docs/fr/Manual.adoc +++ b/docs/fr/Manual.adoc @@ -18,12 +18,13 @@ include::locale.adoc[] [#_about_singe] == À propos de Singe -Singe (SINGE Is Not a Game Emulator, soit « SINGE n'est pas un émulateur de -jeux », et nommé d'après le dragon de Dragon's Lair) est un système de script -basé sur Lua qui permet de prototyper rapidement de nouveaux jeux laserdisc, ou -de créer des jeux entièrement nouveaux. Le langage est facile à apprendre, très -puissant et rapide. Toutes les fonctionnalités nécessaires pour développer votre -propre jeu sont accessibles par une interface de programmation (API) simple. +Singe (Scott's Increasingly Nifty Game Engine, soit « le moteur de jeu de plus +en plus épatant de Scott », et nommé d'après le dragon de Dragon's Lair) est un +système de script basé sur Lua qui permet de prototyper rapidement de nouveaux +jeux laserdisc, ou de créer des jeux entièrement nouveaux. Le langage est facile +à apprendre, très puissant et rapide. Toutes les fonctionnalités nécessaires +pour développer votre propre jeu sont accessibles par une interface de +programmation (API) simple. Singe offre de nombreuses fonctionnalités au développeur de jeux. Parmi les plus intéressantes : @@ -42,8 +43,9 @@ intéressantes : * Prise en charge des périphériques d'entrée analogiques et numériques, pistolets optiques compris * Large prise en charge des formats vidéo et audio -Singe fonctionne sous Windows, macOS et Linux, y compris sur le Raspberry Pi et -sur des consoles portables comme la R36S. +Singe fonctionne sous Windows, macOS et Linux, sur des processeurs ARM comme sur +des processeurs Intel et AMD : PC portables Windows sur ARM, Raspberry Pi et +consoles portables comme la R36S. Côté joueurs, Singe permet de profiter de nombreux jeux non émulés et de jeux nouveaux, depuis le front-end de votre choix ou par le système de menus fourni. @@ -65,7 +67,8 @@ dernière version est la {revnumber}. Pour installer Singe, téléchargez simplement le binaire qui correspond à votre système. Placez-le seul dans un nouveau dossier et lancez-le. Au premier lancement, Singe crée tous les dossiers et fichiers de support nécessaires. Vous -êtes maintenant prêt à installer des jeux. +êtes maintenant prêt à installer des jeux. Sur un PC Windows à processeur ARM, prenez la version `Windows-aarch64` : la +version `Windows-x86_64` y fonctionne aussi, émulée, mais plus lentement. [#_upgrading_singe] === Mettre Singe à jour @@ -957,6 +960,7 @@ Voir <> pour les traces. [cols="1,2",options="header"] |=== | Option | Rôle +| `--debug[=HOST:PORT]` | Connecter le jeu à un débogueur qui écoute sur `HOST:PORT`, ZeroBrane Studio d'ordinaire, qui peut alors l'arrêter sur des points d'arrêt, l'exécuter pas à pas et montrer ce que contiennent ses variables ; voir xref:Reference.adoc#zerobrane[Déboguer dans ZeroBrane Studio]. `HOST` vaut `localhost` par défaut et `PORT` vaut `8172`. Si rien n'écoute, le jeu se joue comme d'habitude une fois la tentative abandonnée, en deux secondes au plus. Par défaut : désactivé. | `--deterministic[=MS]` | Pour les tests seulement : ignorer le temps réel et faire tourner tout le moteur sur une horloge virtuelle qui avance de `MS` millisecondes à chaque image, si bien que le même numéro d'image désigne toujours le même instant. Le disque suit lui aussi l'horloge virtuelle, chacune de ses images durant le temps qu'elle vaut, et les générateurs aléatoires sont initialisés avec le même nombre, si bien qu'une session se répète au pixel près. La valeur est facultative et sert à la fois de pas et de graine, de `1` à `1000`. Par défaut quand l'option est donnée sans valeur : `15`, la durée d'image qu'implique la fréquence propre du moteur. La régulation du rythme n'a aucun sens dans ce mode, le son non plus, sauf si `--record` est aussi donné, auquel cas il est généré au pas de l'image ; voir xref:Reference.adoc#deterministic[Mode de test déterministe]. Par défaut : désactivé. | `-P`, `--pack=DIRECTORY` | Empaqueter le jeu de `DIRECTORY` dans la base de données dont le nom suit les options, puis quitter. Voir xref:Reference.adoc#singlefile[Jeux en un seul fichier]. | `-T`, `--patch=GAME.game` | Remplacer des fichiers du jeu empaqueté à partir du dossier ou de la base de données de correctif dont le nom suit les options, puis quitter. diff --git a/docs/fr/Reference.adoc b/docs/fr/Reference.adoc index 78be697ad..fe056b454 100644 --- a/docs/fr/Reference.adoc +++ b/docs/fr/Reference.adoc @@ -90,6 +90,125 @@ le fichier reste surveillé, donc enregistrer la correction suffit. Comme le rechargement exécute le même nettoyage que lorsqu'on quitte, toute fuite d'un script se voit ici en premier. +[[zerobrane]] +=== Déboguer dans ZeroBrane Studio + +https://studio.zerobrane.com[ZeroBrane Studio] est un IDE Lua gratuit, et Singe +apporte tout ce qu'il faut pour y lancer et y déboguer un jeu : points d'arrêt, +pas à pas, fenêtres Pile d'exécution et Expressions espionnes, un jeu qui reste +immobile tant qu'il est arrêté, un arrêt dans l'IDE quand une erreur va mettre +fin au jeu, et l'autocomplétion avec des infobulles pour toute l'API, en +anglais, en français, en italien, en allemand ou en espagnol. La partie +débogueur est MobDebug, intégré à Singe et démarré par `--debug`, si bien qu'un +jeu n'a besoin d'aucune ligne à lui ; tout autre outil qui parle le protocole de +MobDebug peut se connecter de la même façon. + +[[zerobranesetup]] +==== Mise en place + +Singe décompresse un paquet pour ZeroBrane Studio dans son dossier de support, +sous `Singe/ZeroBrane/singe.lua`, et le tient à jour d'une version à l'autre. +Indiquez à ZeroBrane Studio où il se trouve par une seule ligne dans son +`user.lua` (Édition > Préférences > Paramètres : Utilisateur), puis +redémarrez-le : + +[source,lua] +---- +package "/home/me/Singe/Singe/ZeroBrane/singe.lua" +---- + +Singe apparaît alors dans Projet > Interpréteur Lua, avec Projet > Déboguer avec +Singe et un bouton Singe dans la barre d'outils pour lancer un jeu sous le +débogueur en une seule étape. Les fichiers `.singe` s'ouvrent en Lua, les +documents `.rml` en XML et les feuilles de style `.rcss` en CSS. Faites du +dossier du jeu le projet (Projet > Répertoire de projet > Choisir...). Le +paquet cherche Singe dans ce dossier et dans les trois au-dessus, en prenant la +version la plus récente pour votre machine, puis dans le `PATH` ; s'il n'en +trouve aucun, il vous le demande la première fois que vous lancez un jeu, et +s'en souvient. Projet > Choisir l'exécutable de Singe... en désigne un autre, et +`path.singe = "/path/to/Singe"` dans `user.lua` l'emporte sur les deux. + +[[zerobranerun]] +==== Lancer et déboguer + +Projet > Exécuter (`F6`) lance le jeu, et Projet > Lancer le débogage (`F5`) le +lance sous le débogueur. Le script est le point d'entrée du projet s'il y en a +un (faites un clic droit dessus dans l'arborescence du projet et choisissez +Définir le fichier en tant que point d'entrée), et sinon le fichier en cours +d'édition ; quand celui-ci est un des modules du jeu plutôt qu'un fichier +`.singe`, c'est l'unique fichier `.singe` du projet qui est lancé. Singe démarre +dans le dossier qui contient son exécutable, comme le menu le lancerait, si +bien que `Singe/Framework.singe` et tous les noms relatifs à ce dossier se +résolvent comme d'habitude, et l'entrée `games.dat` du jeu fournit sa vidéo et +ses réglages. Projet > Paramètres de ligne de commande... ajoute vos propres +options, dont `-v` et une vidéo pour un jeu sans entrée. + +Cliquez dans la marge pour poser un point d'arrêt. Le jeu s'y arrête avec ses +variables locales dans la fenêtre Pile d'exécution, la fenêtre Expressions +espionnes évalue des expressions, et Pas à pas détaillé (`F10`), Pas à pas +sommaire (`Shift+F10`) et Pas à pas sortant (`Ctrl+F10`) se comportent comme +dans n'importe quel programme Lua, à l'intérieur de `singeMain` aussi. Projet > +Interrompre arrête un jeu là où il en est. Tant que le jeu est arrêté, le +disque, chaque vidéo et chaque son se mettent en pause exactement comme pour la +touche pause, et reprennent ensuite au même endroit. + +Quand une erreur va mettre fin au jeu -- une erreur Lua, ou un appel du moteur +qui reçoit quelque chose dont il ne peut rien faire --, l'IDE s'arrête d'abord, +dans le `Debugger.singe` du dossier Singe, et la fenêtre Sortie dit ce qui ne +va pas. La fenêtre Pile d'exécution garde sous ce fichier les propres cadres du +script, avec leurs variables locales telles qu'elles étaient. Continuez, et le +jeu se termine comme il l'aurait fait, avec l'erreur et une trace de pile dans +la fenêtre Sortie ; double-cliquez sur une ligne de la trace pour ouvrir le +fichier à cet endroit. + +Le jeu tourne avec `--reload`, si bien qu'enregistrer un script relance le jeu +et le débogueur avec lui, points d'arrêt compris. Projet > Relancer Singe quand +un script change désactive ce comportement. + +[[zerobranecompletion]] +==== Autocomplétion et Référence + +L'autocomplétion propose chaque fonction, callback et constante de l'API, et +une infobulle en donne les arguments et le premier paragraphe de l'entrée. +Projet > Analyser (`Shift+F7`) connaît l'API lui aussi, si bien qu'il signale un appel mal orthographié plutôt que chaque appel. Le +réglage de langue de ZeroBrane Studio choisit la langue des infobulles : +`language = "fr"` dans `user.lua` donne le français, et il en va de même pour +l'italien (`it`), l'allemand (`de`) et l'espagnol (`es`). Faites un clic droit +sur un nom et choisissez Chercher dans la Référence de Singe pour ouvrir son +entrée dans un navigateur ; Aide > Référence de Singe ouvre le début. Les deux +ont besoin du téléchargement de la documentation décompressé à côté de Singe, +sous `Singe-Docs-v3.00` ou `Singe-Docs-v3.00-fr` et ainsi de suite, celui dans +la langue de ZeroBrane Studio étant pris en premier ; +`singe = { reference = "/path/to/Reference.html" }` dans `user.lua` en désigne +un autre. + +[[zerobranecabinet]] +==== Déboguer une borne + +Le débogueur fonctionne à travers un réseau. Sur la machine qui a ZeroBrane +Studio, ouvrez le dossier du jeu comme projet et choisissez Projet > Lancer le +serveur de débogage. Sur la borne, lancez le jeu avec `--debug=HOST`, où `HOST` +est le nom ou l'adresse de cette machine ; le port est `8172` sauf si +`--debug=HOST:PORT` en donne un autre, et la première machine doit accepter les +connexions sur ce port. Une borne qui lance ses jeux depuis le menu peut porter +`debug = "HOST:PORT"` dans son fichier de réglages à la place, et chaque jeu +qu'elle lance se connecte, le menu compris. L'IDE arrête chacun sur sa première +ligne, où Continuer (`F5`) le met en route ; `debugger.runonstart = true` dans +`user.lua` le laisse aller tout droit jusqu'au premier point d'arrêt. La borne +annonce ses propres chemins, que ZeroBrane Studio fait correspondre aux fichiers +du projet la première fois qu'il s'arrête, si bien que le même dossier de jeu +sur les deux machines suffit. Si rien n'écoute, un jeu se joue comme d'habitude +une fois que la tentative de connexion abandonne, ce qui ne prend pas plus de +deux secondes. + +[[zerobranelimits]] +==== Limites + +* Déboguez un jeu non empaqueté, depuis son dossier. Un `.game` empaqueté n'a aucun fichier sur le disque que l'IDE puisse ouvrir. +* Une erreur dans `singeMain` s'arrête dans `Framework.singe`, qui exécute `singeMain` comme une coroutine ; le message donne la ligne de votre script. +* Tant que l'IDE tient le jeu arrêté, la fenêtre ne se redessine pas, et la fermer attend que vous continuiez. Arrêter le processus (`Shift+F5`) termine le jeu aussitôt. +* Singe arrête un script qui ne rend jamais la main quand on ferme sa fenêtre, mais pas sous le débogueur, qui possède le script tant qu'il tourne ; Interrompre trouve où il est bloqué. + [[deterministic]] === Mode de test déterministe @@ -2766,6 +2885,7 @@ installer de logiciel supplémentaire : | lume | `require("lume")` pour les petites choses dont tout jeu a besoin : `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice` | inspect | `require("inspect")` pour afficher une table sous une forme qu'un humain peut lire, pour quand `debugPrint` tient lieu de débogueur | bump | `require("bump")` pour déplacer des choses qui ne doivent pas se traverser -- il répond à la question de savoir où finit un objet qui se déplace, ce que <> ne font délibérément pas +| MobDebug | `require("mobdebug")`, le débogueur auquel parle <> ; `--debug` le démarre, si bien qu'un jeu n'a besoin d'aucune ligne à lui | RmlUi | Le modèle objet propre à l'interface, la variable globale `rmlui` (voir <>) |=== @@ -2901,7 +3021,7 @@ FFmpeg pour tout ce qu'il refuse, si bien qu'à eux deux ils couvrent : d'autres dont 669, AMF, MED, OKT, PTM, STM et ULT. * Les chiptunes via game-music-emu : NSF et NSFE, SPC, VGM, GBS, AY, GYM, HES, KSS et SAP. La forme VGZ compressée par gzip n'est pas lue ; décompressez-la - d'abord en VGM. + d'abord en VGM. Un chiptune qui ne dit pas sa durée joue jusqu'à ce qu'il se taise, ce qui peut ne jamais arriver, si bien qu'il est décodé au fil de la lecture plutôt que d'avance. * Le MIDI, synthétisé à partir d'une SoundFont. Voir <>. La ligne `Audio:` de l'en-tête de la trace (`--program` ou `--trace`) liste les @@ -4691,7 +4811,7 @@ Extension Hypseus, acceptée et ignorée. Voir <> arrête le jeu sur un point d'arrêt, montre ce que contiennent ses variables et le fait avancer ligne par ligne. [[traceheader]] ==== L'en-tête de la trace @@ -7807,6 +7927,38 @@ end nodeSetMesh(playerCar, carMesh, teamLooks[1]) ---- +[#materialsetcutoff] +==== materialSetCutoff + +[source,text] +---- +materialSetCutoff(material, cutoff) +---- + +Jette chaque texel dont l'alpha est inférieur à `cutoff` et dessine le reste entièrement opaque, dans la passe éclairée comme dans la passe d'ombre, si bien qu'une carte de feuillage projette l'ombre de ses feuilles plutôt que celle de son quad. C'est le masquage alpha de glTF : contrairement à <>, il n'a besoin d'aucun tri, mais un texel est gardé entier ou jeté, jamais mélangé. Le seuil est borné entre `0` et `1`, et `0`, la valeur par défaut, désactive le masquage. Un modèle chargé depuis glTF arrive avec le seuil que son fichier donne à un matériau `MASK`. + +*Paramètres :* + +* `material` -- handle de matériau entier. +* `cutoff` -- nombre ; l'alpha, de `0` à `1`, qu'il faut à un texel pour être dessiné. + +*Depuis :* 3.00. +*Voir aussi :* <>, <>, <> + +.Exemple +[source,lua] +---- +-- Un grillage : un seul quad texturé, les trous découpés, les deux faces dessinées. +fence = materialNew() +materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png")) +materialSetCutoff(fence, 0.5) +materialSetDoubleSided(fence, true) +panel = nodeNew() +nodeSetMesh(panel, meshPlane(4, 2), fence) +nodeSetPosition(panel, 0, 1, -5) +nodeSetRotation(panel, 90, 0, 0) +---- + [#materialsetdoublesided] ==== materialSetDoubleSided @@ -9827,7 +9979,7 @@ end id = musicLoad(filename) ---- -Extension Hypseus. Charge une piste de musique par le système de fichiers virtuel et renvoie son handle. Le fichier entier est décodé d'avance, comme pour un son, si bien qu'une longue piste coûte de la mémoire ; le handle est valide jusqu'à <> ou jusqu'à un rechargement. +Extension Hypseus. Charge une piste de musique par le système de fichiers virtuel et renvoie son handle. Le fichier entier est décodé d'avance, comme pour un son, si bien qu'une longue piste coûte de la mémoire, sauf un chiptune qui ne dit pas sa durée ou ce qui ferait plus de 512 Mo une fois décodé, l'un comme l'autre décodé au fil de la lecture ; le handle est valide jusqu'à <> ou jusqu'à un rechargement. Un fichier absent, ou qu'aucun décodeur compilé ne sait lire, met fin au script, comme le fait <>. @@ -17142,6 +17294,9 @@ xref:Manual.adoc#onlineservice[En ligne : comptes, jeux et scores]). Un jeu n'a | `scoreUpdate()` | Une fois par image, depuis `onOverlayUpdate`. Rien n'avance sans lui. + +| `scoreGameId()` +| Le `GAME_ID` du jeu, tiré de son entrée `games.dat`, ou `nil` s'il n'en a pas. Un jeu qui n'en a pas peut être joué, mais pas classé. |=== **Un score est mis en file d'attente, pas envoyé.** Il est écrit sur le disque et remis au service @@ -19684,6 +19839,24 @@ processor = voiceProcessorNew([frame [, rate [, tailMs]]]) *Depuis :* 3.00. +[#frameworkhelpers] +=== Fonctions utilitaires du framework + +`Framework.singe` définit cinq petites fonctions utilitaires à lui, globales +comme le reste de l'API, dont se servent des jeux écrits pour des versions +antérieures. Du code nouveau peut leur préférer `lume` et `inspect`, parmi +<<_included_libraries,les bibliothèques incluses>>. + +[cols="1,2",options="header"] +|=== +| Fonction | Ce qu'elle fait +| `utilDeepCopy(value)` | Une copie de `value` jusqu'au bout : une table est copiée clé par clé, ses clés, ses valeurs et sa métatable copiées à leur tour ; tout le reste revient tel quel. +| `utilDump(value)` | `value` sous forme de texte à afficher. Une table devient `{ ["key"] = value,}`, les tables imbriquées entre leurs propres accolades ; tout le reste passe par `tostring`. Pour une table qui mérite d'être lue, `require("inspect")` fait mieux. +| `utilGetTableSize(t)` | Combien de clés contient `t`, toutes, pas seulement la séquence que mesure `#t`. +| `utilOffsetTable(source, offset)` | Une copie profonde d'une table de la forme de `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, avec `offset` ajouté à chaque `value`. +| `utilTrim(s)` | `s` sans les blancs à ses deux extrémités. +|=== + [#enginecallbacks] === Callbacks du moteur diff --git a/docs/fr/downloadReadme.txt b/docs/fr/downloadReadme.txt index 27bf167b6..ac90d0e80 100644 --- a/docs/fr/downloadReadme.txt +++ b/docs/fr/downloadReadme.txt @@ -1,5 +1,6 @@ Documentation de Singe @VERSION@ +@PREVIEW@ Manual.pdf / .html Le manuel d'utilisation, ici pour la relecture ; une version publiée le porte dans Singe. Reference.pdf / .html Tout ce qu'un jeu peut faire : le modèle de programmation et chaque fonction. Learn.pdf / .html Apprendre à programmer avec Singe : trente et une leçons, de rien à un jeu fini. learn/ Le script terminé de chaque leçon, et le kit graphique des leçons diff --git a/docs/it/Manual.adoc b/docs/it/Manual.adoc index 478642137..ae83aac15 100644 --- a/docs/it/Manual.adoc +++ b/docs/it/Manual.adoc @@ -18,12 +18,13 @@ include::locale.adoc[] [#_about_singe] == Informazioni su Singe -Singe (SINGE Is Not a Game Emulator, «SINGE non è un emulatore di giochi», e -chiamato così dal drago di Dragon's Lair) è un sistema di scripting basato su -Lua che permette di realizzare rapidamente prototipi di nuovi giochi laserdisc, -o di creare giochi del tutto nuovi. Il linguaggio è facile da imparare, molto -potente e veloce. Tutte le funzionalità che servono per sviluppare il tuo gioco -sono disponibili attraverso una semplice interfaccia di programmazione (API). +Singe (Scott's Increasingly Nifty Game Engine, «il motore di gioco sempre più +ingegnoso di Scott», e chiamato così dal drago di Dragon's Lair) è un sistema di +scripting basato su Lua che permette di realizzare rapidamente prototipi di +nuovi giochi laserdisc, o di creare giochi del tutto nuovi. Il linguaggio è +facile da imparare, molto potente e veloce. Tutte le funzionalità che servono +per sviluppare il tuo gioco sono disponibili attraverso una semplice interfaccia +di programmazione (API). Singe offre numerose funzionalità a chi sviluppa giochi. Alcune delle più interessanti sono: @@ -42,8 +43,8 @@ interessanti sono: * Supporto per dispositivi di input analogici e digitali, comprese le pistole ottiche * Ampio supporto per formati video e audio -Singe funziona su Windows, macOS e Linux, compresi il Raspberry Pi e console -portatili come la R36S. +Singe funziona su Windows, macOS e Linux, con processori ARM oltre che Intel e +AMD: portatili Windows su ARM, il Raspberry Pi e console portatili come la R36S. Per chi gioca, Singe permette di godersi molti giochi nuovi e non emulati con qualsiasi front-end, o con il sistema di menu incluso. @@ -65,7 +66,8 @@ prima. Al momento della stesura di questo manuale, l'ultima versione è la Per installare Singe, scarica l'eseguibile adatto al tuo sistema. Mettilo da solo in una cartella nuova e avvialo. Al primo avvio, Singe crea tutte le cartelle e tutti i file di supporto che servono. A questo punto sei pronto per -installare qualche gioco. +installare qualche gioco. Su un PC Windows con processore ARM, prendi la versione `Windows-aarch64`: anche +la versione `Windows-x86_64` ci funziona, emulata, ma più lentamente. [#_upgrading_singe] === Aggiornare Singe @@ -953,6 +955,7 @@ Per le tracce vedi <>. [cols="1,2",options="header"] |=== | Opzione | Scopo +| `--debug[=HOST:PORT]` | Collega il gioco a un debugger in ascolto su `HOST:PORT`, di solito ZeroBrane Studio, che può allora fermarlo ai breakpoint, eseguirlo passo passo e mostrare che cosa c'è nelle sue variabili; vedi xref:Reference.adoc#zerobrane[Fare il debug in ZeroBrane Studio]. `HOST` è `localhost` se non indicato e `PORT` è `8172`. Se nessuno è in ascolto, il gioco si gioca come al solito dopo che il tentativo ha rinunciato, entro due secondi. Predefinito: disattivato. | `--deterministic[=MS]` | Solo per i test: ignora il tempo reale e fa girare l'intero motore su un orologio virtuale che avanza di `MS` millisecondi a ogni fotogramma, così lo stesso numero di fotogramma vuol dire sempre lo stesso istante. Anche il disco segue l'orologio virtuale, con i suoi fotogrammi che durano il tempo che valgono, e i generatori casuali ricevono come seme lo stesso numero, così un'esecuzione si ripete identica fino al pixel. Il valore è facoltativo ed è sia il passo sia il seme, da `1` a `1000`. Predefinito quando l'opzione è data senza valore: `15`, la durata di un fotogramma che deriva dalla frequenza del motore. In questa modalità il ritmo non ha senso, e nemmeno il suono, a meno che non ci sia anche `--record`, nel qual caso viene generato al passo con l'immagine; vedi xref:Reference.adoc#deterministic[Modalità di test deterministica]. Predefinito: disattivato. | `-P`, `--pack=DIRECTORY` | Impacchetta il gioco in `DIRECTORY` nel database indicato dopo le opzioni, poi esce. Vedi xref:Reference.adoc#singlefile[Giochi in un solo file]. | `-T`, `--patch=GAME.game` | Sostituisce dei file nel gioco impacchettato prendendoli dalla cartella o dal database di patch indicato dopo le opzioni, poi esce. diff --git a/docs/it/Reference.adoc b/docs/it/Reference.adoc index a1b524128..724b56d18 100644 --- a/docs/it/Reference.adoc +++ b/docs/it/Reference.adoc @@ -86,6 +86,121 @@ correzione. Poiché il ricaricamento esegue la stessa procedura di smontaggio dell'uscita dal gioco, qualunque risorsa uno script si lasci sfuggire si vede qui per prima. +[[zerobrane]] +=== Fare il debug in ZeroBrane Studio + +https://studio.zerobrane.com[ZeroBrane Studio] è un IDE Lua gratuito, e Singe +porta con sé quello che serve per avviarci un gioco e farne il debug: +breakpoint, esecuzione passo passo, le finestre Stack e Watch, un gioco che +resta fermo finché è fermo il debugger, una sosta nell'IDE quando un errore sta +per chiudere il gioco, e l'autocompletamento con i tooltip per tutta l'API, in +inglese, francese, italiano, tedesco o spagnolo. La parte del debugger è +MobDebug, integrato in Singe e avviato da `--debug`, quindi a un gioco non serve +nessuna riga sua; qualsiasi altro strumento che parli il protocollo di MobDebug +può collegarsi allo stesso modo. + +[[zerobranesetup]] +==== Prepararlo + +Singe decomprime un pacchetto per ZeroBrane Studio nella sua cartella di +supporto, come `Singe/ZeroBrane/singe.lua`, e lo tiene aggiornato da una +versione all'altra. Di' a ZeroBrane Studio dove si trova con una sola riga nel +suo `user.lua` (Modifica > Preferenze > Impostazioni: Utente), poi riavvialo: + +[source,lua] +---- +package "/home/me/Singe/Singe/ZeroBrane/singe.lua" +---- + +Singe compare allora in Progetto > Interprete Lua, con Progetto > Debug con +Singe e un pulsante Singe nella barra degli strumenti per avviare un gioco sotto +il debugger in un solo passo. I file `.singe` si aprono come Lua, i documenti +`.rml` come XML e i fogli di stile `.rcss` come CSS. Fai della cartella del +gioco il progetto (Progetto > Directory del Progetto > Scegli...). Il pacchetto +cerca Singe in quella cartella e nelle tre sopra, prendendo la versione più +recente per la tua macchina, e poi nel `PATH`; se non ne trova nessuno te lo +chiede la prima volta che avvii un gioco, e se lo ricorda. Progetto > Scegli +l'eseguibile di Singe... ne indica un altro, e +`path.singe = "/path/to/Singe"` in `user.lua` ha la meglio su entrambi. + +[[zerobranerun]] +==== Avviare e fare il debug + +Progetto > Lancia (`F6`) avvia il gioco, e Progetto > Inizia il Debug (`F5`) lo +avvia sotto il debugger. Lo script è il file di partenza del progetto, se ce +n'è uno (fai clic destro su di esso nell'albero del progetto e scegli Imposta +come File di Partenza), e altrimenti il file che stai modificando; quando questo +è uno dei moduli del gioco invece di un file `.singe`, parte l'unico file +`.singe` del progetto. Singe parte dalla cartella che contiene il suo +eseguibile, come lo avvierebbe il menu, così `Singe/Framework.singe` e ogni +nome relativo a quella cartella si risolvono come sempre, e la voce del gioco +in `games.dat` fornisce il suo video e le sue impostazioni. Progetto > Parametri +Riga di Comando... aggiunge opzioni tue, tra cui `-v` e un video per un gioco +senza voce. + +Fai clic nel margine per mettere un breakpoint. Il gioco si ferma lì con le sue +variabili locali nella finestra Stack, la finestra Watch valuta espressioni, e +Step Into (`F10`), Step Over (`Shift+F10`) e Step Out (`Ctrl+F10`) si +comportano come in qualsiasi programma Lua, anche dentro `singeMain`. +Progetto > Interrompi ferma un gioco dovunque sia. Finché il gioco è fermo, il +disco, ogni video e ogni suono vanno in pausa proprio come con il tasto di +pausa, e poi riprendono dallo stesso punto. + +Quando un errore sta per chiudere il gioco -- un errore Lua, o una chiamata del +motore che riceve qualcosa che non sa usare -- l'IDE si ferma prima, nel +`Debugger.singe` della cartella Singe, e la finestra Output dice che cosa è +andato storto. La finestra Stack tiene sotto quel file i frame dello script +stesso, con le loro variabili locali com'erano. Continua, e il gioco finisce +come sarebbe finito, con l'errore e un traceback nella finestra Output; fai +doppio clic su una riga del traceback per aprire il file in quel punto. + +Il gioco gira con `--reload`, quindi salvare uno script fa ripartire il gioco e +il debugger con lui, breakpoint compresi. Progetto > Ricarica Singe quando cambia +uno script lo disattiva. + +[[zerobranecompletion]] +==== Autocompletamento e Riferimento + +L'autocompletamento propone ogni funzione, callback e costante dell'API, e un +tooltip ne mostra gli argomenti e il primo paragrafo della voce. +Progetto > Analizza (`Shift+F7`) conosce anch'esso l'API, quindi segnala una chiamata scritta male invece di ogni chiamata. È +l'impostazione della lingua di ZeroBrane Studio a scegliere la lingua dei +tooltip: `language = "fr"` in `user.lua` dà il francese, e lo stesso vale per +l'italiano (`it`), il tedesco (`de`) e lo spagnolo (`es`). Fai clic destro su un +nome e scegli Cerca nel Riferimento di Singe per aprire la sua voce in un +browser; Aiuto > Riferimento di Singe apre l'inizio. Tutti e due hanno bisogno +del download della documentazione decompresso accanto a Singe, come +`Singe-Docs-v3.00` o `Singe-Docs-v3.00-fr` e così via, prendendo per primo +quello nella lingua di ZeroBrane Studio; +`singe = { reference = "/path/to/Reference.html" }` in `user.lua` ne indica un +altro. + +[[zerobranecabinet]] +==== Fare il debug di un cabinato + +Il debugger funziona attraverso una rete. Sulla macchina con ZeroBrane Studio, +apri la cartella del gioco come progetto e scegli Progetto > Avvia Debugger +Server. Sul cabinato, avvia il gioco con `--debug=HOST`, dove `HOST` è il nome o +l'indirizzo di quella macchina; la porta è `8172` a meno che +`--debug=HOST:PORT` non ne dica un'altra, e la prima macchina deve accettare +connessioni su di essa. Un cabinato che avvia i suoi giochi dal menu può avere +invece `debug = "HOST:PORT"` nel suo file delle impostazioni, e ogni gioco che +avvia si collega, menu compreso. L'IDE ferma ciascuno alla sua prima riga, dove +Continua (`F5`) lo fa partire; `debugger.runonstart = true` in `user.lua` lo +lascia andare dritto fino al primo breakpoint. Il cabinato comunica i propri +percorsi, che ZeroBrane Studio fa corrispondere ai file del progetto la prima +volta che si ferma, così basta la stessa cartella del gioco su entrambe le +macchine. Se nessuno è in ascolto, un gioco si gioca come al solito una volta +che il tentativo di collegarsi rinuncia, il che non richiede più di due secondi. + +[[zerobranelimits]] +==== Limiti + +* Fai il debug di un gioco non impacchettato, dalla sua cartella. Un `.game` impacchettato non ha file su disco che l'IDE possa aprire. +* Un errore dentro `singeMain` si ferma in `Framework.singe`, che esegue `singeMain` come una coroutine; il messaggio indica la riga del tuo script. +* Finché l'IDE tiene il gioco fermo, la finestra non si ridisegna, e chiuderla aspetta che tu continui. Ferma il processo (`Shift+F5`) chiude subito il gioco. +* Singe ferma uno script che non restituisce mai il controllo quando si chiude la sua finestra, ma non sotto il debugger, che possiede lo script mentre gira; Interrompi trova dove è bloccato. + [[deterministic]] === Modalità di test deterministica @@ -2695,6 +2810,7 @@ software: | lume | `require("lume")` per le piccole cose che servono a ogni gioco: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice` | inspect | `require("inspect")` per stampare una tabella in una forma che una persona riesce a leggere, per quando il debugger è `debugPrint` | bump | `require("bump")` per muovere cose che non devono attraversarsi a vicenda -- dice dove va a finire chi si muove, cosa che <> deliberatamente non fanno +| MobDebug | `require("mobdebug")`, il debugger con cui parla <>; `--debug` lo avvia, quindi a un gioco non serve nessuna riga sua | RmlUi | Il modello a oggetti della GUI stessa, la variabile globale `rmlui` (vedi <>) |=== @@ -2824,7 +2940,7 @@ tutto ciò che quello rifiuta, quindi insieme coprono: * Moduli tracker tramite libxmp: MOD, S3M, XM e IT, e una cinquantina di altri, tra cui 669, AMF, MED, OKT, PTM, STM e ULT. * Chiptune tramite game-music-emu: NSF e NSFE, SPC, VGM, GBS, AY, GYM, HES, KSS - e SAP. La forma VGZ compressa con gzip non viene letta; decomprimila prima in VGM. + e SAP. La forma VGZ compressa con gzip non viene letta; decomprimila prima in VGM. Un chiptune che non dice quanto dura suona finché non tace, il che può non accadere mai, quindi viene decodificato man mano che suona invece che subito. * MIDI, sintetizzato da un SoundFont. Vedi <>. La riga `Audio:` dell'intestazione della traccia (`--program` o `--trace`) elenca @@ -4620,7 +4736,7 @@ Estensione di Hypseus, accettata e ignorata. Vedi <> ferma il gioco a un breakpoint, mostra che cosa c'è nelle sue variabili e lo fa avanzare una riga alla volta. [[traceheader]] ==== L'intestazione della traccia @@ -7736,6 +7852,38 @@ end nodeSetMesh(playerCar, carMesh, teamLooks[1]) ---- +[#materialsetcutoff] +==== materialSetCutoff + +[source,text] +---- +materialSetCutoff(material, cutoff) +---- + +Scarta ogni texel il cui alfa è sotto `cutoff` e disegna il resto del tutto opaco, nella passata illuminata come in quella delle ombre, così una card di foglie proietta l'ombra delle foglie invece che quella del suo quad. È il mascheramento alfa di glTF: a differenza di <> non ha bisogno di ordinamento, ma un texel è tenuto intero o scartato, mai miscelato. La soglia è limitata tra `0` e `1`, e `0`, il valore predefinito, disattiva il mascheramento. Un modello caricato da glTF arriva con la soglia che il suo file dà a un materiale `MASK`. + +*Parametri:* + +* `material` -- handle intero del materiale. +* `cutoff` -- numero; l'alfa, da `0` a `1`, che serve a un texel per essere disegnato. + +*Disponibile da:* 3.00. +*Vedi anche:* <>, <>, <> + +.Esempio +[source,lua] +---- +-- Una rete metallica: un solo quad con texture, i buchi ritagliati, disegnata su entrambi i lati. +fence = materialNew() +materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png")) +materialSetCutoff(fence, 0.5) +materialSetDoubleSided(fence, true) +panel = nodeNew() +nodeSetMesh(panel, meshPlane(4, 2), fence) +nodeSetPosition(panel, 0, 1, -5) +nodeSetRotation(panel, 90, 0, 0) +---- + [#materialsetdoublesided] ==== materialSetDoubleSided @@ -9751,7 +9899,7 @@ end id = musicLoad(filename) ---- -Estensione di Hypseus. Carica un brano musicale attraverso il file system virtuale e ne restituisce l'handle. L'intero file viene decodificato subito, come un suono, quindi un brano lungo costa memoria; l'handle è valido fino a <> o a un ricaricamento. +Estensione di Hypseus. Carica un brano musicale attraverso il file system virtuale e ne restituisce l'handle. L'intero file viene decodificato subito, come un suono, quindi un brano lungo costa memoria, tranne un chiptune che non dice quanto dura, o ciò che decodificato supererebbe i 512 MB, che vengono decodificati man mano che suonano; l'handle è valido fino a <> o a un ricaricamento. Un file mancante, o che nessun decoder compilato nella build sa leggere, termina lo script, come fa <>. @@ -17043,6 +17191,9 @@ classifica. Vedi <> per il formato. | `scoreUpdate()` | Una volta per fotogramma, da `onOverlayUpdate`. Senza di essa non va avanti niente. + +| `scoreGameId()` +| Il `GAME_ID` del gioco dalla sua voce in `games.dat`, o `nil` se non ne ha. Un gioco che non ce l'ha si può giocare, ma non classificare. |=== **Un punteggio viene messo in coda, non inviato.** Viene scritto su disco e consegnato al @@ -19588,6 +19739,24 @@ processor = voiceProcessorNew([frame [, rate [, tailMs]]]) *Disponibile da:* 3.00. +[#frameworkhelpers] +=== Funzioni di utilità del framework + +`Framework.singe` definisce cinque piccole funzioni di utilità sue, globali come +il resto dell'API, che usano i giochi scritti per le versioni precedenti. Il +codice nuovo può preferire `lume` e `inspect`, tra +<<_included_libraries,le librerie incluse>>. + +[cols="1,2",options="header"] +|=== +| Funzione | Che cosa fa +| `utilDeepCopy(value)` | Una copia di `value` fino in fondo: una tabella è copiata chiave per chiave, e le sue chiavi, i suoi valori e la sua metatabella sono copiati a loro volta; tutto il resto torna così com'è. +| `utilDump(value)` | `value` come testo da stampare. Una tabella diventa `{ ["key"] = value,}`, le tabelle annidate tra parentesi graffe proprie; tutto il resto passa da `tostring`. Per una tabella che valga la pena di leggere, `require("inspect")` fa di meglio. +| `utilGetTableSize(t)` | Quante chiavi contiene `t`, tutte, non solo la sequenza che misura `#t`. +| `utilOffsetTable(source, offset)` | Una copia profonda di una tabella con la forma di `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, con `offset` aggiunto a ogni `value`. +| `utilTrim(s)` | `s` senza gli spazi bianchi alle due estremità. +|=== + [#enginecallbacks] === Callback del motore diff --git a/docs/it/downloadReadme.txt b/docs/it/downloadReadme.txt index f681a89ec..9b8dfa371 100644 --- a/docs/it/downloadReadme.txt +++ b/docs/it/downloadReadme.txt @@ -1,5 +1,6 @@ Documentazione di Singe @VERSION@ +@PREVIEW@ Manual.pdf / .html Il manuale d'uso, qui per la revisione; una versione pubblicata lo porta dentro Singe. Reference.pdf / .html Tutto ciò che un gioco può fare: il modello di programmazione e ogni funzione. Learn.pdf / .html Impara a programmare con Singe: trentuno lezioni, da zero a un gioco finito. learn/ Lo script finito di ogni lezione, e il kit grafico che le lezioni usano diff --git a/src/debugger.c b/src/debugger.c new file mode 100644 index 000000000..0e998a446 --- /dev/null +++ b/src/debugger.c @@ -0,0 +1,166 @@ +/* + * + * Singe 3 + * Copyright (C) 2006-2026 Scott Duensing + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + */ + + +// Singe: the bridge to a Lua debugger. With --debug, every Lua state a game runs in connects to +// ZeroBrane Studio -- or anything else that speaks MobDebug's protocol -- as it is made, so a game +// needs no line of its own to be debugged: MobDebug is embedded, the IDE is found where --debug +// says, and breakpoints work inside coroutines, which is where singeMain runs. While the IDE has +// the script stopped, MobDebug calls _yield every few hundredths of a second; the window is kept +// answering, and the engine is asked to hold the disc and the sounds still until the script hands +// control back. An error that is about to end the game stops in the IDE first, in the Singe +// folder's Debugger.singe, with the script's stack still underneath to look at. + + +#include + +#include "../thirdparty/lua/src/lauxlib.h" + +#include "debugger.h" +#include "util.h" + + +static int32_t _yield(lua_State *L); + + +static DebuggerHoldT _hold = NULL; +static bool _holding = false; // The IDE has stopped the script since it last returned to the engine. +static lua_State *_state = NULL; // The main thread of the attached state. +static int32_t _stopRef = LUA_NOREF; // The loaded Debugger.singe, in the attached state's registry. +static bool _stopping = false; // Inside the stop, so an error there is not stopped on again. + + +// MobDebug's yield callback, made while the IDE has the script stopped: the first one holds the +// world still, and every one keeps the window answering the desktop. +static int32_t _yield(lua_State *L) { + (void)L; + if (!_holding && (_hold != NULL)) { + _holding = true; + _hold(true); + } + SDL_PumpEvents(); + + return 0; +} + + +// MobDebug, required by the state itself so it comes from the embedded copy, is told to call back +// while it waits, to follow coroutines, and to connect. False, and the game runs undebugged, when +// nothing answers at host:port. +bool debuggerAttach(lua_State *L, const char *host, int32_t port, const char *stopFile, DebuggerHoldT hold) { + int32_t top = lua_gettop(L); + bool connected = false; + + debuggerDetach(_state); + _hold = hold; + lua_getglobal(L, "require"); + lua_pushstring(L, "mobdebug"); + if (lua_pcall(L, 1, 1, 0) != LUA_OK) { + utilSay("Debugger: %s", lua_tostring(L, -1)); + lua_settop(L, top); + return false; + } + lua_pushcfunction(L, _yield); + lua_setfield(L, -2, "yield"); + // Every coroutine made from here on is debugged, singeMain's among them. + lua_getfield(L, -1, "coro"); + if (lua_pcall(L, 0, 0, 0) != LUA_OK) { + utilSay("Debugger: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + lua_getfield(L, -1, "start"); + lua_pushstring(L, host); + lua_pushinteger(L, port); + if (lua_pcall(L, 2, 1, 0) == LUA_OK) { + connected = lua_toboolean(L, -1); + } else { + utilSay("Debugger: %s", lua_tostring(L, -1)); + } + lua_settop(L, top); + if (!connected) { + utilSay("Debugger: nothing answered at %s:%d, so the game runs without it.", host, port); + return false; + } + + _state = L; + // Without the file, errors still end the game; they just do not stop in the IDE first. + if (luaL_loadfilex(L, stopFile, "t") == LUA_OK) { + _stopRef = luaL_ref(L, LUA_REGISTRYINDEX); + } else { + utilSay("Debugger: %s; errors will not stop in the IDE.", lua_tostring(L, -1)); + lua_pop(L, 1); + } + + return true; +} + + +// The state is about to close: forget it, and let go of the world if the IDE was holding it. +void debuggerDetach(lua_State *L) { + if ((L == NULL) || (L != _state)) { + return; + } + debuggerRelease(); + _state = NULL; + _stopRef = LUA_NOREF; +} + + +// True when L, or the coroutine it is, belongs to the state the IDE is attached to. +bool debuggerIsAttached(lua_State *L) { + lua_State *mainThread = NULL; + + if (_state == NULL) { + return false; + } + lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD); + mainThread = lua_tothread(L, -1); + lua_pop(L, 1); + + return (mainThread == _state); +} + + +// The script has handed control back to the engine, so whatever the IDE held goes on. +void debuggerRelease(void) { + if (_holding) { + _holding = false; + _hold(false); + } +} + + +// Runs Debugger.singe on the erring thread: it asks MobDebug to stop on its next line, so the IDE +// opens there with the script's own frames below it. Called from the message handler and before +// an API error ends the game; returns when the IDE says to go on. +void debuggerStopOnError(lua_State *L, const char *message) { + if ((_stopRef == LUA_NOREF) || _stopping || !debuggerIsAttached(L)) { + return; + } + _stopping = true; + lua_rawgeti(L, LUA_REGISTRYINDEX, _stopRef); + lua_pushstring(L, (message != NULL) ? message : "(error object is not a string)"); + if (lua_pcall(L, 1, 0, 0) != LUA_OK) { + lua_pop(L, 1); + } + _stopping = false; +} diff --git a/src/debugger.h b/src/debugger.h new file mode 100644 index 000000000..56afa58af --- /dev/null +++ b/src/debugger.h @@ -0,0 +1,52 @@ +/* + * + * Singe 3 + * Copyright (C) 2006-2026 Scott Duensing + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + */ + + +#ifndef DEBUGGER_H +#define DEBUGGER_H + + +#include +#include + +#include "../thirdparty/lua/src/lua.h" + + +#define DEBUGGER_HOST_DEFAULT "localhost" +#define DEBUGGER_PORT_DEFAULT 8172 // MobDebug's, and so ZeroBrane Studio's +#define DEBUGGER_PORT_MAX 65535 +#define DEBUGGER_STOP_FILE "Debugger.singe" // In the Singe folder: where an error stops in the IDE + + +// Called with true when the IDE stops the script, and with false once the script has handed control +// back to the engine, so the engine can hold the world still in between. +typedef void (*DebuggerHoldT)(bool hold); + + +bool debuggerAttach(lua_State *L, const char *host, int32_t port, const char *stopFile, DebuggerHoldT hold); +void debuggerDetach(lua_State *L); +bool debuggerIsAttached(lua_State *L); +void debuggerRelease(void); +void debuggerStopOnError(lua_State *L, const char *message); + + +#endif // DEBUGGER_H diff --git a/src/decode.c b/src/decode.c index 88ead55e3..ecc9584c5 100644 --- a/src/decode.c +++ b/src/decode.c @@ -68,13 +68,14 @@ typedef struct { } SourceT; -static bool _appendAudio(uint8_t **pcm, size_t *bytes, size_t *room, const uint8_t *samples, size_t length); -static int _avioRead(void *opaque, uint8_t *buffer, int size); -static int64_t _avioSeek(void *opaque, int64_t offset, int whence); -static void _close(SourceT *source); -static AVFrame *_firstFrame(SourceT *source); -static float _linear(float value); -static bool _open(SourceT *source, const void *bytes, size_t size, enum AVMediaType type); +static bool _appendAudio(uint8_t **pcm, size_t *bytes, size_t *room, const uint8_t *samples, size_t length); +static int _avioRead(void *opaque, uint8_t *buffer, int size); +static int64_t _avioSeek(void *opaque, int64_t offset, int whence); +static void _close(SourceT *source); +static AVFrame *_firstFrame(SourceT *source); +static float _linear(float value); +static MIX_Audio *_mixerAudio(MIX_Mixer *mixer, void **bytes, size_t size); +static bool _open(SourceT *source, const void *bytes, size_t size, enum AVMediaType type); // ===== Internal helpers ===== @@ -216,6 +217,42 @@ static float _linear(float value) { // Demuxes the bytes and opens a decoder for the best stream of the wanted kind. +// SDL_mixer's own decoders, handed the file in memory. Audio whose length the decoder can tell is +// decoded whole up front, so playing it costs nothing. Audio whose length it cannot tell -- a +// chiptune with no time in it plays until it falls silent, and that can be never -- is kept as it +// came and decoded as it plays, which is how SDL_mixer already keeps audio it knows to be endless: +// decoded whole, one 749-byte NSF became 2 GB of samples and took eight minutes to load. So is +// audio that would decode to more than AUDIO_MAX_BYTES. In that case, and only then, the bytes +// become the audio's and *bytes is set NULL. +static MIX_Audio *_mixerAudio(MIX_Mixer *mixer, void **bytes, size_t size) { + MIX_Audio *audio = MIX_LoadAudioNoCopy(mixer, *bytes, size, false); + SDL_IOStream *io = NULL; + SDL_AudioSpec spec; + int64_t frames = 0; + bool wholeNow = false; + + if (audio == NULL) { + return NULL; + } + frames = MIX_GetAudioDuration(audio); + wholeNow = (frames >= 0) && MIX_GetAudioFormat(audio, &spec) && ((uint64_t)frames * SDL_AUDIO_FRAMESIZE(spec) <= AUDIO_MAX_BYTES); + MIX_DestroyAudio(audio); + if (!wholeNow) { + audio = MIX_LoadAudioNoCopy(mixer, *bytes, size, true); + if (audio != NULL) { + *bytes = NULL; + } + return audio; + } + io = SDL_IOFromConstMem(*bytes, size); + if (io == NULL) { + return NULL; + } + + return MIX_LoadAudio_IO(mixer, io, true, true); +} + + static bool _open(SourceT *source, const void *bytes, size_t size, enum AVMediaType type) { const AVCodec *codec = NULL; unsigned char *buffer = NULL; @@ -382,41 +419,31 @@ MIX_Audio *decodeAudioIO(MIX_Mixer *mixer, SDL_IOStream *io, bool closeio) { MIX_Audio *audio = NULL; void *bytes = NULL; size_t size = 0; - uint8_t header[MIDI_PEEK_BYTES]; bool midi = false; - // A MIDI file is synthesized rather than decoded, and the mixer's own MIDI decoder wants a GUS - // patch set almost nobody installs, so it is recognized before the mixer ever sees the stream. - if (SDL_ReadIO(io, header, sizeof(header)) == sizeof(header)) { - midi = midiIs(header, sizeof(header)); - } - if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) < 0) { - if (closeio) { - SDL_CloseIO(io); - } + // Read once: SDL_mixer is asked about the file in memory, and what it turns down is decoded from + // the same bytes. + bytes = SDL_LoadFile_IO(io, &size, closeio); + if (bytes == NULL) { return NULL; } + // A MIDI file is synthesized rather than decoded, and the mixer's own MIDI decoder wants a GUS + // patch set almost nobody installs, so it is recognized before the mixer ever sees the bytes. + midi = (size >= MIDI_PEEK_BYTES) && midiIs(bytes, MIDI_PEEK_BYTES); if (!midi) { - audio = MIX_LoadAudio_IO(mixer, io, true, false); + audio = _mixerAudio(mixer, &bytes, size); } if (audio == NULL) { // SDL_mixer reads a fixed set of formats. Anything else -- AAC, ALAC, AC-3, WMA and the // rest -- is decoded whole to float samples and handed over as raw PCM, which is what the // mixer would have made of it anyway. - if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) >= 0) { - bytes = SDL_LoadFile_IO(io, &size, false); - if (bytes != NULL) { - if (midi ? midiRender(bytes, size, &decoded) : decodeAudio(bytes, size, &decoded)) { - audio = MIX_LoadRawAudio(mixer, decoded.pcm, decoded.bytes, &decoded.spec); - decodeFreeAudio(&decoded); - } - SDL_free(bytes); - } + if (midi ? midiRender(bytes, size, &decoded) : decodeAudio(bytes, size, &decoded)) { + audio = MIX_LoadRawAudio(mixer, decoded.pcm, decoded.bytes, &decoded.spec); + decodeFreeAudio(&decoded); } } - if (closeio) { - SDL_CloseIO(io); - } + // NULL when the audio kept the bytes. + SDL_free(bytes); return audio; } diff --git a/src/embedded.h b/src/embedded.h index 9b3a3725d..106134cce 100644 --- a/src/embedded.h +++ b/src/embedded.h @@ -31,6 +31,17 @@ #include "generated/missing_png.h" #include "generated/singeLogo.h" #include "generated/Framework_singe.h" +#include "generated/Debugger_singe.h" +#include "generated/zbstudio_singe_lua.h" +#include "generated/zbstudio_api_lua.h" +#include "generated/zbstudio_api_fr_lua.h" +#include "generated/zbstudio_api_it_lua.h" +#include "generated/zbstudio_api_de_lua.h" +#include "generated/zbstudio_api_es_lua.h" +#include "generated/zbstudio_icon_16_png.h" +#include "generated/zbstudio_icon_24_png.h" +#include "generated/zbstudio_icon_32_png.h" +#include "generated/zbstudio_icon_48_png.h" #include "generated/controls_cfg.h" #include "generated/settings_cfg.h" #include "generated/Menu_singe.h" @@ -94,6 +105,9 @@ #include "generated/inspect_lua.h" #include "generated/bump_lua.h" +// MobDebug, for --debug +#include "generated/mobdebug_lua.h" + // Copas #include "generated/copas_lua.h" #include "generated/copas_ftp_lua.h" diff --git a/src/main.c b/src/main.c index 092f80a42..61b4c39fa 100644 --- a/src/main.c +++ b/src/main.c @@ -57,6 +57,7 @@ #include "../thirdparty/uthash/src/utlist.h" +#include "debugger.h" #include "main.h" #include "midi.h" #include "mic.h" @@ -126,6 +127,7 @@ typedef enum LongOptionE { OPT_ABSOLUTES_ONLY = 256, OPT_ALTAUDIO, OPT_APIVERSION, + OPT_DEBUG, OPT_DEINTERLACE, OPT_DETERMINISTIC, OPT_FVALUE, @@ -219,6 +221,7 @@ static const OptionT _options[] = { { OPT_ABSOLUTES_ONLY, "absolutes_only", ap_no, NULL, "keep only the mice that report absolute positions, which is what light guns do", false, true }, { OPT_ALTAUDIO, "altaudio", ap_yes, "SUFFIX", "play .ogg beside the disc video in place of its own audio", false, true }, { OPT_APIVERSION, "apiversion", ap_no, NULL, "print one machine readable version line and exit", false, false }, + { OPT_DEBUG, "debug", ap_maybe, "HOST:PORT", "debug the game in ZeroBrane Studio, or another MobDebug debugger, listening at HOST:PORT (localhost:8172 when left out)", false, true }, { OPT_DEINTERLACE, "deinterlace", ap_yes, "MODE", "what to do with an interlaced picture: off, auto or on", false, true }, { OPT_DETERMINISTIC, "deterministic", ap_maybe, "MS", "for testing: ignore real time and move the clock MS milliseconds each frame, seeding the random generators from the same number", false, true }, { OPT_FVALUE, "fvalue", ap_yes, "NUMBER", "one number handed to the game, which reads it with getFValue()", false, true }, @@ -311,6 +314,7 @@ static int32_t _optionIndex(int32_t code); static int32_t _optionNamed(const char *name); static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]); static bool _parseBoolean(const char *text, bool *value); +static bool _parseDebugTarget(const char *text, char **host, int32_t *port); static bool _parseFloat(const char *text, double *value); static bool _parseGamepadOrder(const char *text); static bool _parseInteger(const char *text, int32_t *value); @@ -695,6 +699,15 @@ static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, cons _showApiVersion(); break; + // Debugger + case OPT_DEBUG: + free(conf->debugHost); + conf->debugHost = NULL; + if (!_parseDebugTarget(arg, &conf->debugHost, &conf->debugPort)) { + _optionFail(exeName, source, utilCreateString("--debug takes HOST, HOST:PORT or :PORT, with a port from 1 to %d.", DEBUGGER_PORT_MAX)); + } + break; + // Reproducible Runs case OPT_DETERMINISTIC: conf->deterministic = true; @@ -1530,6 +1543,7 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) conf->joyMouseRange = JOY_MOUSE_RANGE_DEFAULT; conf->deinterlace = DEINTERLACE_AUTO; conf->deterministicStep = FRAME_TICK_MS; + conf->debugPort = DEBUGGER_PORT_DEFAULT; _applyOptions(exeName, conf, argc, (const char **)argv, NULL); @@ -1558,6 +1572,48 @@ static bool _parseBoolean(const char *text, bool *value) { } +// --debug's HOST:PORT, where the debugger listens. Either half may be left out, and an IPv6 address +// goes in brackets when it carries a port: [::1]:8172. False for a port out of range or a stray +// bracket; *host is only set on success. +static bool _parseDebugTarget(const char *text, char **host, int32_t *port) { + const char *colon = NULL; + const char *close = NULL; + char *name = NULL; + + *port = DEBUGGER_PORT_DEFAULT; + if ((text == NULL) || (*text == 0)) { + *host = strdup(DEBUGGER_HOST_DEFAULT); + return true; + } + if (text[0] == '[') { + close = strchr(text, ']'); + if ((close == NULL) || ((close[1] != 0) && (close[1] != ':'))) { + return false; + } + name = utilStrndup(text + 1, (size_t)(close - text - 1)); + colon = (close[1] == ':') ? close + 1 : NULL; + } else { + colon = strchr(text, ':'); + // Two colons and no brackets is an IPv6 address on its own. + if ((colon != NULL) && (strchr(colon + 1, ':') != NULL)) { + colon = NULL; + } + name = (colon != NULL) ? utilStrndup(text, (size_t)(colon - text)) : strdup(text); + } + if ((colon != NULL) && (!_parseInteger(colon + 1, port) || (*port < 1) || (*port > DEBUGGER_PORT_MAX))) { + free(name); + return false; + } + if (*name == 0) { + free(name); + name = strdup(DEBUGGER_HOST_DEFAULT); + } + *host = name; + + return true; +} + + static bool _parseFloat(const char *text, double *value) { char *end = NULL; double parsed = 0.0; @@ -1770,7 +1826,7 @@ static void _showHeader(void) { // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 utilSay(" ___ ___ _ _ ___ ___"); - utilSay("/ __|_ _| \\| |/ __| __| SINGE Is Not a Game Emulator %s", VERSION_STRING); + utilSay("/ __|_ _| \\| |/ __| __| Scott's Increasingly Nifty Game Engine %s", VERSION_STRING); utilSay("\\__ \\| || .` | (_ | _| Copyright (c) 2006-%s Scott C. Duensing", COPYRIGHT_END_YEAR); utilSay("|___/___|_|\\_|\\___|___| https://KangarooPunch.com https://SingeEngine.com"); utilNewline(); @@ -2025,6 +2081,7 @@ static void _archiveSupport(void) { static void _unpackData(const char *exePath, bool absolute, bool refresh) { const EmbeddedFileT files[] = { { "Framework.singe", Framework_singe, Framework_singe_len }, + { DEBUGGER_STOP_FILE, Debugger_singe, Debugger_singe_len }, { "controls.cfg.example", controls_cfg, controls_cfg_len }, { "settings.cfg.example", settings_cfg, settings_cfg_len }, { "Menu.singe", Menu_singe, Menu_singe_len }, @@ -2055,11 +2112,22 @@ static void _unpackData(const char *exePath, bool absolute, bool refresh) { { "scoreBezel.rml", scoreBezel_rml, scoreBezel_rml_len }, { "scoreBezel.rcss", scoreBezel_rcss, scoreBezel_rcss_len }, { "subtitle.rml", subtitle_rml, subtitle_rml_len }, - { "subtitle.rcss", subtitle_rcss, subtitle_rcss_len } + { "subtitle.rcss", subtitle_rcss, subtitle_rcss_len }, + { "ZeroBrane/singe.lua", zbstudio_singe_lua, zbstudio_singe_lua_len }, + { "ZeroBrane/singe/api.lua", zbstudio_api_lua, zbstudio_api_lua_len }, + { "ZeroBrane/singe/api-fr.lua", zbstudio_api_fr_lua, zbstudio_api_fr_lua_len }, + { "ZeroBrane/singe/api-it.lua", zbstudio_api_it_lua, zbstudio_api_it_lua_len }, + { "ZeroBrane/singe/api-de.lua", zbstudio_api_de_lua, zbstudio_api_de_lua_len }, + { "ZeroBrane/singe/api-es.lua", zbstudio_api_es_lua, zbstudio_api_es_lua_len }, + { "ZeroBrane/singe/icon-16.png", zbstudio_icon_16_png, zbstudio_icon_16_png_len }, + { "ZeroBrane/singe/icon-24.png", zbstudio_icon_24_png, zbstudio_icon_24_png_len }, + { "ZeroBrane/singe/icon-32.png", zbstudio_icon_32_png, zbstudio_icon_32_png_len }, + { "ZeroBrane/singe/icon-48.png", zbstudio_icon_48_png, zbstudio_icon_48_png_len } }; int32_t x = 0; char *temp = NULL; char *data = NULL; + char *folder = NULL; bool created = false; // Extract missing or outdated support files. We do this here so they are not generated if launched from a front end. @@ -2074,6 +2142,14 @@ static void _unpackData(const char *exePath, bool absolute, bool refresh) { for (x = 0; x < (int32_t)(sizeof(files) / sizeof(files[0])); x++) { temp = utilCreateString("%s%c%s", VFS_ENGINE_DIRECTORY, utilGetPathSeparator(), files[x].name); + // A file in a folder of its own, the ZeroBrane Studio package's, gets the folder first. + if (strchr(files[x].name, '/') != NULL) { + folder = utilGetUpToLastPathComponent(temp); + if (!utilMkDirP(folder, DIRECTORY_MODE)) { + utilDie("Unable to create %s directory.", folder); + } + free(folder); + } created |= _extractFile(temp, files[x].data, files[x].length); free(temp); } @@ -2282,6 +2358,7 @@ ConfigT *cloneConf(const ConfigT *conf) { c->keymapFile = _cloneString(conf->keymapFile); c->soundfont = _cloneString(conf->soundfont); c->recordFile = _cloneString(conf->recordFile); + c->debugHost = _cloneString(conf->debugHost); c->deinterlace = conf->deinterlace; c->audioSuffix = _cloneString(conf->audioSuffix); c->gamepadOrder = _cloneString(conf->gamepadOrder); @@ -2383,6 +2460,7 @@ void destroyConf(ConfigT **confPointer) { free(conf->keymapFile); free(conf->soundfont); free(conf->recordFile); + free(conf->debugHost); free(conf->audioSuffix); free(conf->gamepadOrder); free(conf->quitChord); diff --git a/src/scriptWatch.c b/src/scriptWatch.c index 1bb6d57fb..85e980d82 100644 --- a/src/scriptWatch.c +++ b/src/scriptWatch.c @@ -28,7 +28,10 @@ // quit is pending and the call has run WATCH_STOP_MS. Setting a hook from outside the running thread is how Lua's own interpreter // stops a script at Ctrl+C; the hook is set and cleared under a lock the main thread also takes // on entry and return, so it never touches a state that has returned or closed. A script that -// returns sooner never has a hook, and plays exactly as it did. +// returns sooner never has a hook, and plays exactly as it did. A state can hold one hook, so the +// watchdog stays out of the way of anyone else's: it arms only when no hook is set, and clears +// only its own. A debugger stopped at a breakpoint for longer than WATCH_ARM_MS would otherwise +// lose its hook for good, and with it every breakpoint after the first. #if !defined(_WIN32) @@ -60,7 +63,7 @@ static void _stopIfAsked(lua_State *L); static int _watchThread(void *data); -static bool _armed = false; // Under _lock: the hook is set on _state. +static bool _armed = false; // Under _lock: the watchdog has acted on this call (hooked it, or found a hook there). static int32_t _depth = 0; // Under _lock: engine-to-script calls under way. static uint64_t _enteredMS = 0; // Under _lock: when the outermost call began. static SDL_Mutex *_lock = NULL; @@ -98,10 +101,11 @@ static void _leave(lua_State *L) { SDL_LockMutex(_lock); _depth--; if (_depth == 0) { - if (_armed) { + // A hook set since the watchdog armed (a debugger started from the script) is not ours to clear. + if (_armed && (lua_gethook(L) == _hook)) { lua_sethook(L, NULL, 0, 0); - _armed = false; } + _armed = false; _state = NULL; } SDL_UnlockMutex(_lock); @@ -142,7 +146,10 @@ static int _watchThread(void *data) { SDL_Delay(WATCH_POLL_MS); SDL_LockMutex(_lock); if ((_depth > 0) && !_armed && (SDL_GetTicks() - _enteredMS >= WATCH_ARM_MS)) { - lua_sethook(_state, _hook, LUA_MASKCOUNT, WATCH_HOOK_COUNT); + // Someone else's hook (a debugger, a profiler) owns the state; the watchdog stands down for the call. + if (lua_gethook(_state) == NULL) { + lua_sethook(_state, _hook, LUA_MASKCOUNT, WATCH_HOOK_COUNT); + } _armed = true; } SDL_UnlockMutex(_lock); diff --git a/src/singe.c b/src/singe.c index d5d751d5c..62b6a2d0d 100644 --- a/src/singe.c +++ b/src/singe.c @@ -69,6 +69,7 @@ LSEC_API int luaopen_ssl_config(lua_State *L); SSL_CTX *lsec_checkcontext(lua_State *L, int idx); #include "collide.h" +#include "debugger.h" #include "decode.h" #include "main.h" #include "midi.h" @@ -774,6 +775,7 @@ typedef struct GlobalS { RegionT overlayChanged; // Changed on the base overlay since it was last uploaded RegionT overlayStale[OVERLAY_TEXTURES]; // What each texture is behind the base overlay by bool pauseState; // by RDG2010 + bool debuggerPaused; // --debug: the IDE stopped the script and the engine paused the world for it bool pauseEnabled; // by RDG2010 bool quitEnabled; // The engine quits on the quit switch ChordT quitChord; // --quitchord: the machine's own way out, whatever the game maps @@ -975,6 +977,8 @@ static void _confFail(lua_State *L, const char *method, const char *fmt, static void _configPlaces(const char *file, const char *dataDir, const char *scriptFile, char **places); static int32_t _controllerSlot(SDL_JoystickID which); static void _createScriptContext(void); +static void _debuggerHold(bool hold); +static void _debuggerStart(lua_State *L); static int32_t _defaultVolume(int32_t maximum); static bool _delayAndPump(uint32_t ms); static void _deliverKey(bool down, int32_t keysym, int32_t scancode); @@ -1835,6 +1839,8 @@ static const LuaModuleT _luaModules[] = { MODL("inspect", inspect_lua), // bump, which answers what collide* deliberately does not: where a mover ends up MODL("bump", bump_lua), + // MobDebug, the debugger ZeroBrane Studio talks to; --debug starts it for the game + MODL("mobdebug", mobdebug_lua), // Copas MODL("copas", copas_lua), MODL("copas.ftp", copas_ftp_lua), @@ -3043,6 +3049,34 @@ static void _createScriptContext(void) { } +// --debug: while the IDE has the script stopped, the disc, the videos, and the sounds hold still as +// they do for the pause key, unless the game had paused them already. +static void _debuggerHold(bool hold) { + if (hold && !_global.pauseState) { + _global.debuggerPaused = true; + _global.pauseState = true; + _updatePauseState(); + } else if (!hold && _global.debuggerPaused) { + _global.debuggerPaused = false; + _global.pauseState = false; + _updatePauseState(); + } +} + + +// --debug: connects the script's state to the IDE. Done just before the script runs, after the GUI +// has bound itself to the state, so the first line the IDE sees is the script's own. +static void _debuggerStart(lua_State *L) { + char *relative = utilCreateString("%s%c%s", VFS_ENGINE_DIRECTORY, utilGetPathSeparator(), DEBUGGER_STOP_FILE); + char *stopFile = utilAbsolutePath(relative); + + _progTrace("Attaching the debugger at %s:%d", _global.conf->debugHost, _global.conf->debugPort); + debuggerAttach(L, _global.conf->debugHost, _global.conf->debugPort, stopFile, _debuggerHold); + free(stopFile); + free(relative); +} + + // The volume a script starts a family of sounds at, from the non-disc volume setting, on that // family's own scale. Sound effects and music therefore begin equally loud, which is what a user // who set --volume_nonvldp meant; Hypseus instead starts its music mixer a third of the way up. @@ -4862,6 +4896,13 @@ static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) { if (_global.conf->scriptTracing) { utilTrace("%s", message); } + // Under --debug the IDE stops here first, and the message gains the traceback whose file:line + // lines the IDE opens on a double click. + if (debuggerIsAttached(L)) { + debuggerStopOnError(L, message); + luaL_traceback(L, L, message, 1); + utilDie("%s", lua_tostring(L, -1)); + } utilDie("%s", message); } @@ -5705,6 +5746,8 @@ static int32_t _luaTraceback(lua_State *L) { if (message == NULL) { message = "(error object is not a string)"; } + // Under --debug the IDE stops at the error first, with the stack that raised it still there. + debuggerStopOnError(L, message); luaL_traceback(L, L, message, 1); return 1; @@ -7574,6 +7617,7 @@ static void _reloadScript(void) { MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0); MIX_StopTag(videoGetMixer(), MUSIC_TAG, 0); guiDetachLua(); + debuggerDetach(_global.luaContext); lua_close(_global.luaContext); _global.luaContext = NULL; _unloadScriptResources(); @@ -7833,6 +7877,9 @@ static void _rmlEscape(char *out, size_t size, const char *text) { // Loads and runs the game script under the traceback handler. A failure is fatal when the game // starts; on a reload it is reported and the game sits empty until the next one. static void _runScript(bool fatal) { + if (_global.conf->debugHost != NULL) { + _debuggerStart(_global.luaContext); + } _progTrace("Running %s", _global.conf->scriptFile); lua_pushcfunction(_global.luaContext, _luaTraceback); if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL, _global.conf->reload) || scriptWatchCall(_global.luaContext, 0, 0, -2)) { @@ -19172,6 +19219,9 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co // long a frame took wants the answer in the seconds they are sitting through. frameStartNS = SDL_GetTicksNS(); + // Under --debug, whatever the IDE held still while it had the script stopped goes on again. + debuggerRelease(); + // One frame of simulated time, the only place the virtual clock moves. Off it, nothing happens. utilTickAdvance(); if (_global.generatedAudio) { @@ -19741,6 +19791,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co _progTrace("Stopping Lua"); scriptWatchStop(); guiDetachLua(); + debuggerDetach(_global.luaContext); lua_close(_global.luaContext); _global.luaContext = NULL; diff --git a/src/singe.h b/src/singe.h index 9b872aa98..5576d1213 100644 --- a/src/singe.h +++ b/src/singe.h @@ -110,6 +110,7 @@ typedef struct ConfigS { char *audioSuffix; // --altaudio: the disc audio suffix discAudioSuffix takes, applied at startup char *soundfont; // --soundfont: the .sf2 MIDI files are synthesized with, in place of the search char *recordFile; // --record: the MP4 the whole run is written to; with --deterministic, frame exact + char *debugHost; // --debug: where the debugger listens; NULL when the game is not being debugged DeinterlaceE deinterlace; // --deinterlace: what an interlaced picture gets, automatic by default bool sceneLow; // --scenequality low: half resolution, no antialiasing and no bloom, whatever the script asks char *gamepadOrder; // --gamepad_reorder: enumeration positions, one per gamepad slot, as Hypseus writes them @@ -169,6 +170,7 @@ typedef struct ConfigS { int32_t haptic; // --haptic: the strongest rumble step a script may ask for; 0 is no rumble int32_t joyMouseRange; // --js_range: video pixels a frame the cursor moves at full stick deflection int32_t deterministicStep; // --deterministic=MS: simulated milliseconds a frame, and the seed the generators start from + int32_t debugPort; // --debug: the debugger's port double triggerThreshold; // --trigger_threshold: per cent of full travel a trigger counts as pressed at; 0 uses the dead zone double ratioX; // --xratio: the gun coordinate scale a script reads with ratioGetX double ratioY; // --yratio: the same for ratioGetY diff --git a/src/singe.rc.in b/src/singe.rc.in index f64652372..21d364abf 100644 --- a/src/singe.rc.in +++ b/src/singe.rc.in @@ -8,7 +8,7 @@ BEGIN BLOCK "040904E4" BEGIN VALUE "CompanyName", "Kangaroo Punch Studios" - VALUE "FileDescription", "SINGE Is Not a Game Emulator" + VALUE "FileDescription", "Scott's Increasingly Nifty Game Engine" VALUE "FileVersion", "@PROJECT_VERSION@" VALUE "InternalName", "Singe" VALUE "LegalCopyright", "Copyright 2006-@SINGE_COPYRIGHT_END_YEAR@ Scott C. Duensing" diff --git a/src/util.c b/src/util.c index 961e2a98a..9a552e784 100644 --- a/src/util.c +++ b/src/util.c @@ -52,6 +52,9 @@ static bool _consoleEnabled = true; +#ifdef _WIN32 +static bool _consoleOwned = false; // Singe opened a console window of its own, which closes when it exits +#endif static bool _outputHappened = false; static FILE *_utilTraceFile = NULL; @@ -465,13 +468,25 @@ char *utilReadLine(const char *haystack, size_t length, const char **offset) { void utilRedirectConsole(void) { #ifdef _WIN32 CONSOLE_SCREEN_BUFFER_INFO coninfo; + DWORD outputType = FILE_TYPE_UNKNOWN; static bool consoleOpen = false; if (_consoleEnabled && !consoleOpen) { consoleOpen = true; + // Started with its output already going somewhere -- an IDE's pipe, a file -- Singe keeps it + // there rather than opening a console window that would take it away. A console handle is + // not kept: a GUI program is not attached to the console it was started from. + outputType = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); + if ((outputType == FILE_TYPE_PIPE) || (outputType == FILE_TYPE_DISK)) { + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + return; + } + // Allocate a console for this app. AllocConsole(); + _consoleOwned = true; // Set the screen buffer to be big enough to let us scroll text. GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); @@ -618,8 +633,13 @@ void utilTraceVArgs(const char *fmt, va_list args) { } +// Keeps the console Singe opened for itself on screen until a key is pressed, so its last words can +// be read. Output that went to a pipe or a file needs no wait, and the IDE on the far end of the +// pipe would hang on one. void utilWaitForKeyOnWindows(void) { #ifdef _WIN32 - getchar(); + if (_consoleOwned) { + getchar(); + } #endif } diff --git a/testScripts/menuIntro.singe b/testScripts/menuIntro.singe index 4f668f107..634fd27a0 100644 --- a/testScripts/menuIntro.singe +++ b/testScripts/menuIntro.singe @@ -75,7 +75,7 @@ local STRIKE_ANGLE = -14.0 -- and forward through the strike. local DRAGON_YAW = -32.0 -- The dragon alone is turned toward the camera, so the wingbeat is -- seen three quarters on rather than edge on; the name stays square -- to the camera because it has to be read. -local TAGLINE = "SINGE Is Not a Game Emulator" +local TAGLINE = "Scott's Increasingly Nifty Game Engine" local SHOT_TIMES = { 0.25, 1.45, 3.00, 3.60, 4.20, 4.70, 5.10, 5.60, 6.10, 6.60, 7.10, 7.60 } diff --git a/thirdparty/MobDebug/CHANGELOG.md b/thirdparty/MobDebug/CHANGELOG.md new file mode 100644 index 000000000..d689296c4 --- /dev/null +++ b/thirdparty/MobDebug/CHANGELOG.md @@ -0,0 +1,251 @@ +# MobDebug Changelog + +## v0.80 (Jun 16 2021) + - Added workaround for luasocket 3.0-rc1 incompatibility with Lua 5.4.3 (closes #60, closes #61). + Thanks to [Catalin Marincia](https://github.com/cata0309) for the patch. + - Removed comment removal logic from remote commands to limit code modifications (#53). + - Removed check that prevented full stack to be returned by STACK command (closes #55). + - Updated *vararg check to make it work with Lua 5.4 (closes #59). + - Added resetting basedir value when DONE/done() command is executed (#51). + - Improved handling of breakpoints in remapped cases (#51). + - Improve debugging of openresty/lua_nginx scripts (closes #51). + - Removed wxlua-specific logic to improve debugging wxlua apps with `strict` module. + - Updated to improve based on static analyzer report. + - Fixed garbage collection of 'captured' values (pkulchenko/ZeroBraneStudio#919). + - Updated README to add cross-platform debugging and source map support. + - Upgraded Serpent (0.302) to fix serialization issue (pkulchenko/ZeroBraneStudio#900). + - Updated README to add Lua 5.4 support. + - Upgraded Serpent (0.301) to avoid metafield processing when `metatostring` is disabled. + - Added better report when trying to yield into debugging (pkulchenko/ZeroBraneStudio#801). + - Fixed `loadstring` command for content that starts with an empty string. + +## v0.70 (Sep 02 2017) + - Upgraded Serpent (0.30) to allow skipping `__tostring` (closes #27, closes #29). + - Improved handling of `__tostring` failures in stack processing (#27, #29). + - Fixed returning intercepted traceback if the value is not a string (fixes #33). + - Updated traceback reported from `debug.traceback` to avoid skipping one frame (#31). + - Limited setting `debug.traceback` to Lua5.1/LuaJIT (closes #31). + - Upgraded Serpent (0.288) to add `maxlength` and other improvements. + - Updated debugger to extend STACK command with additional options. + - Updated debugger to add support for vararg expressions in EXEC command. + - Updated debugger to extend EXEC command with additional options. + - Fixed updir removal in normalization to only happen at the beginning of the line. + - Updated stack generation for nginx/openresty as it no longer generates additional stack frame. + +## v0.64 (Aug 06 2016) + - Add callback mobdebug.onscratch (thanks to @dodo). + - Added path normalization to file names that don't start with `@`. + - Added populating vararg (`...`) values in the main chunk during debugging. + - Added path normalization to handle paths with up-dir references. + - Added `verbose` option to the `handle` method of the debugger. + - Added `output` method to pass stream information back to the controller. + - Improved error checking after OUTPUT command. + - Updated to reference one source for all hook masks. + - Updated `output` method not to fail when debugging is not started. + - Updated output stream processing to allow for empty strings. + - Updated `vararg` stack handling to work around incorrect LuaJIT 2.0.0 values. + +## v0.63 (Jan 25 2016) + - Added localization for `coroutine.wrap` call to allow modification (closes #17). + - Added `onexit` callback and removed `os.exit` calls from the controller (#19). + - Removed explicit assignment to `loaded` as this can be done only as needed (#23). + - Switched to using `string.*` methods to avoid string metamethods in the debug hook. + - Updated check for session completion to avoid early exit from debugging server (closes #24). + - Updated tests to add `listw` and `listb` commands. + - Upgraded Serpent (0.285) to add `keyignore` option. + - Upgraded Serpent to add numerical format fix and `__tostring` protection. + - Updated `DONE` response to allow for async processing (#19). + - Updated information on Lua 5.3 in README. + +## v0.62 (May 2 2015) + - Added variable arguments (vararg) to the stack trace. + - Updated examples to add `stack`, `basedir`, and `output` commands (closes #15). + - Updated removing breakpoints to remove all after `DELALLB` command. + - Upgraded Serpent to 0.28 with more `tostring` processing (thanks to @andrewstarks). + - Added explicit `tostring` for Lua 5.3 with `LUA_NOCVTN2S` set (pkulchenko/ZeroBraneStudio#401). + - Extended check for `mobdebug.loadstring` result (pkulchenko/ZeroBraneStudio#387). + +## v0.61 (Nov 23 2014) + - Added `basedir` method to explicitly set basedir when needed. + - Added connection timeout and exposed `connect` method for customization. + - Updated documentation for Lua 5.3 support and added features. + - Updated error handling when starting debugging fails due to connection error. + - Fixed `DONE` command to work with Lua 5.1 and 5.2 when breakpoints are set. + - Fixed `DONE` command when executed in scratchpad. + - Fixed `DONE` command to work from fragments loaded with `loop()`. + - Fixed references to `debug` library for environments that don't have it loaded. + +## v0.60 (Aug 30 2014) + - Added support for nginx debugging using internal coroutine API. + - Fixed localizaton in breakpoint handling function. + - Added path processing for chunk names that look like file names. + - Added line mapping support for debugging Lua-based languages. + - Fixed compatibility with Lua 5.2 (#13). + +## v0.56 (May 11 2014) + - Added `pause` method to suspend debugging from the application. + - Added DONE command to stop debugging and continue execution. + - Updated handling of `os.getenv` that may return multiple values (fixes #13). + - Updated coroutine debugging to avoid using `require` calls. + - Added support for setting/deleting breakpoints at run-time. + - Added number conversion for MOBDEBUG_PORT. + - Added check for loading 'os' module that may be absent on some systems. + - Updated Serpent (0.272) to fix array indexes serialization. + - Added closing Lua state during os.exit() for Lua 5.2+ (closes #11). + - explicit load Lua standard library (thanks to Alexey Melnichuk) + - Added explicit loading of `table` table (fixes #9). + +## v0.55 (Dec 14 2013) + - Added `setbreakpoint` and `removebreakpoint` public methods (closes #8). + - Fixed complex values 'captured' by redirected 'print' and not collected. + - Fixed error reporting on debugging non-existing file. + - Added ability to overwrite `line` and `dump` methods for serialization. + - Added handling of case-sensitive partitions on OSX (fixes #6, closes #7). + - Fixed Step Over/Out commands to stay in the same coroutine. + - Updated LICENSE information. + - Added optimization and limiting elements in a table in serializer (Serpent 0.25). + - Added support for `MOAICoroutine.new().run` to enable coroutine debugging. + - Added reporting of socket error for failed start()/loop() calls. + - Fixed debugging on/off handling in 'main' thread for LuaJIT. + - Fixed stepping through blocks with `nil` variables when 'strict' is in effect. + +## v0.54 (Aug 30 2013) + - Added reset of hook visits in case earlier start() call is refused. + - Added saving host/port configuration to reuse in start/loop calls. + - Added handling of code fragments reported as 'source'. + - Improved handling of done() in environments that reuse VMs. + - Reset state to allow multiple start()/done() calls from the same process. + - Replaces `unpack` with `table.unpack` for Lua 5.2 compatibility. + - Updated filenames/source code heuristic to avoid serializing filenames. + - Upgraded Serpent to 0.24 to fix table serialization issue. + - Upgraded Serpent to 0.231 to fix luaffi issue. + - Fixed debugging of zero length scripts. + +## v0.53 (May 06 2013) + - Added handling of case-insensitive filenames on OSX. + - Allowed start() to be called inside other functions (like assert). + - Improved debugging performance. + - Replaced socket.select with non-blocking .receive as it is faster. + - Upgraded serializer to add notification on incomplete output. + - Upgraded serializer (v0.224) to add support for __tostring and __serialize metamethods. + - Updated to allow `debug.traceback()` to be called under debugger. + - Fixed 'breaking' after executing OUT command that never reaches the target level. + - Fixed terminating debugging of an empty script. + - Fixed resetting cached source as it may change when basedir changes. + - Fix stack trace when LuaJIT FFI data is present (add `cdata` handling). + +## v0.52 (Mar 04 2013) + - Added `done()` method to finish debugging and to allow the script to continue. + - Added support for cross-platform remote debugging. + - Added support for code reloading and coroutine debugging under LuaJIT (v2.0.1). + - Added using `socket.connect4` when available. + - Added support for debugging Lua 5.2 scripts. + - Added check for `os.getenv` for those platforms that don't implement it. + - Improved handling of run-time errors in serialized data (__tostring method). + - Improved reporting of debugger errors to the application. + - Moved mosync/mobileLua code into a separate module. + - Fixed an issue with `eval/exec` commands not working immediately after `start()`. + +## v0.51 (Dec 13 2012) + - Added yield callback to customize event loop call during debugging. + - Added custom error handler for start() call to report errors remotely. + - Added serialization of remote 'print' results. + - Added 'output' command to redirect 'print' remotely. + - Added turning JIT off under LuaJIT to get reliable debug hook calls. + - Added setting/using global vars in a way that is 'strict.lua' friendly. + - Changed default port to 8172. Added setting port # using MOBDEBUG_PORT. + - Updated license to add remdebug license information. + - Upgraded to Serpent v0.20 to add serialization of metatables with + __tostring and fix serialization of functions as shared keys. + - Fixed using correct environment after the main script is done. + +## v0.50 (Oct 05 2012) + - Improved path matching when absolute and relative paths are used. + - Improved performance; thanks to Stephen Nichols for profiling and + detailed suggestions. + - Added conversion of file names on Windows to lower case to make + x:\Foo and X:\foo to match in breakpoint checks. + - Added reporting errors in deserializing stack data. + - Fixed an issue with returning stack values with circular references. + - Fixed serialization of usedata values that don't provide tostring(). + - Fixed an issue with wx IDLE event on Linux. + +## v0.49 (Sep 03 2012) + - Added support for unicode names in load and breakpoint commands. + - Added conversion to short path for unicode names on windows. + - Added handling of exit/load/reload commands after watch condition is fired. + - Added support for moai breakpoints and callback debugging. + - Fixed an issue of starting debugging too early in some cases using start(). + - Fixed an issue with coroutine debugging. + - Fixed an issue with yielding to wxwidgets apps while running main loop. + +## v0.48 (Aug 05 2012) + - Added support for coroutine debugging. Added methods (coro/moai) to + support coroutine debugging without adding 'on()' calls to all coroutines. + - Added on/off methods to turn debugging on and off (to improve performance). + - Fixed an issue with localized variables not being properly restored + if there are multiple variables with the same name in the environment. + - Added restoring variables between stack calls to make them reflect changes + that can be caused by EXEC/EVAL commands. + - Fixed compilation errors on scripts with a shebang line. + +## v0.47 (Jun 28 2012) + - Added special handling to debug clients running under LuaJIT. + - Fixed an issue when a remote client could not suspend/exit. + - Added default configuration for start/loop/listen methods. + - Relaxed safety checks to allow debugging lua code called from C + functions (to support debugging Love2d and similar applications). + - Fixed incorrect initial stack level when started with start() command. + - Added short_src field to the reported stack data. + +## v0.46 (Jun 19 2012) + - Added reporting of stack and variable (local/upvalue) information. + - Added support for multiple results returned by EXEC/EVAL commands. + - Added serialization/pretty-printing of results from EXEC/EVAL. + +## v0.45 (May 28 2012) + - Added select() implementation to mosync client to abort/break a running + mosync app. + - Added support for scratchpad. + - Added SUSPEND command to suspend execution of a running script. + - Added 'loadstring' command to load chunks of code to run under debugger. + - Added safety mechanism to LOAD/RELOAD to avoid aborting when there are + C functions in the stack. + - Added reset for loaded modules to get 'require' to be processed correctly + after LOAD/RELOAD commands. + - Added integration with MobileLua event loop to allow debugging of + applications that require event loop processing. + - Improved handling and reporting of run-time errors during debugging. + - Fixed handling of multi-line fragments with comments executed with EXEC/EVAL. + - Fixed exit from the debugger when the application throws an error. + - Fixed an issue with saving/restoring internal local variables. + - Fixed an issue with local variables not being updated after EXEC/EVAL + commands. + +## v0.44 (Mar 20 2012) + - Updated to work with "mosync" namespace (MobileLua 20120319 and MoSync 3.0). + - Removed the use of "module()". + +## v0.43 (Jan 16 2012) + - Fixed errors from LOAD/RELOAD commands when used with start() method. + - Added reporting of a file name and a line number to properly display + source code when attach to a debugger. + +## v0.42 (Dec 30 2011) + - Added support for debugging of wxwidgets applications. + - Added ability to break an application being debugged; this requires + socket.select and async processing (doesn't work from the command line). + +## v0.41 (Dec 17 2011) + - Added slash conversion in filenames to make breakpoints work regardless + of how slashes are specified in file names and in setb/delb commands. + - Added tests with breakpoints in files in subfolders. + - Added optimization for filename processing in the debug hook + (Christoph Kubisch). + - Added conversion of newlines for EVAL/EXEC commands to make them work + with multiline expressions. + - Allowed 'setb' and 'delb' commands to accept '-' as the filename. + The last referenced (checked/stopped at/etc.) filename is used. + +## v0.40 (Dec 02 2011) + - First public release. diff --git a/thirdparty/MobDebug/LICENSE b/thirdparty/MobDebug/LICENSE new file mode 100644 index 000000000..ff764469d --- /dev/null +++ b/thirdparty/MobDebug/LICENSE @@ -0,0 +1,63 @@ +Copyright (C) 2011-2018 Paul Kulchenko + +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. + +--[[ RemDebug License ]]--------------------------------------------------- + +Copyright (c) 2006 The Kepler Project. + +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. + +--[[ Serpent License ]]---------------------------------------------------- + +Copyright (c) 2011-2013 Paul Kulchenko (paul@kulchenko.com) + +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/thirdparty/MobDebug/README.md b/thirdparty/MobDebug/README.md new file mode 100644 index 000000000..3a7049281 --- /dev/null +++ b/thirdparty/MobDebug/README.md @@ -0,0 +1,90 @@ +# Project Description + +MobDebug is a remote debugger for Lua (including Lua 5.1, Lua 5.2, Lua 5.3, Lua 5.4, and LuaJIT 2.x). + +## Features + +MobDebug allows to control the execution of another Lua program remotely, +set breakpoints, and inspect the current state of the program. + +Mobdebug is a cross-platform debugger, which not only works on Windows, macOS, and Linux, but +also supports debugging with the application and debugger running on different platforms. + +It also supports source maps, which allows debugging of Lua-based languages, +like [Moonscript](http://notebook.kulchenko.com/zerobrane/moonscript-debugging-with-zerobrane-studio) +and [GSL-shell](http://notebook.kulchenko.com/zerobrane/gsl-shell-debugging-with-zerobrane-studio). + +MobDebug is based on [RemDebug](http://www.keplerproject.org/remdebug/) and +extends it in several ways: + +* added support for Lua 5.2, Lua 5.3, and Lua 5.4; +* added support for LuaJIT debugging; +* added support for cross-platform debugging (client and server running on different platforms/filesystems); +* added new commands: LOAD, RELOAD, OUT, STACK, DONE; +* added ability to pause and abort running applications; +* added pretty printing and handling of multiple results in EXEC; +* added stack and local/upvalue value reporting (STACK); +* added on/off commands to turn debugging on and off (to improve performance); +* added support for coroutine debugging (see examples/README for details); +* added support for varargs in stack trace; +* added support for vararg expressions in EXEC; +* added support for source maps; +* added support for debugging nginx/OpenResty, Lapis, and wxwidgets applications; +* removed dependency on LuaFileSystem; +* provided integration with [ZeroBrane Studio IDE](http://studio.zerobrane.com/). + +## Usage + +```lua +-- to start a server you can use to debug your application +> lua -e "require('mobdebug').listen()" + +-- to debug a script, add the following line to it: +require("mobdebug").start() +``` + +## Installation + +Make `src/mobdebug.lua` available to your script. +See `examples/README` and `examples/*.lua` for examples of how to use the module. + +## Dependencies + +MobDebug depends on LuaSocket 2.0+ and has been tested with Lua 5.1, Lua 5.2, Lua 5.3, and Lua 5.4. +MobDebug also works with LuaJIT v2.0+; using `loop` and `scratchpad` methods requires v2.0.1. + +## Troubleshooting + +Generally, breakpoints set with in the debugger should work without any additional configuration, +but there may be cases when this doesn't work out of the box, which may happen for several reasons: + +- A breakpoint may be **inside a coroutine**; by default breakpoints inside coroutines are not triggered. +To enable debugging in coroutines, including triggering of breakpoints, you may either +(1) add `require('mobdebug').on()` call to that coroutine, which will enable debugging for that particular coroutine, or +(2) add `require('mobdebug').coro()` call to your script, which will enable debugging for all coroutines created using `coroutine.create` later in the script. +- If you enable coroutine debugging using `require('mobdebug').coro()`, this will **not affect coroutines created using C API** or Lua code wrapped into `coroutine.wrap`. +You can still debug those fragments after adding `require('mobdebug').on()` to the coroutine code. +- The path of the file known to the debugger (the caller of `setb` command) **may not be the same** as the path known to the Lua engine (running the code being debugged). +For example, if you use an embedded engine, you may want to check if the path reported by the engine is normalized (doesn't include `../` references) by checking the result of `debug.getinfo(1,"S").source`. +- The capitalization of the file known to the debugger **may not be the same** as the capitalization of the file known to the Lua engine with the latter running on a case-sensitive system. +For example, if you set a breakpoint on the file `TEST.lua` in the debugger running on Window (case-insensitive), it may not fire in the application running `test.lua` on Linux (with case-sensitive file system). +To avoid this make sure that the capitalization for your project files is the same on both file systems. +- The script you are debugging may **change the current folder** (for example, using `lfs` module) and load the script (using `dofile`) from the changed folder. +To make breakpoints work in this case you may want to **use absolute path** with `dofile`. +- You may have a symlink in your path and be referencing paths to your scripts differently in the code and in the debugger (using a path with symlink in one case and not using it in the other case). +- You may be using your own Lua engine that doesn't report file names relative to the project directory (as set in the debugger). +For example, you set the project directory pointing to `scripts` folder (with `common` subfolder) and the engine reports the file name as `myfile.lua` instead of `common/myfile.lua`; +the debugger will be looking for `scripts/myfile.lua` instead of `scripts/common/myfile.lua` and the file will not be activated and the breakpoints won't work. +You may also be using inconsistent path separators in the file names; for example, `common/myfile.lua` in one case and `common\myfile.lua` in another. +- If you are loading files using `luaL_loadbuffer`, make sure that the chunk name specified (the last parameter) matches the file location. +- If you set/remove breakpoints while the application is running, these changes may not have any effect if only a small number of Lua commands get executed. +To limit the negative impact of socket checks for pending messages, the debugger in the application only checks for incoming requests every 200 statements (by default), so if your tests include fewer statements, then `pause`/`break` commands and toggling breakpoints without suspending the application may not work. +To make the debugger to check more frequently, you can update `checkcount` field (`require('mobdebug').checkcount = 1`) before or after debugging is started. + +## Author + +Paul Kulchenko (paul@kulchenko.com) + +## License + +See LICENSE file diff --git a/thirdparty/MobDebug/src/mobdebug.lua b/thirdparty/MobDebug/src/mobdebug.lua new file mode 100644 index 000000000..8b2cc3bf3 --- /dev/null +++ b/thirdparty/MobDebug/src/mobdebug.lua @@ -0,0 +1,1670 @@ +-- +-- MobDebug -- Lua remote debugger +-- Copyright 2011-23 Paul Kulchenko +-- Based on RemDebug 1.0 Copyright Kepler Project 2005 +-- + +-- use loaded modules or load explicitly on those systems that require that +local require = require +local io = io or require "io" +local table = table or require "table" +local string = string or require "string" +local coroutine = coroutine or require "coroutine" +local debug = require "debug" +-- protect require "os" as it may fail on embedded systems without os module +local os = os or (function(module) + local ok, res = pcall(require, module) + return ok and res or nil +end)("os") + +local mobdebug = { + _NAME = "mobdebug", + _VERSION = "0.805", + _COPYRIGHT = "Paul Kulchenko", + _DESCRIPTION = "Mobile Remote Debugger for the Lua programming language", + port = os and os.getenv and tonumber((os.getenv("MOBDEBUG_PORT"))) or 8172, + checkcount = 200, + yieldtimeout = 0.02, -- yield timeout (s) + connecttimeout = 2, -- connect timeout (s) +} + +local HOOKMASK = "lcr" +local error = error +local getfenv = getfenv +local setfenv = setfenv +local loadstring = loadstring or load -- "load" replaced "loadstring" in Lua 5.2 +local pairs = pairs +local setmetatable = setmetatable +local tonumber = tonumber +local unpack = table.unpack or unpack +local rawget = rawget +local gsub, sub, find = string.gsub, string.sub, string.find + +-- if strict.lua is used, then need to avoid referencing some global +-- variables, as they can be undefined; +-- use rawget to avoid complaints from strict.lua at run-time. +-- it's safe to do the initialization here as all these variables +-- should get defined values (if any) before the debugging starts. +-- there is also global 'wx' variable, which is checked as part of +-- the debug loop as 'wx' can be loaded at any time during debugging. +local genv = _G or _ENV +local jit = rawget(genv, "jit") +local MOAICoroutine = rawget(genv, "MOAICoroutine") + +-- ngx_lua/Openresty requires special handling as its coroutine.* +-- methods use a different mechanism that doesn't allow resume calls +-- from debug hook handlers. +-- Instead, the "original" coroutine.* methods are used. +-- `rawget` needs to be used to protect against `strict` checks +local ngx = rawget(genv, "ngx") +if not ngx then + -- "older" versions of ngx_lua (0.10.x at least) hide ngx table in metatable, + -- so need to use that + local metagindex = getmetatable(genv) and getmetatable(genv).__index + ngx = type(metagindex) == "table" and metagindex.rawget and metagindex:rawget("ngx") or nil +end +local corocreate = ngx and coroutine._create or coroutine.create +local cororesume = ngx and coroutine._resume or coroutine.resume +local coroyield = ngx and coroutine._yield or coroutine.yield +local corostatus = ngx and coroutine._status or coroutine.status +local corowrap = coroutine.wrap + +if not setfenv then -- Lua 5.2+ + -- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html + -- this assumes f is a function + local function findenv(f) + if not debug.getupvalue then return nil end + local level = 1 + repeat + local name, value = debug.getupvalue(f, level) + if name == '_ENV' then return level, value end + level = level + 1 + until name == nil + return nil end + getfenv = function (f) return(select(2, findenv(f)) or _G) end + setfenv = function (f, t) + local level = findenv(f) + if level then debug.setupvalue(f, level, t) end + return f end +end + +-- check for OS and convert file names to lower case on windows +-- (its file system is case insensitive, but case preserving), as setting a +-- breakpoint on x:\Foo.lua will not work if the file was loaded as X:\foo.lua. +-- OSX and Windows behave the same way (case insensitive, but case preserving). +-- OSX can be configured to be case-sensitive, so check for that. This doesn't +-- handle the case of different partitions having different case-sensitivity. +local win = os and os.getenv and (os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows')) and true or false +local mac = not win and (os and os.getenv and os.getenv('DYLD_LIBRARY_PATH') or not io.open("/proc")) and true or false +local iscasepreserving = win or (mac and io.open('/library') ~= nil) + +-- turn jit off based on Mike Pall's comment in this discussion: +-- http://www.freelists.org/post/luajit/Debug-hooks-and-JIT,2 +-- "You need to turn it off at the start if you plan to receive +-- reliable hook calls at any later point in time." +if jit and jit.off then jit.off() end + +local socket = require "socket" +local coro_debugger +local coro_debugee +local coroutines = {}; setmetatable(coroutines, {__mode = "k"}) -- "weak" keys +local events = { BREAK = 1, WATCH = 2, RESTART = 3, STACK = 4 } +local breakpoints = {} +local watches = {} +local lastsource +local lastfile +local watchescnt = 0 +local abort -- default value is nil; this is used in start/loop distinction +local seen_hook = false +local checkcount = 0 +local step_into = false +local step_over = false +local step_level = 0 +local stack_level = 0 +local SAFEWS = "\012" -- "safe" whitespace value +local server +local buf +local outputs = {} +local iobase = {print = print} +local basedir = "" +local deferror = "execution aborted at default debugee" +local debugee = function () + local a = 1 + for _ = 1, 10 do a = a + 1 end + error(deferror) +end +local function q(s) return string.gsub(s, '([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end + +local serpent = (function() ---- include Serpent module for serialization +local n, v = "serpent", "0.302" -- (C) 2012-18 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local getmetatable = debug and debug.getmetatable or getmetatable +local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ +local keyword, globals, G = {}, {}, (_G or _ENV) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local numformat = opts.numformat or "%.17g" + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end + local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s)) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) end + -- protect from those cases where __tostring may fail + if type(mt) == 'table' and metatostring ~= false then + local to, tr = pcall(function() return mt.__tostring(t) end) + local so, sr = pcall(function() return mt.__serialize(t) end) + if (to or so) then -- knows how to serialize itself + seen[t] = insref or spath + t = so and sr or tr + ttype = type(t) + end -- new value falls through to be serialized + end + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('maxlvl', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.keyignore and opts.keyignore[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) + if maxlen then + maxlen = maxlen - #out[#out] + if maxlen < 0 then break end + end + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end + local ok, res = pcall(string.dump, t) + local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local env = (opts and opts.safe == false) and G + or setmetatable({}, { + __index = function(t,k) return t end, + __call = function(t,...) error("cannot call functions") end + }) + local f, res = (loadstring or load)('return '..data, nil, nil, env) + if not f then f, res = (loadstring or load)(data, nil, nil, env) end + if not f then return f, res end + if setfenv then setfenv(f, env) end + return pcall(f) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } +end)() ---- end of Serpent module + +mobdebug.line = serpent.line +mobdebug.dump = serpent.dump +mobdebug.linemap = nil +mobdebug.loadstring = loadstring + +local function removebasedir(path, basedir) + if iscasepreserving then + -- check if the lowercased path matches the basedir + -- if so, return substring of the original path (to not lowercase it) + return path:lower():find('^'..q(basedir:lower())) + and path:sub(#basedir+1) or path + else + return string.gsub(path, '^'..q(basedir), '') + end +end + +local function stack(start) + local function vars(f) + local func = debug.getinfo(f, "f").func + local i = 1 + local locals = {} + -- get locals + while true do + local name, value = debug.getlocal(f, i) + if not name then break end + if string.sub(name, 1, 1) ~= '(' then + locals[name] = {value, select(2,pcall(tostring,value))} + end + i = i + 1 + end + -- get varargs (these use negative indices) + i = 1 + while true do + local name, value = debug.getlocal(f, -i) + if not name then break end + locals[name:gsub("%)$"," "..i..")")] = {value, select(2,pcall(tostring,value))} + i = i + 1 + end + -- get upvalues + i = 1 + local ups = {} + while func and debug.getupvalue do -- check for func as it may be nil for tail calls + local name, value = debug.getupvalue(func, i) + if not name then break end + ups[name] = {value, select(2,pcall(tostring,value))} + i = i + 1 + end + return locals, ups + end + + local stack = {} + local linemap = mobdebug.linemap + for i = (start or 0), 100 do + local source = debug.getinfo(i, "Snl") + if not source then break end + + local src = source.source + if src:find("@") == 1 then + src = src:sub(2):gsub("\\", "/") + if src:find("%./") == 1 then src = src:sub(3) end + end + + table.insert(stack, { -- remove basedir from source + {source.name, removebasedir(src, basedir), + linemap and linemap(source.linedefined, source.source) or source.linedefined, + linemap and linemap(source.currentline, source.source) or source.currentline, + source.what, source.namewhat, source.short_src}, + vars(i+1)}) + end + return stack +end + +local function set_breakpoint(file, line) + if file == '-' and lastfile then file = lastfile + elseif iscasepreserving then file = string.lower(file) end + if not breakpoints[line] then breakpoints[line] = {} end + breakpoints[line][file] = true +end + +local function remove_breakpoint(file, line) + if file == '-' and lastfile then file = lastfile + elseif file == '*' and line == 0 then breakpoints = {} + elseif iscasepreserving then file = string.lower(file) end + if breakpoints[line] then breakpoints[line][file] = nil end +end + +local function has_breakpoint(file, line) + return breakpoints[line] + and breakpoints[line][iscasepreserving and string.lower(file) or file] +end + +local function restore_vars(vars) + if type(vars) ~= 'table' then return end + + -- locals need to be processed in the reverse order, starting from + -- the inner block out, to make sure that the localized variables + -- are correctly updated with only the closest variable with + -- the same name being changed + -- first loop find how many local variables there is, while + -- the second loop processes them from i to 1 + local i = 1 + while true do + local name = debug.getlocal(3, i) + if not name then break end + i = i + 1 + end + i = i - 1 + local written_vars = {} + while i > 0 do + local name = debug.getlocal(3, i) + if not written_vars[name] then + if string.sub(name, 1, 1) ~= '(' then + debug.setlocal(3, i, rawget(vars, name)) + end + written_vars[name] = true + end + i = i - 1 + end + + i = 1 + local func = debug.getinfo(3, "f").func + while debug.getupvalue do + local name = debug.getupvalue(func, i) + if not name then break end + if not written_vars[name] then + if string.sub(name, 1, 1) ~= '(' then + debug.setupvalue(func, i, rawget(vars, name)) + end + written_vars[name] = true + end + i = i + 1 + end +end + +local function capture_vars(level, thread) + level = (level or 0)+2 -- add two levels for this and debug calls + local func = (thread and debug.getinfo(thread, level, "f") or debug.getinfo(level, "f") or {}).func + if not func then return {} end + + local vars = {['...'] = {}} + local i = 1 + while debug.getupvalue do + local name, value = debug.getupvalue(func, i) + if not name then break end + if string.sub(name, 1, 1) ~= '(' then vars[name] = value end + i = i + 1 + end + i = 1 + while true do + local name, value + if thread then + name, value = debug.getlocal(thread, level, i) + else + name, value = debug.getlocal(level, i) + end + if not name then break end + if string.sub(name, 1, 1) ~= '(' then vars[name] = value end + i = i + 1 + end + -- get varargs (these use negative indices) + i = 1 + while true do + local name, value + if thread then + name, value = debug.getlocal(thread, level, -i) + else + name, value = debug.getlocal(level, -i) + end + if not name then break end + vars['...'][i] = value + i = i + 1 + end + -- returned 'vars' table plays a dual role: (1) it captures local values + -- and upvalues to be restored later (in case they are modified in "eval"), + -- and (2) it provides an environment for evaluated chunks. + -- getfenv(func) is needed to provide proper environment for functions, + -- including access to globals, but this causes vars[name] to fail in + -- restore_vars on local variables or upvalues with `nil` values when + -- 'strict' is in effect. To avoid this `rawget` is used in restore_vars. + setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func), __mode = "v" }) + return vars +end + +local function stack_depth(start_depth) + for i = start_depth, 0, -1 do + if debug.getinfo(i, "l") then return i+1 end + end + return start_depth +end + +local function is_safe(stack_level) + -- the stack grows up: 0 is getinfo, 1 is is_safe, 2 is debug_hook, 3 is user function + if stack_level == 3 then return true end + for i = 3, stack_level do + -- return if it is not safe to abort + local info = debug.getinfo(i, "S") + if not info then return true end + if info.what == "C" then return false end + end + return true +end + +local function in_debugger() + local this = debug.getinfo(1, "S").source + -- only need to check few frames as mobdebug frames should be close + for i = 3, 7 do + local info = debug.getinfo(i, "S") + if not info then return false end + if info.source == this then return true end + end + return false +end + +local function is_pending(peer) + -- if there is something already in the buffer, skip check + if not buf and checkcount >= mobdebug.checkcount then + peer:settimeout(0) -- non-blocking + buf = peer:receive(1) + peer:settimeout() -- back to blocking + checkcount = 0 + end + return buf +end + +local function readnext(peer, num) + peer:settimeout(0) -- non-blocking + local res, err, partial = peer:receive(num) + peer:settimeout() -- back to blocking + return res or partial or '', err +end + +local function handle_breakpoint(peer) + -- check if the buffer has the beginning of SETB/DELB command; + -- this is to avoid reading the entire line for commands that + -- don't need to be handled here. + if not buf or not (buf:sub(1,1) == 'S' or buf:sub(1,1) == 'D') then return end + + -- check second character to avoid reading STEP or other S* and D* commands + if #buf == 1 then buf = buf .. readnext(peer, 1) end + if buf:sub(2,2) ~= 'E' then return end + + -- need to read few more characters + buf = buf .. readnext(peer, 5-#buf) + if buf ~= 'SETB ' and buf ~= 'DELB ' then return end + + local res, _, partial = peer:receive("*l") -- get the rest of the line; blocking + if not res then + if partial then buf = buf .. partial end + return + end + + local _, _, cmd, file, line = (buf..res):find("^([A-Z]+)%s+(.-)%s+(%d+)%s*$") + if cmd == 'SETB' then set_breakpoint(file, tonumber(line)) + elseif cmd == 'DELB' then remove_breakpoint(file, tonumber(line)) + else + -- this looks like a breakpoint command, but something went wrong; + -- return here to let the "normal" processing to handle, + -- although this is likely to not go well. + return + end + + buf = nil +end + +local function normalize_path(file) + local n + repeat + file, n = file:gsub("/+%.?/+","/") -- remove all `//` and `/./` references + until n == 0 + -- collapse all up-dir references: this will clobber UNC prefix (\\?\) + -- and disk on Windows when there are too many up-dir references: `D:\foo\..\..\bar`; + -- handle the case of multiple up-dir references: `foo/bar/baz/../../../more`; + -- only remove one at a time as otherwise `../../` could be removed; + repeat + file, n = file:gsub("[^/]+/%.%./", "", 1) + until n == 0 + -- there may still be a leading up-dir reference left (as `/../` or `../`); remove it + return (file:gsub("^(/?)%.%./", "%1")) +end + +local function debug_hook(event, line) + -- (1) LuaJIT needs special treatment. Because debug_hook is set for + -- *all* coroutines, and not just the one being debugged as in regular Lua + -- (http://lua-users.org/lists/lua-l/2011-06/msg00513.html), + -- need to avoid debugging mobdebug's own code as LuaJIT doesn't + -- always correctly generate call/return hook events (there are more + -- calls than returns, which breaks stack depth calculation and + -- 'step' and 'step over' commands stop working; possibly because + -- 'tail return' events are not generated by LuaJIT). + -- the next line checks if the debugger is run under LuaJIT and if + -- one of debugger methods is present in the stack, it simply returns. + -- ngx_lua/Openresty requires a slightly different handling, as it + -- creates a coroutine wrapper, so this processing needs to be skipped. + if jit and not (ngx and type(ngx) == "table" and ngx.say) then + -- when luajit is compiled with LUAJIT_ENABLE_LUA52COMPAT, + -- coroutine.running() returns non-nil for the main thread. + local coro, main = coroutine.running() + if not coro or main then coro = 'main' end + local disabled = coroutines[coro] == false + or coroutines[coro] == nil and coro ~= (coro_debugee or 'main') + if coro_debugee and disabled or not coro_debugee and (disabled or in_debugger()) then + return + end + end + + -- (2) check if abort has been requested and it's safe to abort + if abort and is_safe(stack_level) then error(abort) end + + -- (3) also check if this debug hook has not been visited for any reason. + -- this check is needed to avoid stepping in too early + -- (for example, when coroutine.resume() is executed inside start()). + if not seen_hook and in_debugger() then return end + + if event == "call" then + stack_level = stack_level + 1 + elseif event == "return" or event == "tail return" then + stack_level = stack_level - 1 + elseif event == "line" then + if mobdebug.linemap then + local ok, mappedline = pcall(mobdebug.linemap, line, debug.getinfo(2, "S").source) + if ok then line = mappedline end + if not line then return end + end + + -- may need to fall through because of the following: + -- (1) step_into + -- (2) step_over and stack_level <= step_level (need stack_level) + -- (3) breakpoint; check for line first as it's known; then for file + -- (4) socket call (only do every Xth check) + -- (5) at least one watch is registered + if not ( + step_into or step_over or breakpoints[line] or watchescnt > 0 + or is_pending(server) + ) then checkcount = checkcount + 1; return end + + checkcount = mobdebug.checkcount -- force check on the next command + + -- this is needed to check if the stack got shorter or longer. + -- unfortunately counting call/return calls is not reliable. + -- the discrepancy may happen when "pcall(load, '')" call is made + -- or when "error()" is called in a function. + -- in either case there are more "call" than "return" events reported. + -- this validation is done for every "line" event, but should be "cheap" + -- as it checks for the stack to get shorter (or longer by one call). + -- start from one level higher just in case we need to grow the stack. + -- this may happen after coroutine.resume call to a function that doesn't + -- have any other instructions to execute. it triggers three returns: + -- "return, tail return, return", which needs to be accounted for. + stack_level = stack_depth(stack_level+1) + + local caller = debug.getinfo(2, "S") + + -- grab the filename and fix it if needed + local file = lastfile + if (lastsource ~= caller.source) then + file, lastsource = caller.source, caller.source + -- technically, users can supply names that may not use '@', + -- for example when they call loadstring('...', 'filename.lua'). + -- Unfortunately, there is no reliable/quick way to figure out + -- what is the filename and what is the source code. + -- If the name doesn't start with `@`, assume it's a file name if it's all on one line. + if find(file, "^@") or not find(file, "[\r\n]") then + file = gsub(gsub(file, "^@", ""), "\\", "/") + -- normalize paths that may include up-dir or same-dir references + -- if the path starts from the up-dir or reference, + -- prepend `basedir` to generate absolute path to keep breakpoints working. + -- ignore qualified relative path (`D:../`) and UNC paths (`\\?\`) + if find(file, "^%.%./") then file = basedir..file end + if find(file, "/%.%.?/") then file = normalize_path(file) end + -- need this conversion to be applied to relative and absolute + -- file names as you may write "require 'Foo'" to + -- load "foo.lua" (on a case insensitive file system) and breakpoints + -- set on foo.lua will not work if not converted to the same case. + if iscasepreserving then file = string.lower(file) end + if find(file, "^%./") then file = sub(file, 3) end + -- remove basedir, so that breakpoints are checked properly + file = gsub(file, "^"..q(basedir), "") + -- some file systems allow newlines in file names; remove these. + file = gsub(file, "\n", ' ') + else + file = mobdebug.line(file) + end + + -- set to true if we got here; this only needs to be done once per + -- session, so do it here to at least avoid setting it for every line. + seen_hook = true + lastfile = file + end + + if is_pending(server) then handle_breakpoint(server) end + + local vars, status, res + if (watchescnt > 0) then + vars = capture_vars(1) + for index, value in pairs(watches) do + setfenv(value, vars) + local ok, fired = pcall(value) + if ok and fired then + status, res = cororesume(coro_debugger, events.WATCH, vars, file, line, index) + break -- any one watch is enough; don't check multiple times + end + end + end + + -- need to get into the "regular" debug handler, but only if there was + -- no watch that was fired. If there was a watch, handle its result. + local getin = (status == nil) and + (step_into + -- when coroutine.running() return `nil` (main thread in Lua 5.1), + -- step_over will equal 'main', so need to check for that explicitly. + or (step_over and step_over == (coroutine.running() or 'main') and stack_level <= step_level) + or has_breakpoint(file, line) + or is_pending(server)) + + if getin then + vars = vars or capture_vars(1) + step_into = false + step_over = false + status, res = cororesume(coro_debugger, events.BREAK, vars, file, line) + end + + -- handle 'stack' command that provides stack() information to the debugger + while status and res == 'stack' do + -- resume with the stack trace and variables + if vars then restore_vars(vars) end -- restore vars so they are reflected in stack values + status, res = cororesume(coro_debugger, events.STACK, stack(3), file, line) + end + + -- need to recheck once more as resume after 'stack' command may + -- return something else (for example, 'exit'), which needs to be handled + if status and res and res ~= 'stack' then + if not abort and res == "exit" then mobdebug.onexit(1, true); return end + if not abort and res == "done" then mobdebug.done(); return end + abort = res + -- only abort if safe; if not, there is another (earlier) check inside + -- debug_hook, which will abort execution at the first safe opportunity + if is_safe(stack_level) then error(abort) end + elseif not status and res then + error(res, 2) -- report any other (internal) errors back to the application + end + + if vars then restore_vars(vars) end + + -- last command requested Step Over/Out; store the current thread + if step_over == true then step_over = coroutine.running() or 'main' end + end +end + +local function stringify_results(params, status, ...) + if not status then return status, ... end -- on error report as it + + params = params or {} + if params.nocode == nil then params.nocode = true end + if params.comment == nil then params.comment = 1 end + + local t = {} + for i = 1, select('#', ...) do -- stringify each of the returned values + local ok, res = pcall(mobdebug.line, select(i, ...), params) + t[i] = ok and res or ("%q"):format(res):gsub("\010","n"):gsub("\026","\\026") + end + -- stringify table with all returned values + -- this is done to allow each returned value to be used (serialized or not) + -- intependently and to preserve "original" comments + return pcall(mobdebug.dump, t, {sparse = false}) +end + +local function isrunning() + return coro_debugger and (corostatus(coro_debugger) == 'suspended' or corostatus(coro_debugger) == 'running') +end + +-- this is a function that removes all hooks and closes the socket to +-- report back to the controller that the debugging is done. +-- the script that called `done` can still continue. +local function done() + if not (isrunning() and server) then return end + + if not jit then + for co, debugged in pairs(coroutines) do + if debugged then debug.sethook(co) end + end + end + + debug.sethook() + server:close() + + coro_debugger = nil -- to make sure isrunning() returns `false` + seen_hook = nil -- to make sure that the next start() call works + abort = nil -- to make sure that callback calls use proper "abort" value + basedir = "" -- to reset basedir in case the same module/state is reused +end + +local function debugger_loop(sev, svars, sfile, sline) + local command + local eval_env = svars or {} + local function emptyWatch () return false end + local loaded = {} + for k in pairs(package.loaded) do loaded[k] = true end + + while true do + local line, err + if mobdebug.yield and server.settimeout then server:settimeout(mobdebug.yieldtimeout) end + while true do + line, err = server:receive("*l") + if not line then + if err == "timeout" then + if mobdebug.yield then mobdebug.yield() end + elseif err == "closed" then + error("Debugger connection closed", 0) + else + error(("Unexpected socket error: %s"):format(err), 0) + end + else + -- if there is something in the pending buffer, prepend it to the line + if buf then line = buf .. line; buf = nil end + break + end + end + if server.settimeout then server:settimeout() end -- back to blocking + command = string.sub(line, string.find(line, "^[A-Z]+")) + if command == "SETB" then + local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$") + if file and line then + set_breakpoint(file, tonumber(line)) + server:send("200 OK\n") + else + server:send("400 Bad Request\n") + end + elseif command == "DELB" then + local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$") + if file and line then + remove_breakpoint(file, tonumber(line)) + server:send("200 OK\n") + else + server:send("400 Bad Request\n") + end + elseif command == "EXEC" then + -- extract any optional parameters + local params = string.match(line, "--%s*(%b{})%s*$") + local _, _, chunk = string.find(line, "^[A-Z]+%s+(.+)$") + if chunk then + -- \r is optional, as it may be stripped by some luasocket versions, like the one in LOVE2d + chunk = chunk:gsub("\r?"..SAFEWS, "\n") -- convert safe whitespace back to new line + local func, res = mobdebug.loadstring(chunk) + local status + if func then + local pfunc = params and loadstring("return "..params) -- use internal function + params = pfunc and pfunc() + params = (type(params) == "table" and params or {}) + local stack = tonumber(params.stack) + -- if the requested stack frame is not the current one, then use a new capture + -- with a specific stack frame: `capture_vars(0, coro_debugee)` + local env = stack and coro_debugee and capture_vars(stack-1, coro_debugee) or eval_env + setfenv(func, env) + status, res = stringify_results(params, pcall(func, unpack(rawget(env,'...') or {}))) + end + if status then + if mobdebug.onscratch then mobdebug.onscratch(res) end + server:send("200 OK " .. tostring(#res) .. "\n") + server:send(res) + else + -- fix error if not set (for example, when loadstring is not present) + if not res then res = "Unknown error" end + server:send("401 Error in Expression " .. tostring(#res) .. "\n") + server:send(res) + end + else + server:send("400 Bad Request\n") + end + elseif command == "LOAD" then + local _, _, size, name = string.find(line, "^[A-Z]+%s+(%d+)%s+(%S.-)%s*$") + size = tonumber(size) + + if abort == nil then -- no LOAD/RELOAD allowed inside start() + if size > 0 then server:receive(size) end + if sfile and sline then + server:send("201 Started " .. sfile .. " " .. tostring(sline) .. "\n") + else + server:send("200 OK 0\n") + end + else + -- reset environment to allow required modules to load again + -- remove those packages that weren't loaded when debugger started + for k in pairs(package.loaded) do + if not loaded[k] then package.loaded[k] = nil end + end + + if size == 0 and name == '-' then -- RELOAD the current script being debugged + server:send("200 OK 0\n") + coroyield("load") + else + -- receiving 0 bytes blocks (at least in luasocket 2.0.2), so skip reading + local chunk = size == 0 and "" or server:receive(size) + if chunk then -- LOAD a new script for debugging + local func, res = mobdebug.loadstring(chunk, "@"..name) + if func then + server:send("200 OK 0\n") + debugee = func + coroyield("load") + else + server:send("401 Error in Expression " .. tostring(#res) .. "\n") + server:send(res) + end + else + server:send("400 Bad Request\n") + end + end + end + elseif command == "SETW" then + local _, _, exp = string.find(line, "^[A-Z]+%s+(.+)%s*$") + if exp then + local func, res = mobdebug.loadstring("return(" .. exp .. ")") + if func then + watchescnt = watchescnt + 1 + local newidx = #watches + 1 + watches[newidx] = func + server:send("200 OK " .. tostring(newidx) .. "\n") + else + server:send("401 Error in Expression " .. tostring(#res) .. "\n") + server:send(res) + end + else + server:send("400 Bad Request\n") + end + elseif command == "DELW" then + local _, _, index = string.find(line, "^[A-Z]+%s+(%d+)%s*$") + index = tonumber(index) + if index > 0 and index <= #watches then + watchescnt = watchescnt - (watches[index] ~= emptyWatch and 1 or 0) + watches[index] = emptyWatch + server:send("200 OK\n") + else + server:send("400 Bad Request\n") + end + elseif command == "RUN" then + server:send("200 OK\n") + + local ev, vars, file, line, idx_watch = coroyield() + eval_env = vars + if ev == events.BREAK then + server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n") + elseif ev == events.WATCH then + server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n") + elseif ev == events.RESTART then + -- nothing to do + else + server:send("401 Error in Execution " .. tostring(#file) .. "\n") + server:send(file) + end + elseif command == "STEP" then + server:send("200 OK\n") + step_into = true + + local ev, vars, file, line, idx_watch = coroyield() + eval_env = vars + if ev == events.BREAK then + server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n") + elseif ev == events.WATCH then + server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n") + elseif ev == events.RESTART then + -- nothing to do + else + server:send("401 Error in Execution " .. tostring(#file) .. "\n") + server:send(file) + end + elseif command == "OVER" or command == "OUT" then + server:send("200 OK\n") + step_over = true + + -- OVER and OUT are very similar except for + -- the stack level value at which to stop + if command == "OUT" then step_level = stack_level - 1 + else step_level = stack_level end + + local ev, vars, file, line, idx_watch = coroyield() + eval_env = vars + if ev == events.BREAK then + server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n") + elseif ev == events.WATCH then + server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n") + elseif ev == events.RESTART then + -- nothing to do + else + server:send("401 Error in Execution " .. tostring(#file) .. "\n") + server:send(file) + end + elseif command == "BASEDIR" then + local _, _, dir = string.find(line, "^[A-Z]+%s+(.+)%s*$") + if dir then + basedir = iscasepreserving and string.lower(dir) or dir + -- reset cached source as it may change with basedir + lastsource = nil + server:send("200 OK\n") + else + server:send("400 Bad Request\n") + end + elseif command == "SUSPEND" then + -- do nothing; it already fulfilled its role + elseif command == "DONE" then + coroyield("done") + return -- done with all the debugging + elseif command == "STACK" then + -- first check if we can execute the stack command + -- as it requires yielding back to debug_hook it cannot be executed + -- if we have not seen the hook yet as happens after start(). + -- in this case we simply return an empty result + local vars, ev = {} + if seen_hook then + ev, vars = coroyield("stack") + end + if ev and ev ~= events.STACK then + server:send("401 Error in Execution " .. tostring(#vars) .. "\n") + server:send(vars) + else + local params = string.match(line, "--%s*(%b{})%s*$") + local pfunc = params and loadstring("return "..params) -- use internal function + params = pfunc and pfunc() + params = (type(params) == "table" and params or {}) + if params.nocode == nil then params.nocode = true end + if params.sparse == nil then params.sparse = false end + -- take into account additional levels for the stack frames and data management + if tonumber(params.maxlevel) then params.maxlevel = tonumber(params.maxlevel)+4 end + + local ok, res = pcall(mobdebug.dump, vars, params) + if ok then + server:send("200 OK " .. tostring(res) .. "\n") + else + server:send("401 Error in Execution " .. tostring(#res) .. "\n") + server:send(res) + end + end + elseif command == "OUTPUT" then + local _, _, stream, mode = string.find(line, "^[A-Z]+%s+(%w+)%s+([dcr])%s*$") + if stream and mode and stream == "stdout" then + -- assign "print" in the global environment + local default = mode == 'd' + genv.print = default and iobase.print or corowrap(function() + -- wrapping into coroutine.wrap protects this function from + -- being stepped through in the debugger. + -- don't use vararg (...) as it adds a reference for its values, + -- which may affect how they are garbage collected + while true do + local tbl = {coroutine.yield()} + if mode == 'c' then iobase.print(unpack(tbl)) end + for n = 1, #tbl do + tbl[n] = select(2, pcall(mobdebug.line, tbl[n], {nocode = true, comment = false})) end + local file = table.concat(tbl, "\t").."\n" + server:send("204 Output " .. stream .. " " .. tostring(#file) .. "\n" .. file) + end + end) + if not default then genv.print() end -- "fake" print to start printing loop + server:send("200 OK\n") + else + server:send("400 Bad Request\n") + end + elseif command == "EXIT" then + server:send("200 OK\n") + coroyield("exit") + else + server:send("400 Bad Request\n") + end + end +end + +local function output(stream, data) + if server then return server:send("204 Output "..stream.." "..tostring(#data).."\n"..data) end +end + +local function connect(controller_host, controller_port) + local sock, err = socket.tcp() + if not sock then return nil, err end + + if sock.settimeout then sock:settimeout(mobdebug.connecttimeout) end + local res, err = sock:connect(controller_host, tostring(controller_port)) + if sock.settimeout then sock:settimeout() end + + if not res then return nil, err end + return sock +end + +local lasthost, lastport + +-- Starts a debug session by connecting to a controller +local function start(controller_host, controller_port) + -- only one debugging session can be run (as there is only one debug hook) + if isrunning() then return end + + lasthost = controller_host or lasthost + lastport = controller_port or lastport + + controller_host = lasthost or "localhost" + controller_port = lastport or mobdebug.port + + local err + server, err = mobdebug.connect(controller_host, controller_port) + if server then + -- correct stack depth which already has some calls on it + -- so it doesn't go into negative when those calls return + -- as this breaks subsequence checks in stack_depth(). + -- start from 16th frame, which is sufficiently large for this check. + stack_level = stack_depth(16) + + coro_debugger = corocreate(debugger_loop) + debug.sethook(debug_hook, HOOKMASK) + seen_hook = nil -- reset in case the last start() call was refused + step_into = true -- start with step command + return true + else + print(("Could not connect to %s:%s: %s") + :format(controller_host, controller_port, err or "unknown error")) + end +end + +local function controller(controller_host, controller_port, scratchpad) + -- only one debugging session can be run (as there is only one debug hook) + if isrunning() then return end + + lasthost = controller_host or lasthost + lastport = controller_port or lastport + + controller_host = lasthost or "localhost" + controller_port = lastport or mobdebug.port + + local exitonerror = not scratchpad + local err + server, err = mobdebug.connect(controller_host, controller_port) + if server then + local function report(trace, err) + local msg = err .. "\n" .. trace + server:send("401 Error in Execution " .. tostring(#msg) .. "\n") + server:send(msg) + return err + end + + seen_hook = true -- allow to accept all commands + coro_debugger = corocreate(debugger_loop) + + while true do + step_into = true -- start with step command + abort = false -- reset abort flag from the previous loop + if scratchpad then checkcount = mobdebug.checkcount end -- force suspend right away + + coro_debugee = corocreate(debugee) + debug.sethook(coro_debugee, debug_hook, HOOKMASK) + local status, err = cororesume(coro_debugee, unpack(arg or {})) + + -- was there an error or is the script done? + -- 'abort' state is allowed here; ignore it + if abort then + if tostring(abort) == 'exit' then break end + else + if status then -- no errors + if corostatus(coro_debugee) == "suspended" then + -- the script called `coroutine.yield` in the "main" thread + error("attempt to yield from the main thread", 3) + end + break -- normal execution is done + elseif err and not string.find(tostring(err), deferror) then + -- report the error back + -- err is not necessarily a string, so convert to string to report + report(debug.traceback(coro_debugee), tostring(err)) + if exitonerror then break end + -- check if the debugging is done (coro_debugger is nil) + if not coro_debugger then break end + -- resume once more to clear the response the debugger wants to send + -- need to use capture_vars(0) to capture only two (default) levels, + -- as even though there is controller() call, because of the tail call, + -- the caller may not exist for it; + -- This is not entirely safe as the user may see the local + -- variable from console, but they will be reset anyway. + -- This functionality is used when scratchpad is paused to + -- gain access to remote console to modify global variables. + local status, err = cororesume(coro_debugger, events.RESTART, capture_vars(0)) + if not status or status and err == "exit" then break end + end + end + end + else + print(("Could not connect to %s:%s: %s") + :format(controller_host, controller_port, err or "unknown error")) + return false + end + return true +end + +local function scratchpad(controller_host, controller_port) + return controller(controller_host, controller_port, true) +end + +local function loop(controller_host, controller_port) + return controller(controller_host, controller_port, false) +end + +local function on() + if not (isrunning() and server) then return end + + -- main is set to true under Lua5.2 for the "main" chunk. + -- Lua5.1 returns co as `nil` in that case. + local co, main = coroutine.running() + if main then co = nil end + if co then + coroutines[co] = true + debug.sethook(co, debug_hook, HOOKMASK) + else + if jit then coroutines.main = true end + debug.sethook(debug_hook, HOOKMASK) + end +end + +local function off() + if not (isrunning() and server) then return end + + -- main is set to true under Lua5.2 for the "main" chunk. + -- Lua5.1 returns co as `nil` in that case. + local co, main = coroutine.running() + if main then co = nil end + + -- don't remove coroutine hook under LuaJIT as there is only one (global) hook + if co then + coroutines[co] = false + if not jit then debug.sethook(co) end + else + if jit then coroutines.main = false end + if not jit then debug.sethook() end + end + + -- check if there is any thread that is still being debugged under LuaJIT; + -- if not, turn the debugging off + if jit then + local remove = true + for _, debugged in pairs(coroutines) do + if debugged then remove = false; break end + end + if remove then debug.sethook() end + end +end + +-- Handles server debugging commands +local function handle(params, client, options) + -- when `options.verbose` is not provided, use normal `print`; verbose output can be + -- disabled (`options.verbose == false`) or redirected (`options.verbose == function()...end`) + local verbose = not options or options.verbose ~= nil and options.verbose + local print = verbose and (type(verbose) == "function" and verbose or print) or function() end + local file, line, watch_idx + local _, _, command = string.find(params, "^([a-z]+)") + if command == "run" or command == "step" or command == "out" + or command == "over" or command == "exit" then + client:send(string.upper(command) .. "\n") + client:receive("*l") -- this should consume the first '200 OK' response + while true do + local done = true + local breakpoint = client:receive("*l") + if not breakpoint then + print("Program finished") + return nil, nil, false + end + local _, _, status = string.find(breakpoint, "^(%d+)") + if status == "200" then + -- don't need to do anything + elseif status == "202" then + _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$") + if file and line then + print("Paused at file " .. file .. " line " .. line) + end + elseif status == "203" then + _, _, file, line, watch_idx = string.find(breakpoint, "^203 Paused%s+(.-)%s+(%d+)%s+(%d+)%s*$") + if file and line and watch_idx then + print("Paused at file " .. file .. " line " .. line .. " (watch expression " .. watch_idx .. ": [" .. watches[watch_idx] .. "])") + end + elseif status == "204" then + local _, _, stream, size = string.find(breakpoint, "^204 Output (%w+) (%d+)$") + if stream and size then + local size = tonumber(size) + local msg = size > 0 and client:receive(size) or "" + print(msg) + if outputs[stream] then outputs[stream](msg) end + -- this was just the output, so go back reading the response + done = false + end + elseif status == "401" then + local _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)$") + if size then + local msg = client:receive(tonumber(size)) + print("Error in remote application: " .. msg) + return nil, nil, msg + end + else + print("Unknown error") + return nil, nil, "Debugger error: unexpected response '" .. breakpoint .. "'" + end + if done then break end + end + elseif command == "done" then + client:send(string.upper(command) .. "\n") + -- no response is expected + elseif command == "setb" or command == "asetb" then + _, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$") + if file and line then + -- if this is a file name, and not a file source + if not file:find('^".*"$') then + file = string.gsub(file, "\\", "/") -- convert slash + file = removebasedir(file, basedir) + end + client:send("SETB " .. file .. " " .. line .. "\n") + if command == "asetb" or client:receive("*l") == "200 OK" then + set_breakpoint(file, line) + else + print("Error: breakpoint not inserted") + end + else + print("Invalid command") + end + elseif command == "setw" then + local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$") + if exp then + client:send("SETW " .. exp .. "\n") + local answer = client:receive("*l") + local _, _, watch_idx = string.find(answer, "^200 OK (%d+)%s*$") + if watch_idx then + watches[watch_idx] = exp + print("Inserted watch exp no. " .. watch_idx) + else + local _, _, size = string.find(answer, "^401 Error in Expression (%d+)$") + if size then + local err = client:receive(tonumber(size)):gsub(".-:%d+:%s*","") + print("Error: watch expression not set: " .. err) + else + print("Error: watch expression not set") + end + end + else + print("Invalid command") + end + elseif command == "delb" or command == "adelb" then + _, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$") + if file and line then + -- if this is a file name, and not a file source + if not file:find('^".*"$') then + file = string.gsub(file, "\\", "/") -- convert slash + file = removebasedir(file, basedir) + end + client:send("DELB " .. file .. " " .. line .. "\n") + if command == "adelb" or client:receive("*l") == "200 OK" then + remove_breakpoint(file, line) + else + print("Error: breakpoint not removed") + end + else + print("Invalid command") + end + elseif command == "delallb" then + local file, line = "*", 0 + client:send("DELB " .. file .. " " .. tostring(line) .. "\n") + if client:receive("*l") == "200 OK" then + remove_breakpoint(file, line) + else + print("Error: all breakpoints not removed") + end + elseif command == "delw" then + local _, _, index = string.find(params, "^[a-z]+%s+(%d+)%s*$") + if index then + client:send("DELW " .. index .. "\n") + if client:receive("*l") == "200 OK" then + watches[index] = nil + else + print("Error: watch expression not removed") + end + else + print("Invalid command") + end + elseif command == "delallw" then + for index, exp in pairs(watches) do + client:send("DELW " .. index .. "\n") + if client:receive("*l") == "200 OK" then + watches[index] = nil + else + print("Error: watch expression at index " .. index .. " [" .. exp .. "] not removed") + end + end + elseif command == "eval" or command == "exec" + or command == "load" or command == "loadstring" + or command == "reload" then + local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$") + if exp or (command == "reload") then + if command == "eval" or command == "exec" then + exp = exp:gsub("\r?\n", "\r"..SAFEWS) -- convert new lines, so the fragment can be passed as one line + if command == "eval" then exp = "return " .. exp end + client:send("EXEC " .. exp .. "\n") + elseif command == "reload" then + client:send("LOAD 0 -\n") + elseif command == "loadstring" then + local _, _, _, file, lines = string.find(exp, "^([\"'])(.-)%1%s(.+)") + if not file then + _, _, file, lines = string.find(exp, "^(%S+)%s(.+)") + end + client:send("LOAD " .. tostring(#lines) .. " " .. file .. "\n") + client:send(lines) + else + local file = io.open(exp, "r") + if not file and pcall(require, "winapi") then + -- if file is not open and winapi is there, try with a short path; + -- this may be needed for unicode paths on windows + winapi.set_encoding(winapi.CP_UTF8) + local shortp = winapi.short_path(exp) + file = shortp and io.open(shortp, "r") + end + if not file then return nil, nil, "Cannot open file " .. exp end + -- read the file and remove the shebang line as it causes a compilation error + local lines = file:read("*all"):gsub("^#!.-\n", "\n") + file:close() + + local fname = string.gsub(exp, "\\", "/") -- convert slash + fname = removebasedir(fname, basedir) + client:send("LOAD " .. tostring(#lines) .. " " .. fname .. "\n") + if #lines > 0 then client:send(lines) end + end + while true do + local params, err = client:receive("*l") + if not params then + return nil, nil, "Debugger connection " .. (err or "error") + end + local done = true + local _, _, status, len = string.find(params, "^(%d+).-%s+(%d+)%s*$") + if status == "200" then + len = tonumber(len) + if len > 0 then + local status, res + local str = client:receive(len) + -- handle serialized table with results + local func, err = loadstring(str) + if func then + status, res = pcall(func) + if not status then err = res + elseif type(res) ~= "table" then + err = "received "..type(res).." instead of expected 'table'" + end + end + if err then + print("Error in processing results: " .. err) + return nil, nil, "Error in processing results: " .. err + end + print(unpack(res)) + return res[1], res + end + elseif status == "201" then + _, _, file, line = string.find(params, "^201 Started%s+(.-)%s+(%d+)%s*$") + elseif status == "202" or params == "200 OK" then + -- do nothing; this only happens when RE/LOAD command gets the response + -- that was for the original command that was aborted + elseif status == "204" then + local _, _, stream, size = string.find(params, "^204 Output (%w+) (%d+)$") + if stream and size then + local size = tonumber(size) + local msg = size > 0 and client:receive(size) or "" + print(msg) + if outputs[stream] then outputs[stream](msg) end + -- this was just the output, so go back reading the response + done = false + end + elseif status == "401" then + len = tonumber(len) + local res = client:receive(len) + print("Error in expression: " .. res) + return nil, nil, res + else + print("Unknown error") + return nil, nil, "Debugger error: unexpected response after EXEC/LOAD '" .. params .. "'" + end + if done then break end + end + else + print("Invalid command") + end + elseif command == "listb" then + for l, v in pairs(breakpoints) do + for f in pairs(v) do + print(f .. ": " .. l) + end + end + elseif command == "listw" then + for i, v in pairs(watches) do + print("Watch exp. " .. i .. ": " .. v) + end + elseif command == "suspend" then + client:send("SUSPEND\n") + elseif command == "stack" then + local opts = string.match(params, "^[a-z]+%s+(.+)$") + client:send("STACK" .. (opts and " "..opts or "") .."\n") + local resp = client:receive("*l") + local _, _, status, res = string.find(resp, "^(%d+)%s+%w+%s+(.+)%s*$") + if status == "200" then + local func, err = loadstring(res) + if func == nil then + print("Error in stack information: " .. err) + return nil, nil, err + end + local ok, stack = pcall(func) + if not ok then + print("Error in stack information: " .. stack) + return nil, nil, stack + end + for _,frame in ipairs(stack) do + print(mobdebug.line(frame[1], {comment = false})) + end + return stack + elseif status == "401" then + local _, _, len = string.find(resp, "%s+(%d+)%s*$") + len = tonumber(len) + local res = len > 0 and client:receive(len) or "Invalid stack information." + print("Error in expression: " .. res) + return nil, nil, res + else + print("Unknown error") + return nil, nil, "Debugger error: unexpected response after STACK" + end + elseif command == "output" then + local _, _, stream, mode = string.find(params, "^[a-z]+%s+(%w+)%s+([dcr])%s*$") + if stream and mode then + client:send("OUTPUT "..stream.." "..mode.."\n") + local resp, err = client:receive("*l") + if not resp then + print("Unknown error: "..err) + return nil, nil, "Debugger connection error: "..err + end + local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$") + if status == "200" then + print("Stream "..stream.." redirected") + outputs[stream] = type(options) == 'table' and options.handler or nil + -- the client knows when she is doing, so install the handler + elseif type(options) == 'table' and options.handler then + outputs[stream] = options.handler + else + print("Unknown error") + return nil, nil, "Debugger error: can't redirect "..stream + end + else + print("Invalid command") + end + elseif command == "basedir" then + local _, _, dir = string.find(params, "^[a-z]+%s+(.+)$") + if dir then + dir = string.gsub(dir, "\\", "/") -- convert slash + if not string.find(dir, "/$") then dir = dir .. "/" end + + local remdir = dir:match("\t(.+)") + if remdir then dir = dir:gsub("/?\t.+", "/") end + basedir = dir + + client:send("BASEDIR "..(remdir or dir).."\n") + local resp, err = client:receive("*l") + if not resp then + print("Unknown error: "..err) + return nil, nil, "Debugger connection error: "..err + end + local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$") + if status == "200" then + print("New base directory is " .. basedir) + else + print("Unknown error") + return nil, nil, "Debugger error: unexpected response after BASEDIR" + end + else + print(basedir) + end + elseif command == "help" then + print("setb -- sets a breakpoint") + print("delb -- removes a breakpoint") + print("delallb -- removes all breakpoints") + print("setw -- adds a new watch expression") + print("delw -- removes the watch expression at index") + print("delallw -- removes all watch expressions") + print("run -- runs until next breakpoint") + print("step -- runs until next line, stepping into function calls") + print("over -- runs until next line, stepping over function calls") + print("out -- runs until line after returning from current function") + print("listb -- lists breakpoints") + print("listw -- lists watch expressions") + print("eval -- evaluates expression on the current context and returns its value") + print("exec -- executes statement on the current context") + print("load -- loads a local file for debugging") + print("reload -- restarts the current debugging session") + print("stack -- reports stack trace") + print("output stdout -- capture and redirect io stream (default|copy|redirect)") + print("basedir [] -- sets the base path of the remote application, or shows the current one") + print("done -- stops the debugger and continues application execution") + print("exit -- exits debugger and the application") + else + local _, _, spaces = string.find(params, "^(%s*)$") + if spaces then + return nil, nil, "Empty command" + else + print("Invalid command") + return nil, nil, "Invalid command" + end + end + return file, line +end + +-- Starts debugging server +local function listen(host, port) + host = host or "*" + port = port or mobdebug.port + + local socket = require "socket" + + print("Lua Remote Debugger") + print("Run the program you wish to debug") + + local server = socket.bind(host, port) + local client = server:accept() + + client:send("STEP\n") + client:receive("*l") + + local breakpoint = client:receive("*l") + local _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$") + if file and line then + print("Paused at file " .. file ) + print("Type 'help' for commands") + else + local _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)%s*$") + if size then + print("Error in remote application: ") + print(client:receive(size)) + end + end + + while true do + io.write("> ") + local file, _, err = handle(io.read("*line"), client) + if not file and err == false then break end -- completed debugging + end + + client:close() +end + +local cocreate +local function coro() + if cocreate then return end -- only set once + cocreate = cocreate or coroutine.create + coroutine.create = function(f, ...) + return cocreate(function(...) + mobdebug.on() + return f(...) + end, ...) + end +end + +local moconew +local function moai() + if moconew then return end -- only set once + moconew = moconew or (MOAICoroutine and MOAICoroutine.new) + if not moconew then return end + MOAICoroutine.new = function(...) + local thread = moconew(...) + -- need to support both thread.run and getmetatable(thread).run, which + -- was used in earlier MOAI versions + local mt = thread.run and thread or getmetatable(thread) + local patched = mt.run + mt.run = function(self, f, ...) + return patched(self, function(...) + mobdebug.on() + return f(...) + end, ...) + end + return thread + end +end + +-- make public functions available +mobdebug.setbreakpoint = set_breakpoint +mobdebug.removebreakpoint = remove_breakpoint +mobdebug.listen = listen +mobdebug.loop = loop +mobdebug.scratchpad = scratchpad +mobdebug.handle = handle +mobdebug.connect = connect +mobdebug.start = start +mobdebug.on = on +mobdebug.off = off +mobdebug.moai = moai +mobdebug.coro = coro +mobdebug.done = done +mobdebug.pause = function() step_into = true end +mobdebug.yield = nil -- callback +mobdebug.output = output +mobdebug.onexit = os and os.exit or done +mobdebug.onscratch = nil -- callback +mobdebug.basedir = function(b) if b then basedir = b end return basedir end + +return mobdebug diff --git a/util/demoCapture.sh b/util/demoCapture.sh index ee25b8c55..c3188401d 100755 --- a/util/demoCapture.sh +++ b/util/demoCapture.sh @@ -22,7 +22,7 @@ if [ -n "$OUT" ]; then fi STEP=20 REPO=$(cd "$(dirname "$0")/.." && pwd) -BIN=$(ls "$REPO"/.builddir/Singe-v*-Linux-x86_64 | head -1) +BIN=$(python3 "$REPO/util/singeBinary.py") # The demo's size, from its games.dat: the canvas the script draws and the window it is shown in. WIDTH=$(grep -o 'CANVAS_X *= *[0-9]*' "$REPO/demos/$DEMO/games.dat" | grep -o '[0-9]*$') HEIGHT=$(grep -o 'CANVAS_Y *= *[0-9]*' "$REPO/demos/$DEMO/games.dat" | grep -o '[0-9]*$') diff --git a/util/demoRecord.sh b/util/demoRecord.sh index 2cc7bd4ed..2a748d6b8 100755 --- a/util/demoRecord.sh +++ b/util/demoRecord.sh @@ -13,7 +13,7 @@ DEMO=$1 OUT=$(realpath -m "$2") STEP=20 REPO=$(cd "$(dirname "$0")/.." && pwd) -BIN=$(ls "$REPO"/.builddir/Singe-v*-Linux-x86_64 | head -1) +BIN=$(python3 "$REPO/util/singeBinary.py") # The demo's size, from its games.dat: the canvas the script draws and the window it is shown in. WIDTH=$(grep -o 'CANVAS_X *= *[0-9]*' "$REPO/demos/$DEMO/games.dat" | grep -o '[0-9]*$') HEIGHT=$(grep -o 'CANVAS_Y *= *[0-9]*' "$REPO/demos/$DEMO/games.dat" | grep -o '[0-9]*$') diff --git a/util/neonCoastCar.sh b/util/neonCoastCar.sh index 694b3fee2..b0791f564 100755 --- a/util/neonCoastCar.sh +++ b/util/neonCoastCar.sh @@ -7,7 +7,7 @@ set -euo pipefail REPO=$(cd "$(dirname "$0")/.." && pwd) -BIN=$(ls "$REPO"/.builddir/Singe-v*-Linux-x86_64 | head -1) +BIN=$(python3 "$REPO/util/singeBinary.py") SCRATCH=${SCRATCH:-/tmp/neonCoastCar} # The sheet's pixels to a design unit of the demo (kit.lua, K.sprite): each frame is 288 design # units wide, rendered from a view this many times 1024 by 768. diff --git a/util/renderMenuAd.py b/util/renderMenuAd.py index d980e52f0..b683113eb 100644 --- a/util/renderMenuAd.py +++ b/util/renderMenuAd.py @@ -20,6 +20,7 @@ import re import shutil import subprocess import sys +from singeBinary import singeBinary REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OFFSCREEN_W = 1024 # The biggest the offscreen video driver gives; larger goes under Xvfb. @@ -59,15 +60,6 @@ end ''' -def findBinary(): - folder = os.path.join(REPO, ".builddir", "linux-gcc", "x86_64", "singe") - for name in sorted(os.listdir(folder)): - path = os.path.join(folder, name) - if name.startswith("Singe") and os.path.isfile(path) and os.access(path, os.X_OK): - return path - raise SystemExit("no Singe binary in %s; build first" % folder) - - def render(binary, work, frames, width, height, step): env = dict(os.environ) env["SDL_AUDIO_DRIVER"] = "dummy" @@ -97,7 +89,7 @@ def main(): parser.add_argument("--keep", action="store_true", help="leave the frames behind") args = parser.parse_args() - binary = args.binary or findBinary() + binary = args.binary or singeBinary() work = os.path.join(REPO, ".builddir", "menuAd") frames = os.path.join(work, "frames") shutil.rmtree(frames, ignore_errors=True) diff --git a/util/renderMenuVideo.py b/util/renderMenuVideo.py index 745dc4acc..1c89a1732 100644 --- a/util/renderMenuVideo.py +++ b/util/renderMenuVideo.py @@ -32,6 +32,7 @@ import shutil import subprocess import sys from makeMenuSound import makeSound +from singeBinary import singeBinary REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -122,15 +123,6 @@ end ''' -def findBinary(): - folder = os.path.join(REPO, ".builddir", "linux-gcc", "x86_64", "singe") - for name in sorted(os.listdir(folder)): - path = os.path.join(folder, name) - if name.startswith("Singe") and os.path.isfile(path) and os.access(path, os.X_OK): - return path - raise SystemExit("no Singe binary in %s; build first" % folder) - - def render(binary, work, frames, step): # Offscreen rather than under an X server: Xvfb has no DRI3, so a hardware Vulkan device # cannot build a swapchain there and SDL quietly falls back to the software rasterizer. @@ -157,7 +149,7 @@ def main(): parser.add_argument("--keep", action="store_true", help="leave the frames behind") args = parser.parse_args() - binary = args.binary or findBinary() + binary = args.binary or singeBinary() work = os.path.join(REPO, ".builddir", "menuVideo") # The frames are screenshots, and screenshots belong in the repository's screenshots folder. frames = os.path.join(REPO, "screenshots", "menuBackdrop") diff --git a/util/renderReel.py b/util/renderReel.py index 14f4ee4b2..03fc0eff3 100644 --- a/util/renderReel.py +++ b/util/renderReel.py @@ -22,6 +22,7 @@ import re import shutil import subprocess import sys +from singeBinary import singeBinary REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -110,15 +111,6 @@ def framesIn(folder): return None, 0, 0 -def findBinary(): - folder = os.path.join(REPO, ".builddir", "linux-gcc", "x86_64", "singe") - for name in sorted(os.listdir(folder)): - path = os.path.join(folder, name) - if name.startswith("Singe") and os.path.isfile(path) and os.access(path, os.X_OK): - return path - raise SystemExit("no Singe binary in %s; build first" % folder) - - def engine(binary, work, frames, width, height, step, script, disc): env = dict(os.environ) env["SDL_AUDIO_DRIVER"] = "dummy" @@ -188,7 +180,7 @@ def main(): parser.add_argument("--keep", action="store_true", help="leave the frames behind") args = parser.parse_args() - binary = args.binary or findBinary() + binary = args.binary or singeBinary() work = os.path.join(REPO, ".builddir", "reel") os.makedirs(work, exist_ok=True) fps = 1000.0 / args.step diff --git a/util/shootMenu.py b/util/shootMenu.py index 39a50784d..4a98645bd 100644 --- a/util/shootMenu.py +++ b/util/shootMenu.py @@ -8,7 +8,7 @@ # Usage: python3 util/shootMenu.py (both renderers) # python3 util/shootMenu.py --renderer overlay # -# The engine used is the gcc preset's, as util/renderMenuVideo.py uses; build it first. The +# The engine used is the default preset's (util/singeBinary.py finds it); build it first. The # menu shown is the one that binary carries, so a change to the menu's files needs a build before # a shoot -- the script checks Menu.singe against assets and says so. import argparse @@ -16,6 +16,7 @@ import os import shutil import subprocess import sys +from singeBinary import singeBinary REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -115,15 +116,6 @@ end ''' -def findBinary(): - folder = os.path.join(REPO, ".builddir", "linux-gcc", "x86_64", "singe") - for name in sorted(os.listdir(folder)): - path = os.path.join(folder, name) - if name.startswith("Singe") and os.path.isfile(path) and os.access(path, os.X_OK): - return path - raise SystemExit("no Singe binary in %s; build first" % folder) - - def sheet(renderer, folder): names = sorted(f for f in os.listdir(folder) if f.endswith(".png")) out = os.path.join(REPO, "screenshots", "menu", "sheet-%s.png" % renderer) @@ -186,7 +178,7 @@ def main(): parser.add_argument("--binary", help="the engine to shoot with, in place of the built one") args = parser.parse_args() - binary = args.binary or findBinary() + binary = args.binary or singeBinary() for renderer in ([args.renderer] if args.renderer else RENDERERS): shoot(binary, renderer) diff --git a/util/singeBinary.py b/util/singeBinary.py new file mode 100644 index 000000000..57b40696b --- /dev/null +++ b/util/singeBinary.py @@ -0,0 +1,33 @@ +# The Singe the util scripts run: the one the default preset (linux-x86_64) leaves in .builddir, +# named for the version in CMakeLists.txt exactly as the build names it. Looking it up by name +# rather than taking the first Singe in a folder means a binary an older version or another preset +# left behind is never the one recorded with: sorted, Singe-v3.00- comes before Singe-v3.00b1-. +# Run as a script it prints the path, for the shell scripts: BIN=$(python3 util/singeBinary.py) +import os +import re + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def singeBinary(): + path = os.path.join(REPO, ".builddir", "Singe-v%s-Linux-x86_64" % singeVersion()) + if not os.access(path, os.X_OK): + raise SystemExit("no %s; build the linux-x86_64 preset first" % os.path.relpath(path, REPO)) + return path + + +def singeVersion(): + # The four numbers of the project() line: patch 1 means a beta and tweak says which, so + # 3.00.1.2 is 3.00b2, as CMakeLists.txt makes SINGE_VERSION_FULL. + text = open(os.path.join(REPO, "CMakeLists.txt"), encoding="utf-8").read() + found = re.search(r"^project\(singe2 VERSION (\d+)\.(\d+)\.(\d+)\.(\d+)", text, re.M) + if not found: + raise SystemExit("CMakeLists.txt has no four-number project() version") + major, minor, patch, tweak = found.groups() + if patch == "1": + return "%s.%sb%s" % (major, minor, tweak) + return "%s.%s" % (major, minor) + + +if __name__ == "__main__": + print(singeBinary()) diff --git a/util/zbstudioApi.py b/util/zbstudioApi.py new file mode 100644 index 000000000..6ee0b6d83 --- /dev/null +++ b/util/zbstudioApi.py @@ -0,0 +1,551 @@ +# Builds ZeroBrane Studio's autocompletion and tooltips for Singe, zbstudio/packages/singe/api*.lua, +# from two sources that cannot drift apart: the names a running Singe really defines, and what the +# Reference says about each one. One file per language the Reference is kept in; the package picks +# the one matching ZeroBrane Studio's own language setting. +# +# The names come from Singe itself, run headless on a script that lists its globals after +# Framework.singe has loaded, so nothing here has to know how the engine registers them. The words +# come from each language's Reference.adoc: an entry's signatures, its first paragraph, its Returns +# and Since lines, and, for the constants, the Engine Constants table. Argument types come from the +# English Parameters lists whatever the language, since Lua's type names are English everywhere. +# +# Along the way it lists every engine function the Reference has no entry for, and every entry +# naming a function the engine does not define, which is how a missing entry gets found. --strict +# makes either kind fatal. +# +# Usage: python3 util/zbstudioApi.py [--singe BINARY] [--strict] +import glob +import os +import re +import subprocess +import sys +import tempfile +from singeBinary import singeBinary + + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DOCS = os.path.join(REPO, "docs") +OUT = os.path.join(REPO, "zbstudio", "packages", "singe") +DUMP_TAG = "ZBSTUDIO_API" +TIMEOUT = 120 +TYPE_WORDS = ("integer", "number", "string", "boolean", "table", "function", "nil") + +# How each language writes a range of names: `GAMEPAD_0` to `GAMEPAD_3`. +RANGE_WORDS = "to|\u00e0|bis|a" + +# Globals whose value depends on the run that listed them, so the tooltip gives no value. +VARYING = ("DIR", "SINGE_DEAD_ZONE", "SINGE_DISC", "SINGE_LEGACY_SPRITE_ARGS", "SINGE_TRIGGER_THRESHOLD") + +# The label words of an entry, per language, as the translations write them. Non-ASCII letters are +# escaped so this file stays ASCII. +LABELS = { + "en": {"Parameters": "params", "Returns": "returns", "Notes": "notes", "Since": "since", "See also": "seeAlso"}, + "fr": {"Param\u00e8tres": "params", "Renvoie": "returns", "Remarques": "notes", "Depuis": "since", "Voir aussi": "seeAlso"}, + "it": {"Parametri": "params", "Restituisce": "returns", "Note": "notes", "Disponibile da": "since", "Vedi anche": "seeAlso"}, + "de": {"Parameter": "params", "R\u00fcckgabe": "returns", "Hinweise": "notes", "Seit": "since", "Siehe auch": "seeAlso"}, + "es": {"Par\u00e1metros": "params", "Devuelve": "returns", "Notas": "notes", "Desde": "since", "V\u00e9ase tambi\u00e9n": "seeAlso"}, +} + +# The standard library, which ZeroBrane Studio already describes. +STANDARD = set(""" + _G _VERSION assert collectgarbage coroutine debug dofile error getmetatable io ipairs load loadfile + math next os package pairs pcall print rawequal rawget rawlen rawset require select setmetatable + string table tonumber tostring type utf8 warn xpcall +""".split()) + +# Lists the globals, one line each, and the members of every table that is not the standard +# library's. A member that is itself a table (SCANCODE.LEFT) shows its own keys, so the tooltip can +# say what is in it. +DUMP_SCRIPT = """ +dofile("Singe/Framework.singe") +local function scalar(v) + if type(v) == "string" or type(v) == "number" or type(v) == "boolean" then + return tostring(v):gsub("[\\t\\n]", " ") + end + return "" +end +local skip = {} +for name in ([[%s]]):gmatch("%%S+") do + skip[name] = true +end +for name, value in pairs(_G) do + if type(name) == "string" and not skip[name] then + -- A function's identity, so an alias can be told apart from the function it names. + debugPrint("%s\\tG\\t" .. name .. "\\t" .. type(value) .. "\\t" .. ((type(value) == "function") and tostring(value) or scalar(value))) + if type(value) == "table" then + for key, member in pairs(value) do + if type(key) == "string" then + local keys = "" + if type(member) == "table" then + local list = {} + for k, v in pairs(member) do + list[#list + 1] = tostring(k) .. ((scalar(v) ~= "") and (" = " .. ((type(v) == "string") and ("\\"" .. scalar(v) .. "\\"") or scalar(v))) or "") + end + table.sort(list) + keys = table.concat(list, ", ") + end + debugPrint("%s\\tC\\t" .. name .. "\\t" .. key .. "\\t" .. type(member) .. "\\t" .. (keys ~= "" and keys or scalar(member))) + end + end + end + end +end +singeQuit() +""" + + +# Undocumented globals that are the very function a documented name is: Framework.singe's +# discGetLanguageDescription is videoGetLanguageDescription, for one. +def findAliases(globalsFound, english): + byIdentity = {} + aliases = {} + for name in sorted(english): + found = globalsFound.get(name) + if found and (found["type"] == "function"): + byIdentity.setdefault(found["value"], name) + for name, found in sorted(globalsFound.items()): + if (found["type"] == "function") and (name not in english) and (found["value"] in byIdentity): + aliases[name] = byIdentity[found["value"]] + return aliases + + +def asciiDocToText(text): + text = re.sub(r"<<[^,>]+,([^>]+)>>", r"\1", text) + text = re.sub(r"<<([^>]+)>>", r"\1", text) + text = re.sub(r"xref:[^\[]+\[([^\]]*)\]", r"\1", text) + text = re.sub(r"(https?://\S+?)\[([^\]]*)\]", r"\2 (\1)", text) + text = re.sub(r"`\+?(.*?)\+?`", r"\1", text) + text = re.sub(r"(? 1): + parts.append("\n".join(entry["signatures"])) + if entry["paragraphs"]: + parts.append(asciiDocToText(entry["paragraphs"][0])) + for warning in entry["warnings"]: + parts.append(asciiDocToText(warning)) + for field in ("returns", "since"): + if entry["fields"].get(field): + parts.append(entry["fieldLabels"][field] + " " + asciiDocToText(entry["fields"][field])) + return "\n".join(parts) + + +# The rows of every table in lines, as (cells, line number) pairs. A cell starts at a '|'; a row is +# as many cells as the table has columns, whether they sit on one line or one a line. +def parseTables(lines): + rows = [] + cells = None + columns = 0 + start = 0 + inListing = False + for number, line in enumerate(lines): + if re.match(r"^-{4,}$", line): + inListing = not inListing + continue + if inListing: + continue + attribute = re.match(r'^\[cols="([^"]+)"', line) + if attribute: + columns = len(attribute.group(1).split(",")) + continue + if line.startswith("|==="): + if cells is None: + cells = [] + first = True + start = number + else: + for i in range(0, len(cells) - columns + 1, max(columns, 1)): + rows.append((cells[i:i + columns], start)) + cells = None + columns = 0 + continue + if cells is None: + continue + if line.startswith("|"): + parts = [part.strip() for part in line.split("|")[1:]] + if first and (columns == 0): + columns = len(parts) + first = False + cells.extend(parts) + elif line.strip() and cells: + cells[-1] = (cells[-1] + " " + line.strip()).strip() + return rows + + +def parseEntry(lines, labels): + entry = {"signatures": [], "paragraphs": [], "warnings": [], "params": [], "fields": {}, "fieldLabels": {}} + index = 0 + paragraph = [] + field = None + inListing = False + + # The signature block is the first listing. + while index < len(lines): + if lines[index].startswith("[source"): + index += 2 + while (index < len(lines)) and (lines[index] != "----"): + if lines[index].strip() and (lines[index].strip() != "end"): + entry["signatures"].append(lines[index].strip()) + index += 1 + index += 1 + break + index += 1 + + def flush(): + if paragraph: + text = " ".join(paragraph).strip() + if field == "params": + pass + elif field is not None: + entry["fields"][field] = (entry["fields"].get(field, "") + " " + text).strip() + elif re.match(r"^(WARNING|IMPORTANT|CAUTION):", text): + entry["warnings"].append(text) + else: + entry["paragraphs"].append(text) + paragraph.clear() + + while index < len(lines): + line = lines[index] + index += 1 + if re.match(r"^-{4,}$", line) or re.match(r"^\.{4,}$", line): + inListing = not inListing + continue + if inListing: + continue + if line.startswith(".Example") or line.startswith("[source") or line.startswith("|===") or line.startswith("[cols"): + flush() + field = "skip" + continue + label = re.match(r"^\*([^*]+?)\s*:\*\s*(.*)$", line.replace("\u00a0", " ").replace("\u202f", " ")) + if label and (label.group(1).strip() in labels): + flush() + field = labels[label.group(1).strip()] + entry["fieldLabels"][field] = label.group(1).strip() + ":" + if label.group(2): + paragraph.append(label.group(2)) + continue + if field == "params": + bullet = re.match(r"^\*\s+(.*)$", line) + if bullet: + entry["params"].append(bullet.group(1)) + elif line.strip() and entry["params"]: + entry["params"][-1] += " " + line.strip() + elif not line.strip() and entry["params"]: + field = "skip" + continue + if field == "skip": + continue + if not line.strip(): + flush() + if field is not None: + field = "skip" + continue + if line.startswith("[") and line.endswith("]"): + continue + paragraph.append(line.strip()) + flush() + entry["fields"].pop("skip", None) + return entry + + +# Each language's API chapter entries, keyed by the names in the heading, and the Engine +# Constants table as (names cell, meaning) rows. +def parseReference(path, lang): + lines = open(path, encoding="utf-8").read().split("\n") + headings = [] + pendingId = None + inListing = False + for number, line in enumerate(lines): + if re.match(r"^-{4,}$", line): + inListing = not inListing + continue + if inListing: + continue + anchor = re.match(r"^\[(?:#|\[)([^\],]+)", line) + if anchor and (line.endswith("]") or line.endswith("]]")): + pendingId = anchor.group(1) + continue + heading = re.match(r"^(=+) (.+)$", line) + if heading: + headings.append({"level": len(heading.group(1)), "title": heading.group(2).strip(), "id": pendingId or autoId(heading.group(2)), "line": number}) + pendingId = None + elif line.strip(): + pendingId = None + for i, heading in enumerate(headings): + end = len(lines) + for later in headings[i + 1:]: + if later["level"] <= heading["level"]: + end = later["line"] + break + heading["end"] = end + chapter = next(h for h in headings if h["id"] == "reference") + entries = {} + for heading in headings: + if (heading["level"] != 4) or not (chapter["line"] < heading["line"] < chapter["end"]): + continue + names = [n.strip() for n in heading["title"].split("/")] + if not all(re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", n) for n in names): + continue + entry = parseEntry(lines[heading["line"] + 1:heading["end"]], LABELS[lang]) + entry["anchor"] = heading["id"] + for name in names: + entries[name] = entry + # The calls the chapter's tables describe, the aliases and the score calls among them, for any + # name without an entry of its own. + for row, number in parseTables(lines[chapter["line"]:chapter["end"]]): + if len(row) < 2: + continue + # The Reference opens at the section holding the table. + number += chapter["line"] + section = max((h for h in headings if (h["line"] < number) and (h["level"] >= 3)), key=lambda h: h["line"]) + for token in re.findall(r"`([A-Za-z_][A-Za-z0-9_]*(?:\([^`]*\))?)`", row[0]): + name = re.match(r"^[A-Za-z_][A-Za-z0-9_]*", token).group(0) + # Constants are named in capitals and described by the Engine Constants table. + if (name in entries) or (name.upper() == name): + continue + signature = token if "(" in token else "" + text = " ".join(cell for cell in row[1:] if cell not in ("--", "")) + entries[name] = {"signatures": [signature] if signature else [], "paragraphs": [text], "warnings": [], "params": [], "fields": {}, "fieldLabels": {}, "anchor": section["id"]} + # The Engine Constants rows; the book's paragraphs cover the rest, what Framework.singe adds among them. + constants = [] + section = next(h for h in headings if h["id"] == "_engine_constants") + for line in lines[section["line"] + 1:section["end"]]: + row = re.match(r"^\|\s*(.+?)\s*\|\s*(.+)$", line) + if row and ("`" in row.group(1)): + constants.append((row.group(1), row.group(2))) + return entries, {"table": constants, "paragraphs": bookParagraphs(lines)} + + +# Where a constant is described: its Engine Constants row, a row naming its range or family, the +# first paragraph naming it, or the first naming its family. +def constantMeaning(name, constants): + exact = re.compile(r"`%s[`.\[]" % re.escape(name)) + + def family(cell): + for token in re.findall(r"`([A-Z][A-Z0-9_]*\*?)`", cell): + prefix = token[:-1] if token.endswith("*") else token.rsplit("_", 1)[0] + "_" + ranged = token.endswith("*") or ("..." in cell) or re.search(r"`%s`\s+(?:%s)\s+`" % (re.escape(token), RANGE_WORDS), cell) + if ranged and name.startswith(prefix): + return True + return False + + for test in (lambda cell: exact.search(cell), family): + for cell, meaning in constants["table"]: + if test(cell): + return asciiDocToText(meaning) + for test in (lambda cell: exact.search(cell), family): + for paragraph in constants["paragraphs"]: + if test(paragraph): + return asciiDocToText(paragraph) + return "" + + +# Every prose paragraph of a book, in order, without listings, tables or block attributes. +def bookParagraphs(lines): + paragraphs = [] + paragraph = [] + inListing = False + inTable = False + for line in lines + [""]: + if re.match(r"^-{4,}$", line) or re.match(r"^\.{4,}$", line): + inListing = not inListing + continue + if line.startswith("|==="): + inTable = not inTable + continue + if inListing or inTable: + continue + if line.strip() and not line.startswith(("[", "=", "*", ".", "//")): + paragraph.append(line.strip()) + elif paragraph: + paragraphs.append(" ".join(paragraph)) + paragraph = [] + return paragraphs + + +def argList(entry): + forms = [] + for signature in entry["signatures"]: + call = re.match(r"^(?:function\s+)?(?:[\w, ]+?\s*=\s*)?[A-Za-z_][\w.:]*\s*\((.*)\)\s*$", signature) + if call: + forms.append([a.strip() for a in call.group(1).split(",") if a.strip()]) + if not forms: + return "()", True + types = {} + for param in entry["params"]: + names = re.findall(r"`([A-Za-z_][A-Za-z0-9_]*)`", param.split("--")[0]) + after = param.split("--", 1)[1].strip() if "--" in param else "" + words = re.sub(r"^optional\s+", "", after).split(";")[0].split(",")[0].strip() + kind = words.split(" ")[0] if words.split(" ")[0] in TYPE_WORDS else "" + for name in names: + types[name] = kind + longest = max(forms, key=len) + shortest = min(forms, key=len) + prefixed = all(form == longest[:len(form)] for form in forms) + + def typed(name): + return name + (": " + types[name] if types.get(name) else "") + + if not prefixed: + return "(" + ", ".join(typed(n) for n in forms[0]) + ")", True + required = ", ".join(typed(n) for n in shortest) + optional = ", ".join(typed(n) for n in longest[len(shortest):]) + if optional: + return "(" + required + (" [, " if required else "[") + optional + "])", False + return "(" + required + ")", False + + +def returnList(entry): + lhs = None + for signature in entry["signatures"]: + match = re.match(r"^([\w, ]+?)\s*=\s*[A-Za-z_]", signature) + if match: + lhs = [n.strip() for n in match.group(1).split(",")] + break + if not lhs: + return "()" + first = entry["fields"].get("returns", "").split(" ")[0].strip(".,;").lower() + if (len(lhs) == 1) and (first in TYPE_WORDS): + return "(" + first + ")" + return "(" + ", ".join(lhs) + ")" + + +def build(globalsFound, english, entries, constants): + api = {} + for name, entry in sorted(entries.items()): + englishEntry = english.get(name, entry) + args, allForms = argList(englishEntry) + api[name] = {"type": "function", "description": describe(entry, allForms), "args": args, "returns": returnList(englishEntry)} + # Where the package's Look Up opens the Reference, when it is not the name itself. + if englishEntry["anchor"] != name.lower(): + api[name]["anchor"] = englishEntry["anchor"] + for name, found in sorted(globalsFound.items()): + if name in api: + continue + if found["type"] in ("number", "string", "boolean"): + meaning = constantMeaning(name, constants) + value = found["value"] if found["type"] != "string" else "\"" + found["value"] + "\"" + if name in VARYING: + api[name] = {"type": "value", "description": meaning} + else: + api[name] = {"type": "value", "description": (meaning + "\n" if meaning else "") + "= " + value} + elif (found["type"] == "table") and found["members"]: + childs = {} + for key, member in sorted(found["members"].items()): + if member["type"] == "table": + childs[key] = {"type": "value", "description": "{ " + member["value"] + " }"} + elif member["type"] in ("number", "string", "boolean"): + childs[key] = {"type": "value", "description": "= " + member["value"]} + elif member["type"] == "function": + childs[key] = {"type": "function", "description": "", "args": "(...)", "returns": "()"} + meaning = constantMeaning(name, constants) + api[name] = {"type": "lib", "description": meaning, "childs": childs} + return api + + +def writeLua(path, api, lang): + out = [] + out.append("-- Singe's API for ZeroBrane Studio's autocompletion and tooltips (%s)." % lang) + out.append("-- Generated by util/zbstudioApi.py from the Reference and Singe's own globals; do not edit.") + out.append("return {") + + def emit(table, indent): + for name, item in sorted(table.items()): + out.append("%s%s = {" % (indent, name if re.match(r"^[A-Za-z_]\w*$", name) else "[" + luaString(name) + "]")) + for field in ("type", "description", "args", "returns", "anchor"): + if field in item: + out.append("%s\t%s = %s," % (indent, field, luaString(item[field]))) + if "childs" in item: + out.append("%s\tchilds = {" % indent) + emit(item["childs"], indent + "\t\t") + out.append("%s\t}," % indent) + out.append("%s}," % indent) + + emit(api, "\t") + out.append("}") + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(out) + "\n") + + +def main(): + strict = "--strict" in sys.argv + if "--singe" in sys.argv: + singe = sys.argv[sys.argv.index("--singe") + 1] + else: + singe = singeBinary() + if not os.access(singe, os.X_OK): + sys.exit("No Singe binary at %s to ask for its globals; build one or pass --singe." % singe) + globalsFound = dumpGlobals(singe) + english, englishConstants = parseReference(os.path.join(DOCS, "Reference.adoc"), "en") + + functions = set(n for n, g in globalsFound.items() if g["type"] == "function") + callbacks = set(n for n, e in english.items() if any(sig.startswith("function ") for sig in e["signatures"])) + aliases = findAliases(globalsFound, english) + undocumented = sorted(functions - set(english) - set(aliases)) + unknown = sorted(set(english) - functions - callbacks) + for name in undocumented: + print("Not in the Reference: %s" % name) + for name in unknown: + print("In the Reference but not defined: %s" % name) + + os.makedirs(OUT, exist_ok=True) + for lang in languages(): + path = os.path.join(DOCS, "Reference.adoc") if lang == "en" else os.path.join(DOCS, lang, "Reference.adoc") + entries, constants = (english, englishConstants) if lang == "en" else parseReference(path, lang) + api = build(globalsFound, english, entries, constants) + for alias, target in aliases.items(): + api[alias] = dict(api[target]) + api[alias]["description"] = "= " + target + "\n" + api[target]["description"] + api[alias]["anchor"] = api[target].get("anchor", target.lower()) + target = os.path.join(OUT, "api.lua" if lang == "en" else "api-%s.lua" % lang) + writeLua(target, api, lang) + print("%s %d names, %d bytes" % (os.path.relpath(target, REPO), len(api), os.path.getsize(target))) + if strict and (undocumented or unknown): + sys.exit(1) + + +main() diff --git a/zbstudio/Singe.fbp b/zbstudio/Singe.fbp deleted file mode 100644 index 3727dbc10..000000000 --- a/zbstudio/Singe.fbp +++ /dev/null @@ -1,656 +0,0 @@ - - - - - ; - C++ - 1 - source_name - 0 - 0 - res - UTF-8 - connect - - 1000 - none - - - 0 - MyProject1 - - . - - 1 - 1 - 1 - 1 - UI - 0 - 0 - 0 - - 0 - wxAUI_MGR_DEFAULT - - wxBOTH - - 1 - 0 - 1 - impl_virtual - - - - 0 - wxID_ANY - - 500,-1 - dlgSinge - - - wxDEFAULT_DIALOG_STYLE - ; ; forward_declare - Singe Configuration - - 0 - - - - - 2 - wxBOTH - - - 0 - - fgSizer1 - wxFLEX_GROWMODE_SPECIFIED - none - 0 - 0 - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Singe: - 0 - - 0 - - - 0 - - 1 - lblSinge - 1 - - - protected - 1 - - Resizable - 1 - - wxALIGN_RIGHT - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select Singe Binary - - 0 - - 1 - filePickSinge - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN|wxFLP_USE_TEXTCTRL - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - /home/scott/Singe/Singe-v2.10-Linux-x86_64 - *.* - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Script: - 0 - - 0 - - - 0 - - 1 - lblScript - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select a file - - 0 - - 1 - filePickScript - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN|wxFLP_USE_TEXTCTRL - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - *.* - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Video: - 0 - - 0 - - - 0 - - 1 - lblVideo - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - Select a file - - 0 - - 1 - filePickVideo - 1 - - - protected - 1 - - Resizable - 1 - - wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN|wxFLP_USE_TEXTCTRL - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - *.* - - - - - - - 5 - wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Options: - 0 - - 0 - - - 0 - - 1 - lblOptions - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxALL|wxEXPAND - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - - 0 - - 1 - txtOptions - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - - 5 - wxEXPAND - 1 - - 20 - protected - 0 - - - - 5 - wxEXPAND - 1 - - 20 - protected - 0 - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 5 - wxEXPAND - 1 - - 0 - 1 - 0 - 0 - 0 - 1 - 0 - 0 - - m_sdbSizer1 - protected - m_sdbSizer1OnCancelButtonClick - m_sdbSizer1OnOKButtonClick - - - - 5 - wxEXPAND - 1 - - 20 - protected - 0 - - - - 5 - wxEXPAND - 1 - - 20 - protected - 0 - - - - - - diff --git a/zbstudio/packages/singe.lua b/zbstudio/packages/singe.lua new file mode 100644 index 000000000..203b4cf39 --- /dev/null +++ b/zbstudio/packages/singe.lua @@ -0,0 +1,627 @@ +--[[ + * + * Singe 3 + * Copyright (C) 2006-2026 Scott Duensing + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * +--]] + + +-- Singe for ZeroBrane Studio. Adds Singe as an interpreter, so Project > Run (F6) and Project > +-- Start Debugging (F5) launch the game, with breakpoints, stepping, the Stack and Watch windows, and +-- a stop in the IDE when an error is about to end the game; autocompletion and tooltips for the whole +-- Singe API, in ZeroBrane Studio's language when the Reference is kept in it; .singe files edited as +-- Lua, .rml as XML and .rcss as CSS; a toolbar button; and a way into the Reference from the Help +-- menu and from the word under the mouse. +-- +-- Install by adding one line to ZeroBrane Studio's user.lua (Edit > Preferences > Settings: User), +-- pointing at the copy Singe unpacks into its own folder, which stays current as Singe is updated: +-- +-- package "/path/to/Singe/Singe/ZeroBrane/singe.lua" +-- +-- or by copying singe.lua and the singe folder beside it into ZeroBrane Studio's packages folder. +-- +-- Settings, all optional, also in user.lua: +-- +-- path.singe = "/path/to/Singe-v3.00-Linux-x86_64" -- otherwise found, or asked for once +-- singe = { reload = false } -- do not pass --reload +-- singe = { reference = "/path/to/Reference.html" } -- otherwise found beside Singe + + +local MENU_CHOOSE = ID("singe.choose") +local MENU_DEBUG = ID("singe.debug") +local MENU_LOOKUP = ID("singe.lookup") +local MENU_REFERENCE = ID("singe.reference") +local MENU_RELOAD = ID("singe.reload") +local INTERPRETER = "singe" +local STOP_FILE = "Debugger.singe" -- Where Singe stops when an error is about to end the game +local LOOPBACK = "127.0.0.1" + +-- What each platform's Singe is called: Singe-v3.00b1-Linux-x86_64, Singe-v3.00-Windows-x86_64.exe. +local PLATFORM = { + Windows = { name = "Windows", suffix = "%.exe$" }, + Unix = { name = "Linux", suffix = "$" }, + Macintosh = { name = "Macos", suffix = "$" }, +} +-- ZeroBrane Studio's own word for a Linux machine's architecture, from uname, as Singe spells it. +local ARCHITECTURE = { x64 = "x86_64", x86 = "x86", aarch64 = "aarch64", armhf = "armhf" } + +-- The package's own words, in ZeroBrane Studio's language when the Reference is kept in it. +-- Written in decimal escapes, so this file stays ASCII: \195\169 is a UTF-8 e acute. +local WORDS = { + en = { + debug = "Debug With Singe", + choose = "Choose Singe Executable...", + reload = "Reload Singe When a Script Changes", + reference = "Singe Reference", + lookUp = "Look Up in Singe Reference", + chooseTitle = "Choose the Singe executable", + noReference = "The Singe Reference was not found. Unpack the documentation download (Singe-Docs) beside Singe, or set singe = { reference = \"/path/to/Reference.html\" } in user.lua.", + stopped = "Singe stopped at an error: %s", + stoppedHelp = "The Stack window shows where it happened; Continue (F5) lets the game end as it would have.", + nothingRun = "Singe: no executable chosen, so nothing was run.", + using = "Singe: using %s.", + }, + fr = { + debug = "D\195\169boguer avec Singe", + choose = "Choisir l'ex\195\169cutable de Singe...", + reload = "Relancer Singe quand un script change", + reference = "R\195\169f\195\169rence de Singe", + lookUp = "Chercher dans la R\195\169f\195\169rence de Singe", + chooseTitle = "Choisissez l'ex\195\169cutable de Singe", + noReference = "La R\195\169f\195\169rence de Singe est introuvable. D\195\169compressez le t\195\169l\195\169chargement de la documentation (Singe-Docs) \195\160 c\195\180t\195\169 de Singe, ou indiquez singe = { reference = \"/chemin/vers/Reference.html\" } dans user.lua.", + stopped = "Singe s'est arr\195\170t\195\169 sur une erreur : %s", + stoppedHelp = "La fen\195\170tre Pile d'ex\195\169cution montre o\195\185 elle s'est produite ; Continuer (F5) laisse le jeu se terminer comme il l'aurait fait.", + nothingRun = "Singe : aucun ex\195\169cutable choisi, rien n'a \195\169t\195\169 lanc\195\169.", + using = "Singe : utilise %s.", + }, + it = { + debug = "Debug con Singe", + choose = "Scegli l'eseguibile di Singe...", + reload = "Ricarica Singe quando cambia uno script", + reference = "Riferimento di Singe", + lookUp = "Cerca nel Riferimento di Singe", + chooseTitle = "Scegli l'eseguibile di Singe", + noReference = "Il Riferimento di Singe non \195\168 stato trovato. Decomprimi il download della documentazione (Singe-Docs) accanto a Singe, oppure imposta singe = { reference = \"/percorso/di/Reference.html\" } in user.lua.", + stopped = "Singe si \195\168 fermato su un errore: %s", + stoppedHelp = "La finestra Stack mostra dove \195\168 successo; Continua (F5) lascia che il gioco finisca come avrebbe fatto.", + nothingRun = "Singe: nessun eseguibile scelto, quindi non \195\168 stato avviato nulla.", + using = "Singe: uso %s.", + }, + de = { + debug = "Mit Singe debuggen", + choose = "Singe-Programm w\195\164hlen...", + reload = "Singe neu laden, wenn sich ein Skript \195\164ndert", + reference = "Singe-Referenz", + lookUp = "In der Singe-Referenz nachschlagen", + chooseTitle = "Das Singe-Programm w\195\164hlen", + noReference = "Die Singe-Referenz wurde nicht gefunden. Entpacke den Dokumentations-Download (Singe-Docs) neben Singe, oder setze singe = { reference = \"/pfad/zu/Reference.html\" } in user.lua.", + stopped = "Singe hat bei einem Fehler angehalten: %s", + stoppedHelp = "Das Fenster Stack zeigt, wo er passiert ist; Fortsetzen (F5) l\195\164sst das Spiel enden, wie es geendet h\195\164tte.", + nothingRun = "Singe: kein Programm gew\195\164hlt, daher wurde nichts gestartet.", + using = "Singe: verwende %s.", + }, + es = { + debug = "Depurar con Singe", + choose = "Elegir el ejecutable de Singe...", + reload = "Recargar Singe cuando cambie un script", + reference = "Referencia de Singe", + lookUp = "Buscar en la Referencia de Singe", + chooseTitle = "Elige el ejecutable de Singe", + noReference = "No se encontr\195\179 la Referencia de Singe. Descomprime la descarga de la documentaci\195\179n (Singe-Docs) junto a Singe, o pon singe = { reference = \"/ruta/a/Reference.html\" } en user.lua.", + stopped = "Singe se detuvo en un error: %s", + stoppedHelp = "La ventana Pila muestra d\195\179nde ocurri\195\179; Continuar (F5) deja que el juego termine como lo habr\195\173a hecho.", + nothingRun = "Singe: no se eligi\195\179 ning\195\186n ejecutable, as\195\173 que no se ejecut\195\179 nada.", + using = "Singe: se usa %s.", + }, +} + +-- The sizes of the dragon Singe unpacks beside this file, smallest first. +local ICON_SIZES = { 16, 24, 32, 48 } + + +local api = {} -- The loaded API, which also says where Look Up opens the Reference +local apiTable = nil +local architectures = nil +local findSinge = nil +local gameScript = nil +local isStopFile = nil +local language = nil +local lookUp = nil +local lookUpPosition = nil -- Where the editor's context menu was opened +local openReference = nil +local packageDir = nil +local parentFolder = nil +local plugin = nil +local referencePage = nil +local reloading = nil +local saveSetting = nil +local singeFolder = nil +local singeVersion = nil +local specmapOld = {} +local subfolders = nil +local toolbarBitmap = nil +local withSeparator = nil +local word = nil + + +-- The API for ZeroBrane Studio's language, or English: api-fr.lua beside api.lua, and so on. +function apiTable() + local folder = packageDir() .. "singe/" + local file = folder .. "api-" .. language() .. ".lua" + local chunk = nil + + if (language() == "en") or not wx.wxFileExists(file) then + file = folder .. "api.lua" + end + chunk = loadfile(file) + if not chunk then + ide:Print(("Singe: could not read the API file %s."):format(file)) + return {} + end + return chunk() +end + + +-- The builds of Singe this machine runs, its own first: Singe-v...--. ZeroBrane Studio's +-- ide.osarch is no help outside Linux: on Windows it holds the WINDIR path, and on a Mac it says x64 +-- on Apple silicon too, since ZeroBrane Studio is itself an Intel program there. Windows 11 on ARM +-- and Rosetta both run the x86_64 build when there is no native one. +function architectures() + local cpu = "" + local probe = nil + + if ide.osname == "Windows" then + -- ZeroBrane Studio is a 32-bit program: the machine's own architecture is in + -- PROCESSOR_ARCHITEW6432, and PROCESSOR_ARCHITECTURE is its own. + cpu = (os.getenv("PROCESSOR_ARCHITEW6432") or os.getenv("PROCESSOR_ARCHITECTURE") or ""):upper() + return (cpu == "ARM64") and { "aarch64", "x86_64" } or { "x86_64" } + end + if ide.osname == "Macintosh" then + -- Answers 1 on Apple silicon even to a program running under Rosetta. + probe = io.popen("sysctl -n hw.optional.arm64 2>/dev/null") + if probe then + cpu = probe:read("*l") or "" + probe:close() + end + return (cpu == "1") and { "aarch64", "x86_64" } or { "x86_64" } + end + return { ARCHITECTURE[ide.osarch] or "x86_64" } +end + + +-- Singe's executable: path.singe if set, then the one chosen before, then the newest for this +-- machine in the project's folder or the folders above it, then on the PATH. With ask, a missing +-- one is asked for and remembered. +function findSinge(ask) + local platform = PLATFORM[ide.osname] or PLATFORM.Unix + local kinds = architectures() + local settings = plugin:GetSettings() or {} + local folder = ide:GetProject() + local chosen = nil + + if ide.config.path.singe and wx.wxFileExists(ide.config.path.singe) then + return ide.config.path.singe + end + if settings.singe and wx.wxFileExists(settings.singe) then + return settings.singe + end + for _ = 1, 4 do + if not folder then + break + end + -- The machine's own build first, newest of it; one it runs under emulation only failing that. + local files = ide:GetFileList(folder, false, "Singe*") + for _, kind in ipairs(kinds) do + local wanted = "^Singe%-v[%d%.]+b?%d*%-" .. platform.name .. "%-" .. kind .. platform.suffix + local best = nil + local bestKey = -1 + for _, file in ipairs(files) do + local name = file:match("[^/\\]+$") + if name:match(wanted) and (singeVersion(name) > bestKey) then + best = file + bestKey = singeVersion(name) + end + end + if best then + return best + end + end + folder = parentFolder(folder) + end + for directory in (os.getenv("PATH") or ""):gmatch("[^" .. ((ide.osname == "Windows") and ";" or ":") .. "]+") do + for _, name in ipairs({ "Singe", "singe", "Singe.exe" }) do + if wx.wxFileExists(withSeparator(directory) .. name) then + return withSeparator(directory) .. name + end + end + end + if not ask then + return nil + end + chosen = wx.wxFileSelector(word("chooseTitle"), ide:GetProject() or "", "", "", "*", wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST, ide:GetMainFrame()) + if chosen and (chosen ~= "") then + saveSetting("singe", chosen) + return chosen + end + return nil +end + + +-- The script to run. A .singe file is a game; anything else is one of its modules, so the +-- project's only .singe file runs instead, when it has exactly one. +function gameScript(wfilename) + local file = wfilename:GetFullPath() + local project = ide:GetProject() + local scripts = nil + + if file:lower():match("%.singe$") or not project then + return file + end + scripts = ide:GetFileList(project, false, "*.singe") + if #scripts == 1 then + return scripts[1] + end + return file +end + + +function isStopFile(file) + return (file ~= nil) and (file:gsub("\\", "/"):match("Singe/" .. STOP_FILE:gsub("%.", "%%.") .. "$") ~= nil) +end + + +-- ZeroBrane Studio's language as two letters, English when it has none set. +function language() + local code = tostring(ide.config.language or ""):lower():sub(1, 2) + + return (code ~= "") and code or "en" +end + + +-- The Reference at the word under the mouse: every entry's anchor is its name in lower case. +function lookUp(editor, position) + local first = editor:WordStartPosition(position, true) + local last = editor:WordEndPosition(position, true) + local word = editor:GetTextRange(first, last) + + if word:match("^[%a_][%w_]*$") then + openReference((api[word] and api[word].anchor) or word:lower()) + else + openReference() + end +end + + +function openReference(anchor) + local page = referencePage() + + if not page then + wx.wxMessageBox(word("noReference"), word("reference"), wx.wxICON_INFORMATION + wx.wxOK + wx.wxCENTRE, ide:GetMainFrame()) + return + end + wx.wxLaunchDefaultBrowser("file://" .. page:gsub("\\", "/"):gsub("^([^/])", "/%1") .. (anchor and ("#" .. anchor) or ""), 0) +end + + +-- This file's folder, wherever ZeroBrane Studio loaded it from. +function packageDir() + return GetPathWithSep(plugin:GetFilePath()) +end + + +-- The folder above, normalized, or nil at the top. +function parentFolder(folder) + local name = wx.wxFileName.DirName(folder) + + name:Normalize() + if name:GetDirCount() == 0 then + return nil + end + name:RemoveLastDir() + return name:GetPath(wx.wxPATH_GET_VOLUME) +end + + +-- Reference.html: the configured one, or the newest documentation download beside Singe, in +-- ZeroBrane Studio's language when there is one in it. +function referencePage() + local configured = (plugin:GetConfig() or {}).reference + local folder = singeFolder() + local best = nil + local bestKey = -1 + + if configured and wx.wxFileExists(configured) then + return configured + end + if not folder then + return nil + end + for _, suffix in ipairs({ "-" .. language(), "" }) do + for _, directory in ipairs(subfolders(folder, "Singe-Docs-v*")) do + local name = directory:match("[^/\\]+$") + local page = withSeparator(directory) .. "Reference.html" + if (name:match("^Singe%-Docs%-v[%d%.]+b?%d*" .. suffix:gsub("%-", "%%-") .. "$")) and wx.wxFileExists(page) and (singeVersion(name) > bestKey) then + best = page + bestKey = singeVersion(name) + end + end + if best then + return best + end + end + if wx.wxFileExists(folder .. "Reference.html") then + return folder .. "Reference.html" + end + return nil +end + + +function reloading() + local config = plugin:GetConfig() or {} + local settings = plugin:GetSettings() or {} + + if config.reload ~= nil then + return config.reload + end + return settings.reload ~= false +end + + +function saveSetting(key, value) + local settings = plugin:GetSettings() or {} + + settings[key] = value + plugin:SetSettings(settings) +end + + +-- The folder Singe runs in: the one holding the executable, where its Singe folder and games are. +function singeFolder() + local singe = findSinge(false) + + return singe and wx.wxFileName(singe):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR) or nil +end + + +-- A number that sorts versions the way Singe does: v3.00 above v3.00b2 above v3.00b1. +function singeVersion(name) + local major, minor, beta = name:match("v(%d+)%.(%d+)b?(%d*)") + + if not major then + return 0 + end + return tonumber(major) * 1000000 + tonumber(minor) * 1000 + ((beta ~= "") and tonumber(beta) or 999) +end + + +-- The folders directly inside folder whose names match the wildcard mask, as full paths. +function subfolders(folder, mask) + local found = {} + local dir = wx.wxDir(folder) + local more = false + local name = nil + + if not dir:IsOpened() then + return found + end + more, name = dir:GetFirst(mask, wx.wxDIR_DIRS) + while more do + found[#found + 1] = withSeparator(folder) .. name + more, name = dir:GetNext() + end + return found +end + +-- A folder with its separator on the end: ZeroBrane Studio's GetPathWithSep takes a bare folder +-- for a file and answers its parent. +function withSeparator(folder) + if folder:match("[/\\]$") then + return folder + end + return folder .. GetPathSeparator() +end + + +-- The dragon at the toolbar's size: from beside this file, or from Singe's own folder when the +-- package was loaded from somewhere else, the nearest size up scaled to fit when none is exact. A +-- stock picture stands in when neither has it. +function toolbarBitmap(size) + local folders = { packageDir() .. "singe/" } + local folder = singeFolder() + + if folder then + folders[#folders + 1] = folder .. "Singe/ZeroBrane/singe/" + end + for _, directory in ipairs(folders) do + local found = nil + for _, have in ipairs(ICON_SIZES) do + local file = directory .. "icon-" .. have .. ".png" + if wx.wxFileExists(file) then + found = { file = file, size = have } + if have >= size then + break + end + end + end + if found then + local image = wx.wxImage(found.file, wx.wxBITMAP_TYPE_PNG) + if found.size ~= size then + image:Rescale(size, size, wx.wxIMAGE_QUALITY_HIGH) + end + return wx.wxBitmap(image) + end + end + return wx.wxArtProvider.GetBitmap(wx.wxART_EXECUTABLE_FILE, wx.wxART_TOOLBAR, wx.wxSize(size, size)) +end + +-- One of the package's own words, in ZeroBrane Studio's language or English. +function word(key) + return (WORDS[language()] or WORDS.en)[key] or WORDS.en[key] +end + +local interpreter = { + name = "Singe", + description = "Singe, the engine for laserdisc and video games", + api = { "baselib", "singe" }, + luaversion = "5.4", + fname = INTERPRETER, + hasdebugger = true, + skipcompile = true, -- ZeroBrane Studio's own Lua is older than Singe's 5.4 + takeparameters = true, + unhideanywindow = true, + fexepath = function(self) return findSinge(false) end, + fworkdir = function(self, wfilename) return singeFolder() or self:fprojdir(wfilename) end, + frun = function(self, wfilename, rundebug) + local singe = findSinge(true) + local script = gameScript(wfilename) + local params = self:GetCommandLineArg() + local folder = nil + local cmd = nil + local debug = "" + + if not singe then + ide:Print(word("nothingRun")) + return + end + folder = wx.wxFileName(singe):GetPath(wx.wxPATH_GET_VOLUME + wx.wxPATH_GET_SEPARATOR) + if rundebug then + local debugger = ide:GetDebugger() + -- Names are reported relative to where Singe runs, so breakpoints are matched from there. + debugger:SetOptions({ runstart = ide.config.debugger.runonstart ~= false, basedir = folder }) + debug = (" --debug=%s:%d"):format(ide.config.debugger.hostname or LOOPBACK, debugger:GetPortNumber()) + end + cmd = ('"%s"%s%s%s "%s"'):format(singe, debug, reloading() and " --reload" or "", (params and (#params > 0)) and (" " .. params) or "", script) + return CommandLineRun(cmd, folder, true, true) + end, +} + + +return { + name = "Singe", + description = "Runs and debugs Singe games, with autocompletion and tooltips for the Singe API.", + author = "Scott Duensing", + version = 2.0, + dependencies = "1.70", + + onRegister = function(self) + local project = ide:FindTopMenu("&Project") + local help = ide:FindTopMenu("&Help") + local frame = ide:GetMainFrame() + local toolbar = ide:GetToolBar() + local specmap = ide.config.editor.specmap + + plugin = self + api = apiTable() + ide:AddAPI("lua", "singe", api) + ide:AddInterpreter(INTERPRETER, interpreter) + for extension, spec in pairs({ singe = "lua", rml = "xml", rcss = "css" }) do + specmapOld[extension] = specmap[extension] + specmap[extension] = specmap[extension] or spec + end + + project:AppendSeparator() + project:Append(MENU_DEBUG, word("debug")) + project:Append(MENU_CHOOSE, word("choose")) + project:AppendCheckItem(MENU_RELOAD, word("reload")) + if help then + help:Append(MENU_REFERENCE, word("reference")) + end + frame:Connect(MENU_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED, function() + if ide:GetInterpreter() ~= ide:GetInterpreter(INTERPRETER) then + ide:SetInterpreter(INTERPRETER) + end + ProjectDebug() + end) + frame:Connect(MENU_CHOOSE, wx.wxEVT_COMMAND_MENU_SELECTED, function() + local chosen = wx.wxFileSelector(word("chooseTitle"), singeFolder() or "", "", "", "*", wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST, frame) + if chosen and (chosen ~= "") then + saveSetting("singe", chosen) + ide:Print(word("using"):format(chosen)) + end + end) + frame:Connect(MENU_RELOAD, wx.wxEVT_COMMAND_MENU_SELECTED, function(event) + saveSetting("reload", event:IsChecked()) + end) + frame:Connect(MENU_RELOAD, wx.wxEVT_UPDATE_UI, function(event) + event:Check(reloading()) + end) + frame:Connect(MENU_REFERENCE, wx.wxEVT_COMMAND_MENU_SELECTED, function() + openReference() + end) + frame:Connect(MENU_LOOKUP, wx.wxEVT_COMMAND_MENU_SELECTED, function() + local editor = ide:GetEditor() + if editor then + lookUp(editor, lookUpPosition or editor:GetCurrentPos()) + end + end) + + toolbar:AddTool(MENU_DEBUG, "Singe", toolbarBitmap(toolbar:GetToolBitmapSize():GetWidth()), word("debug")) + toolbar:Realize() + end, + + onUnRegister = function(self) + local toolbar = ide:GetToolBar() + local specmap = ide.config.editor.specmap + + toolbar:DeleteTool(MENU_DEBUG) + toolbar:Realize() + for _, item in ipairs({ MENU_DEBUG, MENU_CHOOSE, MENU_RELOAD, MENU_REFERENCE }) do + ide:RemoveMenuItem(item) + end + for extension, spec in pairs(specmapOld) do + specmap[extension] = spec + end + ide:RemoveInterpreter(INTERPRETER) + ide:RemoveAPI("lua", "singe") + end, + + -- The word under the mouse, looked up in the Reference; the caret stays where it was. + onMenuEditor = function(self, menu, editor, event) + local point = editor:ScreenToClient(event:GetPosition()) + local position = editor:PositionFromPoint(point) + + lookUpPosition = ((point:GetX() >= 0) and (point:GetY() >= 0)) and position or nil + menu:AppendSeparator() + menu:Append(MENU_LOOKUP, word("lookUp")) + end, + + -- Singe stops in its own Debugger.singe when an error is about to end the game. Opened here, so + -- the IDE shows the stop rather than stepping out of a file it has not got open. + onDebuggerPreActivate = function(self, debugger, file, line) + -- Not opened here: this runs inside the debugger's coroutine, where an editor cannot be made. + -- ZeroBrane Studio opens a pending activation on its next update, still stopped meanwhile. + if isStopFile(file) and wx.wxFileExists(file) and not ide:FindDocument(file) then + debugger.activate = { file, line } + return false + end + end, + + -- And says what the error was, which Debugger.singe holds as "message". + onDebuggerActivate = function(self, debugger, file, line) + if not isStopFile(file) then + return + end + debugger:EvalAsync("message", function(value) + local chunk = (loadstring or load)("return " .. tostring(value)) + local ok, text = pcall(chunk or function() return value end) + ide:Print(word("stopped"):format(ok and tostring(text) or tostring(value))) + ide:Print(word("stoppedHelp")) + end) + end, +} diff --git a/zbstudio/packages/singe/api-de.lua b/zbstudio/packages/singe/api-de.lua new file mode 100644 index 000000000..b96d0ef59 --- /dev/null +++ b/zbstudio/packages/singe/api-de.lua @@ -0,0 +1,7967 @@ +-- Singe's API for ZeroBrane Studio's autocompletion and tooltips (de). +-- Generated by util/zbstudioApi.py from the Reference and Singe's own globals; do not edit. +return { + BILLBOARD_ALL = { + type = "value", + description = "Wie ein Knoten sich der Kamera zuwendet, für nodeSetBillboard.\n= 1", + }, + BILLBOARD_NONE = { + type = "value", + description = "Wie ein Knoten sich der Kamera zuwendet, für nodeSetBillboard.\n= 0", + }, + BILLBOARD_Y = { + type = "value", + description = "Wie ein Knoten sich der Kamera zuwendet, für nodeSetBillboard.\n= 2", + }, + BODY_DYNAMIC = { + type = "value", + description = "Körpertypen für bodyNew.\n= 1", + }, + BODY_KINEMATIC = { + type = "value", + description = "Körpertypen für bodyNew.\n= 2", + }, + BODY_STATIC = { + type = "value", + description = "Körpertypen für bodyNew.\n= 0", + }, + COLLIDE_FLOOR = { + type = "value", + description = "Wovon Partikel abprallen, für emitterSetCollide.\n= 1", + }, + COLLIDE_NONE = { + type = "value", + description = "Wovon Partikel abprallen, für emitterSetCollide.\n= 0", + }, + COLLIDE_SCENE = { + type = "value", + description = "Wovon Partikel abprallen, für emitterSetCollide.\n= 2", + }, + Colourb = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ alpha, blue, green, red, rgba }", + }, + __setters = { + type = "value", + description = "{ alpha, blue, green, red, rgba }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + Colourf = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ alpha, blue, green, red, rgba }", + }, + __setters = { + type = "value", + description = "{ alpha, blue, green, red, rgba }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + Context = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + CloseDataModel = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + CreateDocument = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsMouseInteracting = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + LoadDocument = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + OpenDataModel = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessKeyDown = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessKeyUp = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessMouseButtonDown = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessMouseButtonUp = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessMouseLeave = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessMouseMove = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessMouseWheel = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ProcessTextInput = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Render = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + UnloadAllDocuments = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + UnloadDocument = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Update = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ dimensions, documents, dp_ratio, focus_element, hover_element, name, root_element }", + }, + __setters = { + type = "value", + description = "{ dimensions, dp_ratio }", + }, + }, + }, + ContextDocumentsProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + DEBUG_ALL = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 15", + }, + DEBUG_CONSTRAINTS = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 2", + }, + DEBUG_CONTACTS = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 4", + }, + DEBUG_NONE = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 0", + }, + DEBUG_SHAPES = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 1", + }, + DEBUG_STATIC = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 16", + }, + DEBUG_VELOCITIES = { + type = "value", + description = "Was physicsSetDebug zeichnet; addiere sie.\n= 8", + }, + DIR = { + type = "value", + description = "Pfade in einem Skript sind relativ zu dem Ordner, aus dem Singe gestartet wurde, nicht zum Skript. Verwende die globale Variable DIR, die Framework.singe auf den eigenen Ordner des Skripts setzt, damit ein Spiel funktioniert, egal wo es installiert ist:", + }, + DISC_EJECTED = { + type = "value", + description = "Rückgabewerte von discGetState.\n= 5", + }, + DISC_PAUSED = { + type = "value", + description = "Rückgabewerte von discGetState.\n= 4", + }, + DISC_PLAYING = { + type = "value", + description = "Rückgabewerte von discGetState.\n= 3", + }, + DISC_STOPPED = { + type = "value", + description = "Rückgabewerte von discGetState.\n= 2", + }, + Document = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Close = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + CreateElement = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + CreateTextNode = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Hide = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + PullToFront = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + PushToBack = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Show = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, context, first_child, id, inner_rml, last_child, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name, title }", + }, + __setters = { + type = "value", + description = "{ class_name, id, inner_rml, scroll_left, scroll_top, title }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + DocumentFocus = { + type = "lib", + description = "", + childs = { + Auto = { + type = "value", + description = "= 3", + }, + Document = { + type = "value", + description = "= 1", + }, + Keep = { + type = "value", + description = "= 2", + }, + None = { + type = "value", + description = "= 0", + }, + }, + }, + DocumentModal = { + type = "lib", + description = "", + childs = { + Keep = { + type = "value", + description = "= 2", + }, + Modal = { + type = "value", + description = "= 1", + }, + None = { + type = "value", + description = "= 0", + }, + }, + }, + EASE_BACK_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 13", + }, + EASE_BACK_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 15", + }, + EASE_BACK_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 14", + }, + EASE_BOUNCE_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 19", + }, + EASE_BOUNCE_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 21", + }, + EASE_BOUNCE_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 20", + }, + EASE_CUBIC_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 4", + }, + EASE_CUBIC_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 6", + }, + EASE_CUBIC_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 5", + }, + EASE_ELASTIC_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 16", + }, + EASE_ELASTIC_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 18", + }, + EASE_ELASTIC_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 17", + }, + EASE_EXPO_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 10", + }, + EASE_EXPO_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 12", + }, + EASE_EXPO_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 11", + }, + EASE_LINEAR = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 0", + }, + EASE_QUAD_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 1", + }, + EASE_QUAD_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 3", + }, + EASE_QUAD_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 2", + }, + EASE_SINE_IN = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 7", + }, + EASE_SINE_IN_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 9", + }, + EASE_SINE_OUT = { + type = "value", + description = "Die Easing-Kurven sind die üblichen. EASE_LINEAR ist gar keine Kurve; EASE_QUAD_*, EASE_CUBIC_*, EASE_SINE_* und EASE_EXPO_* reichen von sanft bis scharf; EASE_BACK_* schießt über das Ziel hinaus und kommt zurück; EASE_ELASTIC_* federt; EASE_BOUNCE_* fällt und springt auf. Jede gibt es als _IN (langsam am Anfang), _OUT (langsam am Ende) und _IN_OUT (beides).\n= 8", + }, + Element = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ Document, ElementForm, ElementFormControl, ElementFormControlInput, ElementFormControlSelect, ElementFormControlTextArea, ElementTabSet, ElementText }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, first_child, id, inner_rml, last_child, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name }", + }, + __setters = { + type = "value", + description = "{ class_name, id, inner_rml, scroll_left, scroll_top }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementAttributesProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + ElementChildNodesProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + ElementForm = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Submit = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, first_child, id, inner_rml, last_child, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name }", + }, + __setters = { + type = "value", + description = "{ class_name, id, inner_rml, scroll_left, scroll_top }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementFormControl = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, disabled, first_child, id, inner_rml, last_child, name, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name, value }", + }, + __setters = { + type = "value", + description = "{ class_name, disabled, id, inner_rml, name, scroll_left, scroll_top, value }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementFormControlInput = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetSelection = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Select = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetSelection = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, checked, child_nodes, class_name, client_height, client_left, client_top, client_width, disabled, first_child, id, inner_rml, last_child, max, maxlength, min, name, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, size, step, style, tag_name, value }", + }, + __setters = { + type = "value", + description = "{ checked, class_name, disabled, id, inner_rml, max, maxlength, min, name, scroll_left, scroll_top, size, step, value }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementFormControlSelect = { + type = "lib", + description = "", + childs = { + Add = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Remove = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, disabled, first_child, id, inner_rml, last_child, name, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, options, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, selection, style, tag_name, value }", + }, + __setters = { + type = "value", + description = "{ class_name, disabled, id, inner_rml, name, scroll_left, scroll_top, selection, value }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementFormControlTextArea = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetSelection = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Select = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetSelection = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, cols, disabled, first_child, id, inner_rml, last_child, maxlength, name, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, rows, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name, value, wordwrap }", + }, + __setters = { + type = "value", + description = "{ class_name, cols, disabled, id, inner_rml, maxlength, name, rows, scroll_left, scroll_top, value, wordwrap }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementInstancer = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ InstanceElement }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementPtr = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + ElementStyleProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + ElementTabSet = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetPanel = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetTab = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ active_tab, attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, first_child, id, inner_rml, last_child, next_sibling, num_tabs, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name }", + }, + __setters = { + type = "value", + description = "{ active_tab, class_name, id, inner_rml, scroll_left, scroll_top }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + ElementText = { + type = "lib", + description = "", + childs = { + AddEventListener = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + AppendChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + As = { + type = "value", + description = "{ }", + }, + Blur = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Click = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + DispatchEvent = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Focus = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementById = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + GetElementsByTagName = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + HasChildNodes = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + InsertBefore = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + IsClassSet = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Matches = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelector = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + QuerySelectorAll = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RemoveChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ReplaceChild = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + ScrollIntoView = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetAttribute = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + SetClass = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ attributes, child_nodes, class_name, client_height, client_left, client_top, client_width, first_child, id, inner_rml, last_child, next_sibling, offset_height, offset_left, offset_parent, offset_top, offset_width, owner_document, parent_node, previous_sibling, scroll_height, scroll_left, scroll_top, scroll_width, style, tag_name, text }", + }, + __setters = { + type = "value", + description = "{ class_name, id, inner_rml, scroll_left, scroll_top, text }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + Event = { + type = "lib", + description = "", + childs = { + StopImmediatePropagation = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + StopPropagation = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ current_element, parameters, target_element, type }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + EventParametersProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + FILTER_LINEAR = { + type = "value", + description = "Texturabtastung für materialSetFilter.\n= 0", + }, + FILTER_NEAREST = { + type = "value", + description = "Texturabtastung für materialSetFilter.\n= 1", + }, + FONT_QUALITY_BLENDED = { + type = "value", + description = "Argumente für fontQuality.\n= 3", + }, + FONT_QUALITY_SHADED = { + type = "value", + description = "Argumente für fontQuality.\n= 2", + }, + FONT_QUALITY_SOLID = { + type = "value", + description = "Argumente für fontQuality.\n= 1", + }, + GAMEPAD_0 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + AXIS_LEFT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER\", value = 512 }", + }, + AXIS_LEFT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_N\", value = 513 }", + }, + AXIS_LEFT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_P\", value = 514 }", + }, + AXIS_LEFT_X = { + type = "value", + description = "{ name = \"AXIS_LEFT_X\", value = 500 }", + }, + AXIS_LEFT_X_L = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_L\", value = 501 }", + }, + AXIS_LEFT_X_R = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_R\", value = 502 }", + }, + AXIS_LEFT_Y = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y\", value = 503 }", + }, + AXIS_LEFT_Y_D = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_D\", value = 505 }", + }, + AXIS_LEFT_Y_U = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_U\", value = 504 }", + }, + AXIS_RIGHT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER\", value = 515 }", + }, + AXIS_RIGHT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_N\", value = 516 }", + }, + AXIS_RIGHT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_P\", value = 517 }", + }, + AXIS_RIGHT_X = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X\", value = 506 }", + }, + AXIS_RIGHT_X_L = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_L\", value = 507 }", + }, + AXIS_RIGHT_X_R = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_R\", value = 508 }", + }, + AXIS_RIGHT_Y = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y\", value = 509 }", + }, + AXIS_RIGHT_Y_D = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_D\", value = 511 }", + }, + AXIS_RIGHT_Y_U = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_U\", value = 510 }", + }, + BUTTON_A = { + type = "value", + description = "{ name = \"BUTTON_A\", value = 518 }", + }, + BUTTON_B = { + type = "value", + description = "{ name = \"BUTTON_B\", value = 519 }", + }, + BUTTON_BACK = { + type = "value", + description = "{ name = \"BUTTON_BACK\", value = 522 }", + }, + BUTTON_GUIDE = { + type = "value", + description = "{ name = \"BUTTON_GUIDE\", value = 523 }", + }, + BUTTON_LEFT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_LEFT_BUMPER\", value = 527 }", + }, + BUTTON_LEFT_STICK = { + type = "value", + description = "{ name = \"BUTTON_LEFT_STICK\", value = 525 }", + }, + BUTTON_RIGHT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_BUMPER\", value = 528 }", + }, + BUTTON_RIGHT_STICK = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_STICK\", value = 526 }", + }, + BUTTON_START = { + type = "value", + description = "{ name = \"BUTTON_START\", value = 524 }", + }, + BUTTON_X = { + type = "value", + description = "{ name = \"BUTTON_X\", value = 520 }", + }, + BUTTON_Y = { + type = "value", + description = "{ name = \"BUTTON_Y\", value = 521 }", + }, + DPAD_DOWN = { + type = "value", + description = "{ name = \"DPAD_DOWN\", value = 530 }", + }, + DPAD_LEFT = { + type = "value", + description = "{ name = \"DPAD_LEFT\", value = 531 }", + }, + DPAD_RIGHT = { + type = "value", + description = "{ name = \"DPAD_RIGHT\", value = 532 }", + }, + DPAD_UP = { + type = "value", + description = "{ name = \"DPAD_UP\", value = 529 }", + }, + }, + }, + GAMEPAD_1 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + AXIS_LEFT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER\", value = 612 }", + }, + AXIS_LEFT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_N\", value = 613 }", + }, + AXIS_LEFT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_P\", value = 614 }", + }, + AXIS_LEFT_X = { + type = "value", + description = "{ name = \"AXIS_LEFT_X\", value = 600 }", + }, + AXIS_LEFT_X_L = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_L\", value = 601 }", + }, + AXIS_LEFT_X_R = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_R\", value = 602 }", + }, + AXIS_LEFT_Y = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y\", value = 603 }", + }, + AXIS_LEFT_Y_D = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_D\", value = 605 }", + }, + AXIS_LEFT_Y_U = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_U\", value = 604 }", + }, + AXIS_RIGHT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER\", value = 615 }", + }, + AXIS_RIGHT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_N\", value = 616 }", + }, + AXIS_RIGHT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_P\", value = 617 }", + }, + AXIS_RIGHT_X = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X\", value = 606 }", + }, + AXIS_RIGHT_X_L = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_L\", value = 607 }", + }, + AXIS_RIGHT_X_R = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_R\", value = 608 }", + }, + AXIS_RIGHT_Y = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y\", value = 609 }", + }, + AXIS_RIGHT_Y_D = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_D\", value = 611 }", + }, + AXIS_RIGHT_Y_U = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_U\", value = 610 }", + }, + BUTTON_A = { + type = "value", + description = "{ name = \"BUTTON_A\", value = 618 }", + }, + BUTTON_B = { + type = "value", + description = "{ name = \"BUTTON_B\", value = 619 }", + }, + BUTTON_BACK = { + type = "value", + description = "{ name = \"BUTTON_BACK\", value = 622 }", + }, + BUTTON_GUIDE = { + type = "value", + description = "{ name = \"BUTTON_GUIDE\", value = 623 }", + }, + BUTTON_LEFT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_LEFT_BUMPER\", value = 627 }", + }, + BUTTON_LEFT_STICK = { + type = "value", + description = "{ name = \"BUTTON_LEFT_STICK\", value = 625 }", + }, + BUTTON_RIGHT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_BUMPER\", value = 628 }", + }, + BUTTON_RIGHT_STICK = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_STICK\", value = 626 }", + }, + BUTTON_START = { + type = "value", + description = "{ name = \"BUTTON_START\", value = 624 }", + }, + BUTTON_X = { + type = "value", + description = "{ name = \"BUTTON_X\", value = 620 }", + }, + BUTTON_Y = { + type = "value", + description = "{ name = \"BUTTON_Y\", value = 621 }", + }, + DPAD_DOWN = { + type = "value", + description = "{ name = \"DPAD_DOWN\", value = 630 }", + }, + DPAD_LEFT = { + type = "value", + description = "{ name = \"DPAD_LEFT\", value = 631 }", + }, + DPAD_RIGHT = { + type = "value", + description = "{ name = \"DPAD_RIGHT\", value = 632 }", + }, + DPAD_UP = { + type = "value", + description = "{ name = \"DPAD_UP\", value = 629 }", + }, + }, + }, + GAMEPAD_2 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + AXIS_LEFT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER\", value = 712 }", + }, + AXIS_LEFT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_N\", value = 713 }", + }, + AXIS_LEFT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_P\", value = 714 }", + }, + AXIS_LEFT_X = { + type = "value", + description = "{ name = \"AXIS_LEFT_X\", value = 700 }", + }, + AXIS_LEFT_X_L = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_L\", value = 701 }", + }, + AXIS_LEFT_X_R = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_R\", value = 702 }", + }, + AXIS_LEFT_Y = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y\", value = 703 }", + }, + AXIS_LEFT_Y_D = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_D\", value = 705 }", + }, + AXIS_LEFT_Y_U = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_U\", value = 704 }", + }, + AXIS_RIGHT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER\", value = 715 }", + }, + AXIS_RIGHT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_N\", value = 716 }", + }, + AXIS_RIGHT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_P\", value = 717 }", + }, + AXIS_RIGHT_X = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X\", value = 706 }", + }, + AXIS_RIGHT_X_L = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_L\", value = 707 }", + }, + AXIS_RIGHT_X_R = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_R\", value = 708 }", + }, + AXIS_RIGHT_Y = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y\", value = 709 }", + }, + AXIS_RIGHT_Y_D = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_D\", value = 711 }", + }, + AXIS_RIGHT_Y_U = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_U\", value = 710 }", + }, + BUTTON_A = { + type = "value", + description = "{ name = \"BUTTON_A\", value = 718 }", + }, + BUTTON_B = { + type = "value", + description = "{ name = \"BUTTON_B\", value = 719 }", + }, + BUTTON_BACK = { + type = "value", + description = "{ name = \"BUTTON_BACK\", value = 722 }", + }, + BUTTON_GUIDE = { + type = "value", + description = "{ name = \"BUTTON_GUIDE\", value = 723 }", + }, + BUTTON_LEFT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_LEFT_BUMPER\", value = 727 }", + }, + BUTTON_LEFT_STICK = { + type = "value", + description = "{ name = \"BUTTON_LEFT_STICK\", value = 725 }", + }, + BUTTON_RIGHT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_BUMPER\", value = 728 }", + }, + BUTTON_RIGHT_STICK = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_STICK\", value = 726 }", + }, + BUTTON_START = { + type = "value", + description = "{ name = \"BUTTON_START\", value = 724 }", + }, + BUTTON_X = { + type = "value", + description = "{ name = \"BUTTON_X\", value = 720 }", + }, + BUTTON_Y = { + type = "value", + description = "{ name = \"BUTTON_Y\", value = 721 }", + }, + DPAD_DOWN = { + type = "value", + description = "{ name = \"DPAD_DOWN\", value = 730 }", + }, + DPAD_LEFT = { + type = "value", + description = "{ name = \"DPAD_LEFT\", value = 731 }", + }, + DPAD_RIGHT = { + type = "value", + description = "{ name = \"DPAD_RIGHT\", value = 732 }", + }, + DPAD_UP = { + type = "value", + description = "{ name = \"DPAD_UP\", value = 729 }", + }, + }, + }, + GAMEPAD_3 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + AXIS_LEFT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER\", value = 812 }", + }, + AXIS_LEFT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_N\", value = 813 }", + }, + AXIS_LEFT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_LEFT_TRIGGER_P\", value = 814 }", + }, + AXIS_LEFT_X = { + type = "value", + description = "{ name = \"AXIS_LEFT_X\", value = 800 }", + }, + AXIS_LEFT_X_L = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_L\", value = 801 }", + }, + AXIS_LEFT_X_R = { + type = "value", + description = "{ name = \"AXIS_LEFT_X_R\", value = 802 }", + }, + AXIS_LEFT_Y = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y\", value = 803 }", + }, + AXIS_LEFT_Y_D = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_D\", value = 805 }", + }, + AXIS_LEFT_Y_U = { + type = "value", + description = "{ name = \"AXIS_LEFT_Y_U\", value = 804 }", + }, + AXIS_RIGHT_TRIGGER = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER\", value = 815 }", + }, + AXIS_RIGHT_TRIGGER_N = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_N\", value = 816 }", + }, + AXIS_RIGHT_TRIGGER_P = { + type = "value", + description = "{ name = \"AXIS_RIGHT_TRIGGER_P\", value = 817 }", + }, + AXIS_RIGHT_X = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X\", value = 806 }", + }, + AXIS_RIGHT_X_L = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_L\", value = 807 }", + }, + AXIS_RIGHT_X_R = { + type = "value", + description = "{ name = \"AXIS_RIGHT_X_R\", value = 808 }", + }, + AXIS_RIGHT_Y = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y\", value = 809 }", + }, + AXIS_RIGHT_Y_D = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_D\", value = 811 }", + }, + AXIS_RIGHT_Y_U = { + type = "value", + description = "{ name = \"AXIS_RIGHT_Y_U\", value = 810 }", + }, + BUTTON_A = { + type = "value", + description = "{ name = \"BUTTON_A\", value = 818 }", + }, + BUTTON_B = { + type = "value", + description = "{ name = \"BUTTON_B\", value = 819 }", + }, + BUTTON_BACK = { + type = "value", + description = "{ name = \"BUTTON_BACK\", value = 822 }", + }, + BUTTON_GUIDE = { + type = "value", + description = "{ name = \"BUTTON_GUIDE\", value = 823 }", + }, + BUTTON_LEFT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_LEFT_BUMPER\", value = 827 }", + }, + BUTTON_LEFT_STICK = { + type = "value", + description = "{ name = \"BUTTON_LEFT_STICK\", value = 825 }", + }, + BUTTON_RIGHT_BUMPER = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_BUMPER\", value = 828 }", + }, + BUTTON_RIGHT_STICK = { + type = "value", + description = "{ name = \"BUTTON_RIGHT_STICK\", value = 826 }", + }, + BUTTON_START = { + type = "value", + description = "{ name = \"BUTTON_START\", value = 824 }", + }, + BUTTON_X = { + type = "value", + description = "{ name = \"BUTTON_X\", value = 820 }", + }, + BUTTON_Y = { + type = "value", + description = "{ name = \"BUTTON_Y\", value = 821 }", + }, + DPAD_DOWN = { + type = "value", + description = "{ name = \"DPAD_DOWN\", value = 830 }", + }, + DPAD_LEFT = { + type = "value", + description = "{ name = \"DPAD_LEFT\", value = 831 }", + }, + DPAD_RIGHT = { + type = "value", + description = "{ name = \"DPAD_RIGHT\", value = 832 }", + }, + DPAD_UP = { + type = "value", + description = "{ name = \"DPAD_UP\", value = 829 }", + }, + }, + }, + GAMEPAD_AXIS_LEFT_TRIGGER = { + type = "value", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.\n= 4", + }, + GAMEPAD_AXIS_LEFT_X = { + type = "value", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.\n= 0", + }, + GAMEPAD_AXIS_LEFT_Y = { + type = "value", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.\n= 1", + }, + GAMEPAD_AXIS_RIGHT_TRIGGER = { + type = "value", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.\n= 5", + }, + GAMEPAD_AXIS_RIGHT_X = { + type = "value", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.\n= 2", + }, + GAMEPAD_AXIS_RIGHT_Y = { + type = "value", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.\n= 3", + }, + JOINT_BALL = { + type = "value", + description = "Gelenktypen (die joint*-Aufrufe nennen sie direkt).\n= 1", + }, + JOINT_HINGE = { + type = "value", + description = "Gelenktypen (die joint*-Aufrufe nennen sie direkt).\n= 0", + }, + JOINT_SLIDER = { + type = "value", + description = "Gelenktypen (die joint*-Aufrufe nennen sie direkt).\n= 2", + }, + LAYER_FRONT = { + type = "value", + description = "Ob ein Knoten unter dem Overlay oder davor gezeichnet wird, für nodeSetLayer.\n= 1", + }, + LAYER_SCENE = { + type = "value", + description = "Ob ein Knoten unter dem Overlay oder davor gezeichnet wird, für nodeSetLayer.\n= 0", + }, + LIGHT_DIRECTIONAL = { + type = "value", + description = "Argumente für lightNew.\n= 0", + }, + LIGHT_POINT = { + type = "value", + description = "Argumente für lightNew.\n= 1", + }, + LIGHT_SPOT = { + type = "value", + description = "Argumente für lightNew.\n= 2", + }, + Log = { + type = "lib", + description = "", + childs = { + Message = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + logtype = { + type = "value", + description = "{ always = 0, debug = 5, error = 1, info = 4, warning = 3 }", + }, + }, + }, + LuaRmlUi = { + type = "lib", + description = "", + childs = { + CreateContext = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + LoadFontFace = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + RegisterTag = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ contexts, key_identifier, key_modifier }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + MANY_MOUSE = { + type = "value", + description = "Argumente für mouseSetMode.\n= 200", + }, + MODE_FULL = { + type = "value", + description = "Argumente für keyboardSetMode.\n= 1", + }, + MODE_NORMAL = { + type = "value", + description = "Argumente für keyboardSetMode.\n= 0", + }, + MODIFIER = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + ALT = { + type = "value", + description = "{ name = \"ALT\", value = 768 }", + }, + CAPS = { + type = "value", + description = "{ name = \"CAPS\", value = 8192 }", + }, + CTRL = { + type = "value", + description = "{ name = \"CTRL\", value = 192 }", + }, + GUI = { + type = "value", + description = "{ name = \"GUI\", value = 3072 }", + }, + LALT = { + type = "value", + description = "{ name = \"LALT\", value = 256 }", + }, + LCTRL = { + type = "value", + description = "{ name = \"LCTRL\", value = 64 }", + }, + LGUI = { + type = "value", + description = "{ name = \"LGUI\", value = 1024 }", + }, + LSHIFT = { + type = "value", + description = "{ name = \"LSHIFT\", value = 1 }", + }, + MODE = { + type = "value", + description = "{ name = \"MODE\", value = 16384 }", + }, + NONE = { + type = "value", + description = "{ name = \"NONE\", value = 0 }", + }, + NUM = { + type = "value", + description = "{ name = \"NUM\", value = 4096 }", + }, + RALT = { + type = "value", + description = "{ name = \"RALT\", value = 512 }", + }, + RCTRL = { + type = "value", + description = "{ name = \"RCTRL\", value = 128 }", + }, + RGUI = { + type = "value", + description = "{ name = \"RGUI\", value = 2048 }", + }, + RSHIFT = { + type = "value", + description = "{ name = \"RSHIFT\", value = 2 }", + }, + SCROLL = { + type = "value", + description = "{ name = \"SCROLL\", value = 32768 }", + }, + SHIFT = { + type = "value", + description = "{ name = \"SHIFT\", value = 3 }", + }, + }, + }, + MOUSE_0 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + BUTTON_LEFT = { + type = "value", + description = "{ name = \"BUTTON_LEFT\", value = 1000 }", + }, + BUTTON_MIDDLE = { + type = "value", + description = "{ name = \"BUTTON_MIDDLE\", value = 1002 }", + }, + BUTTON_RIGHT = { + type = "value", + description = "{ name = \"BUTTON_RIGHT\", value = 1001 }", + }, + BUTTON_X1 = { + type = "value", + description = "{ name = \"BUTTON_X1\", value = 1003 }", + }, + BUTTON_X2 = { + type = "value", + description = "{ name = \"BUTTON_X2\", value = 1004 }", + }, + WHEEL_DOWN = { + type = "value", + description = "{ name = \"WHEEL_DOWN\", value = 1006 }", + }, + WHEEL_UP = { + type = "value", + description = "{ name = \"WHEEL_UP\", value = 1005 }", + }, + }, + }, + MOUSE_1 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + BUTTON_LEFT = { + type = "value", + description = "{ name = \"BUTTON_LEFT\", value = 1100 }", + }, + BUTTON_MIDDLE = { + type = "value", + description = "{ name = \"BUTTON_MIDDLE\", value = 1102 }", + }, + BUTTON_RIGHT = { + type = "value", + description = "{ name = \"BUTTON_RIGHT\", value = 1101 }", + }, + BUTTON_X1 = { + type = "value", + description = "{ name = \"BUTTON_X1\", value = 1103 }", + }, + BUTTON_X2 = { + type = "value", + description = "{ name = \"BUTTON_X2\", value = 1104 }", + }, + WHEEL_DOWN = { + type = "value", + description = "{ name = \"WHEEL_DOWN\", value = 1106 }", + }, + WHEEL_UP = { + type = "value", + description = "{ name = \"WHEEL_UP\", value = 1105 }", + }, + }, + }, + MOUSE_2 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + BUTTON_LEFT = { + type = "value", + description = "{ name = \"BUTTON_LEFT\", value = 1200 }", + }, + BUTTON_MIDDLE = { + type = "value", + description = "{ name = \"BUTTON_MIDDLE\", value = 1202 }", + }, + BUTTON_RIGHT = { + type = "value", + description = "{ name = \"BUTTON_RIGHT\", value = 1201 }", + }, + BUTTON_X1 = { + type = "value", + description = "{ name = \"BUTTON_X1\", value = 1203 }", + }, + BUTTON_X2 = { + type = "value", + description = "{ name = \"BUTTON_X2\", value = 1204 }", + }, + WHEEL_DOWN = { + type = "value", + description = "{ name = \"WHEEL_DOWN\", value = 1206 }", + }, + WHEEL_UP = { + type = "value", + description = "{ name = \"WHEEL_UP\", value = 1205 }", + }, + }, + }, + MOUSE_3 = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + BUTTON_LEFT = { + type = "value", + description = "{ name = \"BUTTON_LEFT\", value = 1300 }", + }, + BUTTON_MIDDLE = { + type = "value", + description = "{ name = \"BUTTON_MIDDLE\", value = 1302 }", + }, + BUTTON_RIGHT = { + type = "value", + description = "{ name = \"BUTTON_RIGHT\", value = 1301 }", + }, + BUTTON_X1 = { + type = "value", + description = "{ name = \"BUTTON_X1\", value = 1303 }", + }, + BUTTON_X2 = { + type = "value", + description = "{ name = \"BUTTON_X2\", value = 1304 }", + }, + WHEEL_DOWN = { + type = "value", + description = "{ name = \"WHEEL_DOWN\", value = 1306 }", + }, + WHEEL_UP = { + type = "value", + description = "{ name = \"WHEEL_UP\", value = 1305 }", + }, + }, + }, + MOUSE_MANY = { + type = "value", + description = "Argumente für mouseSetMode.\n= 200", + }, + MOUSE_SINGLE = { + type = "value", + description = "Argumente für mouseSetMode.\n= 100", + }, + OVERLAY_NOT_UPDATED = { + type = "value", + description = "Rückgabewerte für onOverlayUpdate.\n= 0", + }, + OVERLAY_UPDATED = { + type = "value", + description = "Rückgabewerte für onOverlayUpdate.\n= 1", + }, + PARTICLE_ADD = { + type = "value", + description = "Blending für emitterSetBlend.\n= 1", + }, + PARTICLE_ALPHA = { + type = "value", + description = "Blending für emitterSetBlend.\n= 0", + }, + PARTICLE_OVER = { + type = "value", + description = "Wo 2D-Partikel gezeichnet werden, für emitterSetLayer.\n= 0", + }, + PARTICLE_UNDER = { + type = "value", + description = "Wo 2D-Partikel gezeichnet werden, für emitterSetLayer.\n= 1", + }, + RENDER_PIXELATED = { + type = "value", + description = "Argumente für spriteQuality / videoQuality.\n= 0", + }, + RENDER_SMOOTH = { + type = "value", + description = "Argumente für spriteQuality / videoQuality.\n= 1", + }, + RmlUiContextsProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + SCANCODE = { + type = "lib", + description = "Framework.singe fügt die Tabellen SCANCODE und MODIFIER hinzu (die Tasten- und Modifikatorwerte von SDL), GAMEPAD_0 bis GAMEPAD_3, MOUSE_0 bis MOUSE_3, die Indizes GAMEPAD_AXIS_* für controllerGetAxis und DIR.", + childs = { + A = { + type = "value", + description = "{ name = \"A\", value = 4 }", + }, + AC_BACK = { + type = "value", + description = "{ name = \"AC_BACK\", value = 270 }", + }, + AC_BOOKMARKS = { + type = "value", + description = "{ name = \"AC_BOOKMARKS\", value = 274 }", + }, + AC_FORWARD = { + type = "value", + description = "{ name = \"AC_FORWARD\", value = 271 }", + }, + AC_HOME = { + type = "value", + description = "{ name = \"AC_HOME\", value = 269 }", + }, + AC_REFRESH = { + type = "value", + description = "{ name = \"AC_REFRESH\", value = 273 }", + }, + AC_SEARCH = { + type = "value", + description = "{ name = \"AC_SEARCH\", value = 268 }", + }, + AC_STOP = { + type = "value", + description = "{ name = \"AC_STOP\", value = 272 }", + }, + AGAIN = { + type = "value", + description = "{ name = \"AGAIN\", value = 121 }", + }, + ALTERASE = { + type = "value", + description = "{ name = \"ALTERASE\", value = 153 }", + }, + APOSTROPHE = { + type = "value", + description = "{ name = \"APOSTROPHE\", value = 52 }", + }, + APP1 = { + type = "value", + description = "{ name = \"APP1\", value = 283 }", + }, + APP2 = { + type = "value", + description = "{ name = \"APP2\", value = 284 }", + }, + APPLICATION = { + type = "value", + description = "{ name = \"APPLICATION\", value = 101 }", + }, + AUDIOFASTFORWARD = { + type = "value", + description = "{ name = \"AUDIOFASTFORWARD\", value = 286 }", + }, + AUDIOMUTE = { + type = "value", + description = "{ name = \"AUDIOMUTE\", value = 262 }", + }, + AUDIONEXT = { + type = "value", + description = "{ name = \"AUDIONEXT\", value = 258 }", + }, + AUDIOPLAY = { + type = "value", + description = "{ name = \"AUDIOPLAY\", value = 261 }", + }, + AUDIOPREV = { + type = "value", + description = "{ name = \"AUDIOPREV\", value = 259 }", + }, + AUDIOREWIND = { + type = "value", + description = "{ name = \"AUDIOREWIND\", value = 285 }", + }, + AUDIOSTOP = { + type = "value", + description = "{ name = \"AUDIOSTOP\", value = 260 }", + }, + B = { + type = "value", + description = "{ name = \"B\", value = 5 }", + }, + BACKSLASH = { + type = "value", + description = "{ name = \"BACKSLASH\", value = 49 }", + }, + BACKSPACE = { + type = "value", + description = "{ name = \"BACKSPACE\", value = 42 }", + }, + BRIGHTNESSDOWN = { + type = "value", + description = "{ name = \"BRIGHTNESSDOWN\", value = 275 }", + }, + BRIGHTNESSUP = { + type = "value", + description = "{ name = \"BRIGHTNESSUP\", value = 276 }", + }, + C = { + type = "value", + description = "{ name = \"C\", value = 6 }", + }, + CALCULATOR = { + type = "value", + description = "{ name = \"CALCULATOR\", value = 266 }", + }, + CANCEL = { + type = "value", + description = "{ name = \"CANCEL\", value = 155 }", + }, + CAPSLOCK = { + type = "value", + description = "{ name = \"CAPSLOCK\", value = 57 }", + }, + CLEAR = { + type = "value", + description = "{ name = \"CLEAR\", value = 156 }", + }, + CLEARAGAIN = { + type = "value", + description = "{ name = \"CLEARAGAIN\", value = 162 }", + }, + COMMA = { + type = "value", + description = "{ name = \"COMMA\", value = 54 }", + }, + COMPUTER = { + type = "value", + description = "{ name = \"COMPUTER\", value = 267 }", + }, + COPY = { + type = "value", + description = "{ name = \"COPY\", value = 124 }", + }, + CRSEL = { + type = "value", + description = "{ name = \"CRSEL\", value = 163 }", + }, + CURRENCYSUBUNIT = { + type = "value", + description = "{ name = \"CURRENCYSUBUNIT\", value = 181 }", + }, + CURRENCYUNIT = { + type = "value", + description = "{ name = \"CURRENCYUNIT\", value = 180 }", + }, + CUT = { + type = "value", + description = "{ name = \"CUT\", value = 123 }", + }, + D = { + type = "value", + description = "{ name = \"D\", value = 7 }", + }, + DECIMALSEPARATOR = { + type = "value", + description = "{ name = \"DECIMALSEPARATOR\", value = 179 }", + }, + DELETE = { + type = "value", + description = "{ name = \"DELETE\", value = 76 }", + }, + DISPLAYSWITCH = { + type = "value", + description = "{ name = \"DISPLAYSWITCH\", value = 277 }", + }, + DOWN = { + type = "value", + description = "{ name = \"DOWN\", value = 81 }", + }, + E = { + type = "value", + description = "{ name = \"E\", value = 8 }", + }, + EJECT = { + type = "value", + description = "{ name = \"EJECT\", value = 281 }", + }, + END = { + type = "value", + description = "{ name = \"END\", value = 77 }", + }, + EQUALS = { + type = "value", + description = "{ name = \"EQUALS\", value = 46 }", + }, + ESCAPE = { + type = "value", + description = "{ name = \"ESCAPE\", value = 41 }", + }, + EXECUTE = { + type = "value", + description = "{ name = \"EXECUTE\", value = 116 }", + }, + EXSEL = { + type = "value", + description = "{ name = \"EXSEL\", value = 164 }", + }, + F = { + type = "value", + description = "{ name = \"F\", value = 9 }", + }, + F1 = { + type = "value", + description = "{ name = \"F1\", value = 58 }", + }, + F10 = { + type = "value", + description = "{ name = \"F10\", value = 67 }", + }, + F11 = { + type = "value", + description = "{ name = \"F11\", value = 68 }", + }, + F12 = { + type = "value", + description = "{ name = \"F12\", value = 69 }", + }, + F13 = { + type = "value", + description = "{ name = \"F13\", value = 104 }", + }, + F14 = { + type = "value", + description = "{ name = \"F14\", value = 105 }", + }, + F15 = { + type = "value", + description = "{ name = \"F15\", value = 106 }", + }, + F16 = { + type = "value", + description = "{ name = \"F16\", value = 107 }", + }, + F17 = { + type = "value", + description = "{ name = \"F17\", value = 108 }", + }, + F18 = { + type = "value", + description = "{ name = \"F18\", value = 109 }", + }, + F19 = { + type = "value", + description = "{ name = \"F19\", value = 110 }", + }, + F2 = { + type = "value", + description = "{ name = \"F2\", value = 59 }", + }, + F20 = { + type = "value", + description = "{ name = \"F20\", value = 111 }", + }, + F21 = { + type = "value", + description = "{ name = \"F21\", value = 112 }", + }, + F22 = { + type = "value", + description = "{ name = \"F22\", value = 113 }", + }, + F23 = { + type = "value", + description = "{ name = \"F23\", value = 114 }", + }, + F24 = { + type = "value", + description = "{ name = \"F24\", value = 115 }", + }, + F3 = { + type = "value", + description = "{ name = \"F3\", value = 60 }", + }, + F4 = { + type = "value", + description = "{ name = \"F4\", value = 61 }", + }, + F5 = { + type = "value", + description = "{ name = \"F5\", value = 62 }", + }, + F6 = { + type = "value", + description = "{ name = \"F6\", value = 63 }", + }, + F7 = { + type = "value", + description = "{ name = \"F7\", value = 64 }", + }, + F8 = { + type = "value", + description = "{ name = \"F8\", value = 65 }", + }, + F9 = { + type = "value", + description = "{ name = \"F9\", value = 66 }", + }, + FIND = { + type = "value", + description = "{ name = \"FIND\", value = 126 }", + }, + G = { + type = "value", + description = "{ name = \"G\", value = 10 }", + }, + GRAVE = { + type = "value", + description = "{ name = \"GRAVE\", value = 53 }", + }, + H = { + type = "value", + description = "{ name = \"H\", value = 11 }", + }, + HELP = { + type = "value", + description = "{ name = \"HELP\", value = 117 }", + }, + HOME = { + type = "value", + description = "{ name = \"HOME\", value = 74 }", + }, + I = { + type = "value", + description = "{ name = \"I\", value = 12 }", + }, + INSERT = { + type = "value", + description = "{ name = \"INSERT\", value = 73 }", + }, + INTERNATIONAL1 = { + type = "value", + description = "{ name = \"INTERNATIONAL1\", value = 135 }", + }, + INTERNATIONAL2 = { + type = "value", + description = "{ name = \"INTERNATIONAL2\", value = 136 }", + }, + INTERNATIONAL3 = { + type = "value", + description = "{ name = \"INTERNATIONAL3\", value = 137 }", + }, + INTERNATIONAL4 = { + type = "value", + description = "{ name = \"INTERNATIONAL4\", value = 138 }", + }, + INTERNATIONAL5 = { + type = "value", + description = "{ name = \"INTERNATIONAL5\", value = 139 }", + }, + INTERNATIONAL6 = { + type = "value", + description = "{ name = \"INTERNATIONAL6\", value = 140 }", + }, + INTERNATIONAL7 = { + type = "value", + description = "{ name = \"INTERNATIONAL7\", value = 141 }", + }, + INTERNATIONAL8 = { + type = "value", + description = "{ name = \"INTERNATIONAL8\", value = 142 }", + }, + INTERNATIONAL9 = { + type = "value", + description = "{ name = \"INTERNATIONAL9\", value = 143 }", + }, + J = { + type = "value", + description = "{ name = \"J\", value = 13 }", + }, + K = { + type = "value", + description = "{ name = \"K\", value = 14 }", + }, + KBDILLUMDOWN = { + type = "value", + description = "{ name = \"KBDILLUMDOWN\", value = 279 }", + }, + KBDILLUMTOGGLE = { + type = "value", + description = "{ name = \"KBDILLUMTOGGLE\", value = 278 }", + }, + KBDILLUMUP = { + type = "value", + description = "{ name = \"KBDILLUMUP\", value = 280 }", + }, + KP_0 = { + type = "value", + description = "{ name = \"KP_0\", value = 98 }", + }, + KP_00 = { + type = "value", + description = "{ name = \"KP_00\", value = 176 }", + }, + KP_000 = { + type = "value", + description = "{ name = \"KP_000\", value = 177 }", + }, + KP_1 = { + type = "value", + description = "{ name = \"KP_1\", value = 89 }", + }, + KP_2 = { + type = "value", + description = "{ name = \"KP_2\", value = 90 }", + }, + KP_3 = { + type = "value", + description = "{ name = \"KP_3\", value = 91 }", + }, + KP_4 = { + type = "value", + description = "{ name = \"KP_4\", value = 92 }", + }, + KP_5 = { + type = "value", + description = "{ name = \"KP_5\", value = 93 }", + }, + KP_6 = { + type = "value", + description = "{ name = \"KP_6\", value = 94 }", + }, + KP_7 = { + type = "value", + description = "{ name = \"KP_7\", value = 95 }", + }, + KP_8 = { + type = "value", + description = "{ name = \"KP_8\", value = 96 }", + }, + KP_9 = { + type = "value", + description = "{ name = \"KP_9\", value = 97 }", + }, + KP_A = { + type = "value", + description = "{ name = \"KP_A\", value = 188 }", + }, + KP_AMPERSAND = { + type = "value", + description = "{ name = \"KP_AMPERSAND\", value = 199 }", + }, + KP_AT = { + type = "value", + description = "{ name = \"KP_AT\", value = 206 }", + }, + KP_B = { + type = "value", + description = "{ name = \"KP_B\", value = 189 }", + }, + KP_BACKSPACE = { + type = "value", + description = "{ name = \"KP_BACKSPACE\", value = 187 }", + }, + KP_BINARY = { + type = "value", + description = "{ name = \"KP_BINARY\", value = 218 }", + }, + KP_C = { + type = "value", + description = "{ name = \"KP_C\", value = 190 }", + }, + KP_CLEAR = { + type = "value", + description = "{ name = \"KP_CLEAR\", value = 216 }", + }, + KP_CLEARENTRY = { + type = "value", + description = "{ name = \"KP_CLEARENTRY\", value = 217 }", + }, + KP_COLON = { + type = "value", + description = "{ name = \"KP_COLON\", value = 203 }", + }, + KP_COMMA = { + type = "value", + description = "{ name = \"KP_COMMA\", value = 133 }", + }, + KP_D = { + type = "value", + description = "{ name = \"KP_D\", value = 191 }", + }, + KP_DBLAMPERSAND = { + type = "value", + description = "{ name = \"KP_DBLAMPERSAND\", value = 200 }", + }, + KP_DBLVERTICALBAR = { + type = "value", + description = "{ name = \"KP_DBLVERTICALBAR\", value = 202 }", + }, + KP_DECIMAL = { + type = "value", + description = "{ name = \"KP_DECIMAL\", value = 220 }", + }, + KP_DIVIDE = { + type = "value", + description = "{ name = \"KP_DIVIDE\", value = 84 }", + }, + KP_E = { + type = "value", + description = "{ name = \"KP_E\", value = 192 }", + }, + KP_ENTER = { + type = "value", + description = "{ name = \"KP_ENTER\", value = 88 }", + }, + KP_EQUALS = { + type = "value", + description = "{ name = \"KP_EQUALS\", value = 103 }", + }, + KP_EQUALSAS400 = { + type = "value", + description = "{ name = \"KP_EQUALSAS400\", value = 134 }", + }, + KP_EXCLAM = { + type = "value", + description = "{ name = \"KP_EXCLAM\", value = 207 }", + }, + KP_F = { + type = "value", + description = "{ name = \"KP_F\", value = 193 }", + }, + KP_GREATER = { + type = "value", + description = "{ name = \"KP_GREATER\", value = 198 }", + }, + KP_HASH = { + type = "value", + description = "{ name = \"KP_HASH\", value = 204 }", + }, + KP_HEXADECIMAL = { + type = "value", + description = "{ name = \"KP_HEXADECIMAL\", value = 221 }", + }, + KP_LEFTBRACE = { + type = "value", + description = "{ name = \"KP_LEFTBRACE\", value = 184 }", + }, + KP_LEFTPAREN = { + type = "value", + description = "{ name = \"KP_LEFTPAREN\", value = 182 }", + }, + KP_LESS = { + type = "value", + description = "{ name = \"KP_LESS\", value = 197 }", + }, + KP_MEMADD = { + type = "value", + description = "{ name = \"KP_MEMADD\", value = 211 }", + }, + KP_MEMCLEAR = { + type = "value", + description = "{ name = \"KP_MEMCLEAR\", value = 210 }", + }, + KP_MEMDIVIDE = { + type = "value", + description = "{ name = \"KP_MEMDIVIDE\", value = 214 }", + }, + KP_MEMMULTIPLY = { + type = "value", + description = "{ name = \"KP_MEMMULTIPLY\", value = 213 }", + }, + KP_MEMRECALL = { + type = "value", + description = "{ name = \"KP_MEMRECALL\", value = 209 }", + }, + KP_MEMSTORE = { + type = "value", + description = "{ name = \"KP_MEMSTORE\", value = 208 }", + }, + KP_MEMSUBTRACT = { + type = "value", + description = "{ name = \"KP_MEMSUBTRACT\", value = 212 }", + }, + KP_MINUS = { + type = "value", + description = "{ name = \"KP_MINUS\", value = 86 }", + }, + KP_MULTIPLY = { + type = "value", + description = "{ name = \"KP_MULTIPLY\", value = 85 }", + }, + KP_OCTAL = { + type = "value", + description = "{ name = \"KP_OCTAL\", value = 219 }", + }, + KP_PERCENT = { + type = "value", + description = "{ name = \"KP_PERCENT\", value = 196 }", + }, + KP_PERIOD = { + type = "value", + description = "{ name = \"KP_PERIOD\", value = 99 }", + }, + KP_PLUS = { + type = "value", + description = "{ name = \"KP_PLUS\", value = 87 }", + }, + KP_PLUSMINUS = { + type = "value", + description = "{ name = \"KP_PLUSMINUS\", value = 215 }", + }, + KP_POWER = { + type = "value", + description = "{ name = \"KP_POWER\", value = 195 }", + }, + KP_RIGHTBRACE = { + type = "value", + description = "{ name = \"KP_RIGHTBRACE\", value = 185 }", + }, + KP_RIGHTPAREN = { + type = "value", + description = "{ name = \"KP_RIGHTPAREN\", value = 183 }", + }, + KP_SPACE = { + type = "value", + description = "{ name = \"KP_SPACE\", value = 205 }", + }, + KP_TAB = { + type = "value", + description = "{ name = \"KP_TAB\", value = 186 }", + }, + KP_VERTICALBAR = { + type = "value", + description = "{ name = \"KP_VERTICALBAR\", value = 201 }", + }, + KP_XOR = { + type = "value", + description = "{ name = \"KP_XOR\", value = 194 }", + }, + L = { + type = "value", + description = "{ name = \"L\", value = 15 }", + }, + LALT = { + type = "value", + description = "{ name = \"LALT\", value = 226 }", + }, + LANG1 = { + type = "value", + description = "{ name = \"LANG1\", value = 144 }", + }, + LANG2 = { + type = "value", + description = "{ name = \"LANG2\", value = 145 }", + }, + LANG3 = { + type = "value", + description = "{ name = \"LANG3\", value = 146 }", + }, + LANG4 = { + type = "value", + description = "{ name = \"LANG4\", value = 147 }", + }, + LANG5 = { + type = "value", + description = "{ name = \"LANG5\", value = 148 }", + }, + LANG6 = { + type = "value", + description = "{ name = \"LANG6\", value = 149 }", + }, + LANG7 = { + type = "value", + description = "{ name = \"LANG7\", value = 150 }", + }, + LANG8 = { + type = "value", + description = "{ name = \"LANG8\", value = 151 }", + }, + LANG9 = { + type = "value", + description = "{ name = \"LANG9\", value = 152 }", + }, + LCTRL = { + type = "value", + description = "{ name = \"LCTRL\", value = 224 }", + }, + LEFT = { + type = "value", + description = "{ name = \"LEFT\", value = 80 }", + }, + LEFTBRACKET = { + type = "value", + description = "{ name = \"LEFTBRACKET\", value = 47 }", + }, + LGUI = { + type = "value", + description = "{ name = \"LGUI\", value = 227 }", + }, + LSHIFT = { + type = "value", + description = "{ name = \"LSHIFT\", value = 225 }", + }, + M = { + type = "value", + description = "{ name = \"M\", value = 16 }", + }, + MAIL = { + type = "value", + description = "{ name = \"MAIL\", value = 265 }", + }, + MAIN_0 = { + type = "value", + description = "{ name = \"MAIN_0\", value = 39 }", + }, + MAIN_1 = { + type = "value", + description = "{ name = \"MAIN_1\", value = 30 }", + }, + MAIN_2 = { + type = "value", + description = "{ name = \"MAIN_2\", value = 31 }", + }, + MAIN_3 = { + type = "value", + description = "{ name = \"MAIN_3\", value = 32 }", + }, + MAIN_4 = { + type = "value", + description = "{ name = \"MAIN_4\", value = 33 }", + }, + MAIN_5 = { + type = "value", + description = "{ name = \"MAIN_5\", value = 34 }", + }, + MAIN_6 = { + type = "value", + description = "{ name = \"MAIN_6\", value = 35 }", + }, + MAIN_7 = { + type = "value", + description = "{ name = \"MAIN_7\", value = 36 }", + }, + MAIN_8 = { + type = "value", + description = "{ name = \"MAIN_8\", value = 37 }", + }, + MAIN_9 = { + type = "value", + description = "{ name = \"MAIN_9\", value = 38 }", + }, + MEDIASELECT = { + type = "value", + description = "{ name = \"MEDIASELECT\", value = 263 }", + }, + MENU = { + type = "value", + description = "{ name = \"MENU\", value = 118 }", + }, + MINUS = { + type = "value", + description = "{ name = \"MINUS\", value = 45 }", + }, + MODE = { + type = "value", + description = "{ name = \"MODE\", value = 257 }", + }, + MUTE = { + type = "value", + description = "{ name = \"MUTE\", value = 127 }", + }, + N = { + type = "value", + description = "{ name = \"N\", value = 17 }", + }, + NONUSBACKSLASH = { + type = "value", + description = "{ name = \"NONUSBACKSLASH\", value = 100 }", + }, + NONUSHASH = { + type = "value", + description = "{ name = \"NONUSHASH\", value = 50 }", + }, + NUMLOCKCLEAR = { + type = "value", + description = "{ name = \"NUMLOCKCLEAR\", value = 83 }", + }, + O = { + type = "value", + description = "{ name = \"O\", value = 18 }", + }, + OPER = { + type = "value", + description = "{ name = \"OPER\", value = 161 }", + }, + OUT = { + type = "value", + description = "{ name = \"OUT\", value = 160 }", + }, + P = { + type = "value", + description = "{ name = \"P\", value = 19 }", + }, + PAGEDOWN = { + type = "value", + description = "{ name = \"PAGEDOWN\", value = 78 }", + }, + PAGEUP = { + type = "value", + description = "{ name = \"PAGEUP\", value = 75 }", + }, + PASTE = { + type = "value", + description = "{ name = \"PASTE\", value = 125 }", + }, + PAUSE = { + type = "value", + description = "{ name = \"PAUSE\", value = 72 }", + }, + PERIOD = { + type = "value", + description = "{ name = \"PERIOD\", value = 55 }", + }, + POWER = { + type = "value", + description = "{ name = \"POWER\", value = 102 }", + }, + PRINTSCREEN = { + type = "value", + description = "{ name = \"PRINTSCREEN\", value = 70 }", + }, + PRIOR = { + type = "value", + description = "{ name = \"PRIOR\", value = 157 }", + }, + Q = { + type = "value", + description = "{ name = \"Q\", value = 20 }", + }, + R = { + type = "value", + description = "{ name = \"R\", value = 21 }", + }, + RALT = { + type = "value", + description = "{ name = \"RALT\", value = 230 }", + }, + RCTRL = { + type = "value", + description = "{ name = \"RCTRL\", value = 228 }", + }, + RETURN = { + type = "value", + description = "{ name = \"RETURN\", value = 40 }", + }, + RETURN2 = { + type = "value", + description = "{ name = \"RETURN2\", value = 158 }", + }, + RGUI = { + type = "value", + description = "{ name = \"RGUI\", value = 231 }", + }, + RIGHT = { + type = "value", + description = "{ name = \"RIGHT\", value = 79 }", + }, + RIGHTBRACKET = { + type = "value", + description = "{ name = \"RIGHTBRACKET\", value = 48 }", + }, + RSHIFT = { + type = "value", + description = "{ name = \"RSHIFT\", value = 229 }", + }, + S = { + type = "value", + description = "{ name = \"S\", value = 22 }", + }, + SCROLLLOCK = { + type = "value", + description = "{ name = \"SCROLLLOCK\", value = 71 }", + }, + SELECT = { + type = "value", + description = "{ name = \"SELECT\", value = 119 }", + }, + SEMICOLON = { + type = "value", + description = "{ name = \"SEMICOLON\", value = 51 }", + }, + SEPARATOR = { + type = "value", + description = "{ name = \"SEPARATOR\", value = 159 }", + }, + SLASH = { + type = "value", + description = "{ name = \"SLASH\", value = 56 }", + }, + SLEEP = { + type = "value", + description = "{ name = \"SLEEP\", value = 282 }", + }, + SPACE = { + type = "value", + description = "{ name = \"SPACE\", value = 44 }", + }, + STOP = { + type = "value", + description = "{ name = \"STOP\", value = 120 }", + }, + SYSREQ = { + type = "value", + description = "{ name = \"SYSREQ\", value = 154 }", + }, + T = { + type = "value", + description = "{ name = \"T\", value = 23 }", + }, + TAB = { + type = "value", + description = "{ name = \"TAB\", value = 43 }", + }, + THOUSANDSSEPARATOR = { + type = "value", + description = "{ name = \"THOUSANDSSEPARATOR\", value = 178 }", + }, + U = { + type = "value", + description = "{ name = \"U\", value = 24 }", + }, + UNDO = { + type = "value", + description = "{ name = \"UNDO\", value = 122 }", + }, + UP = { + type = "value", + description = "{ name = \"UP\", value = 82 }", + }, + V = { + type = "value", + description = "{ name = \"V\", value = 25 }", + }, + VOLUMEDOWN = { + type = "value", + description = "{ name = \"VOLUMEDOWN\", value = 129 }", + }, + VOLUMEUP = { + type = "value", + description = "{ name = \"VOLUMEUP\", value = 128 }", + }, + W = { + type = "value", + description = "{ name = \"W\", value = 26 }", + }, + WWW = { + type = "value", + description = "{ name = \"WWW\", value = 264 }", + }, + X = { + type = "value", + description = "{ name = \"X\", value = 27 }", + }, + Y = { + type = "value", + description = "{ name = \"Y\", value = 28 }", + }, + Z = { + type = "value", + description = "{ name = \"Z\", value = 29 }", + }, + }, + }, + SHAPE_BOX = { + type = "value", + description = "Kollisionsformen für bodyNew.\n= 0", + }, + SHAPE_CAPSULE = { + type = "value", + description = "Kollisionsformen für bodyNew.\n= 2", + }, + SHAPE_CYLINDER = { + type = "value", + description = "Kollisionsformen für bodyNew.\n= 3", + }, + SHAPE_HULL = { + type = "value", + description = "Kollisionsformen für bodyNew.\n= 4", + }, + SHAPE_MESH = { + type = "value", + description = "Kollisionsformen für bodyNew.\n= 5", + }, + SHAPE_SPHERE = { + type = "value", + description = "Kollisionsformen für bodyNew.\n= 1", + }, + SINGE_AXIS_STRIDE = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 3", + }, + SINGE_CONTROLLER_AXES = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 6", + }, + SINGE_CONTROLLER_BUTTONS = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 15", + }, + SINGE_DEAD_ZONE = { + type = "value", + description = "Die DEAD_ZONE aus controls.cfg, in rohen Achseneinheiten.", + }, + SINGE_DISC = { + type = "value", + description = "Wahr, wenn das Spiel eine Laserdisc hat; falsch, wenn die Zeichenfläche die Welt ist.", + }, + SINGE_FRAMEWORK_VERSION = { + type = "value", + description = "Die Engine-Version, als Ganzzahlen, als String (\"v3.00\") und als die Zahl, die singeVersion() zurückgibt.\n= 3.0", + }, + SINGE_GAMEPAD_BASE = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 500", + }, + SINGE_GAMEPAD_BUTTON_OFFSET = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 18", + }, + SINGE_GAMEPAD_STRIDE = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 100", + }, + SINGE_LEGACY_SPRITE_ARGS = { + type = "value", + description = "Wahr, wenn das Spiel die Sprite-Argumentreihenfolge von 2.10 verlangt hat.", + }, + SINGE_MAX_CONTROLLERS = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 4", + }, + SINGE_MAX_MICE = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 4", + }, + SINGE_MOUSE_BASE = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 1000", + }, + SINGE_MOUSE_STRIDE = { + type = "value", + description = "Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; Framework.singe baut daraus die Tabellen GAMEPAD_N und MOUSE_N, und ein Servicebildschirm geht damit ein Pad durch.\n= 100", + }, + SINGE_ROTATE_STEP = { + type = "value", + description = "Was vldpSetScale, vldpSetShift und vldpSetRotate annehmen. Begrenze auf diese Werte statt auf eigene Kopien: Eine Verschiebung außerhalb des Bereichs beendet das Skript.\n= 90", + }, + SINGE_SCALE_MAX = { + type = "value", + description = "Was vldpSetScale, vldpSetShift und vldpSetRotate annehmen. Begrenze auf diese Werte statt auf eigene Kopien: Eine Verschiebung außerhalb des Bereichs beendet das Skript.\n= 100", + }, + SINGE_SCALE_MIN = { + type = "value", + description = "Was vldpSetScale, vldpSetShift und vldpSetRotate annehmen. Begrenze auf diese Werte statt auf eigene Kopien: Eine Verschiebung außerhalb des Bereichs beendet das Skript.\n= 25", + }, + SINGE_SHIFT_MAX = { + type = "value", + description = "Was vldpSetScale, vldpSetShift und vldpSetRotate annehmen. Begrenze auf diese Werte statt auf eigene Kopien: Eine Verschiebung außerhalb des Bereichs beendet das Skript.\n= 100", + }, + SINGE_SHIFT_MIN = { + type = "value", + description = "Was vldpSetScale, vldpSetShift und vldpSetRotate annehmen. Begrenze auf diese Werte statt auf eigene Kopien: Eine Verschiebung außerhalb des Bereichs beendet das Skript.\n= -100", + }, + SINGE_TRIGGER_THRESHOLD = { + type = "value", + description = "Die Schwelle der analogen Trigger in denselben rohen Einheiten; 0, wenn die Trigger auf SINGE_DEAD_ZONE liegen, was der Standard ist.", + }, + SINGE_UPDATE_CHECK = { + type = "value", + description = "= true", + }, + SINGE_VERSION_MAJOR = { + type = "value", + description = "Die Engine-Version, als Ganzzahlen, als String (\"v3.00\") und als die Zahl, die singeVersion() zurückgibt.\n= 3", + }, + SINGE_VERSION_MINOR = { + type = "value", + description = "Die Engine-Version, als Ganzzahlen, als String (\"v3.00\") und als die Zahl, die singeVersion() zurückgibt.\n= 0", + }, + SINGE_VERSION_STRING = { + type = "value", + description = "Die Engine-Version, als Ganzzahlen, als String (\"v3.00\") und als die Zahl, die singeVersion() zurückgibt.\n= \"v3.00b1\"", + }, + SINGLE_MOUSE = { + type = "value", + description = "Argumente für mouseSetMode.\n= 100", + }, + SOFT_BODY = { + type = "value", + description = "Arten von Soft Bodies für softNew.\n= 1", + }, + SOFT_CLOTH = { + type = "value", + description = "Arten von Soft Bodies für softNew.\n= 0", + }, + SOFT_ROPE = { + type = "value", + description = "Arten von Soft Bodies für softNew.\n= 2", + }, + SOUND_ERROR_INVALID = { + type = "value", + description = "-1, was soundPlay zurückgibt, wenn kein Kanal frei ist.\n= -1", + }, + SOUND_REMOVE_HANDLE = { + type = "value", + description = "-1, was soundPlay zurückgibt, wenn kein Kanal frei ist.\n= -1", + }, + SPRITE_ADD = { + type = "value", + description = "Blending für spriteSetBlend.\n= 1", + }, + SPRITE_ALPHA = { + type = "value", + description = "Blending für spriteSetBlend.\n= 0", + }, + SPRITE_MULTIPLY = { + type = "value", + description = "Blending für spriteSetBlend.\n= 2", + }, + SWITCH_BUTTON1 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 6", + }, + SWITCH_BUTTON2 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 7", + }, + SWITCH_BUTTON3 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 8", + }, + SWITCH_BUTTON4 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 21", + }, + SWITCH_COIN1 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 9", + }, + SWITCH_COIN2 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 10", + }, + SWITCH_CONSOLE = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 20", + }, + SWITCH_DOWN = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 2", + }, + SWITCH_GRAB = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 23", + }, + SWITCH_LEFT = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 1", + }, + SWITCH_MOUSE_DISCONNECT = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 24", + }, + SWITCH_PAUSE = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 19", + }, + SWITCH_QUIT = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 18", + }, + SWITCH_RECORD = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 25", + }, + SWITCH_RESET = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 16", + }, + SWITCH_RIGHT = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 3", + }, + SWITCH_SCREENSHOT = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 17", + }, + SWITCH_SERVICE = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 14", + }, + SWITCH_SKILL1 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 11", + }, + SWITCH_SKILL2 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 12", + }, + SWITCH_SKILL3 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 13", + }, + SWITCH_START1 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 4", + }, + SWITCH_START2 = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 5", + }, + SWITCH_TEST = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 15", + }, + SWITCH_TILT = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 22", + }, + SWITCH_UP = { + type = "value", + description = "Werte, die in MODE_NORMAL an onInputPressed / onInputReleased übergeben werden. SWITCH_MOUSE_DISCONNECT löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. SWITCH_RECORD ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.\n= 0", + }, + SelectOptionsProxy = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ }", + }, + __setters = { + type = "value", + description = "{ }", + }, + }, + }, + TONEMAP_ACES = { + type = "value", + description = "Tonkurven für sceneSetTonemap.\n= 2", + }, + TONEMAP_NEUTRAL = { + type = "value", + description = "Tonkurven für sceneSetTonemap.\n= 1", + }, + TONEMAP_NONE = { + type = "value", + description = "Tonkurven für sceneSetTonemap.\n= 0", + }, + VEHICLE_BOAT = { + type = "value", + description = "Fahrzeugarten für vehicleNew.\n= 3", + }, + VEHICLE_CAR = { + type = "value", + description = "Fahrzeugarten für vehicleNew.\n= 0", + }, + VEHICLE_MOTORCYCLE = { + type = "value", + description = "Fahrzeugarten für vehicleNew.\n= 1", + }, + VEHICLE_TANK = { + type = "value", + description = "Fahrzeugarten für vehicleNew.\n= 2", + }, + Vector2f = { + type = "lib", + description = "", + childs = { + DotProduct = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Normalise = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + Rotate = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + __getters = { + type = "value", + description = "{ magnitude, x, y }", + }, + __setters = { + type = "value", + description = "{ x, y }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + Vector2i = { + type = "lib", + description = "", + childs = { + __getters = { + type = "value", + description = "{ magnitude, x, y }", + }, + __setters = { + type = "value", + description = "{ x, y }", + }, + new = { + type = "function", + description = "", + args = "(...)", + returns = "()", + }, + }, + }, + allowSocketCall = { + type = "function", + description = "Hypseus-Erweiterung, in Singe immer false. Hypseus antwortet damit, ob der Benutzer ihm erlaubt hat, Daten an einen Statistikserver zu senden, was dort -noserversend abschaltet; das Senden selbst ist dort auskommentiert. Singe sendet nirgendwohin etwas, also ist nichts erlaubt, und die Antwort ändert sich nie. Die Funktion gibt es, damit ein Spiel, das fragt, nicht an einer fehlenden Funktion stehen bleibt.\nRückgabe: ein Boolean, immer false.\nSeit: 3.00", + args = "()", + returns = "(allowed)", + }, + animationGetTime = { + type = "function", + description = "Die aktuelle Zeit im Clip der Basisebene, ob er spielt oder pausiert, in den eigenen Sekunden des Clips (ein speed von 2 bewegt sie doppelt so schnell). Ein Clip in Schleife springt an seinem Ende auf 0 zurück; einer ohne Schleife bleibt bei seiner Länge stehen. Wurde noch nichts gespielt, ergibt das 0. Clips auf den oberen Ebenen werden nicht gemeldet. Ein Knoten, der keine Instanzwurzel ist, beendet das Skript.\nRückgabe: Sekunden als Zahl.\nSeit: 3.00.", + args = "(node)", + returns = "(seconds)", + }, + animationIsPlaying = { + type = "function", + description = "Ob der Clip der Basisebene auf dieser Instanz weiterläuft. Ein Clip ohne Schleife hört auf zu spielen, wenn er sein Ende erreicht, und animationPause und animationStop machen dies beide false; ein Clip in Schleife bleibt true, bis er gestoppt wird. Anders als bei den übrigen Aufrufen hier beendet ein Knoten, der keine Instanzwurzel ist, nicht das Skript, sondern bekommt false als Antwort.\nRückgabe: Boolean.\nSeit: 3.00.", + args = "(node)", + returns = "(boolean)", + }, + animationPause = { + type = "function", + description = "Friert den Clip der Basisebene bei seiner aktuellen Zeit ein; die Knoten halten diese Pose, bis animationResume ihn fortsetzt oder ein anderer Clip gespielt wird. Clips auf den oberen Ebenen laufen weiter. Eine Instanz zu pausieren, auf der nichts spielt, ist harmlos. Ein Knoten, der keine Instanzwurzel ist, beendet das Skript.\nSeit: 3.00.", + args = "(node)", + returns = "()", + }, + animationPlay = { + type = "function", + description = "Startet einen der Clips des Modells von seinem Anfang an auf der Basisebene dieser Instanz und ersetzt, was dort gespielt hat; die Pose wird sofort aktualisiert. Mit einem fade läuft der alte Clip weiter, während er über so viele Sekunden ausgeblendet wird, und wenn nichts gespielt hat, beginnt die Überblendung bei der Pose, in der die Knoten gerade sind (eine eben abgeschaltete Ragdoll, eine per Skript gesetzte Pose), wobei Knoten, die der neue Clip nicht animiert, sanft in die Ruhepose des Modells zurückgleiten. Ein Knoten, der keine Instanzwurzel ist, ein Name, den das Modell nicht hat, oder eine Zahl außerhalb von 1 bis zur Anzahl der Clips beendet das Skript.\nSeit: 3.00.", + args = "(node, nameOrIndex [, loop: boolean, speed, fade])", + returns = "()", + }, + animationPlayLayer = { + type = "function", + description = "Spielt einen Clip auf einer der vier Ebenen über der Basis, über alles darunter geblendet nach dem Gewicht der Ebene (animationSetLayerWeight, Standard 1) und auf einen Teil des Skeletts beschränkt, wenn eine Maske gesetzt ist (animationSetLayerMask). Höhere Ebenen werden über niedrigere geblendet; jede Ebene führt ihren eigenen Clip, ihre eigene Zeit und Schleife unabhängig von der Basis. nameOrIndex, loop, speed und fade bedeuten dasselbe wie bei animationPlay, mit einem Unterschied: Ein fade auf einer Ebene, die leer war, fährt das Gewicht der Ebene von 0 hoch, statt von einer Pose überzublenden. Eine Ebene außerhalb von 1 bis 4, ein Knoten, der keine Instanzwurzel ist, oder ein Clip, den das Modell nicht hat, beendet das Skript.\nSeit: 3.00.", + args = "(node, layer, nameOrIndex [, loop, speed, fade])", + returns = "()", + }, + animationResume = { + type = "function", + description = "Setzt den Clip der Basisebene dort fort, wo animationPause ihn eingefroren hat, oder dort, wohin animationSetTime ihn inzwischen bewegt hat. Einen Clip fortzusetzen, der bereits spielt, ändert nichts. Eine Instanz ohne Clip auf ihrer Basisebene (nie gespielt oder gestoppt) beendet das Skript, ebenso ein Knoten, der keine Instanzwurzel ist.\nSeit: 3.00.", + args = "(node)", + returns = "()", + }, + animationSetLayerMask = { + type = "function", + description = "Beschränkt eine Ebene auf maskNode und alles darunter, sodass der Clip auf dieser Ebene nur diesen Teil des Modells bewegt und der Rest weiter den Ebenen darunter folgt. maskNode muss einer der eigenen Knoten der Instanz sein, gefunden mit nodeFind(name, node); die Instanzwurzel selbst zählt nicht. maskNode wegzulassen oder nil zu übergeben hebt die Beschränkung auf. Die Maske bleibt über spätere Aufrufe von animationPlayLayer hinweg auf der Ebene. Eine Ebene außerhalb von 1 bis 4 beendet das Skript, ebenso ein Knoten, der keine Instanzwurzel ist, oder ein maskNode, der nicht zu dieser Instanz gehört.\nSeit: 3.00.", + args = "(node, layer [, maskNode])", + returns = "()", + }, + animationSetLayerWeight = { + type = "function", + description = "Wie stark der Clip einer Ebene über dem durchscheint, was darunter liegt, 0 (gar nicht) bis 1 (voll, der Standard); Werte außerhalb dieses Bereichs werden begrenzt. Das Gewicht gehört zur Ebene, nicht zum Clip, also übersteht es animationPlayLayer und animationStop auf dieser Ebene. Ändere es jeden Frame ein wenig, um eine Ebene von Hand ein- oder auszublenden, oder lass einen fade bei animationPlayLayer das einmal erledigen. Eine Ebene außerhalb von 1 bis 4 oder ein Knoten, der keine Instanzwurzel ist, beendet das Skript.\nSeit: 3.00.", + args = "(node, layer, weight)", + returns = "()", + }, + animationSetTime = { + type = "function", + description = "Lässt den Clip der Basisebene zu einer Zeit in seinen eigenen Sekunden springen; negative Werte werden zu 0. Die Pose wird sofort aktualisiert, ob der Clip spielt oder nicht, sodass eine pausierte Instanz auf einen genauen Frame gespult werden kann und eine spielende von der neuen Zeit aus weiterläuft. Eine laufende Überblendung auf der Basisebene wird abgebrochen. Eine Instanz ohne Clip auf ihrer Basisebene beendet das Skript, ebenso ein Knoten, der keine Instanzwurzel ist.\nSeit: 3.00.", + args = "(node, seconds)", + returns = "()", + }, + animationStop = { + type = "function", + description = "Stoppt und vergisst jeden Clip auf der Instanz oder nur den Clip auf einer Ebene über der Basis (1 bis 4); die Basisebene allein lässt sich nicht ohne die Ebenen darüber stoppen. Die Knoten behalten ihre letzte Pose, aus der ein folgendes animationPlay mit einem fade sanft herausführt. Nachdem alles gestoppt wurde, beenden animationResume und animationSetTime das Skript, bis wieder etwas gespielt wird. Gewichte und Masken der Ebenen bleiben erhalten. Eine Ebene außerhalb von 1 bis 4 oder ein Knoten, der keine Instanzwurzel ist, beendet das Skript.\nSeit: 3.00.", + args = "(node [, layer])", + returns = "()", + }, + bodyApplyForce = { + type = "function", + description = "Übt auf einen dynamischen Körper eine Kraft in Newton aus, nur für die Schritte dieses Frames; ruf es jeden Frame auf, um weiter zu schieben. Ohne Punkt wirkt die Kraft durch den Schwerpunkt und bewegt den Körper nur; mit einem Weltpunkt (px, py, pz) dreht sie den Körper außerdem um seinen Mittelpunkt. Eine Kraft beschleunigt um Kraft geteilt durch Masse, also bemiss sie nach bodySetMass. Statische und kinematische Körper ignorieren sie.\nSeit: 3.00.", + args = "(node, fx, fy, fz [, px, py, pz])", + returns = "()", + }, + bodyApplyImpulse = { + type = "function", + description = "Ändert den Impuls eines dynamischen Körpers auf einen Schlag: Ein Impuls von Masse mal Geschwindigkeit fügt diese Geschwindigkeit hinzu. Ohne Punkt wirkt er durch den Schwerpunkt; mit einem Weltpunkt (px, py, pz) dreht er den Körper außerdem, sodass ein Treffer nahe der Oberkante einer Kiste sie umkippt. Das ist der Aufruf für einen Treffer, einen Tritt, eine Explosion oder einen Abschuss, während bodyApplyForce für ein Schieben ist, das anhält. Statische und kinematische Körper ignorieren ihn.\nSeit: 3.00.", + args = "(node, ix, iy, iz [, px, py, pz])", + returns = "()", + }, + bodyDelete = { + type = "function", + description = "Entfernt den Körper aus der Welt; der Knoten und sein Mesh bleiben, wo sie sind. Jedes Gelenk, das am Körper hängt, und jedes darauf gebaute Fahrzeug verschwindet mit ihm. Den Knoten zu löschen entfernt auch seinen Körper, also wird dies nur gebraucht, um einen Knoten ohne seinen Körper zu behalten. Beendet das Skript, wenn der Knoten keinen Körper hat.\nSeit: 3.00.", + args = "(node)", + returns = "()", + }, + bodyGetAngularVelocity = { + type = "function", + description = "Wie schnell sich der Körper um jede Weltachse dreht, in Radiant pro Sekunde, beim letzten Schritt. Ein statischer Körper meldet null. Nützlich für die Tonhöhe eines Rads oder einen Drehzähler.\nRückgabe: Drei Zahlen, Radiant pro Sekunde um X, Y und Z.\nSeit: 3.00.", + args = "(node)", + returns = "(x, y, z)", + }, + bodyGetVelocity = { + type = "function", + description = "Wie schnell sich der Körper entlang jeder Weltachse bewegt, in Welteinheiten pro Sekunde, beim letzten Schritt. Ein statischer Körper meldet null; ein kinematischer Körper meldet die Geschwindigkeit, die sich aus der Bewegung seines Knotens ergab.\nRückgabe: Drei Zahlen, Einheiten pro Sekunde entlang X, Y und Z.\nSeit: 3.00.", + args = "(node)", + returns = "(x, y, z)", + }, + bodyIsResting = { + type = "function", + description = "Ob die Simulation den Körper schlafen gelegt hat, weil er sich nicht mehr bewegt. Statische Körper und mit bodySetEnabled aus der Welt genommene Körper gelten als ruhend. Ein ruhender Körper wacht von selbst auf, wenn ihn etwas berührt oder wenn eine Geschwindigkeit, eine Kraft oder ein Impuls angewendet wird.\nRückgabe: Boolean.\nSeit: 3.00.", + args = "(node)", + returns = "(boolean)", + }, + bodyNew = { + type = "function", + description = "Gibt dem Knoten einen Körper des angegebenen Typs, dort platziert, wo der Knoten gerade in der Welt ist (seine Eltern eingeschlossen), mit einer Kollisionsform, die durch a, b und c in Welteinheiten bemessen und mit der Weltskalierung des Knotens skaliert wird. Ein zweiter Aufruf auf demselben Knoten ersetzt den ersten, und ein Knoten, der eine Spielfigur hatte, verliert sie. Die Masse eines dynamischen Körpers ergibt sich aus dem Volumen seiner Form bei der Dichte von Wasser, also setze bodySetMass bei allem, was sich leicht anfühlen soll. Die Skalierung des Knotens später zu ändern, ändert die Größe des Körpers nicht. Beendet das Skript bei einem unbekannten Typ oder einer unbekannten Form, wenn auf diesem Rechner keine Physik verfügbar ist, wenn die Welt bereits 4096 Körper enthält oder wenn die Form nicht gebaut werden kann (ein dynamisches SHAPE_MESH oder eine Hülle ohne Mesh unter dem Knoten).\nSeit: 3.00.", + args = "(node, type, shape [, a, b, c])", + returns = "()", + }, + bodySetAngularVelocity = { + type = "function", + description = "Setzt die Drehung um jede Weltachse, in Radiant pro Sekunde, und weckt den Körper. Statische Körper ignorieren es. Die Drehung eines kinematischen Körpers wird beim nächsten Schritt von seinem Knoten überschrieben, also wirkt der Aufruf nur bei dynamischen Körpern fort.\nSeit: 3.00.", + args = "(node, x, y, z)", + returns = "()", + }, + bodySetBounce = { + type = "function", + description = "Setzt die Restitution des Körpers, von 0 (bleibt sofort liegen) bis 1 (prallt mit allem zurück, womit er ankam); Werte außerhalb dieses Bereichs werden begrenzt. Der Standard ist 0.1. Die Werte der beiden sich berührenden Körper werden kombiniert, sodass ein lebhafter Ball auf einem dämpfenden Boden trotzdem springt.\nSeit: 3.00.", + args = "(node, bounce)", + returns = "()", + }, + bodySetBuoyancy = { + type = "function", + description = "Wie der Körper schwimmt, wenn er in einem Wasservolumen ist: 1 ist neutral, mehr schwimmt höher, weniger sinkt, und 0 ist ein Stein. Der Standard ist 1.2, das sanft schwimmt. Der Faktor multipliziert die eigene Dichte des Wassers aus bodySetWater, und negative Werte werden auf 0 begrenzt. Nur dynamische Körper werden von Wasser bewegt, aber der Wert kann auf jedem Körper gesetzt werden.\nSeit: 3.00.", + args = "(node, factor)", + returns = "()", + }, + bodySetCurrent = { + type = "function", + description = "Setzt die Strömung in einem Wasservolumen, in Welteinheiten pro Sekunde entlang jeder Weltachse; dynamische Körper, die darin schwimmen, treiben mit der Strömung, und eine schwimmende Spielfigur wird von ihr mitgetragen. Der Standard ist keine Strömung. Setze sie auf dem Knoten, auf dem bodySetWater aufgerufen wurde; auf jedem anderen Körper wird sie gespeichert und bewirkt nichts.\nSeit: 3.00.", + args = "(node, x, y, z)", + returns = "()", + }, + bodySetEnabled = { + type = "function", + description = "Nimmt den Körper mit false aus der Welt: Er kollidiert nicht mehr, bewegt sich nicht mehr, treibt seinen Knoten weder an noch folgt er ihm, und er gilt als ruhend. true setzt ihn dort wieder ein, wo er ist, und weckt ihn. Der Körper behält Form, Masse und Einstellungen, also ist das billiger, als ihn für etwas, das kommt und geht, zu löschen und neu zu erzeugen.\nSeit: 3.00.", + args = "(node, enabled)", + returns = "()", + }, + bodySetFriction = { + type = "function", + description = "Setzt die Oberflächenreibung: 0 ist Eis, 0.5 (der Standard) ist gewöhnlich, höhere Werte greifen stärker. Negative Werte werden auf 0 begrenzt. Die Werte der beiden sich berührenden Körper werden kombiniert, sodass ein rutschiger Boden einen niedrigen Wert auf dem Boden braucht, nicht auf allem, was darauf läuft.\nSeit: 3.00.", + args = "(node, friction)", + returns = "()", + }, + bodySetMass = { + type = "function", + description = "Skaliert die Masse eines dynamischen Körpers und mit ihr seine Trägheit neu. Tu das bei allem, was sich leicht oder schwer anfühlen soll: Der Standard ist das Volumen der Form bei der Dichte von Wasser, sodass ein türgroßer Quader etwa 140 Kilogramm wiegt und ein Stups ihn nicht bewegt. Kräfte und Impulse skalieren mit der Masse, also setze die Masse, bevor du sie abstimmst. Beendet das Skript, wenn der Körper nicht dynamisch oder die Masse nicht positiv ist.\nSeit: 3.00.", + args = "(node, kilograms)", + returns = "()", + }, + bodySetTrigger = { + type = "function", + description = "Macht den Körper mit true zum Trigger: Er schiebt nichts und hält nichts auf, aber onTrigger meldet jeden Körper und jede Spielfigur, die ihn betreten und verlassen. false macht ihn wieder fest: Was in ihm war, wird ohne Meldung des Verlassens vergessen, während ein Trigger, in dem der Körper selbst steht, ihn weiter meldet. Ein Trigger ist meist ein statischer Quader oder eine statische Kugel über einer Tür, einer Ziellinie oder einer Gefahrenzone. Strahlen aus physicsRaycast treffen Trigger wie jeden anderen Körper.\nSeit: 3.00.", + args = "(node, trigger)", + returns = "()", + }, + bodySetVelocity = { + type = "function", + description = "Setzt die Geschwindigkeit des Körpers entlang jeder Weltachse, in Welteinheiten pro Sekunde, und weckt ihn. Das ersetzt jede Geschwindigkeit, die der Körper hatte, während bodyApplyImpulse etwas hinzufügt. Statische Körper ignorieren es, und die Geschwindigkeit eines kinematischen Körpers wird beim nächsten Schritt aus der Bewegung seines Knotens überschrieben.\nSeit: 3.00.", + args = "(node, x, y, z)", + returns = "()", + }, + bodySetWater = { + type = "function", + description = "Füllt einen statischen Körper mit Wasser und macht ihn zum Trigger: Die Oberseite seiner Form ist die Oberfläche, und jeder dynamische Körper darin schwimmt, sinkt oder rollt durch den Auftrieb gegen diese Oberfläche (der eingetauchte Teil der Form zählt, sodass eine schräge Kiste sich aufrichtet), gebremst durch die Widerstände. Eine Spielfigur, deren Mitte unter der Oberfläche liegt, schwimmt. Weil das Volumen ein Trigger ist, feuert onTrigger beim Hinein- und Hinausgehen. Das Wasser wird nicht gezeichnet: Gib dem Knoten ein Material mit Blending oder keines. Beendet das Skript, wenn der Körper nicht statisch ist.\nSeit: 3.00.", + args = "(node, density [, linearDrag, angularDrag])", + returns = "()", + }, + cameraSet = { + type = "function", + description = "cameraSet(node)\ncameraSet(-1)\nMacht einen Knoten zur Kamera: Die Szene wird von seiner Weltposition aus mit Blick entlang seiner eigenen -Z-Richtung gezeichnet, sodass Bewegung und Drehung seiner Eltern zählen. Jeder Knoten geht, auch einer, der zugleich ein Mesh oder ein Licht trägt, und die Kamera zu wechseln ist ein einziger Aufruf. -1 stellt die Standardansicht von (0, 0, 5) mit Blick auf den Ursprung wieder her. Wird der Kameraknoten gelöscht, fällt die Ansicht ebenfalls auf die Standardansicht zurück. Beendet das Skript bei einem ungültigen Knoten-Handle.\nSeit: 3.00.", + args = "(node)", + returns = "()", + }, + cameraSetOrthographic = { + type = "function", + description = "Schaltet auf eine Parallelprojektion um, die height Welteinheiten von oben bis unten zeigt, wobei die Breite dem Seitenverhältnis der Ebene folgt, von near bis far vor der Kamera. Entfernung lässt Dinge nicht schrumpfen, was zu Diagrammen, isometrischen und seitlichen Ansichten und HUD-artigen Szenen passt. Die Einstellung gilt für den aktuellen und jeden späteren Kameraknoten, bis cameraSetPerspective aufgerufen wird. Der Schatten eines gerichteten Lichts benutzt unter einer orthografischen Kamera eine einzige Shadow Map, egal, was sceneSetShadowCascades sagt.\nSeit: 3.00.", + args = "(height, near, far)", + returns = "()", + }, + cameraSetPerspective = { + type = "function", + description = "Schaltet auf die übliche perspektivische Projektion um, den Standard. fov ist das vertikale Sichtfeld in Grad (Standard 60); near und far (Standards 0.1 und 1000) begrenzen, was gezeichnet wird, in Welteinheiten vor der Kamera. Die Tiefengenauigkeit verteilt sich zwischen beiden, also halte near so groß, wie die Szene es erlaubt, und far nicht größer als nötig: 0.05 bis 100 für einen Raum, 0.1 bis 400 für eine Landschaft. Die Einstellung gilt für den aktuellen und jeden späteren Kameraknoten und für gerenderte Ansichten.\nSeit: 3.00.", + args = "(fov, near, far)", + returns = "()", + }, + collideCircles = { + type = "function", + description = "Ob sich zwei Kreise überschneiden. Billiger, als es aussieht: Für eine Ja-oder-Nein-Frage wird keine Quadratwurzel gezogen.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(x1, y1, radius1, x2, y2, radius2)", + returns = "(touching)", + }, + collidePointCircle = { + type = "function", + description = "Ob ein Punkt innerhalb eines Kreises liegt. Der Rand zählt als innen.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(pointX, pointY, x, y, radius)", + returns = "(inside)", + }, + collidePointPolygon = { + type = "function", + description = "Ob ein Punkt innerhalb eines Polygons beliebiger Form liegt, konkave eingeschlossen. Die Tabelle ist eine flache Liste aus x, y, x, y -- die Form, die eine Hitbox in den meisten Spielen ohnehin hat -- und der Umlaufsinn spielt keine Rolle. Weniger als drei Ecken enthalten nichts. Höchstens 256 Ecken.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(pointX, pointY, points: table)", + returns = "(inside)", + }, + collidePointRect = { + type = "function", + description = "Ob ein Punkt innerhalb eines Rechtecks liegt. Der Rand zählt als innen.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(pointX, pointY, x, y, width, height)", + returns = "(inside)", + }, + collideRectCircle = { + type = "function", + description = "Ob sich ein Rechteck und ein Kreis überschneiden; dazu wird der Punkt des Rechtecks gesucht, der dem Mittelpunkt des Kreises am nächsten liegt.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(x, y, width, height, circleX, circleY, radius)", + returns = "(touching)", + }, + collideRects = { + type = "function", + description = "Ob sich zwei Rechtecke überschneiden. Die Funktion, zu der du am häufigsten greifen wirst.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(x1, y1, width1, height1, x2, y2, width2, height2)", + returns = "(touching)", + }, + collideSegments = { + type = "function", + description = "Ob sich zwei Strecken kreuzen: ein Schuss entlang einer Bahn gegen eine Wand oder der Schritt, den ein bewegtes Objekt machen will, gegen die Kante, die es nicht überschreiten darf. Strecken, die aufeinander liegen, ergeben false, weil „wo kreuzen sie sich“ dann keine eindeutige Antwort hat.\nRückgabe: true oder false.\nSeit: 3.00.", + args = "(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)", + returns = "(crossing)", + }, + colorBackground = { + type = "function", + description = "Setzt die Hintergrundfarbe: womit overlayClear das Overlay füllt und was Text in FONT_QUALITY_SHADED aus fontPrint und fontToSprite hinter seine Glyphen malt. Beim Start ist sie transparentes Schwarz, und Alpha ist ohne Angabe 0, sodass colorBackground(0, 0, 0) gefolgt von overlayClear() das Overlay wieder unsichtbar macht. Ein teilweises Alpha über Schwarz dunkelt das Video ab; 255 verdeckt es. Kanalwerte außerhalb des Bereichs werden begrenzt statt abgewiesen.\nSeit: 1.x", + args = "(r, g, b [, a])", + returns = "()", + }, + colorForeground = { + type = "function", + description = "Setzt die Vordergrundfarbe, die overlayBox, overlayCircle, overlayEllipse, overlayLine und overlayPlot verwenden, und die Textfarbe für fontPrint und fontToSprite. Beim Start ist sie deckendes Weiß, und Alpha ist ohne Angabe 255, das Gegenteil von colorBackground. Sprites und overlayPrint werden mit ihren eigenen Pixeln gezeichnet und nicht eingefärbt. Kanalwerte außerhalb des Bereichs werden begrenzt statt abgewiesen.\nSeit: 1.x", + args = "(r, g, b [, a])", + returns = "()", + }, + controllerDoRumble = { + type = "function", + description = "controllerDoRumble(strength, duration)\ncontrollerDoRumble(controller, strength, duration)\nHypseus-Erweiterung. Lässt einen Controller mit beiden Motoren in einer von vier Stärken für eine von vier Dauern vibrieren. Die Form mit zwei Argumenten spricht Controller 0 an. Ein Slot ohne Controller tut nichts (Hypseus bricht dort ab, deshalb sichern sich seine Spiele mit controllerIsValid ab); eine Stärke oder Dauer außerhalb von 1 bis 4 oder ein Index außerhalb von 0 bis 3 bricht das Skript ab. Ein Controller ohne Vibrationsunterstützung ignoriert den Aufruf.\nSeit: 3.00, als Erweiterung von Hypseus Singe.", + args = "(strength, duration)", + returns = "()", + }, + controllerGetAxis = { + type = "function", + description = "Gibt die Stellung einer analogen Achse zurück, wie die Engine sie zuletzt gesehen hat. Der Wert wird bei jedem Achsenereignis aktualisiert, auch bei Bewegung innerhalb der Totzone, und ist 0 für einen Controller, der nicht verbunden ist, oder eine Achse, die sich nie bewegt hat. Sticks reichen von -32768 bis 32767; Trigger ruhen bei 0 und erreichen voll gedrückt 32767. Ein Index außerhalb von 0 bis 3 oder eine Achse außerhalb von 0 bis 5 bricht das Skript ab.\nRückgabe: Ganzzahl, -32768 bis 32767.\nSeit: 2.00", + args = "(controller, axis)", + returns = "(integer)", + }, + controllerGetButton = { + type = "function", + description = "Gibt zurück, ob eine Taste gerade gehalten wird, direkt vom Controller gelesen statt aus der Ereigniswarteschlange. Verwende es zum Aufladen durch Gedrückthalten und für ähnliche Fälle, in denen onInputPressed und onInputReleased umständlich sind. Ein Slot ohne Controller ergibt false. Der Tastencode muss aus der Tabelle GAMEPAD_N desselben Controller-Index stammen; ein Code aus der Tabelle eines anderen Controllers oder ein Achsencode fällt aus dem Tastenbereich und bricht das Skript ab, ebenso ein Index außerhalb von 0 bis 3.\nRückgabe: Boolean.\nSeit: 2.10", + args = "(controller, button)", + returns = "(boolean)", + }, + controllerGetName = { + type = "function", + description = "Der Name des Geräts in einem Controller-Slot, wie SDL ihn meldet, oder nil, wenn der Slot leer ist. Ein Gerät, das SDL nicht erkannt hat, meldet seinen eigenen Namen über das Mapping, das Singe dafür geschrieben hat, sodass ein Service-Bildschirm einen Arcade-Encoder ebenso leicht mit Namen auflisten kann wie ein Markenpad. Ein Index außerhalb von 0 bis 3 bricht das Skript ab.\nRückgabe: String oder nil.\nSeit: 3.00", + args = "(controller)", + returns = "(string)", + }, + controllerGetPadding = { + type = "function", + description = "Hypseus-Erweiterung, in Singe immer 0. Hypseus addiert diesen Offset zu einem Schaltercode, der von einem Gamepad kam, damit ein Spiel erkennen kann, welches Gerät was gedrückt hat, und den Schalter trotzdem durch Abziehen zurückgewinnt. Singe liefert Gamepad-Eingaben als gewöhnliche belegte Schalter ohne eine solche Markierung, deshalb antwortet es mit 0: ein Spiel, das auf den verschobenen Bereich prüft, nimmt seinen anderen Zweig und liest den Schalter so, wie er ist.\nRückgabe: eine Ganzzahl, immer 0.\nSeit: 3.00", + args = "()", + returns = "(padding)", + }, + controllerHowMany = { + type = "function", + description = "Hypseus-Erweiterung. Wie viele Gamepads geöffnet sind, von den vier Slots, die Singe hat. Da jeder Joystick, den Singe öffnen kann, zum Gamepad wird (siehe Jedes Gerät ist ein Gamepad), werden hier auch ein Arcade-Encoder oder ein Spinner mitgezählt.\nRückgabe: Zahl, 0 bis 4.\nSeit: 3.00, als Erweiterung von Hypseus Singe.", + args = "()", + returns = "(number)", + }, + controllerIsValid = { + type = "function", + description = "Hypseus-Erweiterung. Gibt zurück, ob gerade ein Controller im Slot geöffnet ist; Controller kommen und gehen beim Ein- und Ausstecken im laufenden Betrieb, frag also vor jedem controllerDoRumble. Ein Index außerhalb von 0 bis 3 bricht das Skript ab.\nRückgabe: Boolean.\nSeit: 3.00, als Erweiterung von Hypseus Singe.", + args = "(controller)", + returns = "(boolean)", + }, + controllerSetPadding = { + type = "function", + description = "Hypseus-Erweiterung, angenommen und ignoriert. Warum es den Offset, den sie verlangt, in Singe nicht gibt, steht bei controllerGetPadding. Ihn einzuschalten, ohne die Ereignisse zu markieren, wäre schlimmer, als ihn ausgeschaltet zu lassen, weil das Spiel dann einen Offset von Schaltercodes abziehen würde, die nie einen trugen.\nRückgabe: nichts.\nSeit: 3.00", + args = "(boolean)", + returns = "()", + }, + daphneGetHeight = { + type = "function", + description = "Aliase in Framework.singe Bevorzuge die singe*-Entsprechungen.", + args = "()", + returns = "()", + anchor = "unimplementedandlegacy", + }, + daphneGetWidth = { + type = "function", + description = "Aliase in Framework.singe Bevorzuge die singe*-Entsprechungen.", + args = "()", + returns = "()", + anchor = "unimplementedandlegacy", + }, + daphneScreenshot = { + type = "function", + description = "Aliase in Framework.singe Bevorzuge die singe*-Entsprechungen.", + args = "()", + returns = "()", + anchor = "unimplementedandlegacy", + }, + debugPrint = { + type = "function", + description = "Gibt eine Zeile auf der Konsole (Standardausgabe) aus. Wurde Singe mit --noconsole gestartet, wird nichts ausgegeben; mit --trace landen der Aufruf und seine Nachricht außerdem in trace.txt. Das Argument muss ein String oder eine Zahl sein; alles andere bricht das Skript ab, verpacke Werte also in tostring oder verwende string.format. Der Aufruf ist billig genug, um im ausgelieferten Code zu bleiben, wird aber nicht gedrosselt, eine Ausgabe in onOverlayUpdate läuft also in jedem Frame.\nSeit: 1.x", + args = "(message)", + returns = "()", + }, + discAudio = { + type = "function", + description = "Schaltet einen Kanal der Tonspur der Disc ein oder aus. Der Kanal wird auf die eingestellte Laserdisc-Lautstärke (--volume_vldp, Standard 100) gesetzt, wenn enabled true ist, und auf 0, wenn es false ist; der andere Kanal bleibt, wie er ist. Viele Originaltitel hatten Erzählung oder eine Datenspur nur auf einem Kanal, und mit diesem Aufruf bringst du sie zum Schweigen. Ein anderer Kanal als 1 oder 2 beendet das Skript. Ohne Disc tut der Aufruf nichts.\nSeit: 1.x", + args = "(channel, enabled)", + returns = "()", + }, + discAudioSuffix = { + type = "function", + description = "Hypseus-Erweiterung. Schaltet die Disc auf eine eigene Audiodatei neben ihrem Video um: .ogg, wobei die Basis der Name der Videodatei ohne Endung ist, sodass movie.m2v mit \"-es\" die Datei movie-es.ogg abspielt. Die Datei wird beim aktuellen Frame geöffnet, und die Wiedergabe richtet sich neu aus, wie bei discSetAudioTrack; Lautstärke, Wiedergabezustand und Spurindex bleiben erhalten, sofern die neue Datei so viele Spuren hat. Mit einer Frame-Datei schaltet jedes Segment um. Ein leeres Suffix stellt das Standardaudio wieder her: die .ogg neben einer .m2v oder den Ton in jedem anderen Container. Gibt true zurück, wenn die Datei geöffnet wurde; fehlt sie, läuft das bisherige Audio weiter, nichts wird gemerkt, und false kommt zurück (Hypseus hinterlässt in diesem Fall Stille). Das Suffix wird vergessen, wenn das Skript neu geladen wird. Ohne Disc gibt der Aufruf false zurück. Verwende ihn für ein Sprachmenü, dessen Spuren als eigene Dateien mitgeliefert werden; Spuren innerhalb einer Datei sind Aufgabe von discSetAudioTrack.\nRückgabe: Boolean.\nSeit: 3.00, als Erweiterung von Hypseus Singe.", + args = "(suffix: string)", + returns = "(boolean)", + }, + discChangeSpeed = { + type = "function", + description = "Tut nichts. Echte Laserdisc-Player konnten mit einer anderen Geschwindigkeit als 1x laufen; der Software-Player von Singe kann das nicht, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück. Er bleibt erhalten, damit Skripte von vor 2.00 ohne Änderungen laden.\nSeit: 1.x", + args = "(...)", + returns = "()", + }, + discGetAudioTrack = { + type = "function", + description = "Gibt den nullbasierten Index der Tonspur zurück, die die Disc gerade abspielt. Beim Start wählt die Engine die durch --audio oder den Eintrag AUDIO_TRACK in games.dat genannte Spur (Standard 0), sofern die Disc so viele Spuren hat. Gibt ohne Disc 0 zurück.\nRückgabe: ganzzahliger Spurindex.\nSeit: 2.10", + args = "()", + returns = "(integer)", + }, + discGetAudioTracks = { + type = "function", + description = "Gibt zurück, wie viele Audiostreams die Videodatei der Disc enthält. Mehrere Streams sind typischerweise alternative Synchronfassungen. Ein stummes Video meldet 0, ebenso ein Spiel ohne Disc, sichere Schleifen und Modulo-Rechnungen also gegen null ab.\nRückgabe: ganzzahlige Anzahl, 0 oder mehr.\nSeit: 2.10", + args = "()", + returns = "(integer)", + }, + discGetFrame = { + type = "function", + description = "Gibt den Frame zurück, den die Disc gerade zeigt. Mit einer Frame-Datei ist die Nummer die Position in der zusammengesetzten Folge, Segmentgrenzen sind für das Skript also unsichtbar. Gibt 0 zurück, solange die Disc gestoppt ist, und ohne Disc. Frag ihn in onOverlayUpdate ab, um darauf zu reagieren, dass das Video einen Entscheidungspunkt erreicht.\nRückgabe: ganzzahlige Frame-Nummer.\nSeit: 1.x", + args = "()", + returns = "(integer)", + }, + discGetFrameCount = { + type = "function", + description = "Die Anzahl der Frames auf der Disc, also eins mehr als der höchste Frame, den discSearch findet. Eine Frame-Datei antwortet für die ganze Disc statt für das laufende Segment, die Anzahl ändert sich also nicht, wenn Segmente kommen und gehen. Ohne Disc antwortet sie mit 0.\nRückgabe: ganzzahlige Anzahl der Frames, 0 ohne Disc.\nSeit: 3.00.", + args = "()", + returns = "(integer)", + }, + discGetHeight = { + type = "function", + description = "Gibt die Höhe des Disc-Videos in Pixeln zurück. Ohne Disc gibt es die Höhe der Zeichenfläche zurück (CANVAS_Y in games.dat oder --canvas, Standard 480), es ist also immer die Größe des Hintergrunds, über den das Overlay gestreckt wird. Kombiniere es mit discGetWidth, um dem Overlay dieselbe Auflösung wie dem Video zu geben.\nRückgabe: ganzzahlige Höhe in Pixeln.\nSeit: 2.00", + args = "()", + returns = "(integer)", + }, + discGetLanguage = { + type = "function", + description = "Gibt das in der Videodatei gespeicherte Sprach-Tag für eine der Tonspuren der Disc zurück. Das Tag ist ein dreibuchstabiger ISO-639-Code wie \"eng\" oder \"jpn\". Spuren ohne dreibuchstabiges Tag ergeben \"unk\", ebenso jede Spur, wenn es keine Disc gibt. Mit Disc beendet eine Spur außerhalb von 0 bis discGetAudioTracks() - 1 das Skript.\nRückgabe: String mit dem Sprachcode.\nSeit: 2.10", + args = "(track)", + returns = "(string)", + }, + discGetLanguageDescription = { + type = "function", + description = "= videoGetLanguageDescription\nSchlägt den englischen Namen eines Sprachcodes nach. Anders als die übrigen video*-Funktionen nimmt sie kein Handle; sie ist ein reines Nachschlagen in einer Tabelle, das zweibuchstabige Codes nach ISO 639-1 und dreibuchstabige Codes nach ISO 639-2 (sowohl die T- als auch die B-Form) annimmt, verglichen ohne Rücksicht auf Groß- und Kleinschreibung. Unbekannte Codes, auch das \"unk\", das für Spuren ohne Tag zurückkommt, ergeben \"Unknown\". Framework.singe stellt sie zur Symmetrie mit der Disc-API auch als discGetLanguageDescription bereit.\nRückgabe: String mit dem Namen der Sprache oder \"Unknown\".\nSeit: 2.10", + args = "(code)", + returns = "(string)", + anchor = "videogetlanguagedescription", + }, + discGetState = { + type = "function", + description = "Gibt den Transportzustand der Disc als eine der Konstanten DISC_* zurück. Ein Spiel ohne Disc meldet DISC_EJECTED; nach discStop meldet die Disc DISC_STOPPED bis zum nächsten discPlay, discSearch oder discSkipToFrame; ansonsten ist der Zustand DISC_PLAYING, solange das Video vorrückt, und DISC_PAUSED, wenn nicht. Einen eigenen Suchzustand gibt es nicht; eine Disc, die gerade springt, meldet DISC_PAUSED oder DISC_PLAYING, je nachdem, was angefordert wurde.\nRückgabe: eine der folgenden Konstanten, die die Engine definiert und die jedem Skript zur Verfügung stehen.\nSeit: 1.x (RDG)", + args = "()", + returns = "(state)", + }, + discGetSubtitleLanguage = { + type = "function", + description = "Die Sprache, mit der eine Untertitelspur im eigenen Container der Disc gekennzeichnet ist, als der ISO-639-Code, den die Datei trägt (eng, fra, spa). Eine Spur, die die Datei mit nichts kennzeichnet, ergibt einen leeren String, und das ist kein Fehler -- viele Dateien kennzeichnen gar nichts.\nRückgabe: ein String.\nSeit: 3.00.", + args = "(track: number)", + returns = "(language)", + }, + discGetSubtitleTracks = { + type = "function", + description = "Wie viele Untertitelspuren der eigene Container der Disc enthält. 0 ohne Disc und 0 für eine Disc, deren Untertitel als eigene .srt mitgeliefert werden -- was die übliche Anordnung ist und wofür srtLoad da ist.\nRückgabe: eine Zahl.\nSeit: 3.00.", + args = "()", + returns = "(count)", + }, + discGetWidth = { + type = "function", + description = "Gibt die Breite des Disc-Videos in Pixeln zurück. Ohne Disc gibt es die Breite der Zeichenfläche zurück (CANVAS_X in games.dat oder --canvas, Standard 720). Zusammen mit discGetHeight ist das die Größe, auf die das Overlay skaliert wird, weshalb das mitgelieferte Menü beide an overlaySetResolution übergibt.\nRückgabe: ganzzahlige Breite in Pixeln.\nSeit: 2.00", + args = "()", + returns = "(integer)", + }, + discPause = { + type = "function", + description = "Hält die Wiedergabe beim aktuellen Frame an, der auf dem Bildschirm bleibt. Der Ton der Disc pausiert mit. Der Aufruf wird ignoriert, solange die Disc gestoppt ist, und tut ohne Disc nichts. Fortsetzen mit discPlay.\nSeit: 1.x", + args = "()", + returns = "()", + }, + discPauseAtFrame = { + type = "function", + description = "Springt zu frame, zeigt ihn und pausiert. Das ist dieselbe Bindung wie discSearch unter einem zweiten Namen; die beiden sind in jeder Hinsicht austauschbar, einschließlich der dort beschriebenen Regeln für die Begrenzung. Bevorzuge discSearch in neuem Code und behalte discPauseAtFrame nur beim Portieren alter Skripte.\nSeit: 1.18", + args = "(frame)", + returns = "()", + }, + discPlay = { + type = "function", + description = "Startet die Wiedergabe beim aktuellen Frame oder setzt sie fort und hebt den von discStop gesetzten Stoppzustand auf. Der Ton läuft mit dem Video weiter, und die Wiedergabeuhr startet beim aktuellen Frame neu, damit eine lange Pause keinen Sprung verursacht. Tut ohne Disc nichts, außer das Stopp-Flag zu löschen.\nSeit: 1.x", + args = "()", + returns = "()", + }, + discSearch = { + type = "function", + description = "Springt mit der Disc zu frame, zeigt diesen Frame und pausiert darauf; war die Disc gestoppt, wird der Stoppzustand aufgehoben. Die Disc hält den Frame bis zu discPlay, discSkipToFrame oder einem anderen Transportaufruf. Ein negativer Frame wird auf den ersten Frame begrenzt und einer hinter dem Ende auf den letzten; mit einer Frame-Datei wird das Segment gewählt, das den Frame enthält, und die Position innerhalb davon begrenzt. Tut ohne Disc nichts.\nSeit: 1.x", + args = "(frame)", + returns = "()", + }, + discSearchBlanking = { + type = "function", + description = "Tut nichts. Auf Laserdisc-Hardware wählte das, ob der Bildschirm dunkel wurde, während der Player einen Frame suchte; der Software-Player von Singe springt, ohne dunkel zu werden, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück.\nSeit: 1.x", + args = "(...)", + returns = "()", + }, + discSetAudioTrack = { + type = "function", + description = "Schaltet die Disc auf eine andere Tonspur um. Bereits eingereihtes Audio der alten Spur wird verworfen, und die Wiedergabe richtet sich neu aus, die Änderung ist also fast sofort zu hören. Mit Disc beendet eine Spur außerhalb von 0 bis discGetAudioTracks() - 1 das Skript; ohne Disc tut der Aufruf nichts. Verwende ihn, um ein Sprachmenü umzusetzen oder eine zwischen Sitzungen gespeicherte Vorliebe zu beachten.\nSeit: 2.10", + args = "(track)", + returns = "()", + }, + discSetFPS = { + type = "function", + description = "Tut nichts. Die Bildrate stammt aus der Videodatei, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück. Nichts im mitgelieferten Framework.singe ruft ihn auf; das tun nur Skripte von vor 2.00.\nSeit: 1.x", + args = "(...)", + returns = "()", + }, + discSkipBackward = { + type = "function", + description = "Springt um frames vom aktuellen Frame zurück, ohne den Wiedergabe- oder Pausenzustand zu ändern: eine laufende Disc spielt von der neuen Position aus weiter, eine pausierte bleibt dort pausiert. Der Aufruf wird ignoriert, solange die Disc gestoppt ist, und ohne Disc. Die Differenz wird nicht geprüft; ein negativer Wert springt vorwärts, und eine Differenz, die größer als der aktuelle Frame ist, wird bei Frame 0 begrenzt. Mit einer Frame-Datei gilt die Differenz für die Frame-Nummer, die discGetFrame meldet, ein Sprung kann also in ein früheres Segment hinüberreichen.\nSeit: 1.x", + args = "(frames: number)", + returns = "()", + }, + discSkipBlanking = { + type = "function", + description = "Tut nichts. Früher wählte es auf Laserdisc-Hardware, ob der Bildschirm bei einem Sprung dunkel wurde; der Software-Player von Singe wird nie dunkel, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück.\nSeit: 1.x", + args = "(...)", + returns = "()", + }, + discSkipForward = { + type = "function", + description = "Springt um frames vom aktuellen Frame vorwärts, ohne den Wiedergabe- oder Pausenzustand zu ändern. Das Spiegelbild von discSkipBackward: ignoriert, solange gestoppt, und ohne Disc, die Differenz wird nicht geprüft, und ein Ziel hinter dem letzten Frame wird beim letzten Frame begrenzt. Mit einer Frame-Datei gilt die Differenz für die Frame-Nummer, die discGetFrame meldet, ein Sprung kann also in ein späteres Segment hinüberreichen.\nSeit: 1.x", + args = "(frames: number)", + returns = "()", + }, + discSkipToFrame = { + type = "function", + description = "Springt zu frame und spielt von dort ab, egal in welchem Zustand die Disc ist; wenn nötig, wird der Stoppzustand aufgehoben. Im Gegensatz dazu springt discSearch und pausiert. Ein Frame vor dem Anfang oder hinter dem Ende wird auf den ersten oder letzten Frame begrenzt; mit einer Frame-Datei wird das Segment gewählt, das den Frame enthält, und die Position innerhalb davon begrenzt. Tut ohne Disc nichts, außer das Stopp-Flag zu löschen.\nSeit: 1.x", + args = "(frame)", + returns = "()", + }, + discStepBackward = { + type = "function", + description = "Bewegt die Disc genau einen Frame zurück und pausiert darauf, egal in welchem Wiedergabezustand sie vorher war. Ein Schritt bei Frame 0 bleibt auf Frame 0. Wird wie discSkipBackward ignoriert, solange die Disc gestoppt ist. Gedacht für framegenaues Debugging und Service-Bildschirme. Tut ohne Disc nichts.\nSeit: 1.x", + args = "()", + returns = "()", + }, + discStepForward = { + type = "function", + description = "Bewegt die Disc genau einen Frame vorwärts und pausiert darauf, egal in welchem Wiedergabezustand sie vorher war. Ein Schritt auf dem letzten Frame bleibt auf dem letzten Frame. Wie discStepBackward wird der Aufruf ignoriert, solange die Disc gestoppt ist. Tut ohne Disc nichts.\nSeit: 1.x", + args = "()", + returns = "()", + }, + discStop = { + type = "function", + description = "Stoppt die Disc. Die Wiedergabe hält an, das Video wird durch den klassischen blauen Bildschirm ersetzt, discGetState meldet DISC_STOPPED, und discGetFrame gibt 0 zurück, bis die Disc wieder gestartet wird. Während sie gestoppt ist, werden discPause, discSkipForward, discSkipBackward, discStepForward und discStepBackward ignoriert. Jeder der Aufrufe discPlay, discSearch oder discSkipToFrame hebt den Stoppzustand auf. Ein zweites discStop wird ignoriert.\nSeit: 1.x", + args = "()", + returns = "()", + }, + drawTransparent = { + type = "function", + description = "Hypseus-Erweiterung. Entscheidet, ob Farbe 0 auf den danach geladenen Sprites und dem danach gerenderten Text transparent gezeichnet wird. false, der Standard, stanzt Farbe 0 aus, wie Hypseus es tut: Palettenindex 0 oder reines Schwarz, und das schließt deckendes Schwarz in einem Bild mit Alphakanal ein, sodass schwarze Schrift oder ein schwarzer Umriss verschwindet. true zeichnet sie allein nach ihrem Alphakanal, sodass deckendes Schwarz schwarz bleibt. Bereits geladene Sprites behalten die Einstellung, mit der sie geladen wurden, und jedes Skript beginnt bei false.\nRückgabe: nichts.\nSeit: 3.00", + args = "(boolean)", + returns = "()", + }, + emitterBurst = { + type = "function", + description = "Erzeugt count Partikel auf einmal nach dem aktuellen Rezept, mit oder ohne Strom: eine Explosion, ein Spritzer, ein Wölkchen. Es werden nur so viele erzeugt, wie im Pool Platz haben; emitterSetMax legt den Platz fest. Ein 2D-Emitter stößt sie an seinem Punkt von emitterSetPosition aus, ein 3D-Emitter an seinem Knoten, verteilt über emitterSetRadius.\nSeit: 3.00.", + args = "(emitter, count)", + returns = "()", + }, + emitterClear = { + type = "function", + description = "Tötet jetzt jedes lebende Partikel und vergisst jeden noch geschuldeten Bruchteil eines Partikels. Der Strom wird nicht gestoppt: ein gestarteter Emitter erzeugt im nächsten Frame weiter neue Partikel, ruf also auch emitterStop auf, um ihn endgültig zu leeren. Verwende es, wenn eine Szene wechselt und die alten Funken nicht nachglimmen dürfen.\nSeit: 3.00.", + args = "(emitter)", + returns = "()", + }, + emitterDelete = { + type = "function", + description = "Entfernt den Emitter, seine lebenden Partikel und seine Kopie der Textur sofort; nichts blendet aus. Verwende stattdessen emitterStop, um die letzten Partikel zu Ende leben zu lassen. Ein 3D-Emitter wird auch von selbst entfernt, wenn sein Knoten gelöscht wird. Ein Handle, das kein lebender Emitter ist, beendet das Skript, leere die Variable also danach.\nSeit: 3.00.", + args = "(emitter)", + returns = "()", + }, + emitterDraw = { + type = "function", + description = "Nur 2D, aus onOverlayUpdate: stellt die Partikel des Emitters in die Warteschlange, damit sie in diesem Frame gezeichnet werden, über oder unter dem Overlay je nach emitterSetLayer, in Overlay-Koordinaten, die auf das Fenster abgebildet werden. Emitter auf derselben Ebene zeichnen in der Reihenfolge, in der sie eingereiht wurden. Die Warteschlange leert sich in jedem Frame, der Aufruf gehört also in jeden Frame, in dem die Partikel zu sehen sein sollen; einen Emitter nicht zu zeichnen, hält ihn nicht an. Bis zu 64 Emitter pro Ebene werden in einem Frame gezeichnet. Ein 3D-Emitter zeichnet sich selbst in der Szene und beendet hier das Skript.\nSeit: 3.00.", + args = "(emitter)", + returns = "()", + }, + emitterGetCount = { + type = "function", + description = "Wie viele Partikel des Emitters in diesem Frame leben, 0 bis zur Größe des Pools. Nützlich, um einen Pool abzustimmen, für ein Budget auf dem Bildschirm oder um zu wissen, wann ein Stoß verklungen ist.\nRückgabe: Die Anzahl der lebenden Partikel, eine Ganzzahl.\nSeit: 3.00.", + args = "(emitter)", + returns = "(count)", + }, + emitterIsActive = { + type = "function", + description = "Ob der Emitter strömt oder noch lebende Partikel hat: true ab emitterStart oder emitterBurst, bis der Strom gestoppt und das letzte Partikel gestorben ist, false, sobald die Explosion vorbei ist. Ein strömender Emitter, dessen Pool gerade leer ist, ist trotzdem aktiv.\nRückgabe: Boolean.\nSeit: 3.00.", + args = "(emitter)", + returns = "(boolean)", + }, + emitterNew = { + type = "function", + description = "Erzeugt einen Emitter. Ohne Argument ist es ein 2D-Emitter an der Overlay-Position (0, 0), der mit emitterSetPosition bewegt wird; mit einem Knoten ist es ein 3D-Emitter, dessen Partikel an der Weltposition dieses Knotens geboren werden, und er braucht die 3D-Szene, sodass der Aufruf auf einem Rechner ohne sie das Skript beendet (2D-Emitter funktionieren überall). Ein neuer Emitter strömt noch nicht; ruf emitterStart oder emitterBurst auf. Wird der Knoten gelöscht, werden seine Emitter mitgelöscht. Standardwerte: 50 Partikel pro Sekunde, die ein bis zwei Sekunden leben, 30 Grad Streuung, Weiß, das zu transparent verblasst, normales Blending, keine Schwerkraft, keine Dämpfung, keine Drehung, kein Radius, keine Textur, kein Schweif und keine Kollision, ein Pool von 1000; ein 2D-Emitter schickt sie mit 50 bis 100 Overlay-Einheiten pro Sekunde den Bildschirm hinauf und lässt sie von 16 auf 4 schrumpfen, ein 3D-Emitter mit 1 bis 2 Welteinheiten pro Sekunde die Y-Achse hinauf, schrumpfend von 0,2 auf 0,05.\nRückgabe: Das Handle des Emitters.\nSeit: 3.00.", + args = "([node])", + returns = "(emitter)", + }, + emitterSetBlend = { + type = "function", + description = "Wie sich Partikel mit dem verbinden, was hinter ihnen liegt. PARTICLE_ALPHA (Standard) blendet normal, für Rauch, Staub, Regen und Trümmer; PARTICLE_ADD addiert Licht, für Feuer, Funken, Leuchten und Magie, und dunkelt nie ab. Wirkt sofort auf die lebenden Partikel. Alles andere als diese beiden Konstanten beendet das Skript.\nSeit: 3.00.", + args = "(emitter, mode)", + returns = "()", + }, + emitterSetCollide = { + type = "function", + description = "Wovon Partikel abprallen. COLLIDE_NONE (Standard) ist nichts; COLLIDE_FLOOR ist eine waagerechte Ebene in der Höhe floor, unterhalb derer (größeres Y in 2D, kleineres Y in 3D) ein Partikel auf die Ebene zurückgesetzt und reflektiert wird; COLLIDE_SCENE ist die Physikwelt, nur für 3D-Emitter, mit einem Raycast pro Partikel entlang seiner Bewegung in jedem Frame, begrenzt auf 200 Raycasts pro Emitter und Frame und dort fortgesetzt, wo sie aufgehört haben, sodass ein großer Emitter die Kosten über ein paar Frames verteilt. Bei einem 2D-Emitter wird COLLIDE_SCENE angenommen und tut nichts. Ein anderer Modus als diese drei beendet das Skript.\nSeit: 3.00.", + args = "(emitter, mode [, bounce, friction, floor])", + returns = "()", + }, + emitterSetColor = { + type = "function", + description = "Farbe und Deckkraft bei der Geburt (r, g, b, a) und beim Tod (r2, g2, b2, a2), jeweils 0 bis 255, gleichmäßig über die Lebensdauer des Partikels wechselnd; Werte außerhalb des Bereichs werden begrenzt. Standard: von Weiß und deckend zu Weiß und transparent. Die Tönung multipliziert die Textur oder die eingebaute Scheibe, ein weißes Bild nimmt die Farbe also genau an. Ein Ein- und Ausblenden braucht zwei Emitter oder eine Textur, in der die Form steckt; ein Rezept fährt eine einzige gerade Rampe.\nSeit: 3.00.", + args = "(emitter, r, g, b, a, r2, g2, b2, a2)", + returns = "()", + }, + emitterSetDirection = { + type = "function", + description = "Die mittlere Richtung, in die neue Partikel aufbrechen; nur die Richtung zählt, nicht die Länge. 2D-Standard (0, -1), den Bildschirm hinauf, da Y im Overlay nach unten wächst; 3D-Standard (0, 1, 0), die Y-Achse der Welt hinauf. Ein 2D-Emitter ignoriert z; ein 3D-Emitter, der zwei Werte bekommt, erhält z = 0. Ein Nullvektor wird als (0, -1, 0) behandelt. Bei einem 3D-Emitter ist die Richtung in Weltachsen angegeben, nicht in denen des Knotens, dreh sie also selbst, wenn sich der Knoten dreht.\nSeit: 3.00.", + args = "(emitter, x, y [, z])", + returns = "()", + }, + emitterSetDrag = { + type = "function", + description = "Wie schnell Partikel ihre Geschwindigkeit verlieren: in jedem Frame wird die Geschwindigkeit mit 1 - perSecond * dt multipliziert, nie unter 0, sodass 1 den größten Teil der Geschwindigkeit innerhalb einer Sekunde nimmt und 0 (der Standard) sie für immer behält. Negative Werte werden zu 0. Rauch, Glut und Feuerwerk werden langsamer; Funken und Regen nicht. Wirkt ab dem nächsten Frame auf die lebenden Partikel.\nSeit: 3.00.", + args = "(emitter, perSecond)", + returns = "()", + }, + emitterSetFrames = { + type = "function", + description = "Mit einem animierten Sprite als Textur nimmt jedes neue Partikel einen zufälligen Frame von first bis last und behält ihn sein Leben lang; Frames zählen ab 0. Die Werte werden auf die Frames begrenzt, die die Textur hat, und last wird auf first angehoben, wenn es kleiner ist. Standard: nur Frame 0, auf den auch emitterSetTexture zurücksetzt. Mit der eingebauten Scheibe oder einem Sprite aus einem einzigen Frame hat der Bereich keine Wirkung.\nSeit: 3.00.", + args = "(emitter, first, last)", + returns = "()", + }, + emitterSetGravity = { + type = "function", + description = "Eine konstante Beschleunigung auf jedes Partikel, in Overlay-Einheiten (2D) oder Welteinheiten (3D) pro Sekunde zum Quadrat: (0, 500) zieht 2D-Funken den Bildschirm hinunter, (0, -20) lässt 2D-Rauch aufsteigen, (0, -9.8, 0) ist die Erde in der Szene. Standard keine. Sie wirkt ab dem nächsten Frame auf die lebenden Partikel. Partikel sind keine Physikkörper; gib ihnen emitterSetCollide, damit sie von einem Boden oder der Szene abprallen.\nSeit: 3.00.", + args = "(emitter, x, y [, z])", + returns = "()", + }, + emitterSetLayer = { + type = "function", + description = "Nur 2D: wohin emitterDraw die Partikel setzt. PARTICLE_OVER (Standard) zeichnet über allem auf dem Overlay; PARTICLE_UNDER zeichnet unter dem Overlay und über dem Video und der 3D-Szene, für Rauch hinter dem Punktestand. Bei einem 3D-Emitter wird die Einstellung angenommen und ignoriert. Alles andere als diese beiden Konstanten beendet das Skript.\nSeit: 3.00.", + args = "(emitter, layer)", + returns = "()", + }, + emitterSetLife = { + type = "function", + description = "Wie lange jedes neue Partikel lebt, zufällig zwischen den beiden gezogen; Standard 1 bis 2. Seine Größe und Farbe laufen über diese Zeit von ihrem Anfang zu ihrem Ende, die Lebensdauer bestimmt also das Tempo jedes Ausblendens. Das Minimum wird bei 0.001 oder darüber gehalten, und das Maximum wird auf das Minimum angehoben, wenn es kleiner ist. Lebende Partikel behalten die Lebensdauer, mit der sie geboren wurden.\nSeit: 3.00.", + args = "(emitter, minSeconds, maxSeconds)", + returns = "()", + }, + emitterSetLit = { + type = "function", + description = "Nur 3D: schattiert jedes Partikel nach den Lichtern und dem Himmel der Szene, als wäre es eine kleine Kugel, die zur Kamera zeigt, sodass Rauch die Farbe einer Feuerschale annimmt und Staub in der Sonne glitzert. Standardmäßig aus; Schatten werden nicht berücksichtigt. Mit PARTICLE_ADD wird die Beleuchtung trotzdem auf die Tönung angewendet, bevor sie addiert wird. Ein 2D-Emitter speichert den Wert und ignoriert ihn.\nSeit: 3.00.", + args = "(emitter, lit)", + returns = "()", + }, + emitterSetLocal = { + type = "function", + description = "Ob lebende Partikel dem Emitter folgen. false (Standard) lässt jedes dort, wo es geboren wurde, sodass eine durch einen Raum getragene Fackel Rauch hinter sich lässt; true hält die ganze Wolke relativ zum Ursprung des Emitters, sodass sich der Abgasstrahl einer Schubdüse mit dem Schiff bewegt. Das Umschalten überführt die lebenden Partikel ohne Sprung von einem Bezugssystem ins andere. Bei einem 3D-Emitter ist der Ursprung die Weltposition des Knotens; bei einem 2D-Emitter ist es der Punkt von emitterSetPosition, eine lokale 2D-Wolke gleitet also ebenfalls mit. Schweife folgen derselben Regel.\nSeit: 3.00.", + args = "(emitter, local)", + returns = "()", + }, + emitterSetMax = { + type = "function", + description = "Der Pool: wie viele Partikel gleichzeitig leben dürfen, Standard 1000. Ein voller Pool erzeugt keine weiteren, weder aus dem Strom noch aus einem Stoß, bis einige sterben. Eine Änderung der Anzahl baut den Pool neu und tötet jedes lebende Partikel, setz sie also vor dem Start. Bemiss den Pool als Rate mal längste Lebensdauer plus den größten Stoß. Eine Anzahl unter 1 beendet das Skript.\nSeit: 3.00.", + args = "(emitter, count)", + returns = "()", + }, + emitterSetPosition = { + type = "function", + description = "Nur 2D: wo neue Partikel geboren werden, in Overlay-Koordinaten; Standard (0, 0). Setze die Position in jedem Frame, um einem Sprite oder einem 2D-Physikkörper zu folgen. Lebende Partikel bleiben, wo sie sind, außer emitterSetLocal ist eingeschaltet. Ein 3D-Emitter folgt seinem Knoten und beendet hier das Skript; bewege stattdessen den Knoten.\nSeit: 3.00.", + args = "(emitter, x, y)", + returns = "()", + }, + emitterSetRadius = { + type = "function", + description = "Neue Partikel werden irgendwo innerhalb dieses Abstands vom Ursprung des Emitters geboren, gleichmäßig über eine Scheibe (2D) oder eine Kugel (3D), in Overlay- oder Welteinheiten. Standard 0, ein Punkt; negative Werte werden zu 0. Ein Radius füllt ein Volumen, einen Fackelkopf oder ein Stück Staub in der Luft, wo ein Punkt eine Nadel ergäbe.\nSeit: 3.00.", + args = "(emitter, radius)", + returns = "()", + }, + emitterSetRate = { + type = "function", + description = "Wie viele Partikel pro Sekunde ein gestarteter Emitter erzeugt; Standard 50, negative Werte werden zu 0. Bruchteile summieren sich, sodass 0.5 alle zwei Sekunden eines erzeugt. Ändere den Wert während des Stroms, um eine Flamme oder eine Schubdüse zu drosseln. Ein voller Pool (emitterSetMax) erzeugt keine weiteren, bis einige sterben, egal bei welcher Rate.\nSeit: 3.00.", + args = "(emitter, perSecond)", + returns = "()", + }, + emitterSetSize = { + type = "function", + description = "Größe bei der Geburt und beim Tod, in Overlay-Einheiten (2D) oder Welteinheiten (3D), gleichmäßig über die Lebensdauer des Partikels wechselnd; Standard 16 bis 4 in 2D und 0.2 bis 0.05 in 3D. variation (0 bis 1, Standard 0) skaliert beide Größen jedes neuen Partikels mit einem einzigen Zufallsfaktor zwischen 1 - variation und 1 + variation, sodass ein Wölkchen sein ganzes Leben lang etwas größer oder kleiner ist. Negative Größen werden zu 0, und variation wird begrenzt. Ein texturiertes Partikel wird als Quadrat dieser Größe gezeichnet; ein breiteres Bild wird gestaucht.\nSeit: 3.00.", + args = "(emitter, start, finish [, variation])", + returns = "()", + }, + emitterSetSoftness = { + type = "function", + description = "Nur 3D: blendet ein Partikel über distance Welteinheiten aus, wo es auf Geometrie dahinter trifft, sodass ein Rauchwölkchen keine harte Linie zeichnet, wo es eine Wand kreuzt, und ein Springbrunnen in sein Becken eintaucht, statt es zu zerschneiden. 0 (der Standard) schaltet es aus; negative Werte werden zu 0. Solange irgendein Emitter es verwendet, wird die Tiefe der Szene pro Frame einmal mehr gerendert, ein Raspberry Pi sollte es also sparsam einsetzen. Ein 2D-Emitter speichert den Wert und ignoriert ihn.\nSeit: 3.00.", + args = "(emitter, distance)", + returns = "()", + }, + emitterSetSpeed = { + type = "function", + description = "Anfangsgeschwindigkeit jedes neuen Partikels, zufällig zwischen den beiden gezogen, in Overlay-Einheiten pro Sekunde (2D) oder Welteinheiten pro Sekunde (3D); Standard 50 bis 100 in 2D und 1 bis 2 in 3D. Das Maximum wird auf das Minimum angehoben, wenn es kleiner ist. Die Geschwindigkeit bestimmt, wie weit ein Partikel kommt, bevor Dämpfung, Schwerkraft und seine Lebensdauer übernehmen.\nSeit: 3.00.", + args = "(emitter, min, max)", + returns = "()", + }, + emitterSetSpin = { + type = "function", + description = "Drehgeschwindigkeit jedes neuen Partikels in Grad pro Sekunde, zufällig zwischen den beiden gezogen; negative Werte drehen andersherum, und ein Bereich, der 0 überspannt, ergibt beides. Jedes Partikel beginnt mit einem zufälligen Winkel, ob es sich dreht oder nicht. Standard keine. Das Maximum wird auf das Minimum angehoben, wenn es kleiner ist. Die Drehung sieht man an einem texturierten Partikel oder einem Schweif; die eingebaute Scheibe ist rund und sieht in jedem Winkel gleich aus.\nSeit: 3.00.", + args = "(emitter, min, max)", + returns = "()", + }, + emitterSetSpread = { + type = "function", + description = "Der halbe Winkel des Kegels (3D) oder Fächers (2D) um die Richtung, in den neue Partikel geworfen werden, 0 bis 180; 0 ist eine gerade Linie, 180 jede Richtung. Standard 30; Werte außerhalb des Bereichs werden begrenzt. In 3D werden die Richtungen gleichmäßig über die Kappe des Kegels verteilt, sodass sich ein weiter Kegel nicht zu seiner Achse hin ballt.\nSeit: 3.00.", + args = "(emitter, degrees)", + returns = "()", + }, + emitterSetTexture = { + type = "function", + description = "Das Bild, das jedes Partikel trägt: ein geladenes Sprite oder keines (ein Argument oder nil) für die eingebaute weiche Scheibe. Der Emitter behält seine eigene Kopie der Pixel des Sprites, jeden Frame eines animierten Sprites eingeschlossen, das Sprite kann danach also entladen werden. Das Setzen der Textur setzt den Frame-Bereich auf Frame 0 zurück und setzt jedes lebende Partikel auf Frame 0, ruf emitterSetFrames also danach auf, nicht davor. Blending und Tönung wirken auf das Bild wie auf die Scheibe.\nSeit: 3.00.", + args = "(emitter [, sprite])", + returns = "()", + }, + emitterSetTrail = { + type = "function", + description = "Ein Band hinter jedem Partikel durch seine letzten length Positionen, eine pro Frame aufgezeichnet (2 bis 16; 0 oder 1 für keines, der Standard), width breit in Welt- oder Overlay-Einheiten, in 2D wie in 3D. Das Band trägt die aktuelle Tönung des Partikels, verblasst zu seinem Ende hin zu transparent und wird mit der mittleren Spalte des Partikelbilds texturiert, sodass der weiche Rand der Scheibe zum Rand des Bands wird. Die Werte werden auf den Bereich begrenzt. Eine Änderung der Länge baut den Speicher der Schweife neu, lebende Partikel beginnen ihre Bänder also von vorn. Funken, Glut, Leuchtspurgeschosse, Kometen.\nSeit: 3.00.", + args = "(emitter, length, width)", + returns = "()", + }, + emitterStart = { + type = "function", + description = "Beginnt den stetigen Strom mit der eingestellten Rate; von da an erzeugt der Emitter in jedem Frame Partikel bis emitterStop. Einen Emitter zu starten, der bereits strömt, ändert nichts. Der Strom braucht keinen Zeichenaufruf, um zu laufen; ein 2D-Emitter, der strömt, aber nicht gezeichnet wird, verbraucht trotzdem seinen Pool.\nSeit: 3.00.", + args = "(emitter)", + returns = "()", + }, + emitterStop = { + type = "function", + description = "Beendet den Strom; die lebenden Partikel leben ihr Leben zu Ende, und jeder noch geschuldete Bruchteil eines Partikels wird verworfen. emitterBurst funktioniert auch bei einem gestoppten Emitter, und emitterIsActive bleibt true, bis das letzte Partikel stirbt. Verwende emitterClear, um die Partikel außerdem sofort zu entfernen.\nSeit: 3.00.", + args = "(emitter)", + returns = "()", + }, + fontLoad = { + type = "function", + description = "Öffnet eine TrueType-Schriftart in einer Punktgröße und macht sie zur ausgewählten Schriftart. Die Größe ist für die Lebensdauer des Handles fest, lade dieselbe Datei also einmal pro benötigter Größe. Der Name geht wie jedes andere Asset durch das virtuelle Dateisystem; stelle DIR voran für Dateien, die mit deinem Spiel mitgeliefert werden, oder verwende Singe/FreeSansBold.ttf für den mitgelieferten Schriftschnitt. Eine Datei, die sich nicht öffnen lässt, beendet das Skript mit Unable to open und dem Namen.\nRückgabe: ganzzahliges Schriftart-Handle.\nSeit: 1.x", + args = "(filename, pointSize: integer)", + returns = "(integer)", + }, + fontPrint = { + type = "function", + description = "Rendert einen String mit der ausgewählten Schriftart, der aktuellen Qualität und der aktuellen Vordergrundfarbe (bei FONT_QUALITY_SHADED zusätzlich der Hintergrundfarbe) und blittet ihn dann mit seiner oberen linken Ecke bei (x, y) auf das Overlay. Ruf es aus onOverlayUpdate auf, wenn nötig in jedem Frame: die Engine behält jeden String, den sie rendert, mit Schriftart, Qualität, Farben und Text als Schlüssel, und ein späterer Aufruf, der denselben verlangt, blittet ihn nur noch. Strings, die etwa eine Sekunde lang nicht ausgegeben wurden, werden vergessen, und die Strings einer Schriftart verschwinden, wenn sie entladen wird, sodass Text, der sich ändert, nicht mehr kostet als schon immer, und Text, der sich nicht ändert, fast nichts. Eine ganze Seite Highscores in jedem Frame neu auszugeben ist in Ordnung. Ohne ausgewählte Schriftart endet das Skript mit einem Fehler.\nSeit: 1.x", + args = "(x, y, text)", + returns = "()", + }, + fontQuality = { + type = "function", + description = "Wählt, wie fontPrint und fontToSprite Text von nun an rastern. Die Einstellung ist global, nicht pro Schriftart, und beginnt bei FONT_QUALITY_SOLID. Sprites, die fontToSprite schon erzeugt hat, behalten die Qualität, mit der sie gerendert wurden. Ein Wert außerhalb der drei Konstanten beendet das Skript mit einem Fehler.\nSeit: 1.x", + args = "(mode)", + returns = "()", + }, + fontSelect = { + type = "function", + description = "Macht eine geladene Schriftart zu der, die fontPrint und fontToSprite verwenden. Die Auswahl bleibt bis zum nächsten fontSelect oder fontLoad bestehen, da auch das Laden auswählt. Sie kann so oft wie nötig geändert werden, auch mehrmals innerhalb eines onOverlayUpdate. Ein unbekanntes oder entladenes Handle (oder gar keine Zahl) lässt die aktuelle Schriftart ausgewählt und gibt eine Warnung aus, so wie Hypseus Singe es durchgehen lässt.\nSeit: 1.x", + args = "(id)", + returns = "()", + }, + fontToSprite = { + type = "function", + description = "Rendert einen String mit der ausgewählten Schriftart, Qualität und den Farben in ein neues Sprite und gibt dessen Handle zurück. Das Sprite ist von da an unabhängig von der Schriftart, übersteht also fontUnload, und es funktioniert mit jedem sprite*-Aufruf: zeichne es, miss es, skaliere oder drehe es und gib es mit spriteUnload frei. Für einen String, der sich ändert, rendere ein neues Sprite und entlade das alte. Ohne ausgewählte Schriftart endet das Skript mit einem Fehler.\nRückgabe: ganzzahliges Sprite-Handle.\nSeit: 1.x", + args = "(text)", + returns = "(integer)", + }, + fontUnload = { + type = "function", + description = "Schließt eine Schriftart und macht ihr Handle ungültig. Sprites, die mit fontToSprite daraus gemacht wurden, sind nicht betroffen. War die entladene Schriftart die ausgewählte, ist danach keine Schriftart ausgewählt, und das nächste fontPrint oder fontToSprite beendet das Skript mit einem Fehler, bis fontSelect oder fontLoad eine andere wählt.\nSeit: 2.00", + args = "(id)", + returns = "()", + }, + getFValue = { + type = "function", + description = "Hypseus-Erweiterung. Gibt die als --fvalue angegebene Zahl zurück oder 0, wenn die Option nicht verwendet wurde. Sie ist ein Kanal für eine einzige Zahl von demjenigen, der Singe gestartet hat, zum Spiel, gedacht für einen Wert, den ein Frontend von Start zu Start ändern will -- einen Schwierigkeitsgrad, eine Anzahl Start-Credits, einen Menüindex. Die Engine reicht ihn nur durch.\nRückgabe: Zahl.\nSeit: 3.00 (Hypseus)", + args = "()", + returns = "(number)", + }, + getUserString = { + type = "function", + description = "Hypseus-Erweiterung, immer ein leerer String. Trotz des Namens ist es kein String, den der Benutzer gesetzt hat: Hypseus antwortet mit der Installations-ID des Rechners, die es mit Netzwerkstatistiken sendet, genommen aus der MachineGuid von Windows oder der Machine-ID von Linux. Singe sendet nirgendwohin etwas und gibt einem Spiel keinen Fingerabdruck des Rechners, auf dem es läuft.\nRückgabe: ein String, immer leer.\nSeit: 3.00", + args = "()", + returns = "(id)", + }, + guiClose = { + type = "function", + description = "Entlädt das Dokument aus der GUI und verwirft seine Elemente, seine Handler und seine Skripte. Das Dokument-Handle ist danach ungültig. Ein Dokument, das sein eigenes Skript über die API von RmlUi schließt (document:Close()), wird auf dieselbe Weise entladen. Löst bei einem ungültigen Handle No document N in GUI G aus.\nSeit: 3.00.", + args = "(gui, document)", + returns = "()", + }, + guiDelete = { + type = "function", + description = "Gibt die GUI, jedes Dokument darin und ihre Textur frei und beendet das Aktualisieren und Rendern in jedem Frame, das sie kostete. Materialien, die sie über materialSetGui zeigen, kehren zu ihrer eigenen Textur zurück. Das Handle ist danach ungültig und beendet das Skript, wenn es noch einmal verwendet wird; da eine GUI weiter rendert, ob sie gezeichnet wird oder nicht, lösche die, mit denen ein Spiel fertig ist, statt sie zu verstecken.\nSeit: 3.00.", + args = "(gui)", + returns = "()", + }, + guiDraw = { + type = "function", + description = "Legt die Textur der GUI für diesen Frame über das Overlay, in Overlay-Koordinaten, sodass sie wie alles andere auf dem Overlay dem Sinden-Rahmen und der Overscan-Skalierung folgt: mit einem Argument über das ganze Overlay, in ihrer eigenen Größe mit einer Ecke bei x, y oder skaliert in das Rechteck width mal height. Die Stelle des Aufrufs im Zeichnen des Skripts entscheidet, was unter der GUI liegt und was darüber: alles, was vorher auf das Overlay gezeichnet wurde, bleibt darunter, und alles, was danach gezeichnet wird -- bis zum nächsten guiDraw oder bis das Skript das nächste Mal an der Reihe ist -- kommt auf eine eigene Ebene, die über diese GUI gelegt wird. Ein Zeiger, der nach dem HUD gezeichnet wird, liegt über dem HUD; ein danach gezeichnetes zweites Dokument liegt über dem Zeiger, mit seiner eigenen Ebene darüber. Bis zu sechzehn solcher Ebenen pro Frame; ein Aufruf darüber hinaus zeichnet seine GUI ohne etwas darüber. overlayClear leert die Ebene, auf die gerade gezeichnet wird, und overlaySetMonochrome macht nur das Basis-Overlay grau. Der Aufruf gilt einen Frame; mach ihn aus onOverlayUpdate (oder singeMain) in jedem Frame, in dem die GUI sichtbar sein soll. GUIs zeichnen über dem Overlay in der Reihenfolge der Aufrufe (die letzte oben), unter Partikeln in PARTICLE_OVER und der Pausenanzeige, und die GUI, die zuletzt unter dem Zeiger gezeichnet wurde, ist die, die die Maus bekommt. Bis zu sechzehn Aufrufe pro Frame werden beachtet; weitere werden ignoriert. Jede andere Anzahl von Argumenten löst Expected 1, 3 or 5 arguments aus.\nSeit: 3.00.", + args = "(gui [, x, y, width, height])", + returns = "()", + }, + guiDrawScreen = { + type = "function", + description = "Dasselbe wie guiDraw, nur dass das Rechteck über das ganze Fenster gemessen wird statt über das Bild. Die Koordinaten sind weiterhin Overlay-Koordinaten -- 0, 0 ist oben links im Fenster und overlayGetWidth(), overlayGetHeight() unten rechts -- sodass eine GUI auf der Bezel-Grafik neben dem Bild, darüber oder darunter sitzen kann. Ohne Bezel und ohne --scalefactor, --shiftx oder --shifty ist das Bild das ganze Fenster, und dies ist genau guiDraw.\nSeit: 3.00.", + args = "(gui [, x, y, width, height])", + returns = "()", + }, + guiGetHeight = { + type = "function", + description = "Die Höhe der GUI in ihren eigenen Pixeln, wie an guiNew übergeben. Beendet das Skript bei einem ungültigen Handle.\nRückgabe: Eine Ganzzahl.\nSeit: 3.00.", + args = "(gui)", + returns = "(height)", + }, + guiGetStats = { + type = "function", + description = "Was das Rendern aller GUIs im letzten abgeschlossenen Frame gekostet hat, zum Abstimmen von Dokumenten, die Filter, Masken und Schatten verwenden (die alle über eine Ebene in der Größe der GUI zeichnen). Die Zähler gelten für alle GUIs zusammen. In einem Frame, in dem sich das Bild keiner GUI geändert hat, sind sie alle null, da nichts neu gezeichnet wurde (siehe guiRefresh).\nRückgabe: Eine Tabelle mit den Feldern drawCalls (indizierte Zeichenaufrufe, einschließlich Maskenschreibvorgängen, Kompositionen und Filterdurchgängen), layerPushes (betretene Ebenen für Filter, Maskenbilder und Box-Schatten), filterPasses (Durchgänge in voller Größe und Blits innerhalb von Filterketten; eine Unschärfe kostet mehrere), maskWrites (Zeichenaufrufe in die Stencil-Clipmaske), layerBytes (Bytes der derzeit belegten Ebenentexturen; jede hat die Größe ihrer GUI bei vier Bytes pro Pixel) und passes (begonnene Render-Durchgänge; jeder Ebenenwechsel beginnt einen).\nSeit: 3.00.", + args = "()", + returns = "(stats)", + }, + guiGetValue = { + type = "function", + description = "Der Wert des Elements mit dieser id im Dokument, als String: der Wert eines Formularelements (der Text eines Textfelds, der value einer Option eines select, ein Schieberegler in der eigenen Formatierung von RmlUi wie \"85.000000\", das Attribut value eines Kontrollkästchens oder Optionsfelds, \"on\", wenn es keines hat, ob es angekreuzt ist oder nicht; sein Zustand ist sein Attribut checked, das über die API von RmlUi gelesen wird) oder bei jedem anderen Element sein inneres RML. Gibt nil zurück, wenn kein Element diese id hat, sodass eine Seite abgefragt werden kann, ohne das Skript zu beenden; ein falsches GUI- oder Dokument-Handle beendet es. Werte, die länger als 4095 Zeichen sind, werden abgeschnitten.\nRückgabe: Ein String oder nil.\nSeit: 3.00.", + args = "(gui, document, id)", + returns = "(value)", + }, + guiGetWidth = { + type = "function", + description = "Die Breite der GUI in ihren eigenen Pixeln, wie an guiNew übergeben. Beendet das Skript bei einem ungültigen Handle.\nRückgabe: Eine Ganzzahl.\nSeit: 3.00.", + args = "(gui)", + returns = "(width)", + }, + guiHide = { + type = "function", + description = "Versteckt das Dokument: es wird nicht gezeichnet und nimmt keine Eingaben an, behält aber seine Elemente und Werte, und guiShow bringt es so zurück, wie es war. Löst bei einem ungültigen Handle No document N in GUI G aus.\nSeit: 3.00.", + args = "(gui, document)", + returns = "()", + }, + guiLoad = { + type = "function", + description = "Lädt ein RML-Dokument in die GUI, zeigt es an und gibt ein ganzzahliges Dokument-Handle für die anderen Aufrufe zurück. Der Name geht wie der eines Sprites durch das virtuelle Dateisystem und wird relativ zu dem Ordner aufgelöst, in dem Singe gestartet wurde, stelle also DIR voran für Dateien, die mit deinem Spiel mitgeliefert werden; Stylesheets, Bilder und Schriftarten, die im Dokument genannt werden, werden relativ zu der Datei aufgelöst, die sie nennt, und dann über dieselbe Suche, sodass ein gepacktes Spiel sie findet. Die