New tagline. ZBStudio integration. Windows on ARM support.

This commit is contained in:
Scott Duensing 2026-10-16 19:22:54 -05:00
parent 48148d99fb
commit c0b7c41fb9
74 changed files with 44723 additions and 1107 deletions

View file

@ -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

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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

View file

@ -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"

12
INSTALL
View file

@ -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).

View file

@ -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)

View file

@ -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.

36
assets/Debugger.singe Normal file
View file

@ -0,0 +1,36 @@
--[[
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* 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

BIN
assets/menuBackground.mkv (Stored with Git LFS)

Binary file not shown.

View file

@ -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

View file

@ -5,7 +5,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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

View file

@ -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<version>.zip
# and .builddir/Singe-Docs-v<version>-<language>.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[@]}"

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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")

View file

@ -4,7 +4,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -0,0 +1,34 @@
#
# Singe 3
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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",
},
);

View file

@ -4,7 +4,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -0,0 +1,34 @@
#
# Singe 3
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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)

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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).")

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -3,7 +3,7 @@
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# 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.
#

View file

@ -14,7 +14,7 @@ Scott Duensing <scott@kangaroopunch.com>
== 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 <<troubleshooting,When Something Goes Wrong>> 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.

View file

@ -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 <<collide,the collide calls>> deliberately do not
| MobDebug | `require("mobdebug")`, the debugger <<zerobrane,ZeroBrane Studio>> 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 <<gui,GUI>>)
|===
@ -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 <<midi,MIDI>>.
The `Audio:` line of the trace header (`--program` or `--trace`) lists the
@ -4481,7 +4589,7 @@ Hypseus extension, accepted and ignored. See <<controllergetpadding,controllerGe
[#debug]
=== Debug
Two calls for seeing what a script is doing: `debugPrint` writes text, and `lineDraw` draws a line over the 3D scene for one frame (`physicsSetDebug` and `navDraw` draw through the same lines). Printed output goes to the console Singe was started from, so it is visible during development and harmless in a cabinet, where `--noconsole` silences it. When Singe runs with `--trace`, every API call including this one is also written with its arguments to `trace.txt` in the data directory, which is the way to get a permanent record. Command line options are listed in the chapter on running Singe; the ZeroBrane Studio integration in `zbstudio/` offers a debugger when printing is not enough.
Two calls for seeing what a script is doing: `debugPrint` writes text, and `lineDraw` draws a line over the 3D scene for one frame (`physicsSetDebug` and `navDraw` draw through the same lines). Printed output goes to the console Singe was started from, so it is visible during development and harmless in a cabinet, where `--noconsole` silences it. When Singe runs with `--trace`, every API call including this one is also written with its arguments to `trace.txt` in the data directory, which is the way to get a permanent record. Command line options are listed in the chapter on running Singe. When printing is not enough, <<zerobrane,ZeroBrane Studio>> 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 <<materialsetblend,`materialSetBlend`>> 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:* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
.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 <<musicunload,`musicUnload`>> 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 <<musicunload,`musicUnload`>> or a reload.
A file that is missing, or that no compiled-in decoder can read, terminates the script, as <<soundload,`soundLoad`>> 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

View file

@ -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

View file

@ -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 <<troubleshooting,Wenn etwas schiefgeht>> 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.

View file

@ -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 <<collide,die collide-Aufrufe>> absichtlich nicht tun
| MobDebug | `require("mobdebug")`, der Debugger, mit dem <<zerobrane,ZeroBrane Studio>> spricht; `--debug` startet ihn, sodass ein Spiel keine eigene Zeile dafür braucht
| RmlUi | Das eigene Objektmodell der GUI, die globale Variable `rmlui` (siehe <<gui,GUI>>)
|===
@ -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 <<midi,MIDI>>.
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 <<zerobrane,ZeroBrane Studio>> 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 <<materialsetblend,`materialSetBlend`>> 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:* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
.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 <<musicunload,`musicUnload`>> 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 <<musicunload,`musicUnload`>> oder zu einem Neuladen.
Eine Datei, die fehlt oder die kein einkompilierter Decoder lesen kann, beendet das Skript, wie bei <<soundload,`soundLoad`>>.
@ -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

View file

@ -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

View file

@ -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.

View file

@ -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 <<troubleshooting,Cuando algo sale mal>> 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.

View file

@ -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 <<collide,las llamadas de colisión>> no hacen a propósito
| MobDebug | `require("mobdebug")`, el depurador con el que habla <<zerobrane,ZeroBrane Studio>>; `--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 <<gui,GUI>>)
|===
@ -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 <<midi,MIDI>>.
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 <<controllergetpadding,co
[#debug]
=== Depuración
Dos llamadas para ver qué hace un script: `debugPrint` escribe texto, y `lineDraw` dibuja una línea sobre la escena 3D durante un fotograma (`physicsSetDebug` y `navDraw` dibujan con esas mismas líneas). La salida impresa va a la consola desde la que se arrancó Singe, así que se ve durante el desarrollo y es inofensiva en una recreativa, donde `--noconsole` la silencia. Cuando Singe se ejecuta con `--trace`, cada llamada a la API, incluida esta, se escribe también con sus argumentos en `trace.txt`, en la carpeta de datos, que es la forma de obtener un registro permanente. Las opciones de línea de comandos se enumeran en el capítulo sobre cómo ejecutar Singe; la integración con ZeroBrane Studio de `zbstudio/` ofrece un depurador cuando imprimir no basta.
Dos llamadas para ver qué hace un script: `debugPrint` escribe texto, y `lineDraw` dibuja una línea sobre la escena 3D durante un fotograma (`physicsSetDebug` y `navDraw` dibujan con esas mismas líneas). La salida impresa va a la consola desde la que se arrancó Singe, así que se ve durante el desarrollo y es inofensiva en una recreativa, donde `--noconsole` la silencia. Cuando Singe se ejecuta con `--trace`, cada llamada a la API, incluida esta, se escribe también con sus argumentos en `trace.txt`, en la carpeta de datos, que es la forma de obtener un registro permanente. Las opciones de línea de comandos se enumeran en el capítulo sobre cómo ejecutar Singe. Cuando imprimir no basta, <<zerobrane,ZeroBrane Studio>> 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 <<materialsetblend,`materialSetBlend`>> 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:* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
.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 <<musicunload,`musicUnload`>> 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 <<musicunload,`musicUnload`>> o una recarga.
Un archivo que falta, o que ningún decodificador incluido en la compilación puede leer, termina el script, como hace <<soundload,`soundLoad`>>.
@ -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

View file

@ -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

View file

@ -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 <<troubleshooting,Quand quelque chose ne va pas>> 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.

View file

@ -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 <<collide,les appels collide>> ne font délibérément pas
| MobDebug | `require("mobdebug")`, le débogueur auquel parle <<zerobrane,ZeroBrane Studio>> ; `--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 <<gui,GUI>>)
|===
@ -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 <<midi,MIDI>>.
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 <<controllergetpadding,controller
[#debug]
=== Débogage
Deux appels pour voir ce que fait un script : `debugPrint` écrit du texte, et `lineDraw` trace une ligne par-dessus la scène 3D pendant une image (`physicsSetDebug` et `navDraw` dessinent par ces mêmes lignes). La sortie affichée va à la console depuis laquelle Singe a été lancé : elle est donc visible pendant le développement et inoffensive dans une borne, où `--noconsole` la fait taire. Quand Singe tourne avec `--trace`, chaque appel de l'API, celui-ci compris, est aussi écrit avec ses arguments dans `trace.txt`, dans le dossier de données, ce qui est le moyen d'en garder un enregistrement permanent. Les options de ligne de commande sont listées dans le chapitre sur le lancement de Singe ; l'intégration de ZeroBrane Studio dans `zbstudio/` offre un débogueur quand l'affichage ne suffit pas.
Deux appels pour voir ce que fait un script : `debugPrint` écrit du texte, et `lineDraw` trace une ligne par-dessus la scène 3D pendant une image (`physicsSetDebug` et `navDraw` dessinent par ces mêmes lignes). La sortie affichée va à la console depuis laquelle Singe a été lancé : elle est donc visible pendant le développement et inoffensive dans une borne, où `--noconsole` la fait taire. Quand Singe tourne avec `--trace`, chaque appel de l'API, celui-ci compris, est aussi écrit avec ses arguments dans `trace.txt`, dans le dossier de données, ce qui est le moyen d'en garder un enregistrement permanent. Les options de ligne de commande sont listées dans le chapitre sur le lancement de Singe. Quand l'affichage ne suffit pas, <<zerobrane,ZeroBrane Studio>> 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 à <<materialsetblend,`materialSetBlend`>>, 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 :* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
.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'à <<musicunload,`musicUnload`>> 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'à <<musicunload,`musicUnload`>> ou jusqu'à un rechargement.
Un fichier absent, ou qu'aucun décodeur compilé ne sait lire, met fin au script, comme le fait <<soundload,`soundLoad`>>.
@ -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

View file

@ -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

View file

@ -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 <<troubleshooting,Quando qualcosa va storto>>.
[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.

View file

@ -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 <<collide,le chiamate collide>> deliberatamente non fanno
| MobDebug | `require("mobdebug")`, il debugger con cui parla <<zerobrane,ZeroBrane Studio>>; `--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 <<gui,GUI>>)
|===
@ -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 <<midi,MIDI>>.
La riga `Audio:` dell'intestazione della traccia (`--program` o `--trace`) elenca
@ -4620,7 +4736,7 @@ Estensione di Hypseus, accettata e ignorata. Vedi <<controllergetpadding,control
[#debug]
=== Debug
Due chiamate per vedere che cosa sta facendo uno script: `debugPrint` scrive del testo, e `lineDraw` disegna una linea sopra la scena 3D per un fotogramma (`physicsSetDebug` e `navDraw` disegnano tramite le stesse linee). L'output stampato va alla console da cui è stato avviato Singe, quindi è visibile durante lo sviluppo e innocuo in un cabinato, dove `--noconsole` lo zittisce. Quando Singe gira con `--trace`, ogni chiamata dell'API, compresa questa, viene anche scritta con i suoi argomenti in `trace.txt` nella cartella dei dati, ed è questo il modo di averne una registrazione permanente. Le opzioni da riga di comando sono elencate nel capitolo sull'avvio di Singe; l'integrazione con ZeroBrane Studio in `zbstudio/` offre un debugger quando stampare non basta.
Due chiamate per vedere che cosa sta facendo uno script: `debugPrint` scrive del testo, e `lineDraw` disegna una linea sopra la scena 3D per un fotogramma (`physicsSetDebug` e `navDraw` disegnano tramite le stesse linee). L'output stampato va alla console da cui è stato avviato Singe, quindi è visibile durante lo sviluppo e innocuo in un cabinato, dove `--noconsole` lo zittisce. Quando Singe gira con `--trace`, ogni chiamata dell'API, compresa questa, viene anche scritta con i suoi argomenti in `trace.txt` nella cartella dei dati, ed è questo il modo di averne una registrazione permanente. Le opzioni da riga di comando sono elencate nel capitolo sull'avvio di Singe. Quando stampare non basta, <<zerobrane,ZeroBrane Studio>> 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 <<materialsetblend,`materialSetBlend`>> 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:* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
.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 <<musicunload,`musicUnload`>> 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 <<musicunload,`musicUnload`>> o a un ricaricamento.
Un file mancante, o che nessun decoder compilato nella build sa leggere, termina lo script, come fa <<soundload,`soundLoad`>>.
@ -17043,6 +17191,9 @@ classifica. Vedi <<singegetgameid,singeGetGameId>> 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

View file

@ -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

166
src/debugger.c Normal file
View file

@ -0,0 +1,166 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* 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 <SDL3/SDL.h>
#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;
}

52
src/debugger.h Normal file
View file

@ -0,0 +1,52 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* 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 <stdint.h>
#include <stdbool.h>
#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

View file

@ -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;
}

View file

@ -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"

View file

@ -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 <base><SUFFIX>.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);

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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"

View file

@ -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
}

View file

@ -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 }

251
thirdparty/MobDebug/CHANGELOG.md vendored Normal file
View file

@ -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.

63
thirdparty/MobDebug/LICENSE vendored Normal file
View file

@ -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.

90
thirdparty/MobDebug/README.md vendored Normal file
View file

@ -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

1670
thirdparty/MobDebug/src/mobdebug.lua vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -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]*$')

View file

@ -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]*$')

View file

@ -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.

View file

@ -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)

View file

@ -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")

View file

@ -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

View file

@ -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)

33
util/singeBinary.py Normal file
View file

@ -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())

551
util/zbstudioApi.py Normal file
View file

@ -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"(?<![\w*])\*([^*\n]+)\*(?![\w*])", r"\1", text)
text = text.replace("{nbsp}", " ").replace(" +\n", "\n")
text = text.replace("\u00a0", " ").replace("\u202f", " ")
return re.sub(r"[ \t]+", " ", text).strip()
def autoId(title):
return "_" + re.sub(r"[^a-z0-9]+", "_", title.lower()).strip("_")
def dumpGlobals(singe):
script = DUMP_SCRIPT % (" ".join(sorted(STANDARD)), DUMP_TAG, DUMP_TAG)
with tempfile.TemporaryDirectory() as work:
with open(os.path.join(work, "dump.singe"), "w") as f:
f.write(script)
env = dict(os.environ, SDL_VIDEO_DRIVER="offscreen", SDL_GPU_DRIVER="nothing", SDL_AUDIO_DRIVER="dummy")
run = subprocess.run([singe, "-k", "-s", "-d", os.path.join(work, "data"), "dump.singe"], cwd=work, env=env, capture_output=True, text=True, timeout=TIMEOUT)
globalsFound = {}
for line in run.stdout.splitlines():
parts = line.split("\t")
if (len(parts) < 2) or (parts[0] != DUMP_TAG):
continue
if parts[1] == "G":
globalsFound[parts[2]] = {"type": parts[3], "value": parts[4], "members": {}}
elif parts[1] == "C":
globalsFound[parts[2]]["members"][parts[3]] = {"type": parts[4], "value": parts[5]}
if not globalsFound:
sys.exit("Singe listed no globals:\n" + run.stdout[-2000:])
return globalsFound
def languages():
found = ["en"]
for locale in sorted(glob.glob(os.path.join(DOCS, "*", "locale.adoc"))):
found.append(os.path.basename(os.path.dirname(locale)))
return found
def luaString(text):
text = text.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
return "\"" + text + "\""
# The first paragraph, the Returns and Since lines, and, when the forms differ in more than their
# trailing arguments, every form; enough for a tooltip, with the Reference a click away for the rest.
def describe(entry, allForms):
parts = []
if allForms and (len(entry["signatures"]) > 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()

View file

@ -1,656 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<wxFormBuilder_Project>
<FileVersion major="1" minor="17"/>
<object class="Project" expanded="true">
<property name="class_decoration">; </property>
<property name="code_generation">C++</property>
<property name="disconnect_events">1</property>
<property name="disconnect_mode">source_name</property>
<property name="disconnect_php_events">0</property>
<property name="disconnect_python_events">0</property>
<property name="embedded_files_path">res</property>
<property name="encoding">UTF-8</property>
<property name="event_generation">connect</property>
<property name="file"></property>
<property name="first_id">1000</property>
<property name="help_provider">none</property>
<property name="image_path_wrapper_function_name"></property>
<property name="indent_with_spaces"></property>
<property name="internationalize">0</property>
<property name="name">MyProject1</property>
<property name="namespace"></property>
<property name="path">.</property>
<property name="precompiled_header"></property>
<property name="relative_path">1</property>
<property name="skip_lua_events">1</property>
<property name="skip_php_events">1</property>
<property name="skip_python_events">1</property>
<property name="ui_table">UI</property>
<property name="use_array_enum">0</property>
<property name="use_enum">0</property>
<property name="use_microsoft_bom">0</property>
<object class="Dialog" expanded="true">
<property name="aui_managed">0</property>
<property name="aui_manager_style">wxAUI_MGR_DEFAULT</property>
<property name="bg"></property>
<property name="center">wxBOTH</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="event_handler">impl_virtual</property>
<property name="extra_style"></property>
<property name="fg"></property>
<property name="font"></property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="maximum_size"></property>
<property name="minimum_size">500,-1</property>
<property name="name">dlgSinge</property>
<property name="pos"></property>
<property name="size"></property>
<property name="style">wxDEFAULT_DIALOG_STYLE</property>
<property name="subclass">; ; forward_declare</property>
<property name="title">Singe Configuration</property>
<property name="tooltip"></property>
<property name="two_step_creation">0</property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<object class="wxFlexGridSizer" expanded="true">
<property name="cols">2</property>
<property name="flexible_direction">wxBOTH</property>
<property name="growablecols"></property>
<property name="growablerows"></property>
<property name="hgap">0</property>
<property name="minimum_size"></property>
<property name="name">fgSizer1</property>
<property name="non_flexible_grow_mode">wxFLEX_GROWMODE_SPECIFIED</property>
<property name="permission">none</property>
<property name="rows">0</property>
<property name="vgap">0</property>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Singe:</property>
<property name="markup">0</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">lblSinge</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style">wxALIGN_RIGHT</property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxFilePickerCtrl" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="message">Select Singe Binary</property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">filePickSinge</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style">wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN|wxFLP_USE_TEXTCTRL</property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="value">/home/scott/Singe/Singe-v2.10-Linux-x86_64</property>
<property name="wildcard">*.*</property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Script:</property>
<property name="markup">0</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">lblScript</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxFilePickerCtrl" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="message">Select a file</property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">filePickScript</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style">wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN|wxFLP_USE_TEXTCTRL</property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="value"></property>
<property name="wildcard">*.*</property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Video:</property>
<property name="markup">0</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">lblVideo</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxFilePickerCtrl" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="message">Select a file</property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">filePickVideo</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style">wxFLP_DEFAULT_STYLE|wxFLP_FILE_MUST_EXIST|wxFLP_OPEN|wxFLP_USE_TEXTCTRL</property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="value"></property>
<property name="wildcard">*.*</property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Options:</property>
<property name="markup">0</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">lblOptions</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL|wxEXPAND</property>
<property name="proportion">0</property>
<object class="wxTextCtrl" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="maxlength"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">txtOptions</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="value"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="spacer" expanded="true">
<property name="height">20</property>
<property name="permission">protected</property>
<property name="width">0</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="spacer" expanded="true">
<property name="height">20</property>
<property name="permission">protected</property>
<property name="width">0</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="spacer" expanded="true">
<property name="height">0</property>
<property name="permission">protected</property>
<property name="width">0</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="wxStdDialogButtonSizer" expanded="true">
<property name="Apply">0</property>
<property name="Cancel">1</property>
<property name="ContextHelp">0</property>
<property name="Help">0</property>
<property name="No">0</property>
<property name="OK">1</property>
<property name="Save">0</property>
<property name="Yes">0</property>
<property name="minimum_size"></property>
<property name="name">m_sdbSizer1</property>
<property name="permission">protected</property>
<event name="OnCancelButtonClick">m_sdbSizer1OnCancelButtonClick</event>
<event name="OnOKButtonClick">m_sdbSizer1OnOKButtonClick</event>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="spacer" expanded="true">
<property name="height">20</property>
<property name="permission">protected</property>
<property name="width">0</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="spacer" expanded="true">
<property name="height">20</property>
<property name="permission">protected</property>
<property name="width">0</property>
</object>
</object>
</object>
</object>
</object>
</wxFormBuilder_Project>

627
zbstudio/packages/singe.lua Normal file
View file

@ -0,0 +1,627 @@
--[[
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* 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...-<OS>-<this>. 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,
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,274 +0,0 @@
--[[
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* 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.
*
*
--]]
local configId = ID("singemenu.singemenu")
local id = ID("singetoolbar.singemenu")
local tool
local settings
local line = "-------------------------------------------------------------------------------"
-- Escapes a string for use in string.gsub patterns.
local function escapePattern(text)
return (text:gsub("%W", "%%%0"))
end
-- Wraps a path in quotes so spaces survive the shell.
local function quote(path)
return '"' .. path .. '"'
end
local function configureSinge(self)
local UI = {}
UI.dlgSinge = wx.wxDialog (wx.NULL, wx.wxID_ANY, "Singe Configuration", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxDEFAULT_DIALOG_STYLE )
UI.dlgSinge:SetSizeHints( wx.wxSize( 500,-1 ), wx.wxDefaultSize )
UI.fgSizer1 = wx.wxFlexGridSizer( 0, 2, 0, 0 )
UI.fgSizer1:AddGrowableCol( 1 )
UI.fgSizer1:SetFlexibleDirection( wx.wxBOTH )
UI.fgSizer1:SetNonFlexibleGrowMode( wx.wxFLEX_GROWMODE_SPECIFIED )
UI.lblSinge = wx.wxStaticText( UI.dlgSinge, wx.wxID_ANY, "Singe:", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxALIGN_RIGHT )
UI.lblSinge:Wrap( -1 )
UI.fgSizer1:Add( UI.lblSinge, 0, wx.wxALIGN_CENTER_VERTICAL + wx.wxALIGN_RIGHT + wx.wxALL + wx.wxEXPAND, 5 )
UI.filePickSinge = wx.wxFilePickerCtrl( UI.dlgSinge, wx.wxID_ANY, "", "Select Singe Binary", "*.*", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFLP_DEFAULT_STYLE + wx.wxFLP_FILE_MUST_EXIST + wx.wxFLP_OPEN + wx.wxFLP_USE_TEXTCTRL )
UI.fgSizer1:Add( UI.filePickSinge, 0, wx.wxALL + wx.wxEXPAND, 5 )
UI.lblScript = wx.wxStaticText( UI.dlgSinge, wx.wxID_ANY, "Script:", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
UI.lblScript:Wrap( -1 )
UI.fgSizer1:Add( UI.lblScript, 0, wx.wxALIGN_CENTER_VERTICAL + wx.wxALIGN_RIGHT + wx.wxALL + wx.wxEXPAND, 5 )
UI.filePickScript = wx.wxFilePickerCtrl( UI.dlgSinge, wx.wxID_ANY, "", "Select a file", "*.*", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFLP_DEFAULT_STYLE + wx.wxFLP_FILE_MUST_EXIST + wx.wxFLP_OPEN + wx.wxFLP_USE_TEXTCTRL )
UI.fgSizer1:Add( UI.filePickScript, 0, wx.wxALL + wx.wxEXPAND, 5 )
UI.lblVideo = wx.wxStaticText( UI.dlgSinge, wx.wxID_ANY, "Video:", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
UI.lblVideo:Wrap( -1 )
UI.fgSizer1:Add( UI.lblVideo, 0, wx.wxALIGN_CENTER_VERTICAL + wx.wxALIGN_RIGHT + wx.wxALL + wx.wxEXPAND, 5 )
UI.filePickVideo = wx.wxFilePickerCtrl( UI.dlgSinge, wx.wxID_ANY, "", "Select a file", "*.*", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFLP_DEFAULT_STYLE + wx.wxFLP_FILE_MUST_EXIST + wx.wxFLP_OPEN + wx.wxFLP_USE_TEXTCTRL )
UI.fgSizer1:Add( UI.filePickVideo, 0, wx.wxALL + wx.wxEXPAND, 5 )
UI.lblOptions = wx.wxStaticText( UI.dlgSinge, wx.wxID_ANY, "Options:", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
UI.lblOptions:Wrap( -1 )
UI.fgSizer1:Add( UI.lblOptions, 0, wx.wxALIGN_CENTER_VERTICAL + wx.wxALIGN_RIGHT + wx.wxALL, 5 )
UI.txtOptions = wx.wxTextCtrl( UI.dlgSinge, wx.wxID_ANY, "", wx.wxDefaultPosition, wx.wxDefaultSize, 0 )
UI.fgSizer1:Add( UI.txtOptions, 0, wx.wxALL + wx.wxEXPAND, 5 )
UI.fgSizer1:Add( 0, 20, 1, wx.wxEXPAND, 5 )
UI.fgSizer1:Add( 0, 20, 1, wx.wxEXPAND, 5 )
UI.fgSizer1:Add( 0, 0, 1, wx.wxEXPAND, 5 )
UI.m_sdbSizer1 = wx.wxStdDialogButtonSizer()
UI.m_sdbSizer1OK = wx.wxButton( UI.dlgSinge, wx.wxID_OK, "" )
UI.m_sdbSizer1:AddButton( UI.m_sdbSizer1OK )
UI.m_sdbSizer1Cancel = wx.wxButton( UI.dlgSinge, wx.wxID_CANCEL, "" )
UI.m_sdbSizer1:AddButton( UI.m_sdbSizer1Cancel )
UI.m_sdbSizer1:Realize();
UI.fgSizer1:Add( UI.m_sdbSizer1, 1, wx.wxEXPAND, 5 )
UI.fgSizer1:Add( 0, 20, 1, wx.wxEXPAND, 5 )
UI.fgSizer1:Add( 0, 20, 1, wx.wxEXPAND, 5 )
UI.dlgSinge:SetSizer( UI.fgSizer1 )
UI.dlgSinge:Layout()
UI.fgSizer1:Fit( UI.dlgSinge )
UI.dlgSinge:Centre( wx.wxBOTH )
-- Don't let users type in filename boxes
UI.filePickSinge:GetTextCtrl():Connect( wx.wxEVT_KEY_DOWN, function(event) end )
UI.filePickScript:GetTextCtrl():Connect( wx.wxEVT_KEY_DOWN, function(event) end )
UI.filePickVideo:GetTextCtrl():Connect( wx.wxEVT_KEY_DOWN, function(event) end )
settings = settings or {}
if settings.singe then UI.filePickSinge:GetTextCtrl():SetValue(settings.singe) end
if settings.script then UI.filePickScript:GetTextCtrl():SetValue(settings.script) end
if settings.video then UI.filePickVideo:GetTextCtrl():SetValue(settings.video) end
if settings.options then UI.txtOptions:SetValue(settings.options) end
UI.m_sdbSizer1Cancel:Connect( wx.wxEVT_COMMAND_BUTTON_CLICKED, function(event)
event:Skip()
end )
UI.m_sdbSizer1OK:Connect( wx.wxEVT_COMMAND_BUTTON_CLICKED, function(event)
settings.singe = UI.filePickSinge:GetTextCtrl():GetValue()
settings.script = UI.filePickScript:GetTextCtrl():GetValue()
settings.video = UI.filePickVideo:GetTextCtrl():GetValue()
settings.options = UI.txtOptions:GetValue()
self:SetSettings(settings)
event:Skip()
end )
wx.wxGetApp():SetTopWindow(UI.dlgSinge)
UI.dlgSinge:ShowModal()
UI.dlgSinge:Destroy()
end
local function singeEnd()
ide:Print("\n"..line)
end
local function singeOutput(s)
ide:GetOutput():Write(s)
end
local function startSinge(self)
local message = ""
local cdir = wx.wxFileName.GetCwd()
local wdir
local launch
local env
local ok
-- File pickers leave empty strings behind, so test for those as well as nil.
if settings then
if not settings.singe or settings.singe == "" then message = "You must specify the Singe executable in the Singe configuration." end
if not settings.script or settings.script == "" then message = "You must specify the script to run in the Singe configuration." end
if not settings.video or settings.video == "" then message = "You must specify the video to use in the Singe configuration." end
else
message = "Please configure Singe first."
end
if message ~= "" then
wx.wxMessageBox(message, "Uh Oh!", wx.wxICON_ERROR + wx.wxOK + wx.wxCENTRE)
return
end
-- Run from the Singe directory with paths made relative to it.
wdir = settings.singe:match("(.*[/\\])") or "./"
launch = quote(settings.singe) .. ' ' .. (settings.options or "") .. ' -v ' .. quote(settings.video) .. ' ' .. quote(settings.script)
launch = launch:gsub(escapePattern(wdir), "./", 1):gsub(escapePattern(wdir), "")
wdir = wx.wxFileName.DirName(wdir):GetFullPath()
ide:Print(line)
ide:Print(launch)
-- ZeroBrane's own LUA_CPATH would leak into Singe's embedded Lua.
ok, env = wx.wxGetEnv('LUA_CPATH')
wx.wxUnsetEnv('LUA_CPATH')
ide:ExecuteCommand(launch, wdir, singeOutput, singeEnd)
if ok then
wx.wxSetEnv('LUA_CPATH', env)
end
end
return {
name = "Singe Integration",
description = "Adds menu items and a toolbar button to assist with debugging Singe games.",
author = "Scott Duensing",
version = 1.01,
dependencies = "1.0",
onRegister = function(self)
local menu = ide:FindTopMenu("&Project")
menu:Append(id, "Launch Singe")
ide:GetMainFrame():Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function() startSinge(self) end)
menu:Append(configId, "Singe Configuration...")
ide:GetMainFrame():Connect(configId, wx.wxEVT_COMMAND_MENU_SELECTED, function() configureSinge(self) end)
settings = self:GetSettings()
local tb = ide:GetToolBar()
tool = tb:AddTool(id, "Singe"..KSC(id), wx.wxBitmap({
"16 16 136 2",
" c None",
". c #374D34", "+ c #375137", "@ c #344D33", "# c #39503B",
"$ c #445A49", "% c #3F5543", "& c #354A31", "* c #395137",
"= c #385138", "- c #354E35", "; c #3D5441", "> c #77886B",
", c #9AA880", "' c #829271", ") c #405646", "! c #314730",
"~ c #344A31", "{ c #385137", "] c #3A5239", "^ c #374D37",
"/ c #889674", "( c #CFD9A2", "_ c #D3DDA4", ": c #C1CB98",
"< c #4A5F4E", "[ c #3D5346", "} c #394E41", "| c #344C36",
"1 c #395238", "2 c #374F36", "3 c #435743", "4 c #BFCA97",
"5 c #C0CA97", "6 c #BDC796", "7 c #95A27D", "8 c #3D5341",
"9 c #51695C", "0 c #526A5B", "a c #4D6456", "b c #39503D",
"c c #576850", "d c #CAD5A0", "e c #BBC696", "f c #849272",
"g c #435742", "h c #354C35", "i c #3E5447", "j c #4A6154",
"k c #425747", "l c #445948", "m c #496051", "n c #3D5340",
"o c #475B41", "p c #4C6047", "q c #394D38", "r c #506045",
"s c #85926D", "t c #344C35", "u c #324933", "v c #364D36",
"w c #374E37", "x c #375037", "y c #39533A", "z c #395339",
"A c #4B5E42", "B c #3E543A", "C c #3D523A", "D c #2B412D",
"E c #314832", "F c #314831", "G c #365037", "H c #375237",
"I c #324B32", "J c #5F7052", "K c #828D69", "L c #324931",
"M c #354D34", "N c #365036", "O c #314A31", "P c #364E35",
"Q c #334A32", "R c #405440", "S c #2C442D", "T c #375238",
"U c #385238", "V c #2F472F", "W c #29402A", "X c #324932",
"Y c #273D28", "Z c #30472F", "` c #334830", " . c #293D27",
".. c #2B422B", "+. c #365137", "@. c #314A32", "#. c #2B422C",
"$. c #324A32", "%. c #30452F", "&. c #213522", "*. c #1D321F",
"=. c #3D563B", "-. c #385338", ";. c #385237", ">. c #314931",
",. c #29412B", "'. c #273D29", "). c #2E432D", "!. c #4B6045",
"~. c #98A57C", "{. c #395338", "]. c #273E2A", "^. c #2E422D",
"/. c #697959", "(. c #BAC696", "_. c #3C533A", ":. c #2F4730",
"<. c #2E472F", "[. c #304830", "}. c #50634A", "|. c #475D43",
"1. c #273E28", "2. c #3E503A", "3. c #96A27C", "4. c #3A543A",
"5. c #B2BD90", "6. c #B7C294", "7. c #55644A", "8. c #485B44",
"9. c #395239", "0. c #395138", "a. c #385036", "b. c #3A5037",
"c. c #384F36", "d. c #828F6D", "e. c #5B694D",
" . + @ + # $ % ",
" & * = - ; > , ' ) ",
" ! ~ { ] ^ / ( _ : < ",
" [ } | ] 1 2 3 4 5 6 7 8 ",
" 9 0 0 a b 1 2 c d e f g h ",
" i j k l m n h o p q r s t ",
" u v w w x y z z A B C ",
" D E x - F - G H I J K ",
" L M - + N + O P Q R ",
" S + T U T I V W X Y ",
"Z ` ...{ +.+.H H @...#.$.W ",
"%.&.*.@ =.H T U -.;.N >.U ,. ",
" '.).!.~.-.T H -.-.{.1 { ]. ",
" ^./.(._.:.<.[.}.|._.1. ",
" 2.3.J = T U 4.5.6.7. ",
" 8.9.0.a.b.c.d.e. "
}), "Launch Singe")
tb:Realize()
end,
onUnRegister = function(self)
local tb = ide:GetToolBar()
tb:DeleteTool(tool)
tb:Realize()
ide:RemoveMenuItem(id)
end,
}

View file

@ -21,8 +21,22 @@
*
--]]
editor.specmap.singe = 'lua'
-- An example of ZeroBrane Studio's user.lua for working on Singe games (Edit > Preferences >
-- Settings: User). The line that matters loads the Singe package from the folder Singe unpacks it
-- into, which stays current as Singe is updated; point it at your own Singe folder and remove the
-- dashes.
-- package "/path/to/Singe/Singe/ZeroBrane/singe.lua"
-- Where Singe is, when it is not in the game's folder, a folder above it, or on the PATH.
-- path.singe = "/path/to/Singe/Singe-v3.00-Linux-x86_64"
-- --reload is passed unless this says otherwise, and the Reference is found beside Singe unless
-- this names it.
-- singe = { reload = false, reference = "/path/to/Singe/Singe-Docs-v3.00/Reference.html" }
-- Editor settings to match Singe's own scripts.
editor.tabwidth = 4
editor.usetabs = true
editor.usewrap = false