From f6d63b6eb0c37c8e51b95733a407ac9315f44052 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 2 Sep 2026 16:54:23 -0500 Subject: [PATCH] Working on 2.20. Or maybe it'll be 3.0. --- .gitattributes | 3 + .gitignore | 11 +- CHANGELOG | 124 +- CMakeLists.txt | 521 +- INSTALL | 112 +- LICENSES | 16 +- assets/Framework.singe | 1027 +- assets/Manual.lyx | 2467 ----- assets/Menu.singe | 39 +- assets/Service.singe | 6 +- assets/controls.cfg | 10 +- build-all.sh | 277 +- build-docs.sh | 26 + cmake/embed.cmake | 61 + cmake/runToFile.cmake | 8 + docs/Manual.adoc | 3454 +++++++ patches/ActionMax/Emulator.singe | 111 +- patches/README | 18 + patches/daitarn_3_singe/Script/toolbox.singe | 662 +- src/common.h | 9 +- src/embedded.h | 2 +- src/frameFile.c | 404 +- src/frameFile.h | 11 +- src/main.c | 1724 ++-- src/main.h | 17 +- src/singe.c | 9593 ++++++++---------- src/singe.h | 17 +- src/singe.rc | 24 - src/singe.rc.in | 24 + src/util.c | 454 +- src/util.h | 54 +- src/version.h.in | 38 + src/videoPlayer.c | 1249 +-- src/videoPlayer.h | 50 +- zbstudio/{packages => }/Singe.fbp | 0 zbstudio/packages/singetoolbar.lua | 60 +- zbstudio/user.lua | 23 + 37 files changed, 11583 insertions(+), 11123 deletions(-) delete mode 100644 assets/Manual.lyx create mode 100755 build-docs.sh create mode 100644 cmake/embed.cmake create mode 100644 cmake/runToFile.cmake create mode 100644 docs/Manual.adoc create mode 100644 patches/README delete mode 100644 src/singe.rc create mode 100644 src/singe.rc.in create mode 100644 src/version.h.in rename zbstudio/{packages => }/Singe.fbp (100%) diff --git a/.gitattributes b/.gitattributes index b9247734b..3145ba20f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,6 +25,9 @@ *.exe filter=lfs diff=lfs merge=lfs -text *.dll filter=lfs diff=lfs merge=lfs -text *.dat filter=lfs diff=lfs merge=lfs -text +# Singe game and menu descriptors are Lua text, not binary. +games.dat !filter !diff !merge text +menu.dat !filter !diff !merge text *.ttf filter=lfs diff=lfs merge=lfs -text # diff --git a/.gitignore b/.gitignore index 89316aba9..02320104b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,18 @@ *~ **/.builddir/ **/.git-hidden/ -**/.github/ +thirdparty/**/.github/ cmake-build-debug/ *.log *.user temp/ *# +build/ +CMakeCache.txt +CMakeFiles/ +*.a +*.o +docs/Manual.html +docs/Manual.pdf +docs/.asciidoctor/ +.claude/settings.local.json diff --git a/CHANGELOG b/CHANGELOG index e3eef63cc..56ee1790e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,121 @@ +SINGE 2.20 +========== + + Unreleased + + +API Changes +----------- + +- The sprite handle is now the FIRST argument of spriteDraw(), spriteLoop(), + spriteQuality(), spriteRotate(), spriteRotateAndScale(), spriteScale(), and + spriteSetFrame(), matching the video*() functions. Move the last argument + of each call to the front. Games that cannot be edited can set + SINGE_LEGACY_SPRITE_ARGS = true before loading the framework, or add + LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from + Singe 2.10" in the manual. + +- mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old + mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are + now aliases defined in Framework.singe. + +- Handles, counts, and frame numbers are returned as Lua integers. + videoIsPlaying() returns a boolean. mouseSetMode() returns nothing. + +- onControllerMoved() receives the controller index (0 to 3), not SDL's + instance ID. onMouseMoved() in MANY_MOUSE mode receives real relative + motion in its third and fourth arguments. + +- Runtime errors inside callbacks now end the game with a traceback, the way + argument errors always have. + +- Held keys no longer auto-repeat in MODE_NORMAL. + +- The engine parks the disc on frame 1 before the script runs; the framework + no longer seeks when it is loaded, so threaded games keep their own + startup positioning. + +- The pause key now pauses the whole game. The engine freezes the script + (no callbacks, no singeMain), holds sound completions, releases and later + re-presses held inputs, and draws a PAUSED indicator. It acts on the key + press, and SWITCH_PAUSE reaches the script only when the key is disabled + with singeSetPauseKeyEnabled(false). singeSetPauseFlag() still pauses + media without freezing the script. Gamepad and mouse buttons mapped to + pause, quit, screenshot, or grab now work in MODE_FULL too; keyboard + mappings stay raw there. + +- Constants that used to be duplicated in Framework.singe (SWITCH_*, + FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, RENDER_*, SOUND_ERROR_*) are + now defined by the engine. New: DISC_STOPPED/DISC_PLAYING/DISC_PAUSED + for discGetState(), SINGE_VERSION_MAJOR/MINOR/STRING, and the SINGE_* + input code layout that Framework.singe builds GAMEPAD_N and MOUSE_N from. + + +Fixes +----- + +- controllerGetButton() never worked; it rejected every framework button + code. + +- The five argument form of spriteDraw() always failed. + +- spriteSetFrame() on a still image crashed. + +- Unloading the selected font left later fontPrint() calls using freed + memory. + +- overlayCircle() drew around the origin instead of the requested center. + +- overlaySetResolution() broke mouse coordinates and vldpGetPixel() because + the overlay scale was computed with integer division. + +- mouseGetPosition() always returned 0,0 and controllerGetAxis() read the + wrong controller because the axis cache was indexed two different ways. + +- Analog stick release events fired with a stale input code. Controllers + that were unplugged and replugged shifted their codes. + +- The -o/--audio option was lost when the configuration was copied. + +- --option=value forms of numeric options, and options after the script + name, were not accepted. + +- Running "singe game.singe" from the game's own directory wrote its data + to the filesystem root. + +- Framefile playback never advanced past the first segment, and switching + segments dropped the selected audio track. + +- Video timing accumulated main loop latency, drifting behind the audio. + Audio is now the master clock: the picture follows the samples the sound + device has actually consumed, so the two cannot drift apart and the + mixer's buffering latency is accounted for. Silent videos follow the wall + clock. The first frame after a seek was shown one update late. + +- Video without an audio track crashed on the first update. + +- The video audio stream was shared with the mixer thread without a lock. + +- onSoundCompleted was called from the audio thread. It is now delivered + from the game loop. + +- Screenshots read the frame buffer through the window surface, which SDL + forbids alongside a renderer, and could corrupt memory on shutdown. + +- .patch archives crashed the installer. Archives without an explicit top + level directory entry were rejected. Extraction now refuses paths that + escape the installation directory. + +- The Sinden gun arguments could overflow the configuration structure. + +- Windows builds now carry the icon and version resource, with the version + number encoded correctly. + +- The build no longer depends on lyx, upx, or xxd; embedded resources, the + version header, and the manual are generated by CMake. The manual is + embedded again and extracted as Singe/Manual.pdf. + + SINGE 2.10 ========== @@ -8,8 +126,8 @@ New Features ------------ - spriteDraw() now has two more forms. In addition to being able to draw - regular sprites and streteched sprites it can now draw both using the - sprite's center as the anchor instead of the upper right. This is highly + regular sprites and stretched sprites it can now draw both using the + sprite's center as the anchor instead of the upper left. This is highly useful when dealing with rotated sprites. - Animated sprites! Both animated GIF and WEBP images are supported. @@ -38,7 +156,7 @@ New Features directly into the script context. Now there is a proper Lua module search handler. Scripts can properly require() modules from the following: - Lua Standard Library - - Lua Auxillary Library + - Lua Auxiliary Library - LuaFileSystem - LuaSocket - LuaSec diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a878e68e..84887fa35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,8 +20,13 @@ cmake_minimum_required(VERSION 3.22) -set(CMAKE_CXX_STANDARD 17) -project(singe2) +project(singe2 VERSION 2.20 LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# The copyright end year tracks the build date so nothing has to be hand edited. +string(TIMESTAMP SINGE_COPYRIGHT_END_YEAR "%Y") if(NOT DEFINED KANGAROO_OS) @@ -31,201 +36,283 @@ if(NOT DEFINED KANGAROO_ARCH) set(KANGAROO_ARCH x86_64) endif() +# Where build-all.sh installed the third party libraries for this platform. set(BUILD_DIR ${CMAKE_SOURCE_DIR}/.builddir/${KANGAROO_OS}/${KANGAROO_ARCH}) +set(GENERATED_DIR ${CMAKE_BINARY_DIR}/generated) +file(MAKE_DIRECTORY ${GENERATED_DIR}) +# ===== Host tools needed to generate embedded resources ===== + +find_program(IMAGEMAGICK NAMES magick convert) +if(NOT IMAGEMAGICK) + message(FATAL_ERROR "ImageMagick (magick or convert) is required to render the embedded images.") +endif() +find_program(FFMPEG_TOOL NAMES ffmpeg) +if(NOT FFMPEG_TOOL) + message(FATAL_ERROR "ffmpeg is required to build the menu background video.") +endif() +find_program(LUA_TOOL NAMES lua5.4 lua) +if(NOT LUA_TOOL) + message(FATAL_ERROR "A Lua interpreter is required to generate the LuaSec option table.") +endif() +find_program(ASCIIDOCTOR_PDF NAMES asciidoctor-pdf) +if(NOT ASCIIDOCTOR_PDF) + message(FATAL_ERROR "asciidoctor-pdf is required to build the embedded manual (gem install asciidoctor-pdf rouge).") +endif() +find_program(ASCIIDOCTOR NAMES asciidoctor) + + +# ===== Generated sources ===== + +# Version header and Windows resources come from the project() line above. +configure_file(src/version.h.in ${GENERATED_DIR}/version.h @ONLY) +configure_file(src/singe.rc.in ${GENERATED_DIR}/singe.rc @ONLY) + +# LuaSec needs its headers reachable as . +file(GLOB LUASOCKET_HEADERS thirdparty/luasocket/src/*.h) +file(COPY ${LUASOCKET_HEADERS} DESTINATION ${GENERATED_DIR}/include/luasocket) + +# LuaSec option table generated from the OpenSSL headers that were built. +add_custom_command( + OUTPUT ${GENERATED_DIR}/luasec_options.c + COMMAND ${CMAKE_COMMAND} -DOUTPUT=${GENERATED_DIR}/luasec_options.c -DCOMMAND=${LUA_TOOL} "-DARGS=${CMAKE_SOURCE_DIR}/thirdparty/luasec/src/options.lua;-g;${BUILD_DIR}/include/openssl/ssl.h" -P ${CMAKE_SOURCE_DIR}/cmake/runToFile.cmake + DEPENDS thirdparty/luasec/src/options.lua ${BUILD_DIR}/include/openssl/ssl.h cmake/runToFile.cmake + COMMENT "Generating LuaSec options" + VERBATIM +) + +# Embeds a file as a C array. The symbol name is derived from the file name, as xxd does. +set(EMBEDDED_HEADERS) +function(singeEmbed input output prefix) + get_filename_component(name ${input} NAME) + string(MAKE_C_IDENTIFIER "${prefix}${name}" symbol) + get_filename_component(guardName ${output} NAME) + string(MAKE_C_IDENTIFIER "${guardName}" guard) + string(TOUPPER "${guard}" guard) + add_custom_command( + OUTPUT ${output} + COMMAND ${CMAKE_COMMAND} -DINPUT=${input} -DOUTPUT=${output} -DGUARD=${guard} -DSYMBOL=${symbol} -DCOPYRIGHT_END_YEAR=${SINGE_COPYRIGHT_END_YEAR} -P ${CMAKE_SOURCE_DIR}/cmake/embed.cmake + DEPENDS ${input} cmake/embed.cmake + COMMENT "Embedding ${name}" + VERBATIM + ) + set(EMBEDDED_HEADERS ${EMBEDDED_HEADERS} ${output} PARENT_SCOPE) +endfunction() + +# Renders a layered image to a flattened PNG and embeds it. +function(singeEmbedImage name) + add_custom_command( + OUTPUT ${GENERATED_DIR}/${name}.png + COMMAND ${IMAGEMAGICK} ${CMAKE_SOURCE_DIR}/assets/${name}.xcf -background "rgba(0,0,0,0)" -flatten ${GENERATED_DIR}/${name}.png + DEPENDS assets/${name}.xcf + COMMENT "Rendering ${name}.png" + VERBATIM + ) + singeEmbed(${GENERATED_DIR}/${name}.png ${GENERATED_DIR}/${name}.h "") + set(EMBEDDED_HEADERS ${EMBEDDED_HEADERS} PARENT_SCOPE) +endfunction() + +# Embeds a Lua module from the third party tree. +function(singeEmbedLua path prefix) + get_filename_component(name ${path} NAME_WE) + singeEmbed(${CMAKE_SOURCE_DIR}/${path} ${GENERATED_DIR}/${prefix}${name}_lua.h "${prefix}") + set(EMBEDDED_HEADERS ${EMBEDDED_HEADERS} PARENT_SCOPE) +endfunction() + +# Images +singeEmbedImage(font) +singeEmbedImage(icon) +singeEmbedImage(kangarooPunchLogo) +singeEmbedImage(singeLogo) +singeEmbedImage(laserDisc) +singeEmbedImage(magnifyingGlass) +singeEmbedImage(indexing) + +# Windows icon for the resource file. +add_custom_command( + OUTPUT ${GENERATED_DIR}/icon.ico + COMMAND ${IMAGEMAGICK} ${GENERATED_DIR}/icon.png -define icon:auto-resize=64,48,32,16 ${GENERATED_DIR}/icon.ico + DEPENDS ${GENERATED_DIR}/icon.png + COMMENT "Rendering icon.ico" + VERBATIM +) + +# Support files extracted at first run. +singeEmbed(${CMAKE_SOURCE_DIR}/assets/Framework.singe ${GENERATED_DIR}/Framework_singe.h "") +singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "") +singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "") +singeEmbed(${CMAKE_SOURCE_DIR}/assets/FreeSansBold.ttf ${GENERATED_DIR}/FreeSansBold_ttf.h "") + +# Menu background video: two clips cropped to 4:3, scaled to 720x480, and joined. +file(WRITE ${GENERATED_DIR}/menuBackground.txt "file ${GENERATED_DIR}/menuBackground1.mkv\nfile ${GENERATED_DIR}/menuBackground2.mkv\n") +add_custom_command( + OUTPUT ${GENERATED_DIR}/menuBackground.mkv + COMMAND ${FFMPEG_TOOL} -y -loglevel error -i "${CMAKE_SOURCE_DIR}/assets/Singe Engine Intro.mpg" -filter:v "crop=ih/3*4:ih,scale=720:480" -c:v libx264 -c:a aac -f matroska ${GENERATED_DIR}/menuBackground1.mkv + COMMAND ${FFMPEG_TOOL} -y -loglevel error -i ${CMAKE_SOURCE_DIR}/assets/180503_01_PurpleGrid.mp4 -filter:v "crop=ih/3*4:ih,scale=720:480" -c:v libx264 -c:a aac -f matroska ${GENERATED_DIR}/menuBackground2.mkv + COMMAND ${FFMPEG_TOOL} -y -loglevel error -f concat -safe 0 -i ${GENERATED_DIR}/menuBackground.txt -c copy ${GENERATED_DIR}/menuBackground.mkv + DEPENDS "assets/Singe Engine Intro.mpg" assets/180503_01_PurpleGrid.mp4 + COMMENT "Building menuBackground.mkv" + VERBATIM +) +singeEmbed(${GENERATED_DIR}/menuBackground.mkv ${GENERATED_DIR}/menuBackground_mkv.h "") + +# Manual, rendered from the AsciiDoc source and shipped inside the binary. +add_custom_command( + OUTPUT ${GENERATED_DIR}/Manual.pdf + COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Manual.adoc -o ${GENERATED_DIR}/Manual.pdf + DEPENDS docs/Manual.adoc + COMMENT "Rendering Manual.pdf" + VERBATIM +) +singeEmbed(${GENERATED_DIR}/Manual.pdf ${GENERATED_DIR}/Manual_pdf.h "") + +# Lua libraries +singeEmbedLua(thirdparty/luasocket/src/ftp.lua "") +singeEmbedLua(thirdparty/luasocket/src/headers.lua "") +singeEmbedLua(thirdparty/luasocket/src/http.lua "") +singeEmbedLua(thirdparty/luasocket/src/ltn12.lua "") +singeEmbedLua(thirdparty/luasocket/src/mbox.lua "") +singeEmbedLua(thirdparty/luasocket/src/mime.lua "") +singeEmbedLua(thirdparty/luasocket/src/smtp.lua "") +singeEmbedLua(thirdparty/luasocket/src/socket.lua "") +singeEmbedLua(thirdparty/luasocket/src/tp.lua "") +singeEmbedLua(thirdparty/luasocket/src/url.lua "") +singeEmbedLua(thirdparty/luasec/src/https.lua "") +singeEmbedLua(thirdparty/luasec/src/ssl.lua "") +singeEmbedLua(thirdparty/librs232/bindings/lua/rs232.lua "") +singeEmbedLua(thirdparty/copas/src/copas.lua "") +singeEmbedLua(thirdparty/copas/src/copas/ftp.lua "copas_") +singeEmbedLua(thirdparty/copas/src/copas/http.lua "copas_") +singeEmbedLua(thirdparty/copas/src/copas/smtp.lua "copas_") +singeEmbedLua(thirdparty/copas/src/copas/lock.lua "copas_") +singeEmbedLua(thirdparty/copas/src/copas/queue.lua "copas_") +singeEmbedLua(thirdparty/copas/src/copas/semaphore.lua "copas_") +singeEmbedLua(thirdparty/copas/src/copas/timer.lua "copas_") +singeEmbedLua(thirdparty/binaryheap.lua/src/binaryheap.lua "") +singeEmbedLua(thirdparty/timerwheel.lua/src/timerwheel/timerwheel.lua "") +singeEmbedLua(thirdparty/json.lua/json.lua "") + +# Optional HTML manual for browsing: cmake --build . --target docs +if(ASCIIDOCTOR) + add_custom_target(docs + COMMAND ${ASCIIDOCTOR} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Manual.adoc -o ${CMAKE_BINARY_DIR}/Manual.html + DEPENDS ${GENERATED_DIR}/Manual.pdf + COMMENT "Rendering Manual.html" + VERBATIM + ) +endif() + + +# ===== Sources ===== + set(SINGE_SOURCE + src/common.h + src/embedded.h + src/frameFile.c + src/frameFile.h + src/main.c src/main.h src/singe.c src/singe.h - src/util.h - src/embedded.h - src/frameFile.h - src/main.c - src/common.h - src/videoPlayer.c src/stddclmr.h - src/frameFile.c - src/videoPlayer.h src/util.c + src/util.h + src/videoPlayer.c + src/videoPlayer.h ) - set(ARG_PARSER_SOURCE thirdparty/arg_parser/carg_parser.c thirdparty/arg_parser/carg_parser.h ) - -set(JBIG_SOURCE - thirdparty/jbigkit/libjbig/jbig.c - thirdparty/jbigkit/libjbig/jbig.h - thirdparty/jbigkit/libjbig/jbig_ar.c - thirdparty/jbigkit/libjbig/jbig_ar.h -) - - -set(LUA_SOURCE - thirdparty/lua/src/lmem.h - thirdparty/lua/src/ltm.h - thirdparty/lua/src/lutf8lib.c - thirdparty/lua/src/ldo.h - thirdparty/lua/src/ldump.c - thirdparty/lua/src/lstring.h - thirdparty/lua/src/lparser.h - thirdparty/lua/src/lcorolib.c - thirdparty/lua/src/loslib.c - thirdparty/lua/src/lparser.c - thirdparty/lua/src/ltablib.c - thirdparty/lua/src/ltable.c - thirdparty/lua/src/ljumptab.h - thirdparty/lua/src/luaconf.h - thirdparty/lua/src/lstate.c - thirdparty/lua/src/lobject.h - thirdparty/lua/src/lstate.h - thirdparty/lua/src/ldo.c - thirdparty/lua/src/lfunc.c - thirdparty/lua/src/lmathlib.c - thirdparty/lua/src/lua.h - thirdparty/lua/src/lauxlib.h - thirdparty/lua/src/ltm.c - thirdparty/lua/src/lstrlib.c - thirdparty/lua/src/lapi.h - thirdparty/lua/src/lopcodes.c - thirdparty/lua/src/lvm.h - thirdparty/lua/src/linit.c - thirdparty/lua/src/lobject.c - thirdparty/lua/src/ldebug.c - thirdparty/lua/src/lctype.c - thirdparty/lua/src/lgc.c - thirdparty/lua/src/lzio.h - thirdparty/lua/src/lgc.h - thirdparty/lua/src/lctype.h - thirdparty/lua/src/lopcodes.h - thirdparty/lua/src/llimits.h - thirdparty/lua/src/lprefix.h - thirdparty/lua/src/llex.h - thirdparty/lua/src/lundump.c - thirdparty/lua/src/lbaselib.c - thirdparty/lua/src/loadlib.c - thirdparty/lua/src/ldblib.c - thirdparty/lua/src/ldebug.h - thirdparty/lua/src/lundump.h - thirdparty/lua/src/lopnames.h - thirdparty/lua/src/ltable.h - thirdparty/lua/src/lmem.c - thirdparty/lua/src/lcode.h - thirdparty/lua/src/lua.hpp - thirdparty/lua/src/lauxlib.c - thirdparty/lua/src/liolib.c - thirdparty/lua/src/lapi.c - thirdparty/lua/src/lcode.c - thirdparty/lua/src/llex.c - thirdparty/lua/src/lstring.c - thirdparty/lua/src/lvm.c - thirdparty/lua/src/lualib.h - thirdparty/lua/src/lfunc.h - thirdparty/lua/src/lzio.c -) - +file(GLOB LUA_SOURCE thirdparty/lua/src/*.c thirdparty/lua/src/*.h) +list(FILTER LUA_SOURCE EXCLUDE REGEX "/(lua|luac|onelua)\\.c$") set(LUA_FILESYSTEM_SOURCE thirdparty/luafilesystem/src/lfs.c thirdparty/luafilesystem/src/lfs.h ) - set(LUA_SOCKET_SOURCE - thirdparty/luasocket/src/luasocket.h - thirdparty/luasocket/src/luasocket.c - thirdparty/luasocket/src/timeout.h - thirdparty/luasocket/src/timeout.c - thirdparty/luasocket/src/buffer.h - thirdparty/luasocket/src/buffer.c - thirdparty/luasocket/src/io.h - thirdparty/luasocket/src/io.c - thirdparty/luasocket/src/auxiliar.h thirdparty/luasocket/src/auxiliar.c - thirdparty/luasocket/src/compat.h + thirdparty/luasocket/src/auxiliar.h + thirdparty/luasocket/src/buffer.c + thirdparty/luasocket/src/buffer.h thirdparty/luasocket/src/compat.c - thirdparty/luasocket/src/options.h - thirdparty/luasocket/src/options.c - thirdparty/luasocket/src/inet.h - thirdparty/luasocket/src/inet.c - thirdparty/luasocket/src/except.h + thirdparty/luasocket/src/compat.h thirdparty/luasocket/src/except.c - thirdparty/luasocket/src/select.h - thirdparty/luasocket/src/select.c - thirdparty/luasocket/src/tcp.h - thirdparty/luasocket/src/tcp.c - thirdparty/luasocket/src/udp.h - thirdparty/luasocket/src/udp.c - thirdparty/luasocket/src/mime.h + thirdparty/luasocket/src/except.h + thirdparty/luasocket/src/inet.c + thirdparty/luasocket/src/inet.h + thirdparty/luasocket/src/io.c + thirdparty/luasocket/src/io.h + thirdparty/luasocket/src/luasocket.c + thirdparty/luasocket/src/luasocket.h thirdparty/luasocket/src/mime.c + thirdparty/luasocket/src/mime.h + thirdparty/luasocket/src/options.c + thirdparty/luasocket/src/options.h + thirdparty/luasocket/src/select.c + thirdparty/luasocket/src/select.h thirdparty/luasocket/src/socket.h - thirdparty/luasocket/src/pierror.h + thirdparty/luasocket/src/tcp.c + thirdparty/luasocket/src/tcp.h + thirdparty/luasocket/src/timeout.c + thirdparty/luasocket/src/timeout.h + thirdparty/luasocket/src/udp.c + thirdparty/luasocket/src/udp.h ) if(WIN32) - set(LUA_SOCKET_SOURCE - ${LUA_SOCKET_SOURCE} - thirdparty/luasocket/src/wsocket.h + list(APPEND LUA_SOCKET_SOURCE thirdparty/luasocket/src/wsocket.c + thirdparty/luasocket/src/wsocket.h ) -ELSE() - set(LUA_SOCKET_SOURCE - ${LUA_SOCKET_SOURCE} - thirdparty/luasocket/src/usocket.h - thirdparty/luasocket/src/usocket.c - thirdparty/luasocket/src/unix.h +else() + list(APPEND LUA_SOCKET_SOURCE + thirdparty/luasocket/src/serial.c thirdparty/luasocket/src/unix.c - thirdparty/luasocket/src/unixstream.h - thirdparty/luasocket/src/unixstream.c + thirdparty/luasocket/src/unix.h thirdparty/luasocket/src/unixdgram.c thirdparty/luasocket/src/unixdgram.h - thirdparty/luasocket/src/serial.c + thirdparty/luasocket/src/unixstream.c + thirdparty/luasocket/src/unixstream.h + thirdparty/luasocket/src/usocket.c + thirdparty/luasocket/src/usocket.h ) -ENDIF() - +endif() set(LUASEC_SOURCE - thirdparty/luasec/src/compat.h - thirdparty/luasec/src/config.c - ${BUILD_DIR}/generated/luasec_options.c - thirdparty/luasec/src/context.c - thirdparty/luasec/src/context.h - thirdparty/luasec/src/ec.c - thirdparty/luasec/src/ec.h - thirdparty/luasec/src/options.h - thirdparty/luasec/src/ssl.c - thirdparty/luasec/src/ssl.h - thirdparty/luasec/src/x509.c - thirdparty/luasec/src/x509.h + ${GENERATED_DIR}/luasec_options.c + thirdparty/luasec/src/compat.h + thirdparty/luasec/src/config.c + thirdparty/luasec/src/context.c + thirdparty/luasec/src/context.h + thirdparty/luasec/src/ec.c + thirdparty/luasec/src/ec.h + thirdparty/luasec/src/options.h + thirdparty/luasec/src/ssl.c + thirdparty/luasec/src/ssl.h + thirdparty/luasec/src/x509.c + thirdparty/luasec/src/x509.h ) - set(LUA_RS232_SOURCE thirdparty/librs232/bindings/lua/luars232.c thirdparty/librs232/src/rs232.c ) if(WIN32) - set(LUA_RS232_SOURCE - ${LUA_RS232_SOURCE} - thirdparty/librs232/src/rs232_windows.c - ) -ELSE() - set(LUA_RS232_SOURCE - ${LUA_RS232_SOURCE} - thirdparty/librs232/src/rs232_posix.c - ) -ENDIF() - + list(APPEND LUA_RS232_SOURCE thirdparty/librs232/src/rs232_windows.c) +else() + list(APPEND LUA_RS232_SOURCE thirdparty/librs232/src/rs232_posix.c) +endif() set(UTHASH_SOURCE thirdparty/uthash/src/uthash.h thirdparty/uthash/src/utlist.h ) - set(MANYMOUSE_SOURCE thirdparty/manymouse/linux_evdev.c thirdparty/manymouse/macosx_hidmanager.c @@ -236,17 +323,19 @@ set(MANYMOUSE_SOURCE thirdparty/manymouse/x11_xinput2.c ) - set(SDL2_GFX_SOURCE thirdparty/SDL2_gfx/SDL2_rotozoom.h thirdparty/SDL2_gfx/SDL2_rotozoom.c ) +# ===== Target ===== + add_executable(${CMAKE_PROJECT_NAME} ${SINGE_SOURCE} + ${EMBEDDED_HEADERS} + ${GENERATED_DIR}/version.h ${ARG_PARSER_SOURCE} - ${JBIG_SOURCE} ${LUA_SOURCE} ${LUA_FILESYSTEM_SOURCE} ${LUA_SOCKET_SOURCE} @@ -256,36 +345,40 @@ add_executable(${CMAKE_PROJECT_NAME} ${MANYMOUSE_SOURCE} ${SDL2_GFX_SOURCE} ) - - -# Perform pre-build operations. -#add_custom_target(BUILD_PREREQS -# COMMAND ${BUILD_DIR}-deps.sh "${CMAKE_SOURCE_DIR}" -# BYPRODUCTS -# ${CMAKE_SOURCE_DIR}/thirdparty-installed/lib/libarchive.a -#) -#add_dependencies(${CMAKE_PROJECT_NAME} BUILD_PREREQS) - - if(WIN32) - # Note: _WIN32_WINNT=0x0600 sets the minimum compatible version of - # Windows to Vista. The function inet_pton() does not exist before then. - set(DEFINE_LIST - -Dmain=SDL_main - -D_WIN32_WINNT=0x0600 - ) -else() - set(DEFINE_LIST) + enable_language(RC) + target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${GENERATED_DIR}/singe.rc ${GENERATED_DIR}/icon.ico) endif() -target_compile_options(${CMAKE_PROJECT_NAME} PUBLIC - ${DEFINE_LIST} - -DRS232_STATIC - -DFFMS_STATIC + +# Output name matches the release artifact: Singe-v2.20-Linux-x86_64 +string(SUBSTRING ${KANGAROO_OS} 0 1 osInitial) +string(SUBSTRING ${KANGAROO_OS} 1 -1 osRest) +string(TOUPPER ${osInitial} osInitial) +set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME "Singe-v${PROJECT_VERSION}-${osInitial}${osRest}-${KANGAROO_ARCH}") + +# Warnings apply to our code only; the vendored libraries are not ours to fix. +set_source_files_properties(${SINGE_SOURCE} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra") + +target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE + RS232_STATIC + FFMS_STATIC +) +if(WIN32) + # _WIN32_WINNT=0x0600 sets the minimum compatible version of Windows to Vista. + # The function inet_pton() does not exist before then. + target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE + main=SDL_main + _WIN32_WINNT=0x0600 + ) +endif() + +target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE -Wno-deprecated-declarations ) - -target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE + ${CMAKE_BINARY_DIR} + ${GENERATED_DIR}/include ${BUILD_DIR} ${BUILD_DIR}/include ${BUILD_DIR}/include/SDL2 @@ -294,30 +387,21 @@ target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC thirdparty/librs232/include ) - -target_link_directories(${CMAKE_PROJECT_NAME} PUBLIC +target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${BUILD_DIR}/lib ) -if(${KANGAROO_OS} STREQUAL "linux") - set(LIB_LIST - -lX11 - -lvdpau - ${BUILD_DIR}/lib/libjpeg.a - ${BUILD_DIR}/lib/libpng.a - -lstdc++ - ) -endif() -if(${KANGAROO_OS} STREQUAL "pi") - set(LIB_LIST - ${BUILD_DIR}/lib/libjpeg.a - ${BUILD_DIR}/lib/libpng.a - -lstdc++ - ) -endif() -if(${KANGAROO_OS} STREQUAL "macos") - set(LIB_LIST +# ===== Libraries ===== + +# Platform specific system libraries. +if(KANGAROO_OS STREQUAL "linux") + # ffmpeg's vdpau hardware context is always compiled in on X11 hosts. + set(SYSTEM_LIBS -lX11 -lvdpau) +elseif(KANGAROO_OS STREQUAL "pi") + set(SYSTEM_LIBS) +elseif(KANGAROO_OS STREQUAL "macos") + set(SYSTEM_LIBS -Wl,-framework,CoreVideo -Wl,-framework,Cocoa -Wl,-framework,IOKit @@ -335,9 +419,11 @@ if(${KANGAROO_OS} STREQUAL "macos") -lc++ ${OSXCROSS_TARGET_DIR}/darwin/libclang_rt.osx.a ) -endif() -if(${KANGAROO_OS} STREQUAL "windows") - set(LIB_LIST + if(NOT DEFINED OSXCROSS_TARGET_DIR) + message(FATAL_ERROR "OSXCROSS_TARGET_DIR must be set for macOS builds.") + endif() +elseif(KANGAROO_OS STREQUAL "windows") + set(SYSTEM_LIBS -mwindows -static -lmingw32 @@ -359,29 +445,22 @@ if(${KANGAROO_OS} STREQUAL "windows") -lbcrypt -lssp -lcrypt32 - ${BUILD_DIR}/lib/libjpeg.a - ${BUILD_DIR}/lib/libpng.a - -lstdc++ ) +else() + message(FATAL_ERROR "Unknown KANGAROO_OS: ${KANGAROO_OS}") endif() -set(LIB_LIST - ${LIB_LIST} + +# Static third party libraries built by build-all.sh. +set(STATIC_LIBS ${BUILD_DIR}/lib/libarchive.a ${BUILD_DIR}/lib/libavcodec.a ${BUILD_DIR}/lib/libavdevice.a ${BUILD_DIR}/lib/libavfilter.a ${BUILD_DIR}/lib/libavformat.a -# ${BUILD_DIR}/lib/libavif.a ${BUILD_DIR}/lib/libavutil.a -# ${BUILD_DIR}/lib/libbrotlicommon-static.a -# ${BUILD_DIR}/lib/libbrotlidec-static.a ${BUILD_DIR}/lib/libbz2_static.a -# ${BUILD_DIR}/lib/libdav1d.a ${BUILD_DIR}/lib/libffms2.a ${BUILD_DIR}/lib/libfreetype.a -# ${BUILD_DIR}/lib/libharfbuzz.a -# ${BUILD_DIR}/lib/libhwy.a -# ${BUILD_DIR}/lib/libjxl_dec.a ${BUILD_DIR}/lib/liblzma.a ${BUILD_DIR}/lib/libopus.a ${BUILD_DIR}/lib/libopusfile.a @@ -391,11 +470,9 @@ set(LIB_LIST ${BUILD_DIR}/lib/libSDL2_image.a ${BUILD_DIR}/lib/libSDL2main.a ${BUILD_DIR}/lib/libSDL2_mixer.a - ${BUILD_DIR}/lib/libSDL2_test.a ${BUILD_DIR}/lib/libSDL2_ttf.a ${BUILD_DIR}/lib/libswresample.a ${BUILD_DIR}/lib/libswscale.a -# ${BUILD_DIR}/lib/libtiff.a ${BUILD_DIR}/lib/libwavpack.a ${BUILD_DIR}/lib/libwebp.a ${BUILD_DIR}/lib/libwebpdemux.a @@ -405,15 +482,21 @@ set(LIB_LIST ${BUILD_DIR}/lib/libcrypto.a ${BUILD_DIR}/lib/libssl.a ) -target_link_libraries(${CMAKE_PROJECT_NAME} -# -Wl,--start-group - ${LIB_LIST} - ${LIB_LIST} - ${LIB_LIST} -# -Wl,--end-group - -pthread - -lm -) +if(NOT KANGAROO_OS STREQUAL "macos") + list(APPEND STATIC_LIBS + ${BUILD_DIR}/lib/libjpeg.a + ${BUILD_DIR}/lib/libpng.a + -lstdc++ + ) +endif() + +# System libraries follow the static ones so their symbols resolve for everything above. +# Apple's linker has no --start-group, so the list is repeated there instead. +if(KANGAROO_OS STREQUAL "macos") + target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} ${SYSTEM_LIBS} -pthread -lm) +else() + target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group ${SYSTEM_LIBS} -pthread -lm) +endif() install(TARGETS ${CMAKE_PROJECT_NAME}) diff --git a/INSTALL b/INSTALL index e6a393a6a..16fa963d7 100644 --- a/INSTALL +++ b/INSTALL @@ -1,67 +1,45 @@ -SINGE 2.10 -========== - -(For the latest version of this document, visit https://kangaroopunch.com!) - -Welcome to Singe! The Somewhat Interactive Nostalgic Game Engine! - - -INSTALLATION & UPGRADE -====================== - -For the initial installation, create an empty directory and place the Singe -binary inside it. Run the binary with no command line options to cause it -to generate all the files needed to make Singe work properly. For example: - - C:\Singe2\ - C:\Singe2\Singe.exe - -Once you run Singe.exe you'll see additional files and directories: - - C:\Singe2\Singe\ (Several files created in this folder.) - C:\Singe2\Menu.bat (Or .sh on UNIX-ish OSs.) - -To install games, simply unpack them and place their directory inside the -directory you created. For example, ActionMax.7z contains a folder named -"ActionMax". Place it here: - - C:\Singe2\ActionMax\ - -For all Singe 2 and later games, it will automatically appear in the menu. - -To upgrade, back up any changes you may have made to files inside the Singe -subdirectory that was generated during installation. (You really shouldn't -be changing things in there!) Delete the Singe subdirectory, Menu.bat (or -.sh) and run the new Singe binary with no command line arguments to generate -the new files. - - -COMMAND LINE -============ - ___ ___ _ _ ___ ___ -/ __|_ _| \| |/ __| __| Somewhat Interactive Nostalgic Game Engine v2.10 -\__ \| || .` | (_ | _| Copyright (c) 2006-2024 Scott C. Duensing -|___/___|_|\_|\___|___| https://KangarooPunch.com https://SingeEngine.com - -Usage: Singe-v2.10-Windows-x86_64.exe [OPTIONS] scriptName{.singe} - - -a, --aspect=N:D force aspect ratio - -c, --showcalculated show calculated framefile values for debugging - -d, --datadir=PATHNAME alternate location for written files - -e, --volume_nonvldp=PERCENT specify sound effects volume in percent - -f, --fullscreen run in full screen mode - -h, --help this display - -k, --nologos kill the splash screens - -l, --volume_vldp=PERCENT specify laserdisc volume in percent - -m, --nomouse disable mouse - -n, --nocrosshair request game not display gun crosshairs - -o, --audio=TRACK select default track for audio output - -p, --program trace Singe execution to screen and file - -s, --nosound, --mutesound mutes all sound - -t, --trace trace script execution to screen and file - -u, --stretch use ugly stretched video - -v, --framefile=FILENAME use an alternate video file - -w, --fullscreen_window run in windowed full screen mode - -x, --xresolution=VALUE specify horizontal resolution - -y, --yresolution=VALUE specify vertical resolution - -z, --noconsole zero console output +SINGE 2.20 +========== + +(For the latest version of this document, visit https://kangaroopunch.com! + The full manual is extracted to Singe/Manual.pdf on first run.) + +Welcome to Singe! The Somewhat Interactive Nostalgic Game Engine! + + +INSTALLATION & UPGRADE +====================== + +For the initial installation, create an empty directory and place the Singe +binary inside it. Run the binary with no command line options to cause it +to generate all the files needed to make Singe work properly. For example: + + C:\Singe2\ + C:\Singe2\Singe.exe + +Once you run Singe.exe you'll see additional files and directories: + + C:\Singe2\Singe\ (Several files created in this folder.) + C:\Singe2\Menu.bat (Or .sh on UNIX-ish OSs.) + +To install games, simply unpack them and place their directory inside the +directory you created. For example, ActionMax.7z contains a folder named +"ActionMax". Place it here: + + C:\Singe2\ActionMax\ + +For all Singe 2 and later games, it will automatically appear in the menu. + +To upgrade, back up any changes you may have made to files inside the Singe +subdirectory that was generated during installation. (You really shouldn't +be changing things in there!) Delete the Singe subdirectory, Menu.bat (or +.sh) and run the new Singe binary with no command line arguments to generate +the new files. + + +COMMAND LINE +============ + +Run the binary with -h (or --help) for the option summary. Every option is +described in the "Command Line Options" chapter of Singe/Manual.pdf, which +is extracted on first run. diff --git a/LICENSES b/LICENSES index 91b5ad64a..f27a9160e 100644 --- a/LICENSES +++ b/LICENSES @@ -9,16 +9,23 @@ bzip2 bzip2-1.0.6 https://sourceware.org/bzip2 copas MIT https://lunarmodules.github.io/copas ffmpeg LGPL-2.1 https://ffmpeg.org ffms2 GPL-3.0-only https://github.com/FFMS/ffms2 -jbigkit GPL-2.0 https://www.cl.cam.ac.uk/~mgk25/jbigkit +freetype FTL https://freetype.org json.lua MIT https://github.com/rxi/json.lua libarchive BSD-2-Clause https://libarchive.org +libjpeg-turbo IJG https://libjpeg-turbo.org +libogg BSD-3-Clause https://xiph.org/ogg +libpng libpng-2.0 http://www.libpng.org librs232 MIT https://github.com/srdgame/librs232 +libwebp BSD-3-Clause https://developers.google.com/speed/webp +libxmp MIT https://github.com/libxmp/libxmp lua MIT https://www.lua.org luafilesystem MIT https://lunarmodules.github.io/luafilesystem luasec MIT https://github.com/lunarmodules/luasec luasocket MIT https://lunarmodules.github.io/luasocket manymouse Zlib https://icculus.org/manymouse openssl Apache-2.0 https://www.openssl.org +opus BSD-3-Clause https://opus-codec.org +opusfile BSD-3-Clause https://opus-codec.org SDL2 Zlib https://www.libsdl.org SDL2_gfx LGPL-2.1 https://github.com/ferzkopp/SDL_gfx SDL2_image Zlib https://www.libsdl.org @@ -27,7 +34,14 @@ SDL2_ttf Zlib https://www.libsdl.org timerwheel.lua MIT https://tieske.github.io/timerwheel.lua uthash BSD-1-Clause https://troydhanson.github.io/uthash vlc GPL-2.0 https://www.videolan.org/vlc +wavpack BSD-3-Clause https://www.wavpack.com xz Public-Domain https://github.com/tukaani-project/xz zlib Zlib https://zlib.net zstd BSD-3-Clause https://facebook.github.io/zstd + +Fonts +----- + +FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont +BreatheFire (license not recorded - used only by the manual's example) diff --git a/assets/Framework.singe b/assets/Framework.singe index f33b8f4d8..fd8d68d68 100644 --- a/assets/Framework.singe +++ b/assets/Framework.singe @@ -1,535 +1,492 @@ ---[[ - * - * Singe 2 - * Copyright (C) 2006-2024 Scott Duensing - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301, USA. - * - * ---]] - - --- Singe 2.xx Features ------------------------------------------------------- - - -SINGE_FRAMEWORK_VERSION = 2.10 - -if singeGetScriptPath ~= nil then - DIR = singeGetScriptPath():match("(.*[/\\])") or "./" -end - - -function utilDeepCopy(orig) - local orig_type = type(orig) - local copy - if orig_type == 'table' then - copy = {} - for orig_key, orig_value in next, orig, nil do - copy[utilDeepCopy(orig_key)] = utilDeepCopy(orig_value) - end - setmetatable(copy, utilDeepCopy(getmetatable(orig))) - else -- number, string, boolean, etc - copy = orig - end - return copy -end - - -function utilDump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then - k = '"'..k..'"' - end - s = s .. '['..k..'] = ' .. utilDump(v) .. ',' - end - return s .. '} ' - else - return tostring(o) - end -end - - -function utilGetTableSize(t) - local count = 0 - for _, __ in pairs(t) do - count = count + 1 - end - return count -end - - -function utilTrim(s) - return (s:gsub("^%s*(.-)%s*$", "%1")) -end - - -SCANCODE = { - A = { name = "A", value = 4 }, - B = { name = "B", value = 5 }, - C = { name = "C", value = 6 }, - D = { name = "D", value = 7 }, - E = { name = "E", value = 8 }, - F = { name = "F", value = 9 }, - G = { name = "G", value = 10 }, - H = { name = "H", value = 11 }, - I = { name = "I", value = 12 }, - J = { name = "J", value = 13 }, - K = { name = "K", value = 14 }, - L = { name = "L", value = 15 }, - M = { name = "M", value = 16 }, - N = { name = "N", value = 17 }, - O = { name = "O", value = 18 }, - P = { name = "P", value = 19 }, - Q = { name = "Q", value = 20 }, - R = { name = "R", value = 21 }, - S = { name = "S", value = 22 }, - T = { name = "T", value = 23 }, - U = { name = "U", value = 24 }, - V = { name = "V", value = 25 }, - W = { name = "W", value = 26 }, - X = { name = "X", value = 27 }, - Y = { name = "Y", value = 28 }, - Z = { name = "Z", value = 29 }, - MAIN_1 = { name = "MAIN_1", value = 30 }, - MAIN_2 = { name = "MAIN_2", value = 31 }, - MAIN_3 = { name = "MAIN_3", value = 32 }, - MAIN_4 = { name = "MAIN_4", value = 33 }, - MAIN_5 = { name = "MAIN_5", value = 34 }, - MAIN_6 = { name = "MAIN_6", value = 35 }, - MAIN_7 = { name = "MAIN_7", value = 36 }, - MAIN_8 = { name = "MAIN_8", value = 37 }, - MAIN_9 = { name = "MAIN_9", value = 38 }, - MAIN_0 = { name = "MAIN_0", value = 39 }, - RETURN = { name = "RETURN", value = 40 }, - ESCAPE = { name = "ESCAPE", value = 41 }, - BACKSPACE = { name = "BACKSPACE", value = 42 }, - TAB = { name = "TAB", value = 43 }, - SPACE = { name = "SPACE", value = 44 }, - MINUS = { name = "MINUS", value = 45 }, - EQUALS = { name = "EQUALS", value = 46 }, - LEFTBRACKET = { name = "LEFTBRACKET", value = 47 }, - RIGHTBRACKET = { name = "RIGHTBRACKET", value = 48 }, - BACKSLASH = { name = "BACKSLASH", value = 49 }, - NONUSHASH = { name = "NONUSHASH", value = 50 }, - SEMICOLON = { name = "SEMICOLON", value = 51 }, - APOSTROPHE = { name = "APOSTROPHE", value = 52 }, - GRAVE = { name = "GRAVE", value = 53 }, - COMMA = { name = "COMMA", value = 54 }, - PERIOD = { name = "PERIOD", value = 55 }, - SLASH = { name = "SLASH", value = 56 }, - CAPSLOCK = { name = "CAPSLOCK", value = 57 }, - F1 = { name = "F1", value = 58 }, - F2 = { name = "F2", value = 59 }, - F3 = { name = "F3", value = 60 }, - F4 = { name = "F4", value = 61 }, - F5 = { name = "F5", value = 62 }, - F6 = { name = "F6", value = 63 }, - F7 = { name = "F7", value = 64 }, - F8 = { name = "F8", value = 65 }, - F9 = { name = "F9", value = 66 }, - F10 = { name = "F10", value = 67 }, - F11 = { name = "F11", value = 68 }, - F12 = { name = "F12", value = 69 }, - PRINTSCREEN = { name = "PRINTSCREEN", value = 70 }, - SCROLLLOCK = { name = "SCROLLLOCK", value = 71 }, - PAUSE = { name = "PAUSE", value = 72 }, - INSERT = { name = "INSERT", value = 73 }, - HOME = { name = "HOME", value = 74 }, - PAGEUP = { name = "PAGEUP", value = 75 }, - DELETE = { name = "DELETE", value = 76 }, - END = { name = "END", value = 77 }, - PAGEDOWN = { name = "PAGEDOWN", value = 78 }, - RIGHT = { name = "RIGHT", value = 79 }, - LEFT = { name = "LEFT", value = 80 }, - DOWN = { name = "DOWN", value = 81 }, - UP = { name = "UP", value = 82 }, - NUMLOCKCLEAR = { name = "NUMLOCKCLEAR", value = 83 }, - KP_DIVIDE = { name = "KP_DIVIDE", value = 84 }, - KP_MULTIPLY = { name = "KP_MULTIPLY", value = 85 }, - KP_MINUS = { name = "KP_MINUS", value = 86 }, - KP_PLUS = { name = "KP_PLUS", value = 87 }, - KP_ENTER = { name = "KP_ENTER", value = 88 }, - KP_1 = { name = "KP_1", value = 89 }, - KP_2 = { name = "KP_2", value = 90 }, - KP_3 = { name = "KP_3", value = 91 }, - KP_4 = { name = "KP_4", value = 92 }, - KP_5 = { name = "KP_5", value = 93 }, - KP_6 = { name = "KP_6", value = 94 }, - KP_7 = { name = "KP_7", value = 95 }, - KP_8 = { name = "KP_8", value = 96 }, - KP_9 = { name = "KP_9", value = 97 }, - KP_0 = { name = "KP_0", value = 98 }, - KP_PERIOD = { name = "KP_PERIOD", value = 99 }, - NONUSBACKSLASH = { name = "NONUSBACKSLASH", value = 100 }, - APPLICATION = { name = "APPLICATION", value = 101 }, - POWER = { name = "POWER", value = 102 }, - KP_EQUALS = { name = "KP_EQUALS", value = 103 }, - F13 = { name = "F13", value = 104 }, - F14 = { name = "F14", value = 105 }, - F15 = { name = "F15", value = 106 }, - F16 = { name = "F16", value = 107 }, - F17 = { name = "F17", value = 108 }, - F18 = { name = "F18", value = 109 }, - F19 = { name = "F19", value = 110 }, - F20 = { name = "F20", value = 111 }, - F21 = { name = "F21", value = 112 }, - F22 = { name = "F22", value = 113 }, - F23 = { name = "F23", value = 114 }, - F24 = { name = "F24", value = 115 }, - EXECUTE = { name = "EXECUTE", value = 116 }, - HELP = { name = "HELP", value = 117 }, - MENU = { name = "MENU", value = 118 }, - SELECT = { name = "SELECT", value = 119 }, - STOP = { name = "STOP", value = 120 }, - AGAIN = { name = "AGAIN", value = 121 }, - UNDO = { name = "UNDO", value = 122 }, - CUT = { name = "CUT", value = 123 }, - COPY = { name = "COPY", value = 124 }, - PASTE = { name = "PASTE", value = 125 }, - FIND = { name = "FIND", value = 126 }, - MUTE = { name = "MUTE", value = 127 }, - VOLUMEUP = { name = "VOLUMEUP", value = 128 }, - VOLUMEDOWN = { name = "VOLUMEDOWN", value = 129 }, - KP_COMMA = { name = "KP_COMMA", value = 133 }, - KP_EQUALSAS400 = { name = "KP_EQUALSAS400", value = 134 }, - INTERNATIONAL1 = { name = "INTERNATIONAL1", value = 135 }, - INTERNATIONAL2 = { name = "INTERNATIONAL2", value = 136 }, - INTERNATIONAL3 = { name = "INTERNATIONAL3", value = 137 }, - INTERNATIONAL4 = { name = "INTERNATIONAL4", value = 138 }, - INTERNATIONAL5 = { name = "INTERNATIONAL5", value = 139 }, - INTERNATIONAL6 = { name = "INTERNATIONAL6", value = 140 }, - INTERNATIONAL7 = { name = "INTERNATIONAL7", value = 141 }, - INTERNATIONAL8 = { name = "INTERNATIONAL8", value = 142 }, - INTERNATIONAL9 = { name = "INTERNATIONAL9", value = 143 }, - LANG1 = { name = "LANG1", value = 144 }, - LANG2 = { name = "LANG2", value = 145 }, - LANG3 = { name = "LANG3", value = 146 }, - LANG4 = { name = "LANG4", value = 147 }, - LANG5 = { name = "LANG5", value = 148 }, - LANG6 = { name = "LANG6", value = 149 }, - LANG7 = { name = "LANG7", value = 150 }, - LANG8 = { name = "LANG8", value = 151 }, - LANG9 = { name = "LANG9", value = 152 }, - ALTERASE = { name = "ALTERASE", value = 153 }, - SYSREQ = { name = "SYSREQ", value = 154 }, - CANCEL = { name = "CANCEL", value = 155 }, - CLEAR = { name = "CLEAR", value = 156 }, - PRIOR = { name = "PRIOR", value = 157 }, - RETURN2 = { name = "RETURN2", value = 158 }, - SEPARATOR = { name = "SEPARATOR", value = 159 }, - OUT = { name = "OUT", value = 160 }, - OPER = { name = "OPER", value = 161 }, - CLEARAGAIN = { name = "CLEARAGAIN", value = 162 }, - CRSEL = { name = "CRSEL", value = 163 }, - EXSEL = { name = "EXSEL", value = 164 }, - KP_00 = { name = "KP_00", value = 176 }, - KP_000 = { name = "KP_000", value = 177 }, - THOUSANDSSEPARATOR = { name = "THOUSANDSSEPARATOR", value = 178 }, - DECIMALSEPARATOR = { name = "DECIMALSEPARATOR", value = 179 }, - CURRENCYUNIT = { name = "CURRENCYUNIT", value = 180 }, - CURRENCYSUBUNIT = { name = "CURRENCYSUBUNIT", value = 181 }, - KP_LEFTPAREN = { name = "KP_LEFTPAREN", value = 182 }, - KP_RIGHTPAREN = { name = "KP_RIGHTPAREN", value = 183 }, - KP_LEFTBRACE = { name = "KP_LEFTBRACE", value = 184 }, - KP_RIGHTBRACE = { name = "KP_RIGHTBRACE", value = 185 }, - KP_TAB = { name = "KP_TAB", value = 186 }, - KP_BACKSPACE = { name = "KP_BACKSPACE", value = 187 }, - KP_A = { name = "KP_A", value = 188 }, - KP_B = { name = "KP_B", value = 189 }, - KP_C = { name = "KP_C", value = 190 }, - KP_D = { name = "KP_D", value = 191 }, - KP_E = { name = "KP_E", value = 192 }, - KP_F = { name = "KP_F", value = 193 }, - KP_XOR = { name = "KP_XOR", value = 194 }, - KP_POWER = { name = "KP_POWER", value = 195 }, - KP_PERCENT = { name = "KP_PERCENT", value = 196 }, - KP_LESS = { name = "KP_LESS", value = 197 }, - KP_GREATER = { name = "KP_GREATER", value = 198 }, - KP_AMPERSAND = { name = "KP_AMPERSAND", value = 199 }, - KP_DBLAMPERSAND = { name = "KP_DBLAMPERSAND", value = 200 }, - KP_VERTICALBAR = { name = "KP_VERTICALBAR", value = 201 }, - KP_DBLVERTICALBAR = { name = "KP_DBLVERTICALBAR", value = 202 }, - KP_COLON = { name = "KP_COLON", value = 203 }, - KP_HASH = { name = "KP_HASH", value = 204 }, - KP_SPACE = { name = "KP_SPACE", value = 205 }, - KP_AT = { name = "KP_AT", value = 206 }, - KP_EXCLAM = { name = "KP_EXCLAM", value = 207 }, - KP_MEMSTORE = { name = "KP_MEMSTORE", value = 208 }, - KP_MEMRECALL = { name = "KP_MEMRECALL", value = 209 }, - KP_MEMCLEAR = { name = "KP_MEMCLEAR", value = 210 }, - KP_MEMADD = { name = "KP_MEMADD", value = 211 }, - KP_MEMSUBTRACT = { name = "KP_MEMSUBTRACT", value = 212 }, - KP_MEMMULTIPLY = { name = "KP_MEMMULTIPLY", value = 213 }, - KP_MEMDIVIDE = { name = "KP_MEMDIVIDE", value = 214 }, - KP_PLUSMINUS = { name = "KP_PLUSMINUS", value = 215 }, - KP_CLEAR = { name = "KP_CLEAR", value = 216 }, - KP_CLEARENTRY = { name = "KP_CLEARENTRY", value = 217 }, - KP_BINARY = { name = "KP_BINARY", value = 218 }, - KP_OCTAL = { name = "KP_OCTAL", value = 219 }, - KP_DECIMAL = { name = "KP_DECIMAL", value = 220 }, - KP_HEXADECIMAL = { name = "KP_HEXADECIMAL", value = 221 }, - LCTRL = { name = "LCTRL", value = 224 }, - LSHIFT = { name = "LSHIFT", value = 225 }, - LALT = { name = "LALT", value = 226 }, - LGUI = { name = "LGUI", value = 227 }, - RCTRL = { name = "RCTRL", value = 228 }, - RSHIFT = { name = "RSHIFT", value = 229 }, - RALT = { name = "RALT", value = 230 }, - RGUI = { name = "RGUI", value = 231 }, - MODE = { name = "MODE", value = 257 }, - AUDIONEXT = { name = "AUDIONEXT", value = 258 }, - AUDIOPREV = { name = "AUDIOPREV", value = 259 }, - AUDIOSTOP = { name = "AUDIOSTOP", value = 260 }, - AUDIOPLAY = { name = "AUDIOPLAY", value = 261 }, - AUDIOMUTE = { name = "AUDIOMUTE", value = 262 }, - MEDIASELECT = { name = "MEDIASELECT", value = 263 }, - WWW = { name = "WWW", value = 264 }, - MAIL = { name = "MAIL", value = 265 }, - CALCULATOR = { name = "CALCULATOR", value = 266 }, - COMPUTER = { name = "COMPUTER", value = 267 }, - AC_SEARCH = { name = "AC_SEARCH", value = 268 }, - AC_HOME = { name = "AC_HOME", value = 269 }, - AC_BACK = { name = "AC_BACK", value = 270 }, - AC_FORWARD = { name = "AC_FORWARD", value = 271 }, - AC_STOP = { name = "AC_STOP", value = 272 }, - AC_REFRESH = { name = "AC_REFRESH", value = 273 }, - AC_BOOKMARKS = { name = "AC_BOOKMARKS", value = 274 }, - BRIGHTNESSDOWN = { name = "BRIGHTNESSDOWN", value = 275 }, - BRIGHTNESSUP = { name = "BRIGHTNESSUP", value = 276 }, - DISPLAYSWITCH = { name = "DISPLAYSWITCH", value = 277 }, - KBDILLUMTOGGLE = { name = "KBDILLUMTOGGLE", value = 278 }, - KBDILLUMDOWN = { name = "KBDILLUMDOWN", value = 279 }, - KBDILLUMUP = { name = "KBDILLUMUP", value = 280 }, - EJECT = { name = "EJECT", value = 281 }, - SLEEP = { name = "SLEEP", value = 282 }, - APP1 = { name = "APP1", value = 283 }, - APP2 = { name = "APP2", value = 284 }, - AUDIOREWIND = { name = "AUDIOREWIND", value = 285 }, - AUDIOFASTFORWARD = { name = "AUDIOFASTFORWARD", value = 286 } -} - -SCANCODE_MIN = 4 -- Lowest value -SCANCODE_MAX = 286 -- Highest value, not the number of items in the table. - -MODIFIER = { - NONE = { name = "NONE", value = 0x0000 }, - LSHIFT = { name = "LSHIFT", value = 0x0001 }, - RSHIFT = { name = "RSHIFT", value = 0x0002 }, - LCTRL = { name = "LCTRL", value = 0x0040 }, - RCTRL = { name = "RCTRL", value = 0x0080 }, - LALT = { name = "LALT", value = 0x0100 }, - RALT = { name = "RALT", value = 0x0200 }, - LGUI = { name = "LGUI", value = 0x0400 }, - RGUI = { name = "RGUI", value = 0x0800 }, - NUM = { name = "NUM", value = 0x1000 }, - CAPS = { name = "CAPS", value = 0x2000 }, - MODE = { name = "MODE", value = 0x4000 }, - SCROLL = { name = "SCROLL", value = 0x8000 }, - - SHIFT = { name = "SHIFT", value = 0x0001 + 0x0002 }, - CTRL = { name = "CTRL", value = 0x0040 + 0x0080 }, - ALT = { name = "ALT", value = 0x0100 + 0x0200 }, - GUI = { name = "GUI", value = 0x0400 + 0x0800 } -} - -GAMEPAD_0 = { - AXIS_LEFT_X = { name = "AXIS_LEFT_X", value = 500 }, - AXIS_LEFT_X_L = { name = "AXIS_LEFT_X_L", value = 501 }, - AXIS_LEFT_X_R = { name = "AXIS_LEFT_X_R", value = 502 }, - AXIS_LEFT_Y = { name = "AXIS_LEFT_Y", value = 503 }, - AXIS_LEFT_Y_U = { name = "AXIS_LEFT_Y_U", value = 504 }, - AXIS_LEFT_Y_D = { name = "AXIS_LEFT_Y_D", value = 505 }, - AXIS_RIGHT_X = { name = "AXIS_RIGHT_X", value = 506 }, - AXIS_RIGHT_X_L = { name = "AXIS_RIGHT_X_L", value = 507 }, - AXIS_RIGHT_X_R = { name = "AXIS_RIGHT_X_R", value = 508 }, - AXIS_RIGHT_Y = { name = "AXIS_RIGHT_Y", value = 509 }, - AXIS_RIGHT_Y_U = { name = "AXIS_RIGHT_Y_U", value = 510 }, - AXIS_RIGHT_Y_D = { name = "AXIS_RIGHT_Y_D", value = 511 }, - AXIS_LEFT_TRIGGER = { name = "AXIS_LEFT_TRIGGER", value = 512 }, - AXIS_LEFT_TRIGGER_N = { name = "AXIS_LEFT_TRIGGER_N", value = 513 }, - AXIS_LEFT_TRIGGER_P = { name = "AXIS_LEFT_TRIGGER_P", value = 514 }, - AXIS_RIGHT_TRIGGER = { name = "AXIS_RIGHT_TRIGGER", value = 515 }, - AXIS_RIGHT_TRIGGER_N = { name = "AXIS_RIGHT_TRIGGER_N", value = 516 }, - AXIS_RIGHT_TRIGGER_P = { name = "AXIS_RIGHT_TRIGGER_P", value = 517 }, - BUTTON_A = { name = "BUTTON_A", value = 518 }, - BUTTON_B = { name = "BUTTON_B", value = 519 }, - BUTTON_X = { name = "BUTTON_X", value = 520 }, - BUTTON_Y = { name = "BUTTON_Y", value = 521 }, - BUTTON_BACK = { name = "BUTTON_BACK", value = 522 }, - BUTTON_GUIDE = { name = "BUTTON_GUIDE", value = 523 }, - BUTTON_START = { name = "BUTTON_START", value = 524 }, - BUTTON_LEFT_STICK = { name = "BUTTON_LEFT_STICK", value = 525 }, - BUTTON_RIGHT_STICK = { name = "BUTTON_RIGHT_STICK", value = 526 }, - BUTTON_LEFT_BUMPER = { name = "BUTTON_LEFT_BUMPER", value = 527 }, - BUTTON_RIGHT_BUMPER = { name = "BUTTON_RIGHT_BUMPER", value = 528 }, - DPAD_UP = { name = "DPAD_UP", value = 529 }, - DPAD_DOWN = { name = "DPAD_DOWN", value = 530 }, - DPAD_LEFT = { name = "DPAD_LEFT", value = 531 }, - DPAD_RIGHT = { name = "DPAD_RIGHT", value = 532 } -} - -GAMEPAD_0_MIN = 500 -- Lowest value -GAMEPAD_0_MAX = 532 -- Highest value, not the number of items in the table. - -GAMEPAD_1 = utilDeepCopy(GAMEPAD_0) -GAMEPAD_2 = utilDeepCopy(GAMEPAD_0) -GAMEPAD_3 = utilDeepCopy(GAMEPAD_0) - -for key, value in pairs(GAMEPAD_0) do - GAMEPAD_1[key].value = GAMEPAD_1[key].value + 100 - GAMEPAD_2[key].value = GAMEPAD_2[key].value + 200 - GAMEPAD_3[key].value = GAMEPAD_3[key].value + 300 -end - -GAMEPAD_1_MIN = GAMEPAD_0_MAX + 100 -GAMEPAD_1_MAX = GAMEPAD_0_MAX + 100 - -GAMEPAD_2_MIN = GAMEPAD_0_MAX + 200 -GAMEPAD_2_MAX = GAMEPAD_0_MAX + 200 - -GAMEPAD_3_MIN = GAMEPAD_0_MAX + 300 -GAMEPAD_3_MAX = GAMEPAD_0_MAX + 300 - -GAMEPAD_AXIS_LEFT_X = 0 -GAMEPAD_AXIS_LEFT_Y = 1 -GAMEPAD_AXIS_RIGHT_X = 2 -GAMEPAD_AXIS_RIGHT_Y = 3 -GAMEPAD_AXIS_LEFT_TRIGGER = 4 -GAMEPAD_AXIS_RIGHT_TRIGGER = 5 - -MOUSE_0 = { - BUTTON_LEFT = { name = "BUTTON_LEFT", value = 1000 }, - BUTTON_RIGHT = { name = "BUTTON_RIGHT", value = 1001 }, - BUTTON_MIDDLE = { name = "BUTTON_MIDDLE", value = 1002 }, - BUTTON_X1 = { name = "BUTTON_X1", value = 1003 }, - BUTTON_X2 = { name = "BUTTON_X2", value = 1004 }, - WHEEL_UP = { name = "WHEEL_UP", value = 1005 }, - WHEEL_DOWN = { name = "WHEEL_DOWN", value = 1006 } -} - -MOUSE_0_MIN = 1000 -MOUSE_0_MAX = 1006 - -MOUSE_1 = utilDeepCopy(MOUSE_0) -MOUSE_2 = utilDeepCopy(MOUSE_0) -MOUSE_3 = utilDeepCopy(MOUSE_0) - -for key, value in pairs(MOUSE_0) do - MOUSE_1[key].value = MOUSE_1[key].value + 100 - MOUSE_2[key].value = MOUSE_2[key].value + 200 - MOUSE_3[key].value = MOUSE_3[key].value + 300 -end - -MOUSE_1_MIN = MOUSE_0_MAX + 100 -MOUSE_1_MAX = MOUSE_0_MAX + 100 - -MOUSE_2_MIN = MOUSE_0_MAX + 200 -MOUSE_2_MAX = MOUSE_0_MAX + 200 - -MOUSE_3_MIN = MOUSE_0_MAX + 300 -MOUSE_3_MAX = MOUSE_0_MAX + 300 - -SWITCH_BUTTON4 = 21 -SWITCH_TILT = 22 -SWITCH_GRAB = 23 - -MOUSE_SINGLE = 100 -MOUSE_MANY = 200 - -RENDER_SMOOTH = 1 -RENDER_PIXELATED = 0 - -if videoGetLanguageDescription ~= nil then - discGetLanguageDescription = videoGetLanguageDescription -end - - --- Singe 1.xx Features ------------------------------------------------------- - - -if discSetFPS ~= nil then - discSetFPS(29.97) - discSearch(1) -end - -SWITCH_UP = 0 -SWITCH_LEFT = 1 -SWITCH_DOWN = 2 -SWITCH_RIGHT = 3 -SWITCH_START1 = 4 -SWITCH_START2 = 5 -SWITCH_BUTTON1 = 6 -SWITCH_BUTTON2 = 7 -SWITCH_BUTTON3 = 8 -SWITCH_COIN1 = 9 -SWITCH_COIN2 = 10 -SWITCH_SKILL1 = 11 -SWITCH_SKILL2 = 12 -SWITCH_SKILL3 = 13 -SWITCH_SERVICE = 14 -SWITCH_TEST = 15 -SWITCH_RESET = 16 -SWITCH_SCREENSHOT = 17 -SWITCH_QUIT = 18 -SWITCH_PAUSE = 19 -SWITCH_CONSOLE = 20 -SWITCH_BUTTON4 = 21 -- Added in Singe 2.00 -SWITCH_TILT = 22 -- Added in Singe 2.00 - -FONT_QUALITY_SOLID = 1 -FONT_QUALITY_SHADED = 2 -FONT_QUALITY_BLENDED = 3 - -SOUND_ERROR_INVALID = -1 -SOUND_REMOVE_HANDLE = -1 -SOUND_ERROR_FULL = -2 - -OVERLAY_NOT_UPDATED = 0 -OVERLAY_UPDATED = 1 - -SINGLE_MOUSE = 100 -MANY_MOUSE = 200 - -MODE_NORMAL = 0 -MODE_FULL = 1 - --- Make old random number calls still work -random = {} -random.new = math.random - --- Map old Daphne calls to Singe calls -if singeGetHeight ~= nil then - daphneGetHeight = singeGetHeight - daphneGetWidth = singeGetWidth - daphneScreenshot = singeScreenshot -end - - --- Singe 2.10 Threaded Application Support ------------------------------------ - - -if singeMain ~= nil then - onOverlayUpdate = function() - coroutine.resume(SINGE_SELF) - return(OVERLAY_UPDATED) - end - singeYield = coroutine.yield - SINGE_SELF = coroutine.create(singeMain) -end - - +--[[ + * + * Singe 2 + * Copyright (C) 2006-2024 Scott Duensing + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * +--]] + + +-- Singe 2.xx Features ------------------------------------------------------- + + +-- SINGE_FRAMEWORK_VERSION, SWITCH_*, FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, +-- RENDER_*, DISC_*, SOUND_ERROR_* and the SINGE_* input code layout are all +-- pushed by the engine before this file runs, so they are defined in exactly +-- one place: singe.c. + +if singeGetScriptPath ~= nil then + DIR = singeGetScriptPath():match("(.*[/\\])") or "./" +end + + +function utilDeepCopy(original) + local copy + if type(original) == 'table' then + copy = {} + for key, value in next, original, nil do + copy[utilDeepCopy(key)] = utilDeepCopy(value) + end + setmetatable(copy, utilDeepCopy(getmetatable(original))) + else -- number, string, boolean, etc + copy = original + end + return copy +end + + +function utilDump(o) + if type(o) == 'table' then + local s = '{ ' + for k,v in pairs(o) do + if type(k) ~= 'number' then + k = '"'..k..'"' + end + s = s .. '['..k..'] = ' .. utilDump(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end +end + + +function utilGetTableSize(t) + local count = 0 + for _ in pairs(t) do + count = count + 1 + end + return count +end + + +-- Returns a copy of a { NAME = { name = NAME, value = N } } table with every value shifted. +function utilOffsetTable(source, offset) + local copy = utilDeepCopy(source) + for _, entry in pairs(copy) do + entry.value = entry.value + offset + end + return copy +end + + +function utilTrim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + + +SCANCODE = { + A = { name = "A", value = 4 }, + B = { name = "B", value = 5 }, + C = { name = "C", value = 6 }, + D = { name = "D", value = 7 }, + E = { name = "E", value = 8 }, + F = { name = "F", value = 9 }, + G = { name = "G", value = 10 }, + H = { name = "H", value = 11 }, + I = { name = "I", value = 12 }, + J = { name = "J", value = 13 }, + K = { name = "K", value = 14 }, + L = { name = "L", value = 15 }, + M = { name = "M", value = 16 }, + N = { name = "N", value = 17 }, + O = { name = "O", value = 18 }, + P = { name = "P", value = 19 }, + Q = { name = "Q", value = 20 }, + R = { name = "R", value = 21 }, + S = { name = "S", value = 22 }, + T = { name = "T", value = 23 }, + U = { name = "U", value = 24 }, + V = { name = "V", value = 25 }, + W = { name = "W", value = 26 }, + X = { name = "X", value = 27 }, + Y = { name = "Y", value = 28 }, + Z = { name = "Z", value = 29 }, + MAIN_1 = { name = "MAIN_1", value = 30 }, + MAIN_2 = { name = "MAIN_2", value = 31 }, + MAIN_3 = { name = "MAIN_3", value = 32 }, + MAIN_4 = { name = "MAIN_4", value = 33 }, + MAIN_5 = { name = "MAIN_5", value = 34 }, + MAIN_6 = { name = "MAIN_6", value = 35 }, + MAIN_7 = { name = "MAIN_7", value = 36 }, + MAIN_8 = { name = "MAIN_8", value = 37 }, + MAIN_9 = { name = "MAIN_9", value = 38 }, + MAIN_0 = { name = "MAIN_0", value = 39 }, + RETURN = { name = "RETURN", value = 40 }, + ESCAPE = { name = "ESCAPE", value = 41 }, + BACKSPACE = { name = "BACKSPACE", value = 42 }, + TAB = { name = "TAB", value = 43 }, + SPACE = { name = "SPACE", value = 44 }, + MINUS = { name = "MINUS", value = 45 }, + EQUALS = { name = "EQUALS", value = 46 }, + LEFTBRACKET = { name = "LEFTBRACKET", value = 47 }, + RIGHTBRACKET = { name = "RIGHTBRACKET", value = 48 }, + BACKSLASH = { name = "BACKSLASH", value = 49 }, + NONUSHASH = { name = "NONUSHASH", value = 50 }, + SEMICOLON = { name = "SEMICOLON", value = 51 }, + APOSTROPHE = { name = "APOSTROPHE", value = 52 }, + GRAVE = { name = "GRAVE", value = 53 }, + COMMA = { name = "COMMA", value = 54 }, + PERIOD = { name = "PERIOD", value = 55 }, + SLASH = { name = "SLASH", value = 56 }, + CAPSLOCK = { name = "CAPSLOCK", value = 57 }, + F1 = { name = "F1", value = 58 }, + F2 = { name = "F2", value = 59 }, + F3 = { name = "F3", value = 60 }, + F4 = { name = "F4", value = 61 }, + F5 = { name = "F5", value = 62 }, + F6 = { name = "F6", value = 63 }, + F7 = { name = "F7", value = 64 }, + F8 = { name = "F8", value = 65 }, + F9 = { name = "F9", value = 66 }, + F10 = { name = "F10", value = 67 }, + F11 = { name = "F11", value = 68 }, + F12 = { name = "F12", value = 69 }, + PRINTSCREEN = { name = "PRINTSCREEN", value = 70 }, + SCROLLLOCK = { name = "SCROLLLOCK", value = 71 }, + PAUSE = { name = "PAUSE", value = 72 }, + INSERT = { name = "INSERT", value = 73 }, + HOME = { name = "HOME", value = 74 }, + PAGEUP = { name = "PAGEUP", value = 75 }, + DELETE = { name = "DELETE", value = 76 }, + END = { name = "END", value = 77 }, + PAGEDOWN = { name = "PAGEDOWN", value = 78 }, + RIGHT = { name = "RIGHT", value = 79 }, + LEFT = { name = "LEFT", value = 80 }, + DOWN = { name = "DOWN", value = 81 }, + UP = { name = "UP", value = 82 }, + NUMLOCKCLEAR = { name = "NUMLOCKCLEAR", value = 83 }, + KP_DIVIDE = { name = "KP_DIVIDE", value = 84 }, + KP_MULTIPLY = { name = "KP_MULTIPLY", value = 85 }, + KP_MINUS = { name = "KP_MINUS", value = 86 }, + KP_PLUS = { name = "KP_PLUS", value = 87 }, + KP_ENTER = { name = "KP_ENTER", value = 88 }, + KP_1 = { name = "KP_1", value = 89 }, + KP_2 = { name = "KP_2", value = 90 }, + KP_3 = { name = "KP_3", value = 91 }, + KP_4 = { name = "KP_4", value = 92 }, + KP_5 = { name = "KP_5", value = 93 }, + KP_6 = { name = "KP_6", value = 94 }, + KP_7 = { name = "KP_7", value = 95 }, + KP_8 = { name = "KP_8", value = 96 }, + KP_9 = { name = "KP_9", value = 97 }, + KP_0 = { name = "KP_0", value = 98 }, + KP_PERIOD = { name = "KP_PERIOD", value = 99 }, + NONUSBACKSLASH = { name = "NONUSBACKSLASH", value = 100 }, + APPLICATION = { name = "APPLICATION", value = 101 }, + POWER = { name = "POWER", value = 102 }, + KP_EQUALS = { name = "KP_EQUALS", value = 103 }, + F13 = { name = "F13", value = 104 }, + F14 = { name = "F14", value = 105 }, + F15 = { name = "F15", value = 106 }, + F16 = { name = "F16", value = 107 }, + F17 = { name = "F17", value = 108 }, + F18 = { name = "F18", value = 109 }, + F19 = { name = "F19", value = 110 }, + F20 = { name = "F20", value = 111 }, + F21 = { name = "F21", value = 112 }, + F22 = { name = "F22", value = 113 }, + F23 = { name = "F23", value = 114 }, + F24 = { name = "F24", value = 115 }, + EXECUTE = { name = "EXECUTE", value = 116 }, + HELP = { name = "HELP", value = 117 }, + MENU = { name = "MENU", value = 118 }, + SELECT = { name = "SELECT", value = 119 }, + STOP = { name = "STOP", value = 120 }, + AGAIN = { name = "AGAIN", value = 121 }, + UNDO = { name = "UNDO", value = 122 }, + CUT = { name = "CUT", value = 123 }, + COPY = { name = "COPY", value = 124 }, + PASTE = { name = "PASTE", value = 125 }, + FIND = { name = "FIND", value = 126 }, + MUTE = { name = "MUTE", value = 127 }, + VOLUMEUP = { name = "VOLUMEUP", value = 128 }, + VOLUMEDOWN = { name = "VOLUMEDOWN", value = 129 }, + KP_COMMA = { name = "KP_COMMA", value = 133 }, + KP_EQUALSAS400 = { name = "KP_EQUALSAS400", value = 134 }, + INTERNATIONAL1 = { name = "INTERNATIONAL1", value = 135 }, + INTERNATIONAL2 = { name = "INTERNATIONAL2", value = 136 }, + INTERNATIONAL3 = { name = "INTERNATIONAL3", value = 137 }, + INTERNATIONAL4 = { name = "INTERNATIONAL4", value = 138 }, + INTERNATIONAL5 = { name = "INTERNATIONAL5", value = 139 }, + INTERNATIONAL6 = { name = "INTERNATIONAL6", value = 140 }, + INTERNATIONAL7 = { name = "INTERNATIONAL7", value = 141 }, + INTERNATIONAL8 = { name = "INTERNATIONAL8", value = 142 }, + INTERNATIONAL9 = { name = "INTERNATIONAL9", value = 143 }, + LANG1 = { name = "LANG1", value = 144 }, + LANG2 = { name = "LANG2", value = 145 }, + LANG3 = { name = "LANG3", value = 146 }, + LANG4 = { name = "LANG4", value = 147 }, + LANG5 = { name = "LANG5", value = 148 }, + LANG6 = { name = "LANG6", value = 149 }, + LANG7 = { name = "LANG7", value = 150 }, + LANG8 = { name = "LANG8", value = 151 }, + LANG9 = { name = "LANG9", value = 152 }, + ALTERASE = { name = "ALTERASE", value = 153 }, + SYSREQ = { name = "SYSREQ", value = 154 }, + CANCEL = { name = "CANCEL", value = 155 }, + CLEAR = { name = "CLEAR", value = 156 }, + PRIOR = { name = "PRIOR", value = 157 }, + RETURN2 = { name = "RETURN2", value = 158 }, + SEPARATOR = { name = "SEPARATOR", value = 159 }, + OUT = { name = "OUT", value = 160 }, + OPER = { name = "OPER", value = 161 }, + CLEARAGAIN = { name = "CLEARAGAIN", value = 162 }, + CRSEL = { name = "CRSEL", value = 163 }, + EXSEL = { name = "EXSEL", value = 164 }, + KP_00 = { name = "KP_00", value = 176 }, + KP_000 = { name = "KP_000", value = 177 }, + THOUSANDSSEPARATOR = { name = "THOUSANDSSEPARATOR", value = 178 }, + DECIMALSEPARATOR = { name = "DECIMALSEPARATOR", value = 179 }, + CURRENCYUNIT = { name = "CURRENCYUNIT", value = 180 }, + CURRENCYSUBUNIT = { name = "CURRENCYSUBUNIT", value = 181 }, + KP_LEFTPAREN = { name = "KP_LEFTPAREN", value = 182 }, + KP_RIGHTPAREN = { name = "KP_RIGHTPAREN", value = 183 }, + KP_LEFTBRACE = { name = "KP_LEFTBRACE", value = 184 }, + KP_RIGHTBRACE = { name = "KP_RIGHTBRACE", value = 185 }, + KP_TAB = { name = "KP_TAB", value = 186 }, + KP_BACKSPACE = { name = "KP_BACKSPACE", value = 187 }, + KP_A = { name = "KP_A", value = 188 }, + KP_B = { name = "KP_B", value = 189 }, + KP_C = { name = "KP_C", value = 190 }, + KP_D = { name = "KP_D", value = 191 }, + KP_E = { name = "KP_E", value = 192 }, + KP_F = { name = "KP_F", value = 193 }, + KP_XOR = { name = "KP_XOR", value = 194 }, + KP_POWER = { name = "KP_POWER", value = 195 }, + KP_PERCENT = { name = "KP_PERCENT", value = 196 }, + KP_LESS = { name = "KP_LESS", value = 197 }, + KP_GREATER = { name = "KP_GREATER", value = 198 }, + KP_AMPERSAND = { name = "KP_AMPERSAND", value = 199 }, + KP_DBLAMPERSAND = { name = "KP_DBLAMPERSAND", value = 200 }, + KP_VERTICALBAR = { name = "KP_VERTICALBAR", value = 201 }, + KP_DBLVERTICALBAR = { name = "KP_DBLVERTICALBAR", value = 202 }, + KP_COLON = { name = "KP_COLON", value = 203 }, + KP_HASH = { name = "KP_HASH", value = 204 }, + KP_SPACE = { name = "KP_SPACE", value = 205 }, + KP_AT = { name = "KP_AT", value = 206 }, + KP_EXCLAM = { name = "KP_EXCLAM", value = 207 }, + KP_MEMSTORE = { name = "KP_MEMSTORE", value = 208 }, + KP_MEMRECALL = { name = "KP_MEMRECALL", value = 209 }, + KP_MEMCLEAR = { name = "KP_MEMCLEAR", value = 210 }, + KP_MEMADD = { name = "KP_MEMADD", value = 211 }, + KP_MEMSUBTRACT = { name = "KP_MEMSUBTRACT", value = 212 }, + KP_MEMMULTIPLY = { name = "KP_MEMMULTIPLY", value = 213 }, + KP_MEMDIVIDE = { name = "KP_MEMDIVIDE", value = 214 }, + KP_PLUSMINUS = { name = "KP_PLUSMINUS", value = 215 }, + KP_CLEAR = { name = "KP_CLEAR", value = 216 }, + KP_CLEARENTRY = { name = "KP_CLEARENTRY", value = 217 }, + KP_BINARY = { name = "KP_BINARY", value = 218 }, + KP_OCTAL = { name = "KP_OCTAL", value = 219 }, + KP_DECIMAL = { name = "KP_DECIMAL", value = 220 }, + KP_HEXADECIMAL = { name = "KP_HEXADECIMAL", value = 221 }, + LCTRL = { name = "LCTRL", value = 224 }, + LSHIFT = { name = "LSHIFT", value = 225 }, + LALT = { name = "LALT", value = 226 }, + LGUI = { name = "LGUI", value = 227 }, + RCTRL = { name = "RCTRL", value = 228 }, + RSHIFT = { name = "RSHIFT", value = 229 }, + RALT = { name = "RALT", value = 230 }, + RGUI = { name = "RGUI", value = 231 }, + MODE = { name = "MODE", value = 257 }, + AUDIONEXT = { name = "AUDIONEXT", value = 258 }, + AUDIOPREV = { name = "AUDIOPREV", value = 259 }, + AUDIOSTOP = { name = "AUDIOSTOP", value = 260 }, + AUDIOPLAY = { name = "AUDIOPLAY", value = 261 }, + AUDIOMUTE = { name = "AUDIOMUTE", value = 262 }, + MEDIASELECT = { name = "MEDIASELECT", value = 263 }, + WWW = { name = "WWW", value = 264 }, + MAIL = { name = "MAIL", value = 265 }, + CALCULATOR = { name = "CALCULATOR", value = 266 }, + COMPUTER = { name = "COMPUTER", value = 267 }, + AC_SEARCH = { name = "AC_SEARCH", value = 268 }, + AC_HOME = { name = "AC_HOME", value = 269 }, + AC_BACK = { name = "AC_BACK", value = 270 }, + AC_FORWARD = { name = "AC_FORWARD", value = 271 }, + AC_STOP = { name = "AC_STOP", value = 272 }, + AC_REFRESH = { name = "AC_REFRESH", value = 273 }, + AC_BOOKMARKS = { name = "AC_BOOKMARKS", value = 274 }, + BRIGHTNESSDOWN = { name = "BRIGHTNESSDOWN", value = 275 }, + BRIGHTNESSUP = { name = "BRIGHTNESSUP", value = 276 }, + DISPLAYSWITCH = { name = "DISPLAYSWITCH", value = 277 }, + KBDILLUMTOGGLE = { name = "KBDILLUMTOGGLE", value = 278 }, + KBDILLUMDOWN = { name = "KBDILLUMDOWN", value = 279 }, + KBDILLUMUP = { name = "KBDILLUMUP", value = 280 }, + EJECT = { name = "EJECT", value = 281 }, + SLEEP = { name = "SLEEP", value = 282 }, + APP1 = { name = "APP1", value = 283 }, + APP2 = { name = "APP2", value = 284 }, + AUDIOREWIND = { name = "AUDIOREWIND", value = 285 }, + AUDIOFASTFORWARD = { name = "AUDIOFASTFORWARD", value = 286 } +} + +MODIFIER = { + NONE = { name = "NONE", value = 0x0000 }, + LSHIFT = { name = "LSHIFT", value = 0x0001 }, + RSHIFT = { name = "RSHIFT", value = 0x0002 }, + LCTRL = { name = "LCTRL", value = 0x0040 }, + RCTRL = { name = "RCTRL", value = 0x0080 }, + LALT = { name = "LALT", value = 0x0100 }, + RALT = { name = "RALT", value = 0x0200 }, + LGUI = { name = "LGUI", value = 0x0400 }, + RGUI = { name = "RGUI", value = 0x0800 }, + NUM = { name = "NUM", value = 0x1000 }, + CAPS = { name = "CAPS", value = 0x2000 }, + MODE = { name = "MODE", value = 0x4000 }, + SCROLL = { name = "SCROLL", value = 0x8000 }, + + SHIFT = { name = "SHIFT", value = 0x0001 + 0x0002 }, + CTRL = { name = "CTRL", value = 0x0040 + 0x0080 }, + ALT = { name = "ALT", value = 0x0100 + 0x0200 }, + GUI = { name = "GUI", value = 0x0400 + 0x0800 } +} + +-- Controller codes are built from the engine's layout: SINGE_GAMEPAD_BASE plus +-- SINGE_GAMEPAD_STRIDE per controller; each axis takes SINGE_AXIS_STRIDE codes +-- (axis, negative direction, positive direction) and buttons follow at +-- SINGE_GAMEPAD_BUTTON_OFFSET in SDL's button order. +local function gamepadAxisCodes(axis, name, negative, positive) + local base = SINGE_GAMEPAD_BASE + axis * SINGE_AXIS_STRIDE + return { + { name, base }, { name .. "_" .. negative, base + 1 }, { name .. "_" .. positive, base + 2 } + } +end + +local gamepadCodes = {} +for _, entry in ipairs(gamepadAxisCodes(0, "AXIS_LEFT_X", "L", "R")) do gamepadCodes[#gamepadCodes + 1] = entry end +for _, entry in ipairs(gamepadAxisCodes(1, "AXIS_LEFT_Y", "U", "D")) do gamepadCodes[#gamepadCodes + 1] = entry end +for _, entry in ipairs(gamepadAxisCodes(2, "AXIS_RIGHT_X", "L", "R")) do gamepadCodes[#gamepadCodes + 1] = entry end +for _, entry in ipairs(gamepadAxisCodes(3, "AXIS_RIGHT_Y", "U", "D")) do gamepadCodes[#gamepadCodes + 1] = entry end +for _, entry in ipairs(gamepadAxisCodes(4, "AXIS_LEFT_TRIGGER", "N", "P")) do gamepadCodes[#gamepadCodes + 1] = entry end +for _, entry in ipairs(gamepadAxisCodes(5, "AXIS_RIGHT_TRIGGER", "N", "P")) do gamepadCodes[#gamepadCodes + 1] = entry end + +local gamepadButtons = { + "BUTTON_A", "BUTTON_B", "BUTTON_X", "BUTTON_Y", "BUTTON_BACK", "BUTTON_GUIDE", "BUTTON_START", + "BUTTON_LEFT_STICK", "BUTTON_RIGHT_STICK", "BUTTON_LEFT_BUMPER", "BUTTON_RIGHT_BUMPER", + "DPAD_UP", "DPAD_DOWN", "DPAD_LEFT", "DPAD_RIGHT" +} +for index, name in ipairs(gamepadButtons) do + gamepadCodes[#gamepadCodes + 1] = { name, SINGE_GAMEPAD_BASE + SINGE_GAMEPAD_BUTTON_OFFSET + index - 1 } +end + +GAMEPAD_0 = {} +for _, entry in ipairs(gamepadCodes) do + GAMEPAD_0[entry[1]] = { name = entry[1], value = entry[2] } +end +GAMEPAD_1 = utilOffsetTable(GAMEPAD_0, SINGE_GAMEPAD_STRIDE) +GAMEPAD_2 = utilOffsetTable(GAMEPAD_0, SINGE_GAMEPAD_STRIDE * 2) +GAMEPAD_3 = utilOffsetTable(GAMEPAD_0, SINGE_GAMEPAD_STRIDE * 3) + +GAMEPAD_AXIS_LEFT_X = 0 +GAMEPAD_AXIS_LEFT_Y = 1 +GAMEPAD_AXIS_RIGHT_X = 2 +GAMEPAD_AXIS_RIGHT_Y = 3 +GAMEPAD_AXIS_LEFT_TRIGGER = 4 +GAMEPAD_AXIS_RIGHT_TRIGGER = 5 + +-- Mouse codes: SINGE_MOUSE_BASE plus SINGE_MOUSE_STRIDE per mouse; buttons then wheel. +local mouseNames = { "BUTTON_LEFT", "BUTTON_RIGHT", "BUTTON_MIDDLE", "BUTTON_X1", "BUTTON_X2", "WHEEL_UP", "WHEEL_DOWN" } +MOUSE_0 = {} +for index, name in ipairs(mouseNames) do + MOUSE_0[name] = { name = name, value = SINGE_MOUSE_BASE + index - 1 } +end +MOUSE_1 = utilOffsetTable(MOUSE_0, SINGE_MOUSE_STRIDE) +MOUSE_2 = utilOffsetTable(MOUSE_0, SINGE_MOUSE_STRIDE * 2) +MOUSE_3 = utilOffsetTable(MOUSE_0, SINGE_MOUSE_STRIDE * 3) + +-- Older names for engine functions that gained a setter. +if mouseSetEnabled ~= nil then + mouseEnable = function() mouseSetEnabled(true) end + mouseDisable = function() mouseSetEnabled(false) end +end +if singeSetPauseKeyEnabled ~= nil then + singeEnablePauseKey = function() singeSetPauseKeyEnabled(true) end + singeDisablePauseKey = function() singeSetPauseKeyEnabled(false) end +end + +-- Singe 2.20 moved the sprite handle to the first argument. Games written for +-- 2.10 can set SINGE_LEGACY_SPRITE_ARGS = true (or LEGACY_SPRITE_ARGS = true in +-- games.dat) to keep calling the old way. +if SINGE_LEGACY_SPRITE_ARGS and spriteDraw ~= nil then + local newSpriteDraw = spriteDraw + spriteDraw = function(...) + local args = { ... } + local id = table.remove(args) + return newSpriteDraw(id, table.unpack(args)) + end + local newSpriteLoop = spriteLoop + spriteLoop = function(loop, id) return newSpriteLoop(id, loop) end + local newSpriteQuality = spriteQuality + spriteQuality = function(smooth, id) return newSpriteQuality(id, smooth) end + local newSpriteRotate = spriteRotate + spriteRotate = function(angle, id) return newSpriteRotate(id, angle) end + local newSpriteRotateAndScale = spriteRotateAndScale + spriteRotateAndScale = function(...) + local args = { ... } + local id = table.remove(args) + return newSpriteRotateAndScale(id, table.unpack(args)) + end + local newSpriteScale = spriteScale + spriteScale = function(...) + local args = { ... } + local id = table.remove(args) + return newSpriteScale(id, table.unpack(args)) + end + local newSpriteSetFrame = spriteSetFrame + spriteSetFrame = function(frame, id) return newSpriteSetFrame(id, frame) end +end + +if videoGetLanguageDescription ~= nil then + discGetLanguageDescription = videoGetLanguageDescription +end + + +-- Singe 1.xx Features ------------------------------------------------------- + + +-- Make old random number calls still work +random = {} +random.new = math.random + +-- Map old Daphne calls to Singe calls +if singeGetHeight ~= nil then + daphneGetHeight = singeGetHeight + daphneGetWidth = singeGetWidth + daphneScreenshot = singeScreenshot +end + + +-- Singe 2.10 Threaded Application Support ------------------------------------ + + +if singeMain ~= nil then + local singeThread = coroutine.create(singeMain) + onOverlayUpdate = function() + local ok, err = coroutine.resume(singeThread) + if not ok then + error(err, 0) + end + if coroutine.status(singeThread) == "dead" then + singeQuit() + end + return OVERLAY_UPDATED + end + singeYield = coroutine.yield +end + + diff --git a/assets/Manual.lyx b/assets/Manual.lyx deleted file mode 100644 index 724861283..000000000 --- a/assets/Manual.lyx +++ /dev/null @@ -1,2467 +0,0 @@ -#LyX 2.3 created this file. For more info see http://www.lyx.org/ -\lyxformat 544 -\begin_document -\begin_header -\save_transient_properties true -\origin unavailable -\textclass scrbook -\use_default_options true -\begin_modules -theorems-ams -eqs-within-sections -figs-within-sections -\end_modules -\maintain_unincluded_children false -\language english -\language_package default -\inputencoding auto -\fontencoding global -\font_roman "default" "default" -\font_sans "default" "default" -\font_typewriter "default" "default" -\font_math "auto" "auto" -\font_default_family default -\use_non_tex_fonts false -\font_sc false -\font_osf false -\font_sf_scale 100 100 -\font_tt_scale 100 100 -\use_microtype false -\use_dash_ligatures true -\graphics default -\default_output_format default -\output_sync 0 -\bibtex_command default -\index_command default -\paperfontsize default -\spacing single -\use_hyperref false -\papersize default -\use_geometry false -\use_package amsmath 1 -\use_package amssymb 1 -\use_package cancel 1 -\use_package esint 1 -\use_package mathdots 1 -\use_package mathtools 1 -\use_package mhchem 1 -\use_package stackrel 1 -\use_package stmaryrd 1 -\use_package undertilde 1 -\cite_engine basic -\cite_engine_type default -\biblio_style plain -\use_bibtopic false -\use_indices false -\paperorientation portrait -\suppress_date false -\justification true -\use_refstyle 1 -\use_minted 0 -\index Index -\shortcut idx -\color #008000 -\end_index -\secnumdepth 3 -\tocdepth 3 -\paragraph_separation indent -\paragraph_indentation default -\is_math_indent 0 -\math_numbering_side default -\quotes_style english -\dynamic_quotes 0 -\papercolumns 1 -\papersides 1 -\paperpagestyle default -\tracking_changes false -\output_changes false -\html_math_output 0 -\html_css_as_file 0 -\html_be_strict false -\end_header - -\begin_body - -\begin_layout Title -Singe -\end_layout - -\begin_layout Subtitle -the Somewhat Interactive Nostalgic Game Engine -\end_layout - -\begin_layout Author -Kangaroo Punch Studios -\end_layout - -\begin_layout Publishers -https://SingeEngine.com -\end_layout - -\begin_layout Date -Copyright 2006-2024 © Scott Duensing -\end_layout - -\begin_layout Standard -\begin_inset CommandInset toc -LatexCommand tableofcontents - -\end_inset - - -\end_layout - -\begin_layout Part -About Singe -\end_layout - -\begin_layout Standard -Singe, the Somewhat Interactive Nostalgic Game Engine, (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 own game are made available through - a simple application programming interface (API). -\end_layout - -\begin_layout Standard -SINGE provides numerous features to the game developer. - Some of the more interesting ones are: -\end_layout - -\begin_layout Itemize -Object-Oriented Programming Language -\end_layout - -\begin_layout Itemize -Animated Sprites -\end_layout - -\begin_layout Itemize -TrueType Font Support -\end_layout - -\begin_layout Itemize -32-bit Color Space with Transparency -\end_layout - -\begin_layout Itemize -Multi-Channel, Overlapping, Stereo Sound -\end_layout - -\begin_layout Itemize -Analog and Digital Input Device Support -\end_layout - -\begin_layout Itemize -Wide Support for Video and Audio Formats -\end_layout - -\begin_layout Standard -For players, Singe allows many unemulated and new games to be enjoyed using - any desired front end or via the included menu system. - -\end_layout - -\begin_layout Standard -Since the original release of Singe back in 2006, several revisions have - been released, both officially and unofficially. - In 2020, Singe 2.00 was released. - This was a total rewrite from the ground up adding numerous features while - staying compatible with existing 1.xx games. - As of the writing of this manual, the latest release is 2.10. -\end_layout - -\begin_layout Part -Installation and Upgrading -\end_layout - -\begin_layout Section -Installing Singe -\end_layout - -\begin_layout Standard -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’re now ready to install some games! -\end_layout - -\begin_layout Section -Upgrading Singe -\end_layout - -\begin_layout Standard -To upgrade Singe to a newer version, place the new binary in the installation - folder and rename (or delete) the existing /Singe folder. - As with the initial installation, run the new binary with no command line - arguments to generate the new support files. - DO NOT run a new Singe binary with an out-of-date set of support files! -\end_layout - -\begin_layout Section -Things to Know -\end_layout - -\begin_layout Standard -Don't touch the Singe/ folder! Files in this folder belong to Singe and - may be updated or deleted at any time. -\end_layout - -\begin_layout Part -Usage -\end_layout - -\begin_layout Section -Installing Games -\end_layout - -\begin_layout Standard -Games designed for Singe 2.xx and later simply need to be unpacked and copied - into the same folder where you placed the Singe binary. - The included menu system will automatically detect them and add them to - the menu. -\end_layout - -\begin_layout Section -Customizing the Controls -\end_layout - -\begin_layout Standard -By default, Singe maps controllers as if they were Xbox gamepads. - If your controller is not recognized or you wish to change the default - mappings as they appear to Singe, you can use the free SDL2 Gamepad Tool - from http://generalarcade.com/gamepadtool/. - -\end_layout - -\begin_layout Standard -In addition to configuring your controller, you can remap all the input - options for Singe as a whole or for any individual game by using controls.cfg - files. - When determining how inputs are mapped, Singe reads configuration settings - in the following order: -\end_layout - -\begin_layout Enumerate -The defaults from inside Singe. -\end_layout - -\begin_layout Enumerate -controls.cfg from the Singe directory. -\end_layout - -\begin_layout Enumerate -controls.cfg from the root of the data directory. -\end_layout - -\begin_layout Enumerate -controls.cfg from the game's data directory. -\end_layout - -\begin_layout Enumerate -controls.cfg from the game's main script directory. -\end_layout - -\begin_layout Standard -You do not have to specify every option in a custom controls.cfg file. - Feel free to just provide the entries you want changed. -\end_layout - -\begin_layout Standard -After installing Singe, you can find a sample input configuration file at - /Singe/controls.cfg.example. - To see the available configuration options available to be used in this - file, read through /Singe/Framework.singe. -\end_layout - -\begin_layout Section -Command Line Options -\end_layout - -\begin_layout Part -Frequently Asked Questions -\end_layout - -\begin_layout Itemize -Why is it named Singe? -\end_layout - -\begin_layout Quotation -Singe is the name of the dragon in Dragon's Lair. - As Singe (the program) began as an add-on to Daphne (the princess in Dragon's - Lair) I kept with the Dragon's Lair theme. -\end_layout - -\begin_layout Itemize -What is the difference between Daphne, Singe, Hypseus, and Singe 2.xx? -\end_layout - -\begin_layout Quotation -Daphne is an actual laserdisc game emulator that can run a handful of classic - laser games. - Daphne/Singe or Singe 1.xx is the original version of Singe that was an - add-on for the Daphne emulator. - Originally it was shipped as a game DLL for Daphne and then later combined - into a single binary with the Daphne emulation features removed. - Hypseus-Singe is an enhanced fork of Daphne with Singe 1.xx and some Singe - 2.xx support. - Finally, Singe 2.xx is an all-new, built-from-scratch, upgrade of the original - Singe. - In short, if you want accuracy, use Daphne or Hypseus if they support emulating - the desired game. - If it's a Singe game, use Singe 2.xx or you can try Hypseus. - Obviously we'd like you to use Singe 2.xx. - :-) -\end_layout - -\begin_layout Itemize -Why should we not call it an "emulator"? -\end_layout - -\begin_layout Quotation -Emulators use the real software or ROMs from the original game and pretend - to be the machine they were originally intended to run on. - From the game's perspective, it's business as usual. - Singe, on the other hand, requires that every game be re-implemented (aka - "ported") to run on Singe. - While the end result may be very similar, how it works is entirely different. -\end_layout - -\begin_layout Itemize -Can I run Singe 1.xx games on Singe 2.xx? -\end_layout - -\begin_layout Quotation -Yes! Probably. - But you likely won't want to. - Almost everything (that I know of) from Singe 1.xx has been converted and - enhanced for Singe 2.xx. -\end_layout - -\begin_layout Itemize -Why does the game I just installed not show in the menu? -\end_layout - -\begin_layout Quotation -Whoever packaged the game failed to include a proper games.dat file. - Yell at them! -\end_layout - -\begin_layout Itemize -Why is HD video slow? -\end_layout - -\begin_layout Quotation -Due to the way Singe accesses video files to provide frame seeking, it is - unable to offload video decoding to the video card. - High definition video requires a lot of CPU! -\end_layout - -\begin_layout Itemize -Why does my audio stutter on Windows? -\end_layout - -\begin_layout Quotation -Users have discovered that most Singe audio and stuttering problems are - related to their installed audio driver - especially Realtek based devices. - Try updating your sound drivers or switching to the generic Windows HD - Audio driver. - Disable surround sound - use stereo, not 5.1 or 7.1. - If your drivers do not allow you to use stereo, you can use Virtual Audio - Cable to fix this. -\end_layout - -\begin_layout Enumerate -Install VB-CABLE. - (https://vb-audio.com/Cable/) -\end_layout - -\begin_layout Enumerate -Go to Windows sound settings (right-click sound on the taskbar then Sounds - and then Playback). - -\end_layout - -\begin_layout Enumerate -Right-click the Cable Input device and choose "Set as default device". - (You will lose your current sound output. - Don't panic.) -\end_layout - -\begin_layout Enumerate -Go to the Recording tab. - -\end_layout - -\begin_layout Enumerate -Right-click Cable Output, then Properties, then Listen, and finally Enable - Listen to This Device and in the drop-down list choose your actual listening - device. - -\end_layout - -\begin_layout Enumerate -You should have sound again. - Load up Singe, and your audio will be working and lag free! -\end_layout - -\begin_layout Itemize -Why does my audio stutter on the Raspberry Pi? -\end_layout - -\begin_layout Quotation -The Raspberry Pi OS now uses Pipewire as the default audio backend. - Switching to PulseAudio seems to fix the issue. -\end_layout - -\begin_layout Enumerate -Run -\begin_inset Quotes eld -\end_inset - -sudo raspi-config -\begin_inset Quotes erd -\end_inset - - from a terminal window. -\end_layout - -\begin_layout Enumerate -Select -\begin_inset Quotes eld -\end_inset - -Advanced Options -\begin_inset Quotes erd -\end_inset - -. -\end_layout - -\begin_layout Enumerate -Select -\begin_inset Quotes eld -\end_inset - -Audio Config -\begin_inset Quotes erd -\end_inset - -. -\end_layout - -\begin_layout Enumerate -Select -\begin_inset Quotes eld -\end_inset - -PulseAudio -\begin_inset Quotes erd -\end_inset - -. -\end_layout - -\begin_layout Enumerate -Press TAB, select -\begin_inset Quotes eld -\end_inset - -Ok -\begin_inset Quotes erd -\end_inset - -, and let it reboot. -\end_layout - -\begin_layout Part -Game Development -\end_layout - -\begin_layout Section -Lua -\end_layout - -\begin_layout Standard -Singe uses the Lua programming language (http://www.lua.org) for scripting - game logic. - Lua is fast, lightweight, object-oriented, easy to use, and actually used - in the games industry. - A tutorial in Lua is beyond the scope of this document. - You can find video tutorials for Lua on our YouTube channel and additional - documentation by searching the web. -\end_layout - -\begin_layout Section -Basic Rules -\end_layout - -\begin_layout Itemize -Singe is cross-platform, therefore, things you don't normally need to worry - about on Windows are important. - Filenames are case sensitive. - "MyScript.singe" and "myscript.singe" are not the same thing! Also, you should - use UNIX path separators - that is "/" instead of " -\backslash -". - Not only will this make your code look better (since you don't have to - duplicate the forward slash to escape it) but it works everywhere Singe - is supported. -\end_layout - -\begin_layout Itemize -Pick a distinctive name for your game folder. - Do not use spaces. -\end_layout - -\begin_layout Itemize -Use the included Singe Framework. - Do not make a copy of this framework in your game scripts! By including - the provided framework you help with future compatibility as Singe is updated. - Begin (or end) your game script with: -\end_layout - -\begin_layout LyX-Code -dofile("Singe/Framework.singe") -\end_layout - -\begin_layout Itemize -Stay out of the "Singe" folder. - This is managed by Singe and anything added or changed here is subject - to future deletion. -\end_layout - -\begin_layout Itemize -Include a games.dat! This is extremely important for new users! While other - front ends and Singe-based menu systems exist, it's vital that you include - data for the built-in menu system. - Almost all support issues are due to this one simple little file! -\end_layout - -\begin_layout Itemize -Do not distribute non-essential files. - Never ship Singe binaries or Singe provided scripts with your game! Do - not ship index files generated from the videos. -\end_layout - -\begin_layout Section -Game Directory Layout -\end_layout - -\begin_layout Section -Packaging Your Game -\end_layout - -\begin_layout Section -Event Driven... - Or Not? -\end_layout - -\begin_layout Standard -Traditionally, Singe used an event-driven programming model. - That is, Singe handles everything and only calls your code when it needs - to tell you something or update the screen. - This is very efficient and is how most Lua-based game engines handle things. - However, it can be very verbose. - As of Singe 2.10 there is a new -\begin_inset Quotes eld -\end_inset - -threaded -\begin_inset Quotes erd -\end_inset - - model that allows you to write procedural code. -\end_layout - -\begin_layout Subsection -Event Driven -\end_layout - -\begin_layout Standard -With the event-driven programming model, Singe controls the main "program - loop" and is in charge of the order of program execution. - Singe automatically handles all the details of decoding and presenting - video and audio. - It manages controllers, mice, and keyboard input. - When Singe needs something game-specific, it calls part of your script. - The most basic Singe script that demonstrates all the existing "callbacks" - used by Singe looks like this: -\end_layout - -\begin_layout Standard -\begin_inset listings -lstparams "basicstyle={\tiny},tabsize=2" -inline false -status open - -\begin_layout Plain Layout - --- Singe Game Skeleton. -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - --- Load the Singe Framework. - -\end_layout - -\begin_layout Plain Layout - -dofile("Singe/Framework.singe") -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - --- Declare any global variables you need here. -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onControllerMoved(axis, value, which) -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - Reports which controller axis was moved as well as it's current value. -\end_layout - -\begin_layout Plain Layout - - (Range: -32768 to 32767) This is used for analog devices. - Digial input -\end_layout - -\begin_layout Plain Layout - - is handled by onInput and onKey. -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onInputPressed(what) -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - When in keyboard MODE_NORMAL, input events are reported here when the -\end_layout - -\begin_layout Plain Layout - - key or button is first depressed. - For a full list of keys/buttons/controllers, -\end_layout - -\begin_layout Plain Layout - - see Singe/Framework.singe. -\end_layout - -\begin_layout Plain Layout - - For MODE_FULL, this event will be called with the keysym of the key pressed. -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onInputReleased(what) -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - When in keyboard MODE_NORMAL, input events are reported here when the -\end_layout - -\begin_layout Plain Layout - - key or button is released. - For a full list of keys/buttons/controllers, -\end_layout - -\begin_layout Plain Layout - - see Singe/Framework.singe. -\end_layout - -\begin_layout Plain Layout - - For MODE_FULL, this event will be called with the keysym of the key released. -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onKeyPressed(key, scancode) -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - When in keyboard MODE_FULL, input events are reported here when the key - is -\end_layout - -\begin_layout Plain Layout - - pressed. - Both the keysym and scancode are returned. - For a list of available -\end_layout - -\begin_layout Plain Layout - - scancodes, see Singe/Framework.singe. -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onKeyReleased(key, scancode) -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - When in keyboard MODE_FULL, input events are reported here when the key - is -\end_layout - -\begin_layout Plain Layout - - released. - Both the keysym and scancode are returned. - For a list of available -\end_layout - -\begin_layout Plain Layout - - scancodes, see Singe/Framework.singe. -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onMouseMoved(x, y, xr, yr, which) -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - Called when the mouse is moved. - -\end_layout - -\begin_layout Plain Layout - - When in SINGLE_MOUSE mode, absolute X & Y values as well as the -\end_layout - -\begin_layout Plain Layout - - relative change in position is returned. - For MANY_MOUSE mode, only -\end_layout - -\begin_layout Plain Layout - - the relative change is available as well as which mouse was moved. - -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onOverlayUpdate() -\end_layout - -\begin_layout Plain Layout - - --[[ -\end_layout - -\begin_layout Plain Layout - - This is the only place you can safely perform drawing operations! - -\end_layout - -\begin_layout Plain Layout - - If you wish to display a targeting cursor, you will need to save the - -\end_layout - -\begin_layout Plain Layout - - mouse position from onMouseMoved in global variables and then use those - -\end_layout - -\begin_layout Plain Layout - - here to render the cursor. - -\end_layout - -\begin_layout Plain Layout - - --]] -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - - -- Tell Singe if we changed the display or not. - -\end_layout - -\begin_layout Plain Layout - - return(OVERLAY_UPDATED) -- Or OVERLAY_NOT_UPDATED if no drawing was done. - -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onShutdown() -\end_layout - -\begin_layout Plain Layout - - -- Called when the user exits your game. - Free loaded resources here. - -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -function onSoundCompleted(id) -\end_layout - -\begin_layout Plain Layout - - -- The sound "id" just finished playing. - -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - --- Note: There is no "onStartup" event. - -\end_layout - -\begin_layout Plain Layout - --- Any startup code you need can be placed here. -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\end_inset - - -\end_layout - -\begin_layout Subsection -Threaded -\end_layout - -\begin_layout Standard -As of Singe 2.10, you can now use the classic procedural programming model. - In order for this to work, you declare a -\begin_inset Quotes eld -\end_inset - -singeMain() -\begin_inset Quotes erd -\end_inset - - function and include the Singe framework at the end of your program rather - than at the beginning. - Lua is not truly multithreaded so this model relies on the game developer - - you - to cooperatively multitask by calling -\begin_inset Quotes eld -\end_inset - -singeYield() -\begin_inset Quotes erd -\end_inset - - anywhere your code consumes any substantial amount of time. - As with the event-driven model, Singe still manages all input, video decoding, - etc. - A sample threaded program looks like this: -\end_layout - -\begin_layout Standard -\begin_inset listings -lstparams "basicstyle={\tiny},tabsize=2" -inline false -status open - -\begin_layout Plain Layout - -function singeMain() -\end_layout - -\begin_layout Plain Layout - - x = 10 -\end_layout - -\begin_layout Plain Layout - - y = 10 -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - - while(true) do -\end_layout - -\begin_layout Plain Layout - - if SINGE_OK_TO_DRAW then -\end_layout - -\begin_layout Plain Layout - - colorBackground(0, 0, 0, 255) -\end_layout - -\begin_layout Plain Layout - - overlayClear() -\end_layout - -\begin_layout Plain Layout - - -\end_layout - -\begin_layout Plain Layout - - colorForeground(255, 255, 255, 255) -\end_layout - -\begin_layout Plain Layout - - overlayPrint(x, y, "+") -\end_layout - -\begin_layout Plain Layout - - end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - - if keyboardGetLastUp() == SCANCODE.LEFT.value then x = x - 1 end -\end_layout - -\begin_layout Plain Layout - - if keyboardGetLastUp() == SCANCODE.RIGHT.value then x = x + 1 end -\end_layout - -\begin_layout Plain Layout - - if keyboardGetLastUp() == SCANCODE.UP.value then y = y - 1 end -\end_layout - -\begin_layout Plain Layout - - if keyboardGetLastUp() == SCANCODE.DOWN.value then y = y + 1 end -\end_layout - -\begin_layout Plain Layout - - -\end_layout - -\begin_layout Plain Layout - - singeYield() -\end_layout - -\begin_layout Plain Layout - - end -\end_layout - -\begin_layout Plain Layout - -end -\end_layout - -\begin_layout Plain Layout - -\end_layout - -\begin_layout Plain Layout - -dofile("Singe/Framework.singe") -\end_layout - -\end_inset - - -\end_layout - -\begin_layout Subsection -Hybrid -\end_layout - -\begin_layout Section -games.dat -\end_layout - -\begin_layout Standard -The games.dat file allows Singe to automatically locate new games when they - are installed by the end user. - This file is extremely important and must be included with every Singe - game! Place games.dat in the top-most directory of your game. - An example (containing multiple games) is below: -\end_layout - -\begin_layout Standard -\begin_inset listings -lstparams "basicstyle={\tiny},tabsize=2" -inline false -status open - -\begin_layout Plain Layout - -GAMES = { -\end_layout - -\begin_layout Plain Layout - - { -\end_layout - -\begin_layout Plain Layout - - TITLE = ".38 Ambush Alley", -\end_layout - -\begin_layout Plain Layout - - SCRIPT = "ActionMax/38AmbushAlley.singe", -\end_layout - -\begin_layout Plain Layout - - VIDEO = "ActionMax/frame_38AmbushAlley.txt", -\end_layout - -\begin_layout Plain Layout - - DATA = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - STRETCH = false, -\end_layout - -\begin_layout Plain Layout - - NO_MOUSE = false, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_X = 720, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_Y = 480, -\end_layout - -\begin_layout Plain Layout - - SINDEN_GUN = "", -\end_layout - -\begin_layout Plain Layout - - CABINET = "ActionMax/cabinet_38AmbushAlley.png", -\end_layout - -\begin_layout Plain Layout - - MARQUEE = "ActionMax/marquee_ActionMax.png", -\end_layout - -\begin_layout Plain Layout - - ATTRACT = "ActionMax/video_38AmbushAlley.mkv", -\end_layout - -\begin_layout Plain Layout - - ATTRACT_START = 3000, -\end_layout - -\begin_layout Plain Layout - - ATTRACT_END = 3500, -\end_layout - -\begin_layout Plain Layout - - YEAR = 1987, -\end_layout - -\begin_layout Plain Layout - - PLATFORM = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - DEVELOPER = "Sourcing International, Ltd.", -\end_layout - -\begin_layout Plain Layout - - PUBLISHER = "Worlds of Wonder, Inc.", -\end_layout - -\begin_layout Plain Layout - - GENRE = "Shooter", -\end_layout - -\begin_layout Plain Layout - - DESCRIPTION = "Get your target practice in with real police officers - then hit the streets.", -\end_layout - -\begin_layout Plain Layout - - CREATOR = "Scott Duensing", -\end_layout - -\begin_layout Plain Layout - - SOURCE = "http://kangaroopunch.com" -\end_layout - -\begin_layout Plain Layout - - }, -\end_layout - -\begin_layout Plain Layout - - { -\end_layout - -\begin_layout Plain Layout - - TITLE = "Blue Thunder", -\end_layout - -\begin_layout Plain Layout - - SCRIPT = "ActionMax/BlueThunder.singe", -\end_layout - -\begin_layout Plain Layout - - VIDEO = "ActionMax/frame_BlueThunder.txt", -\end_layout - -\begin_layout Plain Layout - - DATA = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - STRETCH = false, -\end_layout - -\begin_layout Plain Layout - - NO_MOUSE = false, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_X = 720, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_Y = 480, -\end_layout - -\begin_layout Plain Layout - - SINDEN_GUN = "", -\end_layout - -\begin_layout Plain Layout - - CABINET = "ActionMax/cabinet_BlueThunder.png", -\end_layout - -\begin_layout Plain Layout - - MARQUEE = "ActionMax/marquee_ActionMax.png", -\end_layout - -\begin_layout Plain Layout - - ATTRACT = "ActionMax/video_BlueThunder.mkv", -\end_layout - -\begin_layout Plain Layout - - ATTRACT_START = 3000, -\end_layout - -\begin_layout Plain Layout - - ATTRACT_END = 3500, -\end_layout - -\begin_layout Plain Layout - - YEAR = 1987, -\end_layout - -\begin_layout Plain Layout - - PLATFORM = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - DEVELOPER = "Sourcing International, Ltd.", -\end_layout - -\begin_layout Plain Layout - - PUBLISHER = "Worlds of Wonder, Inc.", -\end_layout - -\begin_layout Plain Layout - - GENRE = "Shooter", -\end_layout - -\begin_layout Plain Layout - - DESCRIPTION = "Get in your chopper and take out the bad guys in this - action-packed game.", -\end_layout - -\begin_layout Plain Layout - - CREATOR = "Scott Duensing", -\end_layout - -\begin_layout Plain Layout - - SOURCE = "http://kangaroopunch.com" -\end_layout - -\begin_layout Plain Layout - - }, -\end_layout - -\begin_layout Plain Layout - - { -\end_layout - -\begin_layout Plain Layout - - TITLE = "Hydrosub: 2021", -\end_layout - -\begin_layout Plain Layout - - SCRIPT = "ActionMax/Hydrosub2021.singe", -\end_layout - -\begin_layout Plain Layout - - VIDEO = "ActionMax/frame_Hydrosub2021.txt", -\end_layout - -\begin_layout Plain Layout - - DATA = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - STRETCH = false, -\end_layout - -\begin_layout Plain Layout - - NO_MOUSE = false, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_X = 720, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_Y = 480, -\end_layout - -\begin_layout Plain Layout - - SINDEN_GUN = "", -\end_layout - -\begin_layout Plain Layout - - CABINET = "ActionMax/cabinet_Hydrosub2021.png", -\end_layout - -\begin_layout Plain Layout - - MARQUEE = "ActionMax/marquee_ActionMax.png", -\end_layout - -\begin_layout Plain Layout - - ATTRACT = "ActionMax/video_Hydrosub2021.mkv", -\end_layout - -\begin_layout Plain Layout - - ATTRACT_START = 3000, -\end_layout - -\begin_layout Plain Layout - - ATTRACT_END = 3500, -\end_layout - -\begin_layout Plain Layout - - YEAR = 1987, -\end_layout - -\begin_layout Plain Layout - - PLATFORM = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - DEVELOPER = "Sourcing International, Ltd.", -\end_layout - -\begin_layout Plain Layout - - PUBLISHER = "Worlds of Wonder, Inc.", -\end_layout - -\begin_layout Plain Layout - - GENRE = "Shooter", -\end_layout - -\begin_layout Plain Layout - - DESCRIPTION = "Shootout beneath the ocean!", -\end_layout - -\begin_layout Plain Layout - - CREATOR = "Scott Duensing", -\end_layout - -\begin_layout Plain Layout - - SOURCE = "http://kangaroopunch.com" -\end_layout - -\begin_layout Plain Layout - - }, -\end_layout - -\begin_layout Plain Layout - - { -\end_layout - -\begin_layout Plain Layout - - TITLE = "Rescue of Pops Ghostly, The", -\end_layout - -\begin_layout Plain Layout - - SCRIPT = "ActionMax/PopsGhostly.singe", -\end_layout - -\begin_layout Plain Layout - - VIDEO = "ActionMax/frame_PopsGhostly.txt", -\end_layout - -\begin_layout Plain Layout - - DATA = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - STRETCH = false, -\end_layout - -\begin_layout Plain Layout - - NO_MOUSE = false, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_X = 720, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_Y = 480, -\end_layout - -\begin_layout Plain Layout - - SINDEN_GUN = "", -\end_layout - -\begin_layout Plain Layout - - CABINET = "ActionMax/cabinet_PopsGhostly.png", -\end_layout - -\begin_layout Plain Layout - - MARQUEE = "ActionMax/marquee_ActionMax.png", -\end_layout - -\begin_layout Plain Layout - - ATTRACT = "ActionMax/video_PopsGhostly.mkv", -\end_layout - -\begin_layout Plain Layout - - ATTRACT_START = 3000, -\end_layout - -\begin_layout Plain Layout - - ATTRACT_END = 3500, -\end_layout - -\begin_layout Plain Layout - - YEAR = 1987, -\end_layout - -\begin_layout Plain Layout - - PLATFORM = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - DEVELOPER = "Sourcing International, Ltd.", -\end_layout - -\begin_layout Plain Layout - - PUBLISHER = "Worlds of Wonder, Inc.", -\end_layout - -\begin_layout Plain Layout - - GENRE = "Shooter", -\end_layout - -\begin_layout Plain Layout - - DESCRIPTION = "Help Pops Ghostly and his family get rid of the bad spirits - who have taken over the house.", -\end_layout - -\begin_layout Plain Layout - - CREATOR = "Scott Duensing", -\end_layout - -\begin_layout Plain Layout - - SOURCE = "http://kangaroopunch.com" -\end_layout - -\begin_layout Plain Layout - - }, -\end_layout - -\begin_layout Plain Layout - - { -\end_layout - -\begin_layout Plain Layout - - TITLE = "Sonic Fury", -\end_layout - -\begin_layout Plain Layout - - SCRIPT = "ActionMax/SonicFury.singe", -\end_layout - -\begin_layout Plain Layout - - VIDEO = "ActionMax/frame_SonicFury.txt", -\end_layout - -\begin_layout Plain Layout - - DATA = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - STRETCH = false, -\end_layout - -\begin_layout Plain Layout - - NO_MOUSE = false, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_X = 720, -\end_layout - -\begin_layout Plain Layout - - RESOLUTION_Y = 480, -\end_layout - -\begin_layout Plain Layout - - SINDEN_GUN = "", -\end_layout - -\begin_layout Plain Layout - - CABINET = "ActionMax/cabinet_SonicFury.png", -\end_layout - -\begin_layout Plain Layout - - MARQUEE = "ActionMax/marquee_ActionMax.png", -\end_layout - -\begin_layout Plain Layout - - ATTRACT = "ActionMax/video_SonicFury.mkv", -\end_layout - -\begin_layout Plain Layout - - ATTRACT_START = 3000, -\end_layout - -\begin_layout Plain Layout - - ATTRACT_END = 3500, -\end_layout - -\begin_layout Plain Layout - - YEAR = 1987, -\end_layout - -\begin_layout Plain Layout - - PLATFORM = "ActionMax", -\end_layout - -\begin_layout Plain Layout - - DEVELOPER = "Sourcing International, Ltd.", -\end_layout - -\begin_layout Plain Layout - - PUBLISHER = "Worlds of Wonder, Inc.", -\end_layout - -\begin_layout Plain Layout - - GENRE = "Shooter", -\end_layout - -\begin_layout Plain Layout - - DESCRIPTION = "Shoot it out with the bad guys in your fighter jet!", -\end_layout - -\begin_layout Plain Layout - - CREATOR = "Scott Duensing", -\end_layout - -\begin_layout Plain Layout - - SOURCE = "http://kangaroopunch.com" -\end_layout - -\begin_layout Plain Layout - - } -\end_layout - -\begin_layout Plain Layout - -} -\end_layout - -\end_inset - - -\end_layout - -\begin_layout Section -Included Libraries -\end_layout - -\begin_layout Standard -In addition to the standard Lua libraries and the Singe API, the following - libraries are also available for use in Singe programs without the need - to install any additional software: -\end_layout - -\begin_layout Description -Copas - Asynchronous networking -\end_layout - -\begin_layout Description -LuaFileSystem - Expanded filesystem support -\end_layout - -\begin_layout Description -LuaJSON - JavaScript Object Notation encoding and decoding -\end_layout - -\begin_layout Description -LuaSec - TLS/SSL communication -\end_layout - -\begin_layout Description -LuaSocket - TCP and UDP -\end_layout - -\begin_layout Description -LuaRS232 - RS232 serial port access -\end_layout - -\begin_layout Standard -Their usage is beyond the scope of this document. -\end_layout - -\begin_layout Section -Video, Audio, and Container Formats -\end_layout - -\begin_layout Section -API -\end_layout - -\begin_layout Subsection -Color -\end_layout - -\begin_layout Subsubsection -colorBackground -\end_layout - -\begin_layout LyX-Code -colorBackground(r, g, b) -\end_layout - -\begin_layout LyX-Code -colorBackground(r, g, b, o) -\end_layout - -\begin_layout Standard -Specifies the background color to use for following drawing operations. - Red, green, blue, and opacity values are from 0 to 255. - If you omit the opacity value, it will default to 0 (transparent). - The three-argument version of this function is to provide compatibility - with earlier Singe releases. -\end_layout - -\begin_layout Labeling -\labelwidthstring 00.00.0000 -Example: -\end_layout - -\begin_layout LyX-Code -colorBackground(255, 0, 0, 255) – Solid Red -\end_layout - -\begin_layout Subsubsection -colorForeground -\end_layout - -\begin_layout LyX-Code -colorForeground(r, g, b) -\end_layout - -\begin_layout LyX-Code -colorForeground(r, g, b, o) -\end_layout - -\begin_layout Standard -Specifies the foreground color to use for following drawing operations. - Red, green, blue, and opacity values are from 0 to 255. - If you omit the opacity value, it will default to 255 (opaque). - The three-argument version of this function is to provide compatibility - with earlier Singe releases. -\end_layout - -\begin_layout Labeling -\labelwidthstring 00.00.0000 -Example: -\end_layout - -\begin_layout LyX-Code -colorForeground(255, 255, 255, 255) – Solid White -\end_layout - -\begin_layout Subsection -Controller -\end_layout - -\begin_layout Subsubsection -controllerGetAxis -\end_layout - -\begin_layout LyX-Code -v = controllerGetAxis(c, a) -\end_layout - -\begin_layout Standard -Returns the value of the specified axis (a) on controller (c). - Returned values are from -32768 to 32767. -\end_layout - -\begin_layout Subsubsection -controllerGetButton -\end_layout - -\begin_layout Subsection -Debug -\end_layout - -\begin_layout Subsubsection -debugPrint -\end_layout - -\begin_layout Subsection -Disc -\end_layout - -\begin_layout Subsubsection -discAudio -\end_layout - -\begin_layout Subsubsection -discChangeSpeed -\end_layout - -\begin_layout Subsubsection -discGetAudioTrack -\end_layout - -\begin_layout Subsubsection -discGetAudioTracks -\end_layout - -\begin_layout Subsubsection -discGetFrame -\end_layout - -\begin_layout Subsubsection -discGetHeight -\end_layout - -\begin_layout Subsubsection -discGetLanguage -\end_layout - -\begin_layout Subsubsection -discGetState -\end_layout - -\begin_layout Subsubsection -discGetWidth -\end_layout - -\begin_layout Subsubsection -discPause -\end_layout - -\begin_layout Subsubsection -discPauseAtFrame -\end_layout - -\begin_layout Subsubsection -discPlay -\end_layout - -\begin_layout Subsubsection -discSearch -\end_layout - -\begin_layout Subsubsection -discSearchBlanking -\end_layout - -\begin_layout Subsubsection -discSetAudioTrack -\end_layout - -\begin_layout Subsubsection -discSetFPS -\end_layout - -\begin_layout Subsubsection -discSkipBackward -\end_layout - -\begin_layout Subsubsection -discSkipBlanking -\end_layout - -\begin_layout Subsubsection -discSkipForward -\end_layout - -\begin_layout Subsubsection -discSkipToFrame -\end_layout - -\begin_layout Subsubsection -discStepBackward -\end_layout - -\begin_layout Subsubsection -discStepForward -\end_layout - -\begin_layout Subsubsection -discStop -\end_layout - -\begin_layout Subsection -Font -\end_layout - -\begin_layout Subsubsection -fontLoad -\end_layout - -\begin_layout Subsubsection -fontPrint -\end_layout - -\begin_layout Subsubsection -fontQuality -\end_layout - -\begin_layout Subsubsection -fontSelect -\end_layout - -\begin_layout Subsubsection -fontToSprite -\end_layout - -\begin_layout Subsubsection -fontUnload -\end_layout - -\begin_layout Subsection -Keyboard -\end_layout - -\begin_layout Subsubsection -keyboardGetLastDown -\end_layout - -\begin_layout Subsubsection -keyboardGetLastUp -\end_layout - -\begin_layout Subsubsection -keyboardGetMode -\end_layout - -\begin_layout Subsubsection -keyboardGetModifiers -\end_layout - -\begin_layout Subsubsection -keyboardSetMode -\end_layout - -\begin_layout Subsubsection -keyboardIsDown -\end_layout - -\begin_layout Subsection -Mouse -\end_layout - -\begin_layout Subsubsection -mouseEnable -\end_layout - -\begin_layout Subsubsection -mouseDisable -\end_layout - -\begin_layout Subsubsection -mouseGetPosition -\end_layout - -\begin_layout Subsubsection -mouseHowMany -\end_layout - -\begin_layout Subsubsection -mouseSetCaptured -\end_layout - -\begin_layout Subsubsection -mouseSetMode -\end_layout - -\begin_layout Subsection -Overlay -\end_layout - -\begin_layout Subsubsection -overlayBox -\end_layout - -\begin_layout Subsubsection -overlayCircle -\end_layout - -\begin_layout Subsubsection -overlayClear -\end_layout - -\begin_layout Subsubsection -overlayEllipse -\end_layout - -\begin_layout Subsubsection -overlayGetHeight -\end_layout - -\begin_layout Subsubsection -overlayGetWidth -\end_layout - -\begin_layout Subsubsection -overlayLine -\end_layout - -\begin_layout Subsubsection -overlayPlot -\end_layout - -\begin_layout Subsubsection -overlayPrint -\end_layout - -\begin_layout Subsubsection -overlaySetResolution -\end_layout - -\begin_layout Subsection -Script -\end_layout - -\begin_layout Subsubsection -scriptExecute -\end_layout - -\begin_layout Subsubsection -scriptPush -\end_layout - -\begin_layout Subsection -Singe -\end_layout - -\begin_layout Subsubsection -singeDisablePauseKey -\end_layout - -\begin_layout Subsubsection -singeEnablePauseKey -\end_layout - -\begin_layout Subsubsection -singeGetDataPath -\end_layout - -\begin_layout Subsubsection -singeGetHeight -\end_layout - -\begin_layout Subsubsection -singeGetPauseFlag -\end_layout - -\begin_layout Subsubsection -singeGetScriptPath -\end_layout - -\begin_layout Subsubsection -singeGetWidth -\end_layout - -\begin_layout Subsubsection -singeScreenshot -\end_layout - -\begin_layout Subsubsection -singeSetGameName -\end_layout - -\begin_layout Subsubsection -singeSetPauseFlag -\end_layout - -\begin_layout Subsubsection -singeQuit -\end_layout - -\begin_layout Subsubsection -singeVersion -\end_layout - -\begin_layout Subsubsection -singeWantsCrosshairs -\end_layout - -\begin_layout Subsection -Sound -\end_layout - -\begin_layout Subsubsection -soundFullStop -\end_layout - -\begin_layout Subsubsection -soundGetVolume -\end_layout - -\begin_layout Subsubsection -soundIsPlaying -\end_layout - -\begin_layout Subsubsection -soundLoad -\end_layout - -\begin_layout Subsubsection -soundPause -\end_layout - -\begin_layout Subsubsection -soundPlay -\end_layout - -\begin_layout Subsubsection -soundResume -\end_layout - -\begin_layout Subsubsection -soundSetVolume -\end_layout - -\begin_layout Subsubsection -soundStop -\end_layout - -\begin_layout Subsubsection -soundUnload -\end_layout - -\begin_layout Subsection -Sprite -\end_layout - -\begin_layout Subsubsection -spriteDraw -\end_layout - -\begin_layout Subsubsection -spriteGetFrame -\end_layout - -\begin_layout Subsubsection -spriteGetHeight -\end_layout - -\begin_layout Subsubsection -spriteGetWidth -\end_layout - -\begin_layout Subsubsection -spriteIsPlaying -\end_layout - -\begin_layout Subsubsection -spriteLoad -\end_layout - -\begin_layout Subsubsection -spriteLoop -\end_layout - -\begin_layout Subsubsection -spritePause -\end_layout - -\begin_layout Subsubsection -spritePlay -\end_layout - -\begin_layout Subsubsection -spriteQuality -\end_layout - -\begin_layout Subsubsection -spriteRotate -\end_layout - -\begin_layout Subsubsection -spriteRotateAndScale -\end_layout - -\begin_layout Subsubsection -spriteScale -\end_layout - -\begin_layout Subsubsection -spriteSetFrame -\end_layout - -\begin_layout Subsubsection -spriteUnload -\end_layout - -\begin_layout Subsection -Video -\end_layout - -\begin_layout Subsubsection -videoDraw -\end_layout - -\begin_layout Subsubsection -videoGetAudioTrack -\end_layout - -\begin_layout Subsubsection -videoGetAudioTracks -\end_layout - -\begin_layout Subsubsection -videoGetFrame -\end_layout - -\begin_layout Subsubsection -videoGetFrameCount -\end_layout - -\begin_layout Subsubsection -videoGetHeight -\end_layout - -\begin_layout Subsubsection -videoGetLanguage -\end_layout - -\begin_layout Subsubsection -videoGetLanguageDescription -\end_layout - -\begin_layout Subsubsection -videoGetVolume -\end_layout - -\begin_layout Subsubsection -videoGetWidth -\end_layout - -\begin_layout Subsubsection -videoIsPlaying -\end_layout - -\begin_layout Subsubsection -videoLoad -\end_layout - -\begin_layout Subsubsection -videoPause -\end_layout - -\begin_layout Subsubsection -videoPlay -\end_layout - -\begin_layout Subsubsection -videoQuality -\end_layout - -\begin_layout Subsubsection -videoRotate -\end_layout - -\begin_layout Subsubsection -videoRotateAndScale -\end_layout - -\begin_layout Subsubsection -videoScale -\end_layout - -\begin_layout Subsubsection -videoSeek -\end_layout - -\begin_layout Subsubsection -videoSetAudioTrack -\end_layout - -\begin_layout Subsubsection -videoSetVolume -\end_layout - -\begin_layout Subsubsection -videoUnload -\end_layout - -\begin_layout Subsection -VLDP -\end_layout - -\begin_layout Subsubsection -vldpGetHeight -\end_layout - -\begin_layout Subsubsection -vldpGetPixel -\end_layout - -\begin_layout Subsubsection -vldpGetWidth -\end_layout - -\begin_layout Subsubsection -vldpSetVerbose -\end_layout - -\end_body -\end_document diff --git a/assets/Menu.singe b/assets/Menu.singe index 9a76abf80..85976dd81 100644 --- a/assets/Menu.singe +++ b/assets/Menu.singe @@ -24,7 +24,7 @@ dofile("Singe/Framework.singe") -lfs = require("lfs") +local lfs = require("lfs") function cleanTitle(a) @@ -133,12 +133,12 @@ function onOverlayUpdate() -- Cabinet image x = CABINET_X + (CABINET_W - spriteGetWidth(SPRITE_CABINET)) * 0.5 y = CABINET_Y + (CABINET_H - spriteGetHeight(SPRITE_CABINET)) * 0.5 - spriteDraw(x, y, SPRITE_CABINET) + spriteDraw(SPRITE_CABINET, x, y) -- Marquee Image x = MARQUEE_X + (MARQUEE_W - spriteGetWidth(SPRITE_MARQUEE)) * 0.5 y = MARQUEE_Y + (MARQUEE_H - spriteGetHeight(SPRITE_MARQUEE)) * 0.5 - spriteDraw(x, y, SPRITE_MARQUEE) + spriteDraw(SPRITE_MARQUEE, x, y) -- Attract Mode Video videoDraw(VIDEO_ATTRACT, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W, VIDEO_Y + VIDEO_H) @@ -163,7 +163,7 @@ function onOverlayUpdate() if (t >= TEXT_LINE_TOP) then if (c < TEXT_LINE_LIMIT) then if (handle >= 0) then - spriteDraw(TEXT_X, y, handle) + spriteDraw(handle, TEXT_X, y) end y = y + TEXT_LINE_HEIGHT + 1 c = c + 1 @@ -186,6 +186,9 @@ end function onShutdown() unloadGameAssets() saveConfig(not SHUTDOWN_FROM_PUSH) + if freeSans18 then + fontUnload(freeSans18) + end end @@ -193,18 +196,32 @@ function saveConfig(showIntro) if GAME_COUNT > 0 then -- Save what game we're currently viewing local cfg = io.open(CONFIG_FILE, "w") + if cfg then cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n") cfg:write("SHOW_INTRO = " .. tostring(showIntro) .. "\n") - cfg:close() + cfg:close() + else + debugPrint("Unable to write " .. CONFIG_FILE) + end end end function unloadGameAssets() - spriteUnload(SPRITE_CABINET) - spriteUnload(SPRITE_MARQUEE) - videoUnload(VIDEO_ATTRACT) - for _, handle in ipairs(TEXT_SPRITE_LIST) do + -- Nothing is loaded when there are no games. + if SPRITE_CABINET then + spriteUnload(SPRITE_CABINET) + SPRITE_CABINET = nil + end + if SPRITE_MARQUEE then + spriteUnload(SPRITE_MARQUEE) + SPRITE_MARQUEE = nil + end + if VIDEO_ATTRACT then + videoUnload(VIDEO_ATTRACT) + VIDEO_ATTRACT = nil + end + for _, handle in ipairs(TEXT_SPRITE_LIST or {}) do if handle >= 0 then spriteUnload(handle) end @@ -301,7 +318,7 @@ if GAME_COUNT == 0 then debugPrint("No games found! Exiting.") singeQuit() else - overlaySetResolution(vldpGetWidth(), vldpGetHeight()) + overlaySetResolution(discGetWidth(), discGetHeight()) freeSans18 = fontLoad("Singe/FreeSansBold.ttf", 18) fontQuality(FONT_QUALITY_BLENDED) @@ -363,6 +380,8 @@ else local confattr = lfs.attributes(CONFIG_FILE) if confattr then dofile(CONFIG_FILE) + -- A damaged menu.dat must not take the menu down with it. + GAME_SELECTED = tonumber(GAME_SELECTED) or 1 if GAME_SELECTED > GAME_COUNT then GAME_SELECTED = GAME_COUNT end diff --git a/assets/Service.singe b/assets/Service.singe index ac27b5423..d17e39370 100644 --- a/assets/Service.singe +++ b/assets/Service.singe @@ -69,10 +69,10 @@ function onOverlayUpdate() overlayPrint(1, 3, dldGameList.result) end - colorForeground(255, 0, 0) + colorForeground(255, 0, 0, 255) overlayBox(0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1) - spriteDraw((overlayGetWidth() - spriteGetWidth(sprServiceMenu)) / 2, 25, sprServiceMenu) + spriteDraw(sprServiceMenu, (overlayGetWidth() - spriteGetWidth(sprServiceMenu)) / 2, 25) return(OVERLAY_UPDATED) @@ -92,7 +92,7 @@ copas.running = true dldGameList = get("https://kangaroopunch.com/api/singeSoftware") -overlaySetResolution(vldpGetWidth(), vldpGetHeight()) +overlaySetResolution(discGetWidth(), discGetHeight()) DISC_SINGE_LOGO = 140 diff --git a/assets/controls.cfg b/assets/controls.cfg index c8c68d6f4..f7aadffb7 100644 --- a/assets/controls.cfg +++ b/assets/controls.cfg @@ -26,27 +26,29 @@ DEAD_ZONE = 15000 +-- One table per switch, in the order the engine numbers them (SWITCH_UP = 0 ...). +-- Each entry is a { name, value } pair from the SCANCODE, GAMEPAD_N, or MOUSE_N tables. INPUT_UP = { SCANCODE.UP, SCANCODE.KP_8, GAMEPAD_0.AXIS_LEFT_Y_U, GAMEPAD_0.AXIS_RIGHT_Y_U, GAMEPAD_0.DPAD_UP } INPUT_LEFT = { SCANCODE.LEFT, SCANCODE.KP_4, GAMEPAD_0.AXIS_LEFT_X_L, GAMEPAD_0.AXIS_RIGHT_X_L, GAMEPAD_0.DPAD_LEFT } INPUT_DOWN = { SCANCODE.DOWN, SCANCODE.KP_2, GAMEPAD_0.AXIS_LEFT_Y_D, GAMEPAD_0.AXIS_RIGHT_Y_D, GAMEPAD_0.DPAD_DOWN } INPUT_RIGHT = { SCANCODE.RIGHT, SCANCODE.KP_6, GAMEPAD_0.AXIS_LEFT_X_R, GAMEPAD_0.AXIS_RIGHT_X_R, GAMEPAD_0.DPAD_RIGHT } -INPUT_1P_COIN = { SCANCODE.MAIN_5, SCANCODE.C, GAMEPAD_0.BUTTON_LEFT_BUMPER } -INPUT_2P_COIN = { SCANCODE.MAIN_6 } INPUT_1P_START = { SCANCODE.MAIN_1, GAMEPAD_0.BUTTON_RIGHT_BUMPER } INPUT_2P_START = { SCANCODE.MAIN_2 } INPUT_ACTION_1 = { SCANCODE.SPACE, SCANCODE.LCTRL, GAMEPAD_0.BUTTON_A, MOUSE_0.BUTTON_RIGHT } INPUT_ACTION_2 = { SCANCODE.LALT, GAMEPAD_0.BUTTON_B, MOUSE_0.BUTTON_MIDDLE } INPUT_ACTION_3 = { SCANCODE.LSHIFT, GAMEPAD_0.BUTTON_X, MOUSE_0.BUTTON_LEFT } -INPUT_ACTION_4 = { SCANCODE.RSHIFT, GAMEPAD_0.BUTTON_Y, MOUSE_0.BUTTON_X1 } +INPUT_1P_COIN = { SCANCODE.MAIN_5, SCANCODE.C, GAMEPAD_0.BUTTON_LEFT_BUMPER } +INPUT_2P_COIN = { SCANCODE.MAIN_6 } INPUT_SKILL_EASY = { SCANCODE.KP_DIVIDE } INPUT_SKILL_MEDIUM = { SCANCODE.KP_MULTIPLY } INPUT_SKILL_HARD = { SCANCODE.KP_MINUS } INPUT_SERVICE = { SCANCODE.MAIN_9 } INPUT_TEST_MODE = { SCANCODE.F2 } INPUT_RESET_CPU = { SCANCODE.F3 } -INPUT_SCREENSHOT = { SCANCODE.F12, SCANCODE.F11, GAMEPAD_0.BUTTON_BACK } +INPUT_SCREENSHOT = { SCANCODE.F12, SCANCODE.F11, GAMEPAD_0.BUTTON_BACK } -- F12 is taken by some desktops and debuggers, so F11 works too. INPUT_QUIT = { SCANCODE.ESCAPE, SCANCODE.Q } INPUT_PAUSE = { SCANCODE.P, GAMEPAD_0.BUTTON_START } INPUT_CONSOLE = { SCANCODE.GRAVE } +INPUT_ACTION_4 = { SCANCODE.RSHIFT, GAMEPAD_0.BUTTON_Y, MOUSE_0.BUTTON_X1 } INPUT_TILT = { SCANCODE.T } INPUT_GRAB = { SCANCODE.G } diff --git a/build-all.sh b/build-all.sh index 00f25410b..37663ac72 100755 --- a/build-all.sh +++ b/build-all.sh @@ -1,4 +1,4 @@ -#!/bin/bash -x +#!/bin/bash # # Singe 2 @@ -40,9 +40,9 @@ function buildAll() { G_GENERATED=${G_TARGET}/generated COMMON="-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${G_TARGET} -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE}" - export LDFLAGS="-L${G_TARGET}/lib ${LDFLAGS}" - export CFLAGS="-I${G_TARGET}/include ${CFLAGS}" - export CXXFLAGS="-I${G_TARGET}/include ${CXXFLAGS}" + export LDFLAGS="-L${G_TARGET}/lib ${LDFLAGS:-}" + export CFLAGS="-I${G_TARGET}/include ${CFLAGS:-}" + export CXXFLAGS="-I${G_TARGET}/include ${CXXFLAGS:-}" export LD_LIBRARY_PATH="${G_TARGET}/lib" export PKG_CONFIG_LIBDIR="${G_TARGET}/lib/pkgconfig" @@ -55,7 +55,6 @@ function buildAll() { sudo chroot ${SYSROOT} apt-get -y install libasound-dev libxi-dev libvdpau-dev fi -if [[ 1 == 1 ]]; then pushd thirdparty/bzip2 clearAndEnterBuild cmake ${COMMON} \ @@ -92,7 +91,6 @@ if [[ 1 == 1 ]]; then ../build/cmake make install popd -fi pushd thirdparty/SDL2 clearAndEnterBuild @@ -129,7 +127,7 @@ fi -DWEBP_BUILD_WEBPMUX=off \ -DWEBP_BUILD_EXTRAS=off \ .. - # 'make install' failes on zlib, which we don't want anyway. + # 'make install' fails on zlib, which we don't want anyway. make cp -f ../include/SDL_image.h "${G_TARGET}/include/SDL2/." cp -f libSDL2_image.a "${G_TARGET}/lib/." @@ -244,15 +242,10 @@ fi no-tests \ zlib \ enable-zstd - make install + make build_sw + make install_sw popd - lua thirdparty/luasec/src/options.lua -g "${G_TARGET}/include/openssl/ssl.h" > "${G_TARGET}/generated/luasec_options.c" - mkdir -p "${G_TARGET}/include/luasocket" - cp -f \ - thirdparty/luasocket/src/*.h \ - "${G_TARGET}/include/luasocket/." - if [[ "${OS}" == "pi" ]]; then # Hack to make ffmpeg compile. mkdir -p "${G_TARGET}/include/sys" @@ -287,14 +280,8 @@ fi popd pushd thirdparty/ffms2 - #libtoolize --force - #aclocal - #autoheader - #automake --force-missing --add-missing - #autoupdate - #autoconf - #./autogen.sh - #make distclean || true + # The configure script is not checked in; generate it once. + [[ -x configure ]] || NOCONFIGURE=1 ./autogen.sh clearAndEnterBuild ../configure \ --prefix=${G_TARGET} \ @@ -307,97 +294,14 @@ fi make install-libLTLIBRARIES # This weird target prevents building the command line tools. popd - # === Known FFMPEG Types === - createExtensionHeader ffmpeg > ${G_GENERATED}/extensions.h - - # === Overlay Font === - createEmbeddedImage font - - # === Window Icon === - createEmbeddedImage icon - - # === Kangaroo Punch Logo === - createEmbeddedImage kangarooPunchLogo - - # === Singe Logo === - createEmbeddedImage singeLogo - - # === Laser Disc === - createEmbeddedImage laserDisc - - # === Magnifying Glass === - createEmbeddedImage magnifyingGlass - - # === "Indexing" Text === - createEmbeddedImage indexing - - # === Singe Menu Font === - createEmbeddedBinary assets/FreeSansBold.ttf ${G_GENERATED}/FreeSansBold_ttf.h FREESANSBOLD_TTF_H - - # === Singe Menu Background Video === - ffmpeg -i "assets/Singe Engine Intro.mpg" -filter:v 'crop=ih/3*4:ih' -vf scale=720:480 -c:v libx264 -c:a aac -f matroska ${G_TARGET}/temp1.mkv - ffmpeg -i assets/180503_01_PurpleGrid.mp4 -filter:v 'crop=ih/3*4:ih' -vf scale=720:480 -c:v libx264 -c:a aac -f matroska ${G_TARGET}/temp2.mkv - ffmpeg -f concat -safe 0 -i <(echo -e "file ${G_TARGET}/temp1.mkv\nfile ${G_TARGET}/temp2.mkv\n") -c copy ${G_TARGET}/menuBackground.mkv - createEmbeddedBinary ${G_TARGET}/menuBackground.mkv ${G_GENERATED}/menuBackground_mkv.h MENUBACKGROUND_MKV_H - rm ${G_TARGET}/temp1.mkv ${G_TARGET}/temp2.mkv ${G_TARGET}/menuBackground.mkv - - # === LuaSocket === - createEmbeddedBinary thirdparty/luasocket/src/ftp.lua ${G_GENERATED}/ftp_lua.h FTP_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/headers.lua ${G_GENERATED}/headers_lua.h HEADERS_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/http.lua ${G_GENERATED}/http_lua.h HTTP_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/ltn12.lua ${G_GENERATED}/ltn12_lua.h LTN12_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/mbox.lua ${G_GENERATED}/mbox_lua.h MBOX_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/mime.lua ${G_GENERATED}/mime_lua.h MIME_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/smtp.lua ${G_GENERATED}/smtp_lua.h SMTP_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/socket.lua ${G_GENERATED}/socket_lua.h SOCKET_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/tp.lua ${G_GENERATED}/tp_lua.h TP_LUA_H - createEmbeddedBinary thirdparty/luasocket/src/url.lua ${G_GENERATED}/url_lua.h URL_LUA_H - - # === LuaSec === - createEmbeddedBinary thirdparty/luasec/src/https.lua ${G_GENERATED}/https_lua.h HTTPS_LUA_H - createEmbeddedBinary thirdparty/luasec/src/ssl.lua ${G_GENERATED}/ssl_lua.h SSL_LUA_H - - # === LuaRS232 === - createEmbeddedBinary thirdparty/librs232/bindings/lua/rs232.lua ${G_GENERATED}/rs232_lua.h RS232_LUA_H - - # === Copas === - createEmbeddedBinary thirdparty/copas/src/copas.lua ${G_GENERATED}/copas_lua.h COPAS_LUA_H - createEmbeddedBinary thirdparty/copas/src/copas/ftp.lua ${G_GENERATED}/copas_ftp_lua.h COPAS_FTP_LUA_H copas - createEmbeddedBinary thirdparty/copas/src/copas/http.lua ${G_GENERATED}/copas_http_lua.h COPAS_HTTP_LUA_H copas - createEmbeddedBinary thirdparty/copas/src/copas/smtp.lua ${G_GENERATED}/copas_smtp_lua.h COPAS_SMTP_LUA_H copas - createEmbeddedBinary thirdparty/copas/src/copas/lock.lua ${G_GENERATED}/copas_lock_lua.h COPAS_LOCK_LUA_H copas - createEmbeddedBinary thirdparty/copas/src/copas/queue.lua ${G_GENERATED}/copas_queue_lua.h COPAS_QUEUE_LUA_H copas - createEmbeddedBinary thirdparty/copas/src/copas/semaphore.lua ${G_GENERATED}/copas_semaphore_lua.h COPAS_SEMAPHORE_LUA_H copas - createEmbeddedBinary thirdparty/copas/src/copas/timer.lua ${G_GENERATED}/copas_timer_lua.h COPAS_TIMER_LUA_H copas - - # === binaryheap.lua === - createEmbeddedBinary thirdparty/binaryheap.lua/src/binaryheap.lua ${G_GENERATED}/binaryheap_lua.h BINARYHEAP_LUA_H - - # === timerwheel.lua === - createEmbeddedBinary thirdparty/timerwheel.lua/src/timerwheel/timerwheel.lua ${G_GENERATED}/timerwheel_lua.h TIMERWHEEL_LUA_H - - # === json.lua === - createEmbeddedBinary thirdparty/json.lua/json.lua ${G_GENERATED}/json_lua.h JSON_LUA_H - - # === Singe Framework === - createEmbeddedBinary assets/Framework.singe ${G_GENERATED}/Framework_singe.h FRAMEWORK_SINGE_H - - # === Default Config === - createEmbeddedBinary assets/controls.cfg ${G_GENERATED}/controls_cfg.h CONTROLS_CFG_H - - # === Singe Menu App === - createEmbeddedBinary assets/Menu.singe ${G_GENERATED}/Menu_singe.h MENU_SINGE_H - - # === Singe Manual === - #lyx -batch -f all -E pdf ${G_GENERATED}/Manual.pdf assets/Manual.lyx - #createEmbeddedBinary ${G_GENERATED}/Manual.pdf ${G_GENERATED}/Manual_pdf.h MANUAL_H + # Embedded resources, the version header, and the manual are generated by CMake. pushd ${G_TARGET} clearAndEnterBuild - cmake ${COMMON} ${G_BUILDROOT} + cmake ${COMMON} -DKANGAROO_OS=${OS} -DKANGAROO_ARCH=${ARCH} ${G_BUILDROOT} make - #upx -9 --force singe2${SUFFIX} - mv -f singe2${SUFFIX} ${G_BUILDROOT}/${G_BUILDDIR}/Singe-v2.10-${OS^}-${ARCH}${SUFFIX} + # CMake names the binary Singe-v--. + mv -f Singe-v*${SUFFIX} ${G_BUILDROOT}/${G_BUILDDIR}/. popd } @@ -409,134 +313,16 @@ function clearAndEnterBuild() { } -function createEmbeddedBinary() { - local BINFILE=$1 - local SOURCEFILE=$2 # This is assumed to be an absolute path - local BLOCKER=$3 - local PREFIX=$4 - local FILENAME=$(basename ${BINFILE}) - local DIRNAME=$(dirname ${BINFILE}) - - outputLicense > ${SOURCEFILE} - outputHeader ${BLOCKER} >> ${SOURCEFILE} - printf "\n#ifdef EMBED_HERE\n\n" >> ${SOURCEFILE} - pushd ${DIRNAME} - xxd -i ${FILENAME} >> ${SOURCEFILE} - popd - if [[ ! -z ${PREFIX} ]]; then - PREFIX=${PREFIX}_ - sed -i "s/unsigned char /unsigned char ${PREFIX}/" ${SOURCEFILE} - sed -i "s/unsigned int /unsigned int ${PREFIX}/" ${SOURCEFILE} - fi - printf "\n#else // EMBED_HERE\n\n" >> ${SOURCEFILE} - printf "extern unsigned char ${PREFIX}${FILENAME/\./_}[];\n" >> ${SOURCEFILE} - printf "extern unsigned int ${PREFIX}${FILENAME/\./_}_len;\n" >> ${SOURCEFILE} - printf "\n#endif // EMBED_HERE\n\n" >> ${SOURCEFILE} - outputFooter ${BLOCKER} >> ${SOURCEFILE} -} - - -function createEmbeddedImage() { - local BASENAME=$1 - - pushd assets - convert -flatten -background rgba\(0,0,0,0\) ${BASENAME}.xcf ${G_GENERATED}/${BASENAME}.png - createEmbeddedBinary ${G_GENERATED}/${BASENAME}.png ${G_GENERATED}/${BASENAME}.h ${BASENAME^^}_H - rm ${G_GENERATED}/${BASENAME}.png - popd -} - - -function createExtensionHeader() { - local FFMPEG=$1 - local a= - local c=0 - - outputLicense - outputHeader FFMPEG_EXTENSIONS_H - printf "static char *ffmpegExtensions[] = {\n" - printf "\t" - getExtensions "${FFMPEG}" | sort | uniq -u | while read a; do - printf "\"${a}\", " - c=$((c + 1)) - if [[ ${c} -ge 10 ]]; then - printf "\n\t" - c=0 - fi - done - printf "0\n};" - outputFooter FFMPEG_EXTENSIONS_H -} - - -function getExtensions() { - local FFMPEG=$1 - local a= - local b= - local c= - local d= - local e= - local f= - local g= - - "${FFMPEG}" -demuxers 2> /dev/null | while read a b c; do - if [[ "${a}x" == "Dx" ]]; then - "${FFMPEG}" -h demuxer=${b} 2> /dev/null | grep "Common extensions" | while read d e f; do - g=${f/./} - echo -e "${g//,/\\n}" - done - fi - done -} - - -function outputFooter() { - local BLOCKER=$1 - - printf "\n#pragma GCC diagnostic pop\n\n\n#endif // ${BLOCKER}\n" -} - - -function outputHeader() { - local BLOCKER=$1 - - printf "\n\n#ifndef ${BLOCKER}\n#define ${BLOCKER}\n\n\n" - printf "// ===== THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT =====\n\n\n" - printf "#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wunused-variable\"\n" -} - - -function outputLicense() { - cat <<- LICENSE - /* - * - * Singe 2 - * Copyright (C) 2006-2024 Scott Duensing - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - * 02110-1301, USA. - * - */ - LICENSE -} - - # -e = stop script on errors # -u = stop script on undefined variable # -o pipefail = stop pipeline if any step fails -set -e +set -euo pipefail +export MAKEFLAGS="${MAKEFLAGS:--j$(nproc)}" + +if [[ ! -x ../toolchains/toolchains.sh ]]; then + echo "error: ../toolchains/toolchains.sh not found (see INSTALL)" >&2 + exit 1 +fi mkdir -p ${G_BUILDDIR} @@ -544,21 +330,28 @@ mkdir -p ${G_BUILDDIR} # These are required for the build. sudo apt-get install -y \ - xxd \ imagemagick \ lua5.4 \ - lyx \ ffmpeg \ + asciidoctor \ + ruby-asciidoctor-pdf \ + autoconf \ + automake \ + libtool \ libasound-dev \ libxi-dev \ - libvdpau-dev \ - upx-ucl + libvdpau-dev -buildAll linux x86_64 2>&1 | tee ${G_BUILDDIR}/linux-x86_64.log -buildAll macos aarch64 2>&1 | tee ${G_BUILDDIR}/macos-aarch64.log -buildAll pi aarch64 2>&1 | tee ${G_BUILDDIR}/pi-aarch64.log -buildAll windows x86_64 2>&1 | tee ${G_BUILDDIR}/windows-x86_64.log +# Usage: build-all.sh [os arch] (default: every supported platform) +if [[ $# -eq 2 ]]; then + buildAll "$1" "$2" 2>&1 | tee ${G_BUILDDIR}/$1-$2.log +else + buildAll linux x86_64 2>&1 | tee ${G_BUILDDIR}/linux-x86_64.log + buildAll macos aarch64 2>&1 | tee ${G_BUILDDIR}/macos-aarch64.log + buildAll pi aarch64 2>&1 | tee ${G_BUILDDIR}/pi-aarch64.log + buildAll windows x86_64 2>&1 | tee ${G_BUILDDIR}/windows-x86_64.log +fi # === UNSUPPORTED === diff --git a/build-docs.sh b/build-docs.sh new file mode 100755 index 000000000..307647fb9 --- /dev/null +++ b/build-docs.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Build the Singe manual from docs/Manual.adoc. +# Produces docs/Manual.html and docs/Manual.pdf. The version comes from CMakeLists.txt. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +src=$here/docs/Manual.adoc + +if [[ ! -f $src ]]; then + echo "error: $src not found" >&2 + exit 1 +fi +for tool in asciidoctor asciidoctor-pdf; do + if ! command -v "$tool" > /dev/null; then + echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2 + exit 1 + fi +done + +version=$(sed -n 's/^project(singe2 VERSION \([0-9.]*\).*/\1/p' "$here/CMakeLists.txt") + +asciidoctor -a revnumber="$version" "$src" -o "$here/docs/Manual.html" +asciidoctor-pdf -a revnumber="$version" "$src" -o "$here/docs/Manual.pdf" + +echo "built:" +ls -la "$here/docs/Manual.html" "$here/docs/Manual.pdf" diff --git a/cmake/embed.cmake b/cmake/embed.cmake new file mode 100644 index 000000000..eb6f1150b --- /dev/null +++ b/cmake/embed.cmake @@ -0,0 +1,61 @@ +# Embeds a file as a C array. Invoked in script mode by CMakeLists.txt: +# cmake -DINPUT= -DOUTPUT=
-DGUARD= -DSYMBOL= -P embed.cmake +# The header defines the array when EMBED_HERE is defined and declares it extern otherwise. + +file(READ "${INPUT}" content HEX) +string(LENGTH "${content}" hexLength) +math(EXPR byteLength "${hexLength} / 2") + +# 0x00, 0x01, ... twelve per line. +string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " bytes "${content}") +string(REGEX REPLACE "((0x[0-9a-f][0-9a-f], ){12})" "\\1\n\t" bytes "${bytes}") +string(REGEX REPLACE ", $" "" bytes "${bytes}") +string(REGEX REPLACE ",\n\t$" "" bytes "${bytes}") + +file(WRITE "${OUTPUT}" "/* + * + * Singe 2 + * Copyright (C) 2006-${COPYRIGHT_END_YEAR} Scott Duensing + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + */ + + +#ifndef ${GUARD} +#define ${GUARD} + + +// ===== THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT ===== + + +#ifdef EMBED_HERE + +const unsigned char ${SYMBOL}[] = { + ${bytes} +}; +const unsigned int ${SYMBOL}_len = ${byteLength}; + +#else // EMBED_HERE + +extern const unsigned char ${SYMBOL}[]; +extern const unsigned int ${SYMBOL}_len; + +#endif // EMBED_HERE + + +#endif // ${GUARD} +") diff --git a/cmake/runToFile.cmake b/cmake/runToFile.cmake new file mode 100644 index 000000000..6ebdcadc5 --- /dev/null +++ b/cmake/runToFile.cmake @@ -0,0 +1,8 @@ +# Runs a command and captures its standard output in a file. Invoked in script mode: +# cmake -DOUTPUT= -DCOMMAND= -DARGS= -P runToFile.cmake + +execute_process(COMMAND ${COMMAND} ${ARGS} OUTPUT_FILE "${OUTPUT}" RESULT_VARIABLE result) +if(NOT result EQUAL 0) + file(REMOVE "${OUTPUT}") + message(FATAL_ERROR "${COMMAND} failed with status ${result}") +endif() diff --git a/docs/Manual.adoc b/docs/Manual.adoc new file mode 100644 index 000000000..8ff6a6635 --- /dev/null +++ b/docs/Manual.adoc @@ -0,0 +1,3454 @@ += Singe Manual +Scott Duensing +:revnumber: 2.20 +:revdate: 2026 +:doctype: book +:toc: left +:toclevels: 3 +:sectnums: +:sectnumlevels: 3 +:source-highlighter: rouge +:icons: font +:experimental: + +[preface] +== About Singe + +Singe, the Somewhat Interactive Nostalgic Game Engine (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 +own game are made available through a simple application programming interface +(API). + +Singe provides numerous features to the game developer. Some of the more +interesting ones are: + +* Object-oriented programming language +* Animated sprites +* TrueType font support +* 32-bit color space with transparency +* Multi-channel, overlapping, stereo sound +* Analog and digital input device support +* Wide support for video and audio formats + +For players, Singe allows many unemulated and new games to be enjoyed using any +desired front end or via the included menu system. + +Since the original release of Singe back in 2006, several revisions have been +released, both officially and unofficially. In 2020, Singe 2.00 was released. +This was a total rewrite from the ground up, adding numerous features while +staying compatible with existing 1.xx games. As of the writing of this manual, +the latest release is {revnumber}. + +== Installation and Upgrading + +=== Installing Singe + +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. + +=== Upgrading Singe + +To upgrade Singe to a newer version, place the new binary in the installation +folder and rename (or delete) the existing `/Singe` folder. As with +the initial installation, run the new binary with no command line arguments to +generate the new support files. + +WARNING: Do not run a new Singe binary against an out-of-date set of support +files. Always refresh the `Singe/` directory when upgrading. + +=== Things to Know + +Do not touch the `Singe/` folder. Files in this folder belong to Singe and may +be updated or deleted at any time. + +== Usage + +=== Installing Games + +Games designed for Singe 2.xx and later simply need to be unpacked and copied +into the same folder where you placed the Singe binary. The included menu +system will automatically detect them and add them to the menu. + +=== Customizing the Controls + +By default, Singe maps controllers as if they were Xbox gamepads. If your +controller is not recognized or you wish to change the default mappings as they +appear to Singe, you can use the free SDL2 Gamepad Tool from +http://generalarcade.com/gamepadtool/. + +In addition to configuring your controller, you can remap all the input options +for Singe as a whole or for any individual game by using `controls.cfg` files. +When determining how inputs are mapped, Singe reads configuration settings in +the following order: + +. The defaults from inside Singe. +. `controls.cfg` from the directory Singe was started in (the `Singe` directory holds a `controls.cfg.example` template you can copy). +. `controls.cfg` from the root of the data directory. +. `controls.cfg` from the game's data directory. +. `controls.cfg` from the game's main script directory. + +You do not have to specify every option in a custom `controls.cfg` file. Feel +free to just provide the entries you want changed. + +After installing Singe, you can find a sample input configuration file at +`/Singe/controls.cfg.example`. To see the available configuration +options available to be used in this file, read through +`/Singe/Framework.singe`. + +=== Command Line Options + +---- +Singe [OPTIONS] scriptName{.singe} +---- + +The script name is the only required argument. It may be a `.singe` file, or +a directory containing a script of the same name (`ActionMax` finds +`ActionMax/ActionMax.singe`). When no `--framefile` is given, Singe looks for +a video next to the script with the same base name and any extension FFmpeg +can demux, then for a `.txt` framefile. + +[cols="1,2",options="header"] +|=== +| Option | Purpose +| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`). +| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging. +| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory. +| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`. +| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution. +| `-h`, `--help` | Show the option summary and exit. +| `-k`, `--nologos` | Skip the splash screens. +| `-l`, `--volume_vldp=PERCENT` | Laserdisc volume, `0` to `100`. +| `-m`, `--nomouse` | Disable mouse and lightgun input entirely. +| `-n`, `--nocrosshair` | Ask the game not to draw its own crosshair. Exposed to scripts as `singeWantsCrosshairs()`. +| `-o`, `--audio=TRACK` | Default audio track (zero based) for videos with several, such as multi-language releases. +| `-p`, `--program` | Trace engine activity to the console and to `trace.txt` in the data directory. +| `-s`, `--nosound` | Mute all audio. +| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`. +| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio. +| `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script. +| `-w`, `--fullscreen_window` | Borderless window covering the desktop. +| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted. +| `-y`, `--yresolution=VALUE` | Window height. +| `-z`, `--noconsole` | Print nothing (and open no console window on Windows). +|=== + +Options may be given as `-x 640`, `-x640`, or `--xresolution=640`. Numeric +values are validated; a bad value prints the usage text and exits. + +== Frequently Asked Questions + +*Why is it named Singe?* + +Singe is the name of the dragon in Dragon's Lair. As Singe (the program) began +as an add-on to Daphne (the princess in Dragon's Lair) the Dragon's Lair theme +was kept. + +*What is the difference between Daphne, Singe, Hypseus, and Singe 2.xx?* + +Daphne is an actual laserdisc game emulator that can run a handful of classic +laser games. Daphne/Singe (Singe 1.xx) is the original version of Singe that +was an add-on for the Daphne emulator -- originally shipped as a game DLL for +Daphne, then later combined into a single binary with the Daphne emulation +features removed. Hypseus-Singe is an enhanced fork of Daphne with Singe 1.xx +and some Singe 2.xx support. Finally, Singe 2.xx is an all-new, built-from- +scratch upgrade of the original Singe. In short, if you want accuracy, use +Daphne or Hypseus if they support emulating the desired game. If it is a Singe +game, use Singe 2.xx, or try Hypseus. + +*Why should we not call it an "emulator"?* + +Emulators use the real software or ROMs from the original game and pretend to +be the machine they were originally intended to run on. From the game's +perspective, it is business as usual. Singe, on the other hand, requires that +every game be re-implemented (ported) to run on Singe. The end result may be +very similar, but how it works is entirely different. + +*Can I run Singe 1.xx games on Singe 2.xx?* + +Yes, probably. But you likely will not want to. Almost everything from Singe +1.xx has been converted and enhanced for Singe 2.xx. + +*Why does the game I just installed not show in the menu?* + +Whoever packaged the game failed to include a proper `games.dat` file. + +*Why is HD video slow?* + +Due to the way Singe accesses video files to provide frame seeking, it is +unable to offload video decoding to the video card. High definition video +requires a lot of CPU. + +*Why does my audio stutter on Windows?* + +Users have discovered that most Singe audio and stuttering problems are related +to their installed audio driver -- especially Realtek-based devices. Try +updating your sound drivers or switching to the generic Windows HD Audio +driver. Disable surround sound -- use stereo, not 5.1 or 7.1. If your drivers +do not allow you to use stereo, you can use Virtual Audio Cable to fix this: + +. Install VB-CABLE (https://vb-audio.com/Cable/). +. Go to Windows sound settings (right-click sound on the taskbar, then + *Sounds*, then *Playback*). +. Right-click the *Cable Input* device and choose *Set as default device*. + (You will lose your current sound output temporarily.) +. Go to the *Recording* tab. +. Right-click *Cable Output*, then *Properties*, then *Listen*, and finally + *Enable Listen to This Device*. In the drop-down list, choose your actual + listening device. +. You should have sound again. Load up Singe and your audio will be working + and lag-free. + +*Why does my audio stutter on the Raspberry Pi?* + +The Raspberry Pi OS now uses Pipewire as the default audio backend. Switching +to PulseAudio seems to fix the issue: + +. Run `sudo raspi-config` from a terminal window. +. Select *Advanced Options*. +. Select *Audio Config*. +. Select *PulseAudio*. +. Press kbd:[Tab], select *Ok*, and let it reboot. + +== Game Development + +=== Lua + +Singe uses the Lua programming language (http://www.lua.org) for scripting game +logic. Lua is fast, lightweight, object-oriented, easy to use, and actually +used in the games industry. A tutorial in Lua is beyond the scope of this +document. You can find video tutorials for Lua on the Kangaroo Punch YouTube +channel and additional documentation by searching the web. + +=== Basic Rules + +* Singe is cross-platform. Things you do not normally need to worry about on + Windows are important. Filenames are case-sensitive -- `MyScript.singe` and + `myscript.singe` are not the same file. You should also use UNIX path + separators -- `/` rather than `\`. This both makes your code cleaner (no + double-escaped backslashes) and works everywhere Singe is supported. +* Pick a distinctive name for your game folder. Do not use spaces. +* Use the included Singe Framework. Do not copy the framework into your game + scripts. By including the provided framework you help with future + compatibility as Singe is updated. Begin (or end) your game script with: ++ +[source,lua] +---- +dofile("Singe/Framework.singe") +---- +* Stay out of the `Singe/` folder. This is managed by Singe and anything added + or changed here is subject to future deletion. +* Include a `games.dat`. This is extremely important for new users. While + other front ends and Singe-based menu systems exist, it is vital that you + include data for the built-in menu system. Almost all support issues are due + to missing or malformed `games.dat` files. +* Do not distribute non-essential files. Never ship Singe binaries or + Singe-provided scripts with your game. Do not ship index files generated + from the videos. + +=== Game Directory Layout + +A Singe installation is a directory containing the Singe executable, the +`Singe` support directory it creates on first run, one directory per game, +and a `data` directory when the menu or a front end passes `--datadir`. + +---- +Singe/ Support files extracted by the engine + Framework.singe Loaded by every game (dofile it) + Menu.singe The bundled game menu + controls.cfg.example Template for input mappings + Manual.pdf This manual +ActionMax/ One game + games.dat Menu entries for the games in this directory + 38AmbushAlley.singe A script + frame_38AmbushAlley.txt Its framefile (or a video with the same base name) + sprite_*.png, sound_*.wav, font_*.ttf +data/ + ActionMax/ Indexes, trace.txt, screenshots for that game +---- + +Paths inside a script are relative to the directory Singe was started from, +not to the script. Use the `DIR` global, which `Framework.singe` sets to the +script's own directory, so a game works no matter where it is installed: + +[source,lua] +---- +crosshair = spriteLoad(DIR .. "sprite_Crosshair.png") +---- + +Everything Singe writes goes to the data directory (`singeGetDataPath()`), +so a game can live on read-only media. + +=== Packaging Your Game + +Singe installs games itself. Drop an archive into the installation directory, +start Singe, and the archive is validated, unpacked, and deleted. Any format +libarchive reads is accepted (zip, 7z, tar.gz, and so on); the extension +tells Singe what kind of package it is: + +[cols="1,3",options="header"] +|=== +| Extension | Rules +| `.game` | Everything must live inside one top level directory, which must contain a `games.dat`. +| `.tool` | Like a game, but no `games.dat` is required. +| `.patch` | Files may live anywhere; used to update an installed game in place. +|=== + +Every package is rejected if it contains `controls.dat`, `Framework.singe`, +a file whose extension is `exe`, `sh`, `bat`, `cmd`, or `index`, or (for +games and tools) an extensionless file whose name starts with `singe`. Paths +that escape the installation directory are refused as well. Do not ship video +index files; Singe rebuilds them on first run. + +To make a package, archive your game directory so the directory itself is the +top level entry, and rename the result: + +---- +zip -r ActionMax.game ActionMax +---- + +=== Event Driven... Or Not? + +Traditionally, Singe used an event-driven programming model -- Singe handles +everything and only calls your code when it needs to tell you something or +update the screen. This is efficient and is how most Lua-based game engines +work, but it can be verbose. As of Singe 2.10 there is a new _threaded_ model +that allows you to write procedural code. + +==== Event Driven + +With the event-driven programming model, Singe controls the main program loop +and is in charge of the order of execution. Singe automatically handles all +the details of decoding and presenting video and audio. It manages +controllers, mice, and keyboard input. When Singe needs something +game-specific, it calls part of your script. The most basic Singe script that +demonstrates all the existing callbacks looks like this: + +[source,lua] +---- +-- Singe Game Skeleton. + +-- Load the Singe Framework. +dofile("Singe/Framework.singe") + +-- Declare any global variables you need here. + +function onControllerMoved(axis, value, which) + --[[ + Reports which controller axis was moved as well as its current value. + (Range: -32768 to 32767.) This is used for analog devices. Digital + input is handled by onInput and onKey. + --]] +end + +function onInputPressed(what) + --[[ + When in keyboard MODE_NORMAL, input events are reported here when the + key or button is first depressed. For a full list of keys, buttons, + and controllers, see Singe/Framework.singe. For MODE_FULL, this event + will be called with the keysym of the key pressed. + --]] +end + +function onInputReleased(what) + --[[ + When in keyboard MODE_NORMAL, input events are reported here when the + key or button is released. For a full list of keys, buttons, and + controllers, see Singe/Framework.singe. For MODE_FULL, this event + will be called with the keysym of the key released. + --]] +end + +function onKeyPressed(key, scancode) + --[[ + When in keyboard MODE_FULL, input events are reported here when the + key is pressed. Both the keysym and scancode are returned. For a list + of available scancodes, see Singe/Framework.singe. + --]] +end + +function onKeyReleased(key, scancode) + --[[ + When in keyboard MODE_FULL, input events are reported here when the + key is released. Both the keysym and scancode are returned. For a + list of available scancodes, see Singe/Framework.singe. + --]] +end + +function onMouseMoved(x, y, xr, yr, which) + --[[ + Called when the mouse is moved. When in SINGLE_MOUSE mode, absolute + X and Y values as well as the relative change in position are + returned. For MANY_MOUSE mode, only the relative change is available + as well as which mouse was moved. + --]] +end + +function onOverlayUpdate() + --[[ + This is the only place you can safely perform drawing operations. + If you wish to display a targeting cursor, you will need to save the + mouse position from onMouseMoved in global variables and then use + those here to render the cursor. + --]] + + -- Tell Singe if we changed the display or not. + return(OVERLAY_UPDATED) -- Or OVERLAY_NOT_UPDATED if no drawing was done. +end + +function onShutdown() + -- Called when the user exits your game. Free loaded resources here. +end + +function onSoundCompleted(id) + -- The sound "id" just finished playing. +end + +-- Note: There is no "onStartup" event. +-- Any startup code you need can be placed here. +---- + +[#threaded] +==== Threaded + +As of Singe 2.10, you can use the classic procedural programming model. To +use this model, declare a `singeMain()` function and include the Singe +framework at the *end* of your program rather than at the beginning. Lua is +not truly multithreaded, so this model relies on the game developer to +cooperatively multitask by calling `singeYield()` anywhere code consumes a +substantial amount of time. As with the event-driven model, Singe still +manages all input, video decoding, and audio decoding. A sample threaded +program looks like this: + +[source,lua] +---- +function singeMain() + local x = 10 + local y = 10 + + while true do + colorBackground(0, 0, 0, 255) + overlayClear() + + colorForeground(255, 255, 255, 255) + overlayPrint(x, y, "+") + + if keyboardGetLastUp() == SCANCODE.LEFT.value then x = x - 1 end + if keyboardGetLastUp() == SCANCODE.RIGHT.value then x = x + 1 end + if keyboardGetLastUp() == SCANCODE.UP.value then y = y - 1 end + if keyboardGetLastUp() == SCANCODE.DOWN.value then y = y + 1 end + + singeYield() + end +end + +dofile("Singe/Framework.singe") +---- + +==== Hybrid + +The two models combine freely. When `singeMain` exists, `Framework.singe` +installs its own `onOverlayUpdate`, but every other callback still fires: +define `onInputPressed`, `onSoundCompleted`, or `onShutdown` next to +`singeMain` and they are called between the coroutine's yields. A typical +split keeps the game's flow procedural while reacting to input and audio +events: + +[source,lua] +---- +local fired = false + +function onInputPressed(what) + if what == SWITCH_BUTTON1 then + fired = true + end +end + +function onSoundCompleted(channel) + if channel == gunshotChannel then + soundPlay(reloadSound) + end +end + +function singeMain() + while true do + if fired then + fired = false + gunshotChannel = soundPlay(gunshotSound) + end + singeYield() + end +end + +dofile("Singe/Framework.singe") +---- + +Do not define `onOverlayUpdate` yourself in this model; draw from inside +`singeMain` instead. An error raised inside `singeMain` ends the game with a +traceback, and returning from `singeMain` quits cleanly. + +[#pausing] +=== Pausing + +Every game gets a working pause for free. While the pause key is enabled +(the default), pressing the key mapped to `INPUT_PAUSE` makes the engine +pause the disc, every loaded video, and every sound, draw a PAUSED +indicator over the last frame, and stop running the script: no +`onOverlayUpdate`, no `singeMain` resumption, no input, mouse, controller, +or sound callbacks. Timers built on Lua libraries stop with it because +nothing runs them. Pressing the key again resumes everything that was +playing and the script continues where it left off. Only the engine's own +switches (pause, quit, screenshot, grab) still work while frozen. + +Inputs are kept truthful across the freeze. Anything the script believes is +held down is released with `onInputReleased` (or `onKeyReleased` in +`MODE_FULL`) when the pause starts, and whatever is still physically held +when the pause ends is pressed again. A joystick held through a pause never +sticks. + +A game that wants its own pause behavior (a pause menu, a story freeze that +the player may not skip) calls `singeSetPauseKeyEnabled(false)`. The key is +then delivered to the script as `SWITCH_PAUSE` and the game drives +`singeSetPauseFlag` itself, which pauses the media without freezing the +script. + +In `MODE_FULL` the keyboard belongs entirely to the game, so keys mapped to +the engine's switches do nothing there (a `p` typed into a high score entry +must stay a `p`). Gamepad and mouse buttons mapped to those switches keep +working in either mode, since they cannot be typed. + +=== games.dat + +The `games.dat` file allows Singe to automatically locate new games when they +are installed by the end user. This file is extremely important and must be +included with every Singe game. Place `games.dat` in the top-most directory +of your game. An example containing multiple games: + +[source,lua] +---- +GAMES = { + { + TITLE = ".38 Ambush Alley", + SCRIPT = "ActionMax/38AmbushAlley.singe", + VIDEO = "ActionMax/frame_38AmbushAlley.txt", + DATA = "ActionMax", + STRETCH = false, + NO_MOUSE = false, + RESOLUTION_X = 720, + RESOLUTION_Y = 480, + SINDEN_GUN = "", + CABINET = "ActionMax/cabinet_38AmbushAlley.png", + MARQUEE = "ActionMax/marquee_ActionMax.png", + ATTRACT = "ActionMax/video_38AmbushAlley.mkv", + ATTRACT_START = 3000, + ATTRACT_END = 3500, + YEAR = 1987, + PLATFORM = "ActionMax", + DEVELOPER = "Sourcing International, Ltd.", + PUBLISHER = "Worlds of Wonder, Inc.", + GENRE = "Shooter", + DESCRIPTION = "Get your target practice in with real police officers then hit the streets.", + CREATOR = "Scott Duensing", + SOURCE = "http://kangaroopunch.com" + }, + -- ... additional entries ... +} +---- + +The keys `SCRIPT`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, +`RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and `LEGACY_SPRITE_ARGS` are +read by the engine when the menu (or your own script, through +`scriptExecute` / `scriptPush`) launches the entry; they override the +command line. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10 +with the old sprite argument order (see <>). The remaining keys are read by the menu for display. + +[#migrating] +=== Migrating from Singe 2.10 + +Singe 2.20 moved the sprite handle to the first argument of `spriteDraw`, +`spriteLoop`, `spriteQuality`, `spriteRotate`, `spriteRotateAndScale`, +`spriteScale`, and `spriteSetFrame`, so every sprite call now matches the +`video*` family. To update a game, move the last argument of each of those +calls to the front: + +[source,lua] +---- +spriteDraw(x, y, cursor) -- 2.10 +spriteDraw(cursor, x, y) -- 2.20 +spriteRotate(angle, cursor) -- 2.10 +spriteRotate(cursor, angle) -- 2.20 +---- + +A game you cannot edit can opt into the old order instead. Either set the +global before loading the framework: + +[source,lua] +---- +SINGE_LEGACY_SPRITE_ARGS = true +dofile("Singe/Framework.singe") +---- + +or add `LEGACY_SPRITE_ARGS = true` to its `games.dat` entry. Nothing else +changed shape, but a few behaviors did: + +* Handles and counts are returned as Lua integers; `videoIsPlaying` returns + a boolean; `mouseSetMode` returns nothing. +* Errors inside callbacks now end the game with a traceback instead of being + printed and ignored, matching how argument errors have always behaved. +* Held keys no longer repeat in `MODE_NORMAL`. +* The disc is parked on frame 1 by the engine before your script runs; + `Framework.singe` no longer seeks when it is loaded. +* `onMouseMoved` in `MANY_MOUSE` mode passes real movement in its third and + fourth arguments, and `onControllerMoved` passes the controller index + (`0` to `3`) instead of SDL's instance ID. +* `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` / + `singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and + `singeSetPauseKeyEnabled`. +* The pause key now pauses the whole game, not just the media: the script + is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered + to the script only when the key has been disabled. It acts on the key + press rather than the release. See <>. + +=== Engine Constants + +The engine defines these globals before any script runs, so they are +available to `controls.cfg` and to `Framework.singe` alike: + +[cols="2,3",options="header"] +|=== +| Constant | Meaning +| `SWITCH_UP` ... `SWITCH_GRAB` | Values passed to `onInputPressed` / `onInputReleased` in `MODE_NORMAL`. +| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Arguments for `fontQuality`. +| `MODE_NORMAL`, `MODE_FULL` | Arguments for `keyboardSetMode`. +| `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`. +| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`. +| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`. +| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED` | Return values of `discGetState`. +| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free. +| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v2.20"`), and as the number `singeVersion()` returns. +| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`. +| `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order. +| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them. +|=== + +`Framework.singe` adds the `SCANCODE` and `MODIFIER` tables (SDL's key and +modifier values), `GAMEPAD_0` to `GAMEPAD_3`, `MOUSE_0` to `MOUSE_3`, the +`GAMEPAD_AXIS_*` indexes for `controllerGetAxis`, and `DIR`. + +=== Included Libraries + +In addition to the standard Lua libraries and the Singe API, the following +libraries are also available for use in Singe programs without the need to +install any additional software: + +[cols="1,3",options="header"] +|=== +| Library | Purpose +| Copas | Asynchronous networking +| LuaFileSystem | Expanded filesystem support +| json.lua | JavaScript Object Notation encoding and decoding +| binaryheap | Binary heap data structure +| timerwheel | Efficient timer scheduling +| LuaSec | TLS / SSL communication +| LuaSocket | TCP and UDP +| LuaRS232 | RS232 serial port access +|=== + +Their usage is beyond the scope of this document. + +=== Video, Audio, and Container Formats + +Singe decodes video with FFmpeg through FFMS2, so any container and codec the +bundled FFmpeg can demux and decode will play: MP4, MKV, MPEG program streams, +AVI, and the classic Daphne `.m2v` elementary streams with a matching `.ogg` +audio file next to them. Every audio track in the file is available to +`discSetAudioTrack` / `videoSetAudioTrack`; all tracks must share one sample +format, channel count, and rate. + +The first time a video is opened, Singe indexes it and stores the index next +to the game's other data (`.index`). Indexing takes a while for large +files and happens again if the video changes. + +For laserdisc footage the constraints are frame accuracy and seek speed, not +compression. H.264 in MP4 or MKV with a short keyframe interval (one or two +seconds) seeks quickly and plays on every supported platform; long keyframe +intervals make `discSearch` visibly slow because the decoder must walk from +the previous keyframe. Standard definition (720x480 or 720x576) is the sweet +spot. High definition sources work but cost proportionally more CPU on +Raspberry Pi class hardware, and the overlay defaults to half the video +resolution, so oversized video buys little. + +Sound effects go through SDL_mixer: WAV, OGG, FLAC, MP3, Opus, WavPack, and +tracker modules are supported. Short uncompressed WAV files give the lowest +latency. + +== Lua API Reference + +This chapter documents the Lua API exposed by the Singe engine -- every +function your game script can call, and every callback Singe will invoke on +you. It is aimed at developers writing new games or porting video-heavy games +from other engines. + +[#conventions] +=== Conventions + +Every API entry below follows the same structure: + +* *Signature(s)* in a code fence, showing every accepted form. +* *Description* of what the function does and any side effects. +* *Parameters* listed inline, or as a table when there are more than two. +* *Returns* line describing the value pushed back to Lua (omitted when the function returns nothing). +* *Notes*, if there are gotchas worth flagging. +* *Since* -- the Singe version the function first appeared in. +* *See also* -- related functions you will likely use in the same call site. +* *Example* -- a short, realistic snippet in the shape of real game code. + +A few rules apply across the whole API: + +* *Colors* are 8-bit per channel: red, green, blue, and opacity are each integers from `0` to `255`. +* *Overlay coordinates* are in the current overlay resolution (set by `overlaySetResolution`), not screen pixels. The default overlay resolution is half the video resolution in each dimension; call `overlaySetResolution` to change it. +* *Handles* returned by `*Load` functions (sprites, sounds, fonts, videos) are opaque integers. Pass them back unchanged to the other functions in that namespace. Do not do math on them. +* *Invalid arguments abort the script.* If a function is called with the wrong number of arguments, wrong types, or a handle that has already been unloaded, Singe terminates the running script with an error -- it does not return `nil` or a failure code. Validate user input in your script before handing it to the API. +* *Drawing must happen from `onOverlayUpdate`.* Calls to any `overlay*`, `spriteDraw`, `videoDraw`, or `fontPrint` function from other callbacks will not be reflected on screen and may corrupt the overlay. This is the one rule that catches every new Singe developer. + +[#color] +=== Color + +The foreground and background colors are *engine globals*, not per-operation parameters. Set them once with `colorForeground` / `colorBackground`, then every subsequent drawing call (`overlayPrint`, `overlayBox`, `overlayLine`, etc.) uses those colors until you change them again. + +[#colorbackground] +==== colorBackground + +[source,text] +---- +colorBackground(r, g, b) +colorBackground(r, g, b, a) +---- + +Sets the background color used by subsequent drawing operations (primarily `overlayPrint` when it fills the text background). + +* `r`, `g`, `b` -- red, green, blue channels, `0` to `255`. +* `a` -- optional opacity, `0` to `255`. Defaults to `0` (fully transparent) so that text drawn with `overlayPrint` shows through to whatever is behind it. + +The three-argument form exists for compatibility with pre-2.00 scripts and produces the same result as passing `a = 0`. + +*Since:* 1.x +*See also:* <>, <> *(documented in the Overlay section)* + +.Example +[source,lua] +---- +-- Draw a status line with a solid black background behind the text. +function onOverlayUpdate() + colorBackground(0, 0, 0, 255) -- Opaque black. + colorForeground(255, 255, 0, 255) -- Yellow text. + overlayPrint(8, 8, "Score: " .. score) + return OVERLAY_UPDATED +end +---- + +[#colorforeground] +==== colorForeground + +[source,text] +---- +colorForeground(r, g, b) +colorForeground(r, g, b, a) +---- + +Sets the foreground color used by subsequent drawing operations -- text rendered by `overlayPrint` and `fontPrint`, all primitives in the `overlay*` family, and the tint used by `fontToSprite`. + +* `r`, `g`, `b` -- red, green, blue channels, `0` to `255`. +* `a` -- optional opacity, `0` to `255`. Defaults to `255` (fully opaque). This is the opposite default from `colorBackground`, because foreground drawing almost always wants to be visible. + +The three-argument form exists for compatibility with pre-2.00 scripts and produces the same result as passing `a = 255`. + +*Since:* 1.x +*See also:* <>, <>, <>, <> *(all documented in the Overlay section)* + +.Example +[source,lua] +---- +-- Flash the crosshair red when the player is hit, white otherwise. +function onOverlayUpdate() + if hitTimer > 0 then + colorForeground(255, 0, 0, 255) + hitTimer = hitTimer - 1 + else + colorForeground(255, 255, 255, 255) + end + overlayCircle(crosshairX, crosshairY, 8) + return OVERLAY_UPDATED +end +---- + +[#controller] +=== Controller + +Singe supports up to *four* simultaneous game controllers (indices `0` through `3`). Controllers are detected automatically at startup; there is no `controllerLoad`. Inputs reach your game in two ways: + +* *Analog axes* (sticks and triggers) -- reported in real time through the `onControllerMoved` callback *or* polled on demand with `controllerGetAxis`. Values range from `-32768` to `32767`. +* *Digital buttons* (A/B/X/Y, shoulders, D-pad, etc.) -- reach your game through the generic input system only when they are mapped to a switch in `controls.cfg`. Map a button code from the `GAMEPAD_0` / `GAMEPAD_1` / `GAMEPAD_2` / `GAMEPAD_3` tables (for example `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`) and `onInputPressed` / `onInputReleased` then receive the matching `SWITCH_*` value in keyboard `MODE_NORMAL`. Unmapped buttons are ignored in `MODE_NORMAL`; in `MODE_FULL` the raw button code arrives as the `scancode` argument of `onKeyPressed` / `onKeyReleased`. `controllerGetButton` is for polling the live state of a specific button. + +The global `SINGE_DEAD_ZONE` (set from the `DEAD_ZONE` entry in `controls.cfg`) is the recommended threshold below which you should treat axis motion as noise. + +[#controllergetaxis] +==== controllerGetAxis + +[source,text] +---- +value = controllerGetAxis(controller, axis) +---- + +Polls the current position of an analog axis on a controller. + +* `controller` -- controller index, `0` through `3`. +* `axis` -- axis index. Use the named constants: `GAMEPAD_AXIS_LEFT_X`, `GAMEPAD_AXIS_LEFT_Y`, `GAMEPAD_AXIS_RIGHT_X`, `GAMEPAD_AXIS_RIGHT_Y`, `GAMEPAD_AXIS_LEFT_TRIGGER`, `GAMEPAD_AXIS_RIGHT_TRIGGER`. + +*Returns:* integer from `-32768` to `32767`. Triggers use `0` to `32767` (they are one-sided). + +*Since:* 2.00 +*See also:* <>, the `onControllerMoved` callback + +.Example +[source,lua] +---- +-- Steer left/right based on the left analog stick, ignoring the dead zone. +function onOverlayUpdate() + local x = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) + if math.abs(x) > SINGE_DEAD_ZONE then + playerX = playerX + (x / 32768) * steerSpeed + end + drawPlayer() + return OVERLAY_UPDATED +end +---- + +[#controllergetbutton] +==== controllerGetButton + +[source,text] +---- +down = controllerGetButton(controller, button) +---- + +Polls the current state of a digital button on a controller. Use this when you need to know *right now* whether a button is held, rather than waiting for an `onInputPressed` event. + +* `controller` -- controller index, `0` through `3`. +* `button` -- one of the `BUTTON_*` values from the `GAMEPAD_N` table for that controller (e.g. `GAMEPAD_0.BUTTON_A.value`, `GAMEPAD_0.DPAD_UP.value`). + +*Returns:* boolean. `true` while the button is held. + +*Since:* 2.10 +*See also:* <>, the `onInputPressed` callback + +.Example +[source,lua] +---- +-- Charge a shot while the A button is held, fire when released. +function onOverlayUpdate() + if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then + chargeLevel = math.min(chargeLevel + 1, MAX_CHARGE) + elseif chargeLevel > 0 then + fireShot(chargeLevel) + chargeLevel = 0 + end + return OVERLAY_UPDATED +end +---- + +[#debug] +=== Debug + +[#debugprint] +==== debugPrint + +[source,text] +---- +debugPrint(message) +---- + +Writes a string to Singe's trace log (stdout and the trace file when tracing is enabled with `-t`/`--trace`). Useful for step-printf debugging when the ZeroBrane Studio integration is not available. + +* `message` -- any string. + +*Since:* 1.x +*See also:* command-line options `-t` (script trace) and `-p` (engine trace) in the manual + +.Example +[source,lua] +---- +-- Check why the player isn't taking damage. +debugPrint("hit check: player=" .. tostring(playerX) .. "," .. tostring(playerY) .. + " enemy=" .. tostring(enemyX) .. "," .. tostring(enemyY)) +---- + +[#disc] +=== Disc + +The *disc* is the main laserdisc video -- the one Singe was originally built to emulate. A Singe game always has exactly one disc, loaded at startup from the video file declared in `games.dat` (or the command line). + +All `disc*` functions operate on this single, implicit disc. They exist in addition to the more general <> family, which handles additional video assets you load yourself. Use `disc*` for the main gameplay video; use `video*` for extra clips layered on top. + +A handful of `disc*` functions are *unimplemented no-ops* retained for script-level compatibility with very old Singe games. They are listed in <>. + +[#discaudio] +==== discAudio + +[source,text] +---- +discAudio(channel, onOff) +---- + +Toggles one audio channel of the disc's soundtrack on or off. Often used to mute the left or right channel when a game originally used one channel for audio and the other for a data track. + +* `channel` -- `1` for left, `2` for right. +* `onOff` -- boolean. `true` restores the channel to the configured VLDP volume; `false` silences it. + +*Since:* 1.x +*See also:* <> + +.Example +[source,lua] +---- +-- Some titles bake narration into the right channel only. +discAudio(1, false) -- Silence the music on the left. +discAudio(2, true) -- Keep narration on the right. +---- + +[#discchangespeed] +==== discChangeSpeed + +WARNING: *Unimplemented.* This function is a no-op retained for backward compatibility with pre-2.00 scripts. + +[#discgetaudiotrack] +==== discGetAudioTrack + +[source,text] +---- +track = discGetAudioTrack() +---- + +Returns the index of the currently selected audio track for the disc. + +*Returns:* integer track index. + +*Since:* 2.10 +*See also:* <>, <> + +[#discgetaudiotracks] +==== discGetAudioTracks + +[source,text] +---- +count = discGetAudioTracks() +---- + +Returns how many audio tracks the disc's video file contains. Multiple tracks are typically alternate language dubs. + +*Returns:* integer count (>= 1). + +*Since:* 2.10 +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Build a language-select menu from the disc's audio tracks. +for i = 0, discGetAudioTracks() - 1 do + local code = discGetLanguage(i) + local name = discGetLanguageDescription(code) + table.insert(languages, { track = i, label = name }) +end +---- + +[#discgetframe] +==== discGetFrame + +[source,text] +---- +frame = discGetFrame() +---- + +Returns the current frame number on the disc. This is the fundamental time reference for laserdisc games -- almost every interactive decision hinges on "what frame are we on right now?" + +*Returns:* integer. `0` when the disc is stopped. + +*Since:* 1.x +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Trigger a prompt at the moment the player must duck. +function onOverlayUpdate() + local f = discGetFrame() + if f >= 2847 and f <= 2882 and not promptShown then + showPrompt("DUCK!") + promptShown = true + end + return OVERLAY_UPDATED +end +---- + +[#discgetheight] +==== discGetHeight + +[source,text] +---- +height = discGetHeight() +---- + +Returns the height of the disc's video in pixels. Useful if you want to build an overlay resolution that matches the native video. + +*Returns:* integer pixel height. + +*Since:* 2.00 +*See also:* <>, <> + +[#discgetlanguage] +==== discGetLanguage + +[source,text] +---- +code = discGetLanguage(track) +---- + +Returns the ISO language code (e.g. `"eng"`, `"jpn"`, `"fre"`) for an audio track on the disc, as stored in the video file's metadata. + +* `track` -- audio track index, `0` through `discGetAudioTracks() - 1`. + +*Returns:* three-letter language code string, or an empty string if no language tag is set on that track. + +*Since:* 2.10 +*See also:* `discGetLanguageDescription` (aliased in `Framework.singe` to <>), <> + +[#discgetstate] +==== discGetState + +[source,text] +---- +state = discGetState() +---- + +Returns an integer describing the playback state of the disc. + +*Returns:* one of the following constants (defined by the engine and available to every script): + +[cols="1,1,1",options="header"] +|=== +| Constant | Value | Meaning +| `DISC_STOPPED` | `2` | Stopped (`discStop` was called) +| `DISC_PLAYING` | `3` | Playing +| `DISC_PAUSED` | `4` | Paused +|=== + +Singe's lightweight player has no distinct searching state; a disc that is seeking reports `DISC_PAUSED`. + +*Since:* 1.x (RDG) +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Don't let the pause key resume from a story-driven freeze. +if discGetState() == DISC_PAUSED and not storyPaused then + discPlay() +end +---- + +[#discgetwidth] +==== discGetWidth + +[source,text] +---- +width = discGetWidth() +---- + +Returns the width of the disc's video in pixels. + +*Returns:* integer pixel width. + +*Since:* 2.00 +*See also:* <>, <> + +[#discpause] +==== discPause + +[source,text] +---- +discPause() +---- + +Pauses playback on the current frame. A paused disc keeps displaying the frame it stopped on. Has no effect if the disc is already stopped (see `discStop` for the difference). + +*Since:* 1.x +*See also:* <>, <>, <> + +[#discpauseatframe] +==== discPauseAtFrame + +[source,text] +---- +discPauseAtFrame(frame) +---- + +Seeks to the specified frame and pauses on it. In the current engine this is *identical to `discSearch`* -- the two names exist for historical reasons. Prefer `discSearch` in new code; keep `discPauseAtFrame` if you are porting old scripts. + +* `frame` -- target frame number. + +*Since:* 1.18 +*See also:* <> + +[#discplay] +==== discPlay + +[source,text] +---- +discPlay() +---- + +Starts or resumes playback from the current frame. If the disc was stopped (via `discStop`), playback restarts and the disc is marked running again. + +*Since:* 1.x +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Standard attract-mode loop. +function attractLoop() + discSearch(attractStart) -- Seek and pause on the first frame. + discPlay() -- Start rolling. +end +---- + +[#discsearch] +==== discSearch + +[source,text] +---- +discSearch(frame) +---- + +Seeks the disc to the specified frame and pauses on it. The disc will display that frame until you call `discPlay`, `discSkipToFrame`, or another transport function. This is the workhorse frame-positioning call for laserdisc games -- every branch point in an interactive video game is some variation of "seek to the next scene, show me the frame, wait for my decision." + +* `frame` -- target frame number. + +*Since:* 1.x +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Player picked "go left" -- cue the left-corridor scene and wait. +function goLeft() + discSearch(sceneLeftStartFrame) + waitingForPlayer = true +end +---- + +[#discsearchblanking] +==== discSearchBlanking + +WARNING: *Unimplemented.* No-op retained for backward compatibility with pre-2.00 scripts. On real laserdisc hardware this controlled whether the screen blanked during a seek; Singe's software player does not need this. + +[#discsetaudiotrack] +==== discSetAudioTrack + +[source,text] +---- +discSetAudioTrack(track) +---- + +Switches the active audio track on the disc. Use this to implement a language-select menu. + +* `track` -- audio track index, `0` through `discGetAudioTracks() - 1`. Out-of-range values terminate the script. + +*Since:* 2.10 +*See also:* <>, <> + +[#discsetfps] +==== discSetFPS + +WARNING: *Unimplemented.* No-op retained for backward compatibility. `Framework.singe` still calls `discSetFPS(29.97)` at startup for the benefit of old scripts that read the framerate elsewhere. The actual framerate comes from the video file. + +[#discskipbackward] +==== discSkipBackward + +[source,text] +---- +discSkipBackward(delta) +---- + +Seeks backward by a number of frames from the current position. Does *not* change the play/pause state -- a playing disc keeps playing, a paused disc stays paused on the new frame. + +* `delta` -- number of frames to subtract from the current frame. Must be positive. + +*Since:* 1.x +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Rewind 30 frames (about one second at 30fps) when the player misses a prompt. +discSkipBackward(30) +---- + +[#discskipblanking] +==== discSkipBlanking + +WARNING: *Unimplemented.* No-op retained for backward compatibility with pre-2.00 scripts. + +[#discskipforward] +==== discSkipForward + +[source,text] +---- +discSkipForward(delta) +---- + +Seeks forward by a number of frames from the current position. Does not change the play/pause state. Mirror of `discSkipBackward`. + +* `delta` -- number of frames to add to the current frame. + +*Since:* 1.x +*See also:* <>, <> + +[#discskiptoframe] +==== discSkipToFrame + +[source,text] +---- +discSkipToFrame(frame) +---- + +Seeks to a specific frame and *starts playing* from that frame. Contrast with `discSearch`, which seeks and pauses. Use `discSkipToFrame` when you want continuous video playback from a new location. + +* `frame` -- target frame number. + +*Since:* 1.x +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Skip the intro on a replay. +if hasPlayedBefore then + discSkipToFrame(gameplayStart) +else + discSkipToFrame(introStart) +end +---- + +[#discstepbackward] +==== discStepBackward + +[source,text] +---- +discStepBackward() +---- + +Moves the disc back by exactly one frame. Always pauses on the new frame, regardless of previous play state. Intended for frame-accurate debugging and service-mode screens. + +*Since:* 1.x +*See also:* <>, <> + +[#discstepforward] +==== discStepForward + +[source,text] +---- +discStepForward() +---- + +Moves the disc forward by exactly one frame. Always pauses on the new frame, regardless of previous play state. Mirror of `discStepBackward`. + +*Since:* 1.x +*See also:* <> + +[#discstop] +==== discStop + +[source,text] +---- +discStop() +---- + +Stops the disc. Stopped is a stronger state than paused: the display is marked for refresh, and `discGetState` returns `2` rather than `4`. Transport calls that require a running disc (like `discSkipForward` and `discSkipBackward`) will fail silently while stopped. Resume with `discPlay` or any `discSearch` / `discSkipToFrame`. + +*Since:* 1.x +*See also:* <>, <>, <> + +[#font] +=== Font + +Singe has *two* text rendering systems. Know which one you're using: + +* *Console font* -- a fixed 8-bit bitmap font baked into the engine. Accessed through <>. Fast, predictable, grid-aligned, and the only way to print text without loading any assets. +* *TrueType fonts* -- arbitrary `.ttf` files you load at runtime with `fontLoad`, then render with `fontPrint` (direct to overlay) or `fontToSprite` (to a reusable sprite). Scalable, anti-aliased, colorable, but require managing the lifecycle yourself. + +The TrueType system keeps a global *currently selected font*. `fontLoad` selects the font it just loaded; to switch between several loaded fonts, use `fontSelect`. + +Three render qualities are available, set with `fontQuality`: + +[cols="1,1,1",options="header"] +|=== +| Constant | Value | Appearance +| `FONT_QUALITY_SOLID` | `1` | Fastest. 1-bit alpha, jagged edges. Use for dev text. +| `FONT_QUALITY_SHADED` | `2` | Anti-aliased text on a solid rectangle of `colorBackground`. Good for UI panels. +| `FONT_QUALITY_BLENDED` | `3` | Smooth anti-aliased text with per-pixel alpha -- blends cleanly over any background. Highest quality, most common choice. +|=== + +[#fontload] +==== fontLoad + +[source,text] +---- +id = fontLoad(filename, pointSize) +---- + +Loads a TrueType font at the given point size and selects it as the current font. Different point sizes of the same typeface must be loaded as separate fonts -- `pointSize` is baked into the loaded handle. + +* `filename` -- path to a `.ttf` file. Prepend `DIR` for files shipped with your game. +* `pointSize` -- integer point size. Typical values range from `12` (small UI text) to `72` (large title text). + +*Returns:* integer font handle. + +*Since:* 1.x +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Load two sizes of the same face: big for the title, small for scores. +titleFont = fontLoad(DIR .. "fonts/BreatheFire.ttf", 48) +hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 14) +fontQuality(FONT_QUALITY_BLENDED) +---- + +[#fontprint] +==== fontPrint + +[source,text] +---- +fontPrint(x, y, text) +---- + +Renders a string onto the overlay using the currently selected TrueType font, the current foreground color, and (for `FONT_QUALITY_SHADED`) the current background color. Must be called from `onOverlayUpdate`. + +* `x`, `y` -- overlay coordinates of the top-left corner of the rendered text. +* `text` -- the string to render. + +`fontPrint` creates a fresh text surface on every call and frees it after blitting. For text that changes only occasionally (a score, a status line, menu labels), `fontToSprite` is significantly faster since it caches the surface. + +*Since:* 1.x +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Render a live-updating score with the currently selected font. +function onOverlayUpdate() + colorForeground(255, 255, 255, 255) + fontPrint(20, 20, "SCORE: " .. score) + return OVERLAY_UPDATED +end +---- + +[#fontquality] +==== fontQuality + +[source,text] +---- +fontQuality(mode) +---- + +Sets the render quality used by subsequent `fontPrint` and `fontToSprite` calls. This is a *global setting*, not per-font. + +* `mode` -- one of `FONT_QUALITY_SOLID` (`1`), `FONT_QUALITY_SHADED` (`2`), or `FONT_QUALITY_BLENDED` (`3`). + +*Since:* 1.x +*See also:* <>, <> + +[#fontselect] +==== fontSelect + +[source,text] +---- +fontSelect(id) +---- + +Makes a previously loaded font the current one. All subsequent `fontPrint` / `fontToSprite` calls will use this font until you call `fontSelect` again. + +* `id` -- font handle returned by `fontLoad`. + +*Since:* 1.x +*See also:* <> + +.Example +[source,lua] +---- +fontSelect(titleFont) +fontPrint(centerX, 40, "DRAGON'S LAIR") + +fontSelect(hudFont) +fontPrint(8, 8, "Score: " .. score) +---- + +[#fonttosprite] +==== fontToSprite + +[source,text] +---- +id = fontToSprite(text) +---- + +Renders a string to a new sprite using the currently selected font, quality, and foreground color. Returns a sprite handle that can be used with every <> function (draw, scale, rotate, unload). + +This is the recommended path for text that doesn't change often -- a menu label, a level name, a scoreboard header. Render once, cache the sprite, draw it repeatedly. For text that changes every frame (a timer, a live score), either re-render with `fontToSprite` and unload the previous sprite, or use `fontPrint` directly. + +* `text` -- the string to render. + +*Returns:* integer sprite handle. + +*Since:* 1.x +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Pre-render menu labels once at startup. +menuLabels = {} +for i, entry in ipairs(menuEntries) do + menuLabels[i] = fontToSprite(entry.name) +end + +function onOverlayUpdate() + for i, label in ipairs(menuLabels) do + spriteDraw(label, menuX, menuY + i * lineHeight) + end + return OVERLAY_UPDATED +end +---- + +[#fontunload] +==== fontUnload + +[source,text] +---- +fontUnload(id) +---- + +Releases a loaded font. After this call the handle is invalid. + +* `id` -- font handle. + +If you unload the currently selected font, the engine does not automatically pick another -- do not call `fontPrint` or `fontToSprite` until you have `fontSelect`ed a different loaded font. + +*Since:* 2.00 +*See also:* <> + +[#keyboard] +=== Keyboard + +Singe exposes keyboard input in two modes, selected with `keyboardSetMode`: + +* *`MODE_NORMAL`* (default, value `0`) -- Singe translates raw keys into the high-level "switches" defined in `controls.cfg` (jump, coin, start, etc.). Your script only sees `onInputPressed(switch)` / `onInputReleased(switch)` with the `SWITCH_*` values from `Framework.singe`. Use this for gameplay. +* *`MODE_FULL`* (value `1`) -- Singe also delivers every physical keypress through `onKeyPressed(keysym, scancode)` / `onKeyReleased(...)`. Use this when you need raw text entry (high score initials, a debug console, typing player names). + +Either mode, you can poll keyboard state directly with `keyboardIsDown`, `keyboardGetLastDown`, and `keyboardGetLastUp` -- those always work regardless of mode. + +*Scancodes vs. keysyms:* a *scancode* is the physical key position (the `A` key is always scancode `4`, no matter the layout). A *keysym* is the logical character that key produces (`'a'` on US QWERTY, `'q'` on AZERTY). The `SCANCODE` table in `Framework.singe` gives you named scancodes. `keyboardIsDown` works in scancodes. `onKeyPressed` gives you both. + +[#keyboardgetlastdown] +==== keyboardGetLastDown + +[source,text] +---- +scancode = keyboardGetLastDown() +---- + +Returns the SDL scancode of the most recently pressed key (compare against `SCANCODE.*.value`), or `0` if nothing has been pressed since the last read. The value is cleared on every `onOverlayUpdate` pass, making this a one-shot "was a key just pressed?" poll -- ideal for the threaded model. + +*Returns:* integer scancode. + +*Since:* 2.10 +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Threaded-model main loop reacts to one keypress per iteration. +function singeMain() + while true do + local k = keyboardGetLastDown() + if k == SCANCODE.ESCAPE.value then singeQuit() end + if k == SCANCODE.SPACE.value then jump() end + singeYield() + end +end +---- + +[#keyboardgetlastup] +==== keyboardGetLastUp + +[source,text] +---- +scancode = keyboardGetLastUp() +---- + +Returns the SDL scancode of the most recently released key (compare against `SCANCODE.*.value`), or `0` if nothing has been released since the last read. Cleared each frame, like `keyboardGetLastDown`. + +*Returns:* integer scancode. + +*Since:* 2.10 +*See also:* <> + +[#keyboardgetmode] +==== keyboardGetMode + +[source,text] +---- +mode = keyboardGetMode() +---- + +Returns the current keyboard mode: `MODE_NORMAL` (`0`) or `MODE_FULL` (`1`). + +*Since:* 1.x (RDG) +*See also:* <> + +[#keyboardgetmodifiers] +==== keyboardGetModifiers + +[source,text] +---- +mods = keyboardGetModifiers() +---- + +Returns a bitmask of the currently held modifier keys. Test bits against values in the `MODIFIER` table -- for example `mods & MODIFIER.SHIFT.value ~= 0` for either shift key. + +*Returns:* integer bitmask. + +*Since:* 2.10 +*See also:* the `MODIFIER` table in `Framework.singe` + +.Example +[source,lua] +---- +-- Shift+tab opens the debug overlay. +if keyboardGetLastDown() == SCANCODE.TAB.value then + if keyboardGetModifiers() & MODIFIER.SHIFT.value ~= 0 then + debugOverlayVisible = not debugOverlayVisible + end +end +---- + +[#keyboardisdown] +==== keyboardIsDown + +[source,text] +---- +down = keyboardIsDown(scancode) +---- + +Tests whether a specific key is physically held right now. Unlike the `on*` callbacks, this does not wait for events -- it reads the live keyboard state. + +* `scancode` -- a value from the `SCANCODE` table (e.g. `SCANCODE.UP.value`). Out-of-range values quietly return `false`. + +*Returns:* boolean. + +*Since:* 2.10 +*See also:* the `SCANCODE` table in `Framework.singe` + +.Example +[source,lua] +---- +-- Diagonal movement by checking multiple keys each frame. +local dx, dy = 0, 0 +if keyboardIsDown(SCANCODE.LEFT.value) then dx = -1 end +if keyboardIsDown(SCANCODE.RIGHT.value) then dx = 1 end +if keyboardIsDown(SCANCODE.UP.value) then dy = -1 end +if keyboardIsDown(SCANCODE.DOWN.value) then dy = 1 end +playerX = playerX + dx * speed +playerY = playerY + dy * speed +---- + +[#keyboardsetmode] +==== keyboardSetMode + +[source,text] +---- +keyboardSetMode(mode) +---- + +Switches the keyboard between the two input models. Most games should call this once at startup and leave it alone. + +* `mode` -- `MODE_NORMAL` (`0`) for mapped switches only, `MODE_FULL` (`1`) to also receive raw key events. + +*Since:* 1.x (RDG) +*See also:* <> + +.Example +[source,lua] +---- +-- Enter high-score entry, capture raw keys, then restore gameplay mode. +keyboardSetMode(MODE_FULL) +collectInitials() +keyboardSetMode(MODE_NORMAL) +---- + +[#mouse] +=== Mouse + +Singe supports up to *four* simultaneous mice (or lightguns -- Singe treats them identically). One-mouse mode is the default; switch to many-mouse mode only if your game genuinely needs to tell the devices apart (e.g. two-player lightgun cabinets). + +* Enable / disable event dispatch with `mouseSetEnabled`. +* Poll position with `mouseGetPosition`. Event-driven games get position updates through `onMouseMoved`. +* `mouseSetCaptured(true)` grabs the cursor and hides it -- typical for fullscreen gameplay. + +Digital mouse buttons reach `onInputPressed` (keyboard `MODE_NORMAL`) only when mapped to a switch in `controls.cfg`, using the codes in the `MOUSE_0` / `MOUSE_1` / `MOUSE_2` / `MOUSE_3` tables. The shipped defaults map the left button to `SWITCH_BUTTON3`, the right button to `SWITCH_BUTTON1`, and the middle button to `SWITCH_BUTTON2`. + +[#mousedisable] +==== mouseDisable + +[source,text] +---- +mouseDisable() +---- + +Legacy alias for `mouseSetEnabled(false)`, defined in `Framework.singe`. + +*Since:* 1.18 (RDG) +*See also:* <> + +[#mouseenable] +==== mouseEnable + +[source,text] +---- +mouseEnable() +---- + +Legacy alias for `mouseSetEnabled(true)`, defined in `Framework.singe`. + +*Since:* 1.18 (RDG) +*See also:* <> + +[#mousegetposition] +==== mouseGetPosition + +[source,text] +---- +x, y = mouseGetPosition(index) +---- + +Returns the current overlay-coordinate position of a mouse. Use this for polling instead of tracking `onMouseMoved` events if that's more convenient for your architecture (especially in the threaded model). + +* `index` -- mouse index, `0` through `3`. Passing an out-of-range index terminates the script. + +*Returns:* two values, `x` and `y`, in overlay units. + +*Since:* 2.00 +*See also:* <>, the `onMouseMoved` callback + +.Example +[source,lua] +---- +-- Draw a crosshair at mouse 0's position every frame. +function onOverlayUpdate() + local x, y = mouseGetPosition(0) + colorForeground(255, 255, 255, 255) + overlayCircle(x, y, 10) + overlayPlot(x, y) + return OVERLAY_UPDATED +end +---- + +[#mousehowmany] +==== mouseHowMany + +[source,text] +---- +count = mouseHowMany() +---- + +Returns the number of mice currently connected. Capped at 4. Useful for deciding whether a second-player cursor should be drawn. + +*Returns:* integer, `0` to `4`. + +*Since:* 1.18 (RDG) +*See also:* <> + +[#mousesetcaptured] +==== mouseSetCaptured + +[source,text] +---- +mouseSetCaptured(grabbed) +---- + +Grabs or releases the mouse cursor. When grabbed, the cursor is hidden and confined to the Singe window -- the right default for fullscreen gameplay with a targeting reticle. + +* `grabbed` -- boolean. `true` captures; `false` releases. + +*Since:* 2.00 +*See also:* <> + +[#mousesetenabled] +==== mouseSetEnabled + +[source,text] +---- +mouseSetEnabled(enabled) +---- + +Turns mouse event dispatch to your script on or off. When off, the cursor still moves on screen; your callbacks just stop being called. Enabling honors the `--nomouse` command-line flag -- if the user launched Singe with `--nomouse`, the mouse stays disabled. + +* `enabled` -- boolean. + +*Since:* 2.20 +*See also:* <>, <>, <> + +[#mousesetmode] +==== mouseSetMode + +[source,text] +---- +ok = mouseSetMode(mode) +---- + +Switches between single-mouse and many-mouse input modes. + +* `mode` -- `SINGLE_MOUSE` (`100`) for games that treat all mice as one virtual pointer, or `MANY_MOUSE` (`200`) to distinguish multiple mice. `MOUSE_SINGLE` and `MOUSE_MANY` are accepted as aliases. + +Switch to `MANY_MOUSE` when you need to tell two lightguns or two mice apart -- typically a two-player cabinet or a cooperative lightgun game. In `SINGLE_MOUSE` mode, `onMouseMoved` reports absolute and relative positions; in `MANY_MOUSE` mode, only relative motion and the source device index are reliable. + +*Returns:* boolean. `true` if the mode was set successfully. + +*Since:* 1.18 (RDG) +*See also:* <>, the `onMouseMoved` callback + +[#overlay] +=== Overlay + +The overlay is a transparent 32-bit RGBA surface composited over the disc/video every frame. Everything your script draws -- shapes, text, sprites, videos -- goes onto the overlay, not directly to the screen. + +Overlay coordinates are *not* screen pixels. They are in the resolution set by `overlaySetResolution`, which the engine scales to the window size at display time. By convention most games set the overlay to match the disc's native resolution. + +All drawing must happen inside `onOverlayUpdate`. Calls from other callbacks will not be reflected on screen and may corrupt the overlay surface. + +Two things that behave differently from the rest of the overlay API: + +* *`overlayBox` draws outlines only.* There is no filled-rectangle primitive. Build filled regions out of sprites or pre-rendered images. +* *`overlayPrint` uses character-cell coordinates*, not overlay units. The `x` and `y` arguments are multiplied by the built-in console font's cell size internally -- so `overlayPrint(0, 0, "HI")` prints at the top-left in character cell `(0, 0)`, not pixel `(0, 0)`. Every other overlay function uses pixel coordinates. + +All primitives draw in the current <>. `overlayClear` fills with the current <>. + +[#overlaybox] +==== overlayBox + +[source,text] +---- +overlayBox(x1, y1, x2, y2) +---- + +Draws the outline of an axis-aligned rectangle. *Outline only -- there is no fill.* + +* `x1`, `y1`, `x2`, `y2` -- corners of the rectangle in overlay units. + +*Since:* 2.00 +*See also:* <>, <> + +[#overlaycircle] +==== overlayCircle + +[source,text] +---- +overlayCircle(x, y, radius) +---- + +Draws the outline of a circle using the midpoint algorithm. + +* `x`, `y` -- center of the circle in overlay units. +* `radius` -- radius in overlay units. + +*Since:* 2.00 +*See also:* <>, <> + +[#overlayclear] +==== overlayClear + +[source,text] +---- +overlayClear() +---- + +Fills the entire overlay with the current background color (`colorBackground`). Typically called first in `onOverlayUpdate` to wipe last frame's drawing -- unless your game composites over a dynamic video and prefers to redraw only dirty regions. + +Note: because `colorBackground` defaults to transparent (`a = 0`), `overlayClear()` without first setting a solid background is the standard way to erase the overlay back to "invisible." + +*Since:* 1.x +*See also:* <> + +.Example +[source,lua] +---- +function onOverlayUpdate() + overlayClear() -- Erase previous frame (back to transparent). + drawHUD() + drawPlayerIcon() + return OVERLAY_UPDATED +end +---- + +[#overlayellipse] +==== overlayEllipse + +[source,text] +---- +overlayEllipse(x1, y1, x2, y2) +---- + +Draws the outline of an axis-aligned ellipse inscribed in the given bounding rectangle. + +* `x1`, `y1`, `x2`, `y2` -- corners of the bounding box. + +*Since:* 2.00 +*See also:* <> + +[#overlaygetheight] +==== overlayGetHeight + +[source,text] +---- +height = overlayGetHeight() +---- + +Returns the height of the overlay surface in overlay units (whatever `overlaySetResolution` set it to). + +*Returns:* integer height. + +*Since:* 1.x +*See also:* <>, <> + +[#overlaygetwidth] +==== overlayGetWidth + +[source,text] +---- +width = overlayGetWidth() +---- + +Returns the width of the overlay surface in overlay units. + +*Returns:* integer width. + +*Since:* 1.x +*See also:* <>, <> + +[#overlayline] +==== overlayLine + +[source,text] +---- +overlayLine(x1, y1, x2, y2) +---- + +Draws a line between two points. + +* `x1`, `y1` -- first endpoint. +* `x2`, `y2` -- second endpoint. + +*Since:* 2.00 +*See also:* <>, <> + +[#overlayplot] +==== overlayPlot + +[source,text] +---- +overlayPlot(x, y) +---- + +Plots a single pixel in the foreground color. Use sparingly -- plotting many individual pixels in Lua is slow; consider a sprite or a line for anything with structure. + +* `x`, `y` -- pixel coordinates. + +*Since:* 2.00 +*See also:* <> + +[#overlayprint] +==== overlayPrint + +[source,text] +---- +overlayPrint(column, row, text) +---- + +Prints text using the engine's built-in fixed-width console font. *Coordinates are character cells, not pixels* -- `column = 0, row = 0` is the top-left character position, `column = 1` is one font-width to the right. This is different from every other overlay function. + +* `column`, `row` -- character cell coordinates. +* `text` -- the string to print. Text that would extend past the right edge of the overlay is truncated silently. + +Rendered in the current foreground color on a background filled with the current background color (so `colorBackground(0, 0, 0, 0)` gives you background-transparent text). + +*Since:* 1.x +*See also:* <> (for proportional TrueType text), <>, <> + +.Example +[source,lua] +---- +-- Quick dev HUD using the built-in font. +function onOverlayUpdate() + colorBackground(0, 0, 0, 0) -- Transparent background. + colorForeground(255, 255, 0, 255) + overlayPrint(0, 0, "FRAME: " .. discGetFrame()) + overlayPrint(0, 1, "STATE: " .. discGetState()) + return OVERLAY_UPDATED +end +---- + +[#overlaysetresolution] +==== overlaySetResolution + +[source,text] +---- +overlaySetResolution(width, height) +---- + +Resizes the overlay surface. All previously drawn content is discarded. Call this once at startup to pick the coordinate space your game will draw in; don't change it during gameplay. + +* `width`, `height` -- overlay dimensions in pixels. + +Higher resolutions give finer control over sprite placement and sharper text at the cost of more per-frame compositing work. Matching the disc's native resolution (via `discGetWidth` / `discGetHeight`) is a reasonable default. + +*Since:* 2.00 +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Use the disc's native resolution as the overlay coordinate system. +overlaySetResolution(discGetWidth(), discGetHeight()) +---- + +[#script] +=== Script + +WARNING: These functions let a Singe script hand control to another Singe script -- the mechanism behind the built-in `Menu.singe` launcher. Game developers should not reach for these; build your game as a single script and let the menu system handle chaining. They are documented here for completeness and for anyone maintaining the menu itself. + +[#scriptexecute] +==== scriptExecute + +[source,text] +---- +scriptExecute(config) +---- + +Replaces the currently running script with a new one described by a games.dat-style table. The current script exits; control does not return. + +* `config` -- a Lua table with the same fields as a `GAMES[]` entry in `games.dat` (at minimum `SCRIPT`, and usually `VIDEO`, `DATA`, and resolution fields). + +*Since:* 2.00 + +[#scriptpush] +==== scriptPush + +[source,text] +---- +scriptPush(config) +---- + +Starts a new script but remembers the current one on an internal stack. When the new script calls `singeQuit`, the previously running script resumes from startup (it is re-run, not unfrozen in place). This is how `Menu.singe` launches a game and comes back to the menu when the game exits. + +* `config` -- a Lua table describing the script to launch (as for `scriptExecute`). + +*Since:* 2.00 + +[#singe] +=== Singe + +Functions in the `singe*` namespace control the engine itself -- window properties, screenshots, the pause system, quitting, and a handful of useful paths and flags exposed from the command line and `games.dat`. + +[#singedisablepausekey] +==== singeDisablePauseKey + +[source,text] +---- +singeDisablePauseKey() +---- + +Legacy alias for `singeSetPauseKeyEnabled(false)`, defined in `Framework.singe`. + +*Since:* 1.18 (RDG) +*See also:* <> + +[#singeenablepausekey] +==== singeEnablePauseKey + +[source,text] +---- +singeEnablePauseKey() +---- + +Legacy alias for `singeSetPauseKeyEnabled(true)`, defined in `Framework.singe`. + +*Since:* 1.18 (RDG) +*See also:* <> + +[#singegetdatapath] +==== singeGetDataPath + +[source,text] +---- +path = singeGetDataPath() +---- + +Returns the absolute path to the writable data directory for the running game (the directory specified by the `DATA` field in `games.dat`, resolved under Singe's data root). Use this for save games, high scores, and any other per-game persistent state. + +*Returns:* string path, ending in a platform-appropriate separator. + +*Since:* 2.00 +*See also:* <> + +.Example +[source,lua] +---- +-- Read and write high scores alongside the game's data. +local highScoreFile = singeGetDataPath() .. "highscores.json" +---- + +[#singegetheight] +==== singeGetHeight + +[source,text] +---- +height = singeGetHeight() +---- + +Returns the height of the Singe window in *screen pixels*, not overlay units. Useful if you want to know the actual display size (for a configuration screen, or to letterbox the overlay). + +*Returns:* integer pixel height. + +*Since:* 1.x +*See also:* <>, <> + +[#singegetpauseflag] +==== singeGetPauseFlag + +[source,text] +---- +paused = singeGetPauseFlag() +---- + +Returns the engine's pause flag: `true` while the player has paused with the pause key, or while the script has set it with `singeSetPauseFlag`. This is separate from whether the disc is sitting on a paused frame (`discGetState`); read it when you want to know whether a pause was deliberately requested. Note that while the pause key holds the game frozen your script is not running, so from inside a callback the flag can only be seen as `true` after your own `singeSetPauseFlag(true)`. + +*Returns:* boolean. + +*Since:* 1.x (RDG) +*See also:* <> + +[#singegetscriptpath] +==== singeGetScriptPath + +[source,text] +---- +path = singeGetScriptPath() +---- + +Returns the absolute path of the currently running script file. `Framework.singe` derives the global `DIR` variable from this -- you will usually use `DIR` directly rather than calling `singeGetScriptPath` yourself. + +*Returns:* string path. + +*Since:* 1.15 (RDG) +*See also:* <>, the `DIR` global + +[#singegetwidth] +==== singeGetWidth + +[source,text] +---- +width = singeGetWidth() +---- + +Returns the width of the Singe window in screen pixels. Mirror of `singeGetHeight`. + +*Returns:* integer pixel width. + +*Since:* 1.x +*See also:* <>, <> + +[#singequit] +==== singeQuit + +[source,text] +---- +singeQuit() +---- + +Exits the currently running script. If the script was pushed via `scriptPush` (typically, launched from the menu), control returns to the calling script; otherwise Singe itself terminates. + +Call this from `onInputPressed` or a game-over flow when the user chooses to exit. Do not call it from `onShutdown` -- that callback is already the exit path. + +*Since:* 1.x (RDG) + +.Example +[source,lua] +---- +function onInputPressed(what) + if what == SWITCH_QUIT then singeQuit() end +end +---- + +[#singescreenshot] +==== singeScreenshot + +[source,text] +---- +singeScreenshot() +---- + +Captures a PNG screenshot of the currently composited frame (disc + overlay) and writes it under the Singe screenshots directory. The capture is asynchronous; the file is written on the next rendering pass. + +*Since:* 1.x + +[#singesetgamename] +==== singeSetGameName + +[source,text] +---- +singeSetGameName(title) +---- + +Sets the OS window title for the Singe window. Call this once at startup with your game's display name. + +* `title` -- any string. + +*Since:* 1.15 (RDG) + +.Example +[source,lua] +---- +singeSetGameName("Dragon's Lair Remastered") +---- + +[#singesetpauseflag] +==== singeSetPauseFlag + +[source,text] +---- +singeSetPauseFlag(paused) +---- + +Sets the engine's pause flag from the script. `true` pauses the disc, every loaded video, and every sound channel, and remembers which of them were playing; `false` resumes exactly those. Unlike the pause key, this does not freeze your script: callbacks keep firing so the script can decide when to clear the flag again. Use it for story freezes and menus in games that manage their own pause; disable the engine's pause key (`singeSetPauseKeyEnabled(false)`) so the two do not fight. + +* `paused` -- boolean. + +*Since:* 1.x (RDG) +*See also:* <>, <> + +[#singesetpausekeyenabled] +==== singeSetPauseKeyEnabled + +[source,text] +---- +singeSetPauseKeyEnabled(enabled) +---- + +Chooses who owns the pause key mapped to `INPUT_PAUSE` in `controls.cfg`. While enabled (the default), the engine owns it: pressing the key freezes the game completely (see <>) and the script never receives `SWITCH_PAUSE`. While disabled, the engine ignores the key and delivers `SWITCH_PAUSE` to `onInputPressed` / `onInputReleased` like any other switch, so a game can run its own pause logic with `singeSetPauseFlag`. + +* `enabled` -- boolean. + +*Since:* 2.20 +*See also:* <>, <>, <> + +[#singeversion] +==== singeVersion + +[source,text] +---- +version = singeVersion() +---- + +Returns the running Singe engine version as a floating-point number (e.g. `2.10`). Script compatibility shims often branch on this -- look at `Framework.singe` for the pattern. + +*Returns:* number. + +*Since:* 1.x (RDG) + +.Example +[source,lua] +---- +if singeVersion() < 2.10 then + error("This game requires Singe 2.10 or newer.") +end +---- + +[#singewantscrosshairs] +==== singeWantsCrosshairs + +[source,text] +---- +wants = singeWantsCrosshairs() +---- + +Returns `true` if the user launched Singe without the `--nocrosshair` flag. Your game should draw its targeting reticle only when this returns `true`; this lets users with physical Sinden-style lightguns (which display their own on-screen reticle in hardware) turn the software crosshair off. + +*Returns:* boolean. + +*Since:* 2.00 + +.Example +[source,lua] +---- +function onOverlayUpdate() + drawScene() + if singeWantsCrosshairs() then + local x, y = mouseGetPosition(0) + drawCrosshair(x, y) + end + return OVERLAY_UPDATED +end +---- + +[#sound] +=== Sound + +Sound effects are short audio clips (typical format: WAV) loaded into memory and played through SDL_mixer. Distinct from the disc's own audio (which rides with the video) and from `video*`-asset audio. + +*Handles and channels -- the single most confusing thing about this API:* + +* `soundLoad` returns a *sound handle*. Keep it; pass it to `soundPlay` and `soundUnload`. +* `soundPlay` returns a *channel number* -- the mixer slot the sound is playing in. This is *not* the same value as the sound handle. +* `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` all take the *channel number* returned by `soundPlay`, not the sound handle. + +In short: the handle identifies the *data* (the loaded clip); the channel identifies an *instance* of that clip playing right now. One sound can be playing in multiple channels simultaneously (N overlapping footsteps, etc.). + +There is a master effects volume (`soundSetVolume`) that applies to all channels. + +[#soundfullstop] +==== soundFullStop + +[source,text] +---- +soundFullStop() +---- + +Stops every currently playing sound channel at once. Does not unload any sounds. + +*Since:* 1.16 +*See also:* <> + +[#soundgetvolume] +==== soundGetVolume + +[source,text] +---- +volume = soundGetVolume() +---- + +Returns the current master effects volume, `0` through `63`. + +*Returns:* integer. + +*Since:* 1.16 +*See also:* <> + +[#soundisplaying] +==== soundIsPlaying + +[source,text] +---- +playing = soundIsPlaying(channel) +---- + +Tests whether a specific mixer channel is currently playing audio. + +* `channel` -- the channel number returned by `soundPlay`. + +*Returns:* boolean. + +*Since:* 1.16 (RDG) +*See also:* <> + +.Example +[source,lua] +---- +-- Don't re-trigger the alarm while a previous instance is still playing. +if not soundIsPlaying(alarmChannel) then + alarmChannel = soundPlay(alarmSound) +end +---- + +[#soundload] +==== soundLoad + +[source,text] +---- +id = soundLoad(filename) +---- + +Loads an audio clip from disk. + +* `filename` -- path to a sound file (WAV is safest; other SDL_mixer-supported formats work but are not officially supported). + +*Returns:* integer sound handle (a data handle, *not* a playing-channel number). + +*Since:* 1.x +*See also:* <>, <> + +.Example +[source,lua] +---- +gunshot = soundLoad(DIR .. "sounds/shot.wav") +miss = soundLoad(DIR .. "sounds/miss.wav") +---- + +[#soundpause] +==== soundPause + +[source,text] +---- +wasPlaying = soundPause(channel) +---- + +Pauses a playing channel. The channel keeps its position; call `soundResume` to continue. + +* `channel` -- channel number from `soundPlay`. + +*Returns:* boolean indicating whether the channel was playing at the moment of the call. + +*Since:* 1.16 (RDG) +*See also:* <> + +[#soundplay] +==== soundPlay + +[source,text] +---- +channel = soundPlay(id) +---- + +Plays a loaded sound on the next available mixer channel. + +* `id` -- sound handle from `soundLoad`. + +*Returns:* integer channel number you will pass to `soundPause`, `soundResume`, `soundStop`, and `soundIsPlaying`. Returns `-1` (`SOUND_ERROR_INVALID`) if all channels are in use -- check for this if your game can fire lots of concurrent sounds. + +*Since:* 1.x +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Fire and remember the channel so we can cut it off early if needed. +shotChannel = soundPlay(gunshot) +if shotChannel < 0 then + debugPrint("All sound channels in use -- dropping gunshot") +end +---- + +[#soundresume] +==== soundResume + +[source,text] +---- +wasPaused = soundResume(channel) +---- + +Resumes a paused channel from the position where `soundPause` left it. + +* `channel` -- channel number from `soundPlay`. + +*Returns:* boolean indicating whether the channel was paused at the moment of the call. + +*Since:* 1.16 (RDG) +*See also:* <> + +[#soundsetvolume] +==== soundSetVolume + +[source,text] +---- +soundSetVolume(volume) +---- + +Sets the master effects volume. Applied immediately to every currently playing channel. + +* `volume` -- integer `0` (silent) through `63` (loudest). Out-of-range values terminate the script. + +*Since:* 1.16 +*See also:* <> + +[#soundstop] +==== soundStop + +[source,text] +---- +wasPlaying = soundStop(channel) +---- + +Halts a channel immediately. The channel is released back to the pool and the previous channel number is invalidated -- discard it on the Lua side. + +* `channel` -- channel number from `soundPlay`. + +*Returns:* boolean indicating whether the channel was playing when the call was made. + +*Since:* 1.x (RDG) +*See also:* <> + +[#soundunload] +==== soundUnload + +[source,text] +---- +soundUnload(id) +---- + +Frees a loaded sound. Stop any channels playing this sound before unloading; unloading audio data that the mixer is still reading from is undefined behavior. + +* `id` -- sound handle from `soundLoad` (*not* a channel number). + +*Since:* 2.00 +*See also:* <> + +[#sprite] +=== Sprite + +Sprites are 2D bitmap assets loaded from disk. They support: + +* *Any SDL_image-supported format* -- PNG, JPG, BMP, GIF, WEBP, and more. +* *Animation* when the source file is an animated GIF or WEBP. Single-frame images are treated as static sprites; animation functions still work on them but have no effect. +* *Per-sprite rotation and scaling* with optional bilinear filtering. +* *Transparency via color-key* -- pixels whose raw value is `0` in the image's own pixel format are drawn as fully transparent: palette index `0` for indexed images, pure black for 24-bit RGB images, and fully transparent black for images with an alpha channel. Alpha channels are honored as well, so a PNG with real transparency needs no color key. + +Every sprite you load must eventually be released with `spriteUnload`, or from `onShutdown` for assets that live for the lifetime of the game. + +[#spritedraw] +==== spriteDraw + +[source,text] +---- +spriteDraw(id, x, y) -- Natural size, top-left anchor +spriteDraw(id, x, y, centered) -- Natural size, centered anchor +spriteDraw(id, x, y, x2, y2) -- Stretched to rectangle, top-left anchor +spriteDraw(id, x, y, x2, y2, centered) -- Stretched to rectangle, centered anchor +---- + +Draws a previously loaded sprite onto the overlay. Must be called from `onOverlayUpdate`. + +* `x`, `y` -- anchor coordinates in overlay units. +* `x2`, `y2` -- opposite corner for the stretched forms. The sprite is scaled to fit `(x, y)`-`(x2, y2)`. +* `centered` -- boolean. When `true`, `(x, y)` (and the center of the `(x, y)`-`(x2, y2)` rectangle for the stretched forms) is treated as the *center* of the sprite. When `false`, it is the top-left corner. Using `centered = true` is strongly recommended when the sprite has been rotated with `spriteRotate`, because rotated sprites change size and a top-left anchor will appear to drift as the sprite spins. +* `id` -- sprite handle returned by `spriteLoad` or `fontToSprite`. + +Animation advances automatically each frame the sprite is drawn, based on real time elapsed since the previous `spriteDraw` of that sprite. A sprite that is not drawn does not advance its animation. + +*Since:* 1.x. Centered and stretched-centered forms added in 2.10. +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Draw a menu cursor that rotates slowly as a visual flourish. +cursor = spriteLoad(DIR .. "cursor.png") +cursorAngle = 0 + +function onOverlayUpdate() + cursorAngle = (cursorAngle + 2) % 360 + spriteRotate(cursor, cursorAngle) + spriteDraw(cursor, selectionX, selectionY, true) -- Center-anchored. + return OVERLAY_UPDATED +end +---- + +[#spritegetframe] +==== spriteGetFrame + +[source,text] +---- +frame = spriteGetFrame(id) +---- + +Returns the index of the currently displayed frame of an animated sprite. Frames are numbered starting at `0`. + +* `id` -- sprite handle. + +*Returns:* integer frame index, or `0` for a static sprite that has no animation. + +*Since:* 2.10 +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Fire the gun only on the frame where the muzzle flash is visible. +if spriteGetFrame(muzzleFlash) == 2 and not shotFired then + soundPlay(gunshot) + shotFired = true +end +---- + +[#spritegetheight] +==== spriteGetHeight + +[source,text] +---- +h = spriteGetHeight(id) +---- + +Returns the current height of the sprite in overlay units. This reflects any scaling or rotation previously applied via `spriteScale`, `spriteRotate`, or `spriteRotateAndScale` -- it is the *drawn* height, not the height of the source image. + +* `id` -- sprite handle. + +*Returns:* integer height. + +*Since:* 2.00 +*See also:* <>, <> + +[#spritegetwidth] +==== spriteGetWidth + +[source,text] +---- +w = spriteGetWidth(id) +---- + +Returns the current width of the sprite in overlay units. Reflects any scaling or rotation currently applied, like `spriteGetHeight`. + +* `id` -- sprite handle. + +*Returns:* integer width. + +*Since:* 2.00 +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Center a cabinet image inside a fixed drawing area. +function drawCabinet() + local x = cabinetX + (cabinetW - spriteGetWidth(cabinetSprite)) * 0.5 + local y = cabinetY + (cabinetH - spriteGetHeight(cabinetSprite)) * 0.5 + spriteDraw(cabinetSprite, x, y) +end +---- + +[#spriteisplaying] +==== spriteIsPlaying + +[source,text] +---- +playing = spriteIsPlaying(id) +---- + +Reports whether an animated sprite is currently advancing its animation. + +* `id` -- sprite handle. + +*Returns:* boolean. `true` if the animation is playing, `false` if it has been paused (`spritePause`), has finished a non-looping animation, or the sprite has no animation data at all. + +*Since:* 2.10 +*See also:* <>, <>, <> + +[#spriteload] +==== spriteLoad + +[source,text] +---- +id = spriteLoad(filename) +---- + +Loads a bitmap from disk and returns an opaque integer handle. The file is resolved relative to the working directory Singe launched from -- in practice, prepend `DIR` (the running script's directory) for files shipped with your game. + +* `filename` -- path to a PNG, JPG, BMP, GIF, WEBP, or other SDL_image-supported format. Animated GIF and WEBP files load as animated sprites; all other formats load as static sprites. + +*Returns:* integer sprite handle. Store it; you will pass it to every other `sprite*` function. + +*Notes:* + +* There is no `nil` return on failure. If the file cannot be read or decoded, the script is terminated with an error. +* Loading is synchronous and can take noticeable time for large images. In the threaded model, preload sprites before entering your main loop. + +*Since:* 1.x. Animated GIF/WEBP support added in 2.10. +*See also:* <>, <> *(Font section)* + +.Example +[source,lua] +---- +-- Preload all the game's sprites once at startup. +cabinetSprite = spriteLoad(DIR .. "cabinet.png") +marqueeSprite = spriteLoad(DIR .. "marquee.png") +explosion = spriteLoad(DIR .. "explosion.gif") -- Animated. + +spriteLoop(explosion, false) -- Play once, do not loop. +---- + +[#spriteloop] +==== spriteLoop + +[source,text] +---- +spriteLoop(id, shouldLoop) +---- + +Sets whether an animated sprite loops when it reaches the last frame. The default for newly loaded animated sprites is to loop. + +* `id` -- sprite handle. +* `shouldLoop` -- boolean. `true` restarts from frame `0`; `false` stops on the last frame and `spriteIsPlaying` begins returning `false`. + +*Since:* 2.10 +*See also:* <>, <>, <> + +[#spritepause] +==== spritePause + +[source,text] +---- +spritePause(id) +---- + +Pauses animation on an animated sprite. The current frame keeps being drawn. Call `spritePlay` to resume from the same frame. + +* `id` -- sprite handle. + +*Since:* 2.10 +*See also:* <>, <> + +[#spriteplay] +==== spritePlay + +[source,text] +---- +spritePlay(id) +---- + +Starts or resumes animation on an animated sprite. If the animation is already playing, this is a no-op (the frame timer is not reset). + +* `id` -- sprite handle. + +*Since:* 2.10 +*See also:* <>, <> + +[#spritequality] +==== spriteQuality + +[source,text] +---- +spriteQuality(id, smooth) +---- + +Selects the filtering mode used when the sprite is scaled or rotated. + +* `id` -- sprite handle. +* `smooth` -- `RENDER_PIXELATED` (`0`) for nearest-neighbor (sharp, pixelated -- right for low-res art that should stay crunchy), `RENDER_SMOOTH` (`1`) for bilinear filtering (smooth -- right for photographic art and large decorative elements). + +Quality is applied immediately: the cached rotated/scaled surface is rebuilt, so the next `spriteDraw` reflects the new setting. Changing quality on every frame is wasteful; set it once after `spriteLoad`. + +*Since:* 2.10 +*See also:* <>, <> + +[#spriterotate] +==== spriteRotate + +[source,text] +---- +spriteRotate(id, angle) +---- + +Rotates a sprite by the given angle. + +* `id` -- sprite handle. +* `angle` -- rotation in degrees, clockwise. The value is wrapped into `0`-`360` automatically, so `spriteRotate(s, 450)` and `spriteRotate(s, 90)` are equivalent. + +Rotation changes the sprite's bounding box, so `spriteGetWidth` and `spriteGetHeight` return different values after a rotation. Use `spriteDraw(id, x, y, true)` -- the centered form -- to keep the visual center fixed as the sprite rotates. + +*Since:* 2.10 +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Spin the "PRESS START" indicator to draw the eye during attract mode. +function onOverlayUpdate() + attractAngle = (attractAngle + 3) % 360 + spriteRotate(pressStart, attractAngle) + spriteDraw(pressStart, screenCenterX, screenCenterY, true) + return OVERLAY_UPDATED +end +---- + +[#spriterotateandscale] +==== spriteRotateAndScale + +[source,text] +---- +spriteRotateAndScale(id, angle, scale) +spriteRotateAndScale(id, angle, scaleX, scaleY) +---- + +Applies both rotation and scaling in a single call. Cheaper than calling `spriteRotate` and `spriteScale` separately, because the intermediate surface only has to be rebuilt once. + +* `angle` -- rotation in degrees, wrapped to `0`-`360`. +* `scale` -- uniform scale factor (`1.0` = natural size, `2.0` = double size, `0.5` = half size). +* `scaleX`, `scaleY` -- separate horizontal and vertical scale factors for stretching or squishing. +* `id` -- sprite handle. + +*Since:* 2.10 +*See also:* <>, <> + +[#spritescale] +==== spriteScale + +[source,text] +---- +spriteScale(id, scale) +spriteScale(id, scaleX, scaleY) +---- + +Scales a sprite uniformly or along each axis independently. + +* `scale` -- uniform scale factor. +* `scaleX`, `scaleY` -- separate horizontal and vertical factors. +* `id` -- sprite handle. + +The scaled surface is cached, so repeated `spriteDraw` calls are cheap -- but each new `spriteScale` rebuilds that cache. Avoid changing the scale every frame if you do not need to. + +*Since:* 2.10 +*See also:* <>, <> + +[#spritesetframe] +==== spriteSetFrame + +[source,text] +---- +spriteSetFrame(id, frame) +---- + +Jumps an animated sprite directly to a specific frame. The per-frame delay timer is reset, so the sprite will sit on the new frame for its full duration before advancing. + +* `id` -- sprite handle. +* `frame` -- zero-based frame index. Silently ignored if out of range, and for sprites that are not animations. + +*Since:* 2.10 +*See also:* <>, <> + +[#spriteunload] +==== spriteUnload + +[source,text] +---- +spriteUnload(id) +---- + +Releases all memory associated with a sprite -- the source image, the cached rotated/scaled surface, and animation frames for animated sprites. After this call the handle is invalid; passing it to any other sprite function will terminate the script. + +* `id` -- sprite handle. + +*Since:* 2.00 +*See also:* <> + +.Example +[source,lua] +---- +-- Free everything we loaded, called by Singe when the user exits. +function onShutdown() + spriteUnload(cabinetSprite) + spriteUnload(marqueeSprite) + spriteUnload(explosion) +end +---- + +[#video] +=== Video + +The `video*` family handles *additional video assets* on top of the main laserdisc video. Typical uses: a side-video of a character talking in a corner of the screen, a pre-rendered animation that plays once at a story beat, multiple concurrent video cues. You can load any number of videos, unlike the disc (which is a singleton). + +Key differences from the `disc*` family: + +* Videos are loaded by filename at runtime with `videoLoad`; the disc comes from `games.dat`. +* Videos can be *drawn at arbitrary positions on the overlay* (with `videoDraw`), rotated, and scaled. The disc fills the background behind the overlay; videos composite through the overlay like sprites do. +* Each video has its own audio track selection and volume, independent of the disc. +* Argument order is *different* from sprites: `videoDraw(id, x, y, ...)` takes the handle first. Every other `video*` function also takes the handle first. + +Like sprites, videos must be drawn from `onOverlayUpdate` and eventually freed with `videoUnload`. + +[#videodraw] +==== videoDraw + +[source,text] +---- +videoDraw(id, x, y, x2, y2) -- Stretched to rectangle, top-left anchor +videoDraw(id, x, y, centered) -- Scaled/rotated per videoScale/videoRotate, optional center anchor +---- + +Draws the current frame of a video onto the overlay, advancing the video's decode by one frame of wall time. + +* `id` -- video handle from `videoLoad`. +* `x`, `y` -- anchor coordinates (overlay units). +* `x2`, `y2` -- opposite corner for the stretched form. Video is scaled to fit the rectangle. +* `centered` -- boolean (for the scaled/rotated form). When `true`, `(x, y)` is the center of the drawn video; when `false`, it is the top-left. Use `true` for rotated videos. + +To use the non-stretched form with rotation or scaling, apply `videoScale` and/or `videoRotate` first, then pass the boolean form: `videoDraw(id, x, y, true)`. + +*Since:* 2.00. The scaled/rotated form with `centered` flag added in 2.10. +*See also:* <>, <>, <>, <> + +.Example +[source,lua] +---- +-- Draw a picture-in-picture video of the NPC in the upper right. +function onOverlayUpdate() + videoDraw(npcVideo, 900, 40, 1260, 240) -- Stretched to a 360x200 box. + return OVERLAY_UPDATED +end +---- + +[#videogetaudiotrack] +==== videoGetAudioTrack + +[source,text] +---- +track = videoGetAudioTrack(id) +---- + +Returns the index of the currently selected audio track for a loaded video. + +*Returns:* integer track index. + +*Since:* 2.10 +*See also:* <>, <> + +[#videogetaudiotracks] +==== videoGetAudioTracks + +[source,text] +---- +count = videoGetAudioTracks(id) +---- + +Returns the number of audio tracks available on a loaded video. + +*Returns:* integer count. + +*Since:* 2.10 +*See also:* <> + +[#videogetframe] +==== videoGetFrame + +[source,text] +---- +frame = videoGetFrame(id) +---- + +Returns the current frame number of a loaded video. + +*Returns:* integer frame. + +*Since:* 2.00 +*See also:* <>, <> + +[#videogetframecount] +==== videoGetFrameCount + +[source,text] +---- +total = videoGetFrameCount(id) +---- + +Returns the total number of frames in a loaded video. Useful for progress bars, for wrapping a loop, or for detecting when a one-shot clip is almost done. + +*Returns:* integer total frame count. + +*Since:* 2.00 +*See also:* <>, <> + +[#videogetheight] +==== videoGetHeight + +[source,text] +---- +height = videoGetHeight(id) +---- + +Returns the source height of a loaded video in pixels (the native resolution of the file, not how big it's drawn on the overlay). + +*Returns:* integer height. + +*Since:* 2.00 +*See also:* <> + +[#videogetlanguage] +==== videoGetLanguage + +[source,text] +---- +code = videoGetLanguage(id, track) +---- + +Returns the ISO language code for an audio track on a loaded video. + +* `id` -- video handle. +* `track` -- audio track index. + +*Returns:* three-letter language code string (e.g. `"eng"`, `"jpn"`), or empty if no language tag is set. + +*Since:* 2.10 +*See also:* <>, <> + +[#videogetlanguagedescription] +==== videoGetLanguageDescription + +[source,text] +---- +name = videoGetLanguageDescription(code) +---- + +Looks up a human-readable name for an ISO language code. Unlike the other `video*` functions this one does *not* take a video handle -- it is a pure lookup table. + +`Framework.singe` aliases this as `discGetLanguageDescription` for symmetry with the disc API. + +* `code` -- three-letter code like `"eng"` or `"jpn"`. + +*Returns:* English language name (e.g. `"English"`, `"Japanese"`), or `"Unknown"` for unrecognized codes. + +*Since:* 2.10 +*See also:* <>, <> + +.Example +[source,lua] +---- +-- Build a language-select menu from a video's tracks. +for i = 0, videoGetAudioTracks(cutscene) - 1 do + local code = videoGetLanguage(cutscene, i) + local name = videoGetLanguageDescription(code) + table.insert(options, { track = i, label = name }) +end +---- + +[#videogetvolume] +==== videoGetVolume + +[source,text] +---- +left, right = videoGetVolume(id) +---- + +Returns the current per-channel volume of a loaded video. Note this returns *two values* -- left and right. + +*Returns:* two integers, each `0` to `100`. + +*Since:* 2.00 +*See also:* <> + +[#videogetwidth] +==== videoGetWidth + +[source,text] +---- +width = videoGetWidth(id) +---- + +Returns the source width of a loaded video in pixels. + +*Returns:* integer width. + +*Since:* 2.00 +*See also:* <> + +[#videoisplaying] +==== videoIsPlaying + +[source,text] +---- +playing = videoIsPlaying(id) +---- + +Reports whether a loaded video is currently playing. + +*Returns:* boolean (pushed as a number, `0` or `1` -- treat as boolean in Lua: `videoIsPlaying(id) ~= 0`). + +*Since:* 2.00 +*See also:* <>, <>, <> + +[#videoload] +==== videoLoad + +[source,text] +---- +id = videoLoad(filename) +---- + +Loads a video file. Supported formats are everything FFmpeg can decode that Singe ships a codec for (MP4, MKV, WEBM, MPEG, and more). Loading a video creates a per-game data subdirectory under Singe's data root (used for things like seek indexes); this is managed automatically. + +* `filename` -- path to a video file. + +*Returns:* integer video handle. + +The default audio track and volume are taken from the command line or `games.dat`. Change them afterwards with `videoSetAudioTrack` and `videoSetVolume` if needed. + +*Since:* 2.00 +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- Load a cutscene video to play over the paused laserdisc. +cutscene = videoLoad(DIR .. "videos/intro.mp4") +videoSetVolume(cutscene, 80, 80) +videoPlay(cutscene) +---- + +[#videopause] +==== videoPause + +[source,text] +---- +videoPause(id) +---- + +Pauses decoding and audio on a video. The last-decoded frame keeps being drawn by `videoDraw`. + +*Since:* 2.00 +*See also:* <> + +[#videoplay] +==== videoPlay + +[source,text] +---- +videoPlay(id) +---- + +Starts or resumes a video. + +*Since:* 2.00 +*See also:* <>, <> + +[#videoquality] +==== videoQuality + +[source,text] +---- +videoQuality(id, smooth) +---- + +Selects the filtering mode used when the video is scaled or rotated. Applied on the next frame. + +* `id` -- video handle. +* `smooth` -- `RENDER_PIXELATED` (`0`) for nearest-neighbor, `RENDER_SMOOTH` (`1`) for bilinear filtering. + +*Since:* 2.10 +*See also:* <>, <> + +[#videorotate] +==== videoRotate + +[source,text] +---- +videoRotate(id, angle) +---- + +Sets the rotation applied to the video by `videoDraw` (in the non-stretched form). + +* `id` -- video handle. +* `angle` -- degrees clockwise. Wrapped to `0`-`360`. + +*Since:* 2.10 +*See also:* <>, <> + +[#videorotateandscale] +==== videoRotateAndScale + +[source,text] +---- +videoRotateAndScale(id, angle, scale) +videoRotateAndScale(id, angle, scaleX, scaleY) +---- + +Applies both rotation and scaling in one call. + +* `id` -- video handle. +* `angle` -- degrees clockwise. +* `scale` -- uniform factor. +* `scaleX`, `scaleY` -- separate horizontal/vertical factors. + +*Since:* 2.10 +*See also:* <>, <> + +[#videoscale] +==== videoScale + +[source,text] +---- +videoScale(id, scale) +videoScale(id, scaleX, scaleY) +---- + +Scales the video for subsequent `videoDraw` calls in the non-stretched form. + +* `id` -- video handle. +* `scale` -- uniform factor. +* `scaleX`, `scaleY` -- separate horizontal/vertical factors. + +*Since:* 2.10 +*See also:* <>, <> + +[#videoseek] +==== videoSeek + +[source,text] +---- +videoSeek(id, frame) +---- + +Jumps a loaded video to a specific frame. Does not change play/pause state. + +* `id` -- video handle. +* `frame` -- target frame number. + +*Since:* 2.00 +*See also:* <>, <> + +[#videosetaudiotrack] +==== videoSetAudioTrack + +[source,text] +---- +videoSetAudioTrack(id, track) +---- + +Switches the active audio track on a loaded video. Out-of-range tracks terminate the script. + +* `id` -- video handle. +* `track` -- audio track index, `0` through `videoGetAudioTracks(id) - 1`. + +*Since:* 2.10 +*See also:* <> + +[#videosetvolume] +==== videoSetVolume + +[source,text] +---- +videoSetVolume(id, left, right) +---- + +Sets per-channel volume for a loaded video. Silently clamps values to `0`-`100`. + +* `id` -- video handle. +* `left`, `right` -- channel volumes, `0` (silent) to `100` (loudest). + +*Since:* 2.00 +*See also:* <> + +.Example +[source,lua] +---- +-- Fade the cutscene audio down to 20% as we return control to the player. +for v = 100, 20, -4 do + videoSetVolume(cutscene, v, v) + coroutine.yield() -- In threaded mode; or spread across frames otherwise. +end +---- + +[#videounload] +==== videoUnload + +[source,text] +---- +videoUnload(id) +---- + +Releases a loaded video and all associated resources (decoder, surfaces, cached frames). + +* `id` -- video handle. + +*Since:* 2.00 +*See also:* <> + +[#vldp] +=== VLDP + +WARNING: *Legacy.* The `vldp*` namespace predates Singe 2.00 and exists to keep pre-2.00 scripts running. For new code, prefer the modern equivalents: - `vldpGetHeight` -> <> - `vldpGetWidth` -> <> - `vldpSetVerbose` -- unimplemented; ignore. `vldpGetPixel` is the one function in this namespace without a modern replacement and is still useful. + +[#vldpgetheight] +==== vldpGetHeight + +[source,text] +---- +height = vldpGetHeight() +---- + +Returns the height of the main laserdisc video in pixels. Equivalent to `discGetHeight`. + +*Returns:* integer height. + +*Since:* 1.x +*See also:* <> + +[#vldpgetpixel] +==== vldpGetPixel + +[source,text] +---- +r, g, b = vldpGetPixel(x, y) +---- + +Reads the color of a single pixel from the current laserdisc frame at overlay coordinates `(x, y)`. The coordinates are internally scaled from overlay units to video pixels, so you pass the same coordinates you use everywhere else on the overlay. + +* `x`, `y` -- overlay coordinates. + +*Returns:* three integers: red, green, blue, each `0` to `255`. + +This is the preferred technique for *hit detection on pre-recorded video*. Rather than hand-authoring hit boxes for every frame, many laserdisc games tag targets with a specific color in the source footage (e.g. a bright magenta outline invisible to the eye) and check the pixel under the crosshair on a trigger pull. + +*Since:* 1.x +*See also:* <> + +.Example +[source,lua] +---- +-- Lightgun hit check: the target is drawn in pure magenta on enemy frames. +function onInputPressed(what) + -- The default controls.cfg maps MOUSE_0.BUTTON_LEFT to SWITCH_BUTTON3. + if what == SWITCH_BUTTON3 then + local x, y = mouseGetPosition(0) + local r, g, b = vldpGetPixel(x, y) + if r > 200 and g < 50 and b > 200 then + scoreHit() + else + scoreMiss() + end + end +end +---- + +[#vldpgetwidth] +==== vldpGetWidth + +[source,text] +---- +width = vldpGetWidth() +---- + +Returns the width of the main laserdisc video in pixels. Equivalent to `discGetWidth`. + +*Returns:* integer width. + +*Since:* 1.x +*See also:* <> + +[#vldpsetverbose] +==== vldpSetVerbose + +WARNING: *Unimplemented.* No-op. Retained for backward compatibility. + +[#enginecallbacks] +=== Engine Callbacks + +These are functions *you define* in your script. Singe calls them when the corresponding event happens. All are optional -- define only the ones your game needs. + +In the threaded programming model, the `singeMain` function replaces most of them (see <>), but the callbacks still exist and still fire, with one exception: `Framework.singe` installs its own `onOverlayUpdate` to drive the `singeMain` coroutine. Most threaded games define only `singeMain` and `onShutdown`. + +[#oncontrollermoved] +==== onControllerMoved + +[source,text] +---- +function onControllerMoved(axis, value, which) + -- axis: physical axis index on the controller + -- value: -32768 to 32767 + -- which: SDL internal controller instance ID +end +---- + +Called when an analog axis on any connected controller moves. Not throttled -- a continuously moved stick will fire many times per second. Apply the `SINGE_DEAD_ZONE` threshold before reacting. + +The `which` parameter is the controller index, `0` through `3`, matching the index used by `controllerGetAxis` and the `GAMEPAD_0` through `GAMEPAD_3` tables. Most games ignore it and treat all controllers equivalently. + +*See also:* <>, the `GAMEPAD_AXIS_*` constants + +[#oninputpressedoninputreleased] +==== onInputPressed / onInputReleased + +[source,text] +---- +function onInputPressed(what) + -- what: an integer code for the logical input +end + +function onInputReleased(what) + -- what: same as above +end +---- + +Called when a mapped input fires. The value passed depends on the current keyboard mode: + +* *`MODE_NORMAL`*: one of the `SWITCH_*` constants (`SWITCH_UP`, `SWITCH_START1`, `SWITCH_COIN1`, etc.). Keys, controller buttons, and mouse buttons all arrive this way, translated through the mappings in `controls.cfg`; an input that is not mapped to any switch is ignored in this mode. `SWITCH_PAUSE` arrives only when the engine's pause key has been disabled with `singeSetPauseKeyEnabled(false)`; otherwise the engine handles it and freezes the game (see <>). +* *`MODE_FULL`*: the SDL keysym of the key pressed (the character value, which is not the same as the scancode). Controller and mouse buttons pass `0` here. In this mode every raw event also comes through `onKeyPressed` / `onKeyReleased` (see below), which is where the scancode or button code lives. + +.Example +[source,lua] +---- +function onInputPressed(what) + if what == SWITCH_COIN1 then addCredit() + elseif what == SWITCH_START1 then startGame() + elseif what == SWITCH_QUIT then singeQuit() + elseif what == SWITCH_BUTTON3 then fireShot() -- Left mouse button by default. + end +end +---- + +[#onkeypressedonkeyreleased] +==== onKeyPressed / onKeyReleased + +[source,text] +---- +function onKeyPressed(keysym, scancode) +end + +function onKeyReleased(keysym, scancode) +end +---- + +*`MODE_FULL` only.* Called when any keyboard key is pressed or released, with both the logical keysym (what character the key produces) and the physical scancode (what key position was pressed). + +For text entry, use `keysym`. For key-as-button controls, use `scancode` so the binding works across layouts. + +.Example +[source,lua] +---- +-- High score initial entry -- text, so use keysym. +function onKeyPressed(keysym, scancode) + if keysym >= string.byte('A') and keysym <= string.byte('Z') then + initials = initials .. string.char(keysym) + end +end +---- + +[#onmousemoved] +==== onMouseMoved + +[source,text] +---- +function onMouseMoved(x, y, xRelative, yRelative, which) +end +---- + +Called on every mouse-movement event. Parameters depend on the mouse mode: + +* *`SINGLE_MOUSE`* (default): `x` and `y` are absolute overlay coordinates; `xRelative` and `yRelative` are deltas since the previous event; `which` is always `0`. +* *`MANY_MOUSE`*: only `xRelative` / `yRelative` are reliable (they are the raw deltas from the physical device); `which` identifies which mouse moved. + +For a persistently visible crosshair, track the latest `(x, y)` in globals from this callback and render from `onOverlayUpdate`. + +.Example +[source,lua] +---- +cursorX, cursorY = 640, 360 +function onMouseMoved(x, y, xr, yr, which) + cursorX, cursorY = x, y +end +---- + +[#onoverlayupdate] +==== onOverlayUpdate + +[source,text] +---- +function onOverlayUpdate() + -- Draw your frame here. + return OVERLAY_UPDATED -- Or OVERLAY_NOT_UPDATED if nothing changed. +end +---- + +The single callback where drawing is allowed. Singe calls it once per rendering pass. Any `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint`, or similar call outside this callback will not appear on screen and may corrupt the overlay. + +*Return value:* + +* `OVERLAY_UPDATED` (`1`) -- the overlay changed; Singe re-composites it on top of the video. +* `OVERLAY_NOT_UPDATED` (`0`) -- you drew nothing (or the overlay looks the same as last frame); Singe reuses the previous composite. Optimization only; when in doubt, return `OVERLAY_UPDATED`. + +In the threaded model, `Framework.singe` auto-defines `onOverlayUpdate` to resume `singeMain`'s coroutine and always returns `OVERLAY_UPDATED`. Don't define it yourself when using threaded mode. + +[#onshutdown] +==== onShutdown + +[source,text] +---- +function onShutdown() + -- Free loaded sprites, sounds, videos, fonts here. +end +---- + +Called once, just before Singe exits or switches to the next script (via `scriptExecute` / `scriptPush` / `singeQuit`). Use it to free every handle you loaded. + +[#onsoundcompleted] +==== onSoundCompleted + +[source,text] +---- +function onSoundCompleted(channel) + -- channel: the channel number returned by the soundPlay call that just finished. +end +---- + +Called each time a sound finishes playing on its own (not when it is stopped via `soundStop`). Useful for chaining music stingers or reusing channel numbers. + +.Example +[source,lua] +---- +function onSoundCompleted(channel) + if channel == musicChannel then + musicChannel = soundPlay(nextTrack) + end +end +---- + +[#singemain] +==== singeMain + +[source,text] +---- +function singeMain() + while true do + -- Your game logic here. + singeYield() -- Cooperatively yield once per frame. + end +end +---- + +*Threaded model entry point.* Define this function and `Framework.singe` sets up a coroutine that runs it, scheduled from the engine's overlay-update tick. See the Manual's "Threaded" section for the full pattern. + +* `singeYield()` is an alias for `coroutine.yield` -- call it anywhere your code would otherwise consume substantial time, to let the engine continue servicing input, audio, and video. +* Do not also define `onOverlayUpdate` -- `Framework.singe` installs its own when `singeMain` is defined. + +[#unimplementedandlegacy] +=== Unimplemented and Legacy + +A handful of API functions are retained for script-level compatibility but do nothing. They are safe to call; they simply log a trace message and return. Prefer the listed replacements for new code. + +[cols="1,1,1",options="header"] +|=== +| Function | Status | Replacement +| `discChangeSpeed` | No-op | -- (was for variable-speed playback on real LD hardware) +| `discSearchBlanking` | No-op | -- (was screen-blanking during seek on real LD hardware) +| `discSkipBlanking` | No-op | -- (same reason) +| `discSetFPS` | No-op | Framerate is read from the video file. +| `vldpGetHeight` | Works | <> +| `vldpGetWidth` | Works | <> +| `vldpSetVerbose` | No-op | -- +| `colorBackground(r,g,b)` | Works | Prefer the 4-argument form, `colorBackground(r, g, b, a)`. +| `colorForeground(r,g,b)` | Works | Prefer the 4-argument form, `colorForeground(r, g, b, a)`. +| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Aliases in `Framework.singe` | Prefer the `singe*` equivalents. +| `discPauseAtFrame` | Works (alias) | <>. +|=== + +In addition, `Framework.singe` rebinds `random = { new = math.random }` so that pre-2.00 scripts using `random.new()` still work. New code should use `math.random` directly. diff --git a/patches/ActionMax/Emulator.singe b/patches/ActionMax/Emulator.singe index 3601ac9de..996f86740 100755 --- a/patches/ActionMax/Emulator.singe +++ b/patches/ActionMax/Emulator.singe @@ -15,20 +15,20 @@ pixelLow = 0 pixelHigh = 1 pixelUnknown = 2 -sprLightOn = spriteLoad("ActionMax/sprite_LightOn.png") -sprLightOff = spriteLoad("ActionMax/sprite_LightOff.png") -sprActionMax = spriteLoad("ActionMax/sprite_ActionMax.png") -sprCrosshair = spriteLoad("ActionMax/sprite_Crosshair.png") -sprBullet = spriteLoad("ActionMax/sprite_Bullet.png") -sprBoxArt = spriteLoad("ActionMax/sprite_" .. gameID .. ".png") +sprLightOn = spriteLoad(DIR .. "sprite_LightOn.png") +sprLightOff = spriteLoad(DIR .. "sprite_LightOff.png") +sprActionMax = spriteLoad(DIR .. "sprite_ActionMax.png") +sprCrosshair = spriteLoad(DIR .. "sprite_Crosshair.png") +sprBullet = spriteLoad(DIR .. "sprite_Bullet.png") +sprBoxArt = spriteLoad(DIR .. "sprite_" .. gameID .. ".png") -sndActionMax = soundLoad("ActionMax/sound_ActionMax.wav") -sndSteadyAim = soundLoad("ActionMax/sound_ASteadyAimIsCritical.wav") -sndGetReady = soundLoad("ActionMax/sound_GetReadyForAction.wav") -sndGunShot = soundLoad("ActionMax/sound_Gunshot.wav") -sndGoodHit = soundLoad("ActionMax/sound_GoodHit.wav") -sndBadHit = soundLoad("ActionMax/sound_BadHit.wav") -sndGameOver = soundLoad("ActionMax/sound_GameOver.wav") +sndActionMax = soundLoad(DIR .. "sound_ActionMax.wav") +sndSteadyAim = soundLoad(DIR .. "sound_ASteadyAimIsCritical.wav") +sndGetReady = soundLoad(DIR .. "sound_GetReadyForAction.wav") +sndGunShot = soundLoad(DIR .. "sound_Gunshot.wav") +sndGoodHit = soundLoad(DIR .. "sound_GoodHit.wav") +sndBadHit = soundLoad(DIR .. "sound_BadHit.wav") +sndGameOver = soundLoad(DIR .. "sound_GameOver.wav") mouseX = 0 mouseY = 0 @@ -63,11 +63,11 @@ thisSeconds = 0 lastSeconds = 0 heartbeat = false -fntBlueStone20 = fontLoad("ActionMax/font_BlueStone.ttf", 20) -fntChemRea16 = fontLoad("ActionMax/font_chemrea.ttf", 16) -fntChemRea32 = fontLoad("ActionMax/font_chemrea.ttf", 32) -fntChemRea48 = fontLoad("ActionMax/font_chemrea.ttf", 48) -fntLEDReal32 = fontLoad("ActionMax/font_LED_Real.ttf", 32) +fntBlueStone20 = fontLoad(DIR .. "font_BlueStone.ttf", 20) +fntChemRea16 = fontLoad(DIR .. "font_chemrea.ttf", 16) +fntChemRea32 = fontLoad(DIR .. "font_chemrea.ttf", 32) +fntChemRea48 = fontLoad(DIR .. "font_chemrea.ttf", 48) +fntLEDReal32 = fontLoad(DIR .. "font_LED_Real.ttf", 32) colorBackground(0, 0, 0, 0) fontQuality(FONT_QUALITY_BLENDED) @@ -170,51 +170,6 @@ function onInputPressed(intWhat) end ---dbgPaused = 0 ---dbgSpeed = 1 - -function onInputReleased(intWhat) - - --[[ We don't use this. All this is debug stuff. - - if (intWhat == SWITCH_BUTTON1) then - if (currentState == statePlaying) then - discSearch(lengthIntro + lengthGame - 60) - discPlay() - else - if (dbgPaused == 1) then - discPlay() - dbgPaused = 0 - else - discPause() - dbgPaused = 1 - end - end - end - if (intWhat == SWITCH_RIGHT) then - if (dbgSpeed < 4) then - dbgSpeed = dbgSpeed + 1 - end - discChangeSpeed(dbgSpeed, 1) - end - if (intWhat == SWITCH_LEFT) then - if (dbgSpeed > 1) then - dbgSpeed = dbgSpeed - 1 - end - discChangeSpeed(dbgSpeed, 1) - end - if (intWhat == SWITCH_UP) then - discStepBackward() - end - if (intWhat == SWITCH_DOWN) then - discStepForward() - end - - --]] - -end - - function onMouseMoved(intX, intY, intXrel, intYrel) -- Remember the mouse location for use later. @@ -307,9 +262,9 @@ function onOverlayUpdate() elseif (currentState == stateTitle) then - spriteDraw(logoLeft, logoTop, sprActionMax) - spriteDraw(boxLeft, boxTop, sprBoxArt) - spriteDraw(lastGameLeft, lastGameTop, sprLastGame) + spriteDraw(sprActionMax, logoLeft, logoTop) + spriteDraw(sprBoxArt, boxLeft, boxTop) + spriteDraw(sprLastGame, lastGameLeft, lastGameTop) y = scoreTop fontPrint(scoreLeft, y, " Shots Fired: " .. shotsFired) y = y + scoreHeight @@ -326,16 +281,16 @@ function onOverlayUpdate() end fontPrint(scoreLeft, y, " Game Score: " .. scorePercent .. "%") if (heartbeat) then - spriteDraw(pullToStartLeft, pullToStartTop, sprPullToStart) + spriteDraw(sprPullToStart, pullToStartLeft, pullToStartTop) end elseif (currentState == stateMenu) then - spriteDraw(selectGameTypeLeft, selectGameTypeTop, sprSelectGameType) - spriteDraw(standard1Left, standardLimited1Top, sprStandard1) - spriteDraw(standard2Left, standardLimited2Top, sprStandard2) - spriteDraw(limited1Left, standardLimited1Top, sprLimited1) - spriteDraw(limited2Left, standardLimited2Top, sprLimited2) + spriteDraw(sprSelectGameType, selectGameTypeLeft, selectGameTypeTop) + spriteDraw(sprStandard1, standard1Left, standardLimited1Top) + spriteDraw(sprStandard2, standard2Left, standardLimited2Top) + spriteDraw(sprLimited1, limited1Left, standardLimited1Top) + spriteDraw(sprLimited2, limited2Left, standardLimited2Top) if (currentFrame >= lengthIntro + lengthGame + lengthMenu) then discSearch(lengthIntro + lengthGame + 2) @@ -345,7 +300,7 @@ function onOverlayUpdate() elseif (currentState == stateIntro) then if (heartbeat) then - spriteDraw(getReadyLeft, getReadyTop, sprGetReady) + spriteDraw(sprGetReady, getReadyLeft, getReadyTop) end -- Skip into game video when the intro is over. @@ -423,9 +378,9 @@ function onOverlayUpdate() -- Do we need to light the light? if (lightDisplay > 0) then - spriteDraw(lightLeft, lightTop, sprLightOn) + spriteDraw(sprLightOn, lightLeft, lightTop) else - spriteDraw(lightLeft, lightTop, sprLightOff) + spriteDraw(sprLightOff, lightLeft, lightTop) end -- Do we need to draw the ammo display? @@ -440,7 +395,7 @@ function onOverlayUpdate() if (ammoLeft > 0) then bulletStart = overlayGetWidth() - bulletWidth - 5 for i=1,ammoLeft do - spriteDraw(bulletStart, 0, sprBullet) + spriteDraw(sprBullet, bulletStart, 0) bulletStart = bulletStart - bulletWidth end end @@ -448,7 +403,7 @@ function onOverlayUpdate() elseif (currentState == stateGameOver) then - spriteDraw(gameOverLeft, gameOverTop, sprGameOver) + spriteDraw(sprGameOver, gameOverLeft, gameOverTop) if (currentFrame >= lengthIntro + lengthGame + lengthMenu) then discPause() @@ -462,7 +417,7 @@ function onOverlayUpdate() -- Draw gun crosshair (This must be the last thing we draw so it's on top.) if (singeWantsCrosshairs()) then - spriteDraw(mouseX - crosshairCenterX, mouseY - crosshairCenterY, sprCrosshair) + spriteDraw(sprCrosshair, mouseX - crosshairCenterX, mouseY - crosshairCenterY) end return(OVERLAY_UPDATED) diff --git a/patches/README b/patches/README new file mode 100644 index 000000000..3aa8a6a3f --- /dev/null +++ b/patches/README @@ -0,0 +1,18 @@ +Game Patches +============ + +These are corrected copies of scripts from third party Singe games. They +are not part of the engine build; each one replaces the same named file +inside an installed game. Package one as a ".patch" archive (see +"Packaging Your Game" in the manual) or copy the file over the original. + +ActionMax/Emulator.singe + Shared emulator script used by every ActionMax title. Fixes sprite + leaks in the original release and uses the Singe 2.20 sprite argument + order. The per-game wrapper script must define gameID, backgroundFrame, + lengthIntro, lengthGame, lengthMenu, highThreshold, lowThreshold, + sensorX, sensorY, sensorLeft, and sensorTop before loading it. + +daitarn_3_singe/Script/toolbox.singe + Helper library from "Daitarn 3" (Karis, 2020). The calling script must + define OVLW, OVLH, and bPause. diff --git a/patches/daitarn_3_singe/Script/toolbox.singe b/patches/daitarn_3_singe/Script/toolbox.singe index 2eda6ea38..aa372991e 100755 --- a/patches/daitarn_3_singe/Script/toolbox.singe +++ b/patches/daitarn_3_singe/Script/toolbox.singe @@ -1,332 +1,330 @@ ---[[ - -PROGRAM NAME: LUA SINGE -VERSION: 1.1 -AUTHOR: KARIS (2020) - -This file is part of LUA SINGE. - - LUA SINGE 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. - - LUA SINGE 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. - - Thanks to Scott Duensing, RDG. - -]]-- - -iSecs = 0 -iLimit = 0 -lastSeconds = 0 -thisSeconds = 0 - -sprText = nil -sprShadow = nil -sLastText = nil -iLastColor = -1 -iLastShadow = -1 - -tSecs = 0 -tLimit = 0 -tlastSeconds = 0 -tthisSeconds = 0 -bTommy = false - -bGunMute = false -iMuteFrames = 0 -MUTE_DELAY = 35 - -heartbeat = false -blinkSecs = 0 -lastBlinkSecs = 0 - -iRevFrames = 0 -REV_DELAY = 10 -bReversePointer = false -revsetx = 0; revsety = 0 - -iFrameStart = 0; iFrameEnd = 0 - -CHANNEL_LEFT = 1 -CHANNEL_RIGHT = 2 -ALL_CHANNELS = 3 -bMuteAttract = false - -RED = 0 -BLUE = 1 -YELLOW = 2 -GREEN = 3 -ORANGE = 4 -WHITE = 5 -GREY = 6; GRAY = 6 -LIGHTBLUE = 7 -BLACK = 8 - -function singeRandomize() - - math.randomseed(os.time()) -- random initialize - math.random(); math.random(); math.random() -- warming up - -end - -function blinkTimer(thisMS) - - -- Function blinks every second. - - blinkSecs = os.clock() - - if bPause then - - lastBlinkSecs = blinkSecs - - else - - if (blinkSecs - lastBlinkSecs > thisMS) then - heartbeat = not heartbeat - lastBlinkSecs = blinkSecs - - end - - end - -end - -function goTimer(thisMS) - - blinkSecs = os.clock() - - if bPause then - - lastBlinkSecs = blinkSecs - - else - - if (blinkSecs - lastBlinkSecs > thisMS) then - heartbeat = true - lastBlinkSecs = blinkSecs - - end - - end - -end - -function clockRnd() - - local j = 0 - local q = 0 - local w = 0 - local r = 0 - local b1 = true - - j = os.clock() - q, w = math.modf(j) - - s2 = tostring(w) - r = string.find(s2,".") - - if (r == nil) then - - s2 = tostring(q) - s2 = string.sub(s2,string.len(s2), string.len(s2)) - - else - - s2 = string.sub(s2, r + 1) - r = string.len(s2) - - if r == 0 then - - s2 = tostring(q) - s2 = string.sub(s2,string.len(s2), string.len(s2)) - - - elseif r == 2 then - - s2 = string.sub(s2, 2, 2) - - elseif r >= 3 then - - s2 = string.sub(s2, 3, 3) - - end - - end - - w = tonumber(s2) - return w - -end - -function timerOFF() - - iSecs = 0 - iLimit = 0 - -end - -function timerON(thisLong) - - iSecs = 0 - iLimit = thisLong - lastSeconds = os.clock() - -end - -function timerDue() - - thisSeconds = os.clock() - - if bPause then - - lastSeconds = thisSeconds - - else - - if (thisSeconds ~= lastSeconds) then - - iSecs = iSecs + thisSeconds - lastSeconds - lastSeconds = thisSeconds - - end - - if (iSecs >= iLimit) then - - timerOFF() - return true - - else - - return false - - end - - end - -end - - -function muteSound() - - iMuteFrames = 0 - bGunMute = true - -end - -function blinkRev() - - iRevFrames = 0 - bReversePointer = true - -end - -function setupClip(thisA, thisB) - - iFrameStart = thisA - iFrameEnd = thisB - - discSkipToFrame(thisA) - -end - -function monoAudio (thisChannel) - - if thisChannel == CHANNEL_LEFT then - - discAudio (2, false) - discAudio (1, true) - - elseif thisChannel == CHANNEL_RIGHT then - - discAudio (1, false) - discAudio (2, true) - - end - -end - -function resetChannels() - - discAudio(1, true) - discAudio(2, true) - -end - -function muteAudio() - - discAudio(1, false) - discAudio(2, false) - -end - -function setFontColor(thisColor) - - if thisColor == RED then - - colorForeground(255, 0, 0) - - elseif thisColor == BLUE then - - colorForeground(0, 0, 255) - - elseif thisColor == YELLOW then - - colorForeground(255, 255, 0) - - elseif thisColor == GREEN then - - colorForeground(0, 255, 0) - - elseif thisColor == ORANGE then - - colorForeground(255, 150, 0) - - elseif thisColor == WHITE then - - colorForeground(255, 255, 255) - - elseif thisColor == GREY or thisColor == GRAY then - - colorForeground(128, 128, 128) - - elseif thisColor == LIGHTBLUE then - - colorForeground(30, 160, 250) - - elseif thisColor == BLACK then - - colorForeground(0,0,0) - - elseif thisColor == PINK then - - colorForeground(252,0,148) - - end - -end - -function textPrint(thisMsg, thisx, thisy, thisFont, thisColor, thisShadow) - - fontSelect(thisFont) - setFontColor(thisColor) - fontPrint(thisx,thisy,thisMsg) - -end - -function getMiddle(thisPhrase) - - local x = 0 - local y = 0 - local sprite = fontToSprite(thisPhrase) - - x = OVLW/2 - spriteGetWidth(sprite) * 0.5 - y = OVLH/2 - spriteGetHeight(sprite) * 0.5 - - spriteUnload(sprite) - - return x - -end +--[[ + +PROGRAM NAME: LUA SINGE +VERSION: 1.1 +AUTHOR: KARIS (2020) + +This file is part of LUA SINGE. + + LUA SINGE 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. + + LUA SINGE 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. + + Thanks to Scott Duensing, RDG. + +]]-- + +iSecs = 0 +iLimit = 0 +lastSeconds = 0 +thisSeconds = 0 + +sprText = nil +sprShadow = nil +sLastText = nil +iLastColor = -1 +iLastShadow = -1 + +tSecs = 0 +tLimit = 0 +tlastSeconds = 0 +tthisSeconds = 0 +bTommy = false + +bGunMute = false +iMuteFrames = 0 +MUTE_DELAY = 35 + +heartbeat = false +blinkSecs = 0 +lastBlinkSecs = 0 + +iRevFrames = 0 +REV_DELAY = 10 +bReversePointer = false +revsetx = 0; revsety = 0 + +iFrameStart = 0; iFrameEnd = 0 + +CHANNEL_LEFT = 1 +CHANNEL_RIGHT = 2 +ALL_CHANNELS = 3 +bMuteAttract = false + +RED = 0 +BLUE = 1 +YELLOW = 2 +GREEN = 3 +ORANGE = 4 +WHITE = 5 +GREY = 6 +GRAY = 6 +PINK = 7 +LIGHTBLUE = 7 +BLACK = 8 + +function singeRandomize() + + math.randomseed(os.time()) -- random initialize + math.random(); math.random(); math.random() -- warming up + +end + +function blinkTimer(thisMS) + + -- Function blinks every second. + + blinkSecs = os.clock() + + if bPause then + + lastBlinkSecs = blinkSecs + + else + + if (blinkSecs - lastBlinkSecs > thisMS) then + heartbeat = not heartbeat + lastBlinkSecs = blinkSecs + + end + + end + +end + +function goTimer(thisMS) + + blinkSecs = os.clock() + + if bPause then + + lastBlinkSecs = blinkSecs + + else + + if (blinkSecs - lastBlinkSecs > thisMS) then + heartbeat = true + lastBlinkSecs = blinkSecs + + end + + end + +end + +function clockRnd() + + local j = 0 + local q = 0 + local w = 0 + local r = 0 + local b1 = true + + j = os.clock() + q, w = math.modf(j) + + s2 = tostring(w) + r = string.find(s2, ".", 1, true) + + if (r == nil) then + + s2 = tostring(q) + s2 = string.sub(s2,string.len(s2), string.len(s2)) + + else + + s2 = string.sub(s2, r + 1) + r = string.len(s2) + + if r == 0 then + + s2 = tostring(q) + s2 = string.sub(s2,string.len(s2), string.len(s2)) + + + elseif r == 2 then + + s2 = string.sub(s2, 2, 2) + + elseif r >= 3 then + + s2 = string.sub(s2, 3, 3) + + end + + end + + w = tonumber(s2) + return w + +end + +function timerOFF() + + iSecs = 0 + iLimit = 0 + +end + +function timerON(thisLong) + + iSecs = 0 + iLimit = thisLong + lastSeconds = os.clock() + +end + +function timerDue() + + thisSeconds = os.clock() + + if bPause then + + lastSeconds = thisSeconds + + else + + if (thisSeconds ~= lastSeconds) then + + iSecs = iSecs + thisSeconds - lastSeconds + lastSeconds = thisSeconds + + end + + if (iSecs >= iLimit) then + + timerOFF() + return true + + else + + return false + + end + + end + +end + + +function muteSound() + + iMuteFrames = 0 + bGunMute = true + +end + +function blinkRev() + + iRevFrames = 0 + bReversePointer = true + +end + +function setupClip(thisA, thisB) + + iFrameStart = thisA + iFrameEnd = thisB + + discSkipToFrame(thisA) + +end + +function monoAudio (thisChannel) + + if thisChannel == CHANNEL_LEFT then + + discAudio (2, false) + discAudio (1, true) + + elseif thisChannel == CHANNEL_RIGHT then + + discAudio (1, false) + discAudio (2, true) + + end + +end + +function resetChannels() + + discAudio(1, true) + discAudio(2, true) + +end + +function muteAudio() + + discAudio(1, false) + discAudio(2, false) + +end + +function setFontColor(thisColor) + + if thisColor == RED then + + colorForeground(255, 0, 0) + + elseif thisColor == BLUE then + + colorForeground(0, 0, 255) + + elseif thisColor == YELLOW then + + colorForeground(255, 255, 0) + + elseif thisColor == GREEN then + + colorForeground(0, 255, 0) + + elseif thisColor == ORANGE then + + colorForeground(255, 150, 0) + + elseif thisColor == WHITE then + + colorForeground(255, 255, 255) + + elseif thisColor == GREY or thisColor == GRAY then + + colorForeground(128, 128, 128) + + elseif thisColor == LIGHTBLUE then + + colorForeground(30, 160, 250) + + elseif thisColor == BLACK then + + colorForeground(0,0,0) + + elseif thisColor == PINK then + + colorForeground(252,0,148) + + end + +end + +function textPrint(thisMsg, thisx, thisy, thisFont, thisColor, thisShadow) + + fontSelect(thisFont) + setFontColor(thisColor) + fontPrint(thisx,thisy,thisMsg) + +end + +function getMiddle(thisPhrase) + + local sprite = fontToSprite(thisPhrase) + local x = OVLW/2 - spriteGetWidth(sprite) * 0.5 + + spriteUnload(sprite) + + return x + +end diff --git a/src/common.h b/src/common.h index a05f52a04..326fb7607 100644 --- a/src/common.h +++ b/src/common.h @@ -26,13 +26,8 @@ #include - - -#define byte unsigned char - -#define bool unsigned char -#define true 1 -#define false 0 +#include +#include #endif // COMMON_H diff --git a/src/embedded.h b/src/embedded.h index 766cfc846..c117aba10 100644 --- a/src/embedded.h +++ b/src/embedded.h @@ -37,7 +37,7 @@ #include "generated/Menu_singe.h" #include "generated/FreeSansBold_ttf.h" #include "generated/menuBackground_mkv.h" -//#include "generated/Manual_pdf.h" +#include "generated/Manual_pdf.h" // LuaSocket #include "generated/ftp_lua.h" diff --git a/src/frameFile.c b/src/frameFile.c index d7b29fa21..a0597df42 100644 --- a/src/frameFile.c +++ b/src/frameFile.c @@ -21,6 +21,9 @@ */ +#include +#include + #include "../thirdparty/uthash/src/uthash.h" #include "util.h" @@ -28,64 +31,107 @@ #include "frameFile.h" -extern bool _confShowCalculated; +#define REPORT_COLUMN_WIDTH 8 typedef struct FrameLineS { int32_t videoHandle; - int64_t lastFramePlayed; - int64_t frame; + int64_t frame; // First laserdisc frame number in this segment char *filename; } FrameLineT; - typedef struct FrameFileS { int32_t id; int32_t count; - char *videoPath; + int32_t currentIndex; // Segment currently playing + int64_t lastObservedFrame; // Frame of that segment at the last update FrameLineT *files; UT_hash_handle hh; } FrameFileT; -static FrameFileT *_frameFileHash = NULL; -static int32_t _nextId = 0; +static FrameFileT *_frameFileHash = NULL; +static int32_t _nextId = 0; -void _transferProperties(int32_t oldHandle, int32_t newHandle); +static FrameFileT *_getFrameFile(int32_t frameFileHandle, const char *caller); +static void _selectSegment(FrameFileT *f, int32_t index, int64_t frame, int32_t *videoHandle); +static void _showCalculated(const FrameFileT *f); -void _transferProperties(int32_t oldHandle, int32_t newHandle) { - int32_t l = 0; - int32_t r = 0; +static FrameFileT *_getFrameFile(int32_t frameFileHandle, const char *caller) { + FrameFileT *f = NULL; - if ((oldHandle < 0) || (newHandle < 0)) { - // Invalid handle somewhere - return; + HASH_FIND_INT(_frameFileHash, &frameFileHandle, f); + if (!f) { + utilDie("No framefile at index %d in %s.", frameFileHandle, caller); } - // Transfer previous video's properties to this one - videoGetVolume(oldHandle, &l, &r); - videoSetVolume(newHandle, l, r); - if (videoIsPlaying(oldHandle)) { - videoPlay(newHandle); - } else { - videoPause(newHandle); + return f; +} + + +// Make segment "index" the active video at "frame", carrying the previous video's state over. +static void _selectSegment(FrameFileT *f, int32_t index, int64_t frame, int32_t *videoHandle) { + int32_t oldHandle = *videoHandle; + int32_t newHandle = f->files[index].videoHandle; + int32_t left = 0; + int32_t right = 0; + int32_t track = 0; + + videoSeek(newHandle, frame); + if ((oldHandle >= 0) && (oldHandle != newHandle)) { + // Transfer previous video's properties to this one + videoGetVolume(oldHandle, &left, &right); + videoSetVolume(newHandle, left, right); + track = videoGetAudioTrack(oldHandle); + if ((track >= 0) && (track < videoGetAudioTracks(newHandle))) { + videoSetAudioTrack(newHandle, track); + } + if (videoIsPlaying(oldHandle)) { + videoPlay(newHandle); + } else { + videoPause(newHandle); + } + videoPause(oldHandle); } - videoPause(oldHandle); + f->currentIndex = index; + f->lastObservedFrame = frame; + *videoHandle = newHandle; +} + + +static void _showCalculated(const FrameFileT *f) { + int32_t x = 0; + int64_t count = 0; + int64_t next = 0; + + // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 + // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + utilSay("Existing Framefile:\n"); + utilSay(" Start Length End File"); + utilSay("-------- -------- -------- -------------------------------------------------"); + for (x = 0; x < f->count; x++) { + count = videoGetFrameCount(f->files[x].videoHandle); + utilSay("%*" PRId64 " %*" PRId64 " %*" PRId64 " %s", REPORT_COLUMN_WIDTH, f->files[x].frame, REPORT_COLUMN_WIDTH, count, REPORT_COLUMN_WIDTH, f->files[x].frame + count - 1, f->files[x].filename); + } + utilSay("\nIdeal Framefile:\n"); + utilSay(" Start File"); + utilSay("-------- ---------------------------------------------------------------------"); + for (x = 0; x < f->count; x++) { + utilSay("%*" PRId64 " %s", REPORT_COLUMN_WIDTH, next, f->files[x].filename); + next += videoGetFrameCount(f->files[x].videoHandle); + } + utilNewline(); } int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle) { + FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileGetFrame"); int32_t i = 0; - FrameFileT *f = NULL; - - // Get our framefile structure - HASH_FIND_INT(_frameFileHash, &frameFileHandle, f); - if (!f) utilDie("No framefile at index %d in frameFileSeek.", frameFileHandle); // Search through loaded video segments - for (i=0; icount; i++) { + for (i = 0; i < f->count; i++) { if (f->files[i].videoHandle == videoHandle) { // Frame is in this video return videoGetFrame(videoHandle) + f->files[i].frame; @@ -97,193 +143,114 @@ int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle) { } -int32_t frameFileInit(void) { - return 0; // Nothing to do -} - - -int32_t frameFileLoad(char *filename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer, bool showCalculated) { - int32_t result = 0; - int32_t count = 0; - int64_t frame = 0; - int64_t next = 0; - size_t bytes = 0; - char *audio = NULL; - char *data = NULL; - char *path = NULL; - char *offset = NULL; - char *frameLine = NULL; - char *space = NULL; - char *endptr = NULL; - char *temp = NULL; - FrameLineT *files = NULL; - FrameLineT *newFiles = NULL; - FrameFileT *frameFile = NULL; +int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated) { + int32_t count = 0; + int64_t frame = 0; + size_t bytes = 0; + size_t x = 0; + char *audio = NULL; + char *data = NULL; + char *path = NULL; + char *temp = NULL; + char *frameLine = NULL; + char *space = NULL; + char *endptr = NULL; + const char *name = NULL; + const char *offset = NULL; + FrameLineT *files = NULL; + FrameLineT *newFiles = NULL; + FrameFileT *frameFile = NULL; data = utilReadFile(filename, &bytes); - if (!data) utilDie("Unable to open framefile: %s", filename); + if (!data) { + utilDie("Unable to open framefile: %s", filename); + } // Get path where video files live path = utilReadLine(data, bytes, &offset); - if (!path) utilDie("Cannot read video path from framefile!"); + if (!path) { + utilDie("Cannot read video path from framefile!"); + } utilFixPathSeparators(&path, true); // If it's not an absolute path, pre-pend the path to the framefile if ((path[0] != utilGetPathSeparator()) && (path[1] != ':')) { - temp = path; - count = strlen(filename) - strlen(utilGetLastPathComponent(filename)); - path = malloc(sizeof(char) * (count + strlen(temp) + 1)); - memcpy(path, filename, count); - memcpy(&path[count], temp, strlen(temp)); - path[count + strlen(temp)] = 0; + temp = utilGetUpToLastPathComponent(filename); + free(path); + path = utilCreateString("%s%s", temp, path); free(temp); utilFixPathSeparators(&path, true); - count = 0; } // Read frame offsets and filenames - silently ignore bad lines while ((frameLine = utilReadLine(data, bytes, &offset)) != NULL) { - // Covert tabs to spaces - for (next=0; next<(int64_t)strlen(frameLine); next++) { - if (frameLine[next] == 9) frameLine[next] = 32; + // Convert tabs to spaces + for (x = 0; frameLine[x] != 0; x++) { + if (frameLine[x] == '\t') { + frameLine[x] = ' '; + } } - // Find first space in this file. - space = strstr(frameLine, " "); - if (space) { - // Write a zero in there to make it two fields. - *space = 0; - // Is the first part an integer? - endptr = NULL; - frame = strtol(frameLine, &endptr, 10); - *space = 32; - if (endptr == space) { - // Got an integer. Point at filename. - space++; - while (space[0] == 32) { - space++; - } - // Copy frame number and filename into array - newFiles = realloc(files, sizeof(FrameLineT) * (count + 1)); - if (!newFiles) utilDie("Unable to allocate new framefile entry!"); - files = newFiles; - files[count].lastFramePlayed = 0; - files[count].frame = frame; - files[count].filename = utilCreateString("%s%s", path, space); - // Is this an old m2v/ogg pair? - if (strncmp(utilGetFileExtension(files[count].filename), "m2v", 3) == 0) { - // Open split video and audio files - audio = strdup(files[count].filename); - audio[strlen(audio) - 3] = 'o'; - audio[strlen(audio) - 2] = 'g'; - audio[strlen(audio) - 1] = 'g'; - if (utilFileExists(audio)) { - files[count].videoHandle = videoLoadWithAudio(files[count].filename, audio, indexPath, stretchVideo, renderer); - } else { - files[count].videoHandle = videoLoadWithAudio(files[count].filename, NULL, indexPath, stretchVideo, renderer); - } + // Is the first field an integer followed by a space? + frame = strtoll(frameLine, &endptr, 10); + space = strchr(frameLine, ' '); + if ((space != NULL) && (endptr == space)) { + // Got an integer. Point at filename. + name = space; + while (*name == ' ') { + name++; + } + // Copy frame number and filename into array + newFiles = realloc(files, sizeof(FrameLineT) * (size_t)(count + 1)); + if (!newFiles) { + utilDie("Unable to allocate new framefile entry!"); + } + files = newFiles; + files[count].frame = frame; + files[count].filename = utilCreateString("%s%s", path, name); + // Is this an old m2v/ogg pair? + audio = NULL; + if (utilStricmp(utilGetFileExtension(files[count].filename), "m2v") == 0) { + audio = utilCreateString("%.*s.ogg", (int32_t)(strlen(files[count].filename) - strlen("m2v") - 1), files[count].filename); + if (!utilFileExists(audio)) { free(audio); audio = NULL; - } else { - // Open combined video/audio file - files[count].videoHandle = videoLoad(files[count].filename, indexPath, stretchVideo, renderer); } - count++; } + files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer); + free(audio); + count++; } free(frameLine); } free(data); + free(path); + + if (count == 0) { + utilDie("Framefile has no video entries: %s", filename); + } // Allocate new framefile - frameFile = malloc(sizeof(FrameFileT)); - frameFile->id = _nextId; - frameFile->count = count; - frameFile->videoPath = path; - frameFile->files = files; + frameFile = calloc(1, sizeof(FrameFileT)); + if (!frameFile) { + utilDie("Unable to allocate framefile."); + } + frameFile->id = _nextId++; + frameFile->count = count; + frameFile->currentIndex = -1; + frameFile->files = files; HASH_ADD_INT(_frameFileHash, id, frameFile); - result = _nextId++; - // Show debug output? if (showCalculated) { - // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 - // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 - utilSay("Existing Framefile:\n"); - utilSay(" Start Length End File"); - utilSay("-------- -------- -------- -------------------------------------------------"); - for (count=0; countcount; count++) { - frame = videoGetFrameCount(frameFile->files[count].videoHandle); - utilSay("%8ld %8ld %8ld %s", frameFile->files[count].frame, frame, frameFile->files[count].frame + frame, frameFile->files[count].filename); - } - next = 0; - utilSay("\nIdeal Framefile:\n"); - utilSay(" Start File"); - utilSay("-------- ---------------------------------------------------------------------"); - for (count=0; countcount; count++) { - frame = videoGetFrameCount(frameFile->files[count].videoHandle); - utilSay("%8ld %s", next, frameFile->files[count].filename); - next += frame + 1; - } - utilSay(""); + _showCalculated(frameFile); } - return result; + return frameFile->id; } -int32_t frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame) { - int32_t i = 0; - int32_t found = -1; - FrameFileT *f = NULL; - - // Get our framefile structure - HASH_FIND_INT(_frameFileHash, &frameFileHandle, f); - if (!f) utilDie("No framefile at index %d in frameFileSeek.", frameFileHandle); - - // Search through loaded video segments - - // Daphne-like framefile searching - found = 0; - for (i=0; icount; i++) { - if (seekFrame >= f->files[i].frame) { - found = i; - } - } - - /* - // Strict framefile searching - for (i=0; icount; i++) { - if ((seekFrame >= f->files[i].frame) && (seekFrame <= (f->files[i].frame + videoGetFrameCount(f->files[i].videoHandle)))) { - found = i; - break; - } - } - */ - - if (found >= 0) { - // Frame is in this video - *actualFrame = seekFrame - f->files[found].frame; - //utilSay("Frame %ld found in file %d - %s at %ld", seekFrame, found, f->files[found].filename, *actualFrame); - f->files[found].lastFramePlayed = *actualFrame; - videoSeek(f->files[found].videoHandle, *actualFrame); - // Is this a different video from the previous one? - if (*videoHandle != f->files[found].videoHandle) { - // Yes - _transferProperties(*videoHandle, f->files[found].videoHandle); - *videoHandle = f->files[found].videoHandle; - } - return 0; - } - - // Didn't find it - *videoHandle = -1; - *actualFrame = -1; - return 1; -} - - -int32_t frameFileQuit(void) { +void frameFileQuit(void) { FrameFileT *f = NULL; FrameFileT *t = NULL; @@ -291,63 +258,68 @@ int32_t frameFileQuit(void) { HASH_ITER(hh, _frameFileHash, f, t) { frameFileUnload(f->id); } - - return 0; } -int32_t frameFileUnload(int32_t frameFileHandle) { - int32_t i = 0; - FrameFileT *f = NULL; +// Daphne-like framefile searching: the segment is the last one starting at or before the frame. +bool frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame) { + FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileSeek"); + int32_t i = 0; + int32_t found = 0; + int64_t last = 0; - // Get our framefile structure - HASH_FIND_INT(_frameFileHash, &frameFileHandle, f); - if (!f) utilDie("No framefile at index %d in frameFileUnload.", frameFileHandle); + for (i = 0; i < f->count; i++) { + if (seekFrame >= f->files[i].frame) { + found = i; + } + } + + // Clamp inside the segment instead of letting the player wrap it. + *actualFrame = seekFrame - f->files[found].frame; + last = videoGetFrameCount(f->files[found].videoHandle) - 1; + if (*actualFrame < 0) { + *actualFrame = 0; + } + if (*actualFrame > last) { + *actualFrame = last; + } + _selectSegment(f, found, *actualFrame, videoHandle); + + return true; +} + + +void frameFileUnload(int32_t frameFileHandle) { + FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileUnload"); + int32_t i = 0; // Unload videos - for (i=0; icount; i++) { + for (i = 0; i < f->count; i++) { free(f->files[i].filename); videoUnload(f->files[i].videoHandle); } // Free memory free(f->files); - free(f->videoPath); // Remove from hash HASH_DEL(_frameFileHash, f); free(f); - - return 0; } -int32_t frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle) { - int32_t i = 0; +// Did the current video loop back to the start? If so, move to the next segment. +void frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle) { + FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileUpdate"); int64_t frame = 0; - FrameFileT *f = NULL; - // Get our framefile structure - HASH_FIND_INT(_frameFileHash, &frameFileHandle, f); - if (!f) utilDie("No framefile at index %d in frameFileUpdate.", frameFileHandle); - - // Did the current video loop? If so, move to next video. - for (i=0; icount; i++) { - if (f->files[i].videoHandle == *videoHandle) { - frame = videoGetFrame(*videoHandle); - if ((f->files[i].lastFramePlayed > frame) && (frame == 0)) { - // Switch video files - if (i == (f->count - 1)) { - i = 0; - } else { - i++; - } - _transferProperties(*videoHandle, f->files[i].videoHandle); - *videoHandle = f->files[i].videoHandle; - } - break; - } + if ((f->currentIndex < 0) || (f->files[f->currentIndex].videoHandle != *videoHandle)) { + return; + } + frame = videoGetFrame(*videoHandle); + if (frame < f->lastObservedFrame) { + _selectSegment(f, (f->currentIndex + 1) % f->count, 0, videoHandle); + } else { + f->lastObservedFrame = frame; } - - return 0; } diff --git a/src/frameFile.h b/src/frameFile.h index c5a6af32b..6e7145c3d 100644 --- a/src/frameFile.h +++ b/src/frameFile.h @@ -31,12 +31,11 @@ int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle); -int32_t frameFileInit(void); -int32_t frameFileLoad(char *filename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer, bool showCalculated); -int32_t frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame); -int32_t frameFileQuit(void); -int32_t frameFileUnload(int32_t frameFileHandle); -int32_t frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle); +int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated); +void frameFileQuit(void); +bool frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame); +void frameFileUnload(int32_t frameFileHandle); +void frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle); #endif // FRAMEFILE_H diff --git a/src/main.c b/src/main.c index 1b07ab44c..fa927d362 100644 --- a/src/main.c +++ b/src/main.c @@ -21,16 +21,18 @@ */ -// -c -x 720 -y 480 -d data/maddog_dvd -v maddog_dvd/frame_maddog_dvd.txt maddog_dvd/maddog_dvd.singe -// -c -x 640 -y 480 -v ActionMax/frame_SonicFury.txt ActionMax/SonicFury.singe -// -x 640 -y 480 -d data/ActionMax ActionMax/BlueThunder.singe -// -d data/ActionMax -v ActionMax/BlueThunder.mp4 test.singe - - #include -#include +#include +#include +#include +#include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif #include "include/archive.h" #include "include/archive_entry.h" @@ -48,10 +50,30 @@ #include "frameFile.h" #include "videoPlayer.h" #include "singe.h" -#include "generated/extensions.h" +#include "../thirdparty/ffmpeg/libavformat/avformat.h" #include "embedded.h" +#define SUPPORT_DIR "Singe" +#define MENU_OPTIONS "-k -w -d data -v" +#define PRIMARY_DISPLAY 0 +#define MIXER_FREQUENCY 44100 +#define MIXER_CHANNELS 2 +#define MIXER_CHUNK_SAMPLES 4096 +#define MIXER_MIX_CHANNELS 16 +#define MIXER_FORMATS (MIX_INIT_FLAC | MIX_INIT_MID | MIX_INIT_MOD | MIX_INIT_MP3 | MIX_INIT_OGG | MIX_INIT_OPUS | MIX_INIT_WAVPACK) +#define IMAGE_FORMATS (IMG_INIT_JPG | IMG_INIT_PNG | IMG_INIT_WEBP) +#define ARCHIVE_BLOCK_SIZE 10240 +#define USAGE_OPTION_WIDTH 27 + + +typedef enum PackageTypeE { + PACKAGE_GAME = 0, + PACKAGE_TOOL, + PACKAGE_PATCH, + PACKAGE_COUNT +} PackageTypeE; + typedef struct RatioS { int32_t aspectNum; int32_t aspectDom; @@ -72,10 +94,61 @@ typedef struct QueueS { struct QueueS *next; } QueueT; +typedef struct OptionS { + int32_t code; + const char *name; + enum ap_Has_arg hasArgument; + const char *value; // Placeholder shown in the usage text, NULL when the option takes none. + const char *help; + bool hidden; // Parsed but not listed in the usage text. +} OptionT; + +typedef struct EmbeddedFileS { + const char *name; + const uint8_t *data; + size_t length; +} EmbeddedFileT; + static QueueT *_scriptQueue = NULL; -static ModeT _modes[] = { +static const char *const _packageTypes[PACKAGE_COUNT] = { "Game", "Tool", "Patch" }; + +static const char *const _badFilenames[] = { "controls.dat", "Framework.singe", NULL }; + +static const char *const _badExtensions[] = { "exe", "sh", "bat", "cmd", "index", NULL }; + +// Single source of truth for the command line: feeds both the parser and the usage text. +// The overscan and Sinden options work but are hidden until the Sinden border scales mouse input. +static const OptionT _options[] = { + { 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false }, + { 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", true }, + { 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false }, + { 'd', "datadir", ap_yes, "PATHNAME", "alternate location for written files", false }, + { 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false }, + { 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false }, + { 'g', "sindengun", ap_yes, "'PARAMS'", "enable Sinden Light Gun support", true }, + { 'h', "help", ap_no, NULL, "this display", false }, + { 'k', "nologos", ap_no, NULL, "kill the splash screens", false }, + { 'l', "volume_vldp", ap_yes, "PERCENT", "specify laserdisc volume in percent", false }, + { 'm', "nomouse", ap_no, NULL, "disable mouse", false }, + { 'n', "nocrosshair", ap_no, NULL, "request game not display gun crosshairs", false }, + { 'o', "audio", ap_yes, "TRACK", "select default track for audio output", false }, + { 'p', "program", ap_no, NULL, "trace Singe execution to screen and file", false }, + { 's', "nosound", ap_no, NULL, "mutes all sound", false }, + { 't', "trace", ap_no, NULL, "trace script execution to screen and file", false }, + { 'u', "stretch", ap_no, NULL, "use ugly stretched video", false }, + { 'v', "framefile", ap_yes, "FILENAME", "use an alternate video file", false }, + { 'w', "fullscreen_window", ap_no, NULL, "run in windowed full screen mode", false }, + { 'x', "xresolution", ap_yes, "VALUE", "specify horizontal resolution", false }, + { 'y', "yresolution", ap_yes, "VALUE", "specify vertical resolution", false }, + { 'z', "noconsole", ap_no, NULL, "zero console output", false } +}; + +#define OPTION_COUNT (sizeof(_options) / sizeof(_options[0])) + +// Sorted ascending within each ratio; the resolution search relies on that. +static const ModeT _modes[] = { { { 4, 3 }, { 640, 480 } }, { { 4, 3 }, { 800, 600 } }, { { 4, 3 }, { 960, 720 } }, @@ -105,98 +178,386 @@ static ModeT _modes[] = { }; -ConfigT *createConf(char *exeName, int argc, char *argv[]); -bool extractFile(char *filename, unsigned char *data, int32_t length); -void launcher(char *exeName, ConfigT *conf); -void showHeader(void); -void showUsage(char *name, char *message); -void unpackData(char *name); -void unpackGames(void); +static char *_cloneString(const char *string); +static bool _extractArchive(const char *filename); +static bool _extractFile(const char *filename, const uint8_t *data, size_t length); +static char *_findVideoFile(const char *baseName); +static void _launcher(const char *exeName, ConfigT *conf); +static void _mainTrace(const ConfigT *conf, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +static bool _modeMatchesRatio(int32_t index, int32_t ratioIndex); +static struct archive *_openArchive(const char *filename); +static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]); +static bool _parseInteger(const char *text, int32_t *value); +static void _resolveFiles(const char *exeName, ConfigT *conf); +static void _showHeader(void); +static void _showUsage(const char *name, const char *message) __attribute__((noreturn)); +static void _startSDL(void); +static void _stopSDL(void); +static void _unpackData(const char *name); +static void _unpackGames(void); +static bool _validateArchive(const char *filename, PackageTypeE type); -#define mainTrace(...) if (conf->programTracing) utilTrace(__VA_ARGS__) - - -ConfigT *cloneConf(ConfigT *conf) { - int32_t x; - ConfigT *c; - - c = (ConfigT *)calloc(1, sizeof(ConfigT)); - - // Deep copy conf manually to avoid optimizer issues - if (conf->scriptFile) c->scriptFile = strdup(conf->scriptFile); - if (conf->videoFile) c->videoFile = strdup(conf->videoFile); - if (conf->dataDirBase) c->dataDirBase = strdup(conf->dataDirBase); - if (conf->dataDir) c->dataDir = strdup(conf->dataDir); - c->resolutionWasCalculated = conf->resolutionWasCalculated; - c->isFrameFile = conf->isFrameFile; - c->stretchVideo = conf->stretchVideo; - c->noMouse = conf->noMouse; - c->noCrosshair = conf->noCrosshair; - c->noSound = conf->noSound; - c->fullScreen = conf->fullScreen; - c->fullScreenWindow = conf->fullScreenWindow; - c->showCalculated = conf->showCalculated; - c->noConsole = conf->noConsole; - c->noLogos = conf->noLogos; - c->programTracing = conf->programTracing; - c->scriptTracing = conf->scriptTracing; - c->bestRatioIndex = conf->bestRatioIndex; - c->volumeVldp = conf->volumeVldp; - c->volumeNonVldp = conf->volumeNonVldp; - c->scaleFactor = conf->scaleFactor; - c->xResolution = conf->xResolution; - c->yResolution = conf->yResolution; - c->sindenArgc = conf->sindenArgc; - for (x=0; xsindenArgv[x] = conf->sindenArgv[x]; +static char *_cloneString(const char *string) { + if (string == NULL) { + return NULL; } - return c; + return strdup(string); } -ConfigT *createConf(char *exeName, int argc, char *argv[]) { - int32_t x = 0; - int32_t argCount = 0; - int32_t argIndex = 0; - int32_t code = 0; - int32_t aspectNum = -1; - int32_t aspectDom = -1; - char *aspectString = NULL; - char *sindenString = NULL; - char *temp = NULL; - const char *arg = NULL; - const char **cargv = (const char **)argv; - ConfigT *conf = NULL; - struct Arg_parser parser; - static struct ap_Option options[] = { - { 'a', "aspect", ap_yes }, -// { 'b', "scalefactor", ap_yes }, - { 'c', "showcalculated", ap_no }, - { 'd', "datadir", ap_yes }, - { 'e', "volume_nonlvdp", ap_yes }, - { 'f', "fullscreen", ap_no }, -// { 'g', "sindengun", ap_yes }, - { 'h', "help", ap_no }, - { 'k', "nologos", ap_no }, - { 'l', "volume_vldp", ap_yes }, - { 'm', "nomouse", ap_no }, - { 'n', "nocrosshair", ap_no }, - { 'o', "audio", ap_yes }, - { 'p', "program", ap_no }, - { 's', "nosound", ap_no }, - { 't', "trace", ap_no }, - { 'u', "stretch", ap_no }, - { 'v', "framefile", ap_yes }, - { 'w', "fullscreenwindow", ap_no }, - { 'x', "xresolution", ap_yes }, - { 'y', "yresolution", ap_yes }, - { 'z', "noconsole", ap_no }, - { 0, 0, ap_no } - }; +// https://github.com/libarchive/libarchive/wiki/Examples#user-content-A_Complete_Extractor +static bool _extractArchive(const char *filename) { + struct archive *a = NULL; + struct archive *ext = NULL; + struct archive_entry *entry = NULL; + const void *buff = NULL; + size_t size = 0; + la_int64_t offset = 0; + int32_t flags = 0; + int32_t r = 0; + bool ok = true; - if (!ap_init(&parser, argc, cargv, options, 0)) { + // Never let an archive write outside the current directory. + flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS; + flags |= ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS | ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS; + + a = _openArchive(filename); + if (a == NULL) { + return false; + } + ext = archive_write_disk_new(); + archive_write_disk_set_options(ext, flags); + archive_write_disk_set_standard_lookup(ext); + + while (ok) { + r = archive_read_next_header(a, &entry); + if (r == ARCHIVE_EOF) { + break; + } + if (r < ARCHIVE_OK) { + utilSay("%s", archive_error_string(a)); + } + if (r < ARCHIVE_WARN) { + ok = false; + break; + } + r = archive_write_header(ext, entry); + if (r < ARCHIVE_OK) { + utilSay("%s", archive_error_string(ext)); + } else { + if (archive_entry_size(entry) > 0) { + for (;;) { + r = archive_read_data_block(a, &buff, &size, &offset); + if (r == ARCHIVE_EOF) { + r = ARCHIVE_OK; + break; + } + if (r < ARCHIVE_OK) { + utilSay("%s", archive_error_string(a)); + break; + } + r = archive_write_data_block(ext, buff, size, offset); + if (r < ARCHIVE_OK) { + utilSay("%s", archive_error_string(ext)); + break; + } + } + if (r < ARCHIVE_WARN) { + ok = false; + break; + } + } + } + r = archive_write_finish_entry(ext); + if (r < ARCHIVE_OK) { + utilSay("%s", archive_error_string(ext)); + } + if (r < ARCHIVE_WARN) { + ok = false; + break; + } + } + archive_read_close(a); + archive_read_free(a); + archive_write_close(ext); + archive_write_free(ext); + + return ok; +} + + +static bool _extractFile(const char *filename, const uint8_t *data, size_t length) { + FILE *out = NULL; + bool written = false; + + if (utilFileExists(filename)) { + return false; + } + + _showHeader(); + out = fopen(filename, "wb"); + if (!out) { + utilDie("Unable to create %s", filename); + } + written = (fwrite(data, 1, length, out) == length); + fclose(out); + if (!written) { + // Never leave a truncated file behind or it will not be recreated. + unlink(filename); + utilDie("Unable to write %s", filename); + } + utilSay(">>> Created File: %s", filename); + + return true; +} + + +// Tries every extension libavformat can demux - lower case only, Windows users! +static char *_findVideoFile(const char *baseName) { + const AVInputFormat *format = NULL; + void *opaque = NULL; + char *extensions = NULL; + char *extension = NULL; + char *comma = NULL; + char *candidate = NULL; + + while ((format = av_demuxer_iterate(&opaque)) != NULL) { + if (format->extensions == NULL) { + continue; + } + // Extensions are a comma separated list. + extensions = strdup(format->extensions); + for (extension = extensions; extension != NULL; extension = comma) { + comma = strchr(extension, ','); + if (comma != NULL) { + *comma = 0; + comma++; + } + candidate = utilCreateString("%s.%s", baseName, extension); + if (utilFileExists(candidate)) { + free(extensions); + return candidate; + } + free(candidate); + } + free(extensions); + } + + return NULL; +} + + +static void _launcher(const char *exeName, ConfigT *conf) { + int32_t x = 0; + int32_t bestResIndex = -1; + float thisRatio = 0.0f; + float bestRatio = HUGE_VALF; + uint32_t flags = 0; + SDL_Window *window = NULL; + SDL_Renderer *renderer = NULL; + SDL_Surface *icon = NULL; + SDL_DisplayMode mode; + + // Get current screen resolution + if (SDL_GetCurrentDisplayMode(PRIMARY_DISPLAY, &mode) < 0) { + utilDie("%s", SDL_GetError()); + } + _mainTrace(conf, "Display is %dx%d", mode.w, mode.h); + + // Determine resolution if not specified + if ((conf->xResolution <= 0) || (conf->yResolution <= 0)) { + _mainTrace(conf, "Determining resolution settings"); + if (conf->bestRatioIndex < 0) { + // Find our current aspect ratio + for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { + thisRatio = fabsf(((float)_modes[x].ratio.aspectNum / (float)_modes[x].ratio.aspectDom) - ((float)mode.w / (float)mode.h)); + if (thisRatio < bestRatio) { + bestRatio = thisRatio; + conf->bestRatioIndex = x; + } + } + } + if (conf->bestRatioIndex < 0) { + _showUsage(exeName, "Unknown aspect ratio."); + } + _mainTrace(conf, "Aspect ratio is %d:%d", _modes[conf->bestRatioIndex].ratio.aspectNum, _modes[conf->bestRatioIndex].ratio.aspectDom); + // Were both resolutions not specified? + if ((conf->xResolution <= 0) && (conf->yResolution <= 0)) { + // Are we full screen? + if (conf->fullScreen || conf->fullScreenWindow) { + // Use desktop resolution + conf->xResolution = mode.w; + conf->yResolution = mode.h; + } else { + // Find largest window that will fit on the screen but not fill it + for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { + if (_modeMatchesRatio(x, conf->bestRatioIndex) && (_modes[x].resolution.width < mode.w) && (_modes[x].resolution.height < mode.h)) { + bestResIndex = x; + } + } + if (bestResIndex < 0) { + _showUsage(exeName, "No window size fits this display. Specify a resolution or use full screen."); + } + conf->xResolution = _modes[bestResIndex].resolution.width; + conf->yResolution = _modes[bestResIndex].resolution.height; + } + } else { + // Find unprovided width/height using provided value + for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { + if (_modeMatchesRatio(x, conf->bestRatioIndex)) { + if ((conf->xResolution > 0) && (_modes[x].resolution.width == conf->xResolution)) { + conf->yResolution = _modes[x].resolution.height; + break; + } + if ((conf->yResolution > 0) && (_modes[x].resolution.height == conf->yResolution)) { + conf->xResolution = _modes[x].resolution.width; + break; + } + } + } + } + } + _mainTrace(conf, "Resolution is %dx%d", conf->xResolution, conf->yResolution); + // Did we end up with a valid resolution? + if (conf->xResolution <= 0) { + _showUsage(exeName, "Unable to determine X resolution. (Is the Y value sane?)"); + } + if (conf->yResolution <= 0) { + _showUsage(exeName, "Unable to determine Y resolution. (Is the X value sane?)"); + } + if ((conf->xResolution > mode.w) || (conf->yResolution > mode.h)) { + _showUsage(exeName, "Specified resolution is larger than the display."); + } + + // Create Window + _mainTrace(conf, "Creating window"); + window = SDL_CreateWindow("SINGE", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, conf->xResolution, conf->yResolution, 0); + if (window == NULL) { + utilDie("%s", SDL_GetError()); + } + + // Window Icon + _mainTrace(conf, "Setting icon"); + icon = IMG_Load_RW(SDL_RWFromConstMem(icon_png, (int32_t)icon_png_len), 1); + if (icon == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetWindowIcon(window, icon); + SDL_FreeSurface(icon); + icon = NULL; + + // Do we want full screen of some kind? + if (conf->fullScreen || conf->fullScreenWindow) { + _mainTrace(conf, "Going fullscreen"); + flags = conf->fullScreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP; + SDL_SetWindowFullscreen(window, flags); + } + + // Create a renderer. SDL prefers accelerated drivers but can fall back to software. + _mainTrace(conf, "Creating renderer"); + renderer = SDL_CreateRenderer(window, -1, 0); + if (renderer == NULL) { + utilDie("%s", SDL_GetError()); + } + + // Clear screen with black + SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(renderer); + + // Create audio mixer device + _mainTrace(conf, "Configuring mixer"); + if (Mix_OpenAudio(MIXER_FREQUENCY, MIX_DEFAULT_FORMAT, MIXER_CHANNELS, MIXER_CHUNK_SAMPLES) != 0) { + utilDie("%s", Mix_GetError()); + } + Mix_AllocateChannels(MIXER_MIX_CHANNELS); + + // Start our video playback system + _mainTrace(conf, "Initializing laserdisc video"); + videoInit(MIXER_CHUNK_SAMPLES); + + // Finish our setup + _mainTrace(conf, "Disabling screen saver"); + SDL_DisableScreenSaver(); + + // Run Singe! + _mainTrace(conf, "Starting Singe"); + singe(window, renderer, conf); + + // Shutdown - framefiles own video handles, so they go first. + _mainTrace(conf, "Shutting down laserdisc framefile handler"); + frameFileQuit(); + _mainTrace(conf, "Shutting down laserdisc video"); + videoQuit(); + _mainTrace(conf, "Stopping mixer"); + Mix_CloseAudio(); + _mainTrace(conf, "Destroying renderer"); + SDL_DestroyRenderer(renderer); + _mainTrace(conf, "Destroying window"); + SDL_DestroyWindow(window); + _mainTrace(conf, "Re-enabling screen saver"); + SDL_EnableScreenSaver(); +} + + +static void _mainTrace(const ConfigT *conf, const char *fmt, ...) { + va_list args; + + if (conf->programTracing) { + va_start(args, fmt); + utilTraceVArgs(fmt, args); + va_end(args); + } +} + + +static bool _modeMatchesRatio(int32_t index, int32_t ratioIndex) { + return (_modes[index].ratio.aspectNum == _modes[ratioIndex].ratio.aspectNum) && (_modes[index].ratio.aspectDom == _modes[ratioIndex].ratio.aspectDom); +} + + +static struct archive *_openArchive(const char *filename) { + struct archive *a = archive_read_new(); + + archive_read_support_filter_all(a); + archive_read_support_format_all(a); + if (archive_read_open_filename(a, filename, ARCHIVE_BLOCK_SIZE) != ARCHIVE_OK) { + utilSay("!!! Cannot read %s: %s", filename, archive_error_string(a)); + archive_read_free(a); + return NULL; + } + + return a; +} + + +static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) { + int32_t x = 0; + int32_t argIndex = 0; + int32_t code = 0; + int32_t aspectNum = -1; + int32_t aspectDom = -1; + int32_t *target = NULL; + char *aspectString = NULL; + char *sindenString = NULL; + char *temp = NULL; + const char *arg = NULL; + ConfigT *conf = NULL; + struct Arg_parser parser; + struct ap_Option options[OPTION_COUNT + 1]; + + // Build the parser table from our option list. + for (x = 0; x < (int32_t)OPTION_COUNT; x++) { + options[x].code = _options[x].code; + options[x].long_name = _options[x].name; + options[x].has_arg = _options[x].hasArgument; + } + options[OPTION_COUNT].code = 0; + options[OPTION_COUNT].long_name = NULL; + options[OPTION_COUNT].has_arg = ap_no; + + if (!ap_init(&parser, argc, (const char **)argv, options, 0)) { utilDie("Out of memory parsing arguments."); } if (ap_error(&parser)) { @@ -205,36 +566,41 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // Default configuration values conf = (ConfigT *)calloc(1, sizeof(ConfigT)); - if (!conf) utilDie("Out of memory creating config."); + if (!conf) { + utilDie("Out of memory creating config."); + } conf->bestRatioIndex = -1; - conf->volumeVldp = 100; - conf->volumeNonVldp = 100; - conf->scaleFactor = 100; - conf->resolutionWasCalculated = true; + conf->volumeVldp = VOLUME_MAX; + conf->volumeNonVldp = VOLUME_MAX; + conf->scaleFactor = SCALE_FACTOR_MAX; + conf->resolutionWasCalculated = true; // Parse command line - argCount = ap_arguments(&parser); - for (argIndex=0; argIndex < ap_arguments(&parser); ++argIndex) { + for (argIndex = 0; argIndex < ap_arguments(&parser); argIndex++) { + code = ap_code(&parser, argIndex); + arg = ap_argument(&parser, argIndex); + target = NULL; - code = ap_code(&parser, argIndex); - if (!code) break; - arg = ap_argument(&parser, argIndex); - - // Handle options switch (code) { + // Non-option: the script file. + case 0: + if (conf->scriptFile) { + _showUsage(exeName, "Only one script file may be specified."); + } + conf->scriptFile = strdup(arg); + break; + // Aspect case 'a': - if (aspectString) free(aspectString); - aspectString = strdup(arg); + free(aspectString); + aspectString = strdup(arg); conf->resolutionWasCalculated = false; - argCount++; break; // Overscan Zoom case 'b': - conf->scaleFactor = atoi(arg); - argCount++; + target = &conf->scaleFactor; break; // Show Calculated Frame File Values @@ -244,15 +610,13 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // Data Dir case 'd': - if (conf->dataDir) free(conf->dataDir); + free(conf->dataDir); conf->dataDir = strdup(arg); - argCount++; break; // Effects Volume case 'e': - conf->volumeNonVldp = atoi(arg); - argCount++; + target = &conf->volumeNonVldp; break; // Full Screen @@ -262,14 +626,13 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // Sinden Light Gun case 'g': - if (sindenString) free(sindenString); + free(sindenString); sindenString = strdup(arg); - argCount++; break; // Help case 'h': - showUsage(exeName, NULL); + _showUsage(exeName, NULL); break; // No Logos @@ -279,8 +642,7 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // Video Volume case 'l': - conf->volumeVldp = atoi(arg); - argCount++; + target = &conf->volumeVldp; break; // No Mouse @@ -295,8 +657,7 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // Audio Track Output case 'o': - conf->audioOutputTrack = atoi(arg); - argCount++; + target = &conf->audioOutputTrack; break; // Program Tracing @@ -321,9 +682,8 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // Video File case 'v': - if (conf->videoFile) free(conf->videoFile); + free(conf->videoFile); conf->videoFile = strdup(arg); - argCount++; break; // Full Screen Windowed @@ -333,59 +693,136 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { // X Resolution case 'x': - conf->xResolution = atoi(arg); + target = &conf->xResolution; conf->resolutionWasCalculated = false; - argCount++; break; // Y Resolution case 'y': - conf->yResolution = atoi(arg); + target = &conf->yResolution; conf->resolutionWasCalculated = false; - argCount++; break; - // No console output or splash screens + // No console output case 'z': conf->noConsole = true; + utilEnableConsole(false); break; default: - abort(); // Something bad happened - break; + utilDie("Unknown option code %d.", code); + } + + // Numeric options all validate the same way. + if ((target != NULL) && !_parseInteger(arg, target)) { + temp = utilCreateString("Bad value for option -%c: %s", code, arg); + _showUsage(exeName, temp); } } + ap_free(&parser); - // For that dumb OS - if (!conf->noConsole) utilRedirectConsole(); + if (!conf->scriptFile) { + _showUsage(exeName, "No script file specified."); + } + + // Do the full screen options make sense? + if (conf->fullScreen && conf->fullScreenWindow) { + _showUsage(exeName, "Full Screen or Full Screen Windowed. Pick one."); + } + + // Sane volume values? + if ((conf->volumeVldp < VOLUME_MIN) || (conf->volumeVldp > VOLUME_MAX)) { + _showUsage(exeName, "Laserdisc volume must be between 0 and 100 percent."); + } + if ((conf->volumeNonVldp < VOLUME_MIN) || (conf->volumeNonVldp > VOLUME_MAX)) { + _showUsage(exeName, "Effects volume must be between 0 and 100 percent."); + } + + // Sane scale factor? + if ((conf->scaleFactor < SCALE_FACTOR_MIN) || (conf->scaleFactor > SCALE_FACTOR_MAX)) { + _showUsage(exeName, "Display scale must be between 50 and 100 percent."); + } + + // Sinden light gun? + if (sindenString) { + if (conf->scaleFactor != SCALE_FACTOR_MAX) { + _showUsage(exeName, "Cannot use --sindengun and --scalefactor together."); + } + if (!parseSindenString(sindenString, conf)) { + _showUsage(exeName, "Bad argument count to --sindengun."); + } + free(sindenString); + } + + // Did they specify an aspect ratio? + if (aspectString) { + temp = strchr(aspectString, ':'); + if (temp != NULL) { + *temp = 0; + if (!_parseInteger(aspectString, &aspectNum) || !_parseInteger(temp + 1, &aspectDom)) { + aspectNum = -1; + } + } + if ((aspectNum > 0) && (aspectDom > 0)) { + // Do we understand what they asked for? + for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { + if ((_modes[x].ratio.aspectNum == aspectNum) && (_modes[x].ratio.aspectDom == aspectDom)) { + conf->bestRatioIndex = x; + break; + } + } + } + if (conf->bestRatioIndex < 0) { + _showUsage(exeName, "Unknown aspect ratio."); + } + free(aspectString); + } + + return conf; +} + + +static bool _parseInteger(const char *text, int32_t *value) { + char *end = NULL; + long parsed = 0; + + if ((text == NULL) || (*text == 0)) { + return false; + } + parsed = strtol(text, &end, 10); + if (*end != 0) { + return false; + } + *value = (int32_t)parsed; + + return true; +} + + +static void _resolveFiles(const char *exeName, ConfigT *conf) { + size_t length = 0; + const char *extension = NULL; + char *temp = NULL; - // Did we get a filename or path to open? - if ((argc - argCount) != 1) showUsage(exeName, "No script file specified."); - conf->scriptFile = strdup(argv[argCount]); - utilFixPathSeparators(&conf->scriptFile, false); // Exists? + utilFixPathSeparators(&conf->scriptFile, false); if (!utilFileExists(conf->scriptFile)) { // Missing. Is a path? + temp = NULL; if (utilPathExists(conf->scriptFile)) { - // See if the script exists in the path. + // See if the script named for the path exists inside the path. temp = utilCreateString("%s%c%s.singe", conf->scriptFile, utilGetPathSeparator(), utilGetLastPathComponent(conf->scriptFile)); - if (utilFileExists(temp)) { - // Found script named for path inside path. - free(conf->scriptFile); - conf->scriptFile = temp; + if (!utilFileExists(temp)) { + free(temp); temp = NULL; - } else { - // Not in the path either. - free(conf->scriptFile); - conf->scriptFile = NULL; } - } else { - // Not a path either. - free(conf->scriptFile); - conf->scriptFile = NULL; } + free(conf->scriptFile); + conf->scriptFile = temp; + } + if (!conf->scriptFile) { + _showUsage(exeName, "Unable to locate script."); } - if (!conf->scriptFile) showUsage(exeName, "Unable to locate script."); // Do we need to generate a video name? if (conf->videoFile) { @@ -395,24 +832,14 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { conf->videoFile = NULL; } } else { - x = (int32_t)(strlen(conf->scriptFile) - strlen(utilGetFileExtension(conf->scriptFile))) - 1; - if (x < 0) { - x = 0; + // Strip the script's extension (and its dot, when there is one). + extension = utilGetFileExtension(conf->scriptFile); + length = strlen(conf->scriptFile) - strlen(extension); + if (strlen(extension) > 0) { + length--; } - temp = strdup(conf->scriptFile); - temp[x] = 0; - // Check all known extensions - lower case only, Windows users! - x = 0; - while (ffmpegExtensions[x]) { - conf->videoFile = utilCreateString("%s.%s", temp, ffmpegExtensions[x]); - if (utilFileExists(conf->videoFile)) { - break; - } - free(conf->videoFile); - conf->videoFile = NULL; - x++; - } - free(temp); + temp = utilStrndup(conf->scriptFile, length); + conf->videoFile = _findVideoFile(temp); // If we still don't have one, try a framefile if (!conf->videoFile) { conf->videoFile = utilCreateString("%s.txt", temp); @@ -421,299 +848,30 @@ ConfigT *createConf(char *exeName, int argc, char *argv[]) { conf->videoFile = NULL; } } + free(temp); } - if (!conf->videoFile) showUsage(exeName, "Unable to locate video."); - // Is it a framefile? - if (strncmp(utilGetFileExtension(conf->videoFile), "txt", 3) == 0) { - conf->isFrameFile = true; + if (!conf->videoFile) { + _showUsage(exeName, "Unable to locate video."); } + conf->isFrameFile = isFrameFileName(conf->videoFile); - // They provided a data directory. Append the game name. if (conf->dataDir) { - conf->dataDirBase = strdup(conf->dataDir); + // They provided a data directory. Append the game name. + conf->dataDirBase = conf->dataDir; utilFixPathSeparators(&conf->dataDirBase, true); - free(conf->dataDir); - conf->dataDir = utilCreateString("%s%s", conf->dataDirBase, utilGetUpToLastPathComponent(conf->scriptFile)); - // Try to create data directory to ensure it exists. - utilMkDirP(conf->dataDir, 0777); - // Does it exist? - if (!utilPathExists(conf->dataDir)) { - free(conf->dataDir); - conf->dataDir = NULL; - } + conf->dataDir = createDataDir(conf->dataDirBase, conf->scriptFile); } else { // No data directory specified. Use the game folder. conf->dataDirBase = utilCreateString(".%c", utilGetPathSeparator()); - conf->dataDir = strdup(utilGetUpToLastPathComponent(conf->scriptFile)); + conf->dataDir = utilGetUpToLastPathComponent(conf->scriptFile); } - if (!conf->dataDir) showUsage(exeName, "Unable to locate data directory."); - utilFixPathSeparators(&conf->dataDir, true); - - // Do the full screen options make sense? - if (conf->fullScreen && conf->fullScreenWindow) showUsage(exeName, "Full Screen or Full Screen Windowed. Pick one."); - - // Sane volume values? - if ((conf->volumeVldp < 0) || (conf->volumeVldp > 100)) showUsage(exeName, "Laserdisc volume must be between 0 and 100 percent."); - if ((conf->volumeNonVldp < 0) || (conf->volumeNonVldp > 100)) showUsage(exeName, "Effects volume must be between 0 and 100 percent."); - - // Sane scale factor? - if ((conf->scaleFactor < 50) || (conf->scaleFactor > 100)) showUsage(exeName, "Display scale must be between 50 and 100 percent."); - - // Sinden light gun? - if (sindenString) { - if (conf->scaleFactor != 100) showUsage(exeName, "Cannot use --sindengun and --scalefactor together."); - if (!parseSindenString(&sindenString, conf)) showUsage(exeName, "Bad argument count to --sindengun."); + if (!conf->dataDir) { + _showUsage(exeName, "Unable to create data directory."); } - - // Did they specify an aspect ratio? - if (aspectString) { - aspectNum = atoi(aspectString); - temp = strstr(aspectString, ":"); - if (temp) { - temp++; - aspectDom = atoi(temp); - temp = NULL; - } - if ((aspectNum > 0) && (aspectDom > 0)) { - // Do we understand what they asked for? - x = 0; - while (_modes[x].ratio.aspectNum != 0) { - if ((_modes[x].ratio.aspectNum == aspectNum) && (_modes[x].ratio.aspectDom == aspectDom)) { - conf->bestRatioIndex = x; - break; - } - x++; - } - } - free(aspectString); - } - - ap_free(&parser); - - return conf; } -void destroyConf(ConfigT **confPointer) { - ConfigT *conf = *confPointer; - - if (conf->dataDir) free(conf->dataDir); - if (conf->dataDirBase) free(conf->dataDirBase); - if (conf->videoFile) free(conf->videoFile); - if (conf->scriptFile) free(conf->scriptFile); - - free(conf); - conf = NULL; -} - - -bool extractFile(char *filename, unsigned char *data, int32_t length) { - FILE *out; - - if (!utilFileExists(filename)) { - showHeader(); - out = fopen(filename, "wb"); - if (!out) utilDie("Unable to create %s", filename); - fwrite(data, length, 1, out); - fclose(out); - utilSay(">>> Created File: %s", filename); - return true; - } - - return false; -} - - -void launcher(char *exeName, ConfigT *conf) { - int32_t x = 0; - int32_t bestResIndex = -1; - float thisRatio = 0; - float bestRatio = 9999; - int32_t err = 0; - int32_t flags = 0; - char *temp = NULL; - SDL_Window *window = NULL; - SDL_Renderer *renderer = NULL; - SDL_Surface *icon = NULL; - SDL_DisplayMode mode; - - // Get current screen resolution - if (SDL_GetCurrentDisplayMode(0, &mode) < 0) utilDie("%s", SDL_GetError()); - mainTrace("Display is %dx%d", mode.w, mode.h); - - // Determine resolution if not specified - if ((conf->xResolution <= 0) || (conf->yResolution <= 0)) { - mainTrace("Determining resolution settings"); - if (conf->bestRatioIndex < 0) { - // Find our current aspect ratio - x = 0; - while (_modes[x].ratio.aspectNum != 0) { - thisRatio = fabsf(((float)_modes[x].ratio.aspectNum / (float)_modes[x].ratio.aspectDom) - ((float)mode.w / (float)mode.h)); - if (thisRatio < bestRatio) { - bestRatio = thisRatio; - conf->bestRatioIndex = x; - } - x++; - } - } - if (conf->bestRatioIndex < 0) showUsage(exeName, "Unknown aspect ratio."); - mainTrace("Aspect ratio is %d:%d", _modes[conf->bestRatioIndex].ratio.aspectNum, _modes[conf->bestRatioIndex].ratio.aspectDom); - x = 0; - // Were both resolutions not specified? - if ((conf->xResolution <= 0) && (conf->yResolution <= 0)) { - // Are we full screen? - if (conf->fullScreen || conf->fullScreenWindow) { - // Use desktop resolution - conf->xResolution = mode.w; - conf->yResolution = mode.h; - } else { - // Find largest window that will fit on the screen but not fill it - while (_modes[x].ratio.aspectNum != 0) { - if ((_modes[x].ratio.aspectNum == _modes[conf->bestRatioIndex].ratio.aspectNum) && (_modes[x].ratio.aspectDom == _modes[conf->bestRatioIndex].ratio.aspectDom)) { - if ((_modes[x].resolution.width < mode.w) && (_modes[x].resolution.height < mode.h)) { - bestResIndex = x; - } - } - x++; - } - conf->xResolution = _modes[bestResIndex].resolution.width; - conf->yResolution = _modes[bestResIndex].resolution.height; - } - } else { - // Find unprovided width/height using provided value - while (_modes[x].ratio.aspectNum != 0) { - // Is this the aspect ratio we're using? - if ((_modes[conf->bestRatioIndex].ratio.aspectNum == _modes[x].ratio.aspectNum) && (_modes[conf->bestRatioIndex].ratio.aspectDom == _modes[x].ratio.aspectDom)) { - // Do we have the width or height? - if (conf->xResolution > 0) { - // We have the width. Is this the matching entry? - if (_modes[x].resolution.width == conf->xResolution) { - bestResIndex = x; - conf->yResolution = _modes[x].resolution.height; - break; - } - } else { - // We have the height. Is this the matching entry? - if (_modes[x].resolution.height == conf->yResolution) { - bestResIndex = x; - conf->xResolution = _modes[x].resolution.width; - break; - } - } - } - x++; - } - } - } - mainTrace("Resolution is %dx%d", conf->xResolution, conf->yResolution); - // Did we end up with a valid resolution? - if (conf->xResolution <= 0) showUsage(exeName, "Unable to determine X resolution. (Is the Y value sane?)"); - if (conf->yResolution <= 0) showUsage(exeName, "Unable to determine Y resolution. (Is the X value sane?)"); - if ((conf->xResolution > mode.w) || (conf->yResolution > mode.h)) showUsage(exeName, "Specified resolution is larger than the display."); - - // Create Window - mainTrace("Creating window"); - window = SDL_CreateWindow("SINGE", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, conf->xResolution, conf->yResolution, 0 /* SDL_WINDOW_RESIZABLE */); - if (window == NULL) utilDie("%s", SDL_GetError()); - - // Window Icon - mainTrace("Setting icon"); - icon = IMG_LoadPNG_RW(SDL_RWFromMem(icon_png, icon_png_len)); - if (icon == NULL) utilDie("%s", SDL_GetError()); - SDL_SetWindowIcon(window, icon); - SDL_FreeSurface(icon); - icon = NULL; - - // Do we want full screen of some kind? - if (conf->fullScreen || conf->fullScreenWindow) { - mainTrace("Going fullscreen"); - flags = conf->fullScreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP; - SDL_SetWindowFullscreen(window, (Uint32)flags); - } - - // Create an accelerated renderer. - mainTrace("Creating renderer"); - renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); - if (renderer == NULL) utilDie("%s", SDL_GetError()); - - // Clear screen with black - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderClear(renderer); - - // Create audio mixer device - mainTrace("Configuring mixer"); - err = Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 44100 /* freq */ * 16 /* bits */ * 2 /* channels */ * 2 /* seconds */); - if (err != 0) utilDie("%s", Mix_GetError()); - Mix_AllocateChannels(16); - - // Start our video playback system - mainTrace("Initializing laserdisc framefile handler"); - if (frameFileInit()) utilDie("Unable to initialize framefile handler."); - mainTrace("Initializing laserdisc video"); - if (videoInit()) utilDie("Unable to initialize video player."); - - // Finish our setup - mainTrace("Disabling screen saver"); - SDL_DisableScreenSaver(); - - // Run Singe! - mainTrace("Starting Singe"); - singe(window, renderer, conf); - - // Shutdown - mainTrace("Shutting down laserdisc video"); - videoQuit(); - mainTrace("Shutting down laserdisc framefile handler"); - frameFileQuit(); - mainTrace("Stopping mixer"); - Mix_CloseAudio(); - mainTrace("Destroying renderer"); - SDL_DestroyRenderer(renderer); - mainTrace("Destroying window"); - SDL_DestroyWindow(window); - mainTrace("Re-enabling screen saver"); - SDL_EnableScreenSaver(); -} - - -bool parseSindenString(char **sindenStringPointer, ConfigT *conf) { - char *sindenString = *sindenStringPointer; - char *temp = NULL; - - // Was it wrapped in quotes? - if ((sindenString[0] == '"') || (sindenString[0] == '\'')) { - sindenString[0] = ' '; - } - // Ok, this thing can have a mess of different arguments: - // WW - Just the width of the white border - // WW WB - Width of white border and then black border - // RW GW BW WW - Custom color "white" border and width - // RW GW BW WW WB - Custom color "white" border and width then width of black border - // RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width - temp = strtok(sindenString, " "); - while (temp != NULL) { - conf->sindenArgv[conf->sindenArgc++] = atoi(temp); - temp = strtok(NULL, " "); - if ((temp != NULL) && (conf->sindenArgc > SINDEN_OPTION_COUNT)) return false; - } - // Did we get a sane number of arguments? - if ((conf->sindenArgc != SINDEN_WHITE) && (conf->sindenArgc != SINDEN_WHITE_BLACK) && (conf->sindenArgc != SINDEN_CUSTOM_WHITE) && (conf->sindenArgc != SINDEN_CUSTOM_WHITE_BLACK) && (conf->sindenArgc != SINDEN_CUSTOM_WHITE_CUSTOM_BLACK)) return false; - free(sindenString); - - return true; -} - - -void queueScript(ConfigT *conf) { - QueueT *q; - - q = (QueueT *)calloc(1, sizeof(QueueT)); - q->conf = cloneConf(conf); - LL_APPEND(_scriptQueue, q); -} - - -void showHeader(void) { +static void _showHeader(void) { static bool shown = false; if (!shown) { @@ -724,82 +882,74 @@ void showHeader(void) { utilSay("/ __|_ _| \\| |/ __| __| Somewhat Interactive Nostalgic Game Engine %s", VERSION_STRING); utilSay("\\__ \\| || .` | (_ | _| Copyright (c) 2006-%s Scott C. Duensing", COPYRIGHT_END_YEAR); utilSay("|___/___|_|\\_|\\___|___| https://KangarooPunch.com https://SingeEngine.com"); - utilSay(""); + utilNewline(); shown = true; } } -__attribute__((noreturn)) -void showUsage(char *name, char *message) { - int32_t result = 0; +static void _showUsage(const char *name, const char *message) { + int32_t x = 0; + char *longForm = NULL; - showHeader(); + _showHeader(); - // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 - // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 utilSay("Usage: %s [OPTIONS] scriptName{.singe}", utilGetLastPathComponent(name)); - utilSay(""); - utilSay(" -a, --aspect=N:D force aspect ratio"); -// utilSay(" -b, --scalefactor=PERCENT reduce screen size for overscan compensation"); - utilSay(" -c, --showcalculated show calculated framefile values for debugging"); - utilSay(" -d, --datadir=PATHNAME alternate location for written files"); - utilSay(" -e, --volume_nonvldp=PERCENT specify sound effects volume in percent"); - utilSay(" -f, --fullscreen run in full screen mode"); -// utilSay(" -g, --sindengun='PARAMS' enable Sinden Light Gun support"); - utilSay(" -h, --help this display"); - utilSay(" -k, --nologos kill the splash screens"); - utilSay(" -l, --volume_vldp=PERCENT specify laserdisc volume in percent"); - utilSay(" -m, --nomouse disable mouse"); - utilSay(" -n, --nocrosshair request game not display gun crosshairs"); - utilSay(" -o, --audio=TRACK select default track for audio output"); - utilSay(" -p, --program trace Singe execution to screen and file"); - utilSay(" -s, --nosound, --mutesound mutes all sound"); - utilSay(" -t, --trace trace script execution to screen and file"); - utilSay(" -u, --stretch use ugly stretched video"); - utilSay(" -v, --framefile=FILENAME use an alternate video file"); - utilSay(" -w, --fullscreen_window run in windowed full screen mode"); - utilSay(" -x, --xresolution=VALUE specify horizontal resolution"); - utilSay(" -y, --yresolution=VALUE specify vertical resolution"); - utilSay(" -z, --noconsole zero console output"); - utilSay(""); + utilNewline(); + for (x = 0; x < (int32_t)OPTION_COUNT; x++) { + if (_options[x].hidden) { + continue; + } + if (_options[x].value != NULL) { + longForm = utilCreateString("--%s=%s", _options[x].name, _options[x].value); + } else { + longForm = utilCreateString("--%s", _options[x].name); + } + utilSay(" -%c, %-*s%s", _options[x].code, USAGE_OPTION_WIDTH, longForm, _options[x].help); + free(longForm); + } + utilNewline(); if (message) { utilSay("Error: %s", message); - utilSay(""); - result = 1; + utilNewline(); } -#ifdef _WIN32 - if (utilGetConsoleEnabled()) getchar(); -#endif - exit(result); + if (utilGetConsoleEnabled()) { + utilWaitForKeyOnWindows(); + } + exit(message ? EXIT_FAILURE : EXIT_SUCCESS); } -void startSDL(void) { - int32_t err = 0; - int32_t flags = 0; +static void _startSDL(void) { + int32_t initialized = 0; // Init SDL - if (SDL_Init(SDL_INIT_EVERYTHING) != 0) utilDie("%s", SDL_GetError()); + if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { + utilDie("%s", SDL_GetError()); + } - // Init SDL_mixer - flags = MIX_INIT_FLAC | MIX_INIT_MID | MIX_INIT_MOD | MIX_INIT_MP3 | MIX_INIT_OGG | MIX_INIT_OPUS | MIX_INIT_WAVPACK; - err = Mix_Init(flags); - if (err != flags) utilDie("%s", Mix_GetError()); + // Init SDL_mixer - a missing decoder is a warning, not a fatal error. + initialized = Mix_Init(MIXER_FORMATS); + if (initialized != MIXER_FORMATS) { + utilSay("Warning: Some audio formats are unavailable: %s", Mix_GetError()); + } // Init SDL_image - flags = /* IMG_INIT_AVIF | */ IMG_INIT_JPG | /* IMG_INIT_JXL | */ IMG_INIT_PNG | /* IMG_INIT_TIF | */ IMG_INIT_WEBP; - err = IMG_Init(flags); - if (err != flags) utilDie("%s", IMG_GetError()); + initialized = IMG_Init(IMAGE_FORMATS); + if (initialized != IMAGE_FORMATS) { + utilDie("%s", IMG_GetError()); + } // Init SDL_ttf - if (TTF_Init() < 0) utilDie("%s", TTF_GetError()); + if (TTF_Init() < 0) { + utilDie("%s", TTF_GetError()); + } } -void stopSDL(void) { +static void _stopSDL(void) { TTF_Quit(); IMG_Quit(); Mix_Quit(); @@ -807,315 +957,357 @@ void stopSDL(void) { } -void unpackData(char *name) { - char *temp = NULL; - char *data = NULL; - bool created = false; +static void _unpackData(const char *name) { + const EmbeddedFileT files[] = { + { "Framework.singe", Framework_singe, Framework_singe_len }, + { "controls.cfg.example", controls_cfg, controls_cfg_len }, + { "Menu.singe", Menu_singe, Menu_singe_len }, + { "FreeSansBold.ttf", FreeSansBold_ttf, FreeSansBold_ttf_len }, + { "menuBackground.mkv", menuBackground_mkv, menuBackground_mkv_len }, + { "Manual.pdf", Manual_pdf, Manual_pdf_len } + }; + int32_t x = 0; + char *temp = NULL; + char *data = NULL; + bool created = false; // Extract any missing support files. We do this here so they're not generated if launched from a front end. - if (!utilPathExists("Singe")) utilMkDirP("Singe", 0777); + if (!utilMkDirP(SUPPORT_DIR, DIRECTORY_MODE)) { + utilDie("Unable to create %s directory.", SUPPORT_DIR); + } - // Singe/Framework.singe - temp = utilCreateString("Singe%cFramework.singe", utilGetPathSeparator()); - created |= extractFile(temp, Framework_singe, Framework_singe_len); - free(temp); - // Singe/controls.cfg.example - temp = utilCreateString("Singe%ccontrols.cfg.example", utilGetPathSeparator()); - created |= extractFile(temp, controls_cfg, controls_cfg_len); - free(temp); - // Singe/Manual.pdf - /* - temp = utilCreateString("Singe%cManual.pdf", utilGetPathSeparator()); - created |= extractFile(temp, Manual_pdf, Manual_pdf_len); - free(temp); - */ - // Singe/Menu.singe - temp = utilCreateString("Singe%cMenu.singe", utilGetPathSeparator()); - created |= extractFile(temp, Menu_singe, Menu_singe_len); - free(temp); - // Singe/FreeSansBold.ttf - temp = utilCreateString("Singe%cFreeSansBold.ttf", utilGetPathSeparator()); - created |= extractFile(temp, FreeSansBold_ttf, FreeSansBold_ttf_len); - free(temp); - // Singe/menuBackground.mkv - temp = utilCreateString("Singe%cmenuBackground.mkv", utilGetPathSeparator()); - created |= extractFile(temp, menuBackground_mkv, menuBackground_mkv_len); - free(temp); + for (x = 0; x < (int32_t)(sizeof(files) / sizeof(files[0])); x++) { + temp = utilCreateString("%s%c%s", SUPPORT_DIR, utilGetPathSeparator(), files[x].name); + created |= _extractFile(temp, files[x].data, files[x].length); + free(temp); + } // Script to start menu system if (utilGetPathSeparator() == '/') { // Unix-ish - temp = utilCreateString("Menu.sh"); - data = utilCreateString("#!/bin/bash\n\n./%s -k -w -d data -v Singe/menuBackground.mkv Singe/Menu.singe\n", utilGetLastPathComponent(name)); + temp = strdup("Menu.sh"); + data = utilCreateString("#!/bin/sh\n\ncd \"$(dirname \"$0\")\"\n./%s %s %s/menuBackground.mkv %s/Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, SUPPORT_DIR, SUPPORT_DIR); } else { // Winders - temp = utilCreateString("Menu.bat"); - data = utilCreateString("@start %s -k -w -d data -v Singe\\menuBackground.mkv Singe\\Menu.singe\n", utilGetLastPathComponent(name)); + temp = strdup("Menu.bat"); + data = utilCreateString("@start %s %s %s\\menuBackground.mkv %s\\Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, SUPPORT_DIR, SUPPORT_DIR); } - created |= extractFile(temp, (unsigned char *)data, strlen(data)); - utilChMod(temp, 0777); + created |= _extractFile(temp, (const uint8_t *)data, strlen(data)); + utilChMod(temp, DIRECTORY_MODE); free(data); - data = NULL; + free(temp); - temp = NULL; - - if (created) utilSay(""); + if (created) { + utilNewline(); + } } -void unpackGames(void) { - struct dirent *de; - DIR *dir = opendir("."); - struct archive *a; - struct archive *ext; - struct archive_entry *entry; - int flags; - int r; - int x; - int count = 0; - bool ok; - bool hasGamesDat; - const void *buff; - size_t size; - la_int64_t offset; - char *e; - char *gamesDat = NULL; - char *extension; - char *filename; - char *toplevel = NULL; - int packageType = -1; - char *types[] = { "Game", "Tool", "Patch", 0 }; - char *badFilenames[] = { "controls.dat", "Framework.singe", 0 }; - char *badExtensions[] = { "exe", "sh", "bat", "cmd", "index", 0 }; - struct stat fileStat; - bool isFile; +// Games cannot include forbidden extensions, Singe binaries, multiple folders, etc. +// Tools are like games but do not need a games.dat file. +// Patches can be pretty much anything except forbidden extensions. +static void _unpackGames(void) { + struct dirent *de = NULL; + DIR *dir = opendir("."); + struct stat fileStat; + int32_t x = 0; + int32_t count = 0; + PackageTypeE type = PACKAGE_COUNT; - // Games cannot include forbidden extensions, Singe binaries, multiple folders, etc. - // Tools are like games but do not need a games.dat file. - // Patches can be pretty much anything except forbidden extensions. - - if (dir == NULL) utilDie("Could not open the current directory."); + if (dir == NULL) { + utilDie("Could not open the current directory."); + } while ((de = readdir(dir)) != NULL) { - - isFile = false; - if ((strcmp(de->d_name, ".") != 0) && (strcmp(de->d_name, "..") != 0)) { - if (stat(de->d_name, &fileStat) == 0) { -#ifdef _WIN32 - if (S_ISREG(fileStat.st_mode)) { -#else - if (S_ISREG(fileStat.st_mode) || S_ISLNK(fileStat.st_mode)) { -#endif - isFile = true; - } + // Only regular files with a package extension are interesting. + if ((strcmp(de->d_name, ".") == 0) || (strcmp(de->d_name, "..") == 0)) { + continue; + } + if ((stat(de->d_name, &fileStat) != 0) || !S_ISREG(fileStat.st_mode)) { + continue; + } + type = PACKAGE_COUNT; + for (x = 0; x < PACKAGE_COUNT; x++) { + if (utilStricmp(utilGetFileExtension(de->d_name), _packageTypes[x]) == 0) { + type = (PackageTypeE)x; + break; } } + if (type == PACKAGE_COUNT) { + continue; + } - if (isFile) { - packageType = -1; - x = 0; - while (types[x] != NULL) { - if (utilStricmp(utilGetFileExtension(de->d_name), types[x]) == 0) { - packageType = x; - break; - } - x++; + _showHeader(); + count++; + if (_validateArchive(de->d_name, type)) { + utilSay(">>> Installing %s: %s", _packageTypes[type], de->d_name); + if (_extractArchive(de->d_name)) { + unlink(de->d_name); } - if (packageType >= 0) { - showHeader(); - count++; - ok = true; - hasGamesDat = false; + } + } - // Look through archive for things I've told people NOT to ship! - // https://github.com/libarchive/libarchive/wiki/Examples#user-content-List_contents_of_Archive_stored_in_File - a = archive_read_new(); - archive_read_support_filter_all(a); - archive_read_support_format_all(a); - r = archive_read_open_filename(a, de->d_name, 10240); - if (r != ARCHIVE_OK) { - utilSay("!!! Cannot read %s: %s", types[packageType], de->d_name); - ok = false; - } else { - while (archive_read_next_header(a, &entry) == ARCHIVE_OK && ok) { - e = (char *)archive_entry_pathname(entry); - filename = utilGetLastPathComponent(e); - extension = utilGetFileExtension(e); - - // If not a patch, do we have a top level folder name yet? - if ((utilStricmp(types[packageType], "Patch") != 0) && (toplevel == NULL)) { - // No. Is this a folder? - if (strstr(e, "/") == NULL) { - // No. BAD! No files in the root! - ok = false; - utilSay("!!! %s has files in root: %s", types[packageType], de->d_name); - } else { - // Remember this folder. - toplevel = strdup(e); - gamesDat = utilCreateString("%sgames.dat", toplevel); - } - } else { - // If this is not a patch, check for a top level. Is this entry inside it? - if ((utilStricmp(types[packageType], "Patch") != 0) && (!utilStartsWith(e, toplevel))) { - // No. BAD! Everything has to be in the top level. - ok = false; - if (strstr(e, "/") == NULL) { - utilSay("!!! %s has files in root: %s", types[packageType], de->d_name); - } else { - utilSay("!!! %s has multiple top level directories: %s", types[packageType], de->d_name); - } - } else { - // Is this a forbidden file? - x = 0; - while (badFilenames[x] != NULL && ok) { - if (utilStricmp(filename, badFilenames[x]) == 0) { - ok = false; - utilSay("!!! %s has %s: %s", types[packageType], badFilenames[x], de->d_name); - } - x++; - } - // Is this a forbidden extension? - x = 0; - while (badExtensions[x] != NULL && ok) { - if (utilStricmp(extension, badExtensions[x]) == 0) { - ok = false; - utilSay("!!! %s has %s file: %s", types[packageType], badExtensions[x], de->d_name); - } - x++; - } - // Not a patch, no extension, starts with 'singe' - could be unix binary. - if ((utilStricmp(types[packageType], "Patch") != 0) && (ok && strlen(extension) == 0 && utilStartsWith(filename, "singe"))) { - ok = false; - utilSay("!!! %s has singe file: %s", types[packageType], de->d_name); - } - // Is this games.dat? - if (ok && !hasGamesDat && utilStricmp(e, gamesDat) == 0) hasGamesDat = true; - } - } - //utilSay("%s [%s] %s", filename, extension, e); - archive_read_data_skip(a); - } - r = archive_read_free(a); - if (toplevel != NULL) { - free(toplevel); - toplevel = NULL; - } - if (gamesDat != NULL) { - free(gamesDat); - gamesDat = NULL; - } - - // If it's a game, did we get a games.dat? - if (((utilStricmp(types[packageType], "Game") == 0)) && (ok && !hasGamesDat)) { - ok = false; - utilSay("!!! %s has no games.dat: %s", types[packageType], de->d_name); - } - } - - // Unpack it! - if (ok) { - utilSay(">>> Installing %s: %s", types[packageType], de->d_name); - // https://github.com/libarchive/libarchive/wiki/Examples#user-content-A_Complete_Extractor - flags = ARCHIVE_EXTRACT_TIME; - flags |= ARCHIVE_EXTRACT_PERM; - flags |= ARCHIVE_EXTRACT_ACL; - flags |= ARCHIVE_EXTRACT_FFLAGS; - a = archive_read_new(); - archive_read_support_format_all(a); - archive_read_support_filter_all(a); - ext = archive_write_disk_new(); - archive_write_disk_set_options(ext, flags); - archive_write_disk_set_standard_lookup(ext); - if ((r = archive_read_open_filename(a, de->d_name, 10240))) ok = false; - while (ok) { - r = archive_read_next_header(a, &entry); - if (r == ARCHIVE_EOF) break; - if (r < ARCHIVE_OK) utilSay("%s", archive_error_string(a)); - if (r < ARCHIVE_WARN) { - ok = false; - break; - } - r = archive_write_header(ext, entry); - if (r < ARCHIVE_OK) { - utilSay("%s", archive_error_string(ext)); - } else { - if (archive_entry_size(entry) > 0) { - for (;;) { - r = archive_read_data_block(a, &buff, &size, &offset); - if (r == ARCHIVE_EOF) { - r = ARCHIVE_OK; - break; - } - if (r < ARCHIVE_OK) break; - r = archive_write_data_block(ext, buff, size, offset); - if (r < ARCHIVE_OK) { - utilSay("%s", archive_error_string(ext)); - break; - } - } - if (r < ARCHIVE_OK) utilSay("%s", archive_error_string(ext)); - if (r < ARCHIVE_WARN) { - ok = false; - break; - } - } - } - r = archive_write_finish_entry(ext); - if (r < ARCHIVE_OK) utilSay("%s", archive_error_string(ext)); - if (r < ARCHIVE_WARN) { - ok = false; - break; - } - } - archive_read_close(a); - archive_read_free(a); - archive_write_close(ext); - archive_write_free(ext); - - if (ok) unlink(de->d_name); - - } // if ok - } // if extension is valid - } // if it's a file - } // while files - - if (count > 0) utilSay(""); + if (count > 0) { + utilNewline(); + } closedir(dir); } -int main(int argc, char *argv[]) { - char *exeName = (char *)argv[0]; - char *temp = NULL; - ConfigT *conf = NULL; - QueueT *q = NULL; +// Look through archive for things I've told people NOT to ship! +// https://github.com/libarchive/libarchive/wiki/Examples#user-content-List_contents_of_Archive_stored_in_File +static bool _validateArchive(const char *filename, PackageTypeE type) { + struct archive *a = NULL; + struct archive_entry *entry = NULL; + const char *path = NULL; + const char *name = NULL; + const char *extension = NULL; + const char *slash = NULL; + const char *typeName = _packageTypes[type]; + char *toplevel = NULL; + char *gamesDat = NULL; + int32_t x = 0; + bool ok = true; + bool hasGamesDat = false; - unpackData(exeName); - unpackGames(); + a = _openArchive(filename); + if (a == NULL) { + return false; + } + + while (ok && (archive_read_next_header(a, &entry) == ARCHIVE_OK)) { + path = archive_entry_pathname(entry); + name = utilGetLastPathComponent(path); + extension = utilGetFileExtension(path); + + // Everything except a patch must live inside a single top level folder. + if (type != PACKAGE_PATCH) { + if (toplevel == NULL) { + slash = strchr(path, '/'); + if (slash == NULL) { + ok = false; + utilSay("!!! %s has files in root: %s", typeName, filename); + } else { + toplevel = utilStrndup(path, (size_t)(slash - path + 1)); + gamesDat = utilCreateString("%sgames.dat", toplevel); + } + } else { + if (!utilStartsWith(path, toplevel)) { + ok = false; + if (strchr(path, '/') == NULL) { + utilSay("!!! %s has files in root: %s", typeName, filename); + } else { + utilSay("!!! %s has multiple top level directories: %s", typeName, filename); + } + } + } + } + + if (ok) { + // Is this a forbidden file? + for (x = 0; ok && (_badFilenames[x] != NULL); x++) { + if (utilStricmp(name, _badFilenames[x]) == 0) { + ok = false; + utilSay("!!! %s has %s: %s", typeName, _badFilenames[x], filename); + } + } + // Is this a forbidden extension? + for (x = 0; ok && (_badExtensions[x] != NULL); x++) { + if (utilStricmp(extension, _badExtensions[x]) == 0) { + ok = false; + utilSay("!!! %s has %s file: %s", typeName, _badExtensions[x], filename); + } + } + // Not a patch, no extension, starts with 'singe' - could be unix binary. + if (ok && (type != PACKAGE_PATCH) && (strlen(extension) == 0) && utilStartsWith(name, "singe")) { + ok = false; + utilSay("!!! %s has singe file: %s", typeName, filename); + } + // Is this games.dat? + if (ok && (gamesDat != NULL) && (utilStricmp(path, gamesDat) == 0)) { + hasGamesDat = true; + } + } + archive_read_data_skip(a); + } + archive_read_free(a); + free(toplevel); + free(gamesDat); + + // If it's a game, did we get a games.dat? + if (ok && (type == PACKAGE_GAME) && !hasGamesDat) { + ok = false; + utilSay("!!! %s has no games.dat: %s", typeName, filename); + } + + return ok; +} + + +ConfigT *cloneConf(const ConfigT *conf) { + ConfigT *c = (ConfigT *)calloc(1, sizeof(ConfigT)); + + if (!c) { + utilDie("Out of memory cloning config."); + } + + // Copy everything, then give the clone its own strings. + *c = *conf; + c->scriptFile = _cloneString(conf->scriptFile); + c->videoFile = _cloneString(conf->videoFile); + c->dataDirBase = _cloneString(conf->dataDirBase); + c->dataDir = _cloneString(conf->dataDir); + + return c; +} + + +// Builds and creates dataDirBase + directory of filename. Returns a new string or NULL on failure. +char *createDataDir(const char *dataDirBase, const char *filename) { + const char separator = utilGetPathSeparator(); + char *relative = utilGetUpToLastPathComponent(filename); + char *start = relative; + char *path = NULL; + char *p = NULL; + + // Keep absolute paths inside the base: drop any drive letter and leading separators. + if (isalpha((unsigned char)start[0]) && (start[1] == ':')) { + start += 2; + } + while (*start == separator) { + start++; + } + // Neutralize ".." components. + for (p = start; *p != 0; p++) { + if ((p[0] == '.') && (p[1] == '.') && ((p == start) || (p[-1] == separator)) && ((p[2] == separator) || (p[2] == 0))) { + p[0] = '_'; + p[1] = '_'; + } + } + path = utilCreateString("%s%s", dataDirBase, start); + free(relative); + utilFixPathSeparators(&path, true); + + // Try to create data directory to ensure it exists. + if (!utilMkDirP(path, DIRECTORY_MODE)) { + free(path); + return NULL; + } + + return path; +} + + +void destroyConf(ConfigT **confPointer) { + ConfigT *conf = *confPointer; + + if (conf == NULL) { + return; + } + free(conf->dataDir); + free(conf->dataDirBase); + free(conf->videoFile); + free(conf->scriptFile); + free(conf); + *confPointer = NULL; +} + + +bool isFrameFileName(const char *filename) { + return utilStricmp(utilGetFileExtension(filename), "txt") == 0; +} + + +// Ok, this thing can have a mess of different arguments: +// WW - Just the width of the white border +// WW WB - Width of white border and then black border +// RW GW BW WW - Custom color "white" border and width +// RW GW BW WW WB - Custom color "white" border and width then width of black border +// RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width +bool parseSindenString(const char *sindenString, ConfigT *conf) { + const char *p = sindenString; + char *end = NULL; + long value = 0; + + conf->sindenArgc = 0; + while (*p != 0) { + // Skip separators and any quotes the shell left behind. + if ((*p == ' ') || (*p == '"') || (*p == '\'')) { + p++; + continue; + } + if (conf->sindenArgc >= SINDEN_ARG_MAX) { + return false; + } + value = strtol(p, &end, 10); + if (end == p) { + return false; + } + conf->sindenArgv[conf->sindenArgc++] = (int32_t)value; + p = end; + } + + // Did we get a sane number of arguments? + switch (conf->sindenArgc) { + case SINDEN_WHITE: + case SINDEN_WHITE_BLACK: + case SINDEN_CUSTOM_WHITE: + case SINDEN_CUSTOM_WHITE_BLACK: + case SINDEN_CUSTOM_WHITE_CUSTOM_BLACK: + return true; + + default: + return false; + } +} + + +void queueScript(const ConfigT *conf) { + QueueT *q = (QueueT *)calloc(1, sizeof(QueueT)); + + if (!q) { + utilDie("Out of memory queueing script."); + } + q->conf = cloneConf(conf); + LL_APPEND(_scriptQueue, q); +} + + +int main(int argc, char *argv[]) { + const char *exeName = argv[0]; + char *temp = NULL; + ConfigT *conf = NULL; + QueueT *q = NULL; + + // Options first so --help and --noconsole take effect before anything is written. + conf = _parseArguments(exeName, argc, argv); + + // For that dumb OS + utilRedirectConsole(); + + _unpackData(exeName); + _unpackGames(); // Queue initial script - conf = createConf(exeName, argc, argv); + _resolveFiles(exeName, conf); queueScript(conf); destroyConf(&conf); - // Do they want tracing of any kind? - - startSDL(); + _startSDL(); // Run script queue while (_scriptQueue) { - q = _scriptQueue; + // Do they want tracing of any kind? if (q->conf->scriptTracing || q->conf->programTracing) { temp = utilCreateString("%strace.txt", q->conf->dataDir); utilTraceStart(temp); free(temp); - temp = NULL; } - launcher(exeName, q->conf); + _launcher(exeName, q->conf); destroyConf(&q->conf); LL_DELETE(_scriptQueue, q); @@ -1124,11 +1316,11 @@ int main(int argc, char *argv[]) { utilTraceEnd(); } - stopSDL(); + _stopSDL(); -#ifdef _WIN32 - if (utilGetConsoleEnabled()) getchar(); -#endif + if (utilGetConsoleEnabled()) { + utilWaitForKeyOnWindows(); + } - return 0; + return EXIT_SUCCESS; } diff --git a/src/main.h b/src/main.h index 4916d0902..41e2736c1 100644 --- a/src/main.h +++ b/src/main.h @@ -25,15 +25,22 @@ #define MAIN_H -#include "common.h" - #include "singe.h" -ConfigT *cloneConf(ConfigT *conf); +#define VOLUME_MIN 0 +#define VOLUME_MAX 100 +#define SCALE_FACTOR_MIN 50 +#define SCALE_FACTOR_MAX 100 +#define DIRECTORY_MODE 0777 + + +ConfigT *cloneConf(const ConfigT *conf); +char *createDataDir(const char *dataDirBase, const char *filename); void destroyConf(ConfigT **confPointer); -bool parseSindenString(char **sindenStringPointer, ConfigT *conf); -void queueScript(ConfigT *conf); +bool isFrameFileName(const char *filename); +bool parseSindenString(const char *sindenString, ConfigT *conf); +void queueScript(const ConfigT *conf); #endif // MAIN_H diff --git a/src/singe.c b/src/singe.c index c537be1cd..dcfdd7d55 100644 --- a/src/singe.c +++ b/src/singe.c @@ -22,6 +22,7 @@ #include +#include #include "include/SDL2/SDL.h" #include "include/SDL2/SDL_image.h" @@ -56,63 +57,169 @@ LSEC_API int luaopen_ssl_config(lua_State *L); #include "singe.h" // We have to do the embedding here so the Lua module -// definitions can find their lenght properly. They +// definitions can find their length properly. They // can't be external to this source file. #define EMBED_HERE #include "embedded.h" -//#define DEBUG_TOOLS +#define INDEX_DISPLAY_START -1 +#define INDEX_DISPLAY_STOP -2 -#define INDEX_DISPLAY_START -1 -#define INDEX_DISPLAY_STOP -2 +// soundSetVolume/soundGetVolume use this legacy scale; the mixer uses MIX_MAX_VOLUME. +#define AUDIO_MAX_VOLUME 63 +#define MAX_MICE 4 +#define MOUSE_AXIS_COUNT 2 +#define MAX_CONTROLLERS 4 +#define CONTROLLER_AXIS_COUNT 6 +#define CONTROLLER_BUTTON_COUNT 15 +#define CONTROLLER_DEAD_ZONE_DEFAULT 15000 +#define AXIS_COUNT (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + MAX_MICE * MOUSE_AXIS_COUNT) +#define AXIS_INDEX_CONTROLLER(c, a) ((c) * CONTROLLER_AXIS_COUNT + (a)) +#define AXIS_INDEX_MOUSE(m, a) (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + (m) * MOUSE_AXIS_COUNT + (a)) -#define AUDIO_MAX_VOLUME 63 -#define MAX_TITLE_LENGTH 1024 -#define MAX_MICE 4 -#define MOUSE_AXIS_COUNT 2 -#define MAX_CONTROLLERS 4 -#define CONTROLLER_AXIS_COUNT 6 -#define CONTROLLER_BUTTON_COUNT 15 -#define AXIS_COUNT (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + MAX_MICE * MOUSE_AXIS_COUNT) -#define AXIS_KEY_DOWN 0 -#define AXIS_KEY_UP 1 +// Input codes handed to scripts and controls.cfg. Framework.singe builds its tables from these. +#define CODE_GAMEPAD_BASE 500 +#define CODE_GAMEPAD_STRIDE 100 // Per controller +#define CODE_AXIS_STRIDE 3 // Per axis: axis, negative direction, positive direction +#define CODE_AXIS_NEGATIVE 1 +#define CODE_AXIS_POSITIVE 2 +#define CODE_GAMEPAD_BUTTON_OFFSET (CONTROLLER_AXIS_COUNT * CODE_AXIS_STRIDE) +#define CODE_MOUSE_BASE 1000 +#define CODE_MOUSE_STRIDE 100 // Per mouse +#define CODE_MOUSE_BUTTON_COUNT 5 // Left, right, middle, X1, X2 +#define CODE_MOUSE_WHEEL_UP (CODE_MOUSE_BUTTON_COUNT) +#define CODE_MOUSE_WHEEL_DOWN (CODE_MOUSE_BUTTON_COUNT + 1) + +#define FRAME_TICK_MS 15 // Minimum time between onOverlayUpdate calls +#define IDLE_SLEEP_MS 1 +#define OVERLAY_SCALE_DEFAULT 0.5 +#define CONSOLE_FONT_GLYPHS 256 +#define DEGREES_PER_CIRCLE 360.0 +#define ANIMATION_MIN_DELAY_MS 10 // GIFs often carry a zero delay +#define SCREENSHOT_MAX 10000 +#define SOUND_QUEUE_SIZE 64 +#define HELD_KEYS_MAX 64 // Keys physically down at once +#define PAUSE_TEXT "PAUSED" +#define PAUSE_TEXT_SCALE 3 // Console font is small; scale the indicator up +#define COLOR_KEY_VALUE 0 +#define BLUE_SCREEN_BLUE 255 + +#define LOGO_FADE_STEPS 256 +#define LOGO_FADE_STEP_MS 3 +#define LOGO_HOLD_MS 750 + +#define INDEX_SCREEN_WIDTH 1280 +#define INDEX_SCREEN_HEIGHT 720 +#define INDEX_RADIUS_FACTOR 0.3 +#define INDEX_UPDATE_MS 5 +#define INDEX_TEXT_OVERLAP 5 +#define INDEX_GLASS_X_FACTOR 0.15 +#define INDEX_GLASS_Y_FACTOR 0.1 + +// Decoded video frames are BGRA in memory; this is the matching alpha-less surface format. +#if SDL_BYTEORDER == SDL_LIL_ENDIAN +#define VIDEO_SURFACE_FORMAT SDL_PIXELFORMAT_XRGB8888 +#else +#define VIDEO_SURFACE_FORMAT SDL_PIXELFORMAT_BGRX8888 +#endif -typedef struct { - char *name; +typedef enum KeyboardModeE { + KEYBOARD_NORMAL = 0, + KEYBOARD_FULL = 1 +} KeyboardModeE; + +typedef enum MouseModeE { + MOUSE_SINGLE = 100, + MOUSE_MANY = 200 +} MouseModeE; + +// Values match the Daphne LDP states scripts have always compared against. +typedef enum DiscStateE { + DISC_STOPPED = 2, + DISC_PLAYING = 3, + DISC_PAUSED = 4 +} DiscStateE; + +typedef enum FontQualityE { + FONT_QUALITY_SOLID = 1, + FONT_QUALITY_SHADED = 2, + FONT_QUALITY_BLENDED = 3 +} FontQualityE; + +typedef enum RenderQualityE { + RENDER_PIXELATED = 0, + RENDER_SMOOTH = 1 +} RenderQualityE; + +typedef enum OverlayResultE { + OVERLAY_NOT_UPDATED = 0, + OVERLAY_UPDATED = 1 +} OverlayResultE; + +// Index into _global.controlMappings and the SWITCH_* value handed to scripts. +typedef enum InputE { + INPUT_UP = 0, + INPUT_LEFT, + INPUT_DOWN, + INPUT_RIGHT, + INPUT_1P_START, + INPUT_2P_START, + INPUT_ACTION_1, + INPUT_ACTION_2, + INPUT_ACTION_3, + INPUT_1P_COIN, + INPUT_2P_COIN, + INPUT_SKILL_EASY, + INPUT_SKILL_MEDIUM, + INPUT_SKILL_HARD, + INPUT_SERVICE, + INPUT_TEST_MODE, + INPUT_RESET_CPU, + INPUT_SCREENSHOT, + INPUT_QUIT, + INPUT_PAUSE, + INPUT_CONSOLE, + // Added in Singe 2.00 + INPUT_ACTION_4, + INPUT_TILT, + INPUT_GRAB, + INPUT_COUNT +} InputE; + + +typedef struct LuaModuleS { + const char *name; union { - char *source; + const char *source; lua_CFunction openf; }; size_t length; -} luaModuleT; +} LuaModuleT; + +typedef struct InputNameS { + const char *configName; // Table name in controls.cfg + const char *switchName; // Constant name in scripts +} InputNameT; + +typedef struct HeldKeyS { + int32_t keysym; + int32_t scancode; +} HeldKeyT; typedef struct MouseS { - int32_t x; - int32_t y; - int32_t relx; - int32_t rely; - char name[64]; - bool connected; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpadded" - Uint32 buttons; -#pragma GCC diagnostic pop - Uint32 scrolluptick; - Uint32 scrolldowntick; - Uint32 scrolllefttick; - Uint32 scrollrighttick; + int32_t x; + int32_t y; + char name[64]; } MouseT; typedef struct SpriteS { int32_t id; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpadded" - IMG_Animation *animation; - SDL_Surface *originalSurface; - SDL_Surface *surface; -#pragma GCC diagnostic pop + IMG_Animation *animation; // NULL for still images + SDL_Surface *originalSurface; // Owned unless it points into animation->frames + SDL_Surface *surface; // What gets drawn: originalSurface, or a transformed copy + bool surfaceOwned; // True when surface is a transformed copy to free double angle; double scaleX; double scaleY; @@ -126,162 +233,111 @@ typedef struct SpriteS { } SpriteT; typedef struct SoundS { - int32_t id; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpadded" - Mix_Chunk *chunk; -#pragma GCC diagnostic pop - UT_hash_handle hh; + int32_t id; + Mix_Chunk *chunk; + UT_hash_handle hh; } SoundT; typedef struct FontS { - int32_t id; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpadded" - TTF_Font *font; -#pragma GCC diagnostic pop - UT_hash_handle hh; + int32_t id; + TTF_Font *font; + UT_hash_handle hh; } FontT; typedef struct VideoS { - int32_t id; - int32_t handle; - int64_t lastFrame; - bool wasPlayingBeforePause; - SDL_Texture *texture; - SDL_Surface *surface; - SDL_Surface *rotatedZoomedSurface; - double angle; - double scaleX; - double scaleY; - int smooth; - UT_hash_handle hh; + int32_t id; + int32_t handle; + int64_t lastFrame; + bool wasPlayingBeforePause; + bool transformChanged; + SDL_Texture *texture; + SDL_Surface *transformedSurface; + double angle; + double scaleX; + double scaleY; + int32_t smooth; + UT_hash_handle hh; } VideoT; typedef struct MappingS { - char *name; - int32_t frameworkIndex; - int32_t inputCount; - int32_t *input; + int32_t inputCount; + int32_t *input; } MappingT; - -enum { - KEYBD_NORMAL = 0, - KEYBD_FULL -}; - -enum { - MOUSE_SINGLE = 100, - MOUSE_MANY = 200 -}; - -enum { - LDP_ERROR = 0, - LDP_SEARCHING, - LDP_STOPPED, - LDP_PLAYING, - LDP_PAUSED -}; - -enum { - FONT_QUALITY_SOLID = 1, - FONT_QUALITY_SHADED, - FONT_QUALITY_BLENDED -}; - -enum { - // Index into _global.controlMappings array - INPUT_UP = 0, - INPUT_LEFT, - INPUT_DOWN, - INPUT_RIGHT, - INPUT_1P_START, - INPUT_2P_START, - INPUT_ACTION_1, - INPUT_ACTION_2, - INPUT_ACTION_3, - INPUT_1P_COIN, - INPUT_2P_COIN, - INPUT_SKILL_EASY, - INPUT_SKILL_MEDIUM, - INPUT_SKILL_HARD, - INPUT_SERVICE, - INPUT_TEST_MODE, - INPUT_RESET_CPU, - INPUT_SCREENSHOT, - INPUT_QUIT, - INPUT_PAUSE, - INPUT_CONSOLE, - // Added in Singe 2.00 - INPUT_ACTION_4, - INPUT_TILT, - INPUT_GRAB, - INPUT_COUNT -}; - - typedef struct GlobalS { - MouseT mice[MAX_MICE]; - lua_State *luaContext; - SDL_Color colorForeground; - SDL_Color colorBackground; - SDL_Surface *overlay; - SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *videoTexture; - SDL_Surface *consoleFontSurface; - SDL_GameController **controllers; - int32_t controllerCount; - int32_t controllerDeadZone; - int32_t consoleFontWidth; - int32_t consoleFontHeight; - int32_t nextSpriteId; - int32_t nextSoundId; - int32_t nextFontId; - int32_t nextVideoId; - int32_t effectsVolume; - int32_t keyboardMode; - bool keyboardState[SDL_NUM_SCANCODES]; - int32_t keyboardLastDown; - int32_t keyboardLastUp; - int32_t frameFileHandle; - int32_t videoHandle; - int32_t fontQuality; - int32_t mouseMode; - int32_t mouseCount; - int32_t axisCache[AXIS_COUNT]; - double overlayScaleX; // Difference between overlay and video - double overlayScaleY; // Difference between overlay and video - bool pauseState; // by RDG2010 - bool pauseEnabled; // by RDG2010 - bool refreshDisplay; - bool running; - bool discStopped; - bool mouseEnabled; - bool mouseGrabbed; - bool requestScreenShot; - bool wasPlayingBeforePause; - VideoT *videoList; - SpriteT *spriteList; - SoundT *soundList; - FontT *fontList; - FontT *fontCurrent; - MappingT controlMappings[INPUT_COUNT]; - ConfigT *conf; // Local copy of command line options + MouseT mice[MAX_MICE]; + lua_State *luaContext; + SDL_Color colorForeground; + SDL_Color colorBackground; + SDL_Surface *overlay; + SDL_Texture *overlayTexture; + SDL_Window *window; + SDL_Renderer *renderer; + SDL_Texture *videoTexture; + SDL_Surface *consoleFontSurface; + SDL_GameController *controllers[MAX_CONTROLLERS]; + int32_t controllerDeadZone; + int32_t consoleFontWidth; + int32_t consoleFontHeight; + int32_t nextSpriteId; + int32_t nextSoundId; + int32_t nextFontId; + int32_t nextVideoId; + int32_t nextScreenshot; // First index worth checking; lower ones are taken + int32_t effectsVolume; + KeyboardModeE keyboardMode; + bool keyboardState[SDL_NUM_SCANCODES]; + int32_t keyboardLastDown; + int32_t keyboardLastUp; + int32_t frameFileHandle; + int32_t videoHandle; + FontQualityE fontQuality; + MouseModeE mouseMode; + int32_t mouseCount; + int32_t axisCache[AXIS_COUNT]; + int32_t axisCode[AXIS_COUNT]; // Direction code currently pressed per axis, 0 when none + int32_t soundQueue[SOUND_QUEUE_SIZE]; // Channels finished since the last frame (audio thread writes) + int32_t soundQueueCount; + bool frozen; // Engine pause: the script is not being run + bool switchHeld[INPUT_COUNT]; // Switches the script believes are down (MODE_NORMAL) + HeldKeyT heldKeys[HELD_KEYS_MAX]; // Keys the script believes are down (MODE_FULL) + int32_t heldKeyCount; + HeldKeyT physicalKeys[HELD_KEYS_MAX]; // Keys and buttons physically down right now + int32_t physicalKeyCount; + SDL_Texture *pauseTexture; + int32_t pauseTextureWidth; + int32_t pauseTextureHeight; + double overlayScaleX; // Overlay size / video size + double overlayScaleY; + bool overlayDirty; + bool pauseState; // by RDG2010 + bool pauseEnabled; // by RDG2010 + bool refreshDisplay; + bool running; + bool discStopped; + bool mouseEnabled; + bool mouseGrabbed; + bool requestScreenShot; + bool wasPlayingBeforePause; + VideoT *videoList; + SpriteT *spriteList; + SoundT *soundList; + FontT *fontList; + FontT *fontCurrent; + MappingT controlMappings[INPUT_COUNT]; + ConfigT *conf; // Local copy of command line options } GlobalT; -// Other globals -GlobalT _global; +static GlobalT _global; -#define MODL(name, array) { name, { (char *)array }, sizeof(array) } -#define MODC(name, openf) { name, { (char *)openf }, 0 } +#define MODL(name, array) { name, { (const char *)array }, sizeof(array) } +#define MODC(name, openf) { name, { (const char *)openf }, 0 } // Lua Modules -static const luaModuleT luaModules[] = { +static const LuaModuleT _luaModules[] = { // LuaFileSystem MODC("lfs", luaopen_lfs), // LuaSocket @@ -328,4433 +384,3815 @@ static const luaModuleT luaModules[] = { MODL("copas.timer", copas_timer_lua), }; - -int32_t apiColorBackground(lua_State *L); -int32_t apiColorForeground(lua_State *L); - -int32_t apiControllerGetAxis(lua_State *L); -int32_t apiControllerGetButton(lua_State *L); - -int32_t apiDebugPrint(lua_State *L); - -int32_t apiDiscAudio(lua_State *L); -int32_t apiDiscChangeSpeed(lua_State *L); -int32_t apiDiscGetAudioTrack(lua_State *L); -int32_t apiDiscGetAudioTracks(lua_State *L); -int32_t apiDiscGetFrame(lua_State *L); -int32_t apiDiscGetHeight(lua_State *L); -int32_t apiDiscGetLanguage(lua_State *L); -int32_t apiDiscGetState(lua_State *L); -int32_t apiDiscGetWidth(lua_State *L); -int32_t apiDiscPause(lua_State *L); -int32_t apiDiscPauseAtFrame(lua_State *L); -int32_t apiDiscPlay(lua_State *L); -int32_t apiDiscSearch(lua_State *L); -int32_t apiDiscSearchBlanking(lua_State *L); -int32_t apiDiscSetAudioTrack(lua_State *L); -int32_t apiDiscSetFps(lua_State *L); -int32_t apiDiscSkipBackward(lua_State *L); -int32_t apiDiscSkipBlanking(lua_State *L); -int32_t apiDiscSkipForward(lua_State *L); -int32_t apiDiscSkipToFrame(lua_State *L); -int32_t apiDiscStepBackward(lua_State *L); -int32_t apiDiscStepForward(lua_State *L); -int32_t apiDiscStop(lua_State *L); - -int32_t apiFontLoad(lua_State *L); -int32_t apiFontPrint(lua_State *L); -int32_t apiFontQuality(lua_State *L); -int32_t apiFontSelect(lua_State *L); -int32_t apiFontToSprite(lua_State *L); -int32_t apiFontUnload(lua_State *L); - -int32_t apiKeyboardGetLastDown(lua_State *L); -int32_t apiKeyboardGetLastUp(lua_State *L); -int32_t apiKeyboardGetMode(lua_State *L); -int32_t apiKeyboardGetModifiers(lua_State *L); -int32_t apiKeyboardSetMode(lua_State *L); -int32_t apiKeyboardIsDown(lua_State *L); - -int32_t apiMouseDisable(lua_State *L); -int32_t apiMouseEnable(lua_State *L); -int32_t apiMouseGetPosition(lua_State *L); -int32_t apiMouseHowMany(lua_State *L); -int32_t apiMouseSetCaptured(lua_State *L); -int32_t apiMouseSetMode(lua_State *L); - -int32_t apiOverlayBox(lua_State *L); -int32_t apiOverlayCircle(lua_State *L); -int32_t apiOverlayClear(lua_State *L); -int32_t apiOverlayEllipse(lua_State *L); -int32_t apiOverlayGetHeight(lua_State *L); -int32_t apiOverlayGetWidth(lua_State *L); -int32_t apiOverlayLine(lua_State *L); -int32_t apiOverlayPlot(lua_State *L); -int32_t apiOverlayPrint(lua_State *L); -int32_t apiOverlaySetResolution(lua_State *L); - -int32_t apiScriptExecute(lua_State *L); -int32_t apiScriptPush(lua_State *L); - -int32_t apiSingeDisablePauseKey(lua_State *L); -int32_t apiSingeEnablePauseKey(lua_State *L); -int32_t apiSingeGetDataPath(lua_State *L); -int32_t apiSingeGetHeight(lua_State *L); -int32_t apiSingeGetPauseFlag(lua_State *L); -int32_t apiSingeGetScriptPath(lua_State *L); -int32_t apiSingeGetWidth(lua_State *L); -int32_t apiSingeScreenshot(lua_State *L); -int32_t apiSingeSetGameName(lua_State *L); -int32_t apiSingeSetPauseFlag(lua_State *L); -int32_t apiSingeQuit(lua_State *L); -int32_t apiSingeVersion(lua_State *L); -int32_t apiSingeWantsCrosshairs(lua_State *L); - -int32_t apiSoundLoad(lua_State *L); -int32_t apiSoundPlay(lua_State *L); -int32_t apiSoundPause(lua_State *L); -int32_t apiSoundResume(lua_State *L); -int32_t apiSoundIsPlaying(lua_State *L); -int32_t apiSoundStop(lua_State *L); -int32_t apiSoundSetVolume(lua_State *L); -int32_t apiSoundGetVolume(lua_State *L); -int32_t apiSoundFullStop(lua_State *L); -int32_t apiSoundUnload(lua_State *L); - -int32_t apiSpriteDraw(lua_State *L); -int32_t apiSpriteGetFrame(lua_State *L); -int32_t apiSpriteGetHeight(lua_State *L); -int32_t apiSpriteGetWidth(lua_State *L); -int32_t apiSpriteIsPlaying(lua_State *L); -int32_t apiSpriteLoad(lua_State *L); -int32_t apiSpriteLoop(lua_State *L); -int32_t apiSpritePause(lua_State *L); -int32_t apiSpritePlay(lua_State *L); -int32_t apiSpriteQuality(lua_State *L); -int32_t apiSpriteRotate(lua_State *L); -int32_t apiSpriteRotateAndScale(lua_State *L); -int32_t apiSpriteScale(lua_State *L); -int32_t apiSpriteSetFrame(lua_State *L); -int32_t apiSpriteUnload(lua_State *L); - -int32_t apiVideoDraw(lua_State *L); -int32_t apiVideoGetAudioTrack(lua_State *L); -int32_t apiVideoGetAudioTracks(lua_State *L); -int32_t apiVideoGetFrame(lua_State *L); -int32_t apiVideoGetFrameCount(lua_State *L); -int32_t apiVideoGetHeight(lua_State *L); -int32_t apiVideoGetLanguage(lua_State *L); -int32_t apiVideoGetLanguageDescription(lua_State *L); -int32_t apiVideoGetVolume(lua_State *L); -int32_t apiVideoGetWidth(lua_State *L); -int32_t apiVideoIsPlaying(lua_State *L); -int32_t apiVideoLoad(lua_State *L); -int32_t apiVideoPause(lua_State *L); -int32_t apiVideoPlay(lua_State *L); -int32_t apiVideoQuality(lua_State *L); -int32_t apiVideoRotate(lua_State *L); -int32_t apiVideoRotateAndScale(lua_State *L); -int32_t apiVideoScale(lua_State *L); -int32_t apiVideoSeek(lua_State *L); -int32_t apiVideoSetAudioTrack(lua_State *L); -int32_t apiVideoSetVolume(lua_State *L); -int32_t apiVideoUnload(lua_State *L); - -int32_t apiVldpGetHeight(lua_State *L); -int32_t apiVldpGetPixel(lua_State *L); -int32_t apiVldpGetWidth(lua_State *L); -int32_t apiVldpVerbose(lua_State *L); - -ConfigT *buildConfFromTable(lua_State *L); -void doIndexDisplay(int32_t percent); -void doLogos(void); -void callLua(const char *func, const char *sig, ...); -void channelFinished(int channel); -void line(int32_t x1, int32_t y1, int32_t x2, int32_t y2, SDL_Color *c); -void luaDie(lua_State *L, char *method, char *fmt, ...); -int32_t luaError(lua_State *L); -int luaSearcher(lua_State *L); -void luaTrace(lua_State *L, char *method, char *fmt, ...); -void processKey(bool down, int keysym, int32_t scancode); -void progTrace(char *fmt, ...); -void putPixel(int32_t x, int32_t y, SDL_Color *c); -void startControllers(void); -void startLuaContext(lua_State *L); -void stopControllers(void); -SDL_Surface *surfaceCopy(SDL_Surface *source); -void takeScreenshot(void); -void updatePauseState(void); - -#ifdef DEBUG_TOOLS -void luaStackDump(lua_State *L); -#endif - -int32_t apiColorBackground(lua_State *L) { - int32_t n = lua_gettop(L); - double d = 0; - bool result = false; - - if ((n == 3) || (n == 4)) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 1); _global.colorBackground.r = (byte)d; - d = lua_tonumber(L, 2); _global.colorBackground.g = (byte)d; - d = lua_tonumber(L, 3); _global.colorBackground.b = (byte)d; - if (n == 3) { - _global.colorBackground.a = (byte)0; // Default to transparent. - } else { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 4); _global.colorBackground.a = (byte)d; - } else { - _global.colorBackground.a = (byte)0; // Default to transparent. - } - } - result = true; - } - } - } - } - - if (result) { - luaTrace(L, "colorBackground", "%d %d %d %d", _global.colorBackground.r, _global.colorBackground.g, _global.colorBackground.b, _global.colorBackground.a); - } else { - luaDie(L, "colorBackground", "Failed!"); - } - - return 0; -} - - -int32_t apiColorForeground(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - double d = 0; - - if ((n == 3) || (n == 4)) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 1); _global.colorForeground.r = (byte)d; - d = lua_tonumber(L, 2); _global.colorForeground.g = (byte)d; - d = lua_tonumber(L, 3); _global.colorForeground.b = (byte)d; - if (n == 3) { - _global.colorForeground.a = (byte)255; // Default to opaque. - } else { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 4); _global.colorForeground.a = (byte)d; - } else { - _global.colorForeground.a = (byte)255; // Default to opaque. - } - } - result = true; - } - } - } - } - - if (result) { - luaTrace(L, "colorForeground", "%d %d %d %d", _global.colorForeground.r, _global.colorForeground.g, _global.colorForeground.b, _global.colorForeground.a); - } else { - luaDie(L, "colorForeground", "Failed!"); - } - - return 0; -} - - -int32_t apiControllerGetAxis(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t c = 0; - int32_t a = 0; - int32_t v = 0; - double d = 0; - bool result = false; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); c = (int32_t)d; - d = lua_tonumber(L, 2); a = (int32_t)d; - if ((c < 0) || (c >= MAX_CONTROLLERS)) luaDie(L, "controllerGetAxis", "Invalid controller index: %d", c); - if ((a < 0) || (a >= CONTROLLER_AXIS_COUNT)) luaDie(L, "controllerGetAxis", "Invalid controller axis: %d", a); - v = _global.axisCache[c * CONTROLLER_AXIS_COUNT + a]; - result = true; - } - } - } - - if (result) { - luaTrace(L, "controllerGetAxis", "%d %d %d", c, a, v); - lua_pushinteger(L, v); - } else { - luaDie(L, "controllerGetAxis", "Failed!"); - } - - return 1; -} - - - -int32_t apiControllerGetButton(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t c = 0; - int32_t a = 0; - int32_t v = 0; - double d = 0; - bool result = false; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); c = (int32_t)d; - d = lua_tonumber(L, 2); a = (int32_t)d; - if ((c < 0) || (c >= MAX_CONTROLLERS)) luaDie(L, "controllerGetButton", "Invalid controller index: %d", c); - if ((a < 0) || (a >= CONTROLLER_BUTTON_COUNT)) luaDie(L, "controllerGetButton", "Invalid controller button: %d", a); - if (_global.controllers[c] != NULL) { - // Figure out SDL enumeration value from our framework value - // Controller codes begin at 500 and increment in 100 - // The button values line up with the enumeration used by SDL + 18 - a = ((a - 500) - (c * 100)) - 18; - v = SDL_GameControllerGetButton(_global.controllers[c], a); - result = true; - } - } - } - } - - if (result) { - luaTrace(L, "controllerGetButton", "%d %d %d", c, a, v); - lua_pushboolean(L, v); - } else { - luaDie(L, "controllerGetButton", "Failed!"); - } - - return 1; -} - - -int32_t apiSingeGetHeight(lua_State *L) { - int32_t y; - SDL_GetWindowSize(_global.window, NULL, &y); - luaTrace(L, "singeGetHeight", "%d", y); - lua_pushinteger(L, y); - return 1; -} - - -int32_t apiSingeGetWidth(lua_State *L) { - int32_t x; - SDL_GetWindowSize(_global.window, &x, NULL); - luaTrace(L, "singeGetWidth", "%d", x); - lua_pushinteger(L, x); - return 1; -} - - -int32_t apiSingeScreenshot(lua_State *L) { - - luaTrace(L, "singeScreenshot", "Screenshot requested."); - _global.requestScreenShot = true; - - return 0; -} - - -int32_t apiDebugPrint(lua_State *L) { - int32_t n = lua_gettop(L); - - if (n == 1) { - if (lua_isstring(L, 1)) { - luaTrace(L, "DebugPrint", "%s", lua_tostring(L, 1)); - utilSay("%s", lua_tostring(L, 1)); - } - } - - return 0; -} - - -int32_t apiDiscAudio(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t channel = 0; - int32_t left = 0; - int32_t right = 0; - bool onOff = false; - bool result = false; - double d = 0; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isboolean(L, 2)) { - if (_global.videoHandle >= 0) { - d = lua_tonumber(L, 1); channel = (int32_t)d; - d = lua_toboolean(L, 2); onOff = (bool)d; - videoGetVolume(_global.videoHandle, &left, &right); - if (channel == 1) left = (onOff ? _global.conf->volumeVldp : 0); - if (channel == 2) right = (onOff ? _global.conf->volumeVldp : 0); - videoSetVolume(_global.videoHandle, left, right); - result = true; - } - } - } - } - - if (result) { - luaTrace(L, "discAudio", "%d %d", left, right); - } else { - luaDie(L, "discAudio", "Failed!"); - } - - return 0; -} - - -int32_t apiDiscChangeSpeed(lua_State *L) { - (void)L; - //***REMOVED*** - luaTrace(L, "discChangeSpeed", "Unimplemented"); - return 0; -} - - -int32_t apiDiscGetAudioTrack(lua_State *L) { - bool result = false; - int64_t r = 0; - - (void)L; - - if (_global.videoHandle >= 0) { - r = videoGetAudioTrack(_global.videoHandle); - result = true; - } - - if (result) { - luaTrace(L, "discGetAudioTrack", "%ld", r); - } else { - luaDie(L, "discGetAudioTrack", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiDiscGetAudioTracks(lua_State *L) { - bool result = false; - int64_t r = 0; - - (void)L; - - if (_global.videoHandle >= 0) { - r = videoGetAudioTracks(_global.videoHandle); - result = true; - } - - if (result) { - luaTrace(L, "discGetAudioTracks", "%ld", r); - } else { - luaDie(L, "discGetAudioTracks", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiDiscGetFrame(lua_State *L) { - int64_t frame = 0; - - if (!_global.discStopped) { - if (_global.conf->isFrameFile) { - frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle); - } else { - if (_global.videoHandle >= 0) frame = videoGetFrame(_global.videoHandle); - } - } - - luaTrace(L, "discGetFrame", "%ld", frame); - lua_pushinteger(L, frame); - - return 1; -} - - -int32_t apiDiscGetLanguage(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - char *r = NULL; - int32_t track = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); track = (int32_t)d; - r = videoGetLanguage(_global.videoHandle, track); - result = true; - } - } - - if (result) { - luaTrace(L, "discGetLanguage", "%d %s", track, r); - } else { - luaDie(L, "discGetLanguage", "Failed!"); - } - - lua_pushstring(L, r); - return 1; -} - - -int32_t apiDiscPause(lua_State *L) { - (void)L; - if (!_global.discStopped) { - if (_global.videoHandle >= 0) videoPause(_global.videoHandle); - luaTrace(L, "discPause", ""); - } else { - luaTrace(L, "discPause", "Failed! Disc is stopped."); - } - return 0; -} - - -int32_t apiDiscPauseAtFrame(lua_State *L) { - // More RDG oddness. This appears to be identical to discSearch. - return apiDiscSearch(L); -} - - -int32_t apiDiscPlay(lua_State *L) { - (void)L; - if (_global.videoHandle >= 0) videoPlay(_global.videoHandle); - _global.discStopped = false; - luaTrace(L, "discPlay", ""); - return 0; -} - - -int32_t apiDiscSearch(lua_State *L) { - int32_t n = lua_gettop(L); - int64_t frame = 0; - int64_t aFrame = 0; - bool result = false; - double d = 0; - - // No matter the disc state, seek to the frame, display it, and pause. - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); frame = (int64_t)d; - if (_global.conf->isFrameFile) { - frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &aFrame); - } else { - if (_global.videoHandle >= 0) videoSeek(_global.videoHandle, frame); - } - if (_global.videoHandle >= 0) videoPause(_global.videoHandle); - _global.discStopped = false; - result = true; - } - } - - if (result) { - luaTrace(L, "discSearch", "%ld", frame); - } else { - luaDie(L, "discSearch", "Failed!"); - } - - return 0; -} - - -int32_t apiDiscSearchBlanking(lua_State *L) { - (void)L; - //***REMOVED*** - luaTrace(L, "discSearchBlanking", "Unimplemented"); - return 0; -} - - -int32_t apiDiscSetAudioTrack(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int64_t track = 0; - double d; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); track = (int32_t)d; - if (_global.videoHandle >= 0) { - if ((track >= 0) && (track < videoGetAudioTracks(_global.videoHandle))) { - videoSetAudioTrack(_global.videoHandle, track); - } else { - luaDie(L, "discSetAudioTrack", "Invalid audio track in apiDiscSetAudioTrack."); - } - result = true; - } - } - } - - if (result) { - luaTrace(L, "discSetAudioTrack", "%d", track); - } else { - luaDie(L, "discSetAudioTrack", "Failed!"); - } +// One entry per InputE, in order. The config name is the controls.cfg table; the switch name is the Lua constant. +static const InputNameT _inputNames[INPUT_COUNT] = { + { "INPUT_UP", "SWITCH_UP" }, + { "INPUT_LEFT", "SWITCH_LEFT" }, + { "INPUT_DOWN", "SWITCH_DOWN" }, + { "INPUT_RIGHT", "SWITCH_RIGHT" }, + { "INPUT_1P_START", "SWITCH_START1" }, + { "INPUT_2P_START", "SWITCH_START2" }, + { "INPUT_ACTION_1", "SWITCH_BUTTON1" }, + { "INPUT_ACTION_2", "SWITCH_BUTTON2" }, + { "INPUT_ACTION_3", "SWITCH_BUTTON3" }, + { "INPUT_1P_COIN", "SWITCH_COIN1" }, + { "INPUT_2P_COIN", "SWITCH_COIN2" }, + { "INPUT_SKILL_EASY", "SWITCH_SKILL1" }, + { "INPUT_SKILL_MEDIUM", "SWITCH_SKILL2" }, + { "INPUT_SKILL_HARD", "SWITCH_SKILL3" }, + { "INPUT_SERVICE", "SWITCH_SERVICE" }, + { "INPUT_TEST_MODE", "SWITCH_TEST" }, + { "INPUT_RESET_CPU", "SWITCH_RESET" }, + { "INPUT_SCREENSHOT", "SWITCH_SCREENSHOT" }, + { "INPUT_QUIT", "SWITCH_QUIT" }, + { "INPUT_PAUSE", "SWITCH_PAUSE" }, + { "INPUT_CONSOLE", "SWITCH_CONSOLE" }, + { "INPUT_ACTION_4", "SWITCH_BUTTON4" }, + { "INPUT_TILT", "SWITCH_TILT" }, + { "INPUT_GRAB", "SWITCH_GRAB" } +}; + +// SDL numbers mouse buttons left, middle, right; scripts number them left, right, middle. +static const int32_t _sdlMouseButtonToCode[] = { 0, 0, 2, 1, 3, 4 }; + + +static int32_t _apiUnimplemented(lua_State *L, const char *method); +static bool _argBoolean(lua_State *L, const char *method, int32_t index); +static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum); +static FontT *_argFont(lua_State *L, const char *method, int32_t index); +static int32_t _argInteger(lua_State *L, const char *method, int32_t index); +static int64_t _argInteger64(lua_State *L, const char *method, int32_t index); +static double _argNumber(lua_State *L, const char *method, int32_t index); +static SoundT *_argSound(lua_State *L, const char *method, int32_t index); +static SpriteT *_argSprite(lua_State *L, const char *method, int32_t index); +static const char *_argString(lua_State *L, const char *method, int32_t index); +static VideoT *_argVideo(lua_State *L, const char *method, int32_t index); +static ConfigT *_buildConfFromTable(lua_State *L); +static void _callLua(const char *func, const char *sig, ...); +static void _channelFinished(int channel); +static int32_t _controllerSlot(SDL_JoystickID which); +static bool _delayAndPump(uint32_t ms); +static void _deliverKey(bool down, int32_t keysym, int32_t scancode); +static void _discSeek(int64_t frame); +static void _doIndexDisplay(int32_t percent); +static void _doLogos(void); +static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel); +static void _drawPauseIndicator(const SDL_Rect *target); +static InputE _engineSwitch(int32_t scancode); +static void _fireMouseMoved(int32_t device, int32_t x, int32_t y, int32_t xr, int32_t yr); +static void _fontDestroy(FontT *font); +static void _freezeGame(bool freeze); +static void _heldListUpdate(HeldKeyT *list, int32_t *count, bool down, int32_t keysym, int32_t scancode); +static void _loadControlsFile(const char *path); +static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int length); +static SDL_Texture *_loadEmbeddedTexture(const unsigned char *data, unsigned int length, SDL_Surface **surface); +static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn)); +static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args); +static int32_t _luaPanic(lua_State *L); +static int32_t _luaSearcher(lua_State *L); +static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))); +static int32_t _luaTraceback(lua_State *L); +static int32_t _mixerVolume(int32_t effectsVolume); +static int32_t _mouseCode(int32_t device, int32_t button); +static void _overlayTouched(void); +static void _pauseAllVideos(bool pause); +static void _processKey(bool down, int32_t keysym, int32_t scancode); +static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +static void _pushConstants(lua_State *L); +static void _putPixel(int32_t x, int32_t y, uint32_t pixel); +static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha); +static void _releaseAxis(int32_t axisIndex); +static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message); +static void _selectDefaultAudioTrack(int32_t handle); +static void _setMouseCaptured(bool captured); +static void _setPause(bool paused, bool fromKey); +static void _soundDestroy(SoundT *sound); +static void _spriteDestroy(SpriteT *sprite); +static void _spriteFreeSurface(SpriteT *sprite); +static void _spriteRebuildSurface(SpriteT *sprite); +static void _startControllers(void); +static void _startLuaContext(lua_State *L); +static void _stopControllers(void); +static SDL_Surface *_surfaceCopy(SDL_Surface *source); +static void _takeScreenshot(void); +static void _updatePauseState(void); +static void _videoDestroy(VideoT *video); + +static int32_t apiColorBackground(lua_State *L); +static int32_t apiColorForeground(lua_State *L); +static int32_t apiControllerGetAxis(lua_State *L); +static int32_t apiControllerGetButton(lua_State *L); +static int32_t apiDebugPrint(lua_State *L); +static int32_t apiDiscAudio(lua_State *L); +static int32_t apiDiscChangeSpeed(lua_State *L); +static int32_t apiDiscGetAudioTrack(lua_State *L); +static int32_t apiDiscGetAudioTracks(lua_State *L); +static int32_t apiDiscGetFrame(lua_State *L); +static int32_t apiDiscGetHeight(lua_State *L); +static int32_t apiDiscGetLanguage(lua_State *L); +static int32_t apiDiscGetState(lua_State *L); +static int32_t apiDiscGetWidth(lua_State *L); +static int32_t apiDiscPause(lua_State *L); +static int32_t apiDiscPlay(lua_State *L); +static int32_t apiDiscSearch(lua_State *L); +static int32_t apiDiscSearchBlanking(lua_State *L); +static int32_t apiDiscSetAudioTrack(lua_State *L); +static int32_t apiDiscSetFPS(lua_State *L); +static int32_t apiDiscSkipBackward(lua_State *L); +static int32_t apiDiscSkipBlanking(lua_State *L); +static int32_t apiDiscSkipForward(lua_State *L); +static int32_t apiDiscSkipToFrame(lua_State *L); +static int32_t apiDiscStepBackward(lua_State *L); +static int32_t apiDiscStepForward(lua_State *L); +static int32_t apiDiscStop(lua_State *L); +static int32_t apiFontLoad(lua_State *L); +static int32_t apiFontPrint(lua_State *L); +static int32_t apiFontQuality(lua_State *L); +static int32_t apiFontSelect(lua_State *L); +static int32_t apiFontToSprite(lua_State *L); +static int32_t apiFontUnload(lua_State *L); +static int32_t apiKeyboardGetLastDown(lua_State *L); +static int32_t apiKeyboardGetLastUp(lua_State *L); +static int32_t apiKeyboardGetMode(lua_State *L); +static int32_t apiKeyboardGetModifiers(lua_State *L); +static int32_t apiKeyboardIsDown(lua_State *L); +static int32_t apiKeyboardSetMode(lua_State *L); +static int32_t apiMouseGetPosition(lua_State *L); +static int32_t apiMouseHowMany(lua_State *L); +static int32_t apiMouseSetCaptured(lua_State *L); +static int32_t apiMouseSetEnabled(lua_State *L); +static int32_t apiMouseSetMode(lua_State *L); +static int32_t apiOverlayBox(lua_State *L); +static int32_t apiOverlayCircle(lua_State *L); +static int32_t apiOverlayClear(lua_State *L); +static int32_t apiOverlayEllipse(lua_State *L); +static int32_t apiOverlayGetHeight(lua_State *L); +static int32_t apiOverlayGetWidth(lua_State *L); +static int32_t apiOverlayLine(lua_State *L); +static int32_t apiOverlayPlot(lua_State *L); +static int32_t apiOverlayPrint(lua_State *L); +static int32_t apiOverlaySetResolution(lua_State *L); +static int32_t apiScriptExecute(lua_State *L); +static int32_t apiScriptPush(lua_State *L); +static int32_t apiSingeGetDataPath(lua_State *L); +static int32_t apiSingeGetHeight(lua_State *L); +static int32_t apiSingeGetPauseFlag(lua_State *L); +static int32_t apiSingeGetScriptPath(lua_State *L); +static int32_t apiSingeGetWidth(lua_State *L); +static int32_t apiSingeQuit(lua_State *L); +static int32_t apiSingeScreenshot(lua_State *L); +static int32_t apiSingeSetGameName(lua_State *L); +static int32_t apiSingeSetPauseFlag(lua_State *L); +static int32_t apiSingeSetPauseKeyEnabled(lua_State *L); +static int32_t apiSingeVersion(lua_State *L); +static int32_t apiSingeWantsCrosshairs(lua_State *L); +static int32_t apiSoundFullStop(lua_State *L); +static int32_t apiSoundGetVolume(lua_State *L); +static int32_t apiSoundIsPlaying(lua_State *L); +static int32_t apiSoundLoad(lua_State *L); +static int32_t apiSoundPause(lua_State *L); +static int32_t apiSoundPlay(lua_State *L); +static int32_t apiSoundResume(lua_State *L); +static int32_t apiSoundSetVolume(lua_State *L); +static int32_t apiSoundStop(lua_State *L); +static int32_t apiSoundUnload(lua_State *L); +static int32_t apiSpriteDraw(lua_State *L); +static int32_t apiSpriteGetFrame(lua_State *L); +static int32_t apiSpriteGetHeight(lua_State *L); +static int32_t apiSpriteGetWidth(lua_State *L); +static int32_t apiSpriteIsPlaying(lua_State *L); +static int32_t apiSpriteLoad(lua_State *L); +static int32_t apiSpriteLoop(lua_State *L); +static int32_t apiSpritePause(lua_State *L); +static int32_t apiSpritePlay(lua_State *L); +static int32_t apiSpriteQuality(lua_State *L); +static int32_t apiSpriteRotate(lua_State *L); +static int32_t apiSpriteRotateAndScale(lua_State *L); +static int32_t apiSpriteScale(lua_State *L); +static int32_t apiSpriteSetFrame(lua_State *L); +static int32_t apiSpriteUnload(lua_State *L); +static int32_t apiVideoDraw(lua_State *L); +static int32_t apiVideoGetAudioTrack(lua_State *L); +static int32_t apiVideoGetAudioTracks(lua_State *L); +static int32_t apiVideoGetFrame(lua_State *L); +static int32_t apiVideoGetFrameCount(lua_State *L); +static int32_t apiVideoGetHeight(lua_State *L); +static int32_t apiVideoGetLanguage(lua_State *L); +static int32_t apiVideoGetLanguageDescription(lua_State *L); +static int32_t apiVideoGetVolume(lua_State *L); +static int32_t apiVideoGetWidth(lua_State *L); +static int32_t apiVideoIsPlaying(lua_State *L); +static int32_t apiVideoLoad(lua_State *L); +static int32_t apiVideoPause(lua_State *L); +static int32_t apiVideoPlay(lua_State *L); +static int32_t apiVideoQuality(lua_State *L); +static int32_t apiVideoRotate(lua_State *L); +static int32_t apiVideoRotateAndScale(lua_State *L); +static int32_t apiVideoScale(lua_State *L); +static int32_t apiVideoSeek(lua_State *L); +static int32_t apiVideoSetAudioTrack(lua_State *L); +static int32_t apiVideoSetVolume(lua_State *L); +static int32_t apiVideoUnload(lua_State *L); +static int32_t apiVldpGetPixel(lua_State *L); +static int32_t apiVldpSetVerbose(lua_State *L); + + +// ===== Internal helpers ===== + + +// Legacy Daphne functions that Singe never implemented. They trace and return nothing. +static int32_t _apiUnimplemented(lua_State *L, const char *method) { + _luaTrace(L, method, "Unimplemented"); return 0; } -int32_t apiDiscSetFps(lua_State *L) { - (void)L; - //***REMOVED*** - luaTrace(L, "discSetFPS", "Unimplemented"); - return 0; +static bool _argBoolean(lua_State *L, const char *method, int32_t index) { + if (!lua_isboolean(L, index)) { + _luaDie(L, method, "Argument %d must be a boolean.", index); + } + + return lua_toboolean(L, index) != 0; } -int32_t apiDiscSkipBackward(lua_State *L) { - int32_t n = lua_gettop(L); - int64_t frame = 0; - int64_t aFrame = 0; - bool result = false; - double d = 0; +// Dies unless the argument count is within [minimum, maximum]. +static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum) { + int32_t n = lua_gettop(L); - // If disc is not stopped, seek backwards to given frame. Do not change play/pause state. - - if (!_global.discStopped) { - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); - if (_global.videoHandle >= 0) frame = videoGetFrame(_global.videoHandle) - (int64_t)d; - if (_global.conf->isFrameFile) { - frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &aFrame); - } else { - if (_global.videoHandle >= 0) videoSeek(_global.videoHandle, frame); - } - result = true; - } - } - } else { - luaDie(L, "discSkipBackward", "Failed! Disc is stopped."); - return 0; - } - - if (result) { - luaTrace(L, "discSkipBackward", "%ld", frame); - } else { - luaDie(L, "discSkipBackward", "Failed!"); - } - - return 0; + if ((n < minimum) || (n > maximum)) { + if (minimum == maximum) { + _luaDie(L, method, "Expected %d argument(s), got %d.", minimum, n); + } + _luaDie(L, method, "Expected %d to %d arguments, got %d.", minimum, maximum, n); + } } -int32_t apiDiscSkipBlanking(lua_State *L) { - (void)L; - //***REMOVED*** - luaTrace(L, "discSkipBlanking", "Unimplemented"); - return 0; +static FontT *_argFont(lua_State *L, const char *method, int32_t index) { + int32_t id = _argInteger(L, method, index); + FontT *font = NULL; + + HASH_FIND_INT(_global.fontList, &id, font); + if (!font) { + _luaDie(L, method, "No font at index %d.", id); + } + + return font; } -int32_t apiDiscSkipForward(lua_State *L) { - int32_t n = lua_gettop(L); - int64_t frame = 0; - int64_t aFrame = 0; - bool result = false; - double d = 0; - - // If disc is not stopped, seek ahead to given frame. Do not change play/pause state. - - if (!_global.discStopped) { - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); if (_global.videoHandle >= 0) frame = videoGetFrame(_global.videoHandle) + (int64_t)d; - if (_global.conf->isFrameFile) { - frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &aFrame); - } else { - if (_global.videoHandle >= 0) videoSeek(_global.videoHandle, frame); - } - result = true; - } - } - } else { - luaTrace(L, "discSkipForward", "Failed! Disc is stopped."); - return 0; - } - - if (result) { - luaTrace(L, "discSkipForward", "%ld", frame); - } else { - luaDie(L, "discSkipForward", "Failed!"); - } - - return 0; +static int32_t _argInteger(lua_State *L, const char *method, int32_t index) { + return (int32_t)_argNumber(L, method, index); } -int32_t apiDiscSkipToFrame(lua_State *L) { - int32_t n = lua_gettop(L); - int64_t frame = 0; - int64_t aFrame = 0; - bool result = false; - double d = 0; - - // No matter disc state, seek to given frame and play. - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); frame = (int64_t)d; - if (_global.conf->isFrameFile) { - frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &aFrame); - } else { - if (_global.videoHandle >= 0) videoSeek(_global.videoHandle, frame); - } - if (_global.videoHandle >= 0) videoPlay(_global.videoHandle); - _global.discStopped = false; - result = true; - } - } - - if (result) { - luaTrace(L, "discSkipToFrame", "%ld", frame); - } else { - luaDie(L, "discSkipToFrame", "Failed!"); - } - - return 0; +static int64_t _argInteger64(lua_State *L, const char *method, int32_t index) { + return (int64_t)_argNumber(L, method, index); } -int32_t apiDiscStepBackward(lua_State *L) { - int64_t frame = 0; - int64_t aFrame = 0; +static double _argNumber(lua_State *L, const char *method, int32_t index) { + if (!lua_isnumber(L, index)) { + _luaDie(L, method, "Argument %d must be a number.", index); + } - (void)L; - - // No matter disc state, go back a frame. If playing, pause. - - if (_global.videoHandle >= 0) { - if (_global.conf->isFrameFile) { - frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) - 1; - frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &aFrame); - } else { - frame = videoGetFrame(_global.videoHandle) - 1; - videoSeek(_global.videoHandle, frame); - } - videoPause(_global.videoHandle); - } - luaTrace(L, "discStepBackward", "%ld", frame); - - return 0; + return lua_tonumber(L, index); } -int32_t apiDiscStepForward(lua_State *L) { - int64_t frame = 0; - int64_t aFrame = 0; +static SoundT *_argSound(lua_State *L, const char *method, int32_t index) { + int32_t id = _argInteger(L, method, index); + SoundT *sound = NULL; - (void)L; + HASH_FIND_INT(_global.soundList, &id, sound); + if (!sound) { + _luaDie(L, method, "No sound at index %d.", id); + } - // No matter disc state, go forward a frame. If playing, pause. - - if (_global.videoHandle >= 0) { - if (_global.conf->isFrameFile) { - frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) + 1; - frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &aFrame); - } else { - frame = videoGetFrame(_global.videoHandle) + 1; - videoSeek(_global.videoHandle, frame); - } - videoPause(_global.videoHandle); - } - luaTrace(L, "discStepForward", "%ld", frame); - - return 0; + return sound; } -int32_t apiDiscStop(lua_State *L) { - (void)L; - if (!_global.discStopped) { - if (_global.videoHandle >= 0) videoPause(_global.videoHandle); - _global.discStopped = true; - _global.refreshDisplay = true; - luaTrace(L, "discStop", ""); - } else { - luaTrace(L, "discStop", "Failed! Disc is stopped."); - } - return 0; +static SpriteT *_argSprite(lua_State *L, const char *method, int32_t index) { + int32_t id = _argInteger(L, method, index); + SpriteT *sprite = NULL; + + HASH_FIND_INT(_global.spriteList, &id, sprite); + if (!sprite) { + _luaDie(L, method, "No sprite at index %d.", id); + } + + return sprite; } -int32_t apiFontLoad(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - int32_t points = 0; - const char *name = NULL; - double d = 0; - FontT *font = NULL; +static const char *_argString(lua_State *L, const char *method, int32_t index) { + if (!lua_isstring(L, index)) { + _luaDie(L, method, "Argument %d must be a string.", index); + } - if (n == 2) { - if (lua_isstring(L, 1)) { - if (lua_isnumber(L, 2)) { - name = lua_tostring(L, 1); - d = lua_tonumber(L, 2); points = (int32_t)d; - font = (FontT *)calloc(1, sizeof(FontT)); - if (!font) luaDie(L, "fontLoad", "Unable to allocate new font."); - // Load this font. - font->font = TTF_OpenFont(name, points); - if (!font->font) luaDie(L, "fontLoad", "%s", TTF_GetError()); - // Make it the current font and mark it as loaded. - font->id = _global.nextFontId; - result = _global.nextFontId++; - _global.fontCurrent = font; - HASH_ADD_INT(_global.fontList, id, font); - } - } - } - - if (result >= 0) { - luaTrace(L, "fontLoad", "%s %d", name, result); - } else { - luaDie(L, "fontLoad", "Failed!"); - } - - lua_pushnumber(L, result); - return 1; + return lua_tostring(L, index); } -int32_t apiFontPrint(lua_State *L) { - int32_t n = lua_gettop(L); - const char *message = NULL; - double d = 0; - SDL_Surface *textSurface = NULL; - SDL_Rect dest; +static VideoT *_argVideo(lua_State *L, const char *method, int32_t index) { + int32_t id = _argInteger(L, method, index); + VideoT *video = NULL; - if (n == 3) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isstring(L, 3)) { - if (_global.fontCurrent) { - d = lua_tonumber(L, 1); dest.x = (int32_t)d; - d = lua_tonumber(L, 2); dest.y = (int32_t)d; - textSurface = NULL; - message = lua_tostring(L, 3); - switch (_global.fontQuality) { - case FONT_QUALITY_SOLID: - textSurface = TTF_RenderText_Solid(_global.fontCurrent->font, message, _global.colorForeground); - break; + HASH_FIND_INT(_global.videoList, &id, video); + if (!video) { + _luaDie(L, method, "No video at index %d.", id); + } - case FONT_QUALITY_SHADED: - textSurface = TTF_RenderText_Shaded(_global.fontCurrent->font, message, _global.colorForeground, _global.colorBackground); - break; - - case FONT_QUALITY_BLENDED: - textSurface = TTF_RenderText_Blended(_global.fontCurrent->font, message, _global.colorForeground); - break; - - default: - luaDie(L, "fontPrint", "Unknown font quality!"); - break; - } - if (!textSurface) { - luaDie(L, "fontPrint", "Font surface is null!"); - } else { - SDL_SetColorKey(textSurface, true, 0); - dest.w = textSurface->w; - dest.h = textSurface->h; - SDL_BlitSurface(textSurface, NULL, _global.overlay, &dest); - SDL_FreeSurface(textSurface); - } - } - } - } - } - } - - if (textSurface) { - luaTrace(L, "fontPrint", "%s", message); - } else { - luaDie(L, "fontPrint", "Failed!"); - } - - return 0; + return video; } -int32_t apiFontQuality(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - double d = 0; +// Builds a config for scriptExecute/scriptPush from the games.dat style table at stack index 1. +static ConfigT *_buildConfFromTable(lua_State *L) { + const char *confKey = NULL; + const char *valueString = NULL; + bool valueBoolean = false; + int64_t valueNumber = 0; + ConfigT *c = NULL; - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); _global.fontQuality = (int32_t)d; - result = true; - } - } + // Start with current config. + c = cloneConf(_global.conf); - if (result) { - luaTrace(L, "fontQuality", "%d", _global.fontQuality); - } else { - luaDie(L, "fontQuality", "Failed!"); - } + // Update with data in the table on the top of the Lua stack. + lua_pushnil(L); + while (lua_next(L, 1)) { + // Keys must be strings; converting other keys in place would confuse lua_next. + if (lua_type(L, 2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + confKey = lua_tostring(L, 2); + valueString = NULL; + valueBoolean = false; + valueNumber = 0; - return 0; + // Get value + switch (lua_type(L, 3)) { + case LUA_TSTRING: + valueString = lua_tostring(L, 3); + break; + + case LUA_TBOOLEAN: + valueBoolean = lua_toboolean(L, 3); + break; + + case LUA_TNUMBER: + valueNumber = (int64_t)lua_tonumber(L, 3); + break; + + default: + break; + } + + // Update config with new data + if (strcmp(confKey, "SCRIPT") == 0) { + if (valueString == NULL) { + utilDie("SCRIPT must be a string."); + } + free(c->scriptFile); + c->scriptFile = strdup(valueString); + utilFixPathSeparators(&c->scriptFile, false); + } else if (strcmp(confKey, "VIDEO") == 0) { + if (valueString == NULL) { + utilDie("VIDEO must be a string."); + } + free(c->videoFile); + c->videoFile = strdup(valueString); + utilFixPathSeparators(&c->videoFile, false); + c->isFrameFile = isFrameFileName(c->videoFile); + } else if (strcmp(confKey, "STRETCH") == 0) { + c->stretchVideo = valueBoolean; + } else if (strcmp(confKey, "NO_MOUSE") == 0) { + c->noMouse = valueBoolean; + } else if (strcmp(confKey, "RESOLUTION_X") == 0) { + c->xResolution = (int32_t)valueNumber; + } else if (strcmp(confKey, "RESOLUTION_Y") == 0) { + c->yResolution = (int32_t)valueNumber; + } else if (strcmp(confKey, "SINDEN_GUN") == 0) { + if ((valueString == NULL) || !parseSindenString(valueString, c)) { + c->sindenArgc = 0; + } + } else if (strcmp(confKey, "AUDIO_TRACK") == 0) { + c->audioOutputTrack = (int32_t)valueNumber; + } else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) { + c->legacySpriteArgs = valueBoolean; + } + + // Clean up for next pair + lua_pop(L, 1); + } + + // Create new data dir location based on script location. + free(c->dataDir); + c->dataDir = createDataDir(c->dataDirBase, c->scriptFile); + if (!c->dataDir) { + utilDie("Unable to create data directory for %s.", c->scriptFile); + } + + return c; } -int32_t apiFontSelect(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d = 0; - bool result = false; - FontT *font = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); - id = (int32_t)d; - HASH_FIND_INT(_global.fontList, &id, font); - if (!font) luaDie(L, "fontSelect", "No font at index %d in apiFontSelect.", id); - _global.fontCurrent = font; - result = true; - } - } - - if (result) { - luaTrace(L, "fontSelect", "%d", _global.fontCurrent->id); - } else { - luaDie(L, "fontSelect", "Failed!"); - } - - return 0; -} - - -int32_t apiFontUnload(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - double d; - FontT *font = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our font structure - HASH_FIND_INT(_global.fontList, &id, font); - if (!font) luaDie(L, "fontUnload", "No font at index %d in apiFontUnload.", id); - HASH_DEL(_global.fontList, font); - TTF_CloseFont(font->font); - free(font); - result = true; - } - } - - if (result) { - luaTrace(L, "fontUnload", "%d", id); - } else { - luaDie(L, "fontUnload", "Failed!"); - } - - return 0; -} - - -int32_t apiFontToSprite(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - const char *message = NULL; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isstring(L, 1)) { - if (_global.fontCurrent) { - // Create spirte - sprite = (SpriteT *)calloc(1, sizeof(SpriteT)); - if (!sprite) luaDie(L, "fontToSprite", "Unable to allocate new text sprite."); - message = lua_tostring(L, 1); - switch (_global.fontQuality) { - case 1: - sprite->originalSurface = TTF_RenderText_Solid(_global.fontCurrent->font, message, _global.colorForeground); - break; - - case 2: - sprite->originalSurface = TTF_RenderText_Shaded(_global.fontCurrent->font, message, _global.colorForeground, _global.colorBackground); - break; - - case 3: - sprite->originalSurface = TTF_RenderText_Blended(_global.fontCurrent->font, message, _global.colorForeground); - break; - - default: - luaDie(L, "fontToSprite", "Unknown font quality!"); - break; - } - - if (!sprite->originalSurface) { - luaDie(L, "fontToSprite", "Font surface is null!"); - } else { - SDL_SetColorKey(sprite->originalSurface, true, 0); - sprite->surface = surfaceCopy(sprite->originalSurface); - sprite->scaleX = 1.0; - sprite->scaleY = 1.0; - sprite->id = _global.nextSpriteId; - result = _global.nextSpriteId++; - HASH_ADD_INT(_global.spriteList, id, sprite); - } - } - } - } - - if (sprite->surface) { - luaTrace(L, "fontToSprite", "%d %s", result, message); - } else { - luaDie(L, "fontToSprite", "Failed!"); - } - - lua_pushinteger(L, result); - return 1; -} - - -int32_t apiOverlayBox(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t x1 = 0; - int32_t y1 = 0; - int32_t x2 = 0; - int32_t y2 = 0; - //SDL_Rect r; - double d = 0; - bool result = false; - - if (n == 4) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 1); x1 = (int32_t)d; - d = lua_tonumber(L, 2); y1 = (int32_t)d; - d = lua_tonumber(L, 3); x2 = (int32_t)d; - d = lua_tonumber(L, 4); y2 = (int32_t)d; - - /* - r.x = x1; - r.y = y1; - r.w = abs(x2 - x1) + 1; - r.h = abs(y2 - y1) + 1; - */ - - SDL_LockSurface(_global.overlay); - //***TODO*** No filling until I can find an efficient way to blend individual pixels. - //SDL_FillRect(_global.overlay, &r, SDL_MapRGBA(_global.overlay->format, _global.colorBackground.r, _global.colorBackground.g, _global.colorBackground.b, _global.colorBackground.a)); - line(x1, y1, x2, y1, &_global.colorForeground); - line(x2, y1, x2, y2, &_global.colorForeground); - line(x2, y2, x1, y2, &_global.colorForeground); - line(x1, y2, x1, y1, &_global.colorForeground); - SDL_UnlockSurface(_global.overlay); - result = true; - } - } - } - } - } - - if (result) { - luaTrace(L, "overlayBox", "%d %d %d %d", x1, y1, x2, y2); - } else { - luaDie(L, "overlayBox", "Failed!"); - } - - return 0; - -} - - -int32_t apiOverlayCircle(lua_State *L) { - - int32_t n = lua_gettop(L); - int32_t x0 = 0; - int32_t y0 = 0; - int32_t r = 0; - int32_t x = 0; - int32_t y = 0; - int32_t ro = 0; - int32_t xo = 0; - int32_t yo = 0; - int32_t dx = 1; - int32_t dy = 1; - int32_t err = 0; - double d = 0; - bool result = false; - - if (n == 3) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 1); xo = x = (int32_t)d; - d = lua_tonumber(L, 2); yo = y = (int32_t)d; - d = lua_tonumber(L, 3); ro = r = (int32_t)d; - - x = r - 1; - err = dx - (int32_t)(r << 1); - - SDL_LockSurface(_global.overlay); - - while (x >= y) { - putPixel(x0 + x, y0 + y, &_global.colorForeground); - putPixel(x0 + y, y0 + x, &_global.colorForeground); - putPixel(x0 - y, y0 + x, &_global.colorForeground); - putPixel(x0 - x, y0 + y, &_global.colorForeground); - putPixel(x0 - x, y0 - y, &_global.colorForeground); - putPixel(x0 - y, y0 - x, &_global.colorForeground); - putPixel(x0 + y, y0 - x, &_global.colorForeground); - putPixel(x0 + x, y0 - y, &_global.colorForeground); - - if (err <= 0) { - y++; - err += dy; - dy += 2; - } - - if (err > 0) { - x--; - dx += 2; - err += dx - (r << 1); - } - } - - SDL_UnlockSurface(_global.overlay); - result = true; - - } - } - } - } - - if (result) { - luaTrace(L, "overlayCircle", "%d %d %d", xo, yo, ro); - } else { - luaDie(L, "overlayCircle", "Failed!"); - } - - return 0; -} - - -int32_t apiOverlayClear(lua_State *L) { - (void)L; - SDL_FillRect(_global.overlay, NULL, SDL_MapRGBA(_global.overlay->format, _global.colorBackground.r, _global.colorBackground.g, _global.colorBackground.b, _global.colorBackground.a)); - luaTrace(L, "overlayClear", ""); - return 0; -} - - -int32_t apiOverlayEllipse(lua_State *L) { - - int32_t n = lua_gettop(L); - int32_t x0 = 0; - int32_t y0 = 0; - int32_t x1 = 0; - int32_t y1 = 0; - int32_t x0o = 0; - int32_t y0o = 0; - int32_t x1o = 0; - int32_t y1o = 0; - int32_t a = 0; - int32_t b = 0; - int32_t b1 = 0; - int32_t dx = 0; - int32_t dy = 0; - int32_t err = 0; - int32_t e2 = 0; - double d = 0; - bool result = false; - - if (n == 4) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 1); x0o = x0 = (int32_t)d; - d = lua_tonumber(L, 2); y0o = y0 = (int32_t)d; - d = lua_tonumber(L, 3); x1o = x1 = (int32_t)d; - d = lua_tonumber(L, 4); y1o = y1 = (int32_t)d; - - SDL_LockSurface(_global.overlay); - - a = abs(x1 - x0); - b = abs(y1 - y0); - b1 = b & 1; // values of diameter - dx = 4 * (1 - a) * b * b; - dy = 4 * (b1 + 1) * a * a; // error increment - err = dx + dy + b1 * a * a; - - if (x0 > x1) { // if called with swapped points - x0 = x1; - x1 += a; - } - - if (y0 > y1) { //exchange them - y0 = y1; - } - - y0 += (b + 1) / 2; // starting pixel - y1 = y0 - b1; - a *= 8 * a; - b1 = 8 * b * b; - - do { - putPixel(x1, y0, &_global.colorForeground); // I. Quadrant - putPixel(x0, y0, &_global.colorForeground); // II. Quadrant - putPixel(x0, y1, &_global.colorForeground); // III. Quadrant - putPixel(x1, y1, &_global.colorForeground); // IV. Quadrant - e2 = 2 * err; - if (e2 <= dy) { // y step - y0++; - y1--; - err += dy += a; - } - if (e2 >= dx || 2*err > dy) { // x step - x0++; - x1--; - err += dx += b1; - } - } while (x0 <= x1); - - while (y0-y1 < b) { // too early stop of flat ellipses a = 1 - putPixel(x0-1, y0, &_global.colorForeground); // -> finish tip of ellipse - putPixel(x1+1, y0++, &_global.colorForeground); - putPixel(x0-1, y1, &_global.colorForeground); - putPixel(x1+1, y1--, &_global.colorForeground); - } - - SDL_UnlockSurface(_global.overlay); - result = true; - } - } - } - } - } - - if (result) { - luaTrace(L, "overlayEllipse", "%d %d %d %d", x0o, y0o, x1o, y1o); - } else { - luaDie(L, "overlayEllipse", "Failed!"); - } - - return 0; -} - - -int32_t apiOverlayGetHeight(lua_State *L) { - luaTrace(L, "overlayGetHeight", "%d", _global.overlay->h); - lua_pushinteger(L, _global.overlay->h); - return 1; -} - - -int32_t apiOverlayGetWidth(lua_State *L) { - luaTrace(L, "overlayGetWidth", "%d", _global.overlay->w); - lua_pushinteger(L, _global.overlay->w); - return 1; -} - - -int32_t apiOverlayLine(lua_State *L) { - - int32_t n = lua_gettop(L); - int32_t x1 = 0; - int32_t y1 = 0; - int32_t x2 = 0; - int32_t y2 = 0; - double d = 0; - bool result = false; - - if (n == 4) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 1); x1 = (int32_t)d; - d = lua_tonumber(L, 2); y1 = (int32_t)d; - d = lua_tonumber(L, 3); x2 = (int32_t)d; - d = lua_tonumber(L, 4); y2 = (int32_t)d; - - SDL_LockSurface(_global.overlay); - line(x1, y1, x2, y2, &_global.colorForeground); - SDL_UnlockSurface(_global.overlay); - result = true; - } - } - } - } - } - - if (result) { - luaTrace(L, "overlayLine", "%d %d %d %d", x1, y1, x2, y2); - } else { - luaDie(L, "overlayLine", "Failed!"); - } - - return 0; -} - - -int32_t apiOverlayPlot(lua_State *L) { - - int32_t n = lua_gettop(L); - int32_t x1 = 0; - int32_t y1 = 0; - double d = 0; - bool result = false; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); x1 = (int32_t)d; - d = lua_tonumber(L, 2); y1 = (int32_t)d; - - SDL_LockSurface(_global.overlay); - putPixel(x1, y1, &_global.colorForeground); - SDL_UnlockSurface(_global.overlay); - result = true; - } - } - } - - if (result) { - luaTrace(L, "overlayPlot", "%d %d", x1, y1); - } else { - luaDie(L, "overlayPlot", "Failed!"); - } - - return 0; -} - - -int32_t apiOverlayPrint(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t i = 0; - char *s = NULL; - int32_t length = 0; - bool result = false; - SDL_Rect src; - SDL_Rect dst; - - if (n == 3) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isstring(L, 3)) { - src.y = 0; - src.w = _global.consoleFontWidth; - src.h = _global.consoleFontHeight; - dst.x = lua_tonumber(L, 1) * _global.consoleFontWidth; - dst.y = lua_tonumber(L, 2) * _global.consoleFontHeight; - dst.w = _global.consoleFontWidth; - dst.h = _global.consoleFontHeight; - s = (char *)lua_tostring(L, 3); - if (strlen(s) < (uint32_t)((_global.overlay->w - dst.x) / _global.consoleFontWidth)) { - length = strlen(s); - } else { - length = (_global.overlay->w - dst.x) / _global.consoleFontWidth; - } - for (i=0; ichunk = Mix_LoadWAV(name); - if (!sound->chunk) luaDie(L, "soundLoad", "%s", Mix_GetError()); - sound->id = _global.nextSoundId; - result = _global.nextSoundId++; - HASH_ADD_INT(_global.soundList, id, sound); - } - } - - if (sound) { - luaTrace(L, "soundLoad", "%d %s", result, name); - } else { - luaDie(L, "soundLoad", "Failed!"); - } - - lua_pushnumber(L, result); - return 1; -} - - -int32_t apiSoundPlay(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t r = -1; - int32_t id = -1; - double d = 0; - bool result = false; - SoundT *sound = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sound structure - HASH_FIND_INT(_global.soundList, &id, sound); - if (!sound) luaDie(L, "soundPlay", "No sound at index %d in apiSoundPlay.", id); - // Play it (can gracefully fail if we run out of channels) - r = Mix_PlayChannel(-1, sound->chunk, 0); - if (r >= 0) { - Mix_Volume(r, _global.effectsVolume * 2); - } - result = true; - } - } - - if (result) { - luaTrace(L, "soundPlay", "%d", r); - } else { - luaDie(L, "soundPlay", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiSoundPause(lua_State *L) { - // Instructs Daphne to pause a given sample from playing. - // User must feed the sound handle on the lua side. - // e.g. lua code, - // - // thisHandle = soundPlay(mySound) - // soundPause(thisHandle) - // - // Function returns true if sample was paused, false otherwise. - // - // --rdg - - int32_t n = lua_gettop(L); - int32_t channel = -1; - double d = 0; - bool r = false; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); channel = (int32_t)d; - r = Mix_Playing(channel); - Mix_Pause(channel); - result = true; - } - } - - if (result) { - luaTrace(L, "soundPause", "%d %d", channel, r); - } else { - luaDie(L, "soundPause", "Failed!"); - } - - lua_pushboolean(L, r); - return 1; -} - - -int32_t apiSoundResume(lua_State *L) { - // Instructs Daphne to unpause a sound that was previously paused. - // User must feed the sound handle on the lua side. - // e.g. lua code, - // - // thisHandle = soundPlay(mySound) - // soundPause(thisHandle) - // soundResume(thisHandle) - // - // Function returns true if sample was unpaused, false otherwise. - // - // --rdg - - int32_t n = lua_gettop(L); - int32_t channel = -1; - double d = 0; - bool r = false; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); channel = (int32_t)d; - r = Mix_Paused(channel); - Mix_Resume(channel); - result = true; - } - } - - if (result) { - luaTrace(L, "soundResume", "%d %d", channel, r); - } else { - luaDie(L, "soundResume", "Failed!"); - } - - lua_pushboolean(L, r); - return 1; -} - - -int32_t apiSoundIsPlaying(lua_State *L) { - // Checks to see if a certain sound has finished playing. - // User must feed the sound handle on the lua side. - // e.g. lua code, - // - // thisHandle = soundPlay(mySound) - // if (soundIsPlaying(thisSound)) then do something ... end - // - // Function returns true if sample is still playing, false otherwise. - // - // --rdg - - int32_t n = lua_gettop(L); - int32_t channel = -1; - double d = 0; - bool r = false; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); channel = (int32_t)d; - r = (bool)Mix_Playing(channel); - result = true; - } - } - - if (result) { - luaTrace(L, "soundIsPlaying", "%d %d", channel, r); - } else { - luaDie(L, "soundIsPlaying", "Failed!"); - } - - lua_pushboolean(L, r); - return 1; -} - - -int32_t apiSoundStop(lua_State *L) { - // Instructs Daphne to end a sound early. - // User must feed the sound handle on the lua side. - // e.g. lua code, - // - // thisHandle = soundPlay(mySound) - // soundStop(thisHandle) - // - // Function returns true if sample was stopped, false otherwise. - // NOTE: thisHandle will be invalidated as a result of this function. - // Lua doesn't do variables by reference, so it is - // up to the user to keep track of sound handles on the lua script. - // - // --rdg - - int32_t n = lua_gettop(L); - int32_t channel = -1; - double d = 0; - bool r = false; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); channel = (int32_t)d; - r = Mix_Playing(channel); - Mix_HaltChannel(channel); - result = true; - } - } - - if (result) { - luaTrace(L, "soundStop", "%d %d", channel, r); - } else { - luaDie(L, "soundStop", "Failed!"); - } - - lua_pushboolean(L, r); - return 1; -} - - -int32_t apiSoundSetVolume(lua_State *L) { - // Allows manipulation of sample volume. - // Valid values range from 0 to 63 - // e.g. lua code, - // - // soundSetVolume(32) - // - // Function returns nothing. - // - // --rdg - - int32_t n = lua_gettop(L); - int32_t thisValue = 0; - double d = 0; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); thisValue = (int32_t)d; - if (thisValue >= 0 && thisValue <= AUDIO_MAX_VOLUME) { - _global.effectsVolume = thisValue; - Mix_Volume(-1, _global.effectsVolume * 2); - } else { - luaDie(L, "soundSetVolume", "Invalid sound volume value."); - } - } - result = true; - } - - if (result) { - luaTrace(L, "soundSetVolume", "%d", _global.effectsVolume); - } else { - luaDie(L, "soundSetVolume", "Failed!"); - } - - return 0; -} - - -int32_t apiSoundGetVolume(lua_State *L) { - // Returns the current sample volume value. - // e.g. lua code, - // - // local iVolume = soundGetVolume() - // - // Function returns an integer value ranging from 0 to 63. - // - // --rdg - - luaTrace(L, "soundGetVolume", "%d", _global.effectsVolume); - - lua_pushinteger(L, _global.effectsVolume); - return 1; -} - - -int32_t apiSoundFullStop(lua_State *L) { - // Clears the audio queue of any samples actively playing. - // No parameters needed. Function returns nothing. - // e.g. lua code, - // - // soundFullStop() - - (void)L; - - luaTrace(L, "soundFullStop", ""); - - Mix_HaltChannel(-1); - return 0; -} - - -int32_t apiSoundUnload(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - double d; - SoundT *sound = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sound structure - HASH_FIND_INT(_global.soundList, &id, sound); - if (!sound) luaDie(L, "soundUnload", "No sound at index %d in apiSoundUnload.", id); - HASH_DEL(_global.soundList, sound); - Mix_FreeChunk(sound->chunk); - free(sound); - result = true; - } - } - - if (result) { - luaTrace(L, "soundUnload", "%d", id); - } else { - luaDie(L, "soundUnload", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteDraw(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d = 0; - bool center = false; - bool newFrame = false; - SpriteT *sprite = NULL; - SDL_Rect dest; - int ox; - int oy; - - // spriteDraw(x, y, id) - Simple draw - // spriteDraw(x, y, c, id) - Centered draw - // spriteDraw(x, y, x2, y2, id) - Stretched draw - // spriteDraw(x, y, x2, y2, c, id) - Centered Stretched draw - - if ((n >= 3) && (n <= 6)) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); dest.x = (int32_t)d; - d = lua_tonumber(L, 2); dest.y = (int32_t)d; - // Centered? - if ((n == 3) || (n == 4)) { - if (n == 4) { - if (lua_isboolean(L, 3) && lua_isnumber(L, 4)) { - d = lua_toboolean(L, 3); center = (int32_t)d != 0; - d = lua_tonumber(L, 4); id = (int32_t)d; - } - } else { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 3); id = (int32_t)d; - } - } - } - if ((n == 5) || (n == 6)) { - // Target is scaled - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 3); dest.w = (int32_t)d - dest.x + 1; - d = lua_tonumber(L, 4); dest.h = (int32_t)d - dest.y + 1; - // Centered? - if (n == 6) { - if (lua_isboolean(L, 5) && lua_isnumber(L, 6)) { - d = lua_toboolean(L, 5); center = (int32_t)d != 0; - d = lua_tonumber(L, 6); id = (int32_t)d; - } - } else { - if (lua_isnumber(L, 6)) { - d = lua_tonumber(L, 5); id = (int32_t)d; - } - } - } - } - //utilSay("spriteDraw: x=%d y=%d c=%d id=%d", dest.x, dest.y, center, id); - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteDraw", "No sprite at index %d in apiSpriteDraw.", id); - // Figure out animation frame, if needed - if (sprite->animation != NULL && sprite->animating) { - // Find time passed - sprite->ticks += SDL_GetTicks() - sprite->lastTick; - sprite->lastTick = SDL_GetTicks(); - // Find desired frame - while (sprite->animating) { - if (sprite->animation->delays[sprite->currentFrame] < sprite->ticks) { - sprite->ticks -= sprite->animation->delays[sprite->currentFrame]; - sprite->currentFrame++; - newFrame = true; - if (sprite->currentFrame >= sprite->animation->count) { - if (sprite->loop) { - sprite->currentFrame = 0; - } else { - sprite->currentFrame = sprite->animation->count - 1; - sprite->animating = false; - } - } - } else { - break; - } - } - if (newFrame) { - SDL_FreeSurface(sprite->originalSurface); - sprite->originalSurface = surfaceCopy(sprite->animation->frames[sprite->currentFrame]); - SDL_FreeSurface(sprite->surface); - sprite->surface = rotozoomSurfaceXY(sprite->originalSurface, 360 - sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth); - } - } - if ((n == 3) || (n == 4)) { - // No scaling, find width - dest.w = sprite->surface->w; - dest.h = sprite->surface->h; - } - if (center) { - // Move sprite so the drawing coordinate is the center of the sprite - dest.x -= dest.w * 0.5; - dest.y -= dest.h * 0.5; - } - if ((n == 3) || (n == 4)) { - // No scaling - SDL_BlitSurface(sprite->surface, NULL, _global.overlay, &dest); - } else { - // Scaled - SDL_BlitScaled(sprite->surface, NULL, _global.overlay, &dest); - } - } - } - } - - if (id >= 0) { - luaTrace(L, "spriteDraw", "%d %d %d %d %d %d", id, dest.x, dest.y, dest.w, dest.h, center); - } else { - luaDie(L, "spriteDraw", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteGetFrame(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - int32_t id = -1; - double d; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isstring(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sprite structure - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteGetFrame", "No sprite at index %d in apiSpriteGetFrame.", id); - result = sprite->currentFrame; +// Calls a global Lua function. sig lists argument types (d, i, s), then '>' and result types. +static void _callLua(const char *func, const char *sig, ...) { + va_list vl; + bool done = false; + int32_t narg = 0; + int32_t nres = 0; + int32_t handler = 0; + double d = 0; + const int32_t top = lua_gettop(_global.luaContext); + + // Get Function + lua_getglobal(_global.luaContext, func); + if (!lua_isfunction(_global.luaContext, -1)) { + // Function does not exist. Bail. + lua_settop(_global.luaContext, top); + return; + } + + if (_global.conf->scriptTracing) { + utilTrace("%s", func); + } + + // Traceback handler sits below the function. + lua_pushcfunction(_global.luaContext, _luaTraceback); + lua_insert(_global.luaContext, -2); + handler = lua_gettop(_global.luaContext) - 1; + + // Push Arguments + va_start(vl, sig); + while ((*sig) && (!done)) { + switch (*sig++) { + case 'd': // Double + lua_pushnumber(_global.luaContext, va_arg(vl, double)); + break; + + case 'i': // Int + lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Promoted type for varargs. + break; + + case 's': // String + lua_pushstring(_global.luaContext, va_arg(vl, char *)); + break; + + case '>': + done = true; + break; + + default: + utilDie("Invalid argument option (%c)", *(sig - 1)); + } + if (!done) { + narg++; + luaL_checkstack(_global.luaContext, 1, "Too many arguments"); } } - if (result >= 0) { - luaTrace(L, "spriteGetFrame", "%d", result); - } else { - luaDie(L, "spriteGetFrame", "Failed!"); + // Do the call. Script errors are fatal, like every other error in Singe. + nres = (int32_t)strlen(sig); + if (lua_pcall(_global.luaContext, narg, nres, handler) != 0) { + utilDie("Error executing function '%s': %s", func, lua_tostring(_global.luaContext, -1)); } - lua_pushinteger(L, result); - return 1; + // Retrieve results + nres = -nres; // Stack index of first result + while (*sig) { + switch (*sig++) { + case 'd': // Double + *va_arg(vl, double *) = lua_tonumber(_global.luaContext, nres); + break; + + case 'i': // Int - nil or non-numbers read as zero. + d = lua_tonumber(_global.luaContext, nres); + *va_arg(vl, int32_t *) = (int32_t)d; + break; + + default: + utilDie("Invalid option (%c)", *(sig - 1)); + } + nres++; + } + va_end(vl); + + lua_settop(_global.luaContext, top); } -int32_t apiSpriteGetHeight(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - int32_t id = -1; - double d; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isstring(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sprite structure - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteGetHeight", "No sprite at index %d in apiSpriteGetHeight.", id); - result = sprite->surface->h; - } - } - - if (result >= 0) { - luaTrace(L, "spriteGetHeight", "%d", result); - } else { - luaDie(L, "spriteGetHeight", "Failed!"); - } - - lua_pushinteger(L, result); - return 1; +// SDL_mixer calls this from the audio thread (or under its lock). Just queue the channel. +static void _channelFinished(int channel) { + if (_global.soundQueueCount < SOUND_QUEUE_SIZE) { + _global.soundQueue[_global.soundQueueCount++] = channel; + } } -int32_t apiSpriteGetWidth(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - int32_t id = -1; - double d; - SpriteT *sprite = NULL; +// Maps an SDL joystick instance ID to our controller slot, or -1. +static int32_t _controllerSlot(SDL_JoystickID which) { + int32_t x = 0; - if (n == 1) { - if (lua_isstring(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sprite structure - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteGetWidth", "No sprite at index %d in apiSpriteGetWidth.", id); - result = sprite->surface->w; - } - } - - if (result >= 0) { - luaTrace(L, "spriteGetWidth", "%d", result); - } else { - luaDie(L, "spriteGetWidth", "Failed!"); - } - - lua_pushinteger(L, result); - return 1; -} - - -int32_t apiSpriteIsPlaying(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - int32_t id = -1; - double d; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isstring(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sprite structure - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteIsPlaying", "No sprite at index %d in apiSpriteIsPlaying.", id); - result = sprite->animating; + for (x = 0; x < MAX_CONTROLLERS; x++) { + if ((_global.controllers[x] != NULL) && (SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(_global.controllers[x])) == which)) { + return x; } } - if (result >= 0) { - luaTrace(L, "spriteIsPlaying", "%d", result); - } else { - luaDie(L, "spriteIsPlaying", "Failed!"); + return -1; +} + + +// Sleeps while keeping the window responsive. Returns false if the user asked to quit. +static bool _delayAndPump(uint32_t ms) { + SDL_Event event; + uint32_t until = SDL_GetTicks() + ms; + + do { + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + _global.running = false; + } + } + SDL_Delay(IDLE_SLEEP_MS); + } while (SDL_GetTicks() < until); + + return _global.running; +} + + +// Hands a key, button, or axis direction to the script, tracking what it now believes is held. +static void _deliverKey(bool down, int32_t keysym, int32_t scancode) { + int32_t move = 0; + int32_t index = 0; + + if (_global.keyboardMode == KEYBOARD_FULL) { + _heldListUpdate(_global.heldKeys, &_global.heldKeyCount, down, keysym, scancode); + _callLua(down ? "onInputPressed" : "onInputReleased", "i", keysym); + _callLua(down ? "onKeyPressed" : "onKeyReleased", "ii", keysym, scancode); + return; } - lua_pushboolean(L, result); - return 1; + // Mappable switches. The pause switch belongs to the engine while its key is enabled. + for (move = 0; move < INPUT_COUNT; move++) { + if ((move == INPUT_PAUSE) && _global.pauseEnabled) { + continue; + } + for (index = 0; index < _global.controlMappings[move].inputCount; index++) { + if (_global.controlMappings[move].input[index] == scancode) { + _global.switchHeld[move] = down; + _callLua(down ? "onInputPressed" : "onInputReleased", "i", move); + } + } + } } -int32_t apiSpriteLoad(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - const char *name = NULL; - SpriteT *sprite = NULL; - int32_t x = 0; +// Seeks the laserdisc, whichever kind it is. +static void _discSeek(int64_t frame) { + int64_t actualFrame = 0; - if (n == 1) { - if (lua_isstring(L, 1)) { - sprite = (SpriteT *)calloc(1, sizeof(SpriteT)); - if (!sprite) luaDie(L, "spriteLoad", "Unable to allocate new sprite."); - name = lua_tostring(L, 1); - // Try to load requested file as an animation first - sprite->animation = IMG_LoadAnimation(name); - if (sprite->animation) { - // We got something - if (sprite->animation->count < 2) { - // Only one frame - meh - IMG_FreeAnimation(sprite->animation); - sprite->animation = NULL; - } else { - // Set up our transparency - for (x=0; xanimation->count; x++) { - SDL_SetColorKey(sprite->animation->frames[x], true, 0); - } - // Load first frame onto surface - sprite->originalSurface = surfaceCopy(sprite->animation->frames[0]); - } - } - if (sprite->animation == NULL) { - // Load as single image - sprite->originalSurface = IMG_Load(name); - } - if (!sprite->originalSurface) luaDie(L, "spriteLoad", "%s", IMG_GetError()); - SDL_SetColorKey(sprite->originalSurface, true, 0); - sprite->surface = surfaceCopy(sprite->originalSurface); - sprite->scaleX = 1.0; - sprite->scaleY = 1.0; - sprite->id = _global.nextSpriteId; - result = _global.nextSpriteId++; - HASH_ADD_INT(_global.spriteList, id, sprite); - } - } - - if (sprite->originalSurface) { - luaTrace(L, "spriteLoad", "%d %s", result, name); - } else { - luaDie(L, "spriteLoad", "Failed!"); - } - - lua_pushnumber(L, result); - return 1; + if (_global.conf->isFrameFile) { + frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &actualFrame); + } else { + if (_global.videoHandle >= 0) { + videoSeek(_global.videoHandle, frame); + } + } } -int32_t apiSpriteLoop(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - int32_t l; - double d; - SpriteT *sprite = NULL; +// Indexing progress display. INDEX_DISPLAY_START sets up, 0..100 animates, INDEX_DISPLAY_STOP tears down. +static void _doIndexDisplay(int32_t percent) { + static int32_t oldW = 0; + static int32_t oldH = 0; + static int32_t radius = 0; + static int32_t angle = 0; + static uint32_t nextUpdate = 0; + static SDL_Surface *surfDisc = NULL; + static SDL_Surface *surfGlass = NULL; + static SDL_Surface *surfIndex = NULL; + static SDL_Texture *texDisc = NULL; + static SDL_Texture *texGlass = NULL; + static SDL_Texture *texIndex = NULL; + SDL_Rect target; + int32_t vShift = 0; - if (n == 2) { - if (lua_isboolean(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_toboolean(L, 1); l = (int32_t)d; - d = lua_tonumber(L, 2); id = (int32_t)d; - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteLoop", "No sprite at index %d in apiSpriteLoop.", id); - sprite->loop = l; + (void)percent; + + if (percent == INDEX_DISPLAY_START) { + SDL_RenderGetLogicalSize(_global.renderer, &oldW, &oldH); + SDL_RenderSetLogicalSize(_global.renderer, INDEX_SCREEN_WIDTH, INDEX_SCREEN_HEIGHT); + texGlass = _loadEmbeddedTexture(magnifyingGlass_png, magnifyingGlass_png_len, &surfGlass); + texDisc = _loadEmbeddedTexture(laserDisc_png, laserDisc_png_len, &surfDisc); + texIndex = _loadEmbeddedTexture(indexing_png, indexing_png_len, &surfIndex); + radius = (int32_t)(surfDisc->w * INDEX_RADIUS_FACTOR); + nextUpdate = SDL_GetTicks() + INDEX_UPDATE_MS; + return; + } + + if (percent == INDEX_DISPLAY_STOP) { + SDL_DestroyTexture(texDisc); + SDL_DestroyTexture(texGlass); + SDL_DestroyTexture(texIndex); + SDL_FreeSurface(surfDisc); + SDL_FreeSurface(surfGlass); + SDL_FreeSurface(surfIndex); + texDisc = NULL; + texGlass = NULL; + texIndex = NULL; + surfDisc = NULL; + surfGlass = NULL; + surfIndex = NULL; + SDL_RenderSetLogicalSize(_global.renderer, oldW, oldH); + SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(_global.renderer); + SDL_RenderPresent(_global.renderer); + return; + } + + // Display animation + SDL_RenderSetLogicalSize(_global.renderer, INDEX_SCREEN_WIDTH, INDEX_SCREEN_HEIGHT); + SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(_global.renderer); + + // Draw "Indexing" text + vShift = surfIndex->h - INDEX_TEXT_OVERLAP; + target.x = (INDEX_SCREEN_WIDTH - surfIndex->w) / 2; + target.y = INDEX_SCREEN_HEIGHT - vShift; + target.w = surfIndex->w; + target.h = surfIndex->h; + SDL_RenderCopy(_global.renderer, texIndex, NULL, &target); + + // Draw laserdisc + target.x = (INDEX_SCREEN_WIDTH - surfDisc->w) / 2; + target.y = (INDEX_SCREEN_HEIGHT - surfDisc->h) / 2 - vShift; + target.w = surfDisc->w; + target.h = surfDisc->h; + SDL_RenderCopy(_global.renderer, texDisc, NULL, &target); + + // Draw magnifying glass circling the disc + target.x = (int32_t)(surfDisc->w * INDEX_GLASS_X_FACTOR + (INDEX_SCREEN_WIDTH - surfGlass->w) / 2 + cos(angle * M_PI / (DEGREES_PER_CIRCLE / 2)) * radius); + target.y = (int32_t)(surfDisc->h * INDEX_GLASS_Y_FACTOR + (INDEX_SCREEN_HEIGHT - surfGlass->h) / 2 - vShift + sin(angle * M_PI / (DEGREES_PER_CIRCLE / 2)) * radius); + target.w = surfGlass->w; + target.h = surfGlass->h; + SDL_RenderCopy(_global.renderer, texGlass, NULL, &target); + + // Update animation + if (SDL_GetTicks() > nextUpdate) { + angle = (angle + 1) % (int32_t)DEGREES_PER_CIRCLE; + nextUpdate = SDL_GetTicks() + INDEX_UPDATE_MS; + } + + SDL_RenderPresent(_global.renderer); +} + + +// Splash screens: fade in the Kangaroo Punch logo, cross fade to the Singe logo, fade out. +static void _doLogos(void) { + int32_t i = 0; + int32_t w = 0; + int32_t h = 0; + bool keepGoing = true; + SDL_Surface *surfKangaroo = NULL; + SDL_Surface *surfSinge = NULL; + SDL_Texture *texKangaroo = NULL; + SDL_Texture *texSinge = NULL; + + SDL_RenderGetLogicalSize(_global.renderer, &w, &h); + texKangaroo = _loadEmbeddedTexture(kangarooPunchLogo_png, kangarooPunchLogo_png_len, &surfKangaroo); + texSinge = _loadEmbeddedTexture(singeLogo_png, singeLogo_png_len, &surfSinge); + + // Fade in to white with Kangaroo logo + SDL_RenderSetLogicalSize(_global.renderer, surfKangaroo->w, surfKangaroo->h); + for (i = 0; keepGoing && (i < LOGO_FADE_STEPS); i++) { + SDL_SetRenderDrawColor(_global.renderer, (uint8_t)i, (uint8_t)i, (uint8_t)i, SDL_ALPHA_OPAQUE); + SDL_RenderClear(_global.renderer); + SDL_SetTextureAlphaMod(texKangaroo, (uint8_t)i); + SDL_RenderCopy(_global.renderer, texKangaroo, NULL, NULL); + SDL_RenderPresent(_global.renderer); + keepGoing = _delayAndPump(LOGO_FADE_STEP_MS); + } + keepGoing = keepGoing && _delayAndPump(LOGO_HOLD_MS); + + // Cross fade to Singe logo + for (i = 0; keepGoing && (i < LOGO_FADE_STEPS); i++) { + SDL_RenderClear(_global.renderer); + SDL_SetTextureAlphaMod(texKangaroo, (uint8_t)(LOGO_FADE_STEPS - 1 - i)); + SDL_RenderCopy(_global.renderer, texKangaroo, NULL, NULL); + SDL_SetTextureAlphaMod(texSinge, (uint8_t)i); + SDL_RenderCopy(_global.renderer, texSinge, NULL, NULL); + SDL_RenderPresent(_global.renderer); + keepGoing = _delayAndPump(LOGO_FADE_STEP_MS); + } + keepGoing = keepGoing && _delayAndPump(LOGO_HOLD_MS); + + // Fade to black + SDL_RenderSetLogicalSize(_global.renderer, surfSinge->w, surfSinge->h); + for (i = LOGO_FADE_STEPS - 1; keepGoing && (i >= 0); i--) { + SDL_SetRenderDrawColor(_global.renderer, (uint8_t)i, (uint8_t)i, (uint8_t)i, SDL_ALPHA_OPAQUE); + SDL_RenderClear(_global.renderer); + SDL_SetTextureAlphaMod(texSinge, (uint8_t)i); + SDL_RenderCopy(_global.renderer, texSinge, NULL, NULL); + SDL_RenderPresent(_global.renderer); + keepGoing = _delayAndPump(LOGO_FADE_STEP_MS); + } + + SDL_DestroyTexture(texSinge); + SDL_DestroyTexture(texKangaroo); + SDL_FreeSurface(surfSinge); + SDL_FreeSurface(surfKangaroo); + SDL_RenderSetLogicalSize(_global.renderer, w, h); +} + + +// Bresenham line into the overlay. The overlay must be locked by the caller. +static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel) { + int32_t x = x1; + int32_t y = y1; + int32_t dx = abs(x2 - x1); + int32_t dy = abs(y2 - y1); + int32_t incX = (x2 >= x1) ? 1 : -1; + int32_t incY = (y2 >= y1) ? 1 : -1; + int32_t balance = 0; + + if (dx >= dy) { + dy <<= 1; + balance = dy - dx; + dx <<= 1; + while (x != x2) { + _putPixel(x, y, pixel); + if (balance >= 0) { + y += incY; + balance -= dx; + } + balance += dy; + x += incX; + } + } else { + dx <<= 1; + balance = dx - dy; + dy <<= 1; + while (y != y2) { + _putPixel(x, y, pixel); + if (balance >= 0) { + x += incX; + balance -= dy; + } + balance += dx; + y += incY; + } + } + _putPixel(x, y, pixel); +} + + +// Draws the PAUSED indicator, built from the console font on first use, centered on the target. +static void _drawPauseIndicator(const SDL_Rect *target) { + SDL_Surface *text = NULL; + SDL_Rect src; + SDL_Rect dest; + int32_t i = 0; + + if (_global.pauseTexture == NULL) { + _global.pauseTextureWidth = (int32_t)strlen(PAUSE_TEXT) * _global.consoleFontWidth; + _global.pauseTextureHeight = _global.consoleFontHeight; + text = SDL_CreateRGBSurfaceWithFormat(0, _global.pauseTextureWidth, _global.pauseTextureHeight, 32, SDL_PIXELFORMAT_BGRA32); + if (text == NULL) { + utilDie("%s", SDL_GetError()); + } + src.y = 0; + src.w = _global.consoleFontWidth; + src.h = _global.consoleFontHeight; + dest.y = 0; + dest.w = _global.consoleFontWidth; + dest.h = _global.consoleFontHeight; + for (i = 0; PAUSE_TEXT[i] != 0; i++) { + src.x = (uint8_t)PAUSE_TEXT[i] * _global.consoleFontWidth; + dest.x = i * _global.consoleFontWidth; + SDL_BlitSurface(_global.consoleFontSurface, &src, text, &dest); + } + _global.pauseTexture = SDL_CreateTextureFromSurface(_global.renderer, text); + SDL_FreeSurface(text); + if (_global.pauseTexture == NULL) { + utilDie("%s", SDL_GetError()); + } + } + + dest.w = _global.pauseTextureWidth * PAUSE_TEXT_SCALE; + dest.h = _global.pauseTextureHeight * PAUSE_TEXT_SCALE; + dest.x = target->x + (target->w - dest.w) / 2; + dest.y = target->y + (target->h - dest.h) / 2; + SDL_RenderCopy(_global.renderer, _global.pauseTexture, NULL, &dest); +} + + +// Which engine owned switch (pause, quit, screenshot, grab) a code is mapped to, or INPUT_COUNT. +static InputE _engineSwitch(int32_t scancode) { + static const InputE owned[] = { INPUT_PAUSE, INPUT_QUIT, INPUT_SCREENSHOT, INPUT_GRAB }; + int32_t i = 0; + int32_t index = 0; + + for (i = 0; i < (int32_t)(sizeof(owned) / sizeof(owned[0])); i++) { + for (index = 0; index < _global.controlMappings[owned[i]].inputCount; index++) { + if (_global.controlMappings[owned[i]].input[index] == scancode) { + return owned[i]; } } } - if (id >= 0) { - luaTrace(L, "spriteLoop", "%d %d", id, sprite->loop); - } else { - luaDie(L, "spriteLoop", "Failed!"); + return INPUT_COUNT; +} + + +// Caches a mouse position (overlay coordinates) and tells the script. +static void _fireMouseMoved(int32_t device, int32_t x, int32_t y, int32_t xr, int32_t yr) { + _global.axisCache[AXIS_INDEX_MOUSE(device, 0)] = x; + _global.axisCache[AXIS_INDEX_MOUSE(device, 1)] = y; + if (!_global.frozen) { + _callLua("onMouseMoved", "iiiii", x, y, xr, yr, device); } +} + + +static void _fontDestroy(FontT *font) { + if (_global.fontCurrent == font) { + _global.fontCurrent = NULL; + } + HASH_DEL(_global.fontList, font); + TTF_CloseFont(font->font); + free(font); +} + + +// Engine pause. Freezing releases everything the script thinks is held so it never sees a +// stale button; thawing presses whatever is still physically down. +static void _freezeGame(bool freeze) { + int32_t i = 0; + HeldKeyT held[HELD_KEYS_MAX]; + int32_t heldCount = 0; + + if (freeze) { + for (i = 0; i < INPUT_COUNT; i++) { + if (_global.switchHeld[i]) { + _global.switchHeld[i] = false; + _callLua("onInputReleased", "i", i); + } + } + // Copy first: the callbacks may not touch the list, but keep it simple. + heldCount = _global.heldKeyCount; + memcpy(held, _global.heldKeys, sizeof(HeldKeyT) * (size_t)heldCount); + _global.heldKeyCount = 0; + for (i = 0; i < heldCount; i++) { + _callLua("onInputReleased", "i", held[i].keysym); + _callLua("onKeyReleased", "ii", held[i].keysym, held[i].scancode); + } + _global.frozen = true; + } else { + _global.frozen = false; + for (i = 0; i < _global.physicalKeyCount; i++) { + _deliverKey(true, _global.physicalKeys[i].keysym, _global.physicalKeys[i].scancode); + } + _global.keyboardLastDown = SDL_SCANCODE_UNKNOWN; + _global.keyboardLastUp = SDL_SCANCODE_UNKNOWN; + } + _global.refreshDisplay = true; +} + + +// Maintains a list of keys that are down. +static void _heldListUpdate(HeldKeyT *list, int32_t *count, bool down, int32_t keysym, int32_t scancode) { + int32_t i = 0; + + for (i = 0; i < *count; i++) { + if (list[i].scancode == scancode) { + break; + } + } + if (down) { + if ((i == *count) && (*count < HELD_KEYS_MAX)) { + list[*count].keysym = keysym; + list[*count].scancode = scancode; + (*count)++; + } + } else { + if (i < *count) { + (*count)--; + list[i] = list[*count]; + } + } +} + + +// Runs a controls.cfg if it exists. +static void _loadControlsFile(const char *path) { + if (utilFileExists(path)) { + _progTrace("Loading %s", path); + if (luaL_dofile(_global.luaContext, path)) { + utilDie("%s", lua_tostring(_global.luaContext, -1)); + } + } +} + + +static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int length) { + SDL_Surface *surface = IMG_LoadTyped_RW(SDL_RWFromConstMem(data, (int32_t)length), 1, "PNG"); + + if (surface == NULL) { + utilDie("%s", IMG_GetError()); + } + + return surface; +} + + +// Loads an embedded PNG as a texture. The surface stays alive so callers can read its size. +static SDL_Texture *_loadEmbeddedTexture(const unsigned char *data, unsigned int length, SDL_Surface **surface) { + SDL_Texture *texture = NULL; + + *surface = _loadEmbeddedPng(data, length); + texture = SDL_CreateTextureFromSurface(_global.renderer, *surface); + if (texture == NULL) { + utilDie("%s", SDL_GetError()); + } + + return texture; +} + + +// Reports a script level error with the calling Lua line and exits. +static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) { + va_list args; + char *message = NULL; + + va_start(args, fmt); + message = _luaFormat(L, method, fmt, args); + va_end(args); + if (_global.conf->scriptTracing) { + utilTrace("%s", message); + } + utilDie("%s", message); +} + + +// Formats "line:method: message" for tracing and errors. Caller frees. +static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args) { + lua_Debug ar; + int32_t line = 0; + char *body = NULL; + char *message = NULL; + + if (lua_getstack(L, 1, &ar) && lua_getinfo(L, "Sl", &ar)) { + line = ar.currentline; + } + body = utilCreateStringVArgs(fmt, args); + if (!body) { + utilDie("Unable to allocate trace string."); + } + message = utilCreateString("%d:%s: %s", line, method, body); + if (!message) { + utilDie("Unable to allocate trace string."); + } + free(body); + + return message; +} + + +// Lua panic handler: something went wrong outside a protected call. +static int32_t _luaPanic(lua_State *L) { + lua_Debug ar; + int32_t level = 0; + + utilSay("Singe has panicked! Very bad!"); + utilSay("Error: %s", lua_tostring(L, -1)); + utilSay("Stack trace:"); + while (lua_getstack(L, level, &ar) != 0) { + lua_getinfo(L, "nSl", &ar); + utilSay(" %d: function `%s' at line %d %s", level, ar.name ? ar.name : "?", ar.currentline, ar.short_src); + level++; + } + utilSay("Trace complete."); return 0; } -int32_t apiSpritePause(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spritePause", "No sprite at index %d in apiSpritePause.", id); - sprite->animating = false; - } - } - - if (id >= 0) { - luaTrace(L, "spritePause", "%d %d", id, sprite->animating); - } else { - luaDie(L, "spritePause", "Failed!"); - } - - return 0; -} - - -int32_t apiSpritePlay(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spritePlay", "No sprite at index %d in apiSpritePlay.", id); - // Do we need to reset the tick counter? - if (sprite->animating == false) { - sprite->lastTick = SDL_GetTicks(); - } - sprite->animating = true; - } - } - - if (id >= 0) { - luaTrace(L, "spritePlay", "%d %d", id, sprite->animating); - } else { - luaDie(L, "spritePlay", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteQuality(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - int32_t s; - double d; - SpriteT *sprite = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); s = (int32_t)d; - d = lua_tonumber(L, 2); id = (int32_t)d; - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteQuality", "No sprite at index %d in apiSpriteQuality.", id); - sprite->smooth = s; - SDL_FreeSurface(sprite->surface); - sprite->surface = rotozoomSurfaceXY(sprite->originalSurface, 360 - sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth); - } - } - } - - if (id >= 0) { - luaTrace(L, "spriteQuality", "%d %d", id, sprite->smooth); - } else { - luaDie(L, "spriteQuality", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteRotate(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - double a; - SpriteT *sprite = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); a = fmod(d, 360.0); - d = lua_tonumber(L, 2); id = (int32_t)d; - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteRotate", "No sprite at index %d in apiSpriteRotate.", id); - sprite->angle = a; - SDL_FreeSurface(sprite->surface); - sprite->surface = rotozoomSurfaceXY(sprite->originalSurface, 360 - sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth); - } - } - } - - if (id >= 0) { - luaTrace(L, "spriteRotate", "%d %f", id, sprite->angle); - } else { - luaDie(L, "spriteRotate", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteRotateAndScale(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - double a; - double x; - double y; - SpriteT *sprite = NULL; - - if ((n == 3) || (n == 4)) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 1); a = fmod(d, 360.0); - d = lua_tonumber(L, 2); x = d; - if (n == 3) { - y = x; - d = lua_tonumber(L, 3); id = (int32_t)d; - } else { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 4); id = (int32_t)d; - } - } - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteRotateAndScale", "No sprite at index %d in apiSpriteRotateAndScale.", id); - sprite->angle = a; - sprite->scaleX = x; - sprite->scaleY = y; - SDL_FreeSurface(sprite->surface); - sprite->surface = rotozoomSurfaceXY(sprite->originalSurface, 360 - sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth); - } - } - } - } - - if (id >= 0) { - luaTrace(L, "spriteRotateAndScale", "%d %f %f %f", id, sprite->angle, sprite->scaleX, sprite->scaleY); - } else { - luaDie(L, "spriteRotateAndScale", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteScale(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - double x; - double y; - SpriteT *sprite = NULL; - - if ((n == 2) || (n == 3)) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); x = d; - if (n == 2) { - y = x; - d = lua_tonumber(L, 2); id = (int32_t)d; - } else { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 2); y = d; - d = lua_tonumber(L, 3); id = (int32_t)d; - } - } - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteScale", "No sprite at index %d in apiSpriteScale.", id); - sprite->scaleX = x; - sprite->scaleY = y; - SDL_FreeSurface(sprite->surface); - sprite->surface = rotozoomSurfaceXY(sprite->originalSurface, 360 - sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth); - } - } - } - - if (id >= 0) { - luaTrace(L, "spriteScale", "%d %f %f", id, sprite->scaleX, sprite->scaleY); - } else { - luaDie(L, "spriteScale", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteSetFrame(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - int32_t f; - double d; - SpriteT *sprite = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); f = (int32_t)d; - d = lua_tonumber(L, 2); id = (int32_t)d; - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteSetFrame", "No sprite at index %d in apiSpriteSetFrame.", id); - if ((f >= 0) && (f < sprite->animation->count)) { - sprite->currentFrame = f; - sprite->ticks = 0; - } - } - } - } - - if (id >= 0) { - luaTrace(L, "spriteSetFrame", "%d %d", id, sprite->currentFrame); - } else { - luaDie(L, "spriteSetFrame", "Failed!"); - } - - return 0; -} - - -int32_t apiSpriteUnload(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - double d; - SpriteT *sprite = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our sprite structure - HASH_FIND_INT(_global.spriteList, &id, sprite); - if (!sprite) luaDie(L, "spriteUnload", "No sprite at index %d in apiSpriteUnload.", id); - HASH_DEL(_global.spriteList, sprite); - if (sprite->surface) SDL_FreeSurface(sprite->surface); - if (sprite->originalSurface) SDL_FreeSurface(sprite->originalSurface); - if (sprite->animation) IMG_FreeAnimation(sprite->animation); - free(sprite); - result = true; - } - } - - if (result) { - luaTrace(L, "spriteUnload", "%d", id); - } else { - luaDie(L, "spriteUnload", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoDraw(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - int32_t w = 0; - int32_t h = 0; - int64_t frame = 0; - double d = 0.0; - VideoT *video = NULL; - bool center = false; - SDL_Rect dest; - - // videoDraw(id, x1, y1, x2, y2) - Simple/Stretched draw - // videoDraw(id, x1, y1, c) - Scaled/Rotated draw - - if ((n == 4) || (n == 5)) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); dest.x = (int32_t)d; - d = lua_tonumber(L, 3); dest.y = (int32_t)d; - if (n == 5) { - if (lua_isnumber(L, 4) && lua_isnumber(L, 5)) { - d = lua_tonumber(L, 4); dest.w = (int32_t)d - dest.x + 1; - d = lua_tonumber(L, 5); dest.h = (int32_t)d - dest.y + 1; - } - } else { - if (lua_isboolean(L, 4)) { - d = lua_toboolean(L, 4); center = (int32_t)d != 0; - dest.w = 0; - dest.h = 0; - } - } - - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoDraw", "No video at index %d in apiVideoDraw.", id); - frame = videoUpdate(video->handle, &video->texture); - - // New Frame? - if (frame != video->lastFrame) { - // Get new frame into a surface - this is slow - if (video->surface) SDL_FreeSurface(video->surface); - SDL_QueryTexture(video->texture, NULL, NULL, &w, &h); - video->surface = SDL_CreateRGBSurface(0, w, h, 32, 0, 0, 0, 255); - if (!video->surface) utilDie("%s", SDL_GetError()); - if (SDL_SetRenderTarget(_global.renderer, video->texture) < 0) luaDie(L, "videoDraw", "%s", SDL_GetError()); - if (SDL_RenderReadPixels(_global.renderer, NULL, video->surface->format->format, video->surface->pixels, video->surface->pitch) != 0) luaDie(L, "videoDraw", "%s", SDL_GetError()); - if (SDL_SetRenderTarget(_global.renderer, NULL) < 0) luaDie(L, "videoDraw", "%s", SDL_GetError()); - } - - // Render frame into overlay - if ((dest.w == 0) && (dest.h == 0)) { - if (frame != video->lastFrame) { - if (video->rotatedZoomedSurface != NULL) { - SDL_FreeSurface(video->rotatedZoomedSurface); - video->rotatedZoomedSurface = NULL; - } - video->rotatedZoomedSurface = rotozoomSurfaceXY(video->surface, 360 - video->angle, video->scaleX, video->scaleY, video->smooth); - SDL_SetColorKey(video->rotatedZoomedSurface, true, 0); - } - dest.w = video->rotatedZoomedSurface->w; - dest.h = video->rotatedZoomedSurface->h; - // Scaled/Rotated draw - if (center) { - // Move video so the drawing coordinate is the center of the video - dest.x -= dest.w * 0.5; - dest.y -= dest.h * 0.5; - } - SDL_BlitSurface(video->rotatedZoomedSurface, NULL, _global.overlay, &dest); - } else { - // Simple/Stretched draw - if (SDL_BlitScaled(video->surface, NULL, _global.overlay, &dest) != 0) luaDie(L, "videoDraw", "%s", SDL_GetError()); - } - result = true; - } - } - } - } - - if (result) { - luaTrace(L, "videoDraw", "%d %d %d %d %d %ld", id, dest.x, dest.y, dest.x + dest.w, dest.y + dest.h, frame); - } else { - luaDie(L, "videoDraw", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoGetAudioTrack(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int64_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetAudioTrack", "No video at index %d in apiVideoGetAudioTrack.", id); - r = videoGetAudioTrack(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetAudioTrack", "%d %ld", id, r); - } else { - luaDie(L, "videoGetAudioTrack", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoGetAudioTracks(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int64_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetAudioTracks", "No video at index %d in apiVideoGetAudioTracks.", id); - r = videoGetAudioTracks(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetAudioTracks", "%d %ld", id, r); - } else { - luaDie(L, "videoGetAudioTracks", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoGetFrame(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int64_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetFrame", "No video at index %d in apiVideoGetFrame.", id); - r = videoGetFrame(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetFrame", "%d %ld", id, r); - } else { - luaDie(L, "videoGetFrame", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoGetFrameCount(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int64_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetFrameCount", "No video at index %d in apiVideoGetFrameCount.", id); - r = videoGetFrameCount(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetFrameCount", "%d %ld", id, r); - } else { - luaDie(L, "videoGetFrameCount", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoGetLanguage(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - char *r = NULL; - int32_t id = -1; - int32_t track = -1; - double d; - VideoT *video = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); track = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetLanguage", "No video at index %d in apiVideoGetLanguage.", id); - r = videoGetLanguage(video->handle, track); - result = true; - } - } - } - - if (result) { - luaTrace(L, "videoGetLanguage", "%d %d %s", id, track, r); - } else { - luaDie(L, "videoGetLanguage", "Failed!"); - } - - lua_pushstring(L, r); - return 1; -} - - -int32_t apiVideoGetLanguageDescription(lua_State *L) { - int32_t n = lua_gettop(L); - static char *u = "Unknown"; - char *r = u; - char *c = NULL; - - if (n == 1) { - if (lua_isstring(L, 1)) { - c = (char *)lua_tostring(L, 1); - r = videoGetLanguageDescription(c); - } - } - - lua_pushstring(L, r); - return 1; -} - - -int32_t apiVideoGetHeight(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetHeight", "No video at index %d in apiVideoGetHeight.", id); - r = videoGetHeight(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetHeight", "%d %d", id, r); - } else { - luaDie(L, "videoGetHeight", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoGetVolume(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t left = 0; - int32_t right = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetVolume", "No video at index %d in apiVideoGetVolume.", id); - videoGetVolume(video->handle, &left, &right); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetVolume", "%d %d %d", id, left, right); - } else { - luaDie(L, "videoGetVolume", "Failed!"); - } - - lua_pushnumber(L, left); - lua_pushnumber(L, right); - return 2; -} - - -int32_t apiVideoGetWidth(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoGetWidth", "No video at index %d in apiVideoGetWidth.", id); - r = videoGetWidth(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoGetWidth", "%d %d", id, r); - } else { - luaDie(L, "videoGetWidth", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoIsPlaying(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t r = 0; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoIsPlaying", "No video at index %d in apiVideoIsPlaying.", id); - r = videoIsPlaying(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoIsPlaying", "%d %d", id, r); - } else { - luaDie(L, "videoIsPlaying", "Failed!"); - } - - lua_pushnumber(L, r); - return 1; -} - - -int32_t apiVideoLoad(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t result = -1; - const char *name = NULL; - char *data = NULL; - char *temp = NULL; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isstring(L, 1)) { - name = lua_tostring(L, 1); - // Create data directory based on video path. - temp = utilGetUpToLastPathComponent((char *)name); - data = utilCreateString("%s%s", _global.conf->dataDirBase, temp); - free(temp); - temp = NULL; - utilFixPathSeparators(&data, false); - // Be sure it exists. - utilMkDirP(data, 0777); - if (!utilPathExists(data)) { - luaDie(L, "videoLoad", "Unable to create data directory: %s", data); - } - // Load this video. - video = (VideoT *)calloc(1, sizeof(VideoT)); - if (!video) luaDie(L, "videoLoad", "Unable to allocate new video."); - video->handle = videoLoad((char *)name, data, false, _global.renderer); - if (video->handle < 0) luaDie(L, "videoLoad", "Failed to load video: %s", name); - video->id = _global.nextVideoId; - video->lastFrame = -1; - result = _global.nextVideoId++; - HASH_ADD_INT(_global.videoList, id, video); - // Select desired default audio track - if (_global.conf->audioOutputTrack < videoGetAudioTracks(video->handle)) { - videoSetAudioTrack(video->handle, _global.conf->audioOutputTrack); - } - // Set default volume - videoSetVolume(video->handle, _global.conf->volumeNonVldp, _global.conf->volumeNonVldp); - } - } - - if (result >= 0) { - luaTrace(L, "videoLoad", "%s %s %d", name, data, result); - } else { - luaDie(L, "videoLoad", "Failed!"); - } - - free(data); - - lua_pushnumber(L, result); - return 1; -} - - -int32_t apiVideoPause(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoPause", "No video at index %d in apiVideoPause.", id); - videoPause(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoPause", "%d", id); - } else { - luaDie(L, "videoPause", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoPlay(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoPlay", "No video at index %d in apiVideoPlay.", id); - videoPlay(video->handle); - result = true; - } - } - - if (result) { - luaTrace(L, "videoPlay", "%d", id); - } else { - luaDie(L, "videoPlay", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoQuality(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - int32_t s; - double d; - VideoT *video = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); s = (int32_t)d; - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoQuality", "No video at index %d in apiVideoQuality.", id); - video->smooth = s; - } - } - } - - if (id >= 0) { - luaTrace(L, "videoQuality", "%d %d", id, video->smooth); - } else { - luaDie(L, "videoQuality", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoRotate(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - double a; - VideoT *video = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); a = fmod(d, 360.0); - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoRotate", "No video at index %d in apiVideoRotate.", id); - video->angle = a; - } - } - } - - if (id >= 0) { - luaTrace(L, "videoRotate", "%d %f", id, video->angle); - } else { - luaDie(L, "videoRotate", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoRotateAndScale(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - double a; - double x; - double y; - VideoT *video = NULL; - - if ((n == 3) || (n == 4)) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 2); a = fmod(d, 360.0); - d = lua_tonumber(L, 3); x = d; - if (n == 3) { - y = x; - } else { - if (lua_isnumber(L, 4)) { - d = lua_tonumber(L, 4); y = (int32_t)d; - } - } - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoRotateAndScale", "No video at index %d in apiVideoRotateAndScale.", id); - video->angle = a; - video->scaleX = x; - video->scaleY = y; - } - } - } - } - - if (id >= 0) { - luaTrace(L, "videoRotateAndScale", "%d %f %f %f", id, video->angle, video->scaleX, video->scaleY); - } else { - luaDie(L, "videoRotateAndScale", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoScale(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t id = -1; - double d; - double x; - double y; - VideoT *video = NULL; - - if ((n == 2) || (n == 3)) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 2); x = d; - if (n == 2) { - y = x; - } else { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 3); y = d; - } - } - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoScale", "No video at index %d in apiVideoScale.", id); - video->scaleX = x; - video->scaleY = y; - } - } - } - - if (id >= 0) { - luaTrace(L, "videoScale", "%d %f %f", id, video->scaleX, video->scaleY); - } else { - luaDie(L, "videoScale", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoSeek(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - int64_t frame = 0; - double d; - VideoT *video = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); frame = (int64_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoSeek", "No video at index %d in apiVideoSeek.", id); - videoSeek(video->handle, frame); - result = true; - } - } - } - - if (result) { - luaTrace(L, "videoSeek", "%d %ld", id, frame); - } else { - luaDie(L, "videoSeek", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoSetAudioTrack(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - int64_t track = 0; - double d; - VideoT *video = NULL; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); track = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoSetAudioTrack", "No video at index %d in apiVideoSetAudioTrack.", id); - if ((track >= 0) && (track < videoGetAudioTracks(video->handle))) { - videoSetAudioTrack(video->handle, track); - } else { - luaDie(L, "videoSetAudioTrack", "Invalid audio track in video at index %d in apiVideoSetAudioTrack.", id); - } - result = true; - } - } - } - - if (result) { - luaTrace(L, "videoSetAudioTrack", "%d %d", id, track); - } else { - luaDie(L, "videoSetAudioTrack", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoSetVolume(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - int32_t left = 0; - int32_t right = 0; - double d; - VideoT *video = NULL; - - if (n == 3) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - if (lua_isnumber(L, 3)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - d = lua_tonumber(L, 2); left = (int32_t)d; - d = lua_tonumber(L, 3); right = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoSetVolume", "No video at index %d in apiVideoSetVolume.", id); - if (left < 0) left = 0; - if (left > 100) left = 100; - if (right < 0) right = 0; - if (right > 100) right = 100; - videoSetVolume(video->handle, left, right); - result = true; - } - } - } - } - - if (result) { - luaTrace(L, "videoSetVolume", "%d", id, left, right); - } else { - luaDie(L, "videoSetVolume", "Failed!"); - } - - return 0; -} - - -int32_t apiVideoUnload(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - int32_t id = -1; - double d; - VideoT *video = NULL; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); id = (int32_t)d; - // Get our video structure - HASH_FIND_INT(_global.videoList, &id, video); - if (!video) luaDie(L, "videoUnload", "No video at index %d in apiVideoUnload.", id); - HASH_DEL(_global.videoList, video); - videoUnload(video->handle); - if (video->surface) SDL_FreeSurface(video->surface); - if (video->rotatedZoomedSurface) SDL_FreeSurface(video->rotatedZoomedSurface); - free(video); - result = true; - } - } - - if (result) { - luaTrace(L, "videoUnload", "%d", id); - } else { - luaDie(L, "videoUnload", "Failed!"); - } - - return 0; -} - - -int32_t apiVldpGetHeight(lua_State *L) { - int32_t height = 0; - if (_global.videoHandle >= 0) height = videoGetHeight(_global.videoHandle); - luaTrace(L, "vldpGetHeight", "%d", height); - lua_pushinteger(L, height); - return 1; -} - - -int32_t apiVldpGetPixel(lua_State *L) { - int32_t n = lua_gettop(L); - double d = 0; - bool result = false; - byte pixel[SDL_BYTESPERPIXEL(SDL_PIXELFORMAT_BGRA32)]; - SDL_Rect rect; - - if (n == 2) { - if (lua_isnumber(L, 1)) { - if (lua_isnumber(L, 2)) { - rect.h = 1; - rect.w = 1; - d = lua_tonumber(L, 1); rect.x = (int32_t)(d / _global.overlayScaleX); - d = lua_tonumber(L, 2); rect.y = (int32_t)(d / _global.overlayScaleY); - if (SDL_SetRenderTarget(_global.renderer, _global.videoTexture) < 0) luaDie(L, "vldpGetPixel", "%s", SDL_GetError()); - if (SDL_RenderReadPixels(_global.renderer, &rect, SDL_PIXELFORMAT_BGRA32, pixel, SDL_BYTESPERPIXEL(SDL_PIXELFORMAT_BGRA32) * videoGetWidth(_global.videoHandle)) < 0) luaDie(L, "vldpGetPixel", "%s", SDL_GetError()); - if (SDL_SetRenderTarget(_global.renderer, NULL) < 0) luaDie(L, "vldpGetPixel", "%s", SDL_GetError()); - result = true; - } - } - } - - if (result) { - luaTrace(L, "vldpGetPixel", "%d %d %d %d %d", rect.x, rect.y, pixel[2], pixel[1], pixel[0]); - } else { - luaDie(L, "vldpGetPixel", "Failed!"); - } - - lua_pushinteger(L, (int32_t)pixel[2]); // R - lua_pushinteger(L, (int32_t)pixel[1]); // G - lua_pushinteger(L, (int32_t)pixel[0]); // B - - return 3; -} - - -int32_t apiVldpGetWidth(lua_State *L) { - int32_t width = 0; - if (_global.videoHandle >= 0) width = videoGetWidth(_global.videoHandle); - luaTrace(L, "vldpGetHeight", "%d", width); - lua_pushinteger(L, width); - return 1; -} - - -int32_t apiVldpVerbose(lua_State *L) { - /* - * Enables/Disables writing of VLDP playback activity to daphne_log.txt - * Enabled by default. - */ - (void)L; - - //***REMOVED*** - luaTrace(L, "vldpVerbose", "Unimplemented"); - - return 0; -} - - -int32_t apiKeyboardGetLastDown(lua_State *L) { - luaTrace(L, "keyboardGetLastDown", "%d", _global.keyboardLastDown); - lua_pushinteger(L, _global.keyboardLastDown); - return 1; -} - - -int32_t apiKeyboardGetLastUp(lua_State *L) { - luaTrace(L, "keyboardGetLastUp", "%d", _global.keyboardLastUp); - lua_pushinteger(L, _global.keyboardLastUp); - return 1; -} - - -int32_t apiKeyboardGetMode(lua_State *L) { - luaTrace(L, "keyboardGetMode", "%d", _global.keyboardMode); - lua_pushinteger(L, _global.keyboardMode); - return 1; -} - - -int32_t apiKeyboardGetModifiers(lua_State *L) { - SDL_Keymod m = SDL_GetModState(); - luaTrace(L, "keyboardGetModifiers", "%d", (int32_t)m); - lua_pushinteger(L, (int32_t)m); - return 1; -} - - -int32_t apiKeyboardSetMode(lua_State *L) { - - /* - * Singe can scan keyboard input in two ways: - * - * MODE_NORMAL - Singe will only check for keys defined - * in daphne.ini. This is the default behavior. - * - * MODE_FULL - Singe will scan the keyboard for most keypresses. - * - */ - - int32_t n = lua_gettop(L); - double d = 0; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); _global.keyboardMode = (int32_t)d; - result = true; - } - } - - if (result) { - luaTrace(L, "keyboardSetMode", "%d", _global.keyboardMode); - } else { - luaDie(L, "keyboardSetMode", "Failed!"); - } - - return 0; -} - - -int32_t apiKeyboardIsDown(lua_State *L) { - int32_t n = lua_gettop(L); - double d = 0; - int32_t s = 0; - bool r = false; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); s = (int32_t)d; - if ((s >= 0) && (s < SDL_NUM_SCANCODES)) { - r = _global.keyboardState[s]; - } - result = true; - } - } - - if (result) { - luaTrace(L, "keyboardIsDown", "%d", r); - } else { - luaDie(L, "keyboardIsDown", "Failed!"); - } - - lua_pushboolean(L, r); - return 1; -} - - -int32_t apiMouseEnable(lua_State *L) { - // Enables mouse monitoring - (void)L; - luaTrace(L, "mouseEnable", "%d", _global.conf->noMouse); - _global.mouseEnabled = (bool)!_global.conf->noMouse; - return 0; -} - - -int32_t apiMouseDisable(lua_State *L) { - // Disables mouse monitoring - (void)L; - luaTrace(L, "mouseDisable", ""); - _global.mouseEnabled = false; - return 0; -} - - -int32_t apiMouseSetCaptured(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - - if (n == 1) { - if (lua_isboolean(L, 1)) { - _global.mouseGrabbed = (bool)lua_toboolean(L, 1); - if (_global.mouseGrabbed) { - // Grab mouse - SDL_SetWindowGrab(_global.window, SDL_TRUE); - SDL_ShowCursor(SDL_DISABLE); - } else { - // Ungrab mouse - SDL_SetWindowGrab(_global.window, SDL_FALSE); - SDL_ShowCursor(SDL_ENABLE); - } - result = true; - } - } - - if (result) { - luaTrace(L, "mouseSetCaptured", "%d", _global.mouseGrabbed); - } else { - luaDie(L, "mouseSetCaptured", "Failed!"); - } - - return 0; -} - - -int32_t apiMouseSetMode(lua_State *L) { - // Sets the scanning mode for mouse input. - // Can be one of two values: - // - // SINGLE_MOUSE = 100 - // MANY_MOUSE = 200 - // - // Be sure to add these constant declarations to your framework.singe - // By default Singe starts in single mouse mode. - // Use this command if you need to scan multiple mice. - // e.g. lua code, - // - // mouseSetMode(MANY_MOUSE) - // - // Function returns TRUE is mode set was successful, FALSE otherwise. - // - // --rdg - - int32_t n = lua_gettop(L); - double d = 0; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); _global.mouseMode = (int32_t)d; - result = true; - } - } - - //utilSay("MouseMode now %d", _global.mouseMode); - - if (result) { - luaTrace(L, "mouseSetMode", "%d", _global.mouseMode); - } else { - luaDie(L, "mouseSetMode", "Failed!"); - } - - lua_pushboolean(L, result); - return 1; -} - - -int32_t apiMouseGetPosition(lua_State *L) { - int32_t n = lua_gettop(L); - int32_t m = 0; - int32_t x = 0; - int32_t y = 0; - double d = 0; - bool result = false; - - if (n == 1) { - if (lua_isnumber(L, 1)) { - d = lua_tonumber(L, 1); m = (int32_t)d; - if ((m < 0) || (m >= MAX_MICE)) luaDie(L, "mouseGetPosition", "Invalid mouse index: %d", m); - x = _global.axisCache[MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + m * MOUSE_AXIS_COUNT]; - y = _global.axisCache[MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + m * MOUSE_AXIS_COUNT + 1]; - result = true; - } - } - - if (result) { - luaTrace(L, "mouseGetPosition", "%d %d %d", m, x, y); - } else { - luaDie(L, "mouseGetPosition", "Failed!"); - } - - lua_pushinteger(L, x); - lua_pushinteger(L, y); - - return 2; -} - - -int32_t apiMouseHowMany(lua_State *L) { - luaTrace(L, "mouseHowMany", "%d", _global.mouseCount); - lua_pushinteger(L, _global.mouseCount); - return 1; -} - - -int32_t apiDiscGetHeight(lua_State *L) { - int32_t r = 0; - if (_global.videoHandle >= 0) r = videoGetHeight(_global.videoHandle); - luaTrace(L, "discGetHeight", "%d", r); - lua_pushinteger(L, r); - return 1; -} - - -int32_t apiDiscGetWidth(lua_State *L) { - int32_t r = 0; - if (_global.videoHandle >= 0) r = videoGetWidth(_global.videoHandle); - luaTrace(L, "discGetWidth", "%d", r); - lua_pushinteger(L, r); - return 1; -} - - -int32_t apiDiscGetState(lua_State *L) { - int32_t isPlaying = -1; - - /* - * Returns the status of the vldp - * Values returned are - * based on the following enumeration (found in ldp.h). - * - * LDP_ERROR = 0 - * LDP_SEARCHING = 1 - * LDP_STOPPED = 2 - * LDP_PLAYING = 3 - * LDP_PAUSED = 4 - * - */ - - // Our player isn't as sophisticated as the one in Daphne - if (_global.videoHandle >= 0) isPlaying = videoIsPlaying(_global.videoHandle); - luaTrace(L, "discGetState", "%s", _global.discStopped ? "Stopped" : (isPlaying ? "Playing" : "Paused")); - lua_pushinteger(L, _global.discStopped ? LDP_STOPPED : (isPlaying ? LDP_PLAYING : LDP_PAUSED)); - return 1; -} - - -int32_t apiScriptExecute(lua_State *L) { - int32_t n = lua_gettop(L); - ConfigT *conf = NULL; - bool result = false; - - if (n == 1) { - if (lua_istable(L, 1)) { - // Push next script. - conf = buildConfFromTable(L); - queueScript(conf); - destroyConf(&conf); - // Stop this script running. - _global.running = false; - result = true; - } - } - - if (result) { - luaTrace(L, "scriptExecute", "Success."); - } else { - luaDie(L, "scriptExecute", "Failed!"); - } - - return 0; -} - - -int32_t apiScriptPush(lua_State *L) { - int32_t n = lua_gettop(L); - ConfigT *conf = NULL; - bool result = false; - - if (n == 1) { - if (lua_istable(L, 1)) { - // Push next script. - conf = buildConfFromTable(L); - queueScript(conf); - destroyConf(&conf); - // Push this script. - queueScript(_global.conf); - // Stop this script running. - _global.running = false; - result = true; - } - } - - if (result) { - luaTrace(L, "scriptExecute", "Success."); - } else { - luaDie(L, "scriptExecute", "Failed!"); - } - - return 0; -} - - -int32_t apiSingeGetPauseFlag(lua_State *L) { - /* - * This function returns _global.pauseState's value to the lua script. - * - * Sometimes game logic pauses the game (which implies pausing video playback). - * When implementing a pause state it is possible for the player - * to resume playblack at moments where the game is not intended to. - * Boolean g_global.pause state is an internal variable that keeps track - * of this. It's set to true whenever sep_pre_global.pause is called. - * It's set to false whenever sep_pre_play or sep_skip_to_frame is called. - * - * A lua programmer can use this to prevent resuming playback accidentally. - */ - luaTrace(L, "singeGetPauseFlag", "%d", _global.pauseState); - lua_pushboolean(L, _global.pauseState); - return 1; -} - - -int32_t apiSingeSetPauseFlag(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - - if (n == 1) { - if (lua_isboolean(L, 1)) { - _global.pauseState = (bool)lua_toboolean(L, 1); - updatePauseState(); - result = true; - } - } - - if (result) { - luaTrace(L, "singeSetPauseFlag", "%d", _global.pauseState); - } else { - luaDie(L, "singeSetPauseFlag", "Failed!"); - } - - return 0; -} - - -int32_t apiSingeEnablePauseKey(lua_State *L) { - (void)L; - luaTrace(L, "singeEnablePauseKey", ""); - _global.pauseEnabled = true; - return 0; -} - - -int32_t apiSingeDisablePauseKey(lua_State *L) { - (void)L; - luaTrace(L, "singeDisablePauseKey", ""); - _global.pauseEnabled = false; - return 0; -} - - -int32_t apiSingeQuit(lua_State *L) { - (void)L; - luaTrace(L, "singeQuit", ""); - _global.running = false; - return 0; -} - - -int32_t apiSingeVersion(lua_State *L) { - luaTrace(L, "singeVersion", "%f", SINGE_VERSION); - lua_pushnumber(L, SINGE_VERSION); - return 1; -} - - -int32_t apiSingeWantsCrosshairs(lua_State *L) { - luaTrace(L, "singeWantsCrosshairs", "%f", !_global.conf->noCrosshair); - lua_pushboolean(L, !_global.conf->noCrosshair); - return 1; -} - - -int32_t apiSingeGetDataPath(lua_State *L) { - luaTrace(L, "singeGetDataPath", "%s", _global.conf->dataDir); - lua_pushstring(L, _global.conf->dataDir); - return 1; -} - - -int32_t apiSingeSetGameName(lua_State *L) { - int32_t n = lua_gettop(L); - bool result = false; - - if (n == 1) { - if (lua_isstring(L, 1)) { - SDL_SetWindowTitle(_global.window, lua_tostring(L, 1)); - result = true; - } - } - - if (result) { - luaTrace(L, "singeSetGameName", "%s", lua_tostring(L, 1)); - } else { - luaDie(L, "singeSetGameName", "Failed!"); - } - - return 0; -} - - -int32_t apiSingeGetScriptPath(lua_State *L) { - luaTrace(L, "singeGetScriptPath", "%s", _global.conf->scriptFile); - lua_pushstring(L, _global.conf->scriptFile); - return 1; -} - - -ConfigT *buildConfFromTable(lua_State *L) { - char *sindenString = NULL; - const char *confKey = NULL; - const char *valueString = NULL; - bool valueBoolean = false; - int64_t valueNumber = 0; - ConfigT *c = NULL; - - // Start with current config. - c = cloneConf(_global.conf); - - // Update with data in the table on the top of the Lua stack. - lua_pushnil(L); - while (lua_next(L, 1)) { - // Get key - confKey = lua_tostring(L, 2); - - // Get value - switch (lua_type(L, 3)) { - case LUA_TSTRING: - valueString = lua_tostring(L, 3); - valueBoolean = false; - valueNumber = 0; - break; - - case LUA_TBOOLEAN: - valueString = NULL; - valueBoolean = lua_toboolean(L, 3); - valueNumber = 0; - break; - - case LUA_TNUMBER: - valueString = NULL; - valueBoolean = false; - valueNumber = lua_tonumber(L, 3); - break; - - default: - valueString = NULL; - valueBoolean = false; - valueNumber = 0; - break; - } - - // Update config with new data - if (strcmp(confKey, "SCRIPT") == 0) { - if (c->scriptFile) free (c->scriptFile); - c->scriptFile = strdup(valueString); - utilFixPathSeparators(&c->scriptFile, false); - } else if (strcmp(confKey, "VIDEO") == 0) { - if (c->videoFile) free(c->videoFile); - c->videoFile = strdup(valueString); - utilFixPathSeparators(&c->videoFile, false); - // Is it a framefile? - if (strncmp(utilGetFileExtension(c->videoFile), "txt", 3) == 0) { - c->isFrameFile = true; - } - } else if (strcmp(confKey, "STRETCH") == 0) { - c->stretchVideo = valueBoolean; - } else if (strcmp(confKey, "NO_MOUSE") == 0) { - c->noMouse = valueBoolean; - } else if (strcmp(confKey, "RESOLUTION_X") == 0) { - c->xResolution = valueNumber; - } else if (strcmp(confKey, "RESOLUTION_Y") == 0) { - c->yResolution = valueNumber; - } else if (strcmp(confKey, "SINDEN_GUN") == 0) { - if (sindenString) free(sindenString); - sindenString = strdup(valueString); - if (!parseSindenString(&sindenString, c)) c->sindenArgc = 0; - free(sindenString); - } else if (strcmp(confKey, "AUDIO_TRACK") == 0) { - c->audioOutputTrack = valueNumber; - } - - // Clean up for next pair - lua_pop(L, 1); - } - - // Create new data dir location based on script location. - if (c->dataDir) free(c->dataDir); - c->dataDir = utilCreateString("%s%s", c->dataDirBase, utilGetUpToLastPathComponent(c->scriptFile)); - // Try to create data directory to ensure it exists. - utilMkDirP(c->dataDir, 0777); - // Does it exist? - if (!utilPathExists(c->dataDir)) { - utilDie("Unable to create data directory: %s", c->dataDir); - } - - return c; -} - - -void callLua(const char *func, const char *sig, ...) { - va_list vl; - bool done = false; - int32_t narg; - int32_t nres; - int32_t popCount; - double d; - const int32_t top = lua_gettop(_global.luaContext); - - va_start(vl, sig); - - // Get Function - lua_getglobal(_global.luaContext, func); - if (!lua_isfunction(_global.luaContext, -1)) { - // Function does not exist. Bail. - lua_settop(_global.luaContext, top); - return; - } - - if (_global.conf->scriptTracing) utilTrace("%s", func); - - // Push Arguments - narg = 0; - while ((*sig) && (!done)) { - switch (*sig++) { - - case 'd': // Double - lua_pushnumber(_global.luaContext, va_arg(vl, double)); - break; - - case 'i': // Int - lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Not sure I want to change this to int32_t - break; - - case 's': // String - lua_pushstring(_global.luaContext, va_arg(vl, char *)); - break; - - case '>': - done = true; - break; - - default: - utilDie("Invalid argument option (%c)", *(sig - 1)); - } - if (!done) { - narg++; - luaL_checkstack(_global.luaContext, 1, "Too many arguments"); - } - } - - // Do the call - popCount = nres = (int32_t)strlen(sig); // Number of expected results - if (lua_pcall(_global.luaContext, narg, nres, 0) != 0) { - utilSay("Error executing function '%s': %s", func, lua_tostring(_global.luaContext, -1)); - return; - } - - // Retrieve results - nres = -nres; // Stack index of first result - while (*sig) { - switch (*sig++) { - - case 'd': // Double - if (!lua_isnumber(_global.luaContext, nres)) { - utilDie("Wrong result type"); - } - *va_arg(vl, double *) = lua_tonumber(_global.luaContext, nres); - break; - - case 'i': // Int - if (!lua_isnumber(_global.luaContext, nres)) { - utilDie("Wrong result type"); - } - d = lua_tonumber(_global.luaContext, nres); *va_arg(vl, int *) = (int32_t)d; // Not sure I want to change this to int32_t - break; - - case 's': // String - if (!lua_isstring(_global.luaContext, nres)) { - utilDie("Wrong result type"); - } - *va_arg(vl, const char **) = lua_tostring(_global.luaContext, nres); - break; - - default: - utilDie("Invalid option (%c)", *(sig - 1)); - } - nres++; - } - va_end(vl); - - if (popCount > 0) { - lua_pop(_global.luaContext, popCount); - } -} - - -void channelFinished(int channel) { - callLua("onSoundCompleted", "i", channel); -} - - -void doIndexDisplay(int32_t percent) { - - static int32_t oldW = 0; - static int32_t oldH = 0; - static int32_t screenW = 1280; - static int32_t screenH = 720; - static int32_t radius = 0; - static int32_t angle = 0; - static uint32_t nextUpdate = 0; - static uint32_t updateTicks = 0; - static SDL_Surface *surfDisc = NULL; - static SDL_Surface *surfGlass = NULL; - static SDL_Surface *surfIndex = NULL; - static SDL_Texture *texDisc = NULL; - static SDL_Texture *texGlass = NULL; - static SDL_Texture *texIndex = NULL; - - SDL_Rect target; - int32_t vShift; - - // If 'percent' is -1, we're setting this display up. - // 'percent' 0 to 100 is the actual display. - // 'percent' -2 shuts the display down. - - if (percent == INDEX_DISPLAY_START) { - // Setup - SDL_RenderGetLogicalSize(_global.renderer, &oldW, &oldH); - SDL_RenderSetLogicalSize(_global.renderer, screenW, screenH); - - surfGlass = IMG_LoadPNG_RW(SDL_RWFromMem(magnifyingGlass_png, magnifyingGlass_png_len)); - if (!surfGlass) utilDie("%s", IMG_GetError()); - surfDisc = IMG_LoadPNG_RW(SDL_RWFromMem(laserDisc_png, laserDisc_png_len)); - if (!surfDisc) utilDie("%s", IMG_GetError()); - surfIndex = IMG_LoadPNG_RW(SDL_RWFromMem(indexing_png, indexing_png_len)); - if (!surfIndex) utilDie("%s", IMG_GetError()); - - texDisc = SDL_CreateTextureFromSurface(_global.renderer, surfDisc); - if (!texDisc) utilDie("%s", SDL_GetError()); - texGlass = SDL_CreateTextureFromSurface(_global.renderer, surfGlass); - if (!texGlass) utilDie("%s", SDL_GetError()); - texIndex = SDL_CreateTextureFromSurface(_global.renderer, surfIndex); - if (!texIndex) utilDie("%s", SDL_GetError()); - - radius = surfDisc->w * 0.3; - updateTicks = 5; - nextUpdate = SDL_GetTicks() + updateTicks; - return; - } - - if (percent == INDEX_DISPLAY_STOP) { - // Shutdown - SDL_DestroyTexture(texDisc); - SDL_DestroyTexture(texGlass); - SDL_DestroyTexture(texIndex); - texDisc = NULL; - texGlass = NULL; - texIndex = NULL; - - SDL_FreeSurface(surfDisc); - SDL_FreeSurface(surfGlass); - SDL_FreeSurface(surfIndex); - surfDisc = NULL; - surfGlass = NULL; - surfIndex = NULL; - - SDL_RenderSetLogicalSize(_global.renderer, oldW, oldH); - - SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, 255); - SDL_RenderClear(_global.renderer); - SDL_RenderPresent(_global.renderer); - return; - } - - // Display animation - SDL_RenderSetLogicalSize(_global.renderer, screenW, screenH); - SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, 255); - SDL_RenderClear(_global.renderer); - - // Draw "Indexing" text - vShift = surfIndex->h - 5; - target.x = (screenW * 0.5) - (surfIndex->w * 0.5); - target.y = screenH - vShift; - target.w = surfIndex->w; - target.h = surfIndex->h; - SDL_RenderCopy(_global.renderer, texIndex, NULL, &target); - - // Draw laserdisc - target.x = (screenW * 0.5) - (surfDisc->w * 0.5); - target.y = (screenH * 0.5) - (surfDisc->h * 0.5) - vShift; - target.w = surfDisc->w; - target.h = surfDisc->h; - SDL_RenderCopy(_global.renderer, texDisc, NULL, &target); - - // Draw magnifying glass - target.x = (surfDisc->w * 0.15) + (screenW * 0.5) - (surfGlass->w * 0.5) + cos(angle * M_PI / 180) * radius; - target.y = (surfDisc->h * 0.1) + (screenH * 0.5) - vShift - (surfGlass->h * 0.5) + sin(angle * M_PI / 180) * radius; - target.w = surfGlass->w; - target.h = surfGlass->h; - SDL_RenderCopy(_global.renderer, texGlass, NULL, &target); - - // Update animation - if (SDL_GetTicks() > nextUpdate) { - //angle += (SDL_GetTicks() - nextUpdate) / updateTicks; - angle++; - if (angle > 359) angle -= 360; - nextUpdate = SDL_GetTicks() + updateTicks; - } - - SDL_RenderPresent(_global.renderer); -} - - -void doLogos(void) { - int32_t i = 0; - int32_t w = 0; - int32_t h = 0; - SDL_Surface *surfKangaroo = NULL; - SDL_Surface *surfSinge = NULL; - SDL_Texture *texKangaroo = NULL; - SDL_Texture *texSinge = NULL; - - SDL_RenderGetLogicalSize(_global.renderer, &w, &h); - - surfKangaroo = IMG_LoadPNG_RW(SDL_RWFromMem(kangarooPunchLogo_png, kangarooPunchLogo_png_len)); - if (!surfKangaroo) utilDie("%s", IMG_GetError()); - surfSinge = IMG_LoadPNG_RW(SDL_RWFromMem(singeLogo_png, singeLogo_png_len)); - if (!surfSinge) utilDie("%s", IMG_GetError()); - - texKangaroo = SDL_CreateTextureFromSurface(_global.renderer, surfKangaroo); - if (!texKangaroo) utilDie("%s", SDL_GetError()); - texSinge = SDL_CreateTextureFromSurface(_global.renderer, surfSinge); - if (!texSinge) utilDie("%s", SDL_GetError()); - - // Fade in to white with Kangaroo logo - SDL_RenderSetLogicalSize(_global.renderer, surfKangaroo->w, surfKangaroo->h); - for (i=0; i<256; i++) { - SDL_SetRenderDrawColor(_global.renderer, i, i, i, 255); - SDL_RenderClear(_global.renderer); - SDL_SetTextureAlphaMod(texKangaroo, i); - SDL_RenderCopy(_global.renderer, texKangaroo, NULL, NULL); - SDL_RenderPresent(_global.renderer); - SDL_Delay(3); - } - - SDL_Delay(750); - - // Cross fade to Singe logo - for (i=0; i<256; i++) { - SDL_RenderClear(_global.renderer); - SDL_SetTextureAlphaMod(texKangaroo, 255 - i); - SDL_RenderCopy(_global.renderer, texKangaroo, NULL, NULL); - SDL_SetTextureAlphaMod(texSinge, i); - SDL_RenderCopy(_global.renderer, texSinge, NULL, NULL); - SDL_RenderPresent(_global.renderer); - SDL_Delay(3); - } - - SDL_Delay(750); - - // Fade to black - SDL_RenderSetLogicalSize(_global.renderer, surfSinge->w, surfSinge->h); - for (i=255; i>=0; i--) { - SDL_SetRenderDrawColor(_global.renderer, i, i, i, 255); - SDL_RenderClear(_global.renderer); - SDL_SetTextureAlphaMod(texSinge, i); - SDL_RenderCopy(_global.renderer, texSinge, NULL, NULL); - SDL_RenderPresent(_global.renderer); - SDL_Delay(3); - } - - SDL_DestroyTexture(texSinge); - SDL_DestroyTexture(texKangaroo); - - SDL_FreeSurface(surfSinge); - SDL_FreeSurface(surfKangaroo); - - SDL_RenderSetLogicalSize(_global.renderer, w, h); -} - - -void line(int32_t x1, int32_t y1, int32_t x2, int32_t y2, SDL_Color *c) { - int32_t x = 0; - int32_t y = 0; - int32_t dx = 0; - int32_t dy = 0; - int32_t incX = 0; - int32_t incY = 0; - int32_t balance = 0; - - if (x2 >= x1) { - dx = x2 - x1; - incX = 1; - } else { - dx = x1 - x2; - incX = -1; - } - - if (y2 >= y1) { - dy = y2 - y1; - incY = 1; - } else { - dy = y1 - y2; - incY = -1; - } - - x = x1; - y = y1; - - if (dx >= dy) { - dy <<= 1; - balance = dy - dx; - dx <<= 1; - while (x != x2) { - putPixel(x, y, c); - if (balance >= 0) { - y += incY; - balance -= dx; - } - balance += dy; - x += incX; - } - putPixel(x, y, c); - } else { - dx <<= 1; - balance = dx - dy; - dy <<= 1; - while (y != y2) { - putPixel(x, y, c); - if (balance >= 0) { - x += incX; - balance -= dy; - } - balance += dx; - y += incY; - } - putPixel(x, y, c); - } -} - - -void luaDie(lua_State *L, char *method, char *fmt, ...) { - va_list args; - lua_Debug ar; - char *string1 = NULL; - char *string2 = NULL; - - lua_getstack(L, 1, &ar); - lua_getinfo(L, "nSl", &ar); - string1 = utilCreateString("%d:%s: ", ar.currentline, method); - if (!string1) utilDie("Unable to allocate first trace string."); - va_start(args, fmt); - string2 = utilCreateStringVArgs(fmt, args); - if (!string2) utilDie("Unable to allocate second trace string."); - va_end(args); - if (_global.conf->scriptTracing) utilTrace("%s%s", string1, string2); - utilDie("%s%s", string1, string2); - // Can't free strings - we never get here. -} - - -int32_t luaError(lua_State *L) { - lua_Debug ar; - int32_t level = 0; - - utilSay("Singe has panicked! Very bad!"); - utilSay("Error: %s", lua_tostring(L, -1)); - - utilSay("Stack trace:"); - while (lua_getstack(L, level, &ar) != 0) { - lua_getinfo(L, "nSl", &ar); - utilSay(" %d: function `%s' at line %d %s", level, ar.name, ar.currentline, ar.short_src); - level++; - } - utilSay("Trace complete."); - - return 0; -} - - -#ifdef DEBUG_TOOLS -void luaStackDump(lua_State *L) { - int i; - int t; - int top = lua_gettop(L); - - printf("%d: ", top); - for (i=1; i<=top; i++) { - printf("%d - ", i); - t = lua_type(L, i); - switch (t) { - case LUA_TSTRING: - printf("`%s'", lua_tostring(L, i)); - break; - - case LUA_TBOOLEAN: - printf(lua_toboolean(L, i) ? "true" : "false"); - break; - - case LUA_TNUMBER: - printf("%g", lua_tonumber(L, i)); - break; - - default: - printf("%s", lua_typename(L, t)); - break; - - } - printf(" "); - } - printf("\n"); -} -#endif - - -int luaSearcher(lua_State *L) { - // https://leiradel.github.io/2020/03/01/Embedding-Lua-Modules.html - char *modname = (char *)lua_tostring(L, 1); - size_t i; - int res; - - // Iterates over all modules we know. - for (i = 0; i < sizeof(luaModules) / sizeof(luaModules[0]); i++) { - if (strcmp(modname, luaModules[i].name) == 0) { - // Found the module. - if (luaModules[i].length != 0) { +// package.searchers entry serving the embedded Lua modules. +// https://leiradel.github.io/2020/03/01/Embedding-Lua-Modules.html +static int32_t _luaSearcher(lua_State *L) { + const char *modname = lua_tostring(L, 1); + size_t i = 0; + + for (i = 0; i < sizeof(_luaModules) / sizeof(_luaModules[0]); i++) { + if (strcmp(modname, _luaModules[i].name) == 0) { + if (_luaModules[i].length != 0) { // It's a Lua module, return the chunk that defines the module. - res = luaL_loadbufferx(L, luaModules[i].source, luaModules[i].length, modname, "t"); - if (res != LUA_OK) { - // Compilation error. + if (luaL_loadbufferx(L, _luaModules[i].source, _luaModules[i].length, modname, "t") != LUA_OK) { return lua_error(L); } } else { // It's a native module, return the native function that defines the module. - lua_pushcfunction(L, luaModules[i].openf); + lua_pushcfunction(L, _luaModules[i].openf); } return 1; } } - // Oops... - lua_pushfstring(L, "Unknown Lua module: \"%s\"", modname); + // A searcher explains itself with a string when it has nothing. + lua_pushfstring(L, "\n\tno embedded module '%s'", modname); return 1; } -void luaTrace(lua_State *L, char *method, char *fmt, ...) { - va_list args; - lua_Debug ar; - char *string1 = NULL; - char *string2 = NULL; +static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) { + va_list args; + char *message = NULL; - if (_global.conf->scriptTracing) { - lua_getstack(L, 1, &ar); - lua_getinfo(L, "nSl", &ar); - string1 = utilCreateString("%d:%s: ", ar.currentline, method); - if (!string1) utilDie("Unable to allocate first trace string."); - va_start(args, fmt); - string2 = utilCreateStringVArgs(fmt, args); - if (!string2) utilDie("Unable to allocate second trace string."); - va_end(args); - utilTrace("%s%s", string1, string2); - free(string2); - free(string1); - } + if (_global.conf->scriptTracing) { + va_start(args, fmt); + message = _luaFormat(L, method, fmt, args); + va_end(args); + utilTrace("%s", message); + free(message); + } } -void processKey(bool down, int32_t keysym, int32_t scancode) { - int32_t move; - int32_t index; +// Message handler for lua_pcall: appends a traceback to the error. +static int32_t _luaTraceback(lua_State *L) { + const char *message = lua_tostring(L, 1); - //utilSay("U:%d SY:%d SC:%d", down, keysym, scancode); + if (message == NULL) { + message = "(error object is not a string)"; + } + luaL_traceback(L, L, message, 1); - // Keep track of keyboard state for other API calls. + return 1; +} + + +// Converts the script visible 0..AUDIO_MAX_VOLUME scale to the mixer's scale. +static int32_t _mixerVolume(int32_t effectsVolume) { + return effectsVolume * MIX_MAX_VOLUME / AUDIO_MAX_VOLUME; +} + + +// Input code for a mouse button (0 = left, 1 = right, 2 = middle, ...) or wheel offset. +static int32_t _mouseCode(int32_t device, int32_t button) { + return CODE_MOUSE_BASE + device * CODE_MOUSE_STRIDE + button; +} + + +// Every overlay drawing call ends here so the texture is only re-uploaded when needed. +static void _overlayTouched(void) { + _global.overlayDirty = true; +} + + +static void _pauseAllVideos(bool pause) { + VideoT *video = NULL; + VideoT *temp = NULL; + + HASH_ITER(hh, _global.videoList, video, temp) { + if (pause) { + if (videoIsPlaying(video->handle)) { + video->wasPlayingBeforePause = true; + videoPause(video->handle); + } + } else { + if (video->wasPlayingBeforePause) { + video->wasPlayingBeforePause = false; + videoPlay(video->handle); + } + } + } +} + + +// Routes a key, button, or axis direction code: engine switches first, then the script. +static void _processKey(bool down, int32_t keysym, int32_t scancode) { + InputE engine = INPUT_COUNT; + bool keyboard = (scancode < CODE_GAMEPAD_BASE); // Real scancodes never reach the gamepad range + + // Physical state is tracked even while the game is frozen. if (down) { _global.keyboardLastDown = scancode; - if ((scancode >= 0) && (scancode < SDL_NUM_SCANCODES)) { - _global.keyboardState[scancode] = true; - } } else { _global.keyboardLastUp = scancode; - if ((scancode >= 0) && (scancode < SDL_NUM_SCANCODES)) { - _global.keyboardState[scancode] = false; + } + if ((scancode >= 0) && (scancode < SDL_NUM_SCANCODES)) { + _global.keyboardState[scancode] = down; + } + _heldListUpdate(_global.physicalKeys, &_global.physicalKeyCount, down, keysym, scancode); + + // Engine owned switches act on the press. Keyboard mappings only count in MODE_NORMAL, so + // full mode keeps every key for the game; gamepad and mouse buttons cannot be typed, so they + // always count. + if ((_global.keyboardMode == KEYBOARD_NORMAL) || !keyboard) { + engine = _engineSwitch(scancode); + } + if (down) { + switch (engine) { + case INPUT_PAUSE: + if (_global.pauseEnabled) { + _setPause(!_global.pauseState, true); + } + break; + + case INPUT_GRAB: + _setMouseCaptured(!_global.mouseGrabbed); + break; + + case INPUT_QUIT: + _global.running = false; + break; + + case INPUT_SCREENSHOT: + // Force a redraw so the shot is taken now, even while paused. + _global.requestScreenShot = true; + _global.refreshDisplay = true; + break; + + default: + break; } } - if (_global.keyboardMode == KEYBD_NORMAL) { - // Mappable keys - for (move=0; move 0) { - for (index=0; index<_global.controlMappings[move].inputCount; index++) { - //utilSay("Checking %s %d = %d", _global.controlMappings[move].name, _global.controlMappings[move].input[index], scancode); - if (_global.controlMappings[move].input[index] == scancode) { - //utilSay("Sending move %d - %s %d - %s", move, _global.controlMappings[move].name, _global.controlMappings[move].input[index], down ? "down" : "up"); - if (!down) { - if ((move == INPUT_PAUSE) && (_global.pauseEnabled)) { - _global.pauseState = _global.pauseState ? false : true; - updatePauseState(); - } - if (move == INPUT_GRAB) { - if (_global.mouseGrabbed) { - // Ungrab mouse - _global.mouseGrabbed = false; - SDL_SetWindowGrab(_global.window, SDL_FALSE); - SDL_ShowCursor(SDL_ENABLE); - } else { - // Grab mouse - _global.mouseGrabbed = true; - SDL_SetWindowGrab(_global.window, SDL_TRUE); - SDL_ShowCursor(SDL_DISABLE); - } - } - if (move == INPUT_QUIT) { - _global.running = false; - } - if (move == INPUT_SCREENSHOT) { - _global.requestScreenShot = true; - } - } - callLua(down ? "onInputPressed" : "onInputReleased", "i", move); - } - } - } - } - } else { - // Full keyboard - callLua(down ? "onInputPressed" : "onInputReleased", "i", keysym); - callLua(down ? "onKeyPressed" : "onKeyReleased", "ii", keysym, scancode); - } + // The script never sees the pause key while the engine owns it, or anything while frozen. + if (_global.frozen || ((engine == INPUT_PAUSE) && _global.pauseEnabled)) { + return; + } + _deliverKey(down, keysym, scancode); } -void progTrace(char *fmt, ...) { - va_list args; - if (_global.conf->programTracing) { - va_start(args, fmt); - utilTraceVArgs(fmt, args); - va_end(args); - } +static void _progTrace(const char *fmt, ...) { + va_list args; + + if (_global.conf->programTracing) { + va_start(args, fmt); + utilTraceVArgs(fmt, args); + va_end(args); + } } -void putPixel(int32_t x, int32_t y, SDL_Color *c) { +// Constants every script (and controls.cfg) can rely on. These are the single source of truth. +static void _pushConstants(lua_State *L) { + int32_t x = 0; - SDL_Surface *surface = _global.overlay; - int32_t bpp = surface->format->BytesPerPixel; - Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; - Uint32 pixel = SDL_MapRGBA(surface->format, c->r, c->g, c->b, c->a); + for (x = 0; x < INPUT_COUNT; x++) { + lua_pushinteger(L, x); + lua_setglobal(L, _inputNames[x].switchName); + } - if ((x < 0) || (x >= _global.overlay->w) || (y < 0) || (y >= _global.overlay->h)) return; + lua_pushinteger(L, FONT_QUALITY_SOLID); + lua_setglobal(L, "FONT_QUALITY_SOLID"); + lua_pushinteger(L, FONT_QUALITY_SHADED); + lua_setglobal(L, "FONT_QUALITY_SHADED"); + lua_pushinteger(L, FONT_QUALITY_BLENDED); + lua_setglobal(L, "FONT_QUALITY_BLENDED"); - switch (bpp) { - case 1: - *p = (Uint8)pixel; - break; + lua_pushinteger(L, KEYBOARD_NORMAL); + lua_setglobal(L, "MODE_NORMAL"); + lua_pushinteger(L, KEYBOARD_FULL); + lua_setglobal(L, "MODE_FULL"); - case 2: - *(Uint16 *)p = (Uint16)pixel; - break; + lua_pushinteger(L, MOUSE_SINGLE); + lua_setglobal(L, "MOUSE_SINGLE"); + lua_pushinteger(L, MOUSE_MANY); + lua_setglobal(L, "MOUSE_MANY"); + lua_pushinteger(L, MOUSE_SINGLE); + lua_setglobal(L, "SINGLE_MOUSE"); + lua_pushinteger(L, MOUSE_MANY); + lua_setglobal(L, "MANY_MOUSE"); - case 3: - if (SDL_BYTEORDER == SDL_BIG_ENDIAN) { - p[0] = (pixel >> 16) & 0xff; - p[1] = (pixel >> 8) & 0xff; - p[2] = pixel & 0xff; - } else { - p[0] = pixel & 0xff; - p[1] = (pixel >> 8) & 0xff; - p[2] = (pixel >> 16) & 0xff; - } - break; + lua_pushinteger(L, OVERLAY_NOT_UPDATED); + lua_setglobal(L, "OVERLAY_NOT_UPDATED"); + lua_pushinteger(L, OVERLAY_UPDATED); + lua_setglobal(L, "OVERLAY_UPDATED"); - case 4: - *(Uint32 *)p = pixel; - break; - } + lua_pushinteger(L, RENDER_PIXELATED); + lua_setglobal(L, "RENDER_PIXELATED"); + lua_pushinteger(L, RENDER_SMOOTH); + lua_setglobal(L, "RENDER_SMOOTH"); + + lua_pushinteger(L, DISC_STOPPED); + lua_setglobal(L, "DISC_STOPPED"); + lua_pushinteger(L, DISC_PLAYING); + lua_setglobal(L, "DISC_PLAYING"); + lua_pushinteger(L, DISC_PAUSED); + lua_setglobal(L, "DISC_PAUSED"); + + lua_pushinteger(L, -1); + lua_setglobal(L, "SOUND_ERROR_INVALID"); + lua_pushinteger(L, -1); + lua_setglobal(L, "SOUND_REMOVE_HANDLE"); + + // Input code layout so Framework.singe can build the GAMEPAD_N and MOUSE_N tables. + lua_pushinteger(L, CODE_GAMEPAD_BASE); + lua_setglobal(L, "SINGE_GAMEPAD_BASE"); + lua_pushinteger(L, CODE_GAMEPAD_STRIDE); + lua_setglobal(L, "SINGE_GAMEPAD_STRIDE"); + lua_pushinteger(L, CODE_AXIS_STRIDE); + lua_setglobal(L, "SINGE_AXIS_STRIDE"); + lua_pushinteger(L, CODE_GAMEPAD_BUTTON_OFFSET); + lua_setglobal(L, "SINGE_GAMEPAD_BUTTON_OFFSET"); + lua_pushinteger(L, CODE_MOUSE_BASE); + lua_setglobal(L, "SINGE_MOUSE_BASE"); + lua_pushinteger(L, CODE_MOUSE_STRIDE); + lua_setglobal(L, "SINGE_MOUSE_STRIDE"); + lua_pushinteger(L, MAX_CONTROLLERS); + lua_setglobal(L, "SINGE_MAX_CONTROLLERS"); + lua_pushinteger(L, MAX_MICE); + lua_setglobal(L, "SINGE_MAX_MICE"); + + lua_pushinteger(L, SINGE_VERSION_MAJOR); + lua_setglobal(L, "SINGE_VERSION_MAJOR"); + lua_pushinteger(L, SINGE_VERSION_MINOR); + lua_setglobal(L, "SINGE_VERSION_MINOR"); + lua_pushstring(L, VERSION_STRING); + lua_setglobal(L, "SINGE_VERSION_STRING"); + lua_pushnumber(L, SINGE_VERSION); + lua_setglobal(L, "SINGE_FRAMEWORK_VERSION"); + + lua_pushinteger(L, _global.controllerDeadZone); + lua_setglobal(L, "SINGE_DEAD_ZONE"); + lua_pushboolean(L, _global.conf->legacySpriteArgs); + lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS"); } +// Writes one overlay pixel. The overlay is always 32 bit and must be locked by the caller. +static void _putPixel(int32_t x, int32_t y, uint32_t pixel) { + SDL_Surface *surface = _global.overlay; + uint8_t *p = NULL; + + if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) { + return; + } + p = (uint8_t *)surface->pixels + y * surface->pitch + x * surface->format->BytesPerPixel; + memcpy(p, &pixel, sizeof(pixel)); +} + + +// colorXxx(r, g, b[, a]) with components clamped to 0..255. +static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha) { + int32_t n = lua_gettop(L); + int32_t value[4] = { 0, 0, 0, defaultAlpha }; + int32_t x = 0; + + _argCheck(L, method, 3, 4); + for (x = 0; x < n; x++) { + value[x] = _argInteger(L, method, x + 1); + if (value[x] < 0) { + value[x] = 0; + } + if (value[x] > SDL_ALPHA_OPAQUE) { + value[x] = SDL_ALPHA_OPAQUE; + } + } + color->r = (uint8_t)value[0]; + color->g = (uint8_t)value[1]; + color->b = (uint8_t)value[2]; + color->a = (uint8_t)value[3]; + _luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a); +} + + +// Releases whatever direction code an axis was holding. +static void _releaseAxis(int32_t axisIndex) { + if (_global.axisCode[axisIndex] != 0) { + _processKey(false, 0, _global.axisCode[axisIndex]); + _global.axisCode[axisIndex] = 0; + } +} + + +// Renders text with the current font, quality, and colors. +static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message) { + SDL_Surface *surface = NULL; + + if (_global.fontCurrent == NULL) { + _luaDie(L, method, "No font selected."); + } + switch (_global.fontQuality) { + case FONT_QUALITY_SOLID: + surface = TTF_RenderText_Solid(_global.fontCurrent->font, message, _global.colorForeground); + break; + + case FONT_QUALITY_SHADED: + surface = TTF_RenderText_Shaded(_global.fontCurrent->font, message, _global.colorForeground, _global.colorBackground); + break; + + case FONT_QUALITY_BLENDED: + surface = TTF_RenderText_Blended(_global.fontCurrent->font, message, _global.colorForeground); + break; + + default: + _luaDie(L, method, "Unknown font quality!"); + } + if (!surface) { + _luaDie(L, method, "%s", TTF_GetError()); + } + SDL_SetColorKey(surface, SDL_TRUE, COLOR_KEY_VALUE); + + return surface; +} + + +// Applies the command line audio track to a freshly loaded video, when it has one. +static void _selectDefaultAudioTrack(int32_t handle) { + if (_global.conf->audioOutputTrack < videoGetAudioTracks(handle)) { + videoSetAudioTrack(handle, _global.conf->audioOutputTrack); + } +} + + +static void _setMouseCaptured(bool captured) { + _global.mouseGrabbed = captured; + SDL_SetWindowGrab(_global.window, captured ? SDL_TRUE : SDL_FALSE); + SDL_ShowCursor(captured ? SDL_DISABLE : SDL_ENABLE); +} + + +// Changes the pause flag. Only the pause key freezes the script; a script that sets the flag +// itself keeps running so it can clear it again. +static void _setPause(bool paused, bool fromKey) { + _global.pauseState = paused; + _updatePauseState(); + if (paused && fromKey && !_global.frozen) { + _freezeGame(true); + } + if (!paused && _global.frozen) { + _freezeGame(false); + } +} + + +static void _soundDestroy(SoundT *sound) { + HASH_DEL(_global.soundList, sound); + Mix_FreeChunk(sound->chunk); + free(sound); +} + + +static void _spriteDestroy(SpriteT *sprite) { + HASH_DEL(_global.spriteList, sprite); + _spriteFreeSurface(sprite); + if (sprite->animation != NULL) { + // Frames belong to the animation. + IMG_FreeAnimation(sprite->animation); + } else { + SDL_FreeSurface(sprite->originalSurface); + } + free(sprite); +} + + +// Releases the drawn surface if it is a transformed copy. Animation frames are never freed here. +static void _spriteFreeSurface(SpriteT *sprite) { + if (sprite->surfaceOwned) { + SDL_FreeSurface(sprite->surface); + } + sprite->surface = NULL; + sprite->surfaceOwned = false; +} + + +// Rebuilds the drawn surface after a frame, angle, scale, or quality change. +static void _spriteRebuildSurface(SpriteT *sprite) { + _spriteFreeSurface(sprite); + if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0)) { + // Untransformed sprites draw straight from the original. + sprite->surface = sprite->originalSurface; + } else { + sprite->surface = rotozoomSurfaceXY(sprite->originalSurface, -sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth); + if (sprite->surface == NULL) { + utilDie("Unable to transform sprite %d.", sprite->id); + } + sprite->surfaceOwned = true; + } +} + + +static void _startControllers(void) { + int32_t x = 0; + int32_t count = SDL_NumJoysticks(); + + _stopControllers(); + + // Clamp to the first few controllers found. + if (count > MAX_CONTROLLERS) { + count = MAX_CONTROLLERS; + } + for (x = 0; x < count; x++) { + if (!SDL_IsGameController(x)) { + _progTrace("Device %d is not a controller", x); + continue; + } + _global.controllers[x] = SDL_GameControllerOpen(x); + if (_global.controllers[x]) { + _progTrace("Found %d - %s", x, SDL_GameControllerName(_global.controllers[x])); + } else { + _progTrace("Controller %d not opened", x); + } + } + + SDL_GameControllerEventState(SDL_ENABLE); +} + + +// Prepares a Lua state: standard libraries, our constants, and the embedded module searcher. +static void _startLuaContext(lua_State *L) { + size_t length = 0; + size_t i = 0; + + // What to do when bad things happen + lua_atpanic(L, _luaPanic); + + // Register the standard libraries + luaL_openlibs(L); + + _pushConstants(L); + + // Put our searcher at the front of package.searchers. + lua_getglobal(L, "package"); + lua_getfield(L, -1, "searchers"); + length = lua_rawlen(L, -1); + for (i = length + 1; i > 1; i--) { + lua_rawgeti(L, -2, (lua_Integer)(i - 1)); + lua_rawseti(L, -2, (lua_Integer)i); + } + lua_pushcfunction(L, _luaSearcher); + lua_rawseti(L, -2, 1); + lua_pop(L, 2); +} + + +static void _stopControllers(void) { + int32_t x = 0; + + for (x = 0; x < MAX_CONTROLLERS; x++) { + if (_global.controllers[x] != NULL) { + SDL_GameControllerClose(_global.controllers[x]); + _global.controllers[x] = NULL; + } + } + // Anything held on an axis is gone with the controller. + for (x = 0; x < AXIS_COUNT; x++) { + _releaseAxis(x); + _global.axisCache[x] = 0; + } +} + + +static SDL_Surface *_surfaceCopy(SDL_Surface *source) { + SDL_Surface *destination = SDL_CreateRGBSurfaceWithFormat(0, source->w, source->h, source->format->BitsPerPixel, source->format->format); + + if (destination == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_BlitSurface(source, NULL, destination, NULL); + + return destination; +} + + +// Saves the current frame buffer to the next free singeNNN.png in the data directory. +// Must be called before SDL_RenderPresent for the frame being captured. +static void _takeScreenshot(void) { + int32_t x = 0; + int32_t w = 0; + int32_t h = 0; + int32_t logicalW = 0; + int32_t logicalH = 0; + char *filename = NULL; + SDL_Surface *surface = NULL; + + // Each script starts scanning at zero; later shots resume past the last one saved. + for (x = _global.nextScreenshot; x < SCREENSHOT_MAX; x++) { + free(filename); + filename = utilCreateString("%ssinge%03d.png", _global.conf->dataDir, x); + if (!utilFileExists(filename)) { + break; + } + } + if (x >= SCREENSHOT_MAX) { + utilDie("Seriously? You have %d screenshots in this folder? Remove some.", SCREENSHOT_MAX); + } + _global.nextScreenshot = x + 1; + + // Read the whole window, letterbox included: with a logical size set, a NULL rect would + // only cover the scaled viewport, so switch it off around the read. + SDL_RenderGetLogicalSize(_global.renderer, &logicalW, &logicalH); + SDL_RenderSetLogicalSize(_global.renderer, 0, 0); + if (SDL_GetRendererOutputSize(_global.renderer, &w, &h) != 0) { + utilDie("%s", SDL_GetError()); + } + surface = SDL_CreateRGBSurfaceWithFormat(0, w, h, 24, SDL_PIXELFORMAT_RGB24); + if (surface == NULL) { + utilDie("%s", SDL_GetError()); + } + if (SDL_RenderReadPixels(_global.renderer, NULL, surface->format->format, surface->pixels, surface->pitch) != 0) { + utilDie("%s", SDL_GetError()); + } + SDL_RenderSetLogicalSize(_global.renderer, logicalW, logicalH); + if (IMG_SavePNG(surface, filename) < 0) { + utilDie("%s", IMG_GetError()); + } + _progTrace("Saved %s", filename); + SDL_FreeSurface(surface); + free(filename); +} + + +static void _updatePauseState(void) { + if (_global.pauseState) { + // Pause laserdisc + if (!_global.discStopped && videoIsPlaying(_global.videoHandle)) { + _global.wasPlayingBeforePause = true; + videoPause(_global.videoHandle); + } + _pauseAllVideos(true); + Mix_Pause(-1); + } else { + // Resume laserdisc + if (!_global.discStopped && _global.wasPlayingBeforePause) { + _global.wasPlayingBeforePause = false; + videoPlay(_global.videoHandle); + } + _pauseAllVideos(false); + Mix_Resume(-1); + } +} + + +static void _videoDestroy(VideoT *video) { + HASH_DEL(_global.videoList, video); + videoUnload(video->handle); + SDL_FreeSurface(video->transformedSurface); + free(video); +} + + +// ===== Lua API ===== + + +// colorBackground(r, g, b[, a]) Default alpha is transparent so overlayPrint shows the video through. +static int32_t apiColorBackground(lua_State *L) { + _readColor(L, "colorBackground", &_global.colorBackground, SDL_ALPHA_TRANSPARENT); + + return 0; +} + + +// colorForeground(r, g, b[, a]) +static int32_t apiColorForeground(lua_State *L) { + _readColor(L, "colorForeground", &_global.colorForeground, SDL_ALPHA_OPAQUE); + + return 0; +} + + +// value = controllerGetAxis(controller, axis) +static int32_t apiControllerGetAxis(lua_State *L) { + int32_t c = 0; + int32_t a = 0; + int32_t v = 0; + + _argCheck(L, "controllerGetAxis", 2, 2); + c = _argInteger(L, "controllerGetAxis", 1); + a = _argInteger(L, "controllerGetAxis", 2); + if ((c < 0) || (c >= MAX_CONTROLLERS)) { + _luaDie(L, "controllerGetAxis", "Invalid controller index: %d", c); + } + if ((a < 0) || (a >= CONTROLLER_AXIS_COUNT)) { + _luaDie(L, "controllerGetAxis", "Invalid controller axis: %d", a); + } + v = _global.axisCache[AXIS_INDEX_CONTROLLER(c, a)]; + _luaTrace(L, "controllerGetAxis", "%d %d %d", c, a, v); + lua_pushinteger(L, v); + + return 1; +} + + +// pressed = controllerGetButton(controller, GAMEPAD_N.BUTTON_X.value) +static int32_t apiControllerGetButton(lua_State *L) { + int32_t c = 0; + int32_t code = 0; + int32_t button = 0; + bool v = false; + + _argCheck(L, "controllerGetButton", 2, 2); + c = _argInteger(L, "controllerGetButton", 1); + code = _argInteger(L, "controllerGetButton", 2); + if ((c < 0) || (c >= MAX_CONTROLLERS)) { + _luaDie(L, "controllerGetButton", "Invalid controller index: %d", c); + } + // Convert the framework code back to SDL's button enumeration. + button = code - CODE_GAMEPAD_BASE - (c * CODE_GAMEPAD_STRIDE) - CODE_GAMEPAD_BUTTON_OFFSET; + if ((button < 0) || (button >= CONTROLLER_BUTTON_COUNT)) { + _luaDie(L, "controllerGetButton", "Invalid controller button: %d", code); + } + if (_global.controllers[c] != NULL) { + v = SDL_GameControllerGetButton(_global.controllers[c], (SDL_GameControllerButton)button) != 0; + } + _luaTrace(L, "controllerGetButton", "%d %d %d", c, code, v); + lua_pushboolean(L, v); + + return 1; +} + + +// debugPrint(message) +static int32_t apiDebugPrint(lua_State *L) { + const char *message = NULL; + + _argCheck(L, "debugPrint", 1, 1); + message = _argString(L, "debugPrint", 1); + _luaTrace(L, "debugPrint", "%s", message); + utilSay("%s", message); + + return 0; +} + + +// discAudio(channel, enabled) Channel 1 is left, 2 is right. +static int32_t apiDiscAudio(lua_State *L) { + int32_t channel = 0; + int32_t left = 0; + int32_t right = 0; + bool onOff = false; + + _argCheck(L, "discAudio", 2, 2); + channel = _argInteger(L, "discAudio", 1); + onOff = _argBoolean(L, "discAudio", 2); + if ((channel < 1) || (channel > 2)) { + _luaDie(L, "discAudio", "Invalid audio channel: %d", channel); + } + if (_global.videoHandle >= 0) { + videoGetVolume(_global.videoHandle, &left, &right); + if (channel == 1) { + left = onOff ? _global.conf->volumeVldp : 0; + } else { + right = onOff ? _global.conf->volumeVldp : 0; + } + videoSetVolume(_global.videoHandle, left, right); + } + _luaTrace(L, "discAudio", "%d %d", left, right); + + return 0; +} + + +static int32_t apiDiscChangeSpeed(lua_State *L) { + return _apiUnimplemented(L, "discChangeSpeed"); +} + + +// track = discGetAudioTrack() +static int32_t apiDiscGetAudioTrack(lua_State *L) { + int32_t track = 0; + + if (_global.videoHandle >= 0) { + track = videoGetAudioTrack(_global.videoHandle); + } + _luaTrace(L, "discGetAudioTrack", "%d", track); + lua_pushinteger(L, track); + + return 1; +} + + +// count = discGetAudioTracks() +static int32_t apiDiscGetAudioTracks(lua_State *L) { + int32_t count = 0; + + if (_global.videoHandle >= 0) { + count = videoGetAudioTracks(_global.videoHandle); + } + _luaTrace(L, "discGetAudioTracks", "%d", count); + lua_pushinteger(L, count); + + return 1; +} + + +// frame = discGetFrame() +static int32_t apiDiscGetFrame(lua_State *L) { + int64_t frame = 0; + + if (!_global.discStopped && (_global.videoHandle >= 0)) { + if (_global.conf->isFrameFile) { + frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle); + } else { + frame = videoGetFrame(_global.videoHandle); + } + } + _luaTrace(L, "discGetFrame", "%" PRId64, frame); + lua_pushinteger(L, frame); + + return 1; +} + + +// height = discGetHeight() Also registered as vldpGetHeight. +static int32_t apiDiscGetHeight(lua_State *L) { + int32_t height = 0; + + if (_global.videoHandle >= 0) { + height = videoGetHeight(_global.videoHandle); + } + _luaTrace(L, "discGetHeight", "%d", height); + lua_pushinteger(L, height); + + return 1; +} + + +// code = discGetLanguage(track) +static int32_t apiDiscGetLanguage(lua_State *L) { + int32_t track = 0; + const char *language = "unk"; + + _argCheck(L, "discGetLanguage", 1, 1); + track = _argInteger(L, "discGetLanguage", 1); + if (_global.videoHandle >= 0) { + if ((track < 0) || (track >= videoGetAudioTracks(_global.videoHandle))) { + _luaDie(L, "discGetLanguage", "Invalid audio track: %d", track); + } + language = videoGetLanguage(_global.videoHandle, track); + } + _luaTrace(L, "discGetLanguage", "%d %s", track, language); + lua_pushstring(L, language); + + return 1; +} + + +// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED. +static int32_t apiDiscGetState(lua_State *L) { + DiscStateE state = DISC_PAUSED; + + if (_global.discStopped) { + state = DISC_STOPPED; + } else { + if ((_global.videoHandle >= 0) && videoIsPlaying(_global.videoHandle)) { + state = DISC_PLAYING; + } + } + _luaTrace(L, "discGetState", "%d", state); + lua_pushinteger(L, state); + + return 1; +} + + +// width = discGetWidth() Also registered as vldpGetWidth. +static int32_t apiDiscGetWidth(lua_State *L) { + int32_t width = 0; + + if (_global.videoHandle >= 0) { + width = videoGetWidth(_global.videoHandle); + } + _luaTrace(L, "discGetWidth", "%d", width); + lua_pushinteger(L, width); + + return 1; +} + + +// discPause() +static int32_t apiDiscPause(lua_State *L) { + if (_global.discStopped) { + _luaTrace(L, "discPause", "Ignored. Disc is stopped."); + return 0; + } + if (_global.videoHandle >= 0) { + videoPause(_global.videoHandle); + } + _luaTrace(L, "discPause", "Paused."); + + return 0; +} + + +// discPlay() +static int32_t apiDiscPlay(lua_State *L) { + if (_global.videoHandle >= 0) { + videoPlay(_global.videoHandle); + } + _global.discStopped = false; + _luaTrace(L, "discPlay", "Playing."); + + return 0; +} + + +// discSearch(frame) Seeks, shows the frame, and pauses. Also registered as discPauseAtFrame. +static int32_t apiDiscSearch(lua_State *L) { + int64_t frame = 0; + + _argCheck(L, "discSearch", 1, 1); + frame = _argInteger64(L, "discSearch", 1); + _discSeek(frame); + if (_global.videoHandle >= 0) { + videoPause(_global.videoHandle); + } + _global.discStopped = false; + _luaTrace(L, "discSearch", "%" PRId64, frame); + + return 0; +} + + +static int32_t apiDiscSearchBlanking(lua_State *L) { + return _apiUnimplemented(L, "discSearchBlanking"); +} + + +// discSetAudioTrack(track) +static int32_t apiDiscSetAudioTrack(lua_State *L) { + int32_t track = 0; + + _argCheck(L, "discSetAudioTrack", 1, 1); + track = _argInteger(L, "discSetAudioTrack", 1); + if (_global.videoHandle >= 0) { + if ((track < 0) || (track >= videoGetAudioTracks(_global.videoHandle))) { + _luaDie(L, "discSetAudioTrack", "Invalid audio track: %d", track); + } + videoSetAudioTrack(_global.videoHandle, track); + } + _luaTrace(L, "discSetAudioTrack", "%d", track); + + return 0; +} + + +static int32_t apiDiscSetFPS(lua_State *L) { + return _apiUnimplemented(L, "discSetFPS"); +} + + +// discSkipBackward(frames) Play/pause state is unchanged. +static int32_t apiDiscSkipBackward(lua_State *L) { + int64_t frame = 0; + + _argCheck(L, "discSkipBackward", 1, 1); + if (_global.discStopped || (_global.videoHandle < 0)) { + _luaTrace(L, "discSkipBackward", "Ignored. Disc is stopped."); + return 0; + } + frame = videoGetFrame(_global.videoHandle) - _argInteger64(L, "discSkipBackward", 1); + _discSeek(frame); + _luaTrace(L, "discSkipBackward", "%" PRId64, frame); + + return 0; +} + + +static int32_t apiDiscSkipBlanking(lua_State *L) { + return _apiUnimplemented(L, "discSkipBlanking"); +} + + +// discSkipForward(frames) Play/pause state is unchanged. +static int32_t apiDiscSkipForward(lua_State *L) { + int64_t frame = 0; + + _argCheck(L, "discSkipForward", 1, 1); + if (_global.discStopped || (_global.videoHandle < 0)) { + _luaTrace(L, "discSkipForward", "Ignored. Disc is stopped."); + return 0; + } + frame = videoGetFrame(_global.videoHandle) + _argInteger64(L, "discSkipForward", 1); + _discSeek(frame); + _luaTrace(L, "discSkipForward", "%" PRId64, frame); + + return 0; +} + + +// discSkipToFrame(frame) Seeks and plays no matter the disc state. +static int32_t apiDiscSkipToFrame(lua_State *L) { + int64_t frame = 0; + + _argCheck(L, "discSkipToFrame", 1, 1); + frame = _argInteger64(L, "discSkipToFrame", 1); + _discSeek(frame); + if (_global.videoHandle >= 0) { + videoPlay(_global.videoHandle); + } + _global.discStopped = false; + _luaTrace(L, "discSkipToFrame", "%" PRId64, frame); + + return 0; +} + + +// discStepBackward() Go back a frame and pause. +static int32_t apiDiscStepBackward(lua_State *L) { + int64_t frame = 0; + + if (_global.videoHandle >= 0) { + if (_global.conf->isFrameFile) { + frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) - 1; + } else { + frame = videoGetFrame(_global.videoHandle) - 1; + } + if (frame < 0) { + frame = 0; + } + _discSeek(frame); + videoPause(_global.videoHandle); + } + _luaTrace(L, "discStepBackward", "%" PRId64, frame); + + return 0; +} + + +// discStepForward() Go forward a frame and pause. +static int32_t apiDiscStepForward(lua_State *L) { + int64_t frame = 0; + + if (_global.videoHandle >= 0) { + if (_global.conf->isFrameFile) { + frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) + 1; + } else { + frame = videoGetFrame(_global.videoHandle) + 1; + } + _discSeek(frame); + videoPause(_global.videoHandle); + } + _luaTrace(L, "discStepForward", "%" PRId64, frame); + + return 0; +} + + +// discStop() Pauses and shows the classic blue screen until the next play or search. +static int32_t apiDiscStop(lua_State *L) { + if (_global.discStopped) { + _luaTrace(L, "discStop", "Ignored. Disc is stopped."); + return 0; + } + if (_global.videoHandle >= 0) { + videoPause(_global.videoHandle); + } + _global.discStopped = true; + _global.refreshDisplay = true; + _luaTrace(L, "discStop", "Stopped."); + + return 0; +} + + +// id = fontLoad(filename, points) The new font becomes current. +static int32_t apiFontLoad(lua_State *L) { + const char *name = NULL; + int32_t points = 0; + FontT *font = NULL; + + _argCheck(L, "fontLoad", 2, 2); + name = _argString(L, "fontLoad", 1); + points = _argInteger(L, "fontLoad", 2); + font = (FontT *)calloc(1, sizeof(FontT)); + if (!font) { + _luaDie(L, "fontLoad", "Unable to allocate new font."); + } + font->font = TTF_OpenFont(name, points); + if (!font->font) { + _luaDie(L, "fontLoad", "%s", TTF_GetError()); + } + font->id = _global.nextFontId++; + _global.fontCurrent = font; + HASH_ADD_INT(_global.fontList, id, font); + _luaTrace(L, "fontLoad", "%s %d", name, font->id); + lua_pushinteger(L, font->id); + + return 1; +} + + +// fontPrint(x, y, text) Uses the current font. +static int32_t apiFontPrint(lua_State *L) { + const char *message = NULL; + SDL_Surface *text = NULL; + SDL_Rect dest; + + _argCheck(L, "fontPrint", 3, 3); + dest.x = _argInteger(L, "fontPrint", 1); + dest.y = _argInteger(L, "fontPrint", 2); + message = _argString(L, "fontPrint", 3); + text = _renderText(L, "fontPrint", message); + dest.w = text->w; + dest.h = text->h; + SDL_BlitSurface(text, NULL, _global.overlay, &dest); + SDL_FreeSurface(text); + _overlayTouched(); + _luaTrace(L, "fontPrint", "%s", message); + + return 0; +} + + +// fontQuality(FONT_QUALITY_SOLID | FONT_QUALITY_SHADED | FONT_QUALITY_BLENDED) +static int32_t apiFontQuality(lua_State *L) { + int32_t quality = 0; + + _argCheck(L, "fontQuality", 1, 1); + quality = _argInteger(L, "fontQuality", 1); + if ((quality < FONT_QUALITY_SOLID) || (quality > FONT_QUALITY_BLENDED)) { + _luaDie(L, "fontQuality", "Unknown font quality: %d", quality); + } + _global.fontQuality = (FontQualityE)quality; + _luaTrace(L, "fontQuality", "%d", _global.fontQuality); + + return 0; +} + + +// fontSelect(id) +static int32_t apiFontSelect(lua_State *L) { + _argCheck(L, "fontSelect", 1, 1); + _global.fontCurrent = _argFont(L, "fontSelect", 1); + _luaTrace(L, "fontSelect", "%d", _global.fontCurrent->id); + + return 0; +} + + +// id = fontToSprite(text) Renders text with the current font into a new sprite. +static int32_t apiFontToSprite(lua_State *L) { + const char *message = NULL; + SpriteT *sprite = NULL; + + _argCheck(L, "fontToSprite", 1, 1); + message = _argString(L, "fontToSprite", 1); + sprite = (SpriteT *)calloc(1, sizeof(SpriteT)); + if (!sprite) { + _luaDie(L, "fontToSprite", "Unable to allocate new text sprite."); + } + sprite->originalSurface = _renderText(L, "fontToSprite", message); + sprite->surface = sprite->originalSurface; + sprite->scaleX = 1.0; + sprite->scaleY = 1.0; + sprite->id = _global.nextSpriteId++; + HASH_ADD_INT(_global.spriteList, id, sprite); + _luaTrace(L, "fontToSprite", "%d %s", sprite->id, message); + lua_pushinteger(L, sprite->id); + + return 1; +} + + +// fontUnload(id) +static int32_t apiFontUnload(lua_State *L) { + FontT *font = NULL; + + _argCheck(L, "fontUnload", 1, 1); + font = _argFont(L, "fontUnload", 1); + _luaTrace(L, "fontUnload", "%d", font->id); + _fontDestroy(font); + + return 0; +} + + +// scancode = keyboardGetLastDown() Cleared every frame. +static int32_t apiKeyboardGetLastDown(lua_State *L) { + _luaTrace(L, "keyboardGetLastDown", "%d", _global.keyboardLastDown); + lua_pushinteger(L, _global.keyboardLastDown); + + return 1; +} + + +// scancode = keyboardGetLastUp() Cleared every frame. +static int32_t apiKeyboardGetLastUp(lua_State *L) { + _luaTrace(L, "keyboardGetLastUp", "%d", _global.keyboardLastUp); + lua_pushinteger(L, _global.keyboardLastUp); + + return 1; +} + + +// mode = keyboardGetMode() +static int32_t apiKeyboardGetMode(lua_State *L) { + _luaTrace(L, "keyboardGetMode", "%d", _global.keyboardMode); + lua_pushinteger(L, _global.keyboardMode); + + return 1; +} + + +// modifiers = keyboardGetModifiers() SDL KMOD_* bits; compare with the MODIFIER table. +static int32_t apiKeyboardGetModifiers(lua_State *L) { + SDL_Keymod m = SDL_GetModState(); + + _luaTrace(L, "keyboardGetModifiers", "%d", (int32_t)m); + lua_pushinteger(L, (int32_t)m); + + return 1; +} + + +// down = keyboardIsDown(scancode) +static int32_t apiKeyboardIsDown(lua_State *L) { + int32_t scancode = 0; + bool down = false; + + _argCheck(L, "keyboardIsDown", 1, 1); + scancode = _argInteger(L, "keyboardIsDown", 1); + if ((scancode >= 0) && (scancode < SDL_NUM_SCANCODES)) { + down = _global.keyboardState[scancode]; + } + _luaTrace(L, "keyboardIsDown", "%d %d", scancode, down); + lua_pushboolean(L, down); + + return 1; +} + + +// keyboardSetMode(MODE_NORMAL | MODE_FULL) +// MODE_NORMAL only reports inputs mapped in controls.cfg; MODE_FULL reports every key. +static int32_t apiKeyboardSetMode(lua_State *L) { + int32_t mode = 0; + + _argCheck(L, "keyboardSetMode", 1, 1); + mode = _argInteger(L, "keyboardSetMode", 1); + if ((mode != KEYBOARD_NORMAL) && (mode != KEYBOARD_FULL)) { + _luaDie(L, "keyboardSetMode", "Unknown keyboard mode: %d", mode); + } + _global.keyboardMode = (KeyboardModeE)mode; + _luaTrace(L, "keyboardSetMode", "%d", _global.keyboardMode); + + return 0; +} + + +// x, y = mouseGetPosition(mouse) +static int32_t apiMouseGetPosition(lua_State *L) { + int32_t m = 0; + int32_t x = 0; + int32_t y = 0; + + _argCheck(L, "mouseGetPosition", 1, 1); + m = _argInteger(L, "mouseGetPosition", 1); + if ((m < 0) || (m >= MAX_MICE)) { + _luaDie(L, "mouseGetPosition", "Invalid mouse index: %d", m); + } + x = _global.axisCache[AXIS_INDEX_MOUSE(m, 0)]; + y = _global.axisCache[AXIS_INDEX_MOUSE(m, 1)]; + _luaTrace(L, "mouseGetPosition", "%d %d %d", m, x, y); + lua_pushinteger(L, x); + lua_pushinteger(L, y); + + return 2; +} + + +// count = mouseHowMany() +static int32_t apiMouseHowMany(lua_State *L) { + _luaTrace(L, "mouseHowMany", "%d", _global.mouseCount); + lua_pushinteger(L, _global.mouseCount); + + return 1; +} + + +// mouseSetCaptured(captured) Grabs and hides the cursor. +static int32_t apiMouseSetCaptured(lua_State *L) { + _argCheck(L, "mouseSetCaptured", 1, 1); + _setMouseCaptured(_argBoolean(L, "mouseSetCaptured", 1)); + _luaTrace(L, "mouseSetCaptured", "%d", _global.mouseGrabbed); + + return 0; +} + + +// mouseSetEnabled(enabled) Framework.singe aliases mouseEnable() and mouseDisable() to this. +static int32_t apiMouseSetEnabled(lua_State *L) { + _argCheck(L, "mouseSetEnabled", 1, 1); + _global.mouseEnabled = _argBoolean(L, "mouseSetEnabled", 1) && !_global.conf->noMouse; + _luaTrace(L, "mouseSetEnabled", "%d", _global.mouseEnabled); + + return 0; +} + + +// mouseSetMode(MOUSE_SINGLE | MOUSE_MANY) +static int32_t apiMouseSetMode(lua_State *L) { + int32_t mode = 0; + + _argCheck(L, "mouseSetMode", 1, 1); + mode = _argInteger(L, "mouseSetMode", 1); + if ((mode != MOUSE_SINGLE) && (mode != MOUSE_MANY)) { + _luaDie(L, "mouseSetMode", "Unknown mouse mode: %d", mode); + } + _global.mouseMode = (MouseModeE)mode; + _luaTrace(L, "mouseSetMode", "%d", _global.mouseMode); + + return 0; +} + + +// overlayBox(x1, y1, x2, y2) Outline only. +static int32_t apiOverlayBox(lua_State *L) { + int32_t x1 = 0; + int32_t y1 = 0; + int32_t x2 = 0; + int32_t y2 = 0; + uint32_t pixel = 0; + + _argCheck(L, "overlayBox", 4, 4); + x1 = _argInteger(L, "overlayBox", 1); + y1 = _argInteger(L, "overlayBox", 2); + x2 = _argInteger(L, "overlayBox", 3); + y2 = _argInteger(L, "overlayBox", 4); + pixel = SDL_MapRGBA(_global.overlay->format, _global.colorForeground.r, _global.colorForeground.g, _global.colorForeground.b, _global.colorForeground.a); + SDL_LockSurface(_global.overlay); + _drawLine(x1, y1, x2, y1, pixel); + _drawLine(x2, y1, x2, y2, pixel); + _drawLine(x2, y2, x1, y2, pixel); + _drawLine(x1, y2, x1, y1, pixel); + SDL_UnlockSurface(_global.overlay); + _overlayTouched(); + _luaTrace(L, "overlayBox", "%d %d %d %d", x1, y1, x2, y2); + + return 0; +} + + +// overlayCircle(x, y, radius) Midpoint circle. +static int32_t apiOverlayCircle(lua_State *L) { + int32_t x0 = 0; + int32_t y0 = 0; + int32_t r = 0; + int32_t x = 0; + int32_t y = 0; + int32_t dx = 1; + int32_t dy = 1; + int32_t err = 0; + uint32_t pixel = 0; + + _argCheck(L, "overlayCircle", 3, 3); + x0 = _argInteger(L, "overlayCircle", 1); + y0 = _argInteger(L, "overlayCircle", 2); + r = _argInteger(L, "overlayCircle", 3); + x = r - 1; + err = dx - (r << 1); + pixel = SDL_MapRGBA(_global.overlay->format, _global.colorForeground.r, _global.colorForeground.g, _global.colorForeground.b, _global.colorForeground.a); + + SDL_LockSurface(_global.overlay); + while (x >= y) { + _putPixel(x0 + x, y0 + y, pixel); + _putPixel(x0 + y, y0 + x, pixel); + _putPixel(x0 - y, y0 + x, pixel); + _putPixel(x0 - x, y0 + y, pixel); + _putPixel(x0 - x, y0 - y, pixel); + _putPixel(x0 - y, y0 - x, pixel); + _putPixel(x0 + y, y0 - x, pixel); + _putPixel(x0 + x, y0 - y, pixel); + if (err <= 0) { + y++; + err += dy; + dy += 2; + } + if (err > 0) { + x--; + dx += 2; + err += dx - (r << 1); + } + } + SDL_UnlockSurface(_global.overlay); + _overlayTouched(); + _luaTrace(L, "overlayCircle", "%d %d %d", x0, y0, r); + + return 0; +} + + +// overlayClear() Fills with the background color. +static int32_t apiOverlayClear(lua_State *L) { + SDL_FillRect(_global.overlay, NULL, SDL_MapRGBA(_global.overlay->format, _global.colorBackground.r, _global.colorBackground.g, _global.colorBackground.b, _global.colorBackground.a)); + _overlayTouched(); + _luaTrace(L, "overlayClear", "Cleared."); + + return 0; +} + + +// overlayEllipse(x1, y1, x2, y2) Bresenham ellipse inside the given rectangle. +static int32_t apiOverlayEllipse(lua_State *L) { + int32_t x0 = 0; + int32_t y0 = 0; + int32_t x1 = 0; + int32_t y1 = 0; + int32_t a = 0; + int32_t b = 0; + int32_t b1 = 0; + int32_t dx = 0; + int32_t dy = 0; + int32_t err = 0; + int32_t e2 = 0; + uint32_t pixel = 0; + + _argCheck(L, "overlayEllipse", 4, 4); + x0 = _argInteger(L, "overlayEllipse", 1); + y0 = _argInteger(L, "overlayEllipse", 2); + x1 = _argInteger(L, "overlayEllipse", 3); + y1 = _argInteger(L, "overlayEllipse", 4); + pixel = SDL_MapRGBA(_global.overlay->format, _global.colorForeground.r, _global.colorForeground.g, _global.colorForeground.b, _global.colorForeground.a); + _luaTrace(L, "overlayEllipse", "%d %d %d %d", x0, y0, x1, y1); + + a = abs(x1 - x0); + b = abs(y1 - y0); + b1 = b & 1; // values of diameter + dx = 4 * (1 - a) * b * b; + dy = 4 * (b1 + 1) * a * a; // error increment + err = dx + dy + b1 * a * a; + + if (x0 > x1) { // if called with swapped points + x0 = x1; + x1 += a; + } + if (y0 > y1) { // exchange them + y0 = y1; + } + y0 += (b + 1) / 2; // starting pixel + y1 = y0 - b1; + a *= 8 * a; + b1 = 8 * b * b; + + SDL_LockSurface(_global.overlay); + do { + _putPixel(x1, y0, pixel); // I. Quadrant + _putPixel(x0, y0, pixel); // II. Quadrant + _putPixel(x0, y1, pixel); // III. Quadrant + _putPixel(x1, y1, pixel); // IV. Quadrant + e2 = 2 * err; + if (e2 <= dy) { // y step + y0++; + y1--; + dy += a; + err += dy; + } + if (e2 >= dx || 2 * err > dy) { // x step + x0++; + x1--; + dx += b1; + err += dx; + } + } while (x0 <= x1); + + while (y0 - y1 < b) { // too early stop of flat ellipses a = 1 + _putPixel(x0 - 1, y0, pixel); // finish tip of ellipse + _putPixel(x1 + 1, y0, pixel); + y0++; + _putPixel(x0 - 1, y1, pixel); + _putPixel(x1 + 1, y1, pixel); + y1--; + } + SDL_UnlockSurface(_global.overlay); + _overlayTouched(); + + return 0; +} + + +// height = overlayGetHeight() +static int32_t apiOverlayGetHeight(lua_State *L) { + _luaTrace(L, "overlayGetHeight", "%d", _global.overlay->h); + lua_pushinteger(L, _global.overlay->h); + + return 1; +} + + +// width = overlayGetWidth() +static int32_t apiOverlayGetWidth(lua_State *L) { + _luaTrace(L, "overlayGetWidth", "%d", _global.overlay->w); + lua_pushinteger(L, _global.overlay->w); + + return 1; +} + + +// overlayLine(x1, y1, x2, y2) +static int32_t apiOverlayLine(lua_State *L) { + int32_t x1 = 0; + int32_t y1 = 0; + int32_t x2 = 0; + int32_t y2 = 0; + uint32_t pixel = 0; + + _argCheck(L, "overlayLine", 4, 4); + x1 = _argInteger(L, "overlayLine", 1); + y1 = _argInteger(L, "overlayLine", 2); + x2 = _argInteger(L, "overlayLine", 3); + y2 = _argInteger(L, "overlayLine", 4); + pixel = SDL_MapRGBA(_global.overlay->format, _global.colorForeground.r, _global.colorForeground.g, _global.colorForeground.b, _global.colorForeground.a); + SDL_LockSurface(_global.overlay); + _drawLine(x1, y1, x2, y2, pixel); + SDL_UnlockSurface(_global.overlay); + _overlayTouched(); + _luaTrace(L, "overlayLine", "%d %d %d %d", x1, y1, x2, y2); + + return 0; +} + + +// overlayPlot(x, y) +static int32_t apiOverlayPlot(lua_State *L) { + int32_t x = 0; + int32_t y = 0; + uint32_t pixel = 0; + + _argCheck(L, "overlayPlot", 2, 2); + x = _argInteger(L, "overlayPlot", 1); + y = _argInteger(L, "overlayPlot", 2); + pixel = SDL_MapRGBA(_global.overlay->format, _global.colorForeground.r, _global.colorForeground.g, _global.colorForeground.b, _global.colorForeground.a); + SDL_LockSurface(_global.overlay); + _putPixel(x, y, pixel); + SDL_UnlockSurface(_global.overlay); + _overlayTouched(); + _luaTrace(L, "overlayPlot", "%d %d", x, y); + + return 0; +} + + +// overlayPrint(column, row, text) Built in console font; coordinates are character cells. +static int32_t apiOverlayPrint(lua_State *L) { + const uint8_t *text = NULL; + int32_t i = 0; + int32_t length = 0; + int32_t fit = 0; + SDL_Rect src; + SDL_Rect dst; + + _argCheck(L, "overlayPrint", 3, 3); + dst.x = _argInteger(L, "overlayPrint", 1) * _global.consoleFontWidth; + dst.y = _argInteger(L, "overlayPrint", 2) * _global.consoleFontHeight; + dst.w = _global.consoleFontWidth; + dst.h = _global.consoleFontHeight; + src.y = 0; + src.w = _global.consoleFontWidth; + src.h = _global.consoleFontHeight; + text = (const uint8_t *)_argString(L, "overlayPrint", 3); + _luaTrace(L, "overlayPrint", "%s", (const char *)text); + + // Clip to the right edge of the overlay. + length = (int32_t)strlen((const char *)text); + fit = (_global.overlay->w - dst.x) / _global.consoleFontWidth; + if (fit < 0) { + fit = 0; + } + if (length > fit) { + length = fit; + } + for (i = 0; i < length; i++) { + src.x = text[i] * _global.consoleFontWidth; + SDL_BlitSurface(_global.consoleFontSurface, &src, _global.overlay, &dst); + dst.x += _global.consoleFontWidth; + } + _overlayTouched(); + + return 0; +} + + +// overlaySetResolution(width, height) Replaces the overlay; its contents are lost. +static int32_t apiOverlaySetResolution(lua_State *L) { + int32_t width = 0; + int32_t height = 0; + + _argCheck(L, "overlaySetResolution", 2, 2); + width = _argInteger(L, "overlaySetResolution", 1); + height = _argInteger(L, "overlaySetResolution", 2); + if ((width <= 0) || (height <= 0)) { + _luaDie(L, "overlaySetResolution", "Invalid overlay size: %dx%d", width, height); + } + SDL_FreeSurface(_global.overlay); + _global.overlay = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_BGRA32); + if (_global.overlay == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND); + SDL_DestroyTexture(_global.overlayTexture); + _global.overlayTexture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, width, height); + if (_global.overlayTexture == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); + if (_global.videoHandle >= 0) { + _global.overlayScaleX = (double)width / (double)videoGetWidth(_global.videoHandle); + _global.overlayScaleY = (double)height / (double)videoGetHeight(_global.videoHandle); + } + _overlayTouched(); + _luaTrace(L, "overlaySetResolution", "%d %d", width, height); + + return 0; +} + + +// scriptExecute(config) Runs another script after this one ends. +static int32_t apiScriptExecute(lua_State *L) { + ConfigT *conf = NULL; + + _argCheck(L, "scriptExecute", 1, 1); + if (!lua_istable(L, 1)) { + _luaDie(L, "scriptExecute", "Argument 1 must be a table."); + } + conf = _buildConfFromTable(L); + queueScript(conf); + destroyConf(&conf); + _global.running = false; + _luaTrace(L, "scriptExecute", "Queued."); + + return 0; +} + + +// scriptPush(config) Runs another script, then returns to this one. +static int32_t apiScriptPush(lua_State *L) { + ConfigT *conf = NULL; + + _argCheck(L, "scriptPush", 1, 1); + if (!lua_istable(L, 1)) { + _luaDie(L, "scriptPush", "Argument 1 must be a table."); + } + conf = _buildConfFromTable(L); + queueScript(conf); + destroyConf(&conf); + queueScript(_global.conf); + _global.running = false; + _luaTrace(L, "scriptPush", "Queued."); + + return 0; +} + + +// path = singeGetDataPath() +static int32_t apiSingeGetDataPath(lua_State *L) { + _luaTrace(L, "singeGetDataPath", "%s", _global.conf->dataDir); + lua_pushstring(L, _global.conf->dataDir); + + return 1; +} + + +// height = singeGetHeight() Window height in pixels. +static int32_t apiSingeGetHeight(lua_State *L) { + int32_t y = 0; + + SDL_GetWindowSize(_global.window, NULL, &y); + _luaTrace(L, "singeGetHeight", "%d", y); + lua_pushinteger(L, y); + + return 1; +} + + +// paused = singeGetPauseFlag() +static int32_t apiSingeGetPauseFlag(lua_State *L) { + _luaTrace(L, "singeGetPauseFlag", "%d", _global.pauseState); + lua_pushboolean(L, _global.pauseState); + + return 1; +} + + +// path = singeGetScriptPath() +static int32_t apiSingeGetScriptPath(lua_State *L) { + _luaTrace(L, "singeGetScriptPath", "%s", _global.conf->scriptFile); + lua_pushstring(L, _global.conf->scriptFile); + + return 1; +} + + +// width = singeGetWidth() Window width in pixels. +static int32_t apiSingeGetWidth(lua_State *L) { + int32_t x = 0; + + SDL_GetWindowSize(_global.window, &x, NULL); + _luaTrace(L, "singeGetWidth", "%d", x); + lua_pushinteger(L, x); + + return 1; +} + + +// singeQuit() +static int32_t apiSingeQuit(lua_State *L) { + _luaTrace(L, "singeQuit", "Quit requested."); + _global.running = false; + + return 0; +} + + +// singeScreenshot() Saved after the next frame is drawn. +static int32_t apiSingeScreenshot(lua_State *L) { + _luaTrace(L, "singeScreenshot", "Screenshot requested."); + _global.requestScreenShot = true; + _global.refreshDisplay = true; + + return 0; +} + + +// singeSetGameName(title) +static int32_t apiSingeSetGameName(lua_State *L) { + const char *title = NULL; + + _argCheck(L, "singeSetGameName", 1, 1); + title = _argString(L, "singeSetGameName", 1); + SDL_SetWindowTitle(_global.window, title); + _luaTrace(L, "singeSetGameName", "%s", title); + + return 0; +} + + +// singeSetPauseFlag(paused) +static int32_t apiSingeSetPauseFlag(lua_State *L) { + _argCheck(L, "singeSetPauseFlag", 1, 1); + _setPause(_argBoolean(L, "singeSetPauseFlag", 1), false); + _luaTrace(L, "singeSetPauseFlag", "%d", _global.pauseState); + + return 0; +} + + +// singeSetPauseKeyEnabled(enabled) Framework.singe aliases singeEnablePauseKey()/singeDisablePauseKey(). +static int32_t apiSingeSetPauseKeyEnabled(lua_State *L) { + _argCheck(L, "singeSetPauseKeyEnabled", 1, 1); + _global.pauseEnabled = _argBoolean(L, "singeSetPauseKeyEnabled", 1); + _luaTrace(L, "singeSetPauseKeyEnabled", "%d", _global.pauseEnabled); + + return 0; +} + + +// version = singeVersion() +static int32_t apiSingeVersion(lua_State *L) { + _luaTrace(L, "singeVersion", "%s", VERSION_STRING); + lua_pushnumber(L, SINGE_VERSION); + + return 1; +} + + +// wanted = singeWantsCrosshairs() False when --nocrosshair was given. +static int32_t apiSingeWantsCrosshairs(lua_State *L) { + bool wanted = !_global.conf->noCrosshair; + + _luaTrace(L, "singeWantsCrosshairs", "%d", wanted); + lua_pushboolean(L, wanted); + + return 1; +} + + +// soundFullStop() Halts every sound effect channel. +static int32_t apiSoundFullStop(lua_State *L) { + _luaTrace(L, "soundFullStop", "Halting all channels."); + Mix_HaltChannel(-1); + + return 0; +} + + +// volume = soundGetVolume() 0 to AUDIO_MAX_VOLUME. +static int32_t apiSoundGetVolume(lua_State *L) { + _luaTrace(L, "soundGetVolume", "%d", _global.effectsVolume); + lua_pushinteger(L, _global.effectsVolume); + + return 1; +} + + +// playing = soundIsPlaying(channel) +static int32_t apiSoundIsPlaying(lua_State *L) { + int32_t channel = 0; + bool playing = false; + + _argCheck(L, "soundIsPlaying", 1, 1); + channel = _argInteger(L, "soundIsPlaying", 1); + if ((channel < 0) || (channel >= Mix_AllocateChannels(-1))) { + _luaDie(L, "soundIsPlaying", "Invalid channel: %d", channel); + } + playing = Mix_Playing(channel) != 0; + _luaTrace(L, "soundIsPlaying", "%d %d", channel, playing); + lua_pushboolean(L, playing); + + return 1; +} + + +// id = soundLoad(filename) +static int32_t apiSoundLoad(lua_State *L) { + const char *name = NULL; + SoundT *sound = NULL; + + _argCheck(L, "soundLoad", 1, 1); + name = _argString(L, "soundLoad", 1); + sound = (SoundT *)calloc(1, sizeof(SoundT)); + if (!sound) { + _luaDie(L, "soundLoad", "Unable to allocate new sound."); + } + sound->chunk = Mix_LoadWAV(name); + if (!sound->chunk) { + _luaDie(L, "soundLoad", "%s", Mix_GetError()); + } + sound->id = _global.nextSoundId++; + HASH_ADD_INT(_global.soundList, id, sound); + _luaTrace(L, "soundLoad", "%d %s", sound->id, name); + lua_pushinteger(L, sound->id); + + return 1; +} + + +// wasPlaying = soundPause(channel) +static int32_t apiSoundPause(lua_State *L) { + int32_t channel = 0; + bool playing = false; + + _argCheck(L, "soundPause", 1, 1); + channel = _argInteger(L, "soundPause", 1); + if ((channel < 0) || (channel >= Mix_AllocateChannels(-1))) { + _luaDie(L, "soundPause", "Invalid channel: %d", channel); + } + playing = Mix_Playing(channel) != 0; + Mix_Pause(channel); + _luaTrace(L, "soundPause", "%d %d", channel, playing); + lua_pushboolean(L, playing); + + return 1; +} + + +// channel = soundPlay(id) Returns -1 (SOUND_ERROR_INVALID) when every channel is busy. +static int32_t apiSoundPlay(lua_State *L) { + SoundT *sound = NULL; + int32_t channel = -1; + + _argCheck(L, "soundPlay", 1, 1); + sound = _argSound(L, "soundPlay", 1); + channel = Mix_PlayChannel(-1, sound->chunk, 0); + if (channel >= 0) { + Mix_Volume(channel, _mixerVolume(_global.effectsVolume)); + } + _luaTrace(L, "soundPlay", "%d %d", sound->id, channel); + lua_pushinteger(L, channel); + + return 1; +} + + +// wasPaused = soundResume(channel) +static int32_t apiSoundResume(lua_State *L) { + int32_t channel = 0; + bool paused = false; + + _argCheck(L, "soundResume", 1, 1); + channel = _argInteger(L, "soundResume", 1); + if ((channel < 0) || (channel >= Mix_AllocateChannels(-1))) { + _luaDie(L, "soundResume", "Invalid channel: %d", channel); + } + paused = Mix_Paused(channel) != 0; + Mix_Resume(channel); + _luaTrace(L, "soundResume", "%d %d", channel, paused); + lua_pushboolean(L, paused); + + return 1; +} + + +// soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel. +static int32_t apiSoundSetVolume(lua_State *L) { + int32_t volume = 0; + + _argCheck(L, "soundSetVolume", 1, 1); + volume = _argInteger(L, "soundSetVolume", 1); + if ((volume < 0) || (volume > AUDIO_MAX_VOLUME)) { + _luaDie(L, "soundSetVolume", "Invalid sound volume value: %d", volume); + } + _global.effectsVolume = volume; + Mix_Volume(-1, _mixerVolume(_global.effectsVolume)); + _luaTrace(L, "soundSetVolume", "%d", _global.effectsVolume); + + return 0; +} + + +// wasPlaying = soundStop(channel) +static int32_t apiSoundStop(lua_State *L) { + int32_t channel = 0; + bool playing = false; + + _argCheck(L, "soundStop", 1, 1); + channel = _argInteger(L, "soundStop", 1); + if ((channel < 0) || (channel >= Mix_AllocateChannels(-1))) { + _luaDie(L, "soundStop", "Invalid channel: %d", channel); + } + playing = Mix_Playing(channel) != 0; + Mix_HaltChannel(channel); + _luaTrace(L, "soundStop", "%d %d", channel, playing); + lua_pushboolean(L, playing); + + return 1; +} + + +// soundUnload(id) +static int32_t apiSoundUnload(lua_State *L) { + SoundT *sound = NULL; + + _argCheck(L, "soundUnload", 1, 1); + sound = _argSound(L, "soundUnload", 1); + _luaTrace(L, "soundUnload", "%d", sound->id); + _soundDestroy(sound); + + return 0; +} + + +// spriteDraw(id, x, y[, centered]) - Draw at natural size +// spriteDraw(id, x, y, x2, y2[, centered]) - Stretch into the rectangle +static int32_t apiSpriteDraw(lua_State *L) { + int32_t n = lua_gettop(L); + bool center = false; + bool stretched = false; + bool newFrame = false; + uint32_t now = 0; + int32_t delay = 0; + SpriteT *sprite = NULL; + SDL_Rect dest; + + _argCheck(L, "spriteDraw", 3, 6); + sprite = _argSprite(L, "spriteDraw", 1); + dest.x = _argInteger(L, "spriteDraw", 2); + dest.y = _argInteger(L, "spriteDraw", 3); + dest.w = 0; + dest.h = 0; + if (n >= 5) { + stretched = true; + dest.w = _argInteger(L, "spriteDraw", 4) - dest.x + 1; + dest.h = _argInteger(L, "spriteDraw", 5) - dest.y + 1; + } + if ((n == 4) || (n == 6)) { + center = _argBoolean(L, "spriteDraw", n); + } + + // Advance animation, if any. + if ((sprite->animation != NULL) && sprite->animating) { + now = SDL_GetTicks(); + sprite->ticks += now - sprite->lastTick; + sprite->lastTick = now; + while (sprite->animating) { + delay = sprite->animation->delays[sprite->currentFrame]; + if (delay < ANIMATION_MIN_DELAY_MS) { + delay = ANIMATION_MIN_DELAY_MS; + } + if (sprite->ticks < (uint32_t)delay) { + break; + } + sprite->ticks -= (uint32_t)delay; + sprite->currentFrame++; + newFrame = true; + if (sprite->currentFrame >= sprite->animation->count) { + if (sprite->loop) { + sprite->currentFrame = 0; + } else { + sprite->currentFrame = sprite->animation->count - 1; + sprite->animating = false; + } + } + } + if (newFrame) { + sprite->originalSurface = sprite->animation->frames[sprite->currentFrame]; + _spriteRebuildSurface(sprite); + } + } + + if (!stretched) { + dest.w = sprite->surface->w; + dest.h = sprite->surface->h; + } + if (center) { + // Move sprite so the drawing coordinate is the center of the sprite + dest.x -= dest.w / 2; + dest.y -= dest.h / 2; + } + if (stretched) { + SDL_BlitScaled(sprite->surface, NULL, _global.overlay, &dest); + } else { + SDL_BlitSurface(sprite->surface, NULL, _global.overlay, &dest); + } + _overlayTouched(); + _luaTrace(L, "spriteDraw", "%d %d %d %d %d %d", sprite->id, dest.x, dest.y, dest.w, dest.h, center); + + return 0; +} + + +// frame = spriteGetFrame(id) +static int32_t apiSpriteGetFrame(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spriteGetFrame", 1, 1); + sprite = _argSprite(L, "spriteGetFrame", 1); + _luaTrace(L, "spriteGetFrame", "%d %d", sprite->id, sprite->currentFrame); + lua_pushinteger(L, sprite->currentFrame); + + return 1; +} + + +// height = spriteGetHeight(id) Height as drawn, after scaling and rotation. +static int32_t apiSpriteGetHeight(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spriteGetHeight", 1, 1); + sprite = _argSprite(L, "spriteGetHeight", 1); + _luaTrace(L, "spriteGetHeight", "%d %d", sprite->id, sprite->surface->h); + lua_pushinteger(L, sprite->surface->h); + + return 1; +} + + +// width = spriteGetWidth(id) Width as drawn, after scaling and rotation. +static int32_t apiSpriteGetWidth(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spriteGetWidth", 1, 1); + sprite = _argSprite(L, "spriteGetWidth", 1); + _luaTrace(L, "spriteGetWidth", "%d %d", sprite->id, sprite->surface->w); + lua_pushinteger(L, sprite->surface->w); + + return 1; +} + + +// playing = spriteIsPlaying(id) +static int32_t apiSpriteIsPlaying(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spriteIsPlaying", 1, 1); + sprite = _argSprite(L, "spriteIsPlaying", 1); + _luaTrace(L, "spriteIsPlaying", "%d %d", sprite->id, sprite->animating); + lua_pushboolean(L, sprite->animating); + + return 1; +} + + +// id = spriteLoad(filename) Animated GIF and WEBP files load as animations. +static int32_t apiSpriteLoad(lua_State *L) { + const char *name = NULL; + SpriteT *sprite = NULL; + int32_t x = 0; + + _argCheck(L, "spriteLoad", 1, 1); + name = _argString(L, "spriteLoad", 1); + sprite = (SpriteT *)calloc(1, sizeof(SpriteT)); + if (!sprite) { + _luaDie(L, "spriteLoad", "Unable to allocate new sprite."); + } + // Try to load requested file as an animation first + sprite->animation = IMG_LoadAnimation(name); + if ((sprite->animation != NULL) && (sprite->animation->count < 2)) { + // Only one frame - keep it as a still image. + sprite->originalSurface = _surfaceCopy(sprite->animation->frames[0]); + IMG_FreeAnimation(sprite->animation); + sprite->animation = NULL; + } else { + if (sprite->animation != NULL) { + for (x = 0; x < sprite->animation->count; x++) { + SDL_SetColorKey(sprite->animation->frames[x], SDL_TRUE, COLOR_KEY_VALUE); + } + sprite->originalSurface = sprite->animation->frames[0]; + } else { + sprite->originalSurface = IMG_Load(name); + } + } + if (!sprite->originalSurface) { + _luaDie(L, "spriteLoad", "%s", IMG_GetError()); + } + SDL_SetColorKey(sprite->originalSurface, SDL_TRUE, COLOR_KEY_VALUE); + sprite->surface = sprite->originalSurface; + sprite->scaleX = 1.0; + sprite->scaleY = 1.0; + sprite->id = _global.nextSpriteId++; + HASH_ADD_INT(_global.spriteList, id, sprite); + _luaTrace(L, "spriteLoad", "%d %s", sprite->id, name); + lua_pushinteger(L, sprite->id); + + return 1; +} + + +// spriteLoop(id, loop) +static int32_t apiSpriteLoop(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spriteLoop", 2, 2); + sprite = _argSprite(L, "spriteLoop", 1); + sprite->loop = _argBoolean(L, "spriteLoop", 2); + _luaTrace(L, "spriteLoop", "%d %d", sprite->id, sprite->loop); + + return 0; +} + + +// spritePause(id) +static int32_t apiSpritePause(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spritePause", 1, 1); + sprite = _argSprite(L, "spritePause", 1); + sprite->animating = false; + _luaTrace(L, "spritePause", "%d", sprite->id); + + return 0; +} + + +// spritePlay(id) +static int32_t apiSpritePlay(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spritePlay", 1, 1); + sprite = _argSprite(L, "spritePlay", 1); + if (!sprite->animating) { + sprite->lastTick = SDL_GetTicks(); + sprite->animating = true; + } + _luaTrace(L, "spritePlay", "%d", sprite->id); + + return 0; +} + + +// spriteQuality(id, RENDER_PIXELATED | RENDER_SMOOTH) +static int32_t apiSpriteQuality(lua_State *L) { + SpriteT *sprite = NULL; + int32_t smooth = 0; + + _argCheck(L, "spriteQuality", 2, 2); + sprite = _argSprite(L, "spriteQuality", 1); + smooth = _argInteger(L, "spriteQuality", 2) ? RENDER_SMOOTH : RENDER_PIXELATED; + if (smooth != sprite->smooth) { + sprite->smooth = smooth; + _spriteRebuildSurface(sprite); + } + _luaTrace(L, "spriteQuality", "%d %d", sprite->id, sprite->smooth); + + return 0; +} + + +// spriteRotate(id, degrees) Clockwise. +static int32_t apiSpriteRotate(lua_State *L) { + SpriteT *sprite = NULL; + double angle = 0.0; + + _argCheck(L, "spriteRotate", 2, 2); + sprite = _argSprite(L, "spriteRotate", 1); + angle = fmod(_argNumber(L, "spriteRotate", 2), DEGREES_PER_CIRCLE); + if (angle != sprite->angle) { + sprite->angle = angle; + _spriteRebuildSurface(sprite); + } + _luaTrace(L, "spriteRotate", "%d %f", sprite->id, sprite->angle); + + return 0; +} + + +// spriteRotateAndScale(id, degrees, scale) or spriteRotateAndScale(id, degrees, scaleX, scaleY) +static int32_t apiSpriteRotateAndScale(lua_State *L) { + int32_t n = lua_gettop(L); + SpriteT *sprite = NULL; + double angle = 0.0; + double scaleX = 1.0; + double scaleY = 1.0; + + _argCheck(L, "spriteRotateAndScale", 3, 4); + sprite = _argSprite(L, "spriteRotateAndScale", 1); + angle = fmod(_argNumber(L, "spriteRotateAndScale", 2), DEGREES_PER_CIRCLE); + scaleX = _argNumber(L, "spriteRotateAndScale", 3); + scaleY = (n == 4) ? _argNumber(L, "spriteRotateAndScale", 4) : scaleX; + if ((angle != sprite->angle) || (scaleX != sprite->scaleX) || (scaleY != sprite->scaleY)) { + sprite->angle = angle; + sprite->scaleX = scaleX; + sprite->scaleY = scaleY; + _spriteRebuildSurface(sprite); + } + _luaTrace(L, "spriteRotateAndScale", "%d %f %f %f", sprite->id, sprite->angle, sprite->scaleX, sprite->scaleY); + + return 0; +} + + +// spriteScale(id, scale) or spriteScale(id, scaleX, scaleY) +static int32_t apiSpriteScale(lua_State *L) { + int32_t n = lua_gettop(L); + SpriteT *sprite = NULL; + double scaleX = 1.0; + double scaleY = 1.0; + + _argCheck(L, "spriteScale", 2, 3); + sprite = _argSprite(L, "spriteScale", 1); + scaleX = _argNumber(L, "spriteScale", 2); + scaleY = (n == 3) ? _argNumber(L, "spriteScale", 3) : scaleX; + if ((scaleX != sprite->scaleX) || (scaleY != sprite->scaleY)) { + sprite->scaleX = scaleX; + sprite->scaleY = scaleY; + _spriteRebuildSurface(sprite); + } + _luaTrace(L, "spriteScale", "%d %f %f", sprite->id, sprite->scaleX, sprite->scaleY); + + return 0; +} + + +// spriteSetFrame(id, frame) Ignored for still images and out of range frames. +static int32_t apiSpriteSetFrame(lua_State *L) { + SpriteT *sprite = NULL; + int32_t frame = 0; + + _argCheck(L, "spriteSetFrame", 2, 2); + sprite = _argSprite(L, "spriteSetFrame", 1); + frame = _argInteger(L, "spriteSetFrame", 2); + if ((sprite->animation != NULL) && (frame >= 0) && (frame < sprite->animation->count) && (frame != sprite->currentFrame)) { + sprite->currentFrame = frame; + sprite->ticks = 0; + sprite->originalSurface = sprite->animation->frames[frame]; + _spriteRebuildSurface(sprite); + } + _luaTrace(L, "spriteSetFrame", "%d %d", sprite->id, sprite->currentFrame); + + return 0; +} + + +// spriteUnload(id) +static int32_t apiSpriteUnload(lua_State *L) { + SpriteT *sprite = NULL; + + _argCheck(L, "spriteUnload", 1, 1); + sprite = _argSprite(L, "spriteUnload", 1); + _luaTrace(L, "spriteUnload", "%d", sprite->id); + _spriteDestroy(sprite); + + return 0; +} + + +// videoDraw(id, x, y, x2, y2) - Stretch the frame into the rectangle +// videoDraw(id, x, y, centered) - Draw with the video's rotation and scale +static int32_t apiVideoDraw(lua_State *L) { + int32_t n = lua_gettop(L); + VideoT *video = NULL; + bool center = false; + bool newFrame = false; + int64_t frame = 0; + const uint8_t *pixels = NULL; + int32_t pitch = 0; + SDL_Surface *source = NULL; + SDL_Surface *rgba = NULL; + SDL_Rect dest; + + _argCheck(L, "videoDraw", 4, 5); + video = _argVideo(L, "videoDraw", 1); + dest.x = _argInteger(L, "videoDraw", 2); + dest.y = _argInteger(L, "videoDraw", 3); + dest.w = 0; + dest.h = 0; + if (n == 5) { + dest.w = _argInteger(L, "videoDraw", 4) - dest.x + 1; + dest.h = _argInteger(L, "videoDraw", 5) - dest.y + 1; + } else { + center = _argBoolean(L, "videoDraw", 4); + } + + // Advance the video and wrap its decoded frame without copying it. + frame = videoUpdate(video->handle, &video->texture); + if (!videoGetPixels(video->handle, &pixels, &pitch)) { + _luaTrace(L, "videoDraw", "%d no frame yet", video->id); + return 0; + } + newFrame = (frame != video->lastFrame); + video->lastFrame = frame; + source = SDL_CreateRGBSurfaceWithFormatFrom((void *)pixels, videoGetWidth(video->handle), videoGetHeight(video->handle), 32, pitch, VIDEO_SURFACE_FORMAT); + if (source == NULL) { + utilDie("%s", SDL_GetError()); + } + + if (n == 5) { + // Simple/Stretched draw + SDL_BlitScaled(source, NULL, _global.overlay, &dest); + } else { + if ((video->angle == 0.0) && (video->scaleX == 1.0) && (video->scaleY == 1.0)) { + // Untransformed: draw the frame directly. + dest.w = source->w; + dest.h = source->h; + if (center) { + dest.x -= dest.w / 2; + dest.y -= dest.h / 2; + } + SDL_BlitSurface(source, NULL, _global.overlay, &dest); + } else { + // Rebuild the transformed frame only when something changed. + if (newFrame || video->transformChanged || (video->transformedSurface == NULL)) { + SDL_FreeSurface(video->transformedSurface); + // Give the frame an alpha channel so rotated corners come out transparent. + rgba = SDL_ConvertSurfaceFormat(source, SDL_PIXELFORMAT_RGBA32, 0); + if (rgba == NULL) { + utilDie("%s", SDL_GetError()); + } + video->transformedSurface = rotozoomSurfaceXY(rgba, -video->angle, video->scaleX, video->scaleY, video->smooth); + SDL_FreeSurface(rgba); + if (video->transformedSurface == NULL) { + utilDie("Unable to transform video %d.", video->id); + } + video->transformChanged = false; + } + dest.w = video->transformedSurface->w; + dest.h = video->transformedSurface->h; + if (center) { + dest.x -= dest.w / 2; + dest.y -= dest.h / 2; + } + SDL_BlitSurface(video->transformedSurface, NULL, _global.overlay, &dest); + } + } + SDL_FreeSurface(source); + _overlayTouched(); + _luaTrace(L, "videoDraw", "%d %d %d %d %d %" PRId64, video->id, dest.x, dest.y, dest.w, dest.h, frame); + + return 0; +} + + +// track = videoGetAudioTrack(id) +static int32_t apiVideoGetAudioTrack(lua_State *L) { + VideoT *video = NULL; + int32_t track = 0; + + _argCheck(L, "videoGetAudioTrack", 1, 1); + video = _argVideo(L, "videoGetAudioTrack", 1); + track = videoGetAudioTrack(video->handle); + _luaTrace(L, "videoGetAudioTrack", "%d %d", video->id, track); + lua_pushinteger(L, track); + + return 1; +} + + +// count = videoGetAudioTracks(id) +static int32_t apiVideoGetAudioTracks(lua_State *L) { + VideoT *video = NULL; + int32_t count = 0; + + _argCheck(L, "videoGetAudioTracks", 1, 1); + video = _argVideo(L, "videoGetAudioTracks", 1); + count = videoGetAudioTracks(video->handle); + _luaTrace(L, "videoGetAudioTracks", "%d %d", video->id, count); + lua_pushinteger(L, count); + + return 1; +} + + +// frame = videoGetFrame(id) +static int32_t apiVideoGetFrame(lua_State *L) { + VideoT *video = NULL; + int64_t frame = 0; + + _argCheck(L, "videoGetFrame", 1, 1); + video = _argVideo(L, "videoGetFrame", 1); + frame = videoGetFrame(video->handle); + _luaTrace(L, "videoGetFrame", "%d %" PRId64, video->id, frame); + lua_pushinteger(L, frame); + + return 1; +} + + +// count = videoGetFrameCount(id) +static int32_t apiVideoGetFrameCount(lua_State *L) { + VideoT *video = NULL; + int64_t count = 0; + + _argCheck(L, "videoGetFrameCount", 1, 1); + video = _argVideo(L, "videoGetFrameCount", 1); + count = videoGetFrameCount(video->handle); + _luaTrace(L, "videoGetFrameCount", "%d %" PRId64, video->id, count); + lua_pushinteger(L, count); + + return 1; +} + + +// height = videoGetHeight(id) +static int32_t apiVideoGetHeight(lua_State *L) { + VideoT *video = NULL; + int32_t height = 0; + + _argCheck(L, "videoGetHeight", 1, 1); + video = _argVideo(L, "videoGetHeight", 1); + height = videoGetHeight(video->handle); + _luaTrace(L, "videoGetHeight", "%d %d", video->id, height); + lua_pushinteger(L, height); + + return 1; +} + + +// code = videoGetLanguage(id, track) +static int32_t apiVideoGetLanguage(lua_State *L) { + VideoT *video = NULL; + int32_t track = 0; + const char *language = NULL; + + _argCheck(L, "videoGetLanguage", 2, 2); + video = _argVideo(L, "videoGetLanguage", 1); + track = _argInteger(L, "videoGetLanguage", 2); + if ((track < 0) || (track >= videoGetAudioTracks(video->handle))) { + _luaDie(L, "videoGetLanguage", "Invalid audio track %d in video %d.", track, video->id); + } + language = videoGetLanguage(video->handle, track); + _luaTrace(L, "videoGetLanguage", "%d %d %s", video->id, track, language); + lua_pushstring(L, language); + + return 1; +} + + +// name = videoGetLanguageDescription(code) English name for an ISO 639 code. +static int32_t apiVideoGetLanguageDescription(lua_State *L) { + const char *code = NULL; + const char *description = NULL; + + _argCheck(L, "videoGetLanguageDescription", 1, 1); + code = _argString(L, "videoGetLanguageDescription", 1); + description = videoGetLanguageDescription(code); + _luaTrace(L, "videoGetLanguageDescription", "%s %s", code, description); + lua_pushstring(L, description); + + return 1; +} + + +// left, right = videoGetVolume(id) +static int32_t apiVideoGetVolume(lua_State *L) { + VideoT *video = NULL; + int32_t left = 0; + int32_t right = 0; + + _argCheck(L, "videoGetVolume", 1, 1); + video = _argVideo(L, "videoGetVolume", 1); + videoGetVolume(video->handle, &left, &right); + _luaTrace(L, "videoGetVolume", "%d %d %d", video->id, left, right); + lua_pushinteger(L, left); + lua_pushinteger(L, right); + + return 2; +} + + +// width = videoGetWidth(id) +static int32_t apiVideoGetWidth(lua_State *L) { + VideoT *video = NULL; + int32_t width = 0; + + _argCheck(L, "videoGetWidth", 1, 1); + video = _argVideo(L, "videoGetWidth", 1); + width = videoGetWidth(video->handle); + _luaTrace(L, "videoGetWidth", "%d %d", video->id, width); + lua_pushinteger(L, width); + + return 1; +} + + +// playing = videoIsPlaying(id) +static int32_t apiVideoIsPlaying(lua_State *L) { + VideoT *video = NULL; + bool playing = false; + + _argCheck(L, "videoIsPlaying", 1, 1); + video = _argVideo(L, "videoIsPlaying", 1); + playing = videoIsPlaying(video->handle); + _luaTrace(L, "videoIsPlaying", "%d %d", video->id, playing); + lua_pushboolean(L, playing); + + return 1; +} + + +// id = videoLoad(filename) +static int32_t apiVideoLoad(lua_State *L) { + const char *name = NULL; + char *dataDir = NULL; + VideoT *video = NULL; + + _argCheck(L, "videoLoad", 1, 1); + name = _argString(L, "videoLoad", 1); + // The index file lives in a data directory named for the video's directory. + dataDir = createDataDir(_global.conf->dataDirBase, name); + if (dataDir == NULL) { + _luaDie(L, "videoLoad", "Unable to create data directory for %s.", name); + } + video = (VideoT *)calloc(1, sizeof(VideoT)); + if (!video) { + _luaDie(L, "videoLoad", "Unable to allocate new video."); + } + video->handle = videoLoad(name, NULL, dataDir, _global.renderer); + video->id = _global.nextVideoId++; + video->lastFrame = -1; + video->scaleX = 1.0; + video->scaleY = 1.0; + HASH_ADD_INT(_global.videoList, id, video); + _selectDefaultAudioTrack(video->handle); + videoSetVolume(video->handle, _global.conf->volumeNonVldp, _global.conf->volumeNonVldp); + _luaTrace(L, "videoLoad", "%s %s %d", name, dataDir, video->id); + free(dataDir); + lua_pushinteger(L, video->id); + + return 1; +} + + +// videoPause(id) +static int32_t apiVideoPause(lua_State *L) { + VideoT *video = NULL; + + _argCheck(L, "videoPause", 1, 1); + video = _argVideo(L, "videoPause", 1); + videoPause(video->handle); + _luaTrace(L, "videoPause", "%d", video->id); + + return 0; +} + + +// videoPlay(id) +static int32_t apiVideoPlay(lua_State *L) { + VideoT *video = NULL; + + _argCheck(L, "videoPlay", 1, 1); + video = _argVideo(L, "videoPlay", 1); + videoPlay(video->handle); + _luaTrace(L, "videoPlay", "%d", video->id); + + return 0; +} + + +// videoQuality(id, RENDER_PIXELATED | RENDER_SMOOTH) +static int32_t apiVideoQuality(lua_State *L) { + VideoT *video = NULL; + int32_t smooth = 0; + + _argCheck(L, "videoQuality", 2, 2); + video = _argVideo(L, "videoQuality", 1); + smooth = _argInteger(L, "videoQuality", 2) ? RENDER_SMOOTH : RENDER_PIXELATED; + if (smooth != video->smooth) { + video->smooth = smooth; + video->transformChanged = true; + } + _luaTrace(L, "videoQuality", "%d %d", video->id, video->smooth); + + return 0; +} + + +// videoRotate(id, degrees) +static int32_t apiVideoRotate(lua_State *L) { + VideoT *video = NULL; + double angle = 0.0; + + _argCheck(L, "videoRotate", 2, 2); + video = _argVideo(L, "videoRotate", 1); + angle = fmod(_argNumber(L, "videoRotate", 2), DEGREES_PER_CIRCLE); + if (angle != video->angle) { + video->angle = angle; + video->transformChanged = true; + } + _luaTrace(L, "videoRotate", "%d %f", video->id, video->angle); + + return 0; +} + + +// videoRotateAndScale(id, degrees, scale) or videoRotateAndScale(id, degrees, scaleX, scaleY) +static int32_t apiVideoRotateAndScale(lua_State *L) { + int32_t n = lua_gettop(L); + VideoT *video = NULL; + double angle = 0.0; + double scaleX = 1.0; + double scaleY = 1.0; + + _argCheck(L, "videoRotateAndScale", 3, 4); + video = _argVideo(L, "videoRotateAndScale", 1); + angle = fmod(_argNumber(L, "videoRotateAndScale", 2), DEGREES_PER_CIRCLE); + scaleX = _argNumber(L, "videoRotateAndScale", 3); + scaleY = (n == 4) ? _argNumber(L, "videoRotateAndScale", 4) : scaleX; + if ((angle != video->angle) || (scaleX != video->scaleX) || (scaleY != video->scaleY)) { + video->angle = angle; + video->scaleX = scaleX; + video->scaleY = scaleY; + video->transformChanged = true; + } + _luaTrace(L, "videoRotateAndScale", "%d %f %f %f", video->id, video->angle, video->scaleX, video->scaleY); + + return 0; +} + + +// videoScale(id, scale) or videoScale(id, scaleX, scaleY) +static int32_t apiVideoScale(lua_State *L) { + int32_t n = lua_gettop(L); + VideoT *video = NULL; + double scaleX = 1.0; + double scaleY = 1.0; + + _argCheck(L, "videoScale", 2, 3); + video = _argVideo(L, "videoScale", 1); + scaleX = _argNumber(L, "videoScale", 2); + scaleY = (n == 3) ? _argNumber(L, "videoScale", 3) : scaleX; + if ((scaleX != video->scaleX) || (scaleY != video->scaleY)) { + video->scaleX = scaleX; + video->scaleY = scaleY; + video->transformChanged = true; + } + _luaTrace(L, "videoScale", "%d %f %f", video->id, video->scaleX, video->scaleY); + + return 0; +} + + +// videoSeek(id, frame) +static int32_t apiVideoSeek(lua_State *L) { + VideoT *video = NULL; + int64_t frame = 0; + + _argCheck(L, "videoSeek", 2, 2); + video = _argVideo(L, "videoSeek", 1); + frame = _argInteger64(L, "videoSeek", 2); + videoSeek(video->handle, frame); + _luaTrace(L, "videoSeek", "%d %" PRId64, video->id, frame); + + return 0; +} + + +// videoSetAudioTrack(id, track) +static int32_t apiVideoSetAudioTrack(lua_State *L) { + VideoT *video = NULL; + int32_t track = 0; + + _argCheck(L, "videoSetAudioTrack", 2, 2); + video = _argVideo(L, "videoSetAudioTrack", 1); + track = _argInteger(L, "videoSetAudioTrack", 2); + if ((track < 0) || (track >= videoGetAudioTracks(video->handle))) { + _luaDie(L, "videoSetAudioTrack", "Invalid audio track %d in video %d.", track, video->id); + } + videoSetAudioTrack(video->handle, track); + _luaTrace(L, "videoSetAudioTrack", "%d %d", video->id, track); + + return 0; +} + + +// videoSetVolume(id, left, right) Percent, clamped to 0..100. +static int32_t apiVideoSetVolume(lua_State *L) { + VideoT *video = NULL; + int32_t left = 0; + int32_t right = 0; + + _argCheck(L, "videoSetVolume", 3, 3); + video = _argVideo(L, "videoSetVolume", 1); + left = _argInteger(L, "videoSetVolume", 2); + right = _argInteger(L, "videoSetVolume", 3); + if (left < 0) { + left = 0; + } + if (left > VIDEO_VOLUME_MAX) { + left = VIDEO_VOLUME_MAX; + } + if (right < 0) { + right = 0; + } + if (right > VIDEO_VOLUME_MAX) { + right = VIDEO_VOLUME_MAX; + } + videoSetVolume(video->handle, left, right); + _luaTrace(L, "videoSetVolume", "%d %d %d", video->id, left, right); + + return 0; +} + + +// videoUnload(id) +static int32_t apiVideoUnload(lua_State *L) { + VideoT *video = NULL; + + _argCheck(L, "videoUnload", 1, 1); + video = _argVideo(L, "videoUnload", 1); + _luaTrace(L, "videoUnload", "%d", video->id); + _videoDestroy(video); + + return 0; +} + + +// r, g, b = vldpGetPixel(x, y) Overlay coordinates; reads the current laserdisc frame. +static int32_t apiVldpGetPixel(lua_State *L) { + int32_t x = 0; + int32_t y = 0; + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + + _argCheck(L, "vldpGetPixel", 2, 2); + x = (int32_t)(_argNumber(L, "vldpGetPixel", 1) / _global.overlayScaleX); + y = (int32_t)(_argNumber(L, "vldpGetPixel", 2) / _global.overlayScaleY); + if (_global.videoHandle >= 0) { + videoGetPixel(_global.videoHandle, x, y, &r, &g, &b); + } + _luaTrace(L, "vldpGetPixel", "%d %d %d %d %d", x, y, r, g, b); + lua_pushinteger(L, r); + lua_pushinteger(L, g); + lua_pushinteger(L, b); + + return 3; +} + + +static int32_t apiVldpSetVerbose(lua_State *L) { + return _apiUnimplemented(L, "vldpSetVerbose"); +} + + +// ===== Engine entry point ===== + + void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { - int32_t x = 0; - int32_t y = 0; - int32_t xr = 0; - int32_t yr = 0; - int32_t intReturn = 0; - int32_t axisIndex = 0; - int64_t thisFrame = -1; - int64_t lastFrame = -1; - uint32_t frameClock = 0; - bool changed = false; - char *temp = NULL; - char *temp2 = NULL; - SDL_Rect windowTarget; - SDL_Rect sindenWhite; - SDL_Rect sindenBlack; - SDL_Color sindenWhiteColor = { 255, 255, 255, 255 }; - SDL_Color sindenBlackColor = { 0, 0, 0, 255 };; - SDL_Texture *overlayTexture = NULL; - SpriteT *sprite = NULL; - SpriteT *spriteTemp = NULL; - SoundT *sound = NULL; - SoundT *soundTemp = NULL; - FontT *font = NULL; - FontT *fontTemp = NULL; - VideoT *video = NULL; - VideoT *videoTemp = NULL; - SDL_Event event; - ManyMouseEvent mouseEvent; - MouseT *mouse = NULL; - int32_t lastAnalogDirection[AXIS_COUNT]; - //float val = 0; - //float maxval = 0; - int32_t lastMouseX = 0; - int32_t lastMouseY = 0; + int32_t x = 0; + int32_t y = 0; + int32_t xr = 0; + int32_t yr = 0; + int32_t slot = 0; + int32_t axisIndex = 0; + int32_t code = 0; + int32_t intReturn = 0; + int32_t videoWidth = 0; + int32_t videoHeight = 0; + int64_t thisFrame = -1; + int64_t lastFrame = -1; + uint32_t frameClock = 0; + char *temp = NULL; + char *temp2 = NULL; + SDL_Rect windowTarget; + SDL_Rect sindenWhite; + SDL_Rect sindenBlack; + SDL_Color sindenWhiteColor = { 255, 255, 255, SDL_ALPHA_OPAQUE }; + SDL_Color sindenBlackColor = { 0, 0, 0, SDL_ALPHA_OPAQUE }; + SpriteT *sprite = NULL; + SpriteT *spriteTemp = NULL; + SoundT *sound = NULL; + SoundT *soundTemp = NULL; + FontT *font = NULL; + FontT *fontTemp = NULL; + VideoT *video = NULL; + VideoT *videoTemp = NULL; + SDL_Event event; + ManyMouseEvent mouseEvent; + MouseT *mouse = NULL; + int32_t finished[SOUND_QUEUE_SIZE]; + int32_t finishedCount = 0; - // Set up globals - memset(&_global, 0, sizeof(GlobalT)); - _global.colorForeground.r = 255; - _global.colorForeground.g = 255; - _global.colorForeground.b = 255; - _global.colorForeground.a = 255; - _global.effectsVolume = AUDIO_MAX_VOLUME; - _global.keyboardMode = KEYBD_NORMAL; - _global.frameFileHandle = -1; - _global.videoHandle = -1; - _global.fontQuality = FONT_QUALITY_SOLID; - _global.mouseMode = MOUSE_SINGLE; - _global.overlayScaleX = 1; - _global.overlayScaleY = 1; - _global.pauseEnabled = true; - _global.running = true; - _global.discStopped = true; - _global.mouseEnabled = true; + // Set up globals + memset(&_global, 0, sizeof(GlobalT)); + _global.colorForeground.r = SDL_ALPHA_OPAQUE; + _global.colorForeground.g = SDL_ALPHA_OPAQUE; + _global.colorForeground.b = SDL_ALPHA_OPAQUE; + _global.colorForeground.a = SDL_ALPHA_OPAQUE; + _global.effectsVolume = AUDIO_MAX_VOLUME; + _global.keyboardMode = KEYBOARD_NORMAL; + _global.frameFileHandle = -1; + _global.videoHandle = -1; + _global.fontQuality = FONT_QUALITY_SOLID; + _global.mouseMode = MOUSE_SINGLE; + _global.overlayScaleX = OVERLAY_SCALE_DEFAULT; + _global.overlayScaleY = OVERLAY_SCALE_DEFAULT; + _global.controllerDeadZone = CONTROLLER_DEAD_ZONE_DEFAULT; + _global.pauseEnabled = true; + _global.running = true; + _global.discStopped = true; + _global.mouseEnabled = true; + _global.window = window; + _global.renderer = renderer; - // Local copy of config - _global.conf = cloneConf(conf); + // Local copy of config + _global.conf = cloneConf(conf); - // Input mappings - _global.controlMappings[INPUT_UP].name = "INPUT_UP"; - _global.controlMappings[INPUT_LEFT].name = "INPUT_LEFT"; - _global.controlMappings[INPUT_DOWN].name = "INPUT_DOWN"; - _global.controlMappings[INPUT_RIGHT].name = "INPUT_RIGHT"; - _global.controlMappings[INPUT_1P_START].name = "INPUT_1P_START"; - _global.controlMappings[INPUT_2P_START].name = "INPUT_2P_START"; - _global.controlMappings[INPUT_ACTION_1].name = "INPUT_ACTION_1"; - _global.controlMappings[INPUT_ACTION_2].name = "INPUT_ACTION_2"; - _global.controlMappings[INPUT_ACTION_3].name = "INPUT_ACTION_3"; - _global.controlMappings[INPUT_1P_COIN].name = "INPUT_1P_COIN"; - _global.controlMappings[INPUT_2P_COIN].name = "INPUT_2P_COIN"; - _global.controlMappings[INPUT_SKILL_EASY].name = "INPUT_SKILL_EASY"; - _global.controlMappings[INPUT_SKILL_MEDIUM].name = "INPUT_SKILL_MEDIUM"; - _global.controlMappings[INPUT_SKILL_HARD].name = "INPUT_SKILL_HARD"; - _global.controlMappings[INPUT_SERVICE].name = "INPUT_SERVICE"; - _global.controlMappings[INPUT_TEST_MODE].name = "INPUT_TEST_MODE"; - _global.controlMappings[INPUT_RESET_CPU].name = "INPUT_RESET_CPU"; - _global.controlMappings[INPUT_SCREENSHOT].name = "INPUT_SCREENSHOT"; - _global.controlMappings[INPUT_QUIT].name = "INPUT_QUIT"; - _global.controlMappings[INPUT_PAUSE].name = "INPUT_PAUSE"; - _global.controlMappings[INPUT_CONSOLE].name = "INPUT_CONSOLE"; - _global.controlMappings[INPUT_ACTION_4].name = "INPUT_ACTION_4"; - _global.controlMappings[INPUT_TILT].name = "INPUT_TILT"; - _global.controlMappings[INPUT_GRAB].name = "INPUT_GRAB"; - for (x=0; xdataDir, utilGetPathSeparator()); + _loadControlsFile(temp); + free(temp); + temp = utilCreateString("%scontrols.cfg", _global.conf->dataDir); + _loadControlsFile(temp); + free(temp); + temp2 = utilGetUpToLastPathComponent(_global.conf->scriptFile); + temp = utilCreateString("%scontrols.cfg", temp2); + _loadControlsFile(temp); + free(temp); + free(temp2); + // Parse results + lua_getglobal(_global.luaContext, "DEAD_ZONE"); + if (lua_isnumber(_global.luaContext, -1)) { + _global.controllerDeadZone = (int32_t)lua_tonumber(_global.luaContext, -1); + } + lua_pop(_global.luaContext, 1); + _progTrace("Controller dead zone is %d", _global.controllerDeadZone); + for (x = 0; x < INPUT_COUNT; x++) { + // Each INPUT_* table holds { name = ..., value = ... } entries; collect the values. + lua_getglobal(_global.luaContext, _inputNames[x].configName); + if (!lua_istable(_global.luaContext, -1)) { + utilSay("Configuration option %s missing!", _inputNames[x].configName); + lua_pop(_global.luaContext, 1); + continue; + } + y = (int32_t)lua_rawlen(_global.luaContext, -1); + _global.controlMappings[x].input = (int32_t *)calloc((size_t)(y + 1), sizeof(int32_t)); + if (!_global.controlMappings[x].input) { + utilDie("Unable to allocate memory for control mappings."); + } + _global.controlMappings[x].inputCount = 0; + lua_pushnil(_global.luaContext); + while (lua_next(_global.luaContext, -2)) { + if (lua_istable(_global.luaContext, -1)) { + lua_getfield(_global.luaContext, -1, "value"); + if (lua_isnumber(_global.luaContext, -1) && (_global.controlMappings[x].inputCount < y)) { + _global.controlMappings[x].input[_global.controlMappings[x].inputCount++] = (int32_t)lua_tonumber(_global.luaContext, -1); + } + lua_pop(_global.luaContext, 1); + } + lua_pop(_global.luaContext, 1); + } + lua_pop(_global.luaContext, 1); + } + lua_close(_global.luaContext); - // Hang on to some SDL stuff - _global.window = window; - _global.renderer = renderer; + // Show splash screens + if (!_global.conf->noLogos) { + _progTrace("Showing splash screens"); + _doLogos(); + } - // Load controller mappings - progTrace("Creating Lua context for Singe setup"); - _global.luaContext = luaL_newstate(); - startLuaContext(_global.luaContext); - // Load framework - NOTE! SINGE API NOT AVAILABLE AT THIS POINT! - // Any calls in the framework need to be wrapped with nil checks! - progTrace("Loading Singe framework"); - if (luaL_loadbuffer(_global.luaContext, (char *)Framework_singe, Framework_singe_len, "Input Mappings") || lua_pcall(_global.luaContext, 0, 0, 0)) utilDie("%s", lua_tostring(_global.luaContext, -1)); - // Load default mappings - progTrace("Loading default control mappings"); - if (luaL_loadbuffer(_global.luaContext, (char *)controls_cfg, controls_cfg_len, "Input Mappings") || lua_pcall(_global.luaContext, 0, 0, 0)) utilDie("%s", lua_tostring(_global.luaContext, -1)); - if (utilFileExists("controls.cfg")) { - progTrace("Loading controls.cfg"); - if (luaL_dofile(_global.luaContext, "controls.cfg")) utilDie("%s", lua_tostring(_global.luaContext, -1)); - } - // Load mappings in main data dir - temp = utilCreateString("%s..%ccontrols.cfg", _global.conf->dataDir, utilGetPathSeparator()); - if (utilFileExists(temp)) { - progTrace("Loading %s", temp); - if (luaL_dofile(_global.luaContext, temp)) utilDie("%s", lua_tostring(_global.luaContext, -1)); - } - free(temp); - // Load mappings in game data dir - temp = utilCreateString("%scontrols.cfg", _global.conf->dataDir); - if (utilFileExists(temp)) { - progTrace("Loading %s", temp); - if (luaL_dofile(_global.luaContext, temp)) utilDie("%s", lua_tostring(_global.luaContext, -1)); - } - free(temp); - // Load mappings in game script dir - temp = strdup(_global.conf->scriptFile); - temp2 = utilStrndup(temp, strlen(temp) - strlen(utilGetLastPathComponent(temp))); - free(temp); - temp = utilCreateString("%scontrols.cfg", temp2); - if (utilFileExists(temp)) { - progTrace("Loading %s", temp); - if (luaL_dofile(_global.luaContext, temp)) utilDie("%s", lua_tostring(_global.luaContext, -1)); - } - free(temp); - free(temp2); - // Parse results - lua_getglobal(_global.luaContext, "DEAD_ZONE"); - if (lua_isnumber(_global.luaContext, -1)) { - _global.controllerDeadZone = (int32_t)lua_tonumber(_global.luaContext, -1); - progTrace("Controller dead zone is %d", _global.controllerDeadZone); - } - lua_pop(_global.luaContext, 1); - for (x=0; x 0) { - _global.controlMappings[x].inputCount = y; - _global.controlMappings[x].input = (int32_t *)malloc(sizeof(int32_t) * y); - if (!_global.controlMappings[x].input) utilDie("Unable to allocate memory for control mappings."); - } - } else { - utilSay("Configuration option %s missing!", _global.controlMappings[x].name); - } - lua_pop(_global.luaContext, 1); - // Then load them for real. - lua_getglobal(_global.luaContext, _global.controlMappings[x].name); - if (lua_istable(_global.luaContext, -1)) { - y = 0; - lua_pushnil(_global.luaContext); - while (lua_next(_global.luaContext, -2)) { - if (lua_istable(_global.luaContext, -1)) { - lua_pushnil(_global.luaContext); - while (lua_next(_global.luaContext, -2)) { - if (lua_type(_global.luaContext, -2) == LUA_TSTRING) { - if (utilStricmp((char *)lua_tostring(_global.luaContext, -2), "value") == 0) { - _global.controlMappings[x].input[y++] = (int32_t)lua_tonumber(_global.luaContext, -1); - } - } - lua_pop(_global.luaContext, 1); - } - } - lua_pop(_global.luaContext, 1); - } - } - lua_pop(_global.luaContext, 1); - } - lua_close(_global.luaContext); + // Start Lua for game + _progTrace("Creating Lua context for script"); + _global.luaContext = luaL_newstate(); + _startLuaContext(_global.luaContext); - // Show splash screens - if (!_global.conf->noLogos) { - progTrace("Showing splash screens"); - doLogos(); - } - - // Start Lua for game - progTrace("Creating Lua context for script"); - _global.luaContext = luaL_newstate(); - startLuaContext(_global.luaContext); - - // Lua API for Singe + // Lua API for Singe. Comments give the version each call appeared in. lua_register(_global.luaContext, "colorBackground", apiColorBackground); // 1.xx lua_register(_global.luaContext, "colorForeground", apiColorForeground); // 1.xx @@ -4773,12 +4211,12 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { lua_register(_global.luaContext, "discGetState", apiDiscGetState); // 1.xx RDG lua_register(_global.luaContext, "discGetWidth", apiDiscGetWidth); // 2.00 lua_register(_global.luaContext, "discPause", apiDiscPause); // 1.xx - lua_register(_global.luaContext, "discPauseAtFrame", apiDiscPauseAtFrame); // 1.18 + lua_register(_global.luaContext, "discPauseAtFrame", apiDiscSearch); // 1.18 Same as discSearch. lua_register(_global.luaContext, "discPlay", apiDiscPlay); // 1.xx lua_register(_global.luaContext, "discSearch", apiDiscSearch); // 1.xx lua_register(_global.luaContext, "discSearchBlanking", apiDiscSearchBlanking); // 1.xx lua_register(_global.luaContext, "discSetAudioTrack", apiDiscSetAudioTrack); // 2.10 - lua_register(_global.luaContext, "discSetFPS", apiDiscSetFps); // 1.xx + lua_register(_global.luaContext, "discSetFPS", apiDiscSetFPS); // 1.xx lua_register(_global.luaContext, "discSkipBackward", apiDiscSkipBackward); // 1.xx lua_register(_global.luaContext, "discSkipBlanking", apiDiscSkipBlanking); // 1.xx lua_register(_global.luaContext, "discSkipForward", apiDiscSkipForward); // 1.xx @@ -4798,14 +4236,13 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { lua_register(_global.luaContext, "keyboardGetLastUp", apiKeyboardGetLastUp); // 2.10 lua_register(_global.luaContext, "keyboardGetMode", apiKeyboardGetMode); // 1.xx RDG lua_register(_global.luaContext, "keyboardGetModifiers", apiKeyboardGetModifiers); // 2.10 - lua_register(_global.luaContext, "keyboardSetMode", apiKeyboardSetMode); // 1.xx RDG lua_register(_global.luaContext, "keyboardIsDown", apiKeyboardIsDown); // 2.10 + lua_register(_global.luaContext, "keyboardSetMode", apiKeyboardSetMode); // 1.xx RDG - lua_register(_global.luaContext, "mouseEnable", apiMouseEnable); // 1.18 RDG - lua_register(_global.luaContext, "mouseDisable", apiMouseDisable); // 1.18 RDG lua_register(_global.luaContext, "mouseGetPosition", apiMouseGetPosition); // 2.00 lua_register(_global.luaContext, "mouseHowMany", apiMouseHowMany); // 1.18 RDG lua_register(_global.luaContext, "mouseSetCaptured", apiMouseSetCaptured); // 2.00 + lua_register(_global.luaContext, "mouseSetEnabled", apiMouseSetEnabled); // 2.20 mouseEnable/mouseDisable are framework aliases. lua_register(_global.luaContext, "mouseSetMode", apiMouseSetMode); // 1.18 RDG lua_register(_global.luaContext, "overlayBox", apiOverlayBox); // 2.00 @@ -4822,17 +4259,16 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { lua_register(_global.luaContext, "scriptExecute", apiScriptExecute); // 2.00 lua_register(_global.luaContext, "scriptPush", apiScriptPush); // 2.00 - lua_register(_global.luaContext, "singeDisablePauseKey", apiSingeDisablePauseKey); // 1.18 RDG - lua_register(_global.luaContext, "singeEnablePauseKey", apiSingeEnablePauseKey); // 1.18 RDG - lua_register(_global.luaContext, "singeGetDataPath", apiSingeGetDataPath); + lua_register(_global.luaContext, "singeGetDataPath", apiSingeGetDataPath); // 2.00 lua_register(_global.luaContext, "singeGetHeight", apiSingeGetHeight); // 1.xx lua_register(_global.luaContext, "singeGetPauseFlag", apiSingeGetPauseFlag); // 1.xx RDG lua_register(_global.luaContext, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG lua_register(_global.luaContext, "singeGetWidth", apiSingeGetWidth); // 1.xx + lua_register(_global.luaContext, "singeQuit", apiSingeQuit); // 1.xx RDG lua_register(_global.luaContext, "singeScreenshot", apiSingeScreenshot); // 1.xx lua_register(_global.luaContext, "singeSetGameName", apiSingeSetGameName); // 1.15 RDG lua_register(_global.luaContext, "singeSetPauseFlag", apiSingeSetPauseFlag); // 1.xx RDG - lua_register(_global.luaContext, "singeQuit", apiSingeQuit); // 1.xx RDG + lua_register(_global.luaContext, "singeSetPauseKeyEnabled", apiSingeSetPauseKeyEnabled); // 2.20 singeEnablePauseKey/singeDisablePauseKey are framework aliases. lua_register(_global.luaContext, "singeVersion", apiSingeVersion); // 1.xx RDG lua_register(_global.luaContext, "singeWantsCrosshairs", apiSingeWantsCrosshairs); // 2.00 @@ -4847,20 +4283,20 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { lua_register(_global.luaContext, "soundStop", apiSoundStop); // 1.xx RDG lua_register(_global.luaContext, "soundUnload", apiSoundUnload); // 2.00 - lua_register(_global.luaContext, "spriteDraw", apiSpriteDraw); // 1.xx + lua_register(_global.luaContext, "spriteDraw", apiSpriteDraw); // 1.xx Handle first since 2.20. lua_register(_global.luaContext, "spriteGetFrame", apiSpriteGetFrame); // 2.10 lua_register(_global.luaContext, "spriteGetHeight", apiSpriteGetHeight); // 2.00 lua_register(_global.luaContext, "spriteGetWidth", apiSpriteGetWidth); // 2.00 lua_register(_global.luaContext, "spriteIsPlaying", apiSpriteIsPlaying); // 2.10 lua_register(_global.luaContext, "spriteLoad", apiSpriteLoad); // 1.xx - lua_register(_global.luaContext, "spriteLoop", apiSpriteLoop); // 2.10 + lua_register(_global.luaContext, "spriteLoop", apiSpriteLoop); // 2.10 Handle first since 2.20. lua_register(_global.luaContext, "spritePause", apiSpritePause); // 2.10 lua_register(_global.luaContext, "spritePlay", apiSpritePlay); // 2.10 - lua_register(_global.luaContext, "spriteQuality", apiSpriteQuality); // 2.10 - lua_register(_global.luaContext, "spriteRotate", apiSpriteRotate); // 2.10 - lua_register(_global.luaContext, "spriteRotateAndScale", apiSpriteRotateAndScale); // 2.10 - lua_register(_global.luaContext, "spriteScale", apiSpriteScale); // 2.10 - lua_register(_global.luaContext, "spriteSetFrame", apiSpriteSetFrame); // 2.10 + lua_register(_global.luaContext, "spriteQuality", apiSpriteQuality); // 2.10 Handle first since 2.20. + lua_register(_global.luaContext, "spriteRotate", apiSpriteRotate); // 2.10 Handle first since 2.20. + lua_register(_global.luaContext, "spriteRotateAndScale", apiSpriteRotateAndScale); // 2.10 Handle first since 2.20. + lua_register(_global.luaContext, "spriteScale", apiSpriteScale); // 2.10 Handle first since 2.20. + lua_register(_global.luaContext, "spriteSetFrame", apiSpriteSetFrame); // 2.10 Handle first since 2.20. lua_register(_global.luaContext, "spriteUnload", apiSpriteUnload); // 2.00 lua_register(_global.luaContext, "videoDraw", apiVideoDraw); // 2.00 @@ -4886,777 +4322,512 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { lua_register(_global.luaContext, "videoSetVolume", apiVideoSetVolume); // 2.00 lua_register(_global.luaContext, "videoUnload", apiVideoUnload); // 2.00 - lua_register(_global.luaContext, "vldpGetHeight", apiVldpGetHeight); // 1.xx + lua_register(_global.luaContext, "vldpGetHeight", apiDiscGetHeight); // 1.xx Same as discGetHeight. lua_register(_global.luaContext, "vldpGetPixel", apiVldpGetPixel); // 1.xx - lua_register(_global.luaContext, "vldpGetWidth", apiVldpGetWidth); // 1.xx - lua_register(_global.luaContext, "vldpSetVerbose", apiVldpVerbose); // 1.xx + lua_register(_global.luaContext, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth. + lua_register(_global.luaContext, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx - // Open main video file - progTrace("Opening main video file"); - doIndexDisplay(INDEX_DISPLAY_START); - videoSetIndexCallback(doIndexDisplay); - if (_global.conf->isFrameFile) { - _global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, (bool)_global.conf->stretchVideo, _global.renderer, _global.conf->showCalculated); - if (_global.frameFileHandle < 0) utilDie("Unable to load framefile: %s", _global.conf->videoFile); - frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle - } else { - _global.videoHandle = videoLoad(_global.conf->videoFile, _global.conf->dataDir, (bool)_global.conf->stretchVideo, _global.renderer); - } - if (_global.videoHandle < 0) utilDie("Unable to load video file: %s", _global.conf->videoFile); - videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp); - videoSetIndexCallback(NULL); - doIndexDisplay(INDEX_DISPLAY_STOP); + // Open main video file + _progTrace("Opening main video file"); + _doIndexDisplay(INDEX_DISPLAY_START); + videoSetIndexCallback(_doIndexDisplay); + if (_global.conf->isFrameFile) { + _global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated); + frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle + } else { + _global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer); + } + videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp); + videoSetIndexCallback(NULL); + _doIndexDisplay(INDEX_DISPLAY_STOP); + videoWidth = videoGetWidth(_global.videoHandle); + videoHeight = videoGetHeight(_global.videoHandle); - // Should we resize the window? - if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) { - // Is the video wider than the display window? - if ((videoGetWidth(_global.videoHandle) / videoGetHeight(_global.videoHandle)) > (conf->xResolution / conf->yResolution)) { - // Find new window height - conf->yResolution = ((float)conf->xResolution / (float)videoGetWidth(_global.videoHandle) * (float)videoGetHeight(_global.videoHandle)); - changed = true; - } else { - // Find new window width - conf->xResolution = ((float)conf->yResolution / (float)videoGetHeight(_global.videoHandle) * (float)videoGetWidth(_global.videoHandle)); - changed = true; - } - if (changed) { - progTrace("Resizing window to %dx%d based on main video file", conf->xResolution, conf->yResolution); - SDL_SetWindowSize(_global.window, conf->xResolution, conf->yResolution); - progTrace("Destroying old renderer"); - SDL_DestroyRenderer(_global.renderer); - // Recreate an accelerated renderer. - progTrace("Creating new renderer"); - _global.renderer = SDL_CreateRenderer(_global.window, -1, SDL_RENDERER_ACCELERATED); - if (_global.renderer == NULL) utilDie("%s", SDL_GetError()); - // Clear screen with black - SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, 255); - SDL_RenderClear(_global.renderer); - changed = false; - } - } + // Should we resize the window to the video's shape? + if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) { + if (videoWidth * conf->yResolution > videoHeight * conf->xResolution) { + // Video is wider than the window: keep the width, shrink the height. + conf->yResolution = conf->xResolution * videoHeight / videoWidth; + } else { + // Video is taller: keep the height, shrink the width. + conf->xResolution = conf->yResolution * videoWidth / videoHeight; + } + _global.conf->xResolution = conf->xResolution; + _global.conf->yResolution = conf->yResolution; + _progTrace("Resizing window to %dx%d based on main video file", conf->xResolution, conf->yResolution); + SDL_SetWindowSize(_global.window, conf->xResolution, conf->yResolution); + SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(_global.renderer); + } - // Default render location is the entire window - windowTarget.x = 0; - windowTarget.y = 0; - windowTarget.w = videoGetWidth(_global.videoHandle); - windowTarget.h = videoGetHeight(_global.videoHandle); - sindenWhite.x = -1; - sindenBlack.x = -1; + // Everything renders in video coordinates unless the user wants it stretched. + if (!_global.conf->stretchVideo) { + SDL_RenderSetLogicalSize(_global.renderer, videoWidth, videoHeight); + } - // Overscan compensation - if (_global.conf->scaleFactor < 100) { - windowTarget.w = videoGetWidth(_global.videoHandle) * _global.conf->scaleFactor / 100; - windowTarget.h = videoGetHeight(_global.videoHandle) * _global.conf->scaleFactor / 100; - windowTarget.x = (videoGetWidth(_global.videoHandle) - windowTarget.w) / 2; - windowTarget.y = (videoGetHeight(_global.videoHandle) - windowTarget.h) / 2; - } + // Default render location is the entire window + windowTarget.x = 0; + windowTarget.y = 0; + windowTarget.w = videoWidth; + windowTarget.h = videoHeight; + sindenWhite.x = -1; + sindenBlack.x = -1; - // Sinden Light Gun Border Setup - if (_global.conf->sindenArgc > 0) { - //***TODO*** ADD MOUSE SCALING TO COMPENSATE FOR BORDER - sindenWhiteColor.r = 255; - sindenWhiteColor.g = 255; - sindenWhiteColor.b = 255; - sindenWhiteColor.a = 255; - sindenBlackColor.r = 0; - sindenBlackColor.g = 0; - sindenBlackColor.b = 0; - sindenBlackColor.a = 255; - // Ok, this thing can have a mess of different arguments: - switch(_global.conf->sindenArgc) { - // WW - Just the width of the white border - case SINDEN_WHITE: - sindenWhite.x = _global.conf->sindenArgv[0]; - break; - // WW WB - Width of white border and then black border - case SINDEN_WHITE_BLACK: - sindenWhite.x = _global.conf->sindenArgv[0]; - sindenBlack.x = _global.conf->sindenArgv[1]; - break; - // RW GW BW WW - Custom color "white" border and width - case SINDEN_CUSTOM_WHITE: - sindenWhiteColor.r = _global.conf->sindenArgv[0]; - sindenWhiteColor.g = _global.conf->sindenArgv[1]; - sindenWhiteColor.b = _global.conf->sindenArgv[2]; - sindenWhite.x = _global.conf->sindenArgv[3]; - break; - // RW GW BW WW WB - Custom color "white" border and width then width of black border - case SINDEN_CUSTOM_WHITE_BLACK: - sindenWhiteColor.r = _global.conf->sindenArgv[0]; - sindenWhiteColor.g = _global.conf->sindenArgv[1]; - sindenWhiteColor.b = _global.conf->sindenArgv[2]; - sindenWhite.x = _global.conf->sindenArgv[3]; - sindenBlack.x = _global.conf->sindenArgv[4]; - break; - // RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width - case SINDEN_CUSTOM_WHITE_CUSTOM_BLACK: - sindenWhiteColor.r = _global.conf->sindenArgv[0]; - sindenWhiteColor.g = _global.conf->sindenArgv[1]; - sindenWhiteColor.b = _global.conf->sindenArgv[2]; - sindenWhite.x = _global.conf->sindenArgv[3]; - sindenBlackColor.r = _global.conf->sindenArgv[4]; - sindenBlackColor.g = _global.conf->sindenArgv[5]; - sindenBlackColor.b = _global.conf->sindenArgv[6]; - sindenBlack.x = _global.conf->sindenArgv[7]; - break; - } - if (sindenWhite.x >= 0) { - sindenWhite.y = sindenWhite.x; - sindenWhite.w = videoGetWidth(_global.videoHandle) - (sindenWhite.x * 2); - sindenWhite.h = videoGetHeight(_global.videoHandle) - (sindenWhite.y * 2); - } - if (sindenBlack.x >= 0) { - sindenBlack.y = sindenBlack.x; - sindenBlack.w = videoGetWidth(_global.videoHandle) - (sindenBlack.x * 2); - sindenBlack.h = videoGetHeight(_global.videoHandle) - (sindenBlack.y * 2); - sindenWhite.x += sindenBlack.x; - sindenWhite.y += sindenBlack.y; - sindenWhite.w -= (sindenBlack.x * 2); - sindenWhite.h -= (sindenBlack.y * 2); - } - windowTarget = sindenWhite; //***TODO*** We don't really need sindenWhite - } + // Overscan compensation + if (_global.conf->scaleFactor < SCALE_FACTOR_MAX) { + windowTarget.w = videoWidth * _global.conf->scaleFactor / SCALE_FACTOR_MAX; + windowTarget.h = videoHeight * _global.conf->scaleFactor / SCALE_FACTOR_MAX; + windowTarget.x = (videoWidth - windowTarget.w) / 2; + windowTarget.y = (videoHeight - windowTarget.h) / 2; + } - // Create overlay surface - _global.overlayScaleX = 0.5; - _global.overlayScaleY = 0.5; - x = (int32_t)(videoGetWidth(_global.videoHandle) * _global.overlayScaleX); - y = (int32_t)(videoGetHeight(_global.videoHandle) * _global.overlayScaleY); - progTrace("Creating overlay of %dx%d", x, y); - _global.overlay = SDL_CreateRGBSurfaceWithFormat(0, x, y, 32, SDL_PIXELFORMAT_BGRA32); - if (_global.overlay == NULL) utilDie("%s", SDL_GetError()); - SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND); + // Sinden Light Gun Border Setup + if (_global.conf->sindenArgc > 0) { + //***TODO*** ADD MOUSE SCALING TO COMPENSATE FOR BORDER + switch (_global.conf->sindenArgc) { + // WW - Just the width of the white border + case SINDEN_WHITE: + sindenWhite.x = _global.conf->sindenArgv[0]; + break; - // Mouse setup - _global.mouseEnabled = (bool)!_global.conf->noMouse; - progTrace("Initializing ManyMouse"); - _global.mouseCount = ManyMouse_Init(); - progTrace("Mouse Driver: %s", ManyMouse_DriverName()); - progTrace("Mice Found: %d", _global.mouseCount); - if ((_global.mouseCount < 1) && _global.mouseEnabled) utilDie("No mice detected."); - if (_global.mouseCount > MAX_MICE) { - _global.mouseCount = MAX_MICE; - } - memset(_global.mice, 0, sizeof(_global.mice)); - for (x=0; x<_global.mouseCount; x++) { - strncpy(_global.mice[x].name, ManyMouse_DeviceName((unsigned)x), sizeof(_global.mice[x].name)); - _global.mice[x].name[sizeof(_global.mice[x].name) - 1] = 0; - _global.mice[x].x = (int32_t)(videoGetWidth(_global.videoHandle) * _global.overlayScaleX); - _global.mice[x].y = (int32_t)(videoGetHeight(_global.videoHandle) * _global.overlayScaleY); - progTrace("Mouse %d: %s", x, _global.mice[x].name); - } + // WW WB - Width of white border and then black border + case SINDEN_WHITE_BLACK: + sindenWhite.x = _global.conf->sindenArgv[0]; + sindenBlack.x = _global.conf->sindenArgv[1]; + break; - // Grab mouse - progTrace("Grabbing mouse"); - _global.mouseGrabbed = true; - SDL_SetWindowGrab(_global.window, SDL_TRUE); - progTrace("Disabling mouse pointer"); - SDL_ShowCursor(SDL_DISABLE); + // RW GW BW WW - Custom color "white" border and width + case SINDEN_CUSTOM_WHITE: + sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0]; + sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1]; + sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2]; + sindenWhite.x = _global.conf->sindenArgv[3]; + break; - // Clear axis caches - for (x=0; xsindenArgv[0]; + sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1]; + sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2]; + sindenWhite.x = _global.conf->sindenArgv[3]; + sindenBlack.x = _global.conf->sindenArgv[4]; + break; + + // RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width + case SINDEN_CUSTOM_WHITE_CUSTOM_BLACK: + sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0]; + sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1]; + sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2]; + sindenWhite.x = _global.conf->sindenArgv[3]; + sindenBlackColor.r = (uint8_t)_global.conf->sindenArgv[4]; + sindenBlackColor.g = (uint8_t)_global.conf->sindenArgv[5]; + sindenBlackColor.b = (uint8_t)_global.conf->sindenArgv[6]; + sindenBlack.x = _global.conf->sindenArgv[7]; + break; + + default: + utilDie("Bad Sinden argument count: %d", _global.conf->sindenArgc); + } + // The white border is the inner one; the black border (if any) surrounds it. + sindenWhite.y = sindenWhite.x; + sindenWhite.w = videoWidth - (sindenWhite.x * 2); + sindenWhite.h = videoHeight - (sindenWhite.y * 2); + if (sindenBlack.x >= 0) { + sindenBlack.y = sindenBlack.x; + sindenBlack.w = videoWidth - (sindenBlack.x * 2); + sindenBlack.h = videoHeight - (sindenBlack.y * 2); + sindenWhite.x += sindenBlack.x; + sindenWhite.y += sindenBlack.y; + sindenWhite.w -= (sindenBlack.x * 2); + sindenWhite.h -= (sindenBlack.y * 2); + } + windowTarget = sindenWhite; + } + + // Create overlay surface and its texture + x = (int32_t)(videoWidth * _global.overlayScaleX); + y = (int32_t)(videoHeight * _global.overlayScaleY); + _progTrace("Creating overlay of %dx%d", x, y); + _global.overlay = SDL_CreateRGBSurfaceWithFormat(0, x, y, 32, SDL_PIXELFORMAT_BGRA32); + if (_global.overlay == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND); + _global.overlayTexture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, x, y); + if (_global.overlayTexture == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); + _global.overlayDirty = true; + + // Mouse setup + _global.mouseEnabled = !_global.conf->noMouse; + _progTrace("Initializing ManyMouse"); + _global.mouseCount = ManyMouse_Init(); + _progTrace("Mouse Driver: %s", ManyMouse_DriverName()); + _progTrace("Mice Found: %d", _global.mouseCount); + if (_global.mouseCount < 0) { + _global.mouseCount = 0; + } + if (_global.mouseCount > MAX_MICE) { + _global.mouseCount = MAX_MICE; + } + if ((_global.mouseCount < 1) && _global.mouseEnabled) { + utilSay("Warning: No mice detected. Mouse input disabled."); + _global.mouseEnabled = false; + } + for (x = 0; x < _global.mouseCount; x++) { + strncpy(_global.mice[x].name, ManyMouse_DeviceName((unsigned)x), sizeof(_global.mice[x].name) - 1); + _global.mice[x].x = videoWidth / 2; + _global.mice[x].y = videoHeight / 2; + _progTrace("Mouse %d: %s", x, _global.mice[x].name); + } + + // Grab mouse + _progTrace("Grabbing mouse"); + _setMouseCaptured(true); // Controllers are started by the event loop only for the first script in // the queue - so kick 'em here to be sure they're going. - startControllers(); + _startControllers(); - // Set volume - _global.effectsVolume = (int32_t)((float)AUDIO_MAX_VOLUME * (float)_global.conf->volumeNonVldp * (float)0.01); - progTrace("Setting up sound effects mixer"); - Mix_Volume(-1, _global.effectsVolume * 2); + // Set volume + _global.effectsVolume = AUDIO_MAX_VOLUME * _global.conf->volumeNonVldp / VOLUME_MAX; + _progTrace("Setting up sound effects mixer"); + Mix_Volume(-1, _mixerVolume(_global.effectsVolume)); - // Let us know when sounds end - Mix_ChannelFinished(channelFinished); + // Let us know when sounds end + Mix_ChannelFinished(_channelFinished); - // Load overlay font - progTrace("Loading console font"); - _global.consoleFontSurface = IMG_LoadPNG_RW(SDL_RWFromMem(font_png, font_png_len)); - if (_global.consoleFontSurface == NULL) utilDie("%s", SDL_GetError()); - _global.consoleFontWidth = _global.consoleFontSurface->w / 256; - _global.consoleFontHeight = _global.consoleFontSurface->h; - SDL_SetColorKey(_global.consoleFontSurface, true, _global.consoleFontSurface->format->Rmask | _global.consoleFontSurface->format->Bmask); + // Load overlay font + _progTrace("Loading console font"); + _global.consoleFontSurface = _loadEmbeddedPng(font_png, font_png_len); + _global.consoleFontWidth = _global.consoleFontSurface->w / CONSOLE_FONT_GLYPHS; + _global.consoleFontHeight = _global.consoleFontSurface->h; + SDL_SetColorKey(_global.consoleFontSurface, SDL_TRUE, _global.consoleFontSurface->format->Rmask | _global.consoleFontSurface->format->Bmask); - // Start video - progTrace("Starting laserdisc video in stopped state"); - videoPlay(_global.videoHandle); - _global.discStopped = false; - // Select desired default audio track - if (_global.conf->audioOutputTrack < videoGetAudioTracks(_global.videoHandle)) { - videoSetAudioTrack(_global.videoHandle, _global.conf->audioOutputTrack); - } + // The disc always starts parked on frame 1, paused, like discSearch(1). + _progTrace("Parking laserdisc on frame 1"); + _discSeek(1); + videoPause(_global.videoHandle); + _global.discStopped = false; + _selectDefaultAudioTrack(_global.videoHandle); - // Start script - progTrace("Compiling %s", _global.conf->scriptFile); - if (luaL_dofile(_global.luaContext, _global.conf->scriptFile) != 0) utilDie("Error compiling script: %s", lua_tostring(_global.luaContext, -1)); + // Start script + _progTrace("Running %s", _global.conf->scriptFile); + lua_pushcfunction(_global.luaContext, _luaTraceback); + if (luaL_loadfile(_global.luaContext, _global.conf->scriptFile) || lua_pcall(_global.luaContext, 0, 0, -2)) { + utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1)); + } + lua_settop(_global.luaContext, 0); - // Game Loop - progTrace("Script is running"); - while (_global.running) { + // Game Loop + _progTrace("Script is running"); + while (_global.running) { - // SDL Event Loop - while (SDL_PollEvent(&event)) { - switch (event.type) { - case SDL_CONTROLLERAXISMOTION: - axisIndex = event.caxis.which * 2 + event.caxis.axis; - // Is this in a range we care about? - if (abs(event.caxis.value) > _global.controllerDeadZone) { - // Determine which "scancode" to process - see Framework.singe - // Controller codes begin at 500 and increment in 100 - x = event.caxis.which * 100 + 500; - // The axis value lines up with the enumeration used by SDL * 3 - x += event.caxis.axis * 3; - // Finally we add the particular direction we're interested in - x += (event.caxis.value < 0) ? 1 : 2; - // Fire the down/up events for the axis direction - if (lastAnalogDirection[axisIndex] != AXIS_KEY_DOWN) { - processKey(true, 0, x); - lastAnalogDirection[axisIndex] = AXIS_KEY_DOWN; - } - } else { - // Handle "up" events for controller inside dead zone - if (lastAnalogDirection[axisIndex] != AXIS_KEY_UP) { - processKey(false, 0, x); - lastAnalogDirection[axisIndex] = AXIS_KEY_UP; - } - } - // Remember this change - _global.axisCache[axisIndex] = event.caxis.value; - // Fire analog event - callLua("onControllerMoved", "iii", event.caxis.axis, event.caxis.value, event.caxis.which); - break; + // SDL Event Loop + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_CONTROLLERAXISMOTION: + slot = _controllerSlot(event.caxis.which); + if ((slot < 0) || (event.caxis.axis >= CONTROLLER_AXIS_COUNT)) { + break; + } + axisIndex = AXIS_INDEX_CONTROLLER(slot, event.caxis.axis); + // Each axis direction is a "key" so it can be mapped in controls.cfg. + code = CODE_GAMEPAD_BASE + slot * CODE_GAMEPAD_STRIDE + event.caxis.axis * CODE_AXIS_STRIDE; + code += (event.caxis.value < 0) ? CODE_AXIS_NEGATIVE : CODE_AXIS_POSITIVE; + if (abs(event.caxis.value) > _global.controllerDeadZone) { + if (_global.axisCode[axisIndex] != code) { + _releaseAxis(axisIndex); + _processKey(true, 0, code); + _global.axisCode[axisIndex] = code; + } + } else { + _releaseAxis(axisIndex); + } + _global.axisCache[axisIndex] = event.caxis.value; + if (!_global.frozen) { + _callLua("onControllerMoved", "iii", event.caxis.axis, event.caxis.value, slot); + } + break; - case SDL_CONTROLLERBUTTONDOWN: - case SDL_CONTROLLERBUTTONUP: - // Determine which "scancode" to process - see Framework.singe - // Controller codes begin at 500 and increment in 100 - x = event.cbutton.which * 100 + 500; - // The button values line up with the enumeration used by SDL + 18 - x += event.cbutton.button + 18; - // Fire down event - processKey((event.type == SDL_CONTROLLERBUTTONDOWN) ? true : false, 0, x); - break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + slot = _controllerSlot(event.cbutton.which); + if (slot < 0) { + break; + } + code = CODE_GAMEPAD_BASE + slot * CODE_GAMEPAD_STRIDE + CODE_GAMEPAD_BUTTON_OFFSET + event.cbutton.button; + _processKey(event.type == SDL_CONTROLLERBUTTONDOWN, 0, code); + break; - case SDL_CONTROLLERDEVICEADDED: - case SDL_CONTROLLERDEVICEREMOVED: - stopControllers(); - startControllers(); - break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + _startControllers(); + break; - case SDL_KEYDOWN: - case SDL_KEYUP: - processKey((event.type == SDL_KEYDOWN) ? true : false, event.key.keysym.sym, event.key.keysym.scancode); - break; + case SDL_KEYDOWN: + case SDL_KEYUP: + // Mapped switches want one press per key; full mode keeps repeats for text entry. + if (event.key.repeat && (_global.keyboardMode == KEYBOARD_NORMAL)) { + break; + } + _processKey(event.type == SDL_KEYDOWN, event.key.keysym.sym, event.key.keysym.scancode); + break; - case SDL_MOUSEMOTION: - if ((_global.mouseEnabled) && (_global.mouseMode == MOUSE_SINGLE)) { - x = (int32_t)(event.motion.x * _global.overlayScaleX); - y = (int32_t)(event.motion.y * _global.overlayScaleY); - xr = (int32_t)(event.motion.xrel * _global.overlayScaleX); - yr = (int32_t)(event.motion.yrel * _global.overlayScaleY); - // Remember this change - _global.axisCache[MAX_CONTROLLERS * 2] = x; - _global.axisCache[MAX_CONTROLLERS * 2 + 1] = y; - // Fire event - callLua("onMouseMoved", "iiiii", x, y, xr, yr, 0); - } - break; + case SDL_MOUSEMOTION: + if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE)) { + x = (int32_t)(event.motion.x * _global.overlayScaleX); + y = (int32_t)(event.motion.y * _global.overlayScaleY); + xr = (int32_t)(event.motion.xrel * _global.overlayScaleX); + yr = (int32_t)(event.motion.yrel * _global.overlayScaleY); + _fireMouseMoved(0, x, y, xr, yr); + } + break; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - if ((_global.mouseEnabled) && (_global.mouseMode == MOUSE_SINGLE)) { - // Mouse events start at "scancode" 1000 - x = 1000; - // SDL maps buttons L,M,R,X1,X2 starting at 1 - switch (event.button.button) { - case 2: - y = 2; - break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE) && (event.button.button >= SDL_BUTTON_LEFT) && (event.button.button <= SDL_BUTTON_X2)) { + _processKey(event.type == SDL_MOUSEBUTTONDOWN, 0, _mouseCode(0, _sdlMouseButtonToCode[event.button.button])); + } + break; - case 3: - y = 1; - break; + case SDL_MOUSEWHEEL: + if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE) && (event.wheel.y != 0)) { + code = _mouseCode(0, (event.wheel.y > 0) ? CODE_MOUSE_WHEEL_UP : CODE_MOUSE_WHEEL_DOWN); + _processKey(true, 0, code); + _processKey(false, 0, code); + } + break; - default: - y = event.button.button - 1; - break; - } - x += y; - // Fire event - processKey((event.type == SDL_MOUSEBUTTONDOWN) ? true : false, 0, x); - } - break; + case SDL_QUIT: + _progTrace("Quit requested"); + _global.running = false; + break; - case SDL_MOUSEWHEEL: - if ((_global.mouseEnabled) && (_global.mouseMode == MOUSE_SINGLE)) { - // Mouse events start at "scancode" 1000 - x = 1000; - // Scroll events start at 1005 and are mapped UP then DOWN - x += 5 + (event.wheel.y > 0 ? 0 : 1); - // Fire events - processKey(true, 0, x); - processKey(false, 0, x); - } - break; + default: + break; + } + } - case SDL_QUIT: - progTrace("Quit requested"); - _global.running = 0; - break; - } + // Mouse Event Loop - drained even when unused so the queue never fills. + while (ManyMouse_PollEvent(&mouseEvent)) { + if (!_global.mouseEnabled || (_global.mouseMode != MOUSE_MANY) || (mouseEvent.device >= (unsigned)_global.mouseCount)) { + continue; + } + mouse = &_global.mice[mouseEvent.device]; - // Mouse Event Loop - while (ManyMouse_PollEvent(&mouseEvent)) { + switch (mouseEvent.type) { + case MANYMOUSE_EVENT_RELMOTION: + // Integrate the motion into an absolute position clamped to the video. + xr = 0; + yr = 0; + if (mouseEvent.item == 0) { + xr = mouseEvent.value; + mouse->x += xr; + } else { + yr = mouseEvent.value; + mouse->y += yr; + } + if (mouse->x < 0) { + mouse->x = 0; + } + if (mouse->x >= videoWidth) { + mouse->x = videoWidth - 1; + } + if (mouse->y < 0) { + mouse->y = 0; + } + if (mouse->y >= videoHeight) { + mouse->y = videoHeight - 1; + } + x = (int32_t)(mouse->x * _global.overlayScaleX); + y = (int32_t)(mouse->y * _global.overlayScaleY); + xr = (int32_t)(xr * _global.overlayScaleX); + yr = (int32_t)(yr * _global.overlayScaleY); + _fireMouseMoved((int32_t)mouseEvent.device, x, y, xr, yr); + break; - // Just run out the event queue if we're not using ManyMouse - if ((!_global.mouseEnabled) || (_global.mouseMode == MOUSE_SINGLE)) continue; + case MANYMOUSE_EVENT_ABSMOTION: + // Absolute devices (tablets, some guns) report a position within a range. + if (mouseEvent.maxval > mouseEvent.minval) { + if (mouseEvent.item == 0) { + mouse->x = (int32_t)((int64_t)(mouseEvent.value - mouseEvent.minval) * videoWidth / (mouseEvent.maxval - mouseEvent.minval)); + } else { + mouse->y = (int32_t)((int64_t)(mouseEvent.value - mouseEvent.minval) * videoHeight / (mouseEvent.maxval - mouseEvent.minval)); + } + x = (int32_t)(mouse->x * _global.overlayScaleX); + y = (int32_t)(mouse->y * _global.overlayScaleY); + _fireMouseMoved((int32_t)mouseEvent.device, x, y, 0, 0); + } + break; - // Has this one been unplugged? - if (mouseEvent.device >= (unsigned)_global.mouseCount) continue; - mouse = &_global.mice[mouseEvent.device]; + case MANYMOUSE_EVENT_BUTTON: + // Limited to the same five buttons as single-mouse mode. + if (mouseEvent.item < CODE_MOUSE_BUTTON_COUNT) { + _processKey(mouseEvent.value == 1, 0, _mouseCode((int32_t)mouseEvent.device, (int32_t)mouseEvent.item)); + } + break; - switch (mouseEvent.type) { - case MANYMOUSE_EVENT_RELMOTION: - switch (mouseEvent.item) { - case 0: - mouse->relx += mouseEvent.value; - break; + case MANYMOUSE_EVENT_SCROLL: + // Vertical wheel only. + if ((mouseEvent.item == 0) && (mouseEvent.value != 0)) { + code = _mouseCode((int32_t)mouseEvent.device, (mouseEvent.value > 0) ? CODE_MOUSE_WHEEL_UP : CODE_MOUSE_WHEEL_DOWN); + _processKey(true, 0, code); + _processKey(false, 0, code); + } + break; - case 1: - mouse->rely += mouseEvent.value; - break; - } - // Clamp to video size - x = videoGetWidth(_global.videoHandle); - y = videoGetHeight(_global.videoHandle); - if (mouse->relx < 0) mouse->relx = 0; - if (mouse->relx >= x) mouse->relx = x - 1; - if (mouse->rely < 0) mouse->rely = 0; - if (mouse->rely >= y) mouse->rely = y - 1; - x = (int32_t)(mouse->x * _global.overlayScaleX); - y = (int32_t)(mouse->y * _global.overlayScaleY); - xr = (int32_t)(mouse->relx * _global.overlayScaleX); - yr = (int32_t)(mouse->rely * _global.overlayScaleY); - //utilSay("ManyMouse %d: Relative %dx%d r=%dx%d", mouseEvent.device, x, y, xr, yr); - // Remember this change - _global.axisCache[MAX_CONTROLLERS * 2 + mouseEvent.device * 2] = xr; - _global.axisCache[MAX_CONTROLLERS * 2 + mouseEvent.device * 2 + 1] = yr; - // Fire event - //callLua("onMouseMoved", "iiiii", x, y, xr, yr, mouseEvent.device); - // We return relative coords for all parameters since we have no actual X & Y - callLua("onMouseMoved", "iiiii", xr, yr, xr, yr, mouseEvent.device); - break; + default: + break; + } + } - //***TODO*** Doesn't ever seem used? - case MANYMOUSE_EVENT_ABSMOTION: - /* - val = (float)(mouseEvent.value - mouseEvent.minval); - maxval = (float)(mouseEvent.maxval - mouseEvent.minval); - switch (mouseEvent.item) { - case 0: - mouse->x = (val / maxval) * videoGetWidth(_global.videoHandle); - //mouse->x += mouseEvent.value; - break; + // Deliver sound completions on this thread. They wait out an engine pause. + if (!_global.frozen) { + SDL_LockAudio(); + finishedCount = _global.soundQueueCount; + memcpy(finished, _global.soundQueue, sizeof(int32_t) * (size_t)finishedCount); + _global.soundQueueCount = 0; + SDL_UnlockAudio(); + for (x = 0; x < finishedCount; x++) { + _callLua("onSoundCompleted", "i", finished[x]); + } + } - case 1: - mouse->y = (val / maxval) * videoGetHeight(_global.videoHandle); - //mouse->y += mouseEvent.value; - break; - } - x = (int32_t)(mouse->x * _global.overlayScaleX); - y = (int32_t)(mouse->y * _global.overlayScaleY); - xr = (int32_t)(mouse->relx * _global.overlayScaleX); - yr = (int32_t)(mouse->rely * _global.overlayScaleY); - //utilSay("ManyMouse %d: Absolute %dx%d r=%dx%d", mouseEvent.device, x, y, xr, yr); - // Remember this change - _global.axisCache[MAX_CONTROLLERS * 2 + mouseEvent.device * 2] = x; - _global.axisCache[MAX_CONTROLLERS * 2 + mouseEvent.device * 2 + 1] = y; - // Fire event - callLua("onMouseMoved", "iiiii", x, y, xr, yr, mouseEvent.device); -*/ - //progTrace("Unimplemented MANYMOUSE_EVENT_ABSMOTION called"); - break; + // Update video + thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture); + if (_global.conf->isFrameFile) { + frameFileUpdate(_global.frameFileHandle, &_global.videoHandle); + } + // Did we get a new video frame? + if (thisFrame != lastFrame) { + lastFrame = thisFrame; + frameClock = 0; + _global.refreshDisplay = true; + } - case MANYMOUSE_EVENT_BUTTON: - if (mouseEvent.item < 5 /* 32 */) { // Limited to 5 buttons so it matches single-mouse mode - //utilSay("ManyMouse %d Button: %d", mouseEvent.device, mouseEvent.item); - // Mouse events start at "scancode" 1000 with 100 spacing - x = mouseEvent.device * 100 + 1000; - // ManyMouse maps buttons L,R,M,X1,X2 starting at 0 - x += mouseEvent.item; - if (mouseEvent.value == 1) { - // Button pressed - processKey(true, 0, x); - mouse->buttons |= (1 << mouseEvent.item); - } else { - // Button released - processKey(false, 0, x); - mouse->buttons &= ~(1 << mouseEvent.item); - } - } - break; + // Call game code, unless the engine has it paused. + if (!_global.frozen && (SDL_GetTicks() > frameClock)) { + intReturn = OVERLAY_NOT_UPDATED; + _callLua("onOverlayUpdate", ">i", &intReturn); + if (intReturn == OVERLAY_UPDATED) { + _global.refreshDisplay = true; + } + frameClock = SDL_GetTicks() + FRAME_TICK_MS; // Don't eat all the CPU. + // Clear per-frame values. + _global.keyboardLastDown = SDL_SCANCODE_UNKNOWN; + _global.keyboardLastUp = SDL_SCANCODE_UNKNOWN; + } - case MANYMOUSE_EVENT_SCROLL: - if (mouseEvent.item == 0) { - // Mouse events start at "scancode" 1000 with 100 spacing - x = mouseEvent.device * 100 + 1000; - // Scroll events start at 1005 and are mapped UP then DOWN - x += 5 + (mouseEvent.value > 0 ? 0 : 1); - // Fire events - processKey(true, 0, x); - processKey(false, 0, x); - } - break; + // Update display + if (_global.refreshDisplay || _global.overlayDirty) { + // Clear entire display to black + SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(_global.renderer); + // Sinden Gun Border + if (sindenWhite.x >= 0) { + if (sindenBlack.x >= 0) { + SDL_SetRenderDrawColor(_global.renderer, sindenBlackColor.r, sindenBlackColor.g, sindenBlackColor.b, sindenBlackColor.a); + SDL_RenderFillRect(_global.renderer, &sindenBlack); + } + SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a); + SDL_RenderFillRect(_global.renderer, &sindenWhite); + } + // Laserdisc Video + if (_global.discStopped) { + // Stopped discs display blue like the good old days + SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(_global.renderer, &windowTarget); + } else { + SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget); + } + // Overlay + if (_global.overlayDirty) { + SDL_UpdateTexture(_global.overlayTexture, NULL, _global.overlay->pixels, _global.overlay->pitch); + _global.overlayDirty = false; + } + SDL_RenderCopy(_global.renderer, _global.overlayTexture, NULL, &windowTarget); + if (_global.frozen) { + _drawPauseIndicator(&windowTarget); + } + // Save it? + if (_global.requestScreenShot) { + _global.requestScreenShot = false; + _progTrace("Taking screenshot"); + _takeScreenshot(); + } + // Show it + SDL_RenderPresent(_global.renderer); + _global.refreshDisplay = false; + } - case MANYMOUSE_EVENT_DISCONNECT: - mouse->connected = false; - break; - - case MANYMOUSE_EVENT_MAX: - // We don't use this - break; - } - } - } - - // Update video - thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture); - if (_global.conf->isFrameFile) { - frameFileUpdate(_global.frameFileHandle, &_global.videoHandle); - } - // Did we get a new video frame? - if ((thisFrame != lastFrame) && (thisFrame >= 0)) { - lastFrame = thisFrame; - frameClock = 0; - _global.refreshDisplay = true; - } - - // Call game code - if (SDL_GetTicks() > frameClock) { - callLua("onOverlayUpdate", ">i", &intReturn); - if (intReturn == 1) { - _global.refreshDisplay = true; - } - frameClock = SDL_GetTicks() + 15; // Don't eat all the CPU. - // Clear per-frame values. - _global.keyboardLastDown = SDL_SCANCODE_UNKNOWN; - _global.keyboardLastUp = SDL_SCANCODE_UNKNOWN; - } - - // Update display - if (_global.refreshDisplay || _global.discStopped) { - // Clear entire display to black - SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, 255); - SDL_RenderClear(_global.renderer); - // Sinden Gun Border - if (sindenWhite.x >= 0) { - if (sindenBlack.x >= 0) { - // Black and White - SDL_SetRenderDrawColor(_global.renderer, sindenBlackColor.r, sindenBlackColor.g, sindenBlackColor.b, sindenBlackColor.a); - SDL_RenderClear(_global.renderer); - SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a); - SDL_RenderFillRect(_global.renderer, &sindenBlack); - } else { - // Only white - SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a); - SDL_RenderFillRect(_global.renderer, &sindenBlack); - //SDL_RenderClear(_global.renderer); - } - //SDL_RenderFillRect(_global.renderer, &windowTarget); - } - // Laserdisc Video - if (_global.discStopped) { - // Stopped discs display blue like the good old days - SDL_SetRenderTarget(_global.renderer, _global.videoTexture); - SDL_SetRenderDrawColor(_global.renderer, 0, 0, 255, 255); - SDL_RenderFillRect(_global.renderer, &windowTarget); - SDL_SetRenderTarget(_global.renderer, NULL); - } - // Copy current video frame into display - SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget); - // Overlay - overlayTexture = SDL_CreateTextureFromSurface(_global.renderer, _global.overlay); - if (!overlayTexture) utilDie("%s", SDL_GetError()); - if (!_global.conf->stretchVideo) { - SDL_RenderSetLogicalSize(renderer, videoGetWidth(_global.videoHandle), videoGetHeight(_global.videoHandle)); - } - SDL_RenderCopy(_global.renderer, overlayTexture, NULL, &windowTarget); - SDL_DestroyTexture(overlayTexture); - overlayTexture = NULL; - // Show it - SDL_RenderPresent(_global.renderer); - _global.refreshDisplay = false; - // Save it? - if (_global.requestScreenShot) { - _global.requestScreenShot = false; - progTrace("Taking screenshot"); - takeScreenshot(); - } - } - - SDL_Delay(1); - } - - // End game - progTrace("Script is shutting down"); - callLua("onShutdown", ""); - - // Stop all sounds - progTrace("Stopping all audio"); - Mix_HaltChannel(-1); - Mix_ChannelFinished(NULL); - - // Stop Lua - progTrace("Stopping Lua"); - lua_close(_global.luaContext); - - // Free overlay & overlay font - progTrace("Destroying overlay"); - SDL_FreeSurface(_global.overlay); - progTrace("Destroying console font"); - SDL_FreeSurface(_global.consoleFontSurface); - - // Unload fonts - HASH_ITER(hh, _global.fontList, font, fontTemp) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-align" - HASH_DEL(_global.fontList, font); -#pragma GCC diagnostic pop - progTrace("Unloading font handle %d", font->id); - TTF_CloseFont(font->font); - free(font); - } - - // Unload sounds - HASH_ITER(hh, _global.soundList, sound, soundTemp) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-align" - HASH_DEL(_global.soundList, sound); -#pragma GCC diagnostic pop - progTrace("Unloading sound handle %d", sound->id); - Mix_FreeChunk(sound->chunk); - free(sound); - } - - // Unload sprites - HASH_ITER(hh, _global.spriteList, sprite, spriteTemp) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-align" - HASH_DEL(_global.spriteList, sprite); -#pragma GCC diagnostic pop - progTrace("Unloading sprite handle %d", sprite->id); - if (sprite->surface) SDL_FreeSurface(sprite->surface); - if (sprite->originalSurface) SDL_FreeSurface(sprite->originalSurface); - if (sprite->animation) IMG_FreeAnimation(sprite->animation); - free(sprite); + SDL_Delay(IDLE_SLEEP_MS); } - // Unload videos - HASH_ITER(hh, _global.videoList, video, videoTemp) { - HASH_DEL(_global.videoList, video); - progTrace("Unloading video handle %d", video->id); - videoUnload(video->handle); - if (video->surface) SDL_FreeSurface(video->surface); - if (video->rotatedZoomedSurface) SDL_FreeSurface(video->rotatedZoomedSurface); - free(video); - } + // End game + _progTrace("Script is shutting down"); + _callLua("onShutdown", ""); - // Unload background video - progTrace("Unloading main video file"); - if (_global.conf->isFrameFile) { - frameFileUnload(_global.frameFileHandle); - } else { - videoUnload(_global.videoHandle); - } + // Stop all sounds + _progTrace("Stopping all audio"); + Mix_ChannelFinished(NULL); + Mix_HaltChannel(-1); - // Stop controllers - progTrace("Stopping controllers"); - stopControllers(); + // Stop Lua + _progTrace("Stopping Lua"); + lua_close(_global.luaContext); - // Stop mice - progTrace("Re-enabling mouse pointer"); - SDL_ShowCursor(SDL_ENABLE); - progTrace("Stopping ManyMouse"); - ManyMouse_Quit(); + // Free overlay & overlay font + _progTrace("Destroying overlay"); + SDL_DestroyTexture(_global.pauseTexture); + SDL_DestroyTexture(_global.overlayTexture); + SDL_FreeSurface(_global.overlay); + _progTrace("Destroying console font"); + SDL_FreeSurface(_global.consoleFontSurface); - // Release global conf memory - destroyConf(&_global.conf); - - // Release control mappings - for (x=0; x 4) _global.controllerCount = 4; - - _global.controllers = (SDL_GameController **)malloc(sizeof(SDL_GameController *) * (size_t)_global.controllerCount); - for (x=0; x<_global.controllerCount; x++) { - _global.controllers[x] = NULL; - if (SDL_IsGameController(x)) { - _global.controllers[x] = SDL_GameControllerOpen(x); - if (_global.controllers[x]) { - progTrace("Found %d - %s", x, SDL_GameControllerName(_global.controllers[x])); - } else { - progTrace("Controller %d not opened", x); - } - } else { - progTrace("Device %d is not a controller", x); - } - } - - SDL_GameControllerEventState(SDL_ENABLE); -} - - -void startLuaContext(lua_State *L) { - size_t length; - int i; - - // What to do when bad things happen - lua_atpanic(L, luaError); - - // Register the standard libraries - luaL_openlibs(L); - - // Share configured controller DEAD_ZONE with the script - lua_pushinteger(L, _global.controllerDeadZone); - lua_setglobal(L, "SINGE_DEAD_ZONE"); - - // Get the package global table - lua_getglobal(L, "package"); - // Get the list of searchers in the package table - lua_getfield(L, -1, "searchers"); - // Get the number of existing searchers in the table - length = lua_rawlen(L, -1); - // Shift existing elements to make room for ours - for (i=length+1; i>1; i--) { - lua_rawgeti(L, -2, i - 1); - lua_rawseti(L, -2, i); + // Unload resources the script left behind + HASH_ITER(hh, _global.fontList, font, fontTemp) { + _progTrace("Unloading font handle %d", font->id); + _fontDestroy(font); + } + HASH_ITER(hh, _global.soundList, sound, soundTemp) { + _progTrace("Unloading sound handle %d", sound->id); + _soundDestroy(sound); + } + HASH_ITER(hh, _global.spriteList, sprite, spriteTemp) { + _progTrace("Unloading sprite handle %d", sprite->id); + _spriteDestroy(sprite); + } + HASH_ITER(hh, _global.videoList, video, videoTemp) { + _progTrace("Unloading video handle %d", video->id); + _videoDestroy(video); + } + + // Unload background video + _progTrace("Unloading main video file"); + if (_global.conf->isFrameFile) { + frameFileUnload(_global.frameFileHandle); + } else { + videoUnload(_global.videoHandle); + } + + // Stop controllers + _progTrace("Stopping controllers"); + _stopControllers(); + + // Stop mice + _progTrace("Releasing mouse"); + _setMouseCaptured(false); + _progTrace("Stopping ManyMouse"); + ManyMouse_Quit(); + + // Release global conf memory + destroyConf(&_global.conf); + + // Release control mappings + for (x = 0; x < INPUT_COUNT; x++) { + free(_global.controlMappings[x].input); } - // Add our own searcher to the front of the list - lua_pushcfunction(L, luaSearcher); - //lua_rawseti(L, -2, length + 1); - lua_rawseti(L, -2, 1); - // Remove the seachers and the package tables from the stack - lua_pop(L, 2); -} - - -void stopControllers(void) { - int32_t x; - - if (_global.controllerCount > 0) { - for (x=0; x<_global.controllerCount; x++) { - if (_global.controllers[x] != NULL) { - SDL_GameControllerClose(_global.controllers[x]); - _global.controllers[x] = NULL; - } - } - free(_global.controllers); - _global.controllers = NULL; - _global.controllerCount = 0; - } - -} - - -SDL_Surface *surfaceCopy(SDL_Surface *source) { - SDL_Surface *destination = NULL; - destination = SDL_CreateRGBSurface( - source->flags, - source->w, - source->h, - source->format->BitsPerPixel, - source->format->Rmask, - source->format->Gmask, - source->format->Bmask, - source->format->Amask); - if (destination != NULL) SDL_BlitSurface(source, NULL, destination, NULL); - return destination; -} - - -void takeScreenshot(void) { - int32_t x = 0; - char filename[1024]; - void *pixels = NULL; - SDL_Surface *surface = NULL; - SDL_Surface *save = NULL; - SDL_Rect viewport; - - while (x <= 9999) { - snprintf(filename, 1024, "%ssinge%03d.png", _global.conf->dataDir, x); - if (!utilFileExists(filename)) break; - x++; - } - if (x > 9999) utilDie("Seriously? You have 10,000 screenshots in this folder? Remove some."); - - surface = SDL_GetWindowSurface(_global.window); - pixels = (uint8_t *)malloc(surface->w * surface->h * surface->format->BytesPerPixel); - SDL_RenderReadPixels(_global.renderer, &surface->clip_rect, surface->format->format, pixels, surface->w * surface->format->BytesPerPixel); - save = SDL_CreateRGBSurfaceFrom(pixels, - surface->w, - surface->h, - surface->format->BitsPerPixel, - surface->w * surface->format->BytesPerPixel, - surface->format->Rmask, - surface->format->Gmask, - surface->format->Bmask, - surface->format->Amask - ); - if (IMG_SavePNG(save, filename) < 0) utilDie("%s", IMG_GetError()); - SDL_FreeSurface(save); - SDL_FreeSurface(surface); - free(pixels); -} - - -void updatePauseState(void) { - VideoT *video = NULL; - VideoT *videoTemp = NULL; - - if (_global.pauseState) { - // Pause laserdisc - if (!_global.discStopped) { - if (videoIsPlaying(_global.videoHandle)) { - _global.wasPlayingBeforePause = true; - videoPause(_global.videoHandle); - } - } - // Pause videos - HASH_ITER(hh, _global.videoList, video, videoTemp) { - if (videoIsPlaying(video->handle)) { - video->wasPlayingBeforePause = true; - videoPause(video->handle); - } - } - // Pause sounds - Mix_Pause(-1); - } else { - // Resume laserdisc - if ((!_global.discStopped) && (_global.wasPlayingBeforePause)) { - _global.wasPlayingBeforePause = false; - videoPlay(_global.videoHandle); - } - // Resume videos - HASH_ITER(hh, _global.videoList, video, videoTemp) { - if (video->wasPlayingBeforePause) { - video->wasPlayingBeforePause = false; - videoPlay(video->handle); - } - } - // Resume laserdisc - Mix_Resume(-1); - } } diff --git a/src/singe.h b/src/singe.h index f57ff873b..41e12aac8 100644 --- a/src/singe.h +++ b/src/singe.h @@ -28,22 +28,20 @@ #include #include "common.h" +#include "generated/version.h" -// Don't forget to update singe.rc! -#define SINGE_VERSION 2.10 -#define VERSION_STRING "v2.10" -#define COPYRIGHT_END_YEAR "2024" +#define SINDEN_ARG_MAX 8 -enum { +// Number of --sindengun arguments selects the border style. +typedef enum SindenModeE { SINDEN_WHITE = 1, SINDEN_WHITE_BLACK = 2, SINDEN_CUSTOM_WHITE = 4, SINDEN_CUSTOM_WHITE_BLACK = 5, - SINDEN_CUSTOM_WHITE_CUSTOM_BLACK = 8, - SINDEN_OPTION_COUNT = 8 -}; + SINDEN_CUSTOM_WHITE_CUSTOM_BLACK = 8 +} SindenModeE; typedef struct ConfigS { @@ -64,6 +62,7 @@ typedef struct ConfigS { bool noLogos; bool programTracing; bool scriptTracing; + bool legacySpriteArgs; int32_t bestRatioIndex; int32_t volumeVldp; int32_t volumeNonVldp; @@ -71,7 +70,7 @@ typedef struct ConfigS { int32_t xResolution; int32_t yResolution; int32_t sindenArgc; - int32_t sindenArgv[SINDEN_OPTION_COUNT]; + int32_t sindenArgv[SINDEN_ARG_MAX]; int32_t audioOutputTrack; } ConfigT; diff --git a/src/singe.rc b/src/singe.rc deleted file mode 100644 index 36858e7f4..000000000 --- a/src/singe.rc +++ /dev/null @@ -1,24 +0,0 @@ -101 ICON "/tmp/icon.ico" -1 VERSIONINFO -FILEVERSION 2,1,0,0 -PRODUCTVERSION 2,1,0,0 -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904E4" - BEGIN - VALUE "CompanyName", "Kangaroo Punch Studios" - VALUE "FileDescription", "Somewhat Interactive Nostalgic Game Engine" - VALUE "FileVersion", "2.10" - VALUE "InternalName", "Singe" - VALUE "LegalCopyright", "Copyright 2006-2024 Scott C. Duensing" - VALUE "OriginalFilename", "singe.exe" - VALUE "ProductName", "Singe" - VALUE "ProductVersion", "2.10" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END diff --git a/src/singe.rc.in b/src/singe.rc.in new file mode 100644 index 000000000..0c78876f0 --- /dev/null +++ b/src/singe.rc.in @@ -0,0 +1,24 @@ +101 ICON "@GENERATED_DIR@/icon.ico" +1 VERSIONINFO +FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,0,0 +PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,0,0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "Kangaroo Punch Studios" + VALUE "FileDescription", "Somewhat Interactive Nostalgic Game Engine" + VALUE "FileVersion", "@PROJECT_VERSION@" + VALUE "InternalName", "Singe" + VALUE "LegalCopyright", "Copyright 2006-@SINGE_COPYRIGHT_END_YEAR@ Scott C. Duensing" + VALUE "OriginalFilename", "Singe.exe" + VALUE "ProductName", "Singe" + VALUE "ProductVersion", "@PROJECT_VERSION@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END diff --git a/src/util.c b/src/util.c index b35bc78de..daac10ab1 100644 --- a/src/util.c +++ b/src/util.c @@ -29,14 +29,13 @@ #define ourMkdir(p,m) mkdir(p) -static const int CONSOLE_LINES = 1000; +#define CONSOLE_LINES 1000 #else #define ourMkdir mkdir #endif #include -#include #include #include @@ -48,6 +47,29 @@ static bool _outputHappened = false; static FILE *_utilTraceFile = NULL; +static bool _ensureDirectory(const char *path, const mode_t mode); +static void _printLine(FILE *stream, const char *fmt, va_list args) __attribute__((format(printf, 2, 0))); + + +static bool _ensureDirectory(const char *path, const mode_t mode) { + struct stat sb; + + if (stat(path, &sb) != 0) { + // Does not exist - create it. + return (ourMkdir(path, mode) >= 0); + } + + return S_ISDIR(sb.st_mode); +} + + +static void _printLine(FILE *stream, const char *fmt, va_list args) { + vfprintf(stream, fmt, args); + fputc('\n', stream); + fflush(stream); +} + + bool utilChMod(const char *path, const mode_t mode) { bool result = true; @@ -62,7 +84,7 @@ bool utilChMod(const char *path, const mode_t mode) { } -char *utilCreateString(char *format, ...) { +char *utilCreateString(const char *format, ...) { va_list args; char *string; @@ -74,15 +96,18 @@ char *utilCreateString(char *format, ...) { } -__attribute__((__format__(__printf__, 1, 0))) -char *utilCreateStringVArgs(char *format, va_list args) { +char *utilCreateStringVArgs(const char *format, va_list args) { va_list argsCopy; - int32_t size = 0; - char *buffer = NULL; + int32_t size = 0; + char *buffer = NULL; va_copy(argsCopy, args); - size = vsnprintf(NULL, 0, format, argsCopy) + 1; + size = vsnprintf(NULL, 0, format, argsCopy); va_end(argsCopy); + if (size < 0) { + return NULL; + } + size++; buffer = calloc(1, (size_t)size); if (buffer) { vsnprintf(buffer, (size_t)size, format, args); @@ -92,58 +117,48 @@ char *utilCreateStringVArgs(char *format, va_list args) { } -__attribute__((__format__(__printf__, 1, 0))) -__attribute__((noreturn)) -void utilDie(char *fmt, ...) { +void utilDie(const char *fmt, ...) { va_list args; + if (_consoleEnabled) { va_start(args, fmt); - vfprintf(stderr, fmt, args); + _printLine(stderr, fmt, args); va_end(args); - printf("\n"); - fflush(stderr); -#ifdef _WIN32 - getchar(); -#endif + utilWaitForKeyOnWindows(); } - exit(1); + exit(EXIT_FAILURE); } -void utilEnableConsole(bool enable) { +void utilEnableConsole(bool enable) { _consoleEnabled = enable; } -bool utilFileExists(char *filename) { - FILE *file; - if ((file = fopen(filename, "r+"))) { - fclose(file); - return true; +bool utilFileExists(const char *filename) { + struct stat sb; + + if (stat(filename, &sb) != 0) { + return false; } - return false; + + return S_ISREG(sb.st_mode); } void utilFixPathSeparators(char **path, bool slash) { - - int32_t i = 0; - int32_t j = 0; - char *work = *path; - char *temp = NULL; + const char separator = utilGetPathSeparator(); + size_t i = 0; + size_t j = 0; + char *work = *path; + char *grown = NULL; // Flip path separators to whatever our OS wants & remove repeated separators. while (work[i] != 0) { - // Correct separator if (work[i] == '\\' || work[i] == '/') { - // Was the prior character a seprator? - if (j == 0) { - work[j++] = utilGetPathSeparator(); - } else { - if (work[j - 1] != utilGetPathSeparator()) { - // No, accept it. - work[j++] = utilGetPathSeparator(); - } + // Only accept a separator if the prior character was not one. + if ((j == 0) || (work[j - 1] != separator)) { + work[j++] = separator; } } else { work[j++] = work[i]; @@ -152,18 +167,15 @@ void utilFixPathSeparators(char **path, bool slash) { } work[j] = 0; - if (slash) { - // Does this string end with a path separator? - if (work[strlen(work) - 1] != utilGetPathSeparator()) { - // No - append one. - temp = strdup(work); - free(work); - work = malloc(sizeof(char) * (strlen(temp) + 2)); - strcpy(work, temp); - work[strlen(temp)] = utilGetPathSeparator(); - work[strlen(temp) + 1] = 0; - free(temp); + // Does this string need a trailing separator? + if (slash && ((j == 0) || (work[j - 1] != separator))) { + grown = realloc(work, j + 2); + if (!grown) { + utilDie("Unable to allocate memory for path."); } + work = grown; + work[j] = separator; + work[j + 1] = 0; } *path = work; @@ -175,39 +187,33 @@ bool utilGetConsoleEnabled(void) { } -char *utilGetFileExtension(char *filename) { - char *start = filename + strlen(filename); - int32_t x; +char *utilGetFileExtension(const char *filename) { + const char *end = filename + strlen(filename); + const char *last = utilGetLastPathComponent(filename); + const char *dot = strrchr(last, '.'); - // Scan through name and find the last '.' - for (x=0; x<(int32_t)strlen(filename); x++) { - if (filename[x] == '.') { - start = &filename[x + 1]; - } - // Reset if we find a path separator - if (filename[x] == '\\' || filename[x] == '/') { - start = filename + strlen(filename); - } + // No '.' in the last path component means no extension. + if (dot == NULL) { + return (char *)end; } - return start; + return (char *)(dot + 1); } -char *utilGetLastPathComponent(char *pathname) { - static char *start; - int32_t x; +char *utilGetLastPathComponent(const char *pathname) { + const char *start = pathname; + const char *slash = strrchr(pathname, '/'); + const char *backslash = strrchr(pathname, '\\'); - start = pathname; - - // Scan through name and find the last path separator - for (x=0; x<(int32_t)strlen(pathname); x++) { - if (pathname[x] == '\\' || pathname[x] == '/') { - start = &pathname[x + 1]; - } + if (slash != NULL) { + start = slash + 1; + } + if ((backslash != NULL) && (backslash + 1 > start)) { + start = backslash + 1; } - return start; + return (char *)start; } @@ -220,21 +226,17 @@ char utilGetPathSeparator(void) { } -char *utilGetUpToLastPathComponent(char *pathname) { - static char *copy = NULL; - bool dumb = false; // Using (copy == NULL) below didn't work after optimizations, so enter the dummy. - int32_t x; +// Returns a new string the caller must free. Always ends in a path separator. +char *utilGetUpToLastPathComponent(const char *pathname) { + size_t length = strlen(pathname) - strlen(utilGetLastPathComponent(pathname)); + char *copy = NULL; - x = (int32_t)(strlen(pathname) - strlen(utilGetLastPathComponent(pathname))) - 1; - if (x < 0) x = 0; - if (dumb) { - free(copy); - copy = NULL; + if (length == 0) { + // No directory part - use the current directory. + copy = strdup("."); } else { - dumb = true; + copy = utilStrndup(pathname, length); } - copy = strdup(pathname); - copy[x] = 0; utilFixPathSeparators(©, true); return copy; @@ -242,91 +244,74 @@ char *utilGetUpToLastPathComponent(char *pathname) { bool utilMkDirP(const char *dir, const mode_t mode) { - char tmp[UTIL_PATH_MAX]; - char *p = NULL; - struct stat sb; - size_t len; + const char separator = utilGetPathSeparator(); + char tmp[UTIL_PATH_MAX]; + char *p = NULL; + size_t len; // Make copy of dir. len = strnlen(dir, UTIL_PATH_MAX); if (len == 0 || len == UTIL_PATH_MAX) { - return -1; + return false; } memcpy(tmp, dir, len); - tmp[len] = '\0'; + tmp[len] = 0; // Remove trailing slash. - if (tmp[len - 1] == utilGetPathSeparator()) { - tmp[len - 1] = '\0'; - } - - // Does it already exist? - if (stat(tmp, &sb) == 0) { - if (S_ISDIR (sb.st_mode)) { - return true; - } + if (tmp[len - 1] == separator) { + tmp[len - 1] = 0; } // Recursive mkdir. for (p = tmp + 1; *p; p++) { - if (*p == utilGetPathSeparator()) { + if (*p == separator) { *p = 0; - if (stat(tmp, &sb) != 0) { - // Does not exist - create it. - if (ourMkdir(tmp, mode) < 0) { - return false; - } - } else { - if (!S_ISDIR(sb.st_mode)) { - // Not a directory - return false; - } + if (!_ensureDirectory(tmp, mode)) { + return false; } - *p = utilGetPathSeparator(); - } - } - // Check path - if (stat(tmp, &sb) != 0) { - // Does not exist - create it. - if (ourMkdir(tmp, mode) < 0) { - return false; - } - } else { - if (!S_ISDIR(sb.st_mode)) { - // Not a directory - return false; + *p = separator; } } - return true; + return _ensureDirectory(tmp, mode); } -bool utilPathExists(char *pathname) { - DIR *dir = opendir(pathname); - if (dir) { - closedir(dir); - return true; - } - return false; +void utilNewline(void) { + utilSay("%s", ""); } -char *utilReadFile(char *filename, size_t *bytes) { - char *data = NULL; - FILE *in = fopen(filename, "rb"); - size_t read = 0; +bool utilPathExists(const char *pathname) { + struct stat sb; - (void)read; + if (stat(pathname, &sb) != 0) { + return false; + } + + return S_ISDIR(sb.st_mode); +} + + +// Returns a new NUL terminated buffer the caller must free, or NULL. +char *utilReadFile(const char *filename, size_t *bytes) { + char *data = NULL; + FILE *in = fopen(filename, "rb"); + long size = 0; *bytes = 0; if (in) { fseek(in, 0, SEEK_END); - *bytes = ftell(in); + size = ftell(in); fseek(in, 0, SEEK_SET); - data = malloc(sizeof(char) * (*bytes)); - read = fread(data, sizeof(char), *bytes, in); + if (size >= 0) { + data = malloc((size_t)size + 1); + if (data) { + *bytes = fread(data, 1, (size_t)size, in); + data[*bytes] = 0; + } + } fclose(in); } @@ -334,123 +319,91 @@ char *utilReadFile(char *filename, size_t *bytes) { } -char *utilReadLine(char *haystack, size_t length, char **offset) { - size_t bytes = 0; - char *temp = *offset; - char *tail = temp; - char *result = NULL; +// Returns a new string the caller must free, or NULL when no data remains. +char *utilReadLine(const char *haystack, size_t length, const char **offset) { + const char *end = haystack + length; + const char *start = *offset; + const char *tail = NULL; + char *result = NULL; - // They didn't know where to start - if (temp == NULL) { - temp = haystack; - tail = temp; + // They didn't know where to start. + if (start == NULL) { + start = haystack; } + tail = start; - // Is there still data to read? - while ((size_t)(tail - haystack) < length) { - // Is this the end of a line? - if ((tail[0] == 10) || (tail[0] == 13)) { - // Yep! - bytes = tail - temp + 1; - result = malloc(sizeof(char) * bytes); - memcpy(result, temp, bytes - 1); - result[bytes - 1] = 0; - // Read past any additional CR/LFs - while ((tail[0] == 10) || (tail[0] == 13)) { - tail++; - } - // Return where we left off - temp = tail; - *offset = temp; - return result; - } - // Next character + // Find the end of the line or the end of the data. + while ((tail < end) && (*tail != '\n') && (*tail != '\r')) { tail++; } - // Was there data at the end of the block with no CR/LF? - if (tail > temp) { - // Yep. Treat it as a line. - bytes = tail - temp + 1; - result = malloc(sizeof(char) * bytes); - memcpy(result, temp, bytes - 1); - result[bytes - 1] = 0; - temp = tail; - *offset = temp; + // Was there anything on this line? + if (tail > start) { + result = utilStrndup(start, (size_t)(tail - start)); } - // Didn't find anything + // Read past any CR/LFs. + while ((tail < end) && ((*tail == '\n') || (*tail == '\r'))) { + tail++; + } + + // Return where we left off. + *offset = tail; + return result; } void utilRedirectConsole(void) { #ifdef _WIN32 - // http://dslweb.nwnexus.com/~ast/dload/guicon.htm - int hConHandle; // Not changing this int. - intptr_t lStdHandle; CONSOLE_SCREEN_BUFFER_INFO coninfo; - FILE *fp; static bool consoleOpen = false; - if (!consoleOpen) { + if (_consoleEnabled && !consoleOpen) { consoleOpen = true; - // allocate a console for this app + // Allocate a console for this app. AllocConsole(); - // set the screen buffer to be big enough to let us scroll text + // Set the screen buffer to be big enough to let us scroll text. GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); coninfo.dwSize.Y = CONSOLE_LINES; SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); - // redirect unbuffered STDOUT to the console - lStdHandle = (intptr_t)GetStdHandle(STD_OUTPUT_HANDLE); - hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); - fp = _fdopen(hConHandle, "w"); - *stdout = *fp; + // Redirect unbuffered STDOUT, STDIN, and STDERR to the console. + freopen("CONOUT$", "w", stdout); setvbuf(stdout, NULL, _IONBF, 0); - - // redirect unbuffered STDIN to the console - lStdHandle = (intptr_t)GetStdHandle(STD_INPUT_HANDLE); - hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); - fp = _fdopen(hConHandle, "r"); - *stdin = *fp; + freopen("CONIN$", "r", stdin); setvbuf(stdin, NULL, _IONBF, 0); - - // redirect unbuffered STDERR to the console - lStdHandle = (intptr_t)GetStdHandle(STD_ERROR_HANDLE); - hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); - fp = _fdopen(hConHandle, "w"); - *stderr = *fp; + freopen("CONOUT$", "w", stderr); setvbuf(stderr, NULL, _IONBF, 0); } #endif } -__attribute__((__format__(__printf__, 1, 0))) -void utilSay(char *fmt, ...) { +void utilSay(const char *fmt, ...) { va_list args; + if (_consoleEnabled) { va_start(args, fmt); - vfprintf(stdout, fmt, args); + _printLine(stdout, fmt, args); va_end(args); - printf("\n"); - fflush(stdout); _outputHappened = true; } } -bool utilStartsWith(char *string, char *start) { +bool utilStartsWith(const char *string, const char *start) { return strncmp(start, string, strlen(start)) == 0; } -int utilStricmp(char *a, char *b) { +int32_t utilStricmp(const char *a, const char *b) { + int32_t d = 0; + for (;; a++, b++) { - int d = tolower((unsigned char)*a) - tolower((unsigned char)*b); + d = tolower((unsigned char)*a) - tolower((unsigned char)*b); if (d != 0 || !*a) { return d; } @@ -459,16 +412,23 @@ int utilStricmp(char *a, char *b) { // Windows does not have strndup() so we implement our own. -char *utilStrndup( const char *s1, size_t n) { - char *copy = (char *)malloc(n + 1); - memcpy(copy, s1, n); - copy[n] = 0; +char *utilStrndup(const char *s1, size_t n) { + size_t length = strnlen(s1, n); + char *copy = malloc(length + 1); + + if (!copy) { + utilDie("Unable to allocate memory for string."); + } + memcpy(copy, s1, length); + copy[length] = 0; + return copy; } -void utilTrace(char *fmt, ...) { +void utilTrace(const char *fmt, ...) { va_list args; + if (_utilTraceFile) { va_start(args, fmt); utilTraceVArgs(fmt, args); @@ -478,36 +438,38 @@ void utilTrace(char *fmt, ...) { void utilTraceEnd(void) { - if (_utilTraceFile) fclose(_utilTraceFile); -} - - -FILE *utilTraceGetFile(void) { - return _utilTraceFile; -} - - -void utilTraceStart(char *filename) { - _utilTraceFile = fopen(filename, "wt"); - if (!_utilTraceFile) utilDie("Unable to create trace file: %s", filename); -} - - -__attribute__((__format__(__printf__, 1, 0))) -void utilTraceVArgs(char *fmt, va_list args) { -#if defined(va_copy) - va_list args2; -#endif - if (_utilTraceFile) { -#if defined(va_copy) - va_copy(args2, args); - vprintf(fmt, args2); - printf("\n"); - va_end(args2); -#endif - vfprintf(_utilTraceFile, fmt, args); - fprintf(_utilTraceFile, "\n"); - fflush(_utilTraceFile); + fclose(_utilTraceFile); + _utilTraceFile = NULL; } } + + +void utilTraceStart(const char *filename) { + utilTraceEnd(); + _utilTraceFile = fopen(filename, "w"); + if (!_utilTraceFile) { + utilDie("Unable to create trace file: %s", filename); + } +} + + +void utilTraceVArgs(const char *fmt, va_list args) { + va_list argsCopy; + + if (_utilTraceFile) { + if (_consoleEnabled) { + va_copy(argsCopy, args); + _printLine(stdout, fmt, argsCopy); + va_end(argsCopy); + } + _printLine(_utilTraceFile, fmt, args); + } +} + + +void utilWaitForKeyOnWindows(void) { +#ifdef _WIN32 + getchar(); +#endif +} diff --git a/src/util.h b/src/util.h index 7f0453659..30f91c6ae 100644 --- a/src/util.h +++ b/src/util.h @@ -36,31 +36,33 @@ #define UTIL_PATH_MAX 1024 -bool utilChMod(const char *path, const mode_t mode); -char *utilCreateString(char *format, ...); -char *utilCreateStringVArgs(char *format, va_list args); -void utilDie(char *fmt, ...); -void utilEnableConsole(bool enable); -bool utilFileExists(char *filename); -void utilFixPathSeparators(char **path, bool slash); -bool utilGetConsoleEnabled(void); -char *utilGetFileExtension(char *filename); -char *utilGetLastPathComponent(char *pathname); -char utilGetPathSeparator(void); -char *utilGetUpToLastPathComponent(char *pathname); -bool utilMkDirP(const char *dir, const mode_t mode); -bool utilPathExists(char *pathname); -char *utilReadFile(char *filename, size_t *bytes); -char *utilReadLine(char *haystack, size_t length, char **offset); -void utilRedirectConsole(void); -void utilSay(char *fmt, ...); -bool utilStartsWith(char *string, char *start); -int utilStricmp(char *a, char *b); -char *utilStrndup( const char *s1, size_t n); -void utilTrace(char *fmt, ...); -void utilTraceEnd(void); -FILE *utilTraceGetFile(void); -void utilTraceStart(char *filename); -void utilTraceVArgs(char *fmt, va_list args); +bool utilChMod(const char *path, const mode_t mode); +char *utilCreateString(const char *format, ...) __attribute__((format(printf, 1, 2))); +char *utilCreateStringVArgs(const char *format, va_list args) __attribute__((format(printf, 1, 0))); +void utilDie(const char *fmt, ...) __attribute__((format(printf, 1, 2))) __attribute__((noreturn)); +void utilEnableConsole(bool enable); +bool utilFileExists(const char *filename); +void utilFixPathSeparators(char **path, bool slash); +bool utilGetConsoleEnabled(void); +char *utilGetFileExtension(const char *filename); +char *utilGetLastPathComponent(const char *pathname); +char utilGetPathSeparator(void); +char *utilGetUpToLastPathComponent(const char *pathname); +bool utilMkDirP(const char *dir, const mode_t mode); +void utilNewline(void); +bool utilPathExists(const char *pathname); +char *utilReadFile(const char *filename, size_t *bytes); +char *utilReadLine(const char *haystack, size_t length, const char **offset); +void utilRedirectConsole(void); +void utilSay(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +bool utilStartsWith(const char *string, const char *start); +int32_t utilStricmp(const char *a, const char *b); +char *utilStrndup(const char *s1, size_t n); +void utilTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +void utilTraceEnd(void); +void utilTraceStart(const char *filename); +void utilTraceVArgs(const char *fmt, va_list args) __attribute__((format(printf, 1, 0))); +void utilWaitForKeyOnWindows(void); + #endif // UTIL_H diff --git a/src/version.h.in b/src/version.h.in new file mode 100644 index 000000000..972964da8 --- /dev/null +++ b/src/version.h.in @@ -0,0 +1,38 @@ +/* + * + * Singe 2 + * Copyright (C) 2006-@SINGE_COPYRIGHT_END_YEAR@ Scott Duensing + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + */ + + +#ifndef VERSION_H +#define VERSION_H + + +// ===== THIS FILE IS AUTOMATICALLY GENERATED FROM version.h.in - DO NOT EDIT ===== +// The version number lives in the project() line of CMakeLists.txt. + +#define SINGE_VERSION @PROJECT_VERSION@ +#define SINGE_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ +#define SINGE_VERSION_MINOR @PROJECT_VERSION_MINOR@ +#define VERSION_STRING "v@PROJECT_VERSION@" +#define COPYRIGHT_END_YEAR "@SINGE_COPYRIGHT_END_YEAR@" + + +#endif // VERSION_H diff --git a/src/videoPlayer.c b/src/videoPlayer.c index 9954ef806..3829bd271 100644 --- a/src/videoPlayer.c +++ b/src/videoPlayer.c @@ -21,6 +21,8 @@ */ +#include + #include "include/SDL2/SDL_mixer.h" #include "../thirdparty/ffms2/include/ffms.h" #include "../thirdparty/uthash/src/uthash.h" @@ -45,6 +47,14 @@ typedef struct iso639_lang_t iso639_lang_t; #define AUDIO_STREAM_LOW_WATERMARK (24 * 1024) #define AUDIO_SAMPLE_PREREAD 1024 #define AUDIO_SILENCE_SECONDS 2 +#define AUDIO_CHANNELS_MAX 2 +#define BITS_PER_BYTE 8 +#define BYTES_PER_PIXEL 4 +#define ERROR_BUFFER_SIZE 1024 +#define LANGUAGE_CODE_LENGTH 3 +#define MS_PER_SECOND 1000.0 +#define PERCENT_TO_SCALE 0.01f +#define PERCENT_MAX 100 typedef struct AudioStreamS { @@ -56,153 +66,133 @@ typedef struct AudioStreamS { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpadded" typedef struct VideoPlayerS { - int32_t id; - bool playing; - bool resetTime; - byte *audioBuffer; - byte audioSampleBytes; - byte *audioSilenceRaw; - char errMsg[1024]; - int32_t volumeLeft; - int32_t volumeRight; - int32_t videoTrack; - int32_t audioSampleSize; - int32_t mixSampleSize; - int32_t audioSilenceChannel; - int64_t frame; - int64_t audioBufferSize; - int64_t frameDeltaTime; - int64_t lastFrameTime; - int64_t timestamp; - int64_t audioDelta; - int64_t audioPosition; - int64_t framesPlayed; - Uint16 audioFormat; - Uint32 lastTickTime; - Uint32 audioSilenceSize; - //double audioAdjustment; - Mix_Chunk *silenceChunk; - SDL_AudioStream *audioStream; - SDL_Texture *videoTexture; - FFMS_ErrorInfo errInfo; - int32_t currentAudioTrack; - int32_t audioSourceCount; - AudioStreamT *audio; - FFMS_VideoSource *videoSource; - const FFMS_VideoProperties *videoProps; - const FFMS_Frame *propFrame; - const FFMS_TrackTimeBase *videoTimeBase; - const FFMS_Frame *frameData; - const FFMS_FrameInfo *frameInfo; - UT_hash_handle hh; + int32_t id; + bool playing; + bool resetTime; + bool frameDirty; + uint8_t *audioBuffer; + uint8_t *audioSilenceRaw; + uint8_t audioSampleBytes; + char errMsg[ERROR_BUFFER_SIZE]; + int32_t volumeLeft; + int32_t volumeRight; + int32_t videoTrack; + int32_t width; + int32_t height; + int32_t audioSampleSize; + int32_t audioSilenceChannel; + int32_t currentAudioTrack; + int32_t audioSourceCount; + int64_t frame; + int64_t audioBufferSize; + int64_t startTime; // Video time (ms) at the last play/seek + int64_t samplesPlayed; // Mixer frames handed to the device since the last reset + uint32_t lastCallbackTicks; + bool audioClockValid; // A callback has run since the last reset + int64_t audioPosition; + uint32_t startTicks; // Wall clock (ms) at the last play/seek + uint32_t audioSilenceSize; + SDL_AudioFormat audioFormat; + Mix_Chunk *silenceChunk; + SDL_AudioStream *audioStream; + SDL_Texture *videoTexture; + FFMS_ErrorInfo errInfo; + AudioStreamT *audio; + FFMS_VideoSource *videoSource; + FFMS_Track *videoTrackHandle; + const FFMS_VideoProperties *videoProps; + const FFMS_TrackTimeBase *videoTimeBase; + const FFMS_Frame *frameData; + UT_hash_handle hh; } VideoPlayerT; #pragma GCC diagnostic pop -FFMS_Index *_createIndex(char *filename, char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v); -void _dequeueVideoAudio(int channel, void *stream, int len, void *udata); // Callback. Not changing ints. -int FFMS_CC _indexCallBack(int64_t Current, int64_t Total, void *ICPrivate); // Callback. Not changing int. -int32_t _loadVideoAndAudio(char *vFilename, char *aFilename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer); +static int64_t _audioClock(VideoPlayerT *v, uint32_t now); +static FFMS_Index *_createIndex(const char *filename, const char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v); +static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata); // Callback. Not changing ints. +static void _feedAudio(VideoPlayerT *v); +static int64_t _frameTime(VideoPlayerT *v, int64_t frame); +static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller); +static int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *icPrivate); // Callback. Not changing int. +static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index); +static void _loadFrame(VideoPlayerT *v); +static int64_t _msToSamples(VideoPlayerT *v, int64_t ms); +static void _resetClock(VideoPlayerT *v, uint32_t now); -static videoIndexingCallback _indexingFunction = NULL; -static VideoPlayerT *_videoPlayerHash = NULL; -static int32_t _nextId = 0; -static int32_t _mixRate = -1; -static Uint8 _mixChannels = 0; -static SDL_AudioFormat _mixFormat = 0; +static VideoIndexingCallbackT _indexingFunction = NULL; +static VideoPlayerT *_videoPlayerHash = NULL; +static int32_t _nextId = 0; +static int32_t _mixRate = -1; +static uint8_t _mixChannels = 0; +static int32_t _mixFrameBytes = 0; +static int64_t _mixLatencyMs = 0; // Time between handing audio to the device and hearing it +static SDL_AudioFormat _mixFormat = 0; -void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) { // Callback. Not changing ints. - VideoPlayerT *v = (VideoPlayerT *)udata; - int32_t bytesToCopy = bytes; - int32_t available = SDL_AudioStreamAvailable(v->audioStream); - int32_t remainder = 0; - int32_t bytesRead = 0; - int32_t i = 0; - Sint16 *data = stream; +// Presentation time (ms) the listener is hearing right now. Audio is the master clock: +// the picture is fitted to what the device has actually consumed, so the two cannot drift. +static int64_t _audioClock(VideoPlayerT *v, uint32_t now) { + int64_t played = 0; + uint32_t last = 0; + bool valid = false; + int64_t clock = 0; - (void)channel; + SDL_LockAudio(); + played = v->samplesPlayed; + last = v->lastCallbackTicks; + valid = v->audioClockValid; + SDL_UnlockAudio(); - // Don't copy more than we have room for - if (bytesToCopy > available) { - bytesToCopy = available; - } - - // Ensure we only copy complete samples (Is this needed?) - remainder = bytesToCopy % v->audioSampleSize; - bytesToCopy -= remainder; - - //utilSay("B: %d R: %d W: %ld", bytes, remainder, SDL_AudioStreamAvailable(v->audioStream)); - - // Read audio data - bytesRead = SDL_AudioStreamGet(v->audioStream, stream, bytesToCopy); - if (bytesRead < 0) utilDie("%s", SDL_GetError()); - - // We do our own volume per channel here in the mixer - if (_mixChannels < 2) { - // Mono output, average volume levels together - Mix_Volume(channel, (int32_t)((float)MIX_MAX_VOLUME * ((float)v->volumeLeft * (float)v->volumeRight / (float)2) * (float)0.01)); + if (valid) { + // Consumed samples, interpolated since the last callback, less the device buffer still queued. + clock = v->startTime + (played * (int64_t)MS_PER_SECOND / _mixRate) + (int64_t)(now - last) - _mixLatencyMs; } else { - // Stereo output. Assumes MIX_DEFAULT_FORMAT for now. - Mix_Volume(channel, MIX_MAX_VOLUME); - for (i=0; ivolumeLeft * (float)0.01); - data[i + 1] = (Sint16)((float)data[i + 1] * (float)v->volumeRight * (float)0.01); - } + // No callback yet: wall clock, offset the same way so the switch over is seamless. + clock = v->startTime + (int64_t)(now - v->startTicks) - _mixLatencyMs; } + if (clock < v->startTime) { + clock = v->startTime; + } + + return clock; } -int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *ICPrivate) { // Callback. Not changing int. - static int32_t lastPercent = 0; - int32_t thisPercent = 0; - VideoPlayerT *v = (VideoPlayerT *)ICPrivate; - - (void)v; - - if ((current == 0) && (total == 0)) { - lastPercent = 0; // Reset - } else { - thisPercent = (int32_t)((double)current / (double)total * 100.0); - if (thisPercent != lastPercent) { - lastPercent = thisPercent; - // GUI - if (_indexingFunction) { - _indexingFunction(thisPercent); - } - } - } - - return 0; -} - - -FFMS_Index *_createIndex(char *filename, char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v) { - char *indexName = NULL; - FFMS_Index *index = NULL; - FFMS_Indexer *indexer = NULL; +static FFMS_Index *_createIndex(const char *filename, const char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v) { + char *indexName = NULL; + FFMS_Index *index = NULL; + FFMS_Indexer *indexer = NULL; // Index file indexName = utilCreateString("%s%c%s.index", indexPath, utilGetPathSeparator(), utilGetLastPathComponent(filename)); utilFixPathSeparators(&indexName, false); index = FFMS_ReadIndex(indexName, &v->errInfo); - if (index) { - if (FFMS_IndexBelongsToFile(index, filename, &v->errInfo)) { - FFMS_DestroyIndex(index); - index = NULL; - } + if (index && FFMS_IndexBelongsToFile(index, filename, &v->errInfo)) { + FFMS_DestroyIndex(index); + index = NULL; } if (!index) { indexer = FFMS_CreateIndexer(filename, &v->errInfo); - if (indexer == NULL) utilDie("%s", v->errInfo.Buffer); - if (hasAudio) FFMS_TrackTypeIndexSettings(indexer, FFMS_TYPE_AUDIO, 1, 0); - if (hasVideo) FFMS_TrackTypeIndexSettings(indexer, FFMS_TYPE_VIDEO, 1, 0); + if (indexer == NULL) { + utilDie("%s", v->errInfo.Buffer); + } + if (hasAudio) { + FFMS_TrackTypeIndexSettings(indexer, FFMS_TYPE_AUDIO, 1, 0); + } + if (hasVideo) { + FFMS_TrackTypeIndexSettings(indexer, FFMS_TYPE_VIDEO, 1, 0); + } _indexCallBack(0, 0, v); FFMS_SetProgressCallback(indexer, _indexCallBack, v); index = FFMS_DoIndexing2(indexer, FFMS_IEH_ABORT, &v->errInfo); - if (index == NULL) utilDie("%s", v->errInfo.Buffer); - if (FFMS_WriteIndex(indexName, index, &v->errInfo)) utilDie("%s", v->errInfo.Buffer); + if (index == NULL) { + utilDie("%s", v->errInfo.Buffer); + } + if (FFMS_WriteIndex(indexName, index, &v->errInfo)) { + utilDie("%s", v->errInfo.Buffer); + } } free(indexName); @@ -210,386 +200,529 @@ FFMS_Index *_createIndex(char *filename, char *indexPath, bool hasVideo, bool ha } -int32_t _loadVideoAndAudio(char *vFilename, char *aFilename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer) { - int32_t pixelFormats[2]; - int32_t result = -1; - FFMS_Index *vIndex = NULL; - FFMS_Index *aIndex = NULL; - VideoPlayerT *v = NULL; - int32_t x = 0; - int32_t count = 0; - FFMS_Track *track = NULL; - int32_t ttype = FFMS_TYPE_UNKNOWN; - AVFormatContext *fmt_ctx = NULL; - AVDictionaryEntry *tag = NULL; +// Runs on the SDL_mixer audio thread. Everything it touches is guarded by SDL_LockAudio on the main thread. +static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) { + VideoPlayerT *v = (VideoPlayerT *)udata; + int32_t bytesToCopy = bytes; + int32_t available = SDL_AudioStreamAvailable(v->audioStream); + int32_t bytesRead = 0; + int32_t i = 0; + int16_t *data = stream; - // Create new videoPlayer - v = calloc(1, sizeof(VideoPlayerT)); - if (!v) utilDie("Unable to allocate new video player."); + // Don't copy more than we have, and only whole mixer samples. + if (bytesToCopy > available) { + bytesToCopy = available; + } + bytesToCopy -= bytesToCopy % v->audioSampleSize; - // Set some starting values - v->audioSourceCount = 0; - v->currentAudioTrack = -1; - v->videoTrack = -1; - v->audioSilenceChannel = -1; - v->playing = false; // Start paused - v->errInfo.Buffer = v->errMsg; - v->errInfo.BufferSize = sizeof(v->errMsg); - v->errInfo.ErrorType = FFMS_ERROR_SUCCESS; - v->errInfo.SubType = FFMS_ERROR_SUCCESS; + // Read audio data + bytesRead = SDL_AudioStreamGet(v->audioStream, stream, bytesToCopy); + if (bytesRead < 0) { + utilDie("%s", SDL_GetError()); + } - if (aFilename) { - vIndex = _createIndex(vFilename, indexPath, true, false, v); - aIndex = _createIndex(aFilename, indexPath, false, true, v); + // Feed the audio clock. The main thread reads these under the same audio lock. + v->samplesPlayed += bytesRead / _mixFrameBytes; + v->lastCallbackTicks = SDL_GetTicks(); + v->audioClockValid = true; + + // We do our own volume per channel here in the mixer + if (_mixChannels < AUDIO_CHANNELS_MAX) { + // Mono output, average volume levels together + Mix_Volume(channel, (int32_t)((float)MIX_MAX_VOLUME * ((float)(v->volumeLeft + v->volumeRight) / (float)AUDIO_CHANNELS_MAX) * PERCENT_TO_SCALE)); } else { - vIndex = _createIndex(vFilename, indexPath, true, true, v); - aIndex = vIndex; - aFilename = vFilename; - } - - // Find video track - v->videoTrack = FFMS_GetFirstTrackOfType(vIndex, FFMS_TYPE_VIDEO, &v->errInfo); - if (v->videoTrack < 0) utilDie("%s", v->errInfo.Buffer); - v->videoSource = FFMS_CreateVideoSource(vFilename, v->videoTrack, vIndex, -1, FFMS_SEEK_NORMAL, &v->errInfo); - if (v->videoSource == NULL) utilDie("%s", v->errInfo.Buffer); - - // Get video properties - v->videoProps = FFMS_GetVideoProperties(v->videoSource); - v->propFrame = FFMS_GetFrame(v->videoSource, 0, &v->errInfo); - if (v->propFrame == NULL) utilDie("%s", v->errInfo.Buffer); - v->videoTimeBase = FFMS_GetTimeBase(FFMS_GetTrackFromVideo(v->videoSource)); - - // Set up output video format - pixelFormats[0] = FFMS_GetPixFmt("bgra"); - pixelFormats[1] = -1; - if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->propFrame->EncodedWidth, v->propFrame->EncodedHeight, FFMS_RESIZER_BICUBIC, &v->errInfo)) utilDie("%s", v->errInfo.Buffer); - - // Find audio track(s) - for (x=0; x 0) { - // Allocate space for the tracks. - v->audio = (AudioStreamT *)calloc(count, sizeof(AudioStreamT)); - // Now create them. - for (x=0; xaudio[v->audioSourceCount].audioSource = FFMS_CreateAudioSource(aFilename, x, aIndex, FFMS_DELAY_FIRST_VIDEO_TRACK, &v->errInfo); - if (v->audio[v->audioSourceCount].audioSource == NULL) utilDie("%s", v->errInfo.Buffer); - v->audio[v->audioSourceCount].audioProps = FFMS_GetAudioProperties(v->audio[v->audioSourceCount].audioSource); - v->audioSourceCount++; - } + // Stereo output. videoInit guarantees MIX_DEFAULT_FORMAT (16 bit samples). + Mix_Volume(channel, MIX_MAX_VOLUME); + for (i = 0; i < bytesRead / (int32_t)sizeof(int16_t); i += AUDIO_CHANNELS_MAX) { + data[i] = (int16_t)((float)data[i] * (float)v->volumeLeft * PERCENT_TO_SCALE); + data[i + 1] = (int16_t)((float)data[i + 1] * (float)v->volumeRight * PERCENT_TO_SCALE); } - // Use ffmpeg directly to figure out language IDs for audio tracks - if (avformat_open_input(&fmt_ctx, aFilename, NULL, NULL) >= 0) { - count = 0; - for (x=0; xnb_streams; x++) { - if (fmt_ctx->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { - tag = NULL; - while ((tag = (AVDictionaryEntry *)av_dict_iterate(fmt_ctx->streams[x]->metadata, tag))) { - if (utilStricmp("language", tag->key) == 0) { - v->audio[count++].language = strdup(tag->value); - break; - } - } - } - } - avformat_close_input(&fmt_ctx); - } - // Current audio track. - v->currentAudioTrack = 0; } - - // Indicies are now part of audioSource & videoSource, so release these - FFMS_DestroyIndex(vIndex); - vIndex = NULL; - if ((aFilename != vFilename) && (aIndex)) { - FFMS_DestroyIndex(aIndex); - } - aIndex = NULL; - - // Create video texture - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); - v->videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_TARGET, v->propFrame->EncodedWidth, v->propFrame->EncodedHeight); - if (v->videoTexture == NULL) utilDie("%s", SDL_GetError()); - if (!stretchVideo) { - //***TODO*** Is this the best place for this? What if we have videos of multiple sizes? - SDL_RenderSetLogicalSize(renderer, v->propFrame->EncodedWidth, v->propFrame->EncodedHeight); - } - - // Do we have audio? All audio streams must have the same format! - if (v->audioSourceCount > 0) { - // Determine audio format - switch (v->audio[0].audioProps->SampleFormat) { - case FFMS_FMT_U8: - v->audioFormat = AUDIO_U8; - v->audioSampleBytes = 1; - break; - case FFMS_FMT_S16: - v->audioFormat = AUDIO_S16SYS; - v->audioSampleBytes = 2; - break; - case FFMS_FMT_S32: - v->audioFormat = AUDIO_S32SYS; - v->audioSampleBytes = 4; - break; - case FFMS_FMT_FLT: - v->audioFormat = AUDIO_F32SYS; - v->audioSampleBytes = 4; - break; - default: - utilDie("Unknown audio sample format."); - break; - } - if (v->audio[0].audioProps->Channels > 2) utilDie("Only mono and stereo audio are supported."); - - // Create audio stream to convert audio to our desired format - v->audioStream = SDL_NewAudioStream(v->audioFormat, (Uint8)v->audio[0].audioProps->Channels, v->audio[0].audioProps->SampleRate, _mixFormat, _mixChannels, _mixRate); - if (!v->audioStream) utilDie("%s", SDL_GetError()); - - // Create a buffer to read audio into before conversion - v->mixSampleSize = SDL_AUDIO_BITSIZE(_mixFormat) / 8 * _mixChannels; - v->audioSampleSize = v->audioSampleBytes * v->audio[0].audioProps->Channels; - v->audioBufferSize = v->audioSampleSize * AUDIO_SAMPLE_PREREAD; - v->audioBuffer = (byte *)malloc((size_t)v->audioBufferSize * sizeof(byte)); - if (!v->audioBuffer) utilDie("Unable to allocate %ld byte audio buffer.", (size_t)v->audioBufferSize * sizeof(byte)); - - // Create a block of silent audio to overlay with video stream audio - v->audioSilenceSize = (Uint32)(_mixRate * SDL_AUDIO_BITSIZE(_mixFormat) / 8 * AUDIO_SILENCE_SECONDS); - v->audioSilenceRaw = (byte *)calloc(1, (size_t)v->audioSilenceSize * sizeof(byte)); - if (!v->audioSilenceRaw) utilDie("Unable to allocate %ld silence buffer.", v->audioSilenceSize); - - // Load silent audio - v->silenceChunk = Mix_QuickLoad_RAW(v->audioSilenceRaw, v->audioSilenceSize); - if (!v->silenceChunk) utilDie("%s", Mix_GetError()); - - // Start silent audio playback & immediately pause it - v->audioSilenceChannel = Mix_PlayChannel(-1, v->silenceChunk, -1); - if (v->audioSilenceChannel < 0) utilDie("%s", Mix_GetError()); - - // Register effect to provide video stream audio on this channel - Mix_RegisterEffect(v->audioSilenceChannel, _dequeueVideoAudio, NULL, v); - } - - // Default volume, in percent - v->volumeLeft = 100; - v->volumeRight = 100; - - /* - utilSay("Frames: %d (%dx%d) Audio Samples: %ld (%d Hz) %d Channel%s", - v->videoProps->NumFrames, - v->propFrame->EncodedWidth, - v->propFrame->EncodedHeight, - v->audioProps->NumSamples, - v->audioProps->SampleRate, - v->audioProps->Channels, - v->audioProps->Channels == 1 ? "" : "s" - ); - */ - - // Add to player hash - v->id = _nextId; - HASH_ADD_INT(_videoPlayerHash, id, v); - result = _nextId++; - - return result; } -int32_t videoInit(void) { +// Keep the mixer stream topped up from the current audio track. +static void _feedAudio(VideoPlayerT *v) { + AudioStreamT *track = &v->audio[v->currentAudioTrack]; + int64_t count = 0; + int32_t available = 0; - int32_t channels = _mixChannels; + SDL_LockAudio(); + available = SDL_AudioStreamAvailable(v->audioStream); + SDL_UnlockAudio(); + + while ((available < AUDIO_STREAM_LOW_WATERMARK) && (v->audioPosition < track->audioProps->NumSamples)) { + // Don't read past end of audio data + count = track->audioProps->NumSamples - v->audioPosition; + if (count > AUDIO_SAMPLE_PREREAD) { + count = AUDIO_SAMPLE_PREREAD; + } + // Get audio from video stream + if (FFMS_GetAudio(track->audioSource, v->audioBuffer, v->audioPosition, count, &v->errInfo)) { + utilDie("%s", v->errInfo.Buffer); + } + // Feed it to the mixer stream + SDL_LockAudio(); + if (SDL_AudioStreamPut(v->audioStream, v->audioBuffer, (int32_t)(count * v->audioSampleSize)) < 0) { + utilDie("%s", SDL_GetError()); + } + available = SDL_AudioStreamAvailable(v->audioStream); + SDL_UnlockAudio(); + v->audioPosition += count; + } +} + + +// Presentation time of a frame in milliseconds. +static int64_t _frameTime(VideoPlayerT *v, int64_t frame) { + const FFMS_FrameInfo *info = FFMS_GetFrameInfo(v->videoTrackHandle, (int)frame); + + return (int64_t)((double)info->PTS * (double)v->videoTimeBase->Num / (double)v->videoTimeBase->Den); +} + + +static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller) { + VideoPlayerT *v = NULL; + + HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); + if (!v) { + utilDie("No video player at index %d in %s.", playerHandle, caller); + } + + return v; +} + + +static int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *icPrivate) { + static int32_t lastPercent = 0; + int32_t thisPercent = 0; + + (void)icPrivate; + + if ((current == 0) && (total == 0)) { + lastPercent = 0; // Reset + } else { + thisPercent = (int32_t)((double)current / (double)total * (double)PERCENT_MAX); + if ((thisPercent != lastPercent) && _indexingFunction) { + lastPercent = thisPercent; + _indexingFunction(thisPercent); + } + } + + return 0; +} + + +static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index) { + int32_t x = 0; + int32_t count = 0; + const FFMS_AudioProperties *first = NULL; + const FFMS_AudioProperties *props = NULL; + AVFormatContext *fmtCtx = NULL; + AVDictionaryEntry *tag = NULL; + + // Count the audio tracks. + for (x = 0; x < FFMS_GetNumTracks(index); x++) { + if (FFMS_GetTrackType(FFMS_GetTrackFromIndex(index, x)) == FFMS_TYPE_AUDIO) { + count++; + } + } + if (count == 0) { + return; + } + + // Now create them. + v->audio = (AudioStreamT *)calloc((size_t)count, sizeof(AudioStreamT)); + if (!v->audio) { + utilDie("Unable to allocate audio tracks."); + } + for (x = 0; x < FFMS_GetNumTracks(index); x++) { + if (FFMS_GetTrackType(FFMS_GetTrackFromIndex(index, x)) == FFMS_TYPE_AUDIO) { + v->audio[v->audioSourceCount].audioSource = FFMS_CreateAudioSource(filename, x, index, FFMS_DELAY_FIRST_VIDEO_TRACK, &v->errInfo); + if (v->audio[v->audioSourceCount].audioSource == NULL) { + utilDie("%s", v->errInfo.Buffer); + } + v->audio[v->audioSourceCount].audioProps = FFMS_GetAudioProperties(v->audio[v->audioSourceCount].audioSource); + v->audioSourceCount++; + } + } + + // Every track is fed through one SDL_AudioStream, so they must share a format. + first = v->audio[0].audioProps; + for (x = 1; x < v->audioSourceCount; x++) { + props = v->audio[x].audioProps; + if ((props->SampleFormat != first->SampleFormat) || (props->Channels != first->Channels) || (props->SampleRate != first->SampleRate)) { + utilDie("All audio tracks in %s must share the same sample format, channel count, and rate.", filename); + } + } + + // Use ffmpeg directly to figure out language IDs for audio tracks + if (avformat_open_input(&fmtCtx, filename, NULL, NULL) >= 0) { + count = 0; + for (x = 0; (x < (int32_t)fmtCtx->nb_streams) && (count < v->audioSourceCount); x++) { + if (fmtCtx->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + tag = av_dict_get(fmtCtx->streams[x]->metadata, "language", NULL, 0); + if (tag != NULL) { + v->audio[count].language = strdup(tag->value); + } + count++; + } + } + avformat_close_input(&fmtCtx); + } + + v->currentAudioTrack = 0; +} + + +// Decode the current frame and push it to the texture. +static void _loadFrame(VideoPlayerT *v) { + v->frameData = FFMS_GetFrame(v->videoSource, (int)v->frame, &v->errInfo); + if (v->frameData == NULL) { + utilDie("%s", v->errInfo.Buffer); + } + SDL_UpdateTexture(v->videoTexture, NULL, v->frameData->Data[0], v->frameData->Linesize[0]); + v->frameDirty = false; +} + + +static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) { + return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate); +} + + +// Restart the presentation clock at the current frame and realign audio to it. +static void _resetClock(VideoPlayerT *v, uint32_t now) { + v->startTicks = now; + v->startTime = _frameTime(v, v->frame); + v->frameDirty = true; + v->resetTime = false; + if (v->audioSourceCount > 0) { + SDL_LockAudio(); + SDL_AudioStreamClear(v->audioStream); + v->samplesPlayed = 0; + v->lastCallbackTicks = now; + v->audioClockValid = false; + SDL_UnlockAudio(); + v->audioPosition = _msToSamples(v, v->startTime); + } +} + + +int32_t videoGetAudioTrack(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoGetAudioTrack")->currentAudioTrack; +} + + +int32_t videoGetAudioTracks(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoGetAudioTracks")->audioSourceCount; +} + + +int64_t videoGetFrame(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoGetFrame")->frame; +} + + +int64_t videoGetFrameCount(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoGetFrameCount")->videoProps->NumFrames; +} + + +int32_t videoGetHeight(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoGetHeight")->height; +} + + +const char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoGetLanguage"); + const char *r = "unk"; // Unknown Language + + if ((audioTrack < 0) || (audioTrack >= v->audioSourceCount)) { + utilDie("Invalid audio track %d in videoGetLanguage.", audioTrack); + } + if ((v->audio[audioTrack].language != NULL) && (strlen(v->audio[audioTrack].language) == LANGUAGE_CODE_LENGTH)) { + r = v->audio[audioTrack].language; + } + + return r; +} + + +const char *videoGetLanguageDescription(const char *languageCode) { + int32_t i = 0; + + if (languageCode == NULL) { + return "Unknown"; + } + for (i = 0; p_languages[i].psz_eng_name != NULL; i++) { + if ((utilStricmp(languageCode, p_languages[i].psz_iso639_1) == 0) || (utilStricmp(languageCode, p_languages[i].psz_iso639_2T) == 0) || (utilStricmp(languageCode, p_languages[i].psz_iso639_2B) == 0)) { + return p_languages[i].psz_eng_name; + } + } + + return "Unknown"; +} + + +// Reads one pixel of the most recently decoded frame. Returns false if there is no frame yet. +bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel"); + const uint8_t *pixel = NULL; + + if ((v->frameData == NULL) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) { + return false; + } + // Frames are decoded as BGRA. + pixel = v->frameData->Data[0] + (y * v->frameData->Linesize[0]) + (x * BYTES_PER_PIXEL); + *b = pixel[0]; + *g = pixel[1]; + *r = pixel[2]; + + return true; +} + + +// Exposes the most recently decoded BGRA frame. Valid until the next videoUpdate of this player. +bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixels"); + + if (v->frameData == NULL) { + return false; + } + *pixels = v->frameData->Data[0]; + *pitch = v->frameData->Linesize[0]; + + return true; +} + + +void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoGetVolume"); + + if (leftPercent != NULL) { + *leftPercent = v->volumeLeft; + } + if (rightPercent != NULL) { + *rightPercent = v->volumeRight; + } +} + + +int32_t videoGetWidth(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoGetWidth")->width; +} + + +void videoInit(int32_t mixerChunkFrames) { + int32_t channels = 0; // Start FFMS FFMS_Init(0, 0); // Fetch mixer settings - if (!Mix_QuerySpec(&_mixRate, &_mixFormat, &channels)) utilDie("%s", Mix_GetError()); - _mixChannels = (Uint8)channels; + if (!Mix_QuerySpec(&_mixRate, &_mixFormat, &channels)) { + utilDie("%s", Mix_GetError()); + } + _mixChannels = (uint8_t)channels; + _mixFrameBytes = SDL_AUDIO_BITSIZE(_mixFormat) / BITS_PER_BYTE * _mixChannels; + _mixLatencyMs = (int64_t)mixerChunkFrames * (int64_t)MS_PER_SECOND / _mixRate; // Volume only works with MIX_DEFAULT_FORMAT - if (_mixFormat != MIX_DEFAULT_FORMAT) utilDie("videoInit: Only MIX_DEFAULT_FORMAT audio is supported."); + if (_mixFormat != MIX_DEFAULT_FORMAT) { + utilDie("videoInit: Only MIX_DEFAULT_FORMAT audio is supported."); + } - return 0; + // Video textures scale smoothly. + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); } -int32_t videoIsPlaying(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoIsPlaying.", playerHandle); - - return v->playing; +bool videoIsPlaying(int32_t playerHandle) { + return _getPlayer(playerHandle, "videoIsPlaying")->playing; } -int32_t videoGetAudioTrack(int32_t playerHandle) { - VideoPlayerT *v = NULL; +// audioFilename may be NULL when the audio lives in the video file. +int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer) { + int32_t pixelFormats[2]; + FFMS_Index *vIndex = NULL; + FFMS_Index *aIndex = NULL; + VideoPlayerT *v = NULL; + const FFMS_Frame *frame = NULL; + int32_t bytesPerSample = 0; - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetAudioTrack.", playerHandle); + // Create new videoPlayer + v = calloc(1, sizeof(VideoPlayerT)); + if (!v) { + utilDie("Unable to allocate new video player."); + } - return v->currentAudioTrack; -} + // Set some starting values (everything else is zero from calloc) + v->currentAudioTrack = -1; + v->videoTrack = -1; + v->audioSilenceChannel = -1; + v->volumeLeft = VIDEO_VOLUME_MAX; + v->volumeRight = VIDEO_VOLUME_MAX; + v->errInfo.Buffer = v->errMsg; + v->errInfo.BufferSize = sizeof(v->errMsg); + v->errInfo.ErrorType = FFMS_ERROR_SUCCESS; + v->errInfo.SubType = FFMS_ERROR_SUCCESS; - -int32_t videoGetAudioTracks(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetAudioTracks.", playerHandle); - - return v->audioSourceCount; -} - - -int64_t videoGetFrame(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetFrame.", playerHandle); - - return v->frame; -} - - -int64_t videoGetFrameCount(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetFrameCount.", playerHandle); - - return v->videoProps->NumFrames; - -} - - -char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack) { - VideoPlayerT *v = NULL; - static char *u = "unk"; // Unknown Language - char *r = u; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetHeight.", playerHandle); - - if ((audioTrack >= 0) && (audioTrack < v->audioSourceCount)) { - r = v->audio[audioTrack].language; - if ((r == NULL) || (strlen(r) != 3)) { - r = u; - } + if (audioFilename) { + vIndex = _createIndex(videoFilename, indexPath, true, false, v); + aIndex = _createIndex(audioFilename, indexPath, false, true, v); } else { - utilDie("Invalid audio track in videoSetAudioTrack."); + vIndex = _createIndex(videoFilename, indexPath, true, true, v); + aIndex = vIndex; + audioFilename = videoFilename; } - return r; -} + // Find video track + v->videoTrack = FFMS_GetFirstTrackOfType(vIndex, FFMS_TYPE_VIDEO, &v->errInfo); + if (v->videoTrack < 0) { + utilDie("%s", v->errInfo.Buffer); + } + v->videoSource = FFMS_CreateVideoSource(videoFilename, v->videoTrack, vIndex, -1, FFMS_SEEK_NORMAL, &v->errInfo); + if (v->videoSource == NULL) { + utilDie("%s", v->errInfo.Buffer); + } + // Get video properties. The frame returned here is only valid until the next FFMS_GetFrame. + v->videoProps = FFMS_GetVideoProperties(v->videoSource); + v->videoTrackHandle = FFMS_GetTrackFromVideo(v->videoSource); + v->videoTimeBase = FFMS_GetTimeBase(v->videoTrackHandle); + frame = FFMS_GetFrame(v->videoSource, 0, &v->errInfo); + if (frame == NULL) { + utilDie("%s", v->errInfo.Buffer); + } + v->width = frame->EncodedWidth; + v->height = frame->EncodedHeight; -char *videoGetLanguageDescription(char *languageCode) { - static char *u = "Unknown"; - char *r = u; - int32_t i = 0; + // Set up output video format + pixelFormats[0] = FFMS_GetPixFmt("bgra"); + pixelFormats[1] = -1; + if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->width, v->height, FFMS_RESIZER_BICUBIC, &v->errInfo)) { + utilDie("%s", v->errInfo.Buffer); + } - while (p_languages[i].psz_eng_name != NULL) { - if ((utilStricmp(languageCode, (char *)p_languages[i].psz_iso639_1) == 0) || - (utilStricmp(languageCode, (char *)p_languages[i].psz_iso639_2T) == 0) || - (utilStricmp(languageCode, (char *)p_languages[i].psz_iso639_2B) == 0)) { - r = (char *)p_languages[i].psz_eng_name; - break; + // Find audio track(s) + _loadAudio(v, audioFilename, aIndex); + + // Indicies are now part of audioSource & videoSource, so release these + if (aIndex != vIndex) { + FFMS_DestroyIndex(aIndex); + } + FFMS_DestroyIndex(vIndex); + + // Create video texture + v->videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, v->width, v->height); + if (v->videoTexture == NULL) { + utilDie("%s", SDL_GetError()); + } + + // Do we have audio? + if (v->audioSourceCount > 0) { + // Determine audio format + switch (v->audio[0].audioProps->SampleFormat) { + case FFMS_FMT_U8: + v->audioFormat = AUDIO_U8; + v->audioSampleBytes = 1; + break; + + case FFMS_FMT_S16: + v->audioFormat = AUDIO_S16SYS; + v->audioSampleBytes = 2; + break; + + case FFMS_FMT_S32: + v->audioFormat = AUDIO_S32SYS; + v->audioSampleBytes = 4; + break; + + case FFMS_FMT_FLT: + v->audioFormat = AUDIO_F32SYS; + v->audioSampleBytes = 4; + break; + + default: + utilDie("Unknown audio sample format."); + } + if (v->audio[0].audioProps->Channels > AUDIO_CHANNELS_MAX) { + utilDie("Only mono and stereo audio are supported."); + } + + // Create audio stream to convert audio to our desired format + v->audioStream = SDL_NewAudioStream(v->audioFormat, (uint8_t)v->audio[0].audioProps->Channels, v->audio[0].audioProps->SampleRate, _mixFormat, _mixChannels, _mixRate); + if (!v->audioStream) { + utilDie("%s", SDL_GetError()); + } + + // Create a buffer to read audio into before conversion + v->audioSampleSize = v->audioSampleBytes * v->audio[0].audioProps->Channels; + v->audioBufferSize = v->audioSampleSize * AUDIO_SAMPLE_PREREAD; + v->audioBuffer = (uint8_t *)malloc((size_t)v->audioBufferSize); + if (!v->audioBuffer) { + utilDie("Unable to allocate %" PRId64 " byte audio buffer.", v->audioBufferSize); + } + + // Create a block of silent audio to overlay with video stream audio + bytesPerSample = SDL_AUDIO_BITSIZE(_mixFormat) / BITS_PER_BYTE; + v->audioSilenceSize = (uint32_t)(_mixRate * bytesPerSample * _mixChannels * AUDIO_SILENCE_SECONDS); + v->audioSilenceRaw = (uint8_t *)calloc(1, v->audioSilenceSize); + if (!v->audioSilenceRaw) { + utilDie("Unable to allocate %" PRIu32 " byte silence buffer.", v->audioSilenceSize); + } + + // Load silent audio + v->silenceChunk = Mix_QuickLoad_RAW(v->audioSilenceRaw, v->audioSilenceSize); + if (!v->silenceChunk) { + utilDie("%s", Mix_GetError()); + } + + // Start silent audio playback, paused until the video plays + v->audioSilenceChannel = Mix_PlayChannel(-1, v->silenceChunk, -1); + if (v->audioSilenceChannel < 0) { + utilDie("%s", Mix_GetError()); + } + Mix_Pause(v->audioSilenceChannel); + + // Register effect to provide video stream audio on this channel + if (!Mix_RegisterEffect(v->audioSilenceChannel, _dequeueVideoAudio, NULL, v)) { + utilDie("%s", Mix_GetError()); } - i++; } - return r; + // Add to player hash + v->id = _nextId++; + HASH_ADD_INT(_videoPlayerHash, id, v); + + return v->id; } -int32_t videoGetHeight(int32_t playerHandle) { - VideoPlayerT *v = NULL; +void videoPause(int32_t playerHandle) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoPause"); - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetHeight.", playerHandle); - - return v->propFrame->EncodedHeight; + v->playing = false; + if (v->audioSourceCount > 0) { + Mix_Pause(v->audioSilenceChannel); + } } -int32_t videoGetWidth(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetWidth.", playerHandle); - - return v->propFrame->EncodedWidth; -} - - -int32_t videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoGetVolume.", playerHandle); - - if (leftPercent != NULL) *leftPercent = v->volumeLeft; - if (rightPercent != NULL) *rightPercent = v->volumeRight; - - return 0; -} - - -int32_t videoLoad(char *filename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer) { - return _loadVideoAndAudio(filename, NULL, indexPath, stretchVideo, renderer); -} - - -int32_t videoLoadWithAudio(char *vFilename, char *aFilename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer) { - return _loadVideoAndAudio(vFilename, aFilename, indexPath, stretchVideo, renderer); -} - - -int32_t videoPause(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoPause.", playerHandle); - - v->playing = false; - v->resetTime = true; - - return 0; -} - - -int32_t videoPlay(int32_t playerHandle) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoPlay.", playerHandle); +void videoPlay(int32_t playerHandle) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoPlay"); v->playing = true; v->resetTime = true; - - return 0; + if (v->audioSourceCount > 0) { + Mix_Resume(v->audioSilenceChannel); + } } -int32_t videoQuit(void) { - +void videoQuit(void) { VideoPlayerT *v = NULL; VideoPlayerT *t = NULL; @@ -599,86 +732,69 @@ int32_t videoQuit(void) { } FFMS_Deinit(); - - return 0; } -int32_t videoSeek(int32_t playerHandle, int64_t seekFrame) { - VideoPlayerT *v = NULL; +void videoSeek(int32_t playerHandle, int64_t seekFrame) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoSeek"); + int64_t count = v->videoProps->NumFrames; - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoSeek.", playerHandle); - - while (seekFrame >= v->videoProps->NumFrames) { - seekFrame -= v->videoProps->NumFrames; - } - while (seekFrame < 0) { - seekFrame += v->videoProps->NumFrames; - } - - v->frame = seekFrame; - v->resetTime = true; - - return 0; -} - - -int32_t videoSetAudioTrack(int32_t playerHandle, int32_t track) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoSetAudioTrack.", playerHandle); - - if ((track >= 0) && (track < v->audioSourceCount)) { - v->currentAudioTrack = track; + // Wrap into range. + if (count > 0) { + seekFrame = ((seekFrame % count) + count) % count; } else { - utilDie("Invalid audio track in videoSetAudioTrack."); + seekFrame = 0; } - return 0; + v->frame = seekFrame; + v->resetTime = true; } -int32_t videoSetIndexCallback(videoIndexingCallback callback) { +void videoSetAudioTrack(int32_t playerHandle, int32_t track) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoSetAudioTrack"); + + if ((track < 0) || (track >= v->audioSourceCount)) { + utilDie("Invalid audio track %d in videoSetAudioTrack.", track); + } + if (track != v->currentAudioTrack) { + v->currentAudioTrack = track; + // Drop the queued audio from the old track and realign. + v->resetTime = true; + } +} + + +void videoSetIndexCallback(VideoIndexingCallbackT callback) { _indexingFunction = callback; - return 0; } -int32_t videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent) { - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoSetVolume.", playerHandle); +void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoSetVolume"); + // The mixer thread reads these. + SDL_LockAudio(); v->volumeLeft = leftPercent; v->volumeRight = rightPercent; - - return 0; + SDL_UnlockAudio(); } -int32_t videoUnload(int32_t playerHandle) { - VideoPlayerT *v = NULL; +void videoUnload(int32_t playerHandle) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoUnload"); int32_t x = 0; - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoStop.", playerHandle); - if (v->audioSourceCount > 0) { - Mix_HaltChannel(v->audioSilenceChannel); + // Unregister before halting - halting removes effects itself. Mix_UnregisterEffect(v->audioSilenceChannel, _dequeueVideoAudio); + Mix_HaltChannel(v->audioSilenceChannel); Mix_FreeChunk(v->silenceChunk); free(v->audioSilenceRaw); SDL_FreeAudioStream(v->audioStream); free(v->audioBuffer); - for (x=0; xaudioSourceCount; x++) { - if (v->audio[x].language) free(v->audio[x].language); + for (x = 0; x < v->audioSourceCount; x++) { + free(v->audio[x].language); FFMS_DestroyAudioSource(v->audio[x].audioSource); } free(v->audio); @@ -692,104 +808,55 @@ int32_t videoUnload(int32_t playerHandle) { HASH_DEL(_videoPlayerHash, v); #pragma GCC diagnostic pop free(v); - - return 0; } -int32_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) { - int32_t result = -1; - int64_t count = 0; - int64_t threshold = 0; - VideoPlayerT *v = NULL; - - // Get our player structure - HASH_FIND_INT(_videoPlayerHash, &playerHandle, v); - if (!v) utilDie("No video player at index %d in videoUpdate.", playerHandle); - - // Audio drift limit - threshold = v->audio[v->currentAudioTrack].audioSource ? (v->audio[v->currentAudioTrack].audioProps->SampleRate / 2) : 99999; - - // Handle video frames (and time) - //if ((SDL_GetTicks() - v->lastTickTime >= v->frameDeltaTime) || (v->audioDelta > threshold) || v->resetTime) { - if ((SDL_GetTicks() - v->lastTickTime >= v->frameDeltaTime) || v->resetTime) { - - if (v->frameData) { - SDL_UpdateTexture(v->videoTexture, NULL, v->frameData->Data[0], v->frameData->Linesize[0]); - } - - *texture = v->videoTexture; - result = v->frame; - v->framesPlayed++; - - v->frameData = FFMS_GetFrame(v->videoSource, v->frame, &v->errInfo); - if (v->frameData == NULL) utilDie("%s", v->errInfo.Buffer); - v->frameInfo = FFMS_GetFrameInfo(FFMS_GetTrackFromVideo(v->videoSource), v->frame); - v->timestamp = (int64_t)((double)v->frameInfo->PTS * (double)v->videoTimeBase->Num / (double)v->videoTimeBase->Den); // Convert to milliseconds - v->frameDeltaTime = (v->timestamp - v->lastFrameTime); // - (v->audioAdjustment * v->framesPlayed); - v->lastFrameTime = v->timestamp; +// Advances playback to match the audio clock (or the wall clock for silent videos). Returns the frame now on the texture. +int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) { + VideoPlayerT *v = _getPlayer(playerHandle, "videoUpdate"); + uint32_t now = SDL_GetTicks(); + int64_t elapsed = 0; + int64_t count = v->videoProps->NumFrames; + int64_t next = 0; + int64_t lastDuration = 0; + if (v->resetTime) { + _resetClock(v, now); + } else { if (v->playing) { - if (++v->frame >= v->videoProps->NumFrames) { - v->frame = 0; - v->timestamp = 0; - v->resetTime = true; - } - } - - v->lastTickTime = SDL_GetTicks(); - - if (v->resetTime) { + // Where in the video should we be? Follow the audio when there is any. if (v->audioSourceCount > 0) { - SDL_AudioStreamClear(v->audioStream); - v->audioPosition = (int64_t)((double)v->timestamp * 0.001 * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate); - v->audioDelta = 0; + elapsed = _audioClock(v, now); + } else { + elapsed = (int64_t)(now - v->startTicks) + v->startTime; + } + // Advance to the last frame whose presentation time has arrived. + next = v->frame + 1; + while ((next < count) && (_frameTime(v, next) <= elapsed)) { + v->frame = next; + v->frameDirty = true; + next++; + } + // Past the end of the last frame? Loop. + if (next >= count) { + lastDuration = (int64_t)(MS_PER_SECOND * (double)v->videoProps->FPSDenominator / (double)v->videoProps->FPSNumerator); + if (elapsed >= _frameTime(v, count - 1) + lastDuration) { + v->frame = 0; + _resetClock(v, now); + } } - v->lastTickTime = 0; - v->frameDeltaTime = 0; - v->resetTime = false; - v->framesPlayed = 0; } } + if (v->frameDirty) { + _loadFrame(v); + } + *texture = v->videoTexture; + // Handle audio samples - if (v->audioSourceCount > 0) { - // Add more samples to queue? - if ((v->playing) && (SDL_AudioStreamAvailable(v->audioStream) < AUDIO_STREAM_LOW_WATERMARK) && (v->audioPosition < v->audio[v->currentAudioTrack].audioProps->NumSamples)) { - // Maximum samples we can read at a time - count = AUDIO_SAMPLE_PREREAD; - // Don't read past end of audio data - if (v->audioPosition + count >= v->audio[v->currentAudioTrack].audioProps->NumSamples) { - count = v->audio[v->currentAudioTrack].audioProps->NumSamples - v->audioPosition - 1; - } - // Are we reading anything? - if (count > 0) { - // Get audio from video stream - if (FFMS_GetAudio(v->audio[v->currentAudioTrack].audioSource, v->audioBuffer, v->audioPosition, count, &v->errInfo)) utilDie("%s", v->errInfo.Buffer); - // Feed it to the mixer stream - if (SDL_AudioStreamPut(v->audioStream, v->audioBuffer, (int32_t)(count * v->audioSampleSize)) < 0) utilDie("%s", SDL_GetError()); - v->audioPosition += count; - } - } - - // Used to determine if we should play two frames rapidly to catch up to audio - v->audioDelta = labs((long)(v->audioPosition - (int64_t)((double)v->timestamp * 0.001 * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate))); - - // Did we trip the audio sync compensation? - if (v->audioDelta > threshold) { - v->frameDeltaTime *= 0.5; - //utilSay("Adjusting delta %f", v->frameDeltaTime); - /* - // Adjust frame rate to try and match - if (v->audioDelta > 0) { - v->audioAdjustment += 0.000001; - } else { - v->audioAdjustment -= 0.000001; - } - */ - } - //utilSay("D %ld T %ld A %f F %f", v->audioDelta, threshold, v->audioAdjustment, v->frameDeltaTime); + if (v->playing && (v->audioSourceCount > 0)) { + _feedAudio(v); } - return result; + return v->frame; } diff --git a/src/videoPlayer.h b/src/videoPlayer.h index 49a7abd40..a5dc9cd63 100644 --- a/src/videoPlayer.h +++ b/src/videoPlayer.h @@ -30,31 +30,35 @@ #include "common.h" -typedef void (*videoIndexingCallback)(int32_t); +#define VIDEO_VOLUME_MAX 100 -int32_t videoInit(void); -int32_t videoIsPlaying(int32_t playerHandle); -int32_t videoGetAudioTrack(int32_t playerHandle); -int32_t videoGetAudioTracks(int32_t playerHandle); -int64_t videoGetFrame(int32_t playerHandle); -int64_t videoGetFrameCount(int32_t playerHandle); -int32_t videoGetHeight(int32_t playerHandle); -char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack); -char *videoGetLanguageDescription(char *languageCode); -int32_t videoGetWidth(int32_t playerHandle); -int32_t videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent); -int32_t videoLoad(char *filename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer); -int32_t videoLoadWithAudio(char *vFilename, char *aFilename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer); -int32_t videoPause(int32_t playerHandle); -int32_t videoPlay(int32_t playerHandle); -int32_t videoQuit(void); -int32_t videoSeek(int32_t playerHandle, int64_t seekFrame); -int32_t videoSetAudioTrack(int32_t playerHandle, int32_t track); -int32_t videoSetIndexCallback(videoIndexingCallback callback); -int32_t videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent); -int32_t videoUnload(int32_t playerHandle); -int32_t videoUpdate(int32_t playerHandle, SDL_Texture **texture); +typedef void (*VideoIndexingCallbackT)(int32_t percent); + + +int32_t videoGetAudioTrack(int32_t playerHandle); +int32_t videoGetAudioTracks(int32_t playerHandle); +int64_t videoGetFrame(int32_t playerHandle); +int64_t videoGetFrameCount(int32_t playerHandle); +int32_t videoGetHeight(int32_t playerHandle); +const char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack); +const char *videoGetLanguageDescription(const char *languageCode); +bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b); +bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch); +void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent); +int32_t videoGetWidth(int32_t playerHandle); +void videoInit(int32_t mixerChunkFrames); +bool videoIsPlaying(int32_t playerHandle); +int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer); +void videoPause(int32_t playerHandle); +void videoPlay(int32_t playerHandle); +void videoQuit(void); +void videoSeek(int32_t playerHandle, int64_t seekFrame); +void videoSetAudioTrack(int32_t playerHandle, int32_t track); +void videoSetIndexCallback(VideoIndexingCallbackT callback); +void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent); +void videoUnload(int32_t playerHandle); +int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture); #endif // VIDEOPLAYER_H diff --git a/zbstudio/packages/Singe.fbp b/zbstudio/Singe.fbp similarity index 100% rename from zbstudio/packages/Singe.fbp rename to zbstudio/Singe.fbp diff --git a/zbstudio/packages/singetoolbar.lua b/zbstudio/packages/singetoolbar.lua index ddb481bca..98ecdf06c 100644 --- a/zbstudio/packages/singetoolbar.lua +++ b/zbstudio/packages/singetoolbar.lua @@ -5,7 +5,7 @@ * * 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 2 + * 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, @@ -15,7 +15,8 @@ * * 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. + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. * * --]] @@ -28,25 +29,20 @@ local settings local line = "-------------------------------------------------------------------------------" -local function utilDump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then - k = '"'..k..'"' - end - s = s .. '['..k..'] = ' .. utilDump(v) .. ',' - end - return s .. '} ' - else - return tostring(o) - end +-- 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) - - UI = {} + 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 ) @@ -116,12 +112,11 @@ local function configureSinge(self) UI.filePickScript:GetTextCtrl():Connect( wx.wxEVT_KEY_DOWN, function(event) end ) UI.filePickVideo:GetTextCtrl():Connect( wx.wxEVT_KEY_DOWN, function(event) end ) - if settings then - 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 - 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() @@ -161,10 +156,11 @@ local function startSinge(self) 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 then message = "You must specify the Singe executable in the Singe configuration." end - if not settings.script then message = "You must specify the script to run in the Singe configuration." end - if not settings.video then message = "You must specify the video to use in the Singe configuration." end + 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 @@ -174,17 +170,21 @@ local function startSinge(self) return end + -- Run from the Singe directory with paths made relative to it. wdir = settings.singe:match("(.*[/\\])") or "./" - launch = settings.singe .. ' ' .. settings.options .. ' -v ' .. settings.video .. ' ' .. settings.script - launch = launch:gsub(wdir, "./", 1):gsub(wdir, "") + 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) - wx.wxSetEnv('LUA_CPATH', env) + if ok then + wx.wxSetEnv('LUA_CPATH', env) + end end @@ -192,7 +192,7 @@ return { name = "Singe Integration", description = "Adds menu items and a toolbar button to assist with debugging Singe games.", author = "Scott Duensing", - version = 1.00, + version = 1.01, dependencies = "1.0", onRegister = function(self) diff --git a/zbstudio/user.lua b/zbstudio/user.lua index 9f2c00cd8..cc0cdc121 100644 --- a/zbstudio/user.lua +++ b/zbstudio/user.lua @@ -1,3 +1,26 @@ +--[[ + * + * Singe 2 + * Copyright (C) 2006-2024 Scott Duensing + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * +--]] + editor.specmap.singe = 'lua' editor.tabwidth = 4