Compare commits
No commits in common. "master" and "ffms-video-player" have entirely different histories.
master
...
ffms-video
32386 changed files with 50619 additions and 10187496 deletions
90
.gitattributes
vendored
90
.gitattributes
vendored
|
|
@ -1,90 +0,0 @@
|
|||
#
|
||||
# Image
|
||||
#
|
||||
*.xcf filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.bmp filter=lfs diff=lfs merge=lfs -text
|
||||
*.avif filter=lfs diff=lfs merge=lfs -text
|
||||
*.tiff filter=lfs diff=lfs merge=lfs -text
|
||||
*.ico filter=lfs diff=lfs merge=lfs -text
|
||||
*.jxl filter=lfs diff=lfs merge=lfs -text
|
||||
*.pcx filter=lfs diff=lfs merge=lfs -text
|
||||
*.pnm filter=lfs diff=lfs merge=lfs -text
|
||||
*.pgm filter=lfs diff=lfs merge=lfs -text
|
||||
*.qoi filter=lfs diff=lfs merge=lfs -text
|
||||
*.tga filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||
*.jbg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
#
|
||||
# Other Binary
|
||||
#
|
||||
*.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
|
||||
|
||||
#
|
||||
# Fonts
|
||||
#
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
#
|
||||
# Audio
|
||||
#
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
*.flac filter=lfs diff=lfs merge=lfs -text
|
||||
*.vgz filter=lfs diff=lfs merge=lfs -text
|
||||
*.nsf filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||
*.mod filter=lfs diff=lfs merge=lfs -text
|
||||
*.it filter=lfs diff=lfs merge=lfs -text
|
||||
*.itz filter=lfs diff=lfs merge=lfs -text
|
||||
*.MOD filter=lfs diff=lfs merge=lfs -text
|
||||
*.med filter=lfs diff=lfs merge=lfs -text
|
||||
*.xm filter=lfs diff=lfs merge=lfs -text
|
||||
*.oxm filter=lfs diff=lfs merge=lfs -text
|
||||
*.raw filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
*.opus filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
#
|
||||
# Document
|
||||
#
|
||||
*.odt filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
*.docx filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
#
|
||||
# Video
|
||||
#
|
||||
*.mkv filter=lfs diff=lfs merge=lfs -text
|
||||
*.avi filter=lfs diff=lfs merge=lfs -text
|
||||
*.mov filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.mpg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
#
|
||||
# Archive
|
||||
#
|
||||
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||
*.xz filter=lfs diff=lfs merge=lfs -text
|
||||
*.lz filter=lfs diff=lfs merge=lfs -text
|
||||
*.lzma filter=lfs diff=lfs merge=lfs -text
|
||||
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||
*.tar filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
#
|
||||
# Model
|
||||
#
|
||||
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||
*.stl filter=lfs diff=lfs merge=lfs -text
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
|
|
@ -1,24 +1,8 @@
|
|||
*~
|
||||
**/.builddir/
|
||||
**/.git-hidden/
|
||||
thirdparty/**/.github/
|
||||
cmake-build-debug/
|
||||
*.log
|
||||
*.user
|
||||
temp/
|
||||
*#
|
||||
build/
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
*.a
|
||||
*.o
|
||||
docs/.asciidoctor/
|
||||
.claude/settings.local.json
|
||||
**/__pycache__/
|
||||
|
||||
# Extracted by the binary when it is run from here; never source.
|
||||
/Singe/
|
||||
/Menu.sh
|
||||
/Menu.bat
|
||||
/data/
|
||||
screenshots/
|
||||
*.ISO
|
||||
*.iso
|
||||
*.m4v
|
||||
*.index
|
||||
*.avi
|
||||
*.vob
|
||||
|
|
|
|||
982
CMakeLists.txt
982
CMakeLists.txt
|
|
@ -1,982 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
# The version, in CMake's four numbers, because that is all CMake's project() will take:
|
||||
#
|
||||
# major . minor . patch . tweak
|
||||
# ^^^^^ ^^^^^ ^^^^^
|
||||
# kept as written (00 stays 00)
|
||||
# 0 = a release, 1 = a beta
|
||||
# which beta
|
||||
#
|
||||
# so 3.00.0.0 is "v3.00" and 3.00.1.2 is "v3.00b2". One line to edit, no second variable, and
|
||||
# the parts stay numbers, which is what the Windows resource needs anyway.
|
||||
#
|
||||
# A beta of 3.00 comes BEFORE 3.00, and the master service knows it: it reads the same shape out
|
||||
# of a version string and ranks a beta below the release of the same number, so a cabinet on
|
||||
# v3.00b2 is offered v3.00 when it arrives.
|
||||
project(singe2 VERSION 3.00.1.1 LANGUAGES C CXX)
|
||||
|
||||
if(PROJECT_VERSION_PATCH EQUAL 1)
|
||||
set(SINGE_VERSION_FULL "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}b${PROJECT_VERSION_TWEAK}")
|
||||
else()
|
||||
set(SINGE_VERSION_FULL "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
endif()
|
||||
|
||||
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)
|
||||
set(KANGAROO_OS linux)
|
||||
endif()
|
||||
if(NOT DEFINED KANGAROO_ARCH)
|
||||
set(KANGAROO_ARCH x86_64)
|
||||
endif()
|
||||
|
||||
# Where the superbuild installs the third party libraries for this platform. SINGE_TREE lets a
|
||||
# second toolchain for the same platform keep its own tree (the zig Linux build beside the gcc one).
|
||||
if(NOT DEFINED SINGE_TREE)
|
||||
set(SINGE_TREE ${KANGAROO_OS})
|
||||
endif()
|
||||
set(BUILD_DIR ${CMAKE_SOURCE_DIR}/.builddir/${SINGE_TREE}/${KANGAROO_ARCH})
|
||||
set(GENERATED_DIR ${CMAKE_BINARY_DIR}/generated)
|
||||
file(MAKE_DIRECTORY ${GENERATED_DIR})
|
||||
|
||||
# With SINGE_SUPERBUILD on (the presets' default) this configure builds the vendored libraries
|
||||
# first and then Singe as a nested project; see cmake/Superbuild.cmake.
|
||||
option(SINGE_SUPERBUILD "Build the vendored libraries, then Singe" OFF)
|
||||
if(SINGE_SUPERBUILD)
|
||||
include(cmake/Superbuild.cmake)
|
||||
return()
|
||||
endif()
|
||||
|
||||
|
||||
# ===== 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 <luasocket/...>.
|
||||
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()
|
||||
|
||||
# The canvas size of a GIMP image, from the XCF header (ImageMagick only sees the layers, and its
|
||||
# flatten crops to the first one).
|
||||
function(singeXcfCanvas xcf widthVar heightVar)
|
||||
file(READ ${xcf} header LIMIT 22 HEX)
|
||||
string(SUBSTRING ${header} 28 8 widthHex)
|
||||
string(SUBSTRING ${header} 36 8 heightHex)
|
||||
math(EXPR width "0x${widthHex}")
|
||||
math(EXPR height "0x${heightHex}")
|
||||
set(${widthVar} ${width} PARENT_SCOPE)
|
||||
set(${heightVar} ${height} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Renders assets/<source>.xcf as <name>.png the way GIMP would: every layer at its offset onto the
|
||||
# image's own canvas, anything outside it clipped.
|
||||
function(singeRenderImage name source)
|
||||
singeXcfCanvas(${CMAKE_SOURCE_DIR}/assets/${source}.xcf width height)
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/${name}.png
|
||||
COMMAND ${IMAGEMAGICK} -background "rgba(0,0,0,0)" -size ${width}x${height} xc:none ${CMAKE_SOURCE_DIR}/assets/${source}.xcf -flatten ${GENERATED_DIR}/${name}.png
|
||||
DEPENDS assets/${source}.xcf
|
||||
COMMENT "Rendering ${name}.png (${width}x${height})"
|
||||
VERBATIM
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# Renders a layered image and embeds the PNG.
|
||||
function(singeEmbedImage name)
|
||||
singeRenderImage(${name} ${name})
|
||||
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()
|
||||
|
||||
# assets/ holds only what is embedded in Singe. Sources those files are made from (the traced
|
||||
# artwork, OBJ and STL models, the sound samples) and anything else not embedded live in media/.
|
||||
|
||||
# Images
|
||||
singeEmbedImage(font)
|
||||
# The icon is drawn large; the window gets a 128 pixel copy and the Windows .ico the usual sizes,
|
||||
# both cut from a square, transparent-padded render so a non-square source keeps its proportions.
|
||||
set(ICON_SIZE 1024)
|
||||
singeRenderImage(iconFull icon)
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/iconSquare.png
|
||||
COMMAND ${IMAGEMAGICK} ${GENERATED_DIR}/iconFull.png -background "rgba(0,0,0,0)" -resize "${ICON_SIZE}x${ICON_SIZE}>" -gravity center -extent ${ICON_SIZE}x${ICON_SIZE} ${GENERATED_DIR}/iconSquare.png
|
||||
DEPENDS ${GENERATED_DIR}/iconFull.png
|
||||
COMMENT "Rendering iconSquare.png"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/icon.png
|
||||
COMMAND ${IMAGEMAGICK} ${GENERATED_DIR}/iconSquare.png -resize 128x128 ${GENERATED_DIR}/icon.png
|
||||
DEPENDS ${GENERATED_DIR}/iconSquare.png
|
||||
COMMENT "Rendering icon.png"
|
||||
VERBATIM
|
||||
)
|
||||
singeEmbed(${GENERATED_DIR}/icon.png ${GENERATED_DIR}/icon.h "")
|
||||
singeEmbedImage(kangarooPunchLogo)
|
||||
|
||||
# The picture shown where a game's art should be. Drawn here rather than kept as a file: it is
|
||||
# ours, it needs no license, and it cannot drift from what the code expects. Loud on purpose --
|
||||
# a blank space reads as a layout mistake, this reads as "your path is wrong".
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/missing.png
|
||||
COMMAND ${IMAGEMAGICK} -size 256x256 pattern:checkerboard -auto-level -fill "#c0208090" -colorize 60
|
||||
-fill "#ffd84a" -pointsize 34 -gravity center -annotate +0-14 "MISSING"
|
||||
-fill "#ffffffcc" -pointsize 18 -annotate +0+22 "art"
|
||||
-bordercolor "#ffd84a" -border 4 ${GENERATED_DIR}/missing.png
|
||||
COMMENT "Drawing missing.png"
|
||||
VERBATIM
|
||||
)
|
||||
singeEmbed(${GENERATED_DIR}/missing.png ${GENERATED_DIR}/missing_png.h "")
|
||||
singeEmbedImage(singeLogo)
|
||||
|
||||
# Windows icon for the resource file.
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/icon.ico
|
||||
COMMAND ${IMAGEMAGICK} ${GENERATED_DIR}/iconSquare.png -define icon:auto-resize=256,64,48,32,16 ${GENERATED_DIR}/icon.ico
|
||||
DEPENDS ${GENERATED_DIR}/iconSquare.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/Debugger.singe ${GENERATED_DIR}/Debugger_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "")
|
||||
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/cacerts.pem ${GENERATED_DIR}/cacerts_pem.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Master.singe ${GENERATED_DIR}/Master_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Backdrop.singe ${GENERATED_DIR}/Backdrop_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuDocument.singe ${GENERATED_DIR}/MenuDocument_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuOverlay.singe ${GENERATED_DIR}/MenuOverlay_singe.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.rml ${GENERATED_DIR}/Menu_rml.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menu.rcss ${GENERATED_DIR}/menu_rcss.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/gui.rcss ${GENERATED_DIR}/gui_rcss.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/scoreBezel.rml ${GENERATED_DIR}/scoreBezel_rml.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/scoreBezel.rcss ${GENERATED_DIR}/scoreBezel_rcss.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/subtitle.rml ${GENERATED_DIR}/subtitle_rml.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/subtitle.rcss ${GENERATED_DIR}/subtitle_rcss.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/FreeSansBold.ttf ${GENERATED_DIR}/FreeSansBold_ttf.h "")
|
||||
|
||||
# The two models the menu's intro is made of. Singe's own branding, so they belong with the engine
|
||||
# the way the font and the logos do -- the menu cannot reach into a game directory for them.
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/DragonModel.glb ${GENERATED_DIR}/DragonModel_glb.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/SingeText.glb ${GENERATED_DIR}/SingeText_glb.h "")
|
||||
|
||||
# Menu background video: a recording of the backdrop the menu draws, for the machines that cannot
|
||||
# draw it. It is not built here -- rendering it needs a GPU device and the engine itself, which is
|
||||
# the wrong way round for a build -- so it is an asset like the models are, made by
|
||||
# util/renderMenuVideo.py and re-made whenever assets/Backdrop.singe changes.
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menuBackground.mkv ${GENERATED_DIR}/menuBackground_mkv.h "")
|
||||
|
||||
# The same sound the video carries, for the renderer that draws the backdrop instead of playing it.
|
||||
# util/renderMenuVideo.py writes the two together.
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menuIntro.flac ${GENERATED_DIR}/menuIntro_flac.h "")
|
||||
|
||||
# Calibration click for the menu's audio delay screen: 20 ms of white noise with a fast fade.
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/click.wav
|
||||
COMMAND ${FFMPEG_TOOL} -y -loglevel error -f lavfi -i "anoisesrc=d=0.02:c=white:r=44100:a=0.6" -af "afade=t=out:st=0:d=0.02:curve=exp" -ac 1 -c:a pcm_s16le ${GENERATED_DIR}/click.wav
|
||||
COMMENT "Building click.wav"
|
||||
VERBATIM
|
||||
)
|
||||
singeEmbed(${GENERATED_DIR}/click.wav ${GENERATED_DIR}/click_wav.h "")
|
||||
|
||||
# Manual, rendered from the AsciiDoc source into the top-level build directory beside the binaries
|
||||
# (build-docs.sh writes the same files) and shipped inside the binary. compress deflates the PDF's
|
||||
# streams, which halves it. Its translations, one folder each under docs/, are rendered into a
|
||||
# folder of their language there and shipped as Manual-<language>.pdf; embedded.h and the table in
|
||||
# main.c list the same languages.
|
||||
set(MANUAL_DIR ${CMAKE_SOURCE_DIR}/.builddir)
|
||||
set(SINGE_DOC_LANGUAGES fr it de es)
|
||||
add_custom_command(
|
||||
OUTPUT ${MANUAL_DIR}/Manual.pdf
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
|
||||
COMMAND ${ASCIIDOCTOR_PDF} -a compress -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/Manual.adoc -o ${MANUAL_DIR}/Manual.pdf
|
||||
DEPENDS docs/Manual.adoc
|
||||
COMMENT "Rendering Manual.pdf"
|
||||
VERBATIM
|
||||
)
|
||||
singeEmbed(${MANUAL_DIR}/Manual.pdf ${GENERATED_DIR}/Manual_pdf.h "")
|
||||
foreach(language ${SINGE_DOC_LANGUAGES})
|
||||
add_custom_command(
|
||||
OUTPUT ${MANUAL_DIR}/${language}/Manual.pdf
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}/${language}
|
||||
COMMAND ${ASCIIDOCTOR_PDF} -a compress -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/${language}/Manual.adoc -o ${MANUAL_DIR}/${language}/Manual.pdf
|
||||
DEPENDS docs/${language}/Manual.adoc docs/${language}/locale.adoc
|
||||
COMMENT "Rendering ${language}/Manual.pdf"
|
||||
VERBATIM
|
||||
)
|
||||
singeEmbed(${MANUAL_DIR}/${language}/Manual.pdf ${GENERATED_DIR}/${language}_Manual_pdf.h "${language}_")
|
||||
endforeach()
|
||||
|
||||
# The ZeroBrane Studio package and its API, one per documentation language (util/zbstudioApi.py).
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/zbstudio/packages/singe.lua ${GENERATED_DIR}/zbstudio_singe_lua.h "zbstudio_")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/zbstudio/packages/singe/api.lua ${GENERATED_DIR}/zbstudio_api_lua.h "zbstudio_")
|
||||
foreach(language ${SINGE_DOC_LANGUAGES})
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/zbstudio/packages/singe/api-${language}.lua ${GENERATED_DIR}/zbstudio_api_${language}_lua.h "zbstudio_")
|
||||
endforeach()
|
||||
# Its toolbar button is the dragon, at the sizes ZeroBrane Studio's toolbar comes in, cut from the
|
||||
# same square render as the window's icon.
|
||||
foreach(size 16 24 32 48)
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/icon-${size}.png
|
||||
COMMAND ${IMAGEMAGICK} ${GENERATED_DIR}/iconSquare.png -resize ${size}x${size} ${GENERATED_DIR}/icon-${size}.png
|
||||
DEPENDS ${GENERATED_DIR}/iconSquare.png
|
||||
COMMENT "Rendering icon-${size}.png"
|
||||
VERBATIM
|
||||
)
|
||||
singeEmbed(${GENERATED_DIR}/icon-${size}.png ${GENERATED_DIR}/zbstudio_icon_${size}_png.h "zbstudio_")
|
||||
endforeach()
|
||||
|
||||
# The license Singe is under and the notices its third-party libraries and assets require. Both
|
||||
# have to reach whoever gets the binary, so they travel inside it like the manual does.
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/COPYING ${GENERATED_DIR}/COPYING.h "")
|
||||
singeEmbed(${CMAKE_SOURCE_DIR}/LICENSES ${GENERATED_DIR}/LICENSES.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/future.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 "")
|
||||
singeEmbedLua(thirdparty/middleclass/middleclass.lua "")
|
||||
singeEmbedLua(thirdparty/lume/lume.lua "")
|
||||
singeEmbedLua(thirdparty/inspect.lua/inspect.lua "")
|
||||
singeEmbedLua(thirdparty/bump.lua/bump.lua "")
|
||||
singeEmbedLua(thirdparty/MobDebug/src/mobdebug.lua "")
|
||||
|
||||
# Optional HTML for browsing: cmake --build . --target docs. The user manual is the one book the
|
||||
# binary carries; the reference and the beginner's course are the optional
|
||||
# documentation download that build-docs.sh packs. Each translation goes in its language's folder.
|
||||
if(ASCIIDOCTOR)
|
||||
set(DOCS_COMMANDS
|
||||
COMMAND ${ASCIIDOCTOR} -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/Manual.adoc -o ${MANUAL_DIR}/Manual.html
|
||||
COMMAND ${ASCIIDOCTOR} -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/Reference.adoc -o ${MANUAL_DIR}/Reference.html
|
||||
)
|
||||
set(DOCS_DEPENDS ${MANUAL_DIR}/Manual.pdf)
|
||||
foreach(language ${SINGE_DOC_LANGUAGES})
|
||||
list(APPEND DOCS_COMMANDS
|
||||
COMMAND ${ASCIIDOCTOR} -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/${language}/Manual.adoc -o ${MANUAL_DIR}/${language}/Manual.html
|
||||
COMMAND ${ASCIIDOCTOR} -a revnumber=${SINGE_VERSION_FULL} ${CMAKE_SOURCE_DIR}/docs/${language}/Reference.adoc -o ${MANUAL_DIR}/${language}/Reference.html
|
||||
)
|
||||
list(APPEND DOCS_DEPENDS ${MANUAL_DIR}/${language}/Manual.pdf)
|
||||
endforeach()
|
||||
add_custom_target(docs
|
||||
${DOCS_COMMANDS}
|
||||
DEPENDS ${DOCS_DEPENDS}
|
||||
COMMENT "Rendering Manual.html and Reference.html in every language"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
# ===== Sources =====
|
||||
|
||||
set(SINGE_SOURCE
|
||||
src/collide.c
|
||||
src/collide.h
|
||||
src/common.h
|
||||
src/debugger.c
|
||||
src/debugger.h
|
||||
src/decode.c
|
||||
src/decode.h
|
||||
src/embedded.h
|
||||
src/frameFile.c
|
||||
src/rotoZoom.c
|
||||
src/rotoZoom.h
|
||||
src/frameFile.h
|
||||
src/main.c
|
||||
src/main.h
|
||||
src/pack.c
|
||||
src/pack.h
|
||||
src/particles.c
|
||||
src/particles.h
|
||||
src/persist.c
|
||||
src/persist.h
|
||||
src/scheduler.c
|
||||
src/scheduler.h
|
||||
src/stats.c
|
||||
src/stats.h
|
||||
src/physics.h
|
||||
src/navRecast.cpp
|
||||
src/physicsJolt.cpp
|
||||
src/gui.cpp
|
||||
src/gui.h
|
||||
src/guiRender.cpp
|
||||
src/guiRender.h
|
||||
src/hdr.c
|
||||
src/ktx2.h
|
||||
src/ktx2Basis.cpp
|
||||
thirdparty/basis_universal/transcoder/basisu_transcoder.cpp
|
||||
src/model.c
|
||||
src/model.h
|
||||
src/math3d.c
|
||||
src/math3d.h
|
||||
src/midi.c
|
||||
src/midi.h
|
||||
src/midiIo.c
|
||||
src/midiIo.h
|
||||
src/mic.c
|
||||
src/mic.h
|
||||
src/voice.c
|
||||
src/voice.h
|
||||
src/netTrust.c
|
||||
src/netTrust.h
|
||||
src/record.c
|
||||
src/record.h
|
||||
thirdparty/tinysoundfont/tsf.h
|
||||
thirdparty/tinysoundfont/tml.h
|
||||
src/render.c
|
||||
src/render.h
|
||||
src/renderGles.c
|
||||
src/renderGlesApi.h
|
||||
src/renderGlesLoad.c
|
||||
src/renderGpu.c
|
||||
src/scene.c
|
||||
src/scene.h
|
||||
src/scriptWatch.c
|
||||
src/scriptWatch.h
|
||||
src/singe.c
|
||||
src/singe.h
|
||||
src/stddclmr.h
|
||||
src/util.c
|
||||
src/util.h
|
||||
src/videoPlayer.c
|
||||
src/videoPlayer.h
|
||||
src/vfs.c
|
||||
src/vfs.h
|
||||
)
|
||||
|
||||
# The binding reports this from sqlite3.lversion(); it is the version of thirdparty/lsqlite3/lsqlite3.c.
|
||||
set(LSQLITE_VERSION "0.9.7")
|
||||
set(SQLITE_SOURCE
|
||||
thirdparty/sqlite/sqlite3.c
|
||||
thirdparty/sqlite/sqlite3.h
|
||||
thirdparty/lsqlite3/lsqlite3.c
|
||||
)
|
||||
set_source_files_properties(thirdparty/lsqlite3/lsqlite3.c PROPERTIES COMPILE_DEFINITIONS "LSQLITE_VERSION=\"${LSQLITE_VERSION}\"")
|
||||
|
||||
# SpeexDSP 1.2.1 (thirdparty/speexdsp): echo cancellation, noise suppression, gain, voice activity,
|
||||
# and a jitter buffer, for voice.c. It has no CMake build, so its library sources are compiled here,
|
||||
# floating point, with its own small FFT. speexdsp_config_types.h is written by hand (see its top).
|
||||
set(SPEEXDSP_SOURCE
|
||||
thirdparty/speexdsp/libspeexdsp/buffer.c
|
||||
thirdparty/speexdsp/libspeexdsp/fftwrap.c
|
||||
thirdparty/speexdsp/libspeexdsp/filterbank.c
|
||||
thirdparty/speexdsp/libspeexdsp/jitter.c
|
||||
thirdparty/speexdsp/libspeexdsp/mdf.c
|
||||
thirdparty/speexdsp/libspeexdsp/preprocess.c
|
||||
thirdparty/speexdsp/libspeexdsp/resample.c
|
||||
thirdparty/speexdsp/libspeexdsp/scal.c
|
||||
thirdparty/speexdsp/libspeexdsp/smallft.c
|
||||
)
|
||||
set_source_files_properties(${SPEEXDSP_SOURCE} PROPERTIES COMPILE_DEFINITIONS "FLOATING_POINT;USE_SMALLFT;EXPORT=" COMPILE_OPTIONS "-w")
|
||||
|
||||
set(ARG_PARSER_SOURCE
|
||||
thirdparty/arg_parser/carg_parser.c
|
||||
thirdparty/arg_parser/carg_parser.h
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# The PUC-Rio md5 library, as Hypseus bundles it (its sumhexa is named hexsum), so the games that
|
||||
# require("md5") get the same five functions with the same behavior.
|
||||
set(LUA_MD5_SOURCE
|
||||
thirdparty/md5/md5.c
|
||||
thirdparty/md5/md5.h
|
||||
thirdparty/md5/md5lib.c
|
||||
)
|
||||
|
||||
# The rest of the Lua modules Hypseus exposes, so a game written against it finds them here too:
|
||||
# LuaBitOp, lrandom, Lua CJSON and LPeg.
|
||||
set(LUA_BITOP_SOURCE
|
||||
thirdparty/luabitop/lbit.c
|
||||
)
|
||||
|
||||
set(LUA_RANDOM_SOURCE
|
||||
thirdparty/lrandom/lrandom.c
|
||||
)
|
||||
|
||||
set(LUA_CJSON_SOURCE
|
||||
thirdparty/lua-cjson/fpconv.c
|
||||
thirdparty/lua-cjson/fpconv.h
|
||||
thirdparty/lua-cjson/lua_cjson.c
|
||||
thirdparty/lua-cjson/strbuf.c
|
||||
thirdparty/lua-cjson/strbuf.h
|
||||
)
|
||||
|
||||
set(LPEG_SOURCE
|
||||
thirdparty/lpeg/lpcap.c
|
||||
thirdparty/lpeg/lpcap.h
|
||||
thirdparty/lpeg/lpcode.c
|
||||
thirdparty/lpeg/lpcode.h
|
||||
thirdparty/lpeg/lpcset.c
|
||||
thirdparty/lpeg/lpcset.h
|
||||
thirdparty/lpeg/lpprint.c
|
||||
thirdparty/lpeg/lpprint.h
|
||||
thirdparty/lpeg/lptree.c
|
||||
thirdparty/lpeg/lptree.h
|
||||
thirdparty/lpeg/lptypes.h
|
||||
thirdparty/lpeg/lpvm.c
|
||||
thirdparty/lpeg/lpvm.h
|
||||
)
|
||||
|
||||
set(LUA_SOCKET_SOURCE
|
||||
thirdparty/luasocket/src/auxiliar.c
|
||||
thirdparty/luasocket/src/auxiliar.h
|
||||
thirdparty/luasocket/src/buffer.c
|
||||
thirdparty/luasocket/src/buffer.h
|
||||
thirdparty/luasocket/src/compat.c
|
||||
thirdparty/luasocket/src/compat.h
|
||||
thirdparty/luasocket/src/except.c
|
||||
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/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)
|
||||
list(APPEND LUA_SOCKET_SOURCE
|
||||
thirdparty/luasocket/src/wsocket.c
|
||||
thirdparty/luasocket/src/wsocket.h
|
||||
)
|
||||
else()
|
||||
list(APPEND LUA_SOCKET_SOURCE
|
||||
thirdparty/luasocket/src/serial.c
|
||||
thirdparty/luasocket/src/unix.c
|
||||
thirdparty/luasocket/src/unix.h
|
||||
thirdparty/luasocket/src/unixdgram.c
|
||||
thirdparty/luasocket/src/unixdgram.h
|
||||
thirdparty/luasocket/src/unixstream.c
|
||||
thirdparty/luasocket/src/unixstream.h
|
||||
thirdparty/luasocket/src/usocket.c
|
||||
thirdparty/luasocket/src/usocket.h
|
||||
)
|
||||
endif()
|
||||
|
||||
set(LUASEC_SOURCE
|
||||
${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)
|
||||
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
|
||||
thirdparty/manymouse/macosx_hidutilities.c
|
||||
thirdparty/manymouse/manymouse.c
|
||||
thirdparty/manymouse/manymouse.h
|
||||
thirdparty/manymouse/windows_wminput.c
|
||||
thirdparty/manymouse/x11_xinput2.c
|
||||
)
|
||||
|
||||
|
||||
# ===== Target =====
|
||||
|
||||
add_executable(${CMAKE_PROJECT_NAME}
|
||||
${SINGE_SOURCE}
|
||||
${EMBEDDED_HEADERS}
|
||||
${GENERATED_DIR}/version.h
|
||||
${ARG_PARSER_SOURCE}
|
||||
${LUA_SOURCE}
|
||||
${LUA_FILESYSTEM_SOURCE}
|
||||
${LUA_MD5_SOURCE}
|
||||
${LUA_BITOP_SOURCE}
|
||||
${LUA_RANDOM_SOURCE}
|
||||
${LUA_CJSON_SOURCE}
|
||||
${LPEG_SOURCE}
|
||||
${SQLITE_SOURCE}
|
||||
${LUA_SOCKET_SOURCE}
|
||||
${LUASEC_SOURCE}
|
||||
${LUA_RS232_SOURCE}
|
||||
${UTHASH_SOURCE}
|
||||
${MANYMOUSE_SOURCE}
|
||||
${SPEEXDSP_SOURCE}
|
||||
)
|
||||
if(WIN32)
|
||||
if(SINGE_ZIG_TARGET)
|
||||
# CMake archives every object into objects.a for MinGW links, and zig's linker cannot take a
|
||||
# compiled resource from inside an archive; compiled on its own it links as a plain input.
|
||||
add_custom_command(
|
||||
OUTPUT ${GENERATED_DIR}/singe.res
|
||||
COMMAND ${CMAKE_RC_COMPILER} /fo ${GENERATED_DIR}/singe.res ${GENERATED_DIR}/singe.rc
|
||||
DEPENDS ${GENERATED_DIR}/singe.rc ${GENERATED_DIR}/icon.ico
|
||||
COMMENT "Compiling resources"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(singeResources DEPENDS ${GENERATED_DIR}/singe.res)
|
||||
add_dependencies(${CMAKE_PROJECT_NAME} singeResources)
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE ${GENERATED_DIR}/singe.res)
|
||||
else()
|
||||
enable_language(RC)
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${GENERATED_DIR}/singe.rc ${GENERATED_DIR}/icon.ico)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Output name matches the release artifact: Singe-v3.00-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${SINGE_VERSION_FULL}-${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")
|
||||
# librs232 stamps its build date into the module; clang under zig makes that an error by default.
|
||||
set_source_files_properties(thirdparty/librs232/bindings/lua/luars232.c PROPERTIES COMPILE_OPTIONS "-Wno-date-time")
|
||||
# The Basis transcoder is warning-heavy third-party C++; it is compiled quietly.
|
||||
set_source_files_properties(thirdparty/basis_universal/transcoder/basisu_transcoder.cpp PROPERTIES COMPILE_OPTIONS "-w")
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
# The platform part of the release name (Linux-x86_64), so a script can ask which release it
|
||||
# is and fetch the right one.
|
||||
SINGE_PLATFORM="${osInitial}${osRest}-${KANGAROO_ARCH}"
|
||||
RS232_STATIC
|
||||
BASISD_SUPPORT_KTX2=1
|
||||
BASISD_SUPPORT_KTX2_ZSTD=1
|
||||
BASISD_SUPPORT_PVRTC1=0
|
||||
BASISD_SUPPORT_PVRTC2=0
|
||||
BASISD_SUPPORT_ATC=0
|
||||
BASISD_SUPPORT_FXT1=0
|
||||
BASISD_SUPPORT_ETC2_EAC_A8=0
|
||||
BASISD_SUPPORT_ETC2_EAC_RG11=0
|
||||
SQLITE_DQS=0
|
||||
SQLITE_THREADSAFE=1
|
||||
SQLITE_DEFAULT_MEMSTATUS=0
|
||||
SQLITE_OMIT_SHARED_CACHE
|
||||
)
|
||||
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
|
||||
_WIN32_WINNT=0x0600
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
-Wno-deprecated-declarations
|
||||
)
|
||||
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
${CMAKE_BINARY_DIR}
|
||||
${GENERATED_DIR}/include
|
||||
${BUILD_DIR}
|
||||
${BUILD_DIR}/include
|
||||
thirdparty/lua/src
|
||||
thirdparty/sqlite
|
||||
thirdparty/luasec/src
|
||||
thirdparty/librs232/include
|
||||
thirdparty/basis_universal/transcoder
|
||||
# Opus is the one SDL3_mixer builds for Ogg Opus music (libopus.a in STATIC_LIBS), so the voice
|
||||
# codec (voice.c) and the music decoder are one copy of one version.
|
||||
thirdparty/SDL3_mixer/external/opus/include
|
||||
thirdparty/speexdsp/include
|
||||
)
|
||||
|
||||
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
${BUILD_DIR}/lib
|
||||
)
|
||||
|
||||
|
||||
# ===== Libraries =====
|
||||
|
||||
# Platform specific system libraries.
|
||||
if(KANGAROO_OS STREQUAL "linux")
|
||||
if(KANGAROO_ARCH MATCHES "^(aarch64|armhf)$")
|
||||
set(SYSTEM_LIBS -ldl)
|
||||
# An ARM board decodes video through its V4L2 memory-to-memory device (videoPlayer.c).
|
||||
# Nothing here is particular to a Raspberry Pi: any board whose kernel offers one is served.
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE SINGE_V4L2_DECODE)
|
||||
# The stateless decoders are a separate interface again, and FFmpeg builds them only when its
|
||||
# own dependencies were met, so the engine follows what FFmpeg actually did.
|
||||
if(EXISTS ${BUILD_DIR}/build/ffmpeg/config.h)
|
||||
file(READ ${BUILD_DIR}/build/ffmpeg/config.h ffmpegConfig)
|
||||
if(ffmpegConfig MATCHES "#define CONFIG_V4L2_REQUEST 1")
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE SINGE_V4L2_REQUEST_DECODE)
|
||||
message(STATUS "FFmpeg carries the V4L2 stateless decoders; enabling them in the player")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
# ffmpeg's vdpau hardware context is always compiled in on X11 hosts.
|
||||
set(SYSTEM_LIBS -lX11 -lvdpau -ldl -rdynamic) # -rdynamic gives the crash backtrace function names
|
||||
endif()
|
||||
# Release binaries are stripped; the crash backtrace reads the dynamic symbols -rdynamic exports,
|
||||
# which stripping keeps.
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
list(APPEND SYSTEM_LIBS -s)
|
||||
endif()
|
||||
elseif(KANGAROO_OS STREQUAL "macos")
|
||||
set(SYSTEM_LIBS
|
||||
-Wl,-framework,CoreVideo
|
||||
-Wl,-framework,Cocoa
|
||||
-Wl,-framework,IOKit
|
||||
-Wl,-framework,ForceFeedback
|
||||
-Wl,-framework,Carbon
|
||||
-Wl,-framework,CoreAudio
|
||||
-Wl,-framework,AudioToolbox
|
||||
-Wl,-framework,AVFoundation
|
||||
-Wl,-framework,Foundation
|
||||
-Wl,-framework,Security
|
||||
-Wl,-weak_framework,GameController
|
||||
-Wl,-weak_framework,Metal
|
||||
-Wl,-weak_framework,QuartzCore
|
||||
-Wl,-weak_framework,CoreHaptics
|
||||
-Wl,-weak_framework,CoreMIDI
|
||||
-Wl,-weak_framework,UniformTypeIdentifiers
|
||||
-liconv
|
||||
-lc++
|
||||
)
|
||||
if(NOT SINGE_ZIG_TARGET)
|
||||
# osxcross builds need clang's runtime library named; zig carries its own.
|
||||
if(NOT DEFINED OSXCROSS_TARGET_DIR)
|
||||
message(FATAL_ERROR "OSXCROSS_TARGET_DIR must be set for macOS builds without zig.")
|
||||
endif()
|
||||
list(APPEND SYSTEM_LIBS ${OSXCROSS_TARGET_DIR}/darwin/libclang_rt.osx.a)
|
||||
endif()
|
||||
elseif(KANGAROO_OS STREQUAL "windows")
|
||||
# A GUI-subsystem executable, so no console window opens; zig's linker wants the flag spelled out.
|
||||
if(SINGE_ZIG_TARGET)
|
||||
set(windowsSubsystem -Wl,--subsystem,windows)
|
||||
else()
|
||||
set(windowsSubsystem -mwindows)
|
||||
endif()
|
||||
set(SYSTEM_LIBS
|
||||
${windowsSubsystem}
|
||||
-static
|
||||
-lmingw32
|
||||
-latomic
|
||||
-lole32
|
||||
-luser32
|
||||
-lkernel32
|
||||
-lgdi32
|
||||
-lwinmm
|
||||
-limm32
|
||||
-loleaut32
|
||||
-lversion
|
||||
-luuid
|
||||
-lrpcrt4
|
||||
-ladvapi32
|
||||
-lsetupapi
|
||||
-lshell32
|
||||
-ldinput8
|
||||
-lws2_32
|
||||
-lbcrypt
|
||||
-lcrypt32
|
||||
)
|
||||
if(NOT SINGE_ZIG_TARGET)
|
||||
# MinGW's stack protector runtime; zig's compiler-rt carries it itself.
|
||||
list(APPEND SYSTEM_LIBS -lssp)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Unknown KANGAROO_OS: ${KANGAROO_OS}")
|
||||
endif()
|
||||
|
||||
# FFmpeg's hardware decoding backends (VA-API, VDPAU, DRM, V4L2, VideoToolbox) are whatever the
|
||||
# superbuild configured; its pkg-config files name the system libraries and frameworks they need, so
|
||||
# read them back.
|
||||
if(NOT KANGAROO_OS STREQUAL "windows")
|
||||
foreach(pc libavutil libavcodec)
|
||||
set(pcFile ${BUILD_DIR}/lib/pkgconfig/${pc}.pc)
|
||||
if(EXISTS ${pcFile})
|
||||
file(STRINGS ${pcFile} pcLibs REGEX "^Libs:")
|
||||
string(REGEX REPLACE "^Libs: *" "" pcLibs "${pcLibs}")
|
||||
separate_arguments(pcLibs)
|
||||
set(framework FALSE)
|
||||
foreach(token ${pcLibs})
|
||||
if(framework)
|
||||
list(APPEND SYSTEM_LIBS -Wl,-framework,${token})
|
||||
set(framework FALSE)
|
||||
elseif(token STREQUAL "-framework")
|
||||
set(framework TRUE)
|
||||
elseif(token MATCHES "^-l" AND NOT token MATCHES "^-l(avutil|avcodec|z|m|atomic|pthread|iconv)$")
|
||||
list(APPEND SYSTEM_LIBS ${token})
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES SYSTEM_LIBS)
|
||||
endif()
|
||||
|
||||
# Static third party libraries built by build-all.sh.
|
||||
set(STATIC_LIBS
|
||||
${BUILD_DIR}/lib/libavcodec.a
|
||||
${BUILD_DIR}/lib/libavdevice.a
|
||||
${BUILD_DIR}/lib/libavfilter.a
|
||||
${BUILD_DIR}/lib/libavformat.a
|
||||
${BUILD_DIR}/lib/libavutil.a
|
||||
${BUILD_DIR}/lib/libfreetype.a
|
||||
${BUILD_DIR}/lib/libopus.a
|
||||
${BUILD_DIR}/lib/libopusfile.a
|
||||
${BUILD_DIR}/lib/libogg.a
|
||||
${BUILD_DIR}/lib/libSDL3.a
|
||||
${BUILD_DIR}/lib/libSDL3_image.a
|
||||
${BUILD_DIR}/lib/libSDL3_mixer.a
|
||||
${BUILD_DIR}/lib/libSDL3_ttf.a
|
||||
${BUILD_DIR}/lib/libswresample.a
|
||||
${BUILD_DIR}/lib/libswscale.a
|
||||
${BUILD_DIR}/lib/libwavpack.a
|
||||
${BUILD_DIR}/lib/libwebp.a
|
||||
${BUILD_DIR}/lib/libwebpdemux.a
|
||||
${BUILD_DIR}/lib/libwebpmux.a
|
||||
${BUILD_DIR}/lib/libsharpyuv.a
|
||||
${BUILD_DIR}/lib/libxmp.a
|
||||
${BUILD_DIR}/lib/libz.a
|
||||
${BUILD_DIR}/lib/libzstd.a
|
||||
${BUILD_DIR}/lib/libcrypto.a
|
||||
${BUILD_DIR}/lib/libssl.a
|
||||
)
|
||||
# Not every archive exists in every tree: SDL3_image builds its own jpeg and png decoders where the
|
||||
# platform has none (macOS uses ImageIO for jpeg), brotli arrives with FreeType's WOFF2 support, and
|
||||
# libgme with the chiptune decoder. Link whichever ones are there.
|
||||
foreach(optionalLib libjpeg.a libpng16.a libbrotlidec.a libbrotlicommon.a libgme.a libdav1d.a libjxl_dec.a libhwy.a)
|
||||
if(EXISTS ${BUILD_DIR}/lib/${optionalLib})
|
||||
list(APPEND STATIC_LIBS ${BUILD_DIR}/lib/${optionalLib})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# The scene shaders: src/shaders/scene.hlsl compiled at build time into a header of SPIR-V, DXIL
|
||||
# and MSL blobs by SDL_shadercross, which the superbuild builds for the host (cmake/hostTools.cmake)
|
||||
# and names in SINGE_SHADERCROSS; configuring this project alone finds one on the PATH.
|
||||
if(NOT SINGE_SHADERCROSS)
|
||||
find_program(SINGE_SHADERCROSS shadercross)
|
||||
if(NOT SINGE_SHADERCROSS)
|
||||
message(FATAL_ERROR "SDL_shadercross is needed to compile the scene shaders; build through the superbuild (cmake --preset ...) or set SINGE_SHADERCROSS.")
|
||||
endif()
|
||||
endif()
|
||||
# SPIRV-Cross turns the same SPIR-V into GLSL ES for the GLES backend (PLAN.md section 56). The
|
||||
# superbuild builds it beside shadercross and hands it over, so there it is fatal when missing: a
|
||||
# warning let every release binary ship with null ES strings and no GLES backend, unnoticed.
|
||||
# Configuring this project alone it is only a warning; the header then carries null ES strings,
|
||||
# the GLES backend declines to start, and every other platform builds exactly as before.
|
||||
if(SINGE_FROM_SUPERBUILD)
|
||||
if((NOT SINGE_SPIRV_CROSS) OR (NOT EXISTS ${SINGE_SPIRV_CROSS}))
|
||||
message(FATAL_ERROR "The superbuild did not supply spirv-cross (SINGE_SPIRV_CROSS='${SINGE_SPIRV_CROSS}'); without it there are no OpenGL ES shaders.")
|
||||
endif()
|
||||
else()
|
||||
if(NOT SINGE_SPIRV_CROSS)
|
||||
find_program(SINGE_SPIRV_CROSS spirv-cross)
|
||||
endif()
|
||||
if(NOT SINGE_SPIRV_CROSS)
|
||||
message(WARNING "spirv-cross was not found; this build will have no OpenGL ES shaders and so no GLES backend.")
|
||||
endif()
|
||||
endif()
|
||||
set(shaderHeader ${CMAKE_BINARY_DIR}/generated/shaders/sceneShaders.h)
|
||||
add_custom_command(
|
||||
OUTPUT ${shaderHeader}
|
||||
COMMAND ${CMAKE_COMMAND} -DSHADERCROSS=${SINGE_SHADERCROSS} -DSPIRVCROSS=${SINGE_SPIRV_CROSS} -DSOURCE=${CMAKE_SOURCE_DIR}/src/shaders/scene.hlsl -DOUTPUT=${shaderHeader} -P ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/src/shaders/scene.hlsl ${CMAKE_SOURCE_DIR}/src/sceneShared.h ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
|
||||
COMMENT "Compiling the scene shaders"
|
||||
)
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${shaderHeader})
|
||||
# The GUI shaders the same way: src/shaders/gui.hlsl into guiShaders.h for guiRender.cpp. The
|
||||
# entry list is a CMake list, so it is quoted as one argument and VERBATIM keeps the semicolons.
|
||||
set(guiShaderHeader ${CMAKE_BINARY_DIR}/generated/shaders/guiShaders.h)
|
||||
set(guiShaderEntries "guiVertex:vertex;guiFragmentColor:fragment;guiFragmentTexture:fragment;guiFragmentFilter:fragment;guiFragmentBlur:fragment;guiFragmentColorMatrix:fragment;guiFragmentGradient:fragment")
|
||||
add_custom_command(
|
||||
OUTPUT ${guiShaderHeader}
|
||||
COMMAND ${CMAKE_COMMAND} -DSHADERCROSS=${SINGE_SHADERCROSS} -DSPIRVCROSS=${SINGE_SPIRV_CROSS} -DSOURCE=${CMAKE_SOURCE_DIR}/src/shaders/gui.hlsl -DOUTPUT=${guiShaderHeader} "-DENTRIES=${guiShaderEntries}" -DPREFIX=guiShader -DTYPE=GuiShaderT -P ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/src/shaders/gui.hlsl ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
|
||||
COMMENT "Compiling the GUI shaders"
|
||||
VERBATIM
|
||||
)
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${guiShaderHeader})
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR}/generated)
|
||||
|
||||
# Rockchip's hardware decoders are reached by name (videoPlayer.c), and only exist when FFmpeg was
|
||||
# built against the Rockchip MPP library. Asking FFmpeg itself is what decides it.
|
||||
if(EXISTS ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc)
|
||||
file(READ ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc avcodecPc)
|
||||
if(avcodecPc MATCHES "rockchip_mpp")
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE SINGE_RKMPP_DECODE)
|
||||
message(STATUS "FFmpeg carries the rkmpp decoders; enabling them in the player")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Jolt Physics comes with the compile definitions it was built with (SIMD level, layer bits) through
|
||||
# its exported target; the one C++ file in the tree (physicsJolt.cpp) needs them. Its C++ runtime is
|
||||
# zig's own libc++, linked statically, or the host's libstdc++ under gcc; macOS already lists -lc++.
|
||||
find_package(Jolt CONFIG REQUIRED PATHS ${BUILD_DIR}/lib/cmake/Jolt NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
|
||||
# Recast Navigation the same way (navRecast.cpp): Recast bakes, Detour queries, DetourCrowd steers.
|
||||
find_package(recastnavigation CONFIG REQUIRED PATHS ${BUILD_DIR}/lib/cmake/recastnavigation NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
|
||||
# RmlUi (gui.cpp, and guiRender.cpp, the SDL_GPU render interface forked from RmlUi's Backends).
|
||||
# Its package config looks for Freetype::Freetype and Lua::Lua only when they are not already
|
||||
# targets, so both are declared here from the superbuild's FreeType and the engine's own Lua.
|
||||
add_library(Freetype::Freetype STATIC IMPORTED)
|
||||
set_target_properties(Freetype::Freetype PROPERTIES IMPORTED_LOCATION ${BUILD_DIR}/lib/libfreetype.a INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/thirdparty/SDL3_ttf/external/freetype/include)
|
||||
add_library(Lua::Lua INTERFACE IMPORTED)
|
||||
set_target_properties(Lua::Lua PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/thirdparty/lua/src)
|
||||
find_package(RmlUi CONFIG REQUIRED PATHS ${BUILD_DIR}/lib/cmake/RmlUi NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
|
||||
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF)
|
||||
# Jolt is built without RTTI, so the file that subclasses its debug renderer must be too, or the
|
||||
# subclass's vtable wants type information the library never made.
|
||||
set_source_files_properties(src/physicsJolt.cpp PROPERTIES COMPILE_OPTIONS "-fno-rtti")
|
||||
if(NOT KANGAROO_OS STREQUAL "macos")
|
||||
if(SINGE_ZIG_TARGET)
|
||||
list(APPEND SYSTEM_LIBS -lc++)
|
||||
else()
|
||||
list(APPEND SYSTEM_LIBS -lstdc++)
|
||||
endif()
|
||||
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} Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
|
||||
else()
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
|
||||
endif()
|
||||
|
||||
|
||||
install(TARGETS ${CMAKE_PROJECT_NAME})
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
{
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 25,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "base",
|
||||
"hidden": true,
|
||||
"generator": "Unix Makefiles",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"SINGE_SUPERBUILD": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-x86_64",
|
||||
"displayName": "Linux x86_64 (zig, glibc 2.28, release)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/linux/x86_64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/zig/x86_64-linux-gnu.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "x86_64-linux-gnu",
|
||||
"SINGE_CROSS_OS": "linux",
|
||||
"SINGE_SUFFIX": "",
|
||||
"SINGE_TREE": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-x86_64-gcc",
|
||||
"displayName": "Linux x86_64 (host gcc, developer build)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/linux-gcc/x86_64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/linux-x86_64.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "x86_64-linux-gnu",
|
||||
"SINGE_CROSS_OS": "linux",
|
||||
"SINGE_SUFFIX": "-gcc",
|
||||
"SINGE_TREE": "linux-gcc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-x86_64",
|
||||
"displayName": "Windows x86_64 (zig)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/windows/x86_64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/zig/x86_64-windows-gnu.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "x86_64-w64-mingw32",
|
||||
"SINGE_CROSS_OS": "mingw32",
|
||||
"SINGE_SUFFIX": ".exe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "windows-aarch64",
|
||||
"displayName": "Windows on ARM64 (zig)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/windows/aarch64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/zig/aarch64-windows-gnu.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "aarch64-w64-mingw32",
|
||||
"SINGE_CROSS_OS": "mingw32",
|
||||
"SINGE_SUFFIX": ".exe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linux-aarch64",
|
||||
"displayName": "ARM64 Linux (zig)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/linux/aarch64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/zig/aarch64-linux-gnu.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "aarch64-linux-gnu",
|
||||
"SINGE_CROSS_OS": "linux",
|
||||
"SINGE_SUFFIX": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "macos-aarch64",
|
||||
"displayName": "macOS Apple silicon (zig)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/macos/aarch64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/zig/aarch64-macos.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "aarch64-apple-darwin22.4",
|
||||
"SINGE_CROSS_OS": "darwin",
|
||||
"SINGE_SUFFIX": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "macos-x86_64",
|
||||
"displayName": "macOS Intel (zig)",
|
||||
"inherits": "base",
|
||||
"binaryDir": "${sourceDir}/.builddir/macos/x86_64/superbuild",
|
||||
"toolchainFile": "${sourceDir}/cmake/zig/x86_64-macos.cmake",
|
||||
"cacheVariables": {
|
||||
"SINGE_TRIPLE": "x86_64-apple-darwin22.4",
|
||||
"SINGE_CROSS_OS": "darwin",
|
||||
"SINGE_SUFFIX": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "linux-x86_64",
|
||||
"configurePreset": "linux-x86_64"
|
||||
},
|
||||
{
|
||||
"name": "linux-x86_64-gcc",
|
||||
"configurePreset": "linux-x86_64-gcc"
|
||||
},
|
||||
{
|
||||
"name": "windows-x86_64",
|
||||
"configurePreset": "windows-x86_64"
|
||||
},
|
||||
{
|
||||
"name": "windows-aarch64",
|
||||
"configurePreset": "windows-aarch64"
|
||||
},
|
||||
{
|
||||
"name": "linux-aarch64",
|
||||
"configurePreset": "linux-aarch64"
|
||||
},
|
||||
{
|
||||
"name": "macos-aarch64",
|
||||
"configurePreset": "macos-aarch64"
|
||||
},
|
||||
{
|
||||
"name": "macos-x86_64",
|
||||
"configurePreset": "macos-x86_64"
|
||||
}
|
||||
]
|
||||
}
|
||||
674
COPYING
674
COPYING
|
|
@ -1,674 +0,0 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
151
INSTALL
151
INSTALL
|
|
@ -1,151 +0,0 @@
|
|||
SINGE 3.00
|
||||
==========
|
||||
|
||||
(For the latest version of this document, visit https://kangaroopunch.com!
|
||||
The full manual is extracted to Singe/Manual.pdf on first run, beside
|
||||
Singe/COPYING and Singe/LICENSES.)
|
||||
|
||||
Welcome to Singe! Scott's Increasingly Nifty 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.
|
||||
|
||||
|
||||
BUILDING FROM SOURCE
|
||||
====================
|
||||
|
||||
On a Debian based system a checkout builds with one command:
|
||||
|
||||
./build-all.sh linux x86_64
|
||||
|
||||
It installs the host packages it needs with apt (asks for your sudo
|
||||
password once), then runs the CMake superbuild, which builds every vendored
|
||||
library (zlib, zstd, SDL3 and its satellites, OpenSSL, FFmpeg) into
|
||||
.builddir/<os>/<arch> and then Singe against them. The finished binary is
|
||||
copied to .builddir/Singe-v<version>-<Os>-<arch>.
|
||||
|
||||
Host packages (what build-all.sh installs): build-essential cmake
|
||||
git-lfs pkg-config perl nasm llvm autoconf automake libtool meson
|
||||
ninja-build imagemagick ffmpeg lua5.4 asciidoctor ruby-asciidoctor-pdf
|
||||
libva-dev libvdpau-dev libdrm-dev libgl-dev libegl-dev libgles-dev
|
||||
libgbm-dev libasound2-dev libpulse-dev libpipewire-0.3-dev
|
||||
libjack-jackd2-dev libsndio-dev libudev-dev libdbus-1-dev
|
||||
libibus-1.0-dev libxkbcommon-dev libx11-dev libxext-dev libxfixes-dev
|
||||
libxi-dev libxcursor-dev libxrandr-dev libxss-dev libxtst-dev
|
||||
libwayland-dev wayland-protocols libdecor-0-dev.
|
||||
|
||||
The artwork, the font, the models, and the menu video in assets/ and
|
||||
media/ are stored with Git LFS. Clone with git-lfs present, or those
|
||||
files arrive as small text pointers and the build embeds the pointers
|
||||
instead of the assets; a checkout already made without it is repaired
|
||||
with "git lfs install && git lfs pull". build-all.sh stops with an
|
||||
error rather than build a binary around them.
|
||||
|
||||
The Linux release build and the Windows build use zig as the compiler:
|
||||
the build fetches a pinned zig release (version and SHA-256 in
|
||||
cmake/zig/zig.cmake) into .builddir/toolchains on first use. For Linux it
|
||||
targets glibc 2.28, so the binary runs on distributions back to Debian 10
|
||||
and Ubuntu 18.04 while linking the host's platform libraries (X11, ALSA,
|
||||
VA-API, VDPAU, DRM); for Windows zig carries the headers and import
|
||||
libraries itself, so nothing else is installed:
|
||||
|
||||
./build-all.sh windows x86_64
|
||||
./build-all.sh windows aarch64 Windows on ARM64
|
||||
|
||||
The ARM64 Windows build needs llvm-nm (Debian's llvm package) for FFmpeg's
|
||||
configure, as the macOS build does, since binutils cannot read ARM64 Windows
|
||||
objects. OpenSSL has no MinGW target for ARM64 of its own;
|
||||
cmake/opensslMingwArm64.conf gives its Configure one.
|
||||
|
||||
A developer build with the host's own gcc, glibc and symbols is the
|
||||
linux-x86_64-gcc preset (binary Singe-v3.00-Linux-x86_64-gcc).
|
||||
|
||||
macOS (Apple silicon, macOS 13 or newer) builds with zig too, but needs
|
||||
an Apple SDK for the frameworks.
|
||||
Apple only ships it inside Xcode, so by default the build fetches the
|
||||
MacOSX15.5 SDK tarball from the community mirror osxcross users share
|
||||
(github.com/joseluisq/macosx-sdks), checks its SHA-256, and unpacks it to
|
||||
.builddir/toolchains/MacOSX.sdk on first use. To avoid the download,
|
||||
put a tarball holding a single MacOSX*.sdk directory at
|
||||
|
||||
.builddir/toolchains/MacOSX.sdk.tar.xz
|
||||
|
||||
(on a Mac with Xcode, osxcross's tools/gen_sdk_package.sh makes exactly
|
||||
that), or point SINGE_MACOS_SDK at an unpacked SDK. SINGE_MACOS_SDK_URL
|
||||
and SINGE_MACOS_SDK_SHA256 pick a different mirror. Then:
|
||||
|
||||
./build-all.sh macos universal
|
||||
|
||||
builds the Apple silicon and Intel presets (macos-aarch64, macos-x86_64)
|
||||
and joins them with llvm-lipo into Singe-v3.00-Macos-universal. Either
|
||||
architecture alone is ./build-all.sh macos aarch64 or x86_64.
|
||||
|
||||
The 3D scene's shaders are HLSL, compiled when Singe is built. The
|
||||
superbuild builds SDL_shadercross and SPIRV-Cross for the host machine
|
||||
once (into .builddir/toolchains/host, shared by every preset) and fetches
|
||||
Microsoft's prebuilt DirectX Shader Compiler, which SDL_shadercross
|
||||
drives, into .builddir/toolchains/dxc. That prebuilt package exists for
|
||||
x86_64 Linux hosts; on another host, unpack a DirectX Shader Compiler
|
||||
release (lib/libdxcompiler.so, lib/libdxil.so, include/dxc/dxcapi.h) at
|
||||
that path yourself, or build it from source there.
|
||||
|
||||
The ARM64 Linux build targets any ARM64 Linux system with glibc 2.31 or
|
||||
newer, the 64-bit Raspberry Pi OS among them. It is not Pi specific: the
|
||||
decoder it builds talks to any V4L2 memory-to-memory device, so one binary
|
||||
also serves Amlogic, Exynos, Qualcomm and other boards whose kernel offers
|
||||
one, and it carries Rockchip's own decoders besides. 3D games need a Pi 4
|
||||
or later, which has OpenGL ES 3.1 (and Vulkan); the Pi 3 has only OpenGL
|
||||
ES 2.0 and plays 2D games only. The build
|
||||
uses zig. The platform headers and libraries it links against are Debian
|
||||
bookworm arm64 packages listed in cmake/zig/arm64Packages.cmake, fetched
|
||||
from snapshot.debian.org and unpacked with dpkg-deb into
|
||||
.builddir/toolchains/sysroot-aarch64-linux-gnu on first use:
|
||||
|
||||
./build-all.sh linux aarch64
|
||||
|
||||
Useful forms (anything after the platform goes to cmake --build):
|
||||
|
||||
./build-all.sh linux x86_64 --target singe Singe alone
|
||||
./build-all.sh linux x86_64 --target rebuild-ffmpeg redo one library
|
||||
|
||||
Without the wrapper:
|
||||
|
||||
cmake --preset linux-x86_64 && cmake --build --preset linux-x86_64
|
||||
|
||||
Presets: linux-x86_64, linux-x86_64-gcc, windows-x86_64, windows-aarch64,
|
||||
linux-aarch64, macos-aarch64, macos-x86_64 (CMakePresets.json).
|
||||
89
LICENSES
89
LICENSES
|
|
@ -1,89 +0,0 @@
|
|||
Singe was made possible by a number of third-party software libraries
|
||||
generously provided as open source by their respective authors. Without
|
||||
these tools, Singe would not exist.
|
||||
|
||||
The version column is the release vendored under thirdparty/ (or, for the
|
||||
libraries bundled inside the SDL trees and the host tools, the copy those
|
||||
trees carry). Projects without releases are listed by snapshot date.
|
||||
|
||||
|
||||
arg_parser 1.21 BSD-2-Clause http://savannah.nongnu.org/projects/arg-parser
|
||||
basis_universal 2.50 Apache-2.0 https://github.com/BinomialLLC/basis_universal
|
||||
binaryheap.lua 0.4 MIT http://tieske.github.io/binaryheap.lua
|
||||
brotli 1.2.0 MIT https://github.com/google/brotli
|
||||
bump.lua 3.1.7 MIT https://github.com/kikito/bump.lua
|
||||
cgltf 1.15 MIT https://github.com/jkuhlmann/cgltf
|
||||
copas 4.12.0 MIT https://lunarmodules.github.io/copas
|
||||
dav1d 1.5.4 BSD-2-Clause https://code.videolan.org/videolan/dav1d
|
||||
DirectXShaderCompiler 1.10.2605.37 NCSA https://github.com/microsoft/DirectXShaderCompiler (prebuilt, downloaded at build time; host tool only)
|
||||
ffmpeg 9.0.1 LGPL-2.1 https://ffmpeg.org
|
||||
freetype 2.13.2 FTL https://freetype.org (bundled with SDL3_ttf)
|
||||
game-music-emu 0.6.6 LGPL-2.1 https://github.com/libgme/game-music-emu (bundled with SDL3_mixer)
|
||||
harfbuzz 14.4.0 MIT https://harfbuzz.github.io (bundled with SDL3_ttf)
|
||||
highway 1.4.0 Apache-2.0 https://github.com/google/highway (bundled with libjxl)
|
||||
inspect.lua 3.1.0 MIT https://github.com/kikito/inspect.lua
|
||||
JoltPhysics 5.6.0 MIT https://github.com/jrouwe/JoltPhysics
|
||||
json.lua 0.1.2 MIT https://github.com/rxi/json.lua
|
||||
libjpeg 9f IJG https://ijg.org (bundled with SDL3_image)
|
||||
libjxl 0.7.3-SDL BSD-3-Clause https://github.com/libsdl-org/libjxl (bundled with SDL3_image)
|
||||
libogg 1.3.5 BSD-3-Clause https://xiph.org/ogg (bundled with SDL3_mixer)
|
||||
libpng 1.6.58 libpng-2.0 http://www.libpng.org (bundled with SDL3_image)
|
||||
librs232 1.0.4 MIT https://github.com/srdgame/librs232
|
||||
libwebp 1.3.2 BSD-3-Clause https://developers.google.com/speed/webp (bundled with SDL3_image)
|
||||
libxmp 4.7.0 MIT https://github.com/libxmp/libxmp (bundled with SDL3_mixer)
|
||||
lsqlite3 0.9.7 MIT http://lua.sqlite.org
|
||||
lua 5.4.9 MIT https://www.lua.org
|
||||
luafilesystem 1.9.0 MIT https://lunarmodules.github.io/luafilesystem
|
||||
luasec 1.3.2 MIT https://github.com/lunarmodules/luasec
|
||||
luasocket 3.1.0 MIT https://lunarmodules.github.io/luasocket
|
||||
lume 2.3.0 MIT https://github.com/rxi/lume
|
||||
manymouse 0.0.3 Zlib https://icculus.org/manymouse
|
||||
middleclass 4.1.1 MIT https://github.com/kikito/middleclass
|
||||
MobDebug 0.805 MIT https://github.com/pkulchenko/MobDebug
|
||||
openssl 3.5.8 Apache-2.0 https://www.openssl.org
|
||||
opus 1.4 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer; also Singe's voice codec)
|
||||
opusfile 0.12 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer)
|
||||
plutosvg 0.0.8 MIT https://github.com/sammycage/plutosvg (bundled with SDL3_ttf)
|
||||
plutovg 1.3.3 MIT https://github.com/sammycage/plutovg (bundled with SDL3_ttf)
|
||||
recastnavigation 1.6.0 Zlib https://github.com/recastnavigation/recastnavigation
|
||||
RmlUi 6.3 MIT https://github.com/mikke89/RmlUi
|
||||
SDL3 3.4.16 Zlib https://www.libsdl.org
|
||||
SDL3_image 3.4.6 Zlib https://www.libsdl.org
|
||||
SDL3_mixer 3.2.4 Zlib https://www.libsdl.org
|
||||
SDL3_ttf 3.2.2 Zlib https://www.libsdl.org
|
||||
SDL_shadercross main 2026-09 Zlib https://github.com/libsdl-org/SDL_shadercross (host tool only)
|
||||
skcms 0.7.3-SDL BSD-3-Clause https://skia.googlesource.com/skcms (bundled with libjxl)
|
||||
speexdsp 1.2.1 BSD-3-Clause https://www.speex.org
|
||||
SPIRV-Cross main 2026-09 Apache-2.0 https://github.com/KhronosGroup/SPIRV-Cross (host tool only)
|
||||
sqlite 3.53.4 Public-Domain https://sqlite.org
|
||||
timerwheel.lua 1.0.2 MIT https://tieske.github.io/timerwheel.lua
|
||||
TinySoundFont 2026-07-19 MIT https://github.com/schellingb/TinySoundFont
|
||||
uthash 2.4.0 BSD-1-Clause https://troydhanson.github.io/uthash
|
||||
vlc 3.0 headers LGPL-2.1 https://www.videolan.org/vlc (two language-code headers only)
|
||||
wavpack 5.9.0 BSD-3-Clause https://www.wavpack.com (bundled with SDL3_mixer)
|
||||
zlib 1.3.2 Zlib https://zlib.net
|
||||
zstd 1.5.7 BSD-3-Clause https://facebook.github.io/zstd
|
||||
|
||||
|
||||
Fonts
|
||||
-----
|
||||
|
||||
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
|
||||
|
||||
|
||||
Sounds
|
||||
------
|
||||
|
||||
The menu's intro (assets/menuIntro.flac, written by util/makeMenuSound.py) is built on three
|
||||
recordings from SoundBible, each under the Creative Commons Attribution 3.0 licence
|
||||
(https://creativecommons.org/licenses/by/3.0/) and kept as FLAC under media/samples:
|
||||
|
||||
Explosion Ultra Bass Mark DiAngelo CC BY 3.0 https://soundbible.com/1807-Explosion-Ultra-Bass.html
|
||||
Large Fireball Mike Koenig CC BY 3.0 https://soundbible.com/1348-Large-Fireball.html
|
||||
Dragon Fire Breath and Roar Daniel Simon CC BY 3.0 https://soundbible.com/2127-Dragon-Fire-Breath-and-Roar.html
|
||||
|
||||
|
||||
Data
|
||||
----
|
||||
|
||||
Mozilla CA list 20260601 (Debian ca-certificates) MPL-2.0 https://wiki.mozilla.org/CA (assets/cacerts.pem)
|
||||
|
|
@ -1,713 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- The menu's backdrop: the intro and the grid the menu sits on, as one scene.
|
||||
--
|
||||
-- A charge goes off, the Singe dragon and name fly out of it toward the camera, the tagline fades
|
||||
-- in underneath, the dragon rears and roars, then rears again and breathes flame -- and then the
|
||||
-- grid comes up out of the dark
|
||||
-- beneath the logo while the smoke is still clearing, the logo lifts away, and what is left is the
|
||||
-- backdrop the menu sits on. One scene from the first frame to the last: no cut, and nothing
|
||||
-- loads at the join.
|
||||
--
|
||||
-- It lives here rather than inside Singe/MenuDocument.singe because two things draw it: the menu,
|
||||
-- and util/renderMenuVideo.py, which records it so a machine with no GPU can play what a machine
|
||||
-- with one draws. A second copy would be a second thing to keep in step, and the recording would
|
||||
-- quietly stop matching the menu.
|
||||
--
|
||||
-- Needs the GPU device the 3D scene uses; Singe/MenuOverlay.singe is the path for a machine
|
||||
-- without one.
|
||||
|
||||
-- The backdrop: the intro and the grid the menu sits on, as one scene. Every number here came
|
||||
-- from testScripts/menuIntro.singe, which is where the pacing was worked out; changing one is
|
||||
-- best done there first, where a contact sheet of the whole eight seconds is one command away.
|
||||
local BLAST_Z = -30.0 -- Where the charge goes off.
|
||||
local BLAST_Y = 2.5 -- and how high, which is where the logo comes from.
|
||||
local REST_Z = -8.0 -- Where the logo ends up.
|
||||
local CAM_Z = 2.0
|
||||
local FLY_START = 0.30 -- Seconds: the logo leaves the blast.
|
||||
local FLY_END = 1.90 -- and has settled.
|
||||
local TAG_START = 2.10 -- The tagline begins to appear.
|
||||
local TAG_END = 3.00
|
||||
local SPINS = 2.0 -- Turns the logo makes on the way out.
|
||||
local IDLE_TURN = 7.0 -- Degrees it drifts either side once it has settled.
|
||||
local IDLE_RATE = 0.9 -- Radians a second of that drift.
|
||||
local GRID_UP = 4.30 -- Seconds: the grid starts to come up out of the dark.
|
||||
local GRID_LIT = 5.60 -- and is fully lit.
|
||||
local LIFT_FROM = 5.90 -- The logo starts to leave.
|
||||
local LIFT_TO = 7.30 -- and is gone; the menu has the screen from here.
|
||||
local LOOP_AT = 9.90 -- Where the recording of all this begins its looping section, and
|
||||
-- where Singe/menuIntro.flac, which Singe/Menu.singe plays over
|
||||
-- either renderer, has finished fading out. The menu has had the
|
||||
-- screen since LIFT_TO; these last seconds are the grid running on
|
||||
-- under it while the music leaves, which is why this is not simply
|
||||
-- LIFT_TO. It is a whole number of frames -- and of keyframes -- at
|
||||
-- the step util/renderMenuVideo.py records at, which checks that
|
||||
-- rather than taking it on trust.
|
||||
local LIFT_RISE = 13.0 -- World units it climbs on the way out.
|
||||
local MEASURE_FROM = 2.0 -- Seconds: the backdrop is timed from here, the logo settled and flapping,
|
||||
local MEASURE_TO = 4.0 -- to here, before the grid comes up, and the average sets this machine's
|
||||
-- tier (PLAN.md section 57). The menu is the benchmark: it runs anyway.
|
||||
local TIER_MS = { 2.5, 6.0, 20.0 } -- A frame of it at or under these milliseconds is tier 3, 2, or 1;
|
||||
-- slower is 0. First guesses from this machine and the design's
|
||||
-- reference hardware, to be set again on the real parts.
|
||||
|
||||
-- The backdrop, which is the grid the menu sits on. Same numbers as testScripts/gridBackground.
|
||||
local CELL = 6.0
|
||||
local LINE_W = 0.12
|
||||
local CROSS_W = 0.18
|
||||
local SPREAD = 16.0
|
||||
local HALF_X = 12
|
||||
local DEPTH = 14
|
||||
-- The grid repeats itself every GRID_PERIOD, because it is slid by one cell and wrapped. The
|
||||
-- period is the constant and the speed follows from it, rather than the other way round, so that
|
||||
-- a recording of the loop can be a whole number of frames: at 6 units and 9 a second the period
|
||||
-- was 2/3 of a second, which is not a whole number of frames at any integer step, and the recorded
|
||||
-- loop could never quite meet itself.
|
||||
local GRID_PERIOD = 0.66
|
||||
local GRID_SPEED = CELL / GRID_PERIOD
|
||||
local FOG_R = 8
|
||||
local FOG_G = 3
|
||||
local FOG_B = 20
|
||||
local FOG_NEAR = 12
|
||||
local FOG_FAR = 72
|
||||
local SUN_R = 10.5
|
||||
local SUN_Y = 7.6
|
||||
local SUN_Z = -34.0
|
||||
local SUN_SLICES = 6
|
||||
local SUN_SEGMENTS = 72
|
||||
local HORIZ_W = 70.0
|
||||
local HORIZ_H = 0.5
|
||||
local HORIZ_Z = -38.0
|
||||
local HORIZ_D = 2.2
|
||||
local CAM_Y = 2.7 -- Eye height, shared by the logo and the grid: the backdrop's
|
||||
-- perspective is tuned to it, and at the intro's old 1.2 the grid came
|
||||
-- up flattened with its nearest line a slab across the bottom.
|
||||
local LOGO_Y = 0.15 -- Where the logo settles. It is the dragon that is centered on the
|
||||
-- sun, not the pair of them: this puts the dragon's middle on the
|
||||
-- sun's middle, which leaves the name below it crossing the horizon
|
||||
-- rather than sitting across the sun's face and hiding it. Measured
|
||||
-- rather than judged by eye -- a still with the dragon hidden gives
|
||||
-- the sun's disc, one with it shown gives the dragon's -- so it is
|
||||
-- worth re-measuring if either model changes.
|
||||
|
||||
-- The dragon carries no animation, but util/dragonModel.py builds it out of named parts -- head,
|
||||
-- jaw, neck, wingL, wingR and the rest -- and every one of them is its own node. They all sit at the
|
||||
-- model's origin though, so turning a wing node would swing the wing round the dragon's middle
|
||||
-- rather than its shoulder. The joints are baked into the model now; coordinates below are the
|
||||
-- model's own: it faces +X, +Y is up, and the wings reach out along +/-Z.
|
||||
local MOUTH = { 2.88, 5.46, 0.00 } -- Where the flame leaves, in model coordinates: in the gap
|
||||
-- between the snout and the jaw once the jaw has dropped, back
|
||||
-- from the nose so the fire comes out of the mouth, not the nose.
|
||||
local HEAD_AT = { 0.62, 5.69, 0.00 } -- The head's baked joint; see media/DragonPivots.json.
|
||||
local FLAP_MAX = 34.0 -- Degrees a wing rises and falls.
|
||||
local FLAP_FAST = 13.0 -- Radians a second while it is flying out of the blast.
|
||||
local FLAP_SLOW = 3.4 -- and once it has settled.
|
||||
local ROAR_BACK = 2.20 -- Seconds: the head starts to rear for the roar, which builds in
|
||||
-- media/samples/dragonFireBreathAndRoar.flac from here,
|
||||
local ROAR_TOP = 2.50 -- is fully back as the roar reaches its loudest,
|
||||
local ROAR_HOLD = 2.85 -- held there until the roar falls away,
|
||||
-- and down again by REAR_BACK, so the rear for the fire reads as a
|
||||
-- second one rather than as the roar carrying on.
|
||||
local ROAR_ANGLE = 24.0 -- Degrees the head tips back to roar.
|
||||
local JAW_OPEN = -30.0 -- Degrees the lower jaw drops, for the roar and for the fire.
|
||||
local NECK_SHARE = 0.35 -- How much of the head's pitch the neck takes as well.
|
||||
local REAR_BACK = 3.05 -- Seconds: the head starts to rear for the fire.
|
||||
local REAR_TOP = 3.40 -- fully back.
|
||||
local REAR_STRIKE = 3.58 -- and thrown forward.
|
||||
local BREATH_START = 3.46
|
||||
local BREATH_END = 4.35
|
||||
local REAR_DONE = 4.90 -- Back to where it started.
|
||||
local REAR_ANGLE = 34.0 -- Degrees the head tips back.
|
||||
local STRIKE_ANGLE = -14.0 -- and forward through the strike.
|
||||
local DRAGON_YAW = -148.0 -- The dragon faces left, turned toward the camera so the wingbeat is
|
||||
-- seen three quarters on rather than edge on; the name stays square
|
||||
-- to the camera because it has to be read.
|
||||
local TAGLINE = "Scott's Increasingly Nifty Game Engine"
|
||||
|
||||
|
||||
local backdropStarted = 0 -- singeGetTicks() when the backdrop's clock began.
|
||||
local backdropTagX = 0
|
||||
local backdropTagY = 0
|
||||
local blown = false -- Whether the charge has gone off.
|
||||
local gridLit = false -- Whether the grid has finished coming up and its colors are final.
|
||||
local gridShown = false -- Whether the grid, the horizon and the sun are drawn at all yet.
|
||||
local backdropParts = {} -- Those nodes: hidden through the intro, shown as the grid comes up.
|
||||
local backdropFont = nil
|
||||
local measuring = false -- Whether the backdrop is being timed right now.
|
||||
local measured = false -- Whether this run has timed it, or has no intro to time.
|
||||
|
||||
-- ===== The backdrop ==========================================================================
|
||||
--
|
||||
-- The intro and the grid are one scene: the blast, the logo out of it, the tagline, and then the
|
||||
-- grid rising out of the dark beneath the logo while the smoke is still clearing, the logo lifting
|
||||
-- away, and the backdrop left running under the menu. There is no cut and nothing loads at the
|
||||
-- join, which is the whole point of doing it this way rather than playing a video and switching.
|
||||
|
||||
-- The scene itself. Opaque black to begin with: the intro happens in the dark, and the disc
|
||||
-- behind it is stopped rather than shown through, so this is the whole picture from the first
|
||||
-- frame until the grid comes up and the background goes with it.
|
||||
sceneEnable(true)
|
||||
sceneSetBackground(0, 0, 0, 255)
|
||||
sceneSetAmbient(14, 12, 16)
|
||||
sceneSetBloom(1.2, 0.22)
|
||||
sceneSetTonemap(TONEMAP_ACES)
|
||||
|
||||
|
||||
-- The dragon's parts carry their own joints now: util/objToGlb.py bakes them from
|
||||
-- media/DragonPivots.json, so wingL turns at the shoulder and head at the top of the neck without
|
||||
-- anything being rigged here. This used to build them at run time from bounding boxes, which
|
||||
-- worked but left every future use of the model to re-derive the same thing.
|
||||
local function joint(root, name, parent)
|
||||
local part = nodeFind(name, root)
|
||||
|
||||
if (part ~= nil) and (parent ~= nil) then
|
||||
local px, py, pz = nodeGetPosition(part)
|
||||
local qx, qy, qz = nodeGetPosition(parent)
|
||||
|
||||
-- Both parts carry their own baked joint, measured from the model's origin. Hanging one
|
||||
-- off the other makes the child's offset count from the parent's, so the two would add up
|
||||
-- and the head would fly off above the neck -- which is exactly what it did.
|
||||
nodeSetParent(part, parent)
|
||||
nodeSetPosition(part, px - qx, py - qy, pz - qz)
|
||||
end
|
||||
|
||||
return part
|
||||
end
|
||||
|
||||
|
||||
-- Eases in: slow to start, quick away. The logo leaves this way so the move begins as a drift.
|
||||
local function easeIn(t)
|
||||
return t * t * t
|
||||
end
|
||||
|
||||
|
||||
-- Eases out: fast off the mark, slow into the stop.
|
||||
local function easeOut(t)
|
||||
return 1 - (1 - t) * (1 - t) * (1 - t)
|
||||
end
|
||||
|
||||
|
||||
-- How far through a span the time is, 0 before it and 1 after.
|
||||
local function span(t, from, to)
|
||||
return math.max(math.min((t - from) / (to - from), 1), 0)
|
||||
end
|
||||
|
||||
|
||||
local blast = nodeNew()
|
||||
nodeSetPosition(blast, 0, BLAST_Y, BLAST_Z)
|
||||
|
||||
local smoke = emitterNew(blast)
|
||||
emitterSetMax(smoke, 400)
|
||||
emitterSetBlend(smoke, PARTICLE_ALPHA)
|
||||
emitterSetRadius(smoke, 1.6)
|
||||
emitterSetSpread(smoke, 360)
|
||||
emitterSetSpeed(smoke, 2.5, 9.0)
|
||||
emitterSetDrag(smoke, 1.3)
|
||||
emitterSetGravity(smoke, 0, 3.2, 0)
|
||||
emitterSetLife(smoke, 1.2, 2.4)
|
||||
emitterSetSize(smoke, 3.2, 12.0)
|
||||
emitterSetSpin(smoke, -40, 40)
|
||||
emitterSetColor(smoke, 64, 46, 38, 170, 10, 9, 12, 0)
|
||||
|
||||
local core = emitterNew(blast)
|
||||
emitterSetMax(core, 500)
|
||||
emitterSetBlend(core, PARTICLE_ADD)
|
||||
emitterSetRadius(core, 1.1)
|
||||
emitterSetSpread(core, 360)
|
||||
emitterSetSpeed(core, 5.0, 20.0)
|
||||
emitterSetDrag(core, 1.7)
|
||||
emitterSetGravity(core, 0, 5.0, 0)
|
||||
emitterSetLife(core, 0.6, 1.3)
|
||||
emitterSetSize(core, 2.0, 8.2)
|
||||
emitterSetColor(core, 255, 232, 150, 255, 190, 35, 0, 0)
|
||||
|
||||
local flash = emitterNew(blast)
|
||||
emitterSetMax(flash, 20)
|
||||
emitterSetBlend(flash, PARTICLE_ADD)
|
||||
emitterSetRadius(flash, 0.4)
|
||||
emitterSetSpread(flash, 360)
|
||||
emitterSetSpeed(flash, 0, 1.5)
|
||||
emitterSetLife(flash, 0.10, 0.22)
|
||||
emitterSetSize(flash, 6.0, 15.0)
|
||||
emitterSetColor(flash, 255, 255, 235, 255, 255, 150, 40, 0)
|
||||
|
||||
local sparks = emitterNew(blast)
|
||||
emitterSetMax(sparks, 300)
|
||||
emitterSetBlend(sparks, PARTICLE_ADD)
|
||||
emitterSetRadius(sparks, 0.5)
|
||||
emitterSetSpread(sparks, 360)
|
||||
emitterSetSpeed(sparks, 12.0, 34.0)
|
||||
emitterSetDrag(sparks, 0.25)
|
||||
emitterSetGravity(sparks, 0, -15.0, 0)
|
||||
emitterSetLife(sparks, 0.5, 1.5)
|
||||
emitterSetSize(sparks, 0.30, 0.05)
|
||||
emitterSetSpin(sparks, -200, 200)
|
||||
emitterSetTrail(sparks, 8, 0.10)
|
||||
emitterSetColor(sparks, 255, 226, 160, 255, 255, 80, 15, 0)
|
||||
|
||||
-- The blast's own light, so the logo is lit by the fire it comes out of.
|
||||
local fireLight = lightNew(LIGHT_POINT)
|
||||
nodeSetParent(fireLight, blast)
|
||||
lightSetColor(fireLight, 255, 150, 60)
|
||||
lightSetRange(fireLight, 60)
|
||||
lightSetIntensity(fireLight, 0)
|
||||
|
||||
-- A key light for the logo once the fire has died, from over the camera's shoulder.
|
||||
local key = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(key, 4, 6, 10)
|
||||
nodeLookAt(key, 0, 0, REST_Z)
|
||||
lightSetIntensity(key, 1.15)
|
||||
|
||||
-- A magenta rim from behind and to the left, the color the grid behind the menu is drawn in, so
|
||||
-- the logo is not a flat white cut-out and the intro and the menu share a palette.
|
||||
local rim = lightNew(LIGHT_DIRECTIONAL)
|
||||
nodeSetPosition(rim, -7, 3, REST_Z - 9)
|
||||
nodeLookAt(rim, 0, 1.5, REST_Z)
|
||||
lightSetColor(rim, 255, 70, 200)
|
||||
lightSetIntensity(rim, 2.6)
|
||||
|
||||
-- The dragon over the name, assembled the way testScripts/scene30.singe does it.
|
||||
local logo = nodeNew()
|
||||
local text = modelInstance(modelLoad("Singe/SingeText.glb"))
|
||||
nodeSetParent(text, logo)
|
||||
local dragon = modelInstance(modelLoad("Singe/DragonModel.glb"))
|
||||
nodeSetParent(dragon, logo)
|
||||
nodeSetPosition(dragon, 0, 4.85, 0)
|
||||
nodeSetRotation(dragon, 0, DRAGON_YAW, 0)
|
||||
nodeSetScale(dragon, 0.9)
|
||||
nodeSetScale(logo, 0.5)
|
||||
nodeSetVisible(logo, false)
|
||||
|
||||
-- The rig. The head hangs off the neck so that rearing the neck carries the head with it, and
|
||||
-- the jaw off the head for the same reason. Tip first: joint measures the parent where it sits,
|
||||
-- so the jaw has to be hung on the head while the head is still at its own baked joint rather than
|
||||
-- already moved to be relative to the neck.
|
||||
local wingLeft = joint(dragon, "wingL")
|
||||
local wingRite = joint(dragon, "wingR")
|
||||
local neck = joint(dragon, "neck")
|
||||
local jaw = joint(dragon, "jaw", nodeFind("head", dragon))
|
||||
local head = joint(dragon, "head", neck)
|
||||
|
||||
-- The flame, on a node at the dragon's mouth so it follows the head round.
|
||||
local jet = nodeNew()
|
||||
|
||||
nodeSetParent(jet, head)
|
||||
-- Relative to the head's own joint, which is where its node now sits.
|
||||
nodeSetPosition(jet, MOUTH[1] - HEAD_AT[1], MOUTH[2] - HEAD_AT[2], MOUTH[3] - HEAD_AT[3])
|
||||
|
||||
-- The flame's own light, so the dragon is lit by what it is breathing.
|
||||
local breathLight = lightNew(LIGHT_POINT)
|
||||
|
||||
nodeSetParent(breathLight, jet)
|
||||
lightSetColor(breathLight, 255, 160, 60)
|
||||
lightSetRange(breathLight, 14)
|
||||
lightSetIntensity(breathLight, 0)
|
||||
|
||||
local breath = emitterNew(jet)
|
||||
emitterSetMax(breath, 400)
|
||||
emitterSetBlend(breath, PARTICLE_ADD)
|
||||
emitterSetRadius(breath, 0.05)
|
||||
emitterSetSpread(breath, 13)
|
||||
emitterSetSpeed(breath, 4.5, 9.5)
|
||||
emitterSetDrag(breath, 3.0)
|
||||
emitterSetLife(breath, 0.28, 0.55)
|
||||
emitterSetSize(breath, 0.10, 1.30)
|
||||
emitterSetRate(breath, 260)
|
||||
emitterSetColor(breath, 255, 244, 200, 255, 235, 60, 0, 0)
|
||||
|
||||
-- The tagline is measured in backdropBegin instead, once a font has been selected: at this point
|
||||
-- the document renderer has not chosen one, and fontToSprite ends the game without it.
|
||||
|
||||
-- ===== The backdrop ==========================================================================
|
||||
--
|
||||
-- Built now and left unlit. Bringing it up is a ramp on the emissive colors rather than anything
|
||||
-- appearing, so the grid rises out of the dark the way a light comes on instead of cutting in.
|
||||
|
||||
local function quad(p, i, x0, z0, x1, z1, x2, z2, x3, z3)
|
||||
local base = #p / 3
|
||||
|
||||
for _, c in ipairs({ { x0, z0 }, { x1, z1 }, { x2, z2 }, { x3, z3 } }) do
|
||||
p[#p + 1] = c[1]
|
||||
p[#p + 1] = 0
|
||||
p[#p + 1] = c[2]
|
||||
end
|
||||
for _, n in ipairs({ 1, 2, 3, 1, 3, 4 }) do
|
||||
i[#i + 1] = base + n
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- A line of constant width thins to nothing as it recedes and breaks into crawling dots; widening
|
||||
-- it with distance holds it at about the same width on screen the whole way to the horizon.
|
||||
local function spread(z)
|
||||
return 1 + (-z) / SPREAD
|
||||
end
|
||||
|
||||
|
||||
-- The height at z that lies on the horizon line, so the bar and the sun's cut land on the same row.
|
||||
local function horizonY(z)
|
||||
return CAM_Y - (CAM_Z - z) * math.tan(math.rad(HORIZ_D))
|
||||
end
|
||||
|
||||
|
||||
local function gridMesh()
|
||||
local p = {}
|
||||
local i = {}
|
||||
local far = -DEPTH * CELL
|
||||
local wide = HALF_X * CELL
|
||||
local hN = LINE_W / 2
|
||||
local hF = hN * spread(far)
|
||||
|
||||
for n = -HALF_X, HALF_X do
|
||||
local x = n * CELL
|
||||
|
||||
quad(p, i, x - hN, CELL, x + hN, CELL, x + hF, far, x - hF, far)
|
||||
end
|
||||
for n = -1, DEPTH do
|
||||
local z = -n * CELL
|
||||
local h = CROSS_W / 2 * spread(z)
|
||||
|
||||
quad(p, i, -wide, z + h, wide, z + h, wide, z - h, -wide, z - h)
|
||||
end
|
||||
|
||||
return meshNew(p, nil, nil, i)
|
||||
end
|
||||
|
||||
|
||||
-- The sun: the part of a disc above yClip, as a fan from the middle of the chord.
|
||||
local function sunMesh(yClip)
|
||||
local p = { 0, yClip, 0 }
|
||||
local i = {}
|
||||
local a0 = math.asin(math.max(math.min(yClip / SUN_R, 1), -1))
|
||||
|
||||
for n = 0, SUN_SEGMENTS do
|
||||
local a = a0 + (n / SUN_SEGMENTS) * (math.pi - a0 * 2)
|
||||
|
||||
p[#p + 1] = math.cos(a) * SUN_R
|
||||
p[#p + 1] = math.sin(a) * SUN_R
|
||||
p[#p + 1] = 0
|
||||
end
|
||||
for n = 1, SUN_SEGMENTS do
|
||||
i[#i + 1] = 1
|
||||
i[#i + 1] = n + 1
|
||||
i[#i + 1] = n + 2
|
||||
end
|
||||
|
||||
return meshNew(p, nil, nil, i)
|
||||
end
|
||||
|
||||
|
||||
-- Unlit, and brought up by their base color rather than by emissive. The intro has a key light
|
||||
-- and a rim for the logo, and a lit material with a black base still catches their specular: the
|
||||
-- grid and the sun showed as a gray sheen from the first frame, lit before they were meant to
|
||||
-- exist. Unlit takes the lights out of it entirely and the ramp below is the only thing that
|
||||
-- decides when the backdrop appears.
|
||||
local gridMat = materialNew()
|
||||
materialSetColor(gridMat, 0, 0, 0)
|
||||
materialSetUnlit(gridMat, true)
|
||||
materialSetDoubleSided(gridMat, true)
|
||||
|
||||
local sunMat2 = materialNew()
|
||||
materialSetColor(sunMat2, 0, 0, 0)
|
||||
materialSetUnlit(sunMat2, true)
|
||||
materialSetDoubleSided(sunMat2, true)
|
||||
|
||||
local horizMat = materialNew()
|
||||
materialSetColor(horizMat, 0, 0, 0)
|
||||
materialSetUnlit(horizMat, true)
|
||||
materialSetDoubleSided(horizMat, true)
|
||||
|
||||
local sliceMat = materialNew()
|
||||
materialSetColor(sliceMat, FOG_R, FOG_G, FOG_B)
|
||||
materialSetUnlit(sliceMat, true)
|
||||
materialSetDoubleSided(sliceMat, true)
|
||||
|
||||
local backdrop = nodeNew()
|
||||
local grid = nodeNew()
|
||||
|
||||
nodeSetMesh(grid, gridMesh(), gridMat)
|
||||
nodeSetParent(grid, backdrop)
|
||||
backdropParts[#backdropParts + 1] = grid
|
||||
|
||||
local horizon = nodeNew()
|
||||
nodeSetMesh(horizon, meshPlane(HORIZ_W * 2, HORIZ_H), horizMat)
|
||||
nodeSetRotation(horizon, 90, 0, 0)
|
||||
nodeSetPosition(horizon, 0, horizonY(HORIZ_Z), HORIZ_Z)
|
||||
nodeSetParent(horizon, backdrop)
|
||||
backdropParts[#backdropParts + 1] = horizon
|
||||
|
||||
local sunCut = horizonY(SUN_Z) - SUN_Y
|
||||
local backSun = nodeNew()
|
||||
|
||||
nodeSetMesh(backSun, sunMesh(sunCut), sunMat2)
|
||||
nodeSetPosition(backSun, 0, SUN_Y, SUN_Z)
|
||||
nodeSetParent(backSun, backdrop)
|
||||
backdropParts[#backdropParts + 1] = backSun
|
||||
|
||||
for n = 1, SUN_SLICES do
|
||||
local slice = nodeNew()
|
||||
local height = 0.62 - (n - 1) * 0.07
|
||||
local y = sunCut + 0.5 + (n - 1) * 1.3
|
||||
local w = math.sqrt(math.max(SUN_R * SUN_R - (math.abs(y) + height / 2) ^ 2, 0))
|
||||
|
||||
nodeSetMesh(slice, meshPlane(w * 2, height), sliceMat)
|
||||
nodeSetRotation(slice, 90, 0, 0)
|
||||
nodeSetPosition(slice, 0, SUN_Y + y, SUN_Z + 0.4)
|
||||
backdropParts[#backdropParts + 1] = slice
|
||||
nodeSetParent(slice, backdrop)
|
||||
end
|
||||
|
||||
|
||||
local camera = nodeNew()
|
||||
nodeSetPosition(camera, 0, CAM_Y, CAM_Z)
|
||||
cameraSet(camera)
|
||||
|
||||
|
||||
|
||||
|
||||
-- The backdrop is the disc for the intro and the 3D scene after it. The grid section of
|
||||
-- Singe/menuBackground.mkv is never reached: this renderer has the GPU device the document needs,
|
||||
-- which is the same one the scene draws with, so the grid is drawn rather than played.
|
||||
-- One frame of the backdrop's timeline.
|
||||
function backdropFrame()
|
||||
local t = (singeGetTicks() - backdropStarted) / 1000.0
|
||||
local fly = span(t, FLY_START, FLY_END)
|
||||
local tag = span(t, TAG_START, TAG_END)
|
||||
|
||||
if not blown then
|
||||
blown = true
|
||||
emitterBurst(smoke, 85)
|
||||
emitterBurst(core, 110)
|
||||
emitterBurst(flash, 5)
|
||||
emitterBurst(sparks, 130)
|
||||
end
|
||||
|
||||
-- The machine's tier, from how long this very scene takes to draw.
|
||||
if not measured then
|
||||
if (not measuring) and (t >= MEASURE_FROM) then
|
||||
sceneMeasure(true)
|
||||
measuring = true
|
||||
elseif measuring and (t >= MEASURE_TO) then
|
||||
local ms = sceneMeasure(false)
|
||||
local tier = 0
|
||||
|
||||
measuring = false
|
||||
measured = true
|
||||
if ms then
|
||||
for index, limit in ipairs(TIER_MS) do
|
||||
if (tier == 0) and (ms <= limit) then
|
||||
tier = #TIER_MS + 1 - index
|
||||
end
|
||||
end
|
||||
if singeCapabilities().tier ~= tier then
|
||||
singeSaveTier(tier)
|
||||
end
|
||||
debugPrint(string.format("Backdrop: %.2f ms a frame, tier %d", ms, tier))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The fire's light dies over the first second and a half.
|
||||
lightSetIntensity(fireLight, 400 * (1 - span(t, 0.05, 1.5)))
|
||||
|
||||
-- The grid comes up out of the dark while the smoke is still clearing, so the two overlap and
|
||||
-- there is no moment where one thing has ended and the next has not begun.
|
||||
local lit = span(t, GRID_UP, GRID_LIT)
|
||||
|
||||
-- Until then the grid, the horizon and the sun are not drawn at all. Black and unlit, they
|
||||
-- were still there: a black line across the blast's glow is a line, and the whole grid showed
|
||||
-- through the explosion as its outline.
|
||||
if (lit > 0) and not gridShown then
|
||||
gridShown = true
|
||||
for _, part in ipairs(backdropParts) do
|
||||
nodeSetVisible(part, true)
|
||||
end
|
||||
end
|
||||
if (lit > 0) and not gridLit then
|
||||
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
|
||||
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
|
||||
materialSetColor(sunMat2, 255 * lit, 130 * lit, 55 * lit)
|
||||
-- The sky stops being black at the same time, or the grid would rise into a void.
|
||||
sceneSetBackground(FOG_R * lit, FOG_G * lit, FOG_B * lit, 255)
|
||||
sceneSetFog(FOG_R, FOG_G, FOG_B, FOG_NEAR, FOG_FAR)
|
||||
-- Once fully lit the colors are final, so they are not written again every frame.
|
||||
gridLit = (lit >= 1)
|
||||
end
|
||||
if lit > 0 then
|
||||
-- Scrolling from the moment it is visible, so it is already alive when it arrives.
|
||||
nodeSetPosition(grid, 0, 0, ((t - GRID_UP) * GRID_SPEED) % CELL)
|
||||
end
|
||||
|
||||
if fly > 0 then
|
||||
local e = easeOut(fly)
|
||||
-- Once it has arrived it keeps turning slowly, so the held frame is not a still picture.
|
||||
local idle = IDLE_TURN * math.sin((t - FLY_END) * IDLE_RATE) * fly
|
||||
local yaw = 360 * SPINS * (1 - e) + idle
|
||||
|
||||
-- Out of the blast, then up and away once the grid has taken over. Rising rather than
|
||||
-- fading: a model has no alpha to fade without touching every material it came with.
|
||||
local leave = easeIn(span(t, LIFT_FROM, LIFT_TO))
|
||||
|
||||
nodeSetVisible(logo, leave < 1)
|
||||
nodeSetPosition(logo, 0, LOGO_Y * e + LIFT_RISE * leave, BLAST_Z + (REST_Z - BLAST_Z) * e)
|
||||
nodeSetRotation(logo, 0, yaw, 0)
|
||||
|
||||
-- Wings: beating hard on the way out, easing to a hover once it has arrived.
|
||||
local rate = FLAP_FAST + (FLAP_SLOW - FLAP_FAST) * e
|
||||
local flap = FLAP_MAX * math.sin(t * rate)
|
||||
|
||||
nodeSetRotation(wingLeft, -flap, 0, 0)
|
||||
nodeSetRotation(wingRite, flap, 0, 0)
|
||||
|
||||
-- The head rears to roar and comes down; then it rears again, snaps forward, and settles,
|
||||
-- and the flame goes with the strike.
|
||||
local pitch = 0
|
||||
|
||||
if t < ROAR_TOP then
|
||||
pitch = ROAR_ANGLE * easeOut(span(t, ROAR_BACK, ROAR_TOP))
|
||||
elseif t < REAR_BACK then
|
||||
pitch = ROAR_ANGLE * (1 - span(t, ROAR_HOLD, REAR_BACK))
|
||||
elseif t < REAR_TOP then
|
||||
pitch = REAR_ANGLE * span(t, REAR_BACK, REAR_TOP)
|
||||
elseif t < REAR_STRIKE then
|
||||
local k = span(t, REAR_TOP, REAR_STRIKE)
|
||||
|
||||
pitch = REAR_ANGLE + (STRIKE_ANGLE - REAR_ANGLE) * k
|
||||
else
|
||||
pitch = STRIKE_ANGLE * (1 - easeOut(span(t, REAR_STRIKE, REAR_DONE)))
|
||||
end
|
||||
nodeSetRotation(head, 0, 0, pitch)
|
||||
nodeSetRotation(neck, 0, 0, pitch * NECK_SHARE)
|
||||
|
||||
-- The mouth opens as the head goes back for the roar and shuts as it comes down, then
|
||||
-- opens again for the fire and stays open until the flame is spent.
|
||||
local roarJaw = span(t, ROAR_BACK, ROAR_TOP) * (1 - span(t, ROAR_HOLD, REAR_BACK))
|
||||
local fireJaw = span(t, REAR_BACK, BREATH_START) * (1 - span(t, BREATH_END, REAR_DONE))
|
||||
local jawPitch = JAW_OPEN * math.max(roarJaw, fireJaw)
|
||||
|
||||
nodeSetRotation(jaw, 0, 0, jawPitch)
|
||||
|
||||
-- The flame leaves the mouth along the dragon's own forward, which the whole logo has been
|
||||
-- turning; a 3D emitter takes its direction in world axes, so it is turned here by hand.
|
||||
-- The head rides on the neck, so its pitch in the world is its own plus the neck's, and the
|
||||
-- flame follows the middle of the open mouth, half the jaw's drop below the head's line.
|
||||
local aim = math.rad(pitch * (1 + NECK_SHARE) + jawPitch / 2)
|
||||
local spin = math.rad(yaw + DRAGON_YAW)
|
||||
|
||||
emitterSetDirection(breath, math.cos(aim) * math.cos(spin), math.sin(aim), -math.cos(aim) * math.sin(spin))
|
||||
if t >= BREATH_START and t < BREATH_END then
|
||||
emitterStart(breath)
|
||||
lightSetIntensity(breathLight, 26)
|
||||
else
|
||||
emitterStop(breath)
|
||||
lightSetIntensity(breathLight, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- The tagline goes with the logo. It is drawn into the overlay, which the menu clears every
|
||||
-- frame, so it has to be redrawn here rather than left standing.
|
||||
local gone = span(t, LIFT_FROM, LIFT_TO - 0.4)
|
||||
|
||||
if (tag > 0) and (gone < 1) then
|
||||
colorForeground(235, 225, 240, math.floor(255 * tag * (1 - gone)))
|
||||
fontPrint(backdropTagX, backdropTagY, TAGLINE)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- Starts the backdrop's clock. showIntro false starts it past the end of the intro, so returning
|
||||
-- from a game lands on the settled grid and the same timeline runs on from there.
|
||||
function backdropBegin(showIntro)
|
||||
-- A font of its own. The document renderer draws everything else through RmlUi and never
|
||||
-- selects one, so the tagline -- the only thing put straight into the overlay -- brings it.
|
||||
-- Thirty points on the menu's 480 rows, and in step with the overlay elsewhere: a recording
|
||||
-- made at a larger size (util/renderMenuAd.py) lays the tagline out at that size rather than
|
||||
-- scaling the menu's up.
|
||||
backdropFont = fontLoad("Singe/FreeSansBold.ttf", math.floor(30 * overlayGetHeight() / 480 + 0.5))
|
||||
fontSelect(backdropFont)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
-- fontPrint has no centering, so the tagline is measured once through a throwaway sprite; it
|
||||
-- is drawn rather than kept as a sprite because a sprite cannot be tinted and the fade needs
|
||||
-- the foreground color's alpha.
|
||||
local measure = fontToSprite(TAGLINE)
|
||||
|
||||
backdropTagX = math.floor((overlayGetWidth() - spriteGetWidth(measure)) / 2)
|
||||
backdropTagY = math.floor(overlayGetHeight() * 0.80)
|
||||
spriteUnload(measure)
|
||||
|
||||
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
|
||||
blown = not showIntro
|
||||
measured = not showIntro
|
||||
measuring = false
|
||||
gridLit = false
|
||||
gridShown = false
|
||||
for _, part in ipairs(backdropParts) do
|
||||
nodeSetVisible(part, false)
|
||||
end
|
||||
if not showIntro then
|
||||
nodeSetVisible(logo, false)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Seconds since the backdrop began.
|
||||
function backdropTime()
|
||||
return (singeGetTicks() - backdropStarted) / 1000.0
|
||||
end
|
||||
|
||||
|
||||
-- Whether the intro is over and the screen belongs to whatever sits on the backdrop.
|
||||
function backdropReady()
|
||||
return backdropTime() >= LIFT_TO
|
||||
end
|
||||
|
||||
|
||||
-- How long the whole thing lasts, and how long one turn of the grid takes, so a recording knows
|
||||
-- where the intro ends and how much of the loop to keep.
|
||||
function backdropIntroSeconds()
|
||||
return LIFT_TO
|
||||
end
|
||||
|
||||
|
||||
function backdropLoopSeconds()
|
||||
return GRID_PERIOD
|
||||
end
|
||||
|
||||
|
||||
-- Where the intro ends and the loop begins, which is where the sting hands over to the bed.
|
||||
function backdropLoopAt()
|
||||
return LOOP_AT
|
||||
end
|
||||
|
||||
|
||||
function backdropFinish()
|
||||
if backdropFont then
|
||||
fontUnload(backdropFont)
|
||||
backdropFont = nil
|
||||
end
|
||||
end
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
-- Singe stopped here because an error is about to end your game. This only happens under --debug,
|
||||
-- with a debugger such as ZeroBrane Studio attached.
|
||||
--
|
||||
-- What went wrong is in "message": hover over it, or look in the Stack window. Where it went wrong
|
||||
-- is in the Stack window too, a frame or two below this one -- pick your own function there to see
|
||||
-- its local variables as they were. Continue, and the game ends just as it would have.
|
||||
|
||||
|
||||
local message = ...
|
||||
|
||||
require("mobdebug").pause()
|
||||
return message
|
||||
BIN
assets/DragonModel.glb
(Stored with Git LFS)
BIN
assets/DragonModel.glb
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,768 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
-- 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
|
||||
-- Hypseus Singe's name for the quit key setting, with the opposite sense: catching the
|
||||
-- key means the engine stops quitting on it.
|
||||
if singeSetQuitKeyEnabled ~= nil then
|
||||
keyboardCatchQuit = function(catch) singeSetQuitKeyEnabled(not catch) end
|
||||
end
|
||||
|
||||
|
||||
-- Hypseus Singe Compatibility ------------------------------------------------
|
||||
|
||||
|
||||
-- Names Hypseus Singe registers in its own engine and Singe answers from here.
|
||||
-- Every one is guarded, so an engine that grows a real implementation wins.
|
||||
|
||||
|
||||
-- Hypseus extension. Chooses an overlay resolution and hands it to
|
||||
-- overlaySetResolution, which is all Hypseus's own call does: 1 is the disc's
|
||||
-- video size, 3 is 360x240, 4 is the size given (ignored without one, as in
|
||||
-- Hypseus) and anything else, 2 included, is half the disc on both axes. Singe's
|
||||
-- discGetWidth and discGetHeight report the canvas size when there is no disc,
|
||||
-- which is the space a disc-less game draws into, so no special case is needed.
|
||||
local OVERLAY_PRESET_FULL = 1
|
||||
local OVERLAY_PRESET_NONSQUARE = 3
|
||||
local OVERLAY_PRESET_CUSTOM = 4
|
||||
local OVERLAY_NONSQUARE_WIDTH = 360
|
||||
local OVERLAY_NONSQUARE_HEIGHT = 240
|
||||
local OVERLAY_HALF = 2
|
||||
if setOverlaySize == nil and overlaySetResolution ~= nil then
|
||||
function setOverlaySize(preset, width, height)
|
||||
if preset == OVERLAY_PRESET_FULL then
|
||||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||
elseif preset == OVERLAY_PRESET_NONSQUARE then
|
||||
overlaySetResolution(OVERLAY_NONSQUARE_WIDTH, OVERLAY_NONSQUARE_HEIGHT)
|
||||
elseif preset == OVERLAY_PRESET_CUSTOM then
|
||||
if width ~= nil and height ~= nil then
|
||||
overlaySetResolution(width, height)
|
||||
end
|
||||
else
|
||||
overlaySetResolution(discGetWidth() // OVERLAY_HALF, discGetHeight() // OVERLAY_HALF)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Hypseus extension. Hypseus registers one C function under two names and Singe
|
||||
-- already has the other one, so this is a pure alias of overlaySetResolution.
|
||||
if setOverlayResolution == nil and overlaySetResolution ~= nil then
|
||||
setOverlayResolution = overlaySetResolution
|
||||
end
|
||||
|
||||
-- Hypseus extension. True in Hypseus while it is rewriting a game's asset paths
|
||||
-- for an -espath install. Singe's virtual file system reaches a game's files
|
||||
-- wherever they were installed, so there is nothing to rewrite and false is the
|
||||
-- right answer: the games that ask keep the MYDIR they already have.
|
||||
if rewriteStatus == nil then
|
||||
function rewriteStatus() return false end
|
||||
end
|
||||
|
||||
-- Singe 3.00 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 and not SINGE_LEGACY_SPRITE_WRAPPED then
|
||||
-- Once only: a game may load this file more than once, and a second wrap would undo the first.
|
||||
SINGE_LEGACY_SPRITE_WRAPPED = true
|
||||
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
|
||||
-- Hypseus extensions ship in the old order too: (x, y, frame, spr),
|
||||
-- (x, y, scale, frame, spr) or (x, y, scaleX, scaleY, frame, spr).
|
||||
local newSpriteDrawFrame = spriteDrawFrame
|
||||
spriteDrawFrame = function(...)
|
||||
local args = { ... }
|
||||
local id = table.remove(args)
|
||||
local frame = table.remove(args)
|
||||
return newSpriteDrawFrame(id, args[1], args[2], frame, table.unpack(args, 3, #args))
|
||||
end
|
||||
local newSpriteResetColorKey = spriteResetColorKey
|
||||
spriteResetColorKey = function(enabled, id) return newSpriteResetColorKey(id, enabled) end
|
||||
local newSpriteRotateFrame = spriteRotateFrame
|
||||
spriteRotateFrame = function(degrees, frame, id) return newSpriteRotateFrame(id, degrees, frame) end
|
||||
local newSpriteDrawRotatedFrame = spriteDrawRotatedFrame
|
||||
spriteDrawRotatedFrame = function(...)
|
||||
local args = { ... }
|
||||
local id = table.remove(args)
|
||||
return newSpriteDrawRotatedFrame(id, table.unpack(args))
|
||||
end
|
||||
-- spriteDrawGrid is the one Hypseus writes with the handle in the middle,
|
||||
-- (x, y, spr, rectX, rectY, width, height), not on the end.
|
||||
local newSpriteDrawGrid = spriteDrawGrid
|
||||
spriteDrawGrid = function(x, y, id, ...)
|
||||
return newSpriteDrawGrid(id, x, y, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- Hypseus names for calls Singe already has, and nothing more: each is a plain
|
||||
-- alias, so it takes exactly the arguments the Singe call takes. They are set
|
||||
-- after the legacy wrapping above, which means the six spriteAnim ones follow
|
||||
-- SINGE_LEGACY_SPRITE_ARGS with the rest of the sprite family -- old order when
|
||||
-- a game asked for it, handle first when it did not -- and a game never has two
|
||||
-- argument orders in play at once.
|
||||
if hypseusGetHeight == nil and singeGetHeight ~= nil then
|
||||
hypseusGetHeight = singeGetHeight
|
||||
end
|
||||
if hypseusGetWidth == nil and singeGetWidth ~= nil then
|
||||
hypseusGetWidth = singeGetWidth
|
||||
end
|
||||
if takeScreenshot == nil and singeScreenshot ~= nil then
|
||||
takeScreenshot = singeScreenshot
|
||||
end
|
||||
-- Hypseus counts real mice apart from the ones it derives from a gamepad; Singe
|
||||
-- has no derived mice, so mouseHowMany is already the real count.
|
||||
if mouseHowManyReal == nil and mouseHowMany ~= nil then
|
||||
mouseHowManyReal = mouseHowMany
|
||||
end
|
||||
if spriteAnimGetFrame == nil and spriteGetFrame ~= nil then
|
||||
spriteAnimGetFrame = spriteGetFrame
|
||||
end
|
||||
if spriteAnimIsPlaying == nil and spriteIsPlaying ~= nil then
|
||||
spriteAnimIsPlaying = spriteIsPlaying
|
||||
end
|
||||
if spriteAnimLoop == nil and spriteLoop ~= nil then
|
||||
spriteAnimLoop = spriteLoop
|
||||
end
|
||||
if spriteAnimPause == nil and spritePause ~= nil then
|
||||
spriteAnimPause = spritePause
|
||||
end
|
||||
if spriteAnimPlay == nil and spritePlay ~= nil then
|
||||
spriteAnimPlay = spritePlay
|
||||
end
|
||||
if spriteAnimSetFrame == nil and spriteSetFrame ~= nil then
|
||||
spriteAnimSetFrame = spriteSetFrame
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
-- ===== Online high scores =====
|
||||
--
|
||||
-- A thin layer over Singe/Master.singe so a game says scoreSubmit(1000) and nothing else. The
|
||||
-- account, the server address and the player's name belong to the machine, not to the game, and
|
||||
-- were set up once in the menu's Online Account tool.
|
||||
--
|
||||
-- Nothing here waits on the network. A score is written to a queue on disk and sent when there is
|
||||
-- a connection, because a cabinet is often offline and a game must never stall on the wire to show
|
||||
-- its own board. scoreUpdate() has to run once a frame for any of it to progress.
|
||||
|
||||
local masterLoaded = false
|
||||
local scoreLowest = {} -- The boards this game ranks the lowest score first, by full name
|
||||
|
||||
|
||||
local function scoreLoad()
|
||||
if not masterLoaded then
|
||||
-- Loaded on first use rather than at startup: a game that never posts a score should not
|
||||
-- pay for the module or open the file.
|
||||
dofile("Singe/Master.singe")
|
||||
masterLoaded = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function scoreReady()
|
||||
scoreLoad()
|
||||
return masterSignedIn()
|
||||
end
|
||||
|
||||
|
||||
-- The id a game is known by, from its games.dat entry. A game without one can be played but not
|
||||
-- ranked: there is nothing stable to key a board on, and inventing one here would make a different
|
||||
-- board every time the file moved.
|
||||
function scoreGameId()
|
||||
return singeGetGameId()
|
||||
end
|
||||
|
||||
|
||||
-- The longest board name the service takes (BOARD_MAX in its lib/scores.lua).
|
||||
local SCORE_BOARD_MAX = 32
|
||||
|
||||
|
||||
-- A board name in the form the service takes: lower case, and letters, digits, '-', '_' and '.',
|
||||
-- anything else made '-'.
|
||||
local function scoreBoardClean(text)
|
||||
local clean = tostring(text):lower():gsub("[^%w%-_%.]", "-")
|
||||
|
||||
return clean
|
||||
end
|
||||
|
||||
|
||||
-- The board a score goes on. Every title in a download shares the download's GAME_ID, so boards are
|
||||
-- kept apart by the script that is running: its file name without the extension is the board when
|
||||
-- the game names none, and comes before the name the game gives when it does, so "hard" from
|
||||
-- Raid.singe is raid.hard and the hard modes of two games in one collection stay apart. A game
|
||||
-- offered twice (two languages, say: one script, two games.dat entries) keeps one set of boards.
|
||||
-- Past SCORE_BOARD_MAX the script's part is shortened first, so the mode's name survives.
|
||||
local function scoreBoardFor(board)
|
||||
local script = singeGetScriptPath() or ""
|
||||
local name = scoreBoardClean(((script:match("([^/\\]+)$") or script):gsub("%.[^.]*$", "")))
|
||||
local mode = nil
|
||||
|
||||
if name == "" then
|
||||
name = "default"
|
||||
end
|
||||
if (board == nil) or (board == "") then
|
||||
return name:sub(1, SCORE_BOARD_MAX)
|
||||
end
|
||||
mode = scoreBoardClean(board):sub(1, SCORE_BOARD_MAX)
|
||||
if #mode >= SCORE_BOARD_MAX - 1 then
|
||||
return mode
|
||||
end
|
||||
return name:sub(1, SCORE_BOARD_MAX - #mode - 1) .. "." .. mode
|
||||
end
|
||||
|
||||
|
||||
-- Say that a board ranks the lowest score first: fewest strokes, deaths, or seconds. Called once,
|
||||
-- before the board's first score, because a board's first score fixes its direction for good and the
|
||||
-- service refuses a score sent the other way afterwards. board as scoreSubmit takes it.
|
||||
function scoreLowestWins(board)
|
||||
scoreLowest[scoreBoardFor(board)] = true
|
||||
end
|
||||
|
||||
|
||||
-- Say that a play is starting. Optional, and worth calling: the service then times the play by its
|
||||
-- own clock, which is the one thing in a submission that has not been through this machine. A
|
||||
-- cabinet with no connection simply records no time.
|
||||
function scoreBegin()
|
||||
local id = scoreGameId()
|
||||
|
||||
if not id or not scoreReady() then
|
||||
return false
|
||||
end
|
||||
masterPlayStart(id, function() end)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- Queue a score. Returns false when the cabinet has no account or the game has no id, so a game
|
||||
-- can decide whether to say anything about it.
|
||||
function scoreSubmit(value, board, meta)
|
||||
local id = scoreGameId()
|
||||
local name = scoreBoardFor(board)
|
||||
|
||||
if not id or not scoreReady() then
|
||||
return false
|
||||
end
|
||||
masterSubmitScore(id, value, name, meta, scoreLowest[name])
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- The board, as a callback: onDone(ok, result) where result.top is the list, result.standing is
|
||||
-- this player's place, and result.order is "lowest" or "highest", the way the board ranks. A cabinet
|
||||
-- that is not signed in reads it too, for a game the service offers everyone, so a game can show what
|
||||
-- signing in would post to; it has no standing.
|
||||
function scoreBoard(onDone, board)
|
||||
local id = scoreGameId()
|
||||
local name = scoreBoardFor(board)
|
||||
|
||||
if not id then
|
||||
onDone(false, "this game has no GAME_ID")
|
||||
return false
|
||||
end
|
||||
scoreLoad()
|
||||
masterBoard(id, name, onDone, scoreLowest[name])
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function scorePlayerName()
|
||||
return scoreReady() and MASTER.name or nil
|
||||
end
|
||||
|
||||
|
||||
-- Whether this cabinet is signed in to an online account, so a game showing the boards can ask a
|
||||
-- player to sign in when it is not.
|
||||
function scoreSignedIn()
|
||||
return scoreReady()
|
||||
end
|
||||
|
||||
|
||||
function scoreWaiting()
|
||||
return masterLoaded and masterQueueLength() or 0
|
||||
end
|
||||
|
||||
|
||||
-- Once a frame, from onOverlayUpdate. Safe to call in a game that never posts anything.
|
||||
function scoreUpdate()
|
||||
if masterLoaded then
|
||||
masterPump()
|
||||
end
|
||||
end
|
||||
BIN
assets/FreeSansBold.ttf
(Stored with Git LFS)
BIN
assets/FreeSansBold.ttf
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,522 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- The master service, from the cabinet's side.
|
||||
--
|
||||
-- One place that knows the server address, the signed-in account and the player's name, so the menu
|
||||
-- and every game agree about them. It lives in the data root rather than a game's own directory
|
||||
-- because an account belongs to the machine: a game posting a score needs the same token the menu
|
||||
-- signed in with.
|
||||
--
|
||||
-- Everything here is asynchronous -- see Singe/Net.singe for why -- so every call takes a function
|
||||
-- and calls it back. masterPump() must run once a frame for any of it to make progress.
|
||||
|
||||
|
||||
dofile("Singe/Net.singe")
|
||||
|
||||
local cjson = require("cjson")
|
||||
|
||||
MASTER_DEFAULT_URL = "https://master.singeengine.com"
|
||||
|
||||
MASTER = {
|
||||
url = MASTER_DEFAULT_URL,
|
||||
token = nil,
|
||||
email = nil,
|
||||
name = nil,
|
||||
queue = {}, -- scores waiting for a connection
|
||||
}
|
||||
|
||||
local settingsPath = nil
|
||||
local flushing = false
|
||||
local lastFlush = 0
|
||||
local flushDue = false
|
||||
local FLUSH_EVERY = 30000
|
||||
|
||||
-- master.dat holds a session token and an email address, so it is not kept as plain text. A cabinet
|
||||
-- signs in unattended, so whatever reads the file back is on the machine too: this keeps the token
|
||||
-- and the address from being read at a glance or copied out of a text editor, and an edited file
|
||||
-- reads as no account at all, but it is no protection against someone with the file and this source.
|
||||
-- The file is SEAL_MAGIC, a random nonce, a tag, and the JSON XORed with a keystream: SHA-256 of the
|
||||
-- key, the nonce, and a block counter. The tag is SHA-256 of the key, the nonce, and the result.
|
||||
local SEAL_MAGIC = "SMD1"
|
||||
local SEAL_KEY = "Singe master.dat" -- Not a secret: see above
|
||||
local SEAL_NONCE = 16 -- Bytes
|
||||
local SEAL_TAG = 16 -- Bytes
|
||||
local SEAL_BLOCK = 32 -- Bytes of keystream a SHA-256 gives
|
||||
|
||||
|
||||
local function path()
|
||||
if not settingsPath then
|
||||
settingsPath = singeGetSystemInfo().dataRoot .. "master.dat"
|
||||
end
|
||||
return settingsPath
|
||||
end
|
||||
|
||||
|
||||
-- SHA-256 as bytes rather than hex.
|
||||
local function sealDigest(data)
|
||||
return (utilSha256(data):gsub("%x%x", function(pair) return string.char(tonumber(pair, 16)) end))
|
||||
end
|
||||
|
||||
|
||||
-- text XORed with the keystream for nonce: the same call seals and unseals.
|
||||
local function sealStream(nonce, text)
|
||||
local out = {}
|
||||
|
||||
for block = 0, (#text - 1) // SEAL_BLOCK do
|
||||
local key = sealDigest(SEAL_KEY .. nonce .. string.pack(">I4", block))
|
||||
local piece = text:sub(block * SEAL_BLOCK + 1, (block + 1) * SEAL_BLOCK)
|
||||
local bytes = {}
|
||||
for i = 1, #piece do
|
||||
bytes[i] = string.char(piece:byte(i) ~ key:byte(i))
|
||||
end
|
||||
out[#out + 1] = table.concat(bytes)
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
|
||||
local function sealTag(nonce, sealed)
|
||||
return sealDigest(SEAL_KEY .. "tag" .. nonce .. sealed):sub(1, SEAL_TAG)
|
||||
end
|
||||
|
||||
|
||||
-- text as master.dat keeps it, with a fresh nonce each time.
|
||||
local function seal(text)
|
||||
local nonce = sealDigest(tostring(os.time()) .. os.clock() .. tostring({}) .. math.random()):sub(1, SEAL_NONCE)
|
||||
local sealed = sealStream(nonce, text)
|
||||
|
||||
return SEAL_MAGIC .. nonce .. sealTag(nonce, sealed) .. sealed
|
||||
end
|
||||
|
||||
|
||||
-- The text master.dat holds: unsealed, or as it is when it is still plain JSON (from before files
|
||||
-- were sealed); nil when the tag does not match.
|
||||
local function unseal(data)
|
||||
local nonce = data:sub(#SEAL_MAGIC + 1, #SEAL_MAGIC + SEAL_NONCE)
|
||||
local tag = data:sub(#SEAL_MAGIC + SEAL_NONCE + 1, #SEAL_MAGIC + SEAL_NONCE + SEAL_TAG)
|
||||
local sealed = data:sub(#SEAL_MAGIC + SEAL_NONCE + SEAL_TAG + 1)
|
||||
|
||||
if data:sub(1, #SEAL_MAGIC) ~= SEAL_MAGIC then
|
||||
return data
|
||||
end
|
||||
if (#nonce ~= SEAL_NONCE) or (sealTag(nonce, sealed) ~= tag) then
|
||||
return nil
|
||||
end
|
||||
return sealStream(nonce, sealed)
|
||||
end
|
||||
|
||||
|
||||
function masterLoad()
|
||||
local file = io.open(path(), "rb")
|
||||
local data = nil
|
||||
local text = nil
|
||||
local ok = false
|
||||
local stored = nil
|
||||
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
data = file:read("a")
|
||||
file:close()
|
||||
text = unseal(data)
|
||||
if text == nil then
|
||||
return
|
||||
end
|
||||
ok, stored = pcall(cjson.decode, text)
|
||||
if ok and type(stored) == "table" then
|
||||
MASTER.url = stored.url or MASTER_DEFAULT_URL
|
||||
MASTER.token = stored.token
|
||||
MASTER.email = stored.email
|
||||
MASTER.name = stored.name
|
||||
MASTER.queue = stored.queue or {}
|
||||
-- A file from before sealing is sealed as soon as it is read.
|
||||
if text == data then
|
||||
masterSave()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function masterSave()
|
||||
local file = io.open(path(), "wb")
|
||||
|
||||
if not file then
|
||||
return false
|
||||
end
|
||||
file:write(seal(cjson.encode({ url = MASTER.url, token = MASTER.token, email = MASTER.email,
|
||||
name = MASTER.name, queue = MASTER.queue })))
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function masterSignedIn()
|
||||
return MASTER.token ~= nil
|
||||
end
|
||||
|
||||
|
||||
-- Every call goes through here so the token, the content type and the JSON decoding are in one
|
||||
-- place, and so a 401 always means the same thing: whatever we were holding is no longer a
|
||||
-- session, and pretending otherwise only produces a second failure later.
|
||||
local function request(method, endpoint, body, onDone, opts)
|
||||
opts = opts or {}
|
||||
local headers = { ["Content-Type"] = "application/json" }
|
||||
|
||||
if MASTER.token and not opts.anonymous then
|
||||
headers["Authorization"] = "Bearer " .. MASTER.token
|
||||
end
|
||||
if opts.bodyFile then
|
||||
headers["Content-Type"] = opts.contentType or "application/octet-stream"
|
||||
end
|
||||
netRequest({
|
||||
url = MASTER.url .. endpoint, method = method, headers = headers,
|
||||
body = body and cjson.encode(body) or nil, bodyFile = opts.bodyFile,
|
||||
toFile = opts.toFile, onProgress = opts.onProgress,
|
||||
}, function(ok, result)
|
||||
if not ok then
|
||||
return onDone(false, result.error)
|
||||
end
|
||||
local parsed = {}
|
||||
if result.body and result.body ~= "" then
|
||||
local decoded, value = pcall(cjson.decode, result.body)
|
||||
if decoded and type(value) == "table" then
|
||||
parsed = value
|
||||
end
|
||||
end
|
||||
if result.status == 401 and not opts.anonymous then
|
||||
MASTER.token = nil
|
||||
masterSave()
|
||||
return onDone(false, "signed out by the server", parsed, result)
|
||||
end
|
||||
if result.status >= 400 then
|
||||
return onDone(false, parsed.message or ("the server said " .. result.status), parsed, result)
|
||||
end
|
||||
onDone(true, parsed, parsed, result)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
MASTER_REQUEST = request
|
||||
|
||||
|
||||
function masterSignIn(email, password, onDone)
|
||||
request("POST", "/v1/account/login", { email = email, password = password }, function(ok, reply)
|
||||
if not ok then
|
||||
return onDone(false, reply)
|
||||
end
|
||||
MASTER.token = reply.token
|
||||
MASTER.email = reply.email
|
||||
masterSave()
|
||||
-- The display name is not part of signing in, so it is fetched once and kept: a game
|
||||
-- about to post a score should not have to discover it has none mid-play.
|
||||
masterWhoAmI(function() end)
|
||||
onDone(true, reply)
|
||||
end, { anonymous = true })
|
||||
end
|
||||
|
||||
|
||||
function masterRegister(email, password, onDone)
|
||||
request("POST", "/v1/account/register", { email = email, password = password }, onDone, { anonymous = true })
|
||||
end
|
||||
|
||||
|
||||
function masterRecover(email, onDone)
|
||||
request("POST", "/v1/account/recover", { email = email }, onDone, { anonymous = true })
|
||||
end
|
||||
|
||||
|
||||
function masterSignOut(onDone)
|
||||
local had = MASTER.token
|
||||
|
||||
MASTER.token = nil
|
||||
MASTER.email = nil
|
||||
MASTER.name = nil
|
||||
masterSave()
|
||||
if had then
|
||||
-- Told after the fact: the cabinet is signed out either way, and a server that cannot be
|
||||
-- reached must not leave somebody looking signed in.
|
||||
MASTER.token = had
|
||||
request("POST", "/v1/account/logout", nil, function() end)
|
||||
MASTER.token = nil
|
||||
end
|
||||
if onDone then onDone(true) end
|
||||
end
|
||||
|
||||
|
||||
function masterWhoAmI(onDone)
|
||||
request("GET", "/v1/account", nil, function(ok, reply)
|
||||
if ok then
|
||||
MASTER.email = reply.email or MASTER.email
|
||||
masterSave()
|
||||
end
|
||||
onDone(ok, reply)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function masterSetName(name, onDone)
|
||||
request("POST", "/v1/player/name", { name = name }, function(ok, reply)
|
||||
if ok then
|
||||
MASTER.name = reply.name
|
||||
masterSave()
|
||||
end
|
||||
onDone(ok, reply)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function masterCatalogue(onDone)
|
||||
request("GET", "/v1/catalogue", nil, function(ok, reply)
|
||||
if ok then
|
||||
for _, game in ipairs(reply.games or {}) do
|
||||
game.version = game.version and math.floor(game.version) or nil
|
||||
game.installed = game.installed and math.floor(game.installed) or nil
|
||||
game.size = game.size and math.floor(game.size) or nil
|
||||
-- Every title the download holds, in its games.dat order.
|
||||
for _, entry in ipairs(game.entries or {}) do
|
||||
entry.entry = entry.entry and math.floor(entry.entry) or nil
|
||||
entry.year = entry.year and math.floor(entry.year) or nil
|
||||
entry.tier = entry.tier and math.floor(entry.tier) or nil
|
||||
end
|
||||
end
|
||||
end
|
||||
onDone(ok, ok and reply.games or reply)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- Download a game to a file and check it against the digest the catalogue published. A download
|
||||
-- that arrives wrong is worse than one that fails: it installs, and then does not run.
|
||||
function masterDownload(gameId, version, toFile, expectSha, onProgress, onDone)
|
||||
-- JSON has one number type and Lua 5.4 has two, so a version that went out as 1 comes back as
|
||||
-- 1.0 and would build a path ending "/1.0" that the service does not recognize.
|
||||
local path = string.format("/v1/download/%s/%d", gameId, math.floor(version))
|
||||
|
||||
request("GET", path, nil, function(ok, reply, _, result)
|
||||
if not ok then
|
||||
return onDone(false, reply)
|
||||
end
|
||||
local file = io.open(toFile, "rb")
|
||||
if not file then
|
||||
return onDone(false, "the download did not land on disk")
|
||||
end
|
||||
local bytes = file:read("*a")
|
||||
file:close()
|
||||
if expectSha and utilSha256(bytes) ~= expectSha then
|
||||
os.remove(toFile)
|
||||
return onDone(false, "the download did not match its published checksum")
|
||||
end
|
||||
onDone(true, { bytes = #bytes, file = toFile })
|
||||
end, { toFile = toFile, onProgress = onProgress })
|
||||
end
|
||||
|
||||
|
||||
-- What this machine holds, and what is newer. The list is the menu's own, read off every
|
||||
-- games.dat it loaded (GAME_ID and VERSION), and the service keeps none of it: it answers which
|
||||
-- of these have a newer version and which it has never heard of. No sign-in needed to ask.
|
||||
function masterVersions(held, onDone)
|
||||
local games = {}
|
||||
|
||||
for _, entry in ipairs(held) do
|
||||
games[#games + 1] = { id = entry.id, version = math.floor(entry.version or 0) }
|
||||
end
|
||||
request("POST", "/v1/versions", { games = games }, function(ok, reply)
|
||||
if ok then
|
||||
for _, update in ipairs(reply.updates or {}) do
|
||||
update.installed = update.installed and math.floor(update.installed) or 0
|
||||
update.latest = update.latest and math.floor(update.latest) or nil
|
||||
update.size = update.size and math.floor(update.size) or nil
|
||||
end
|
||||
end
|
||||
onDone(ok, ok and (reply.updates or {}) or reply, ok and (reply.unknown or {}) or nil)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- Send a packed game up for review. The file goes as the body, from disk, so a big game is not
|
||||
-- held in memory; onProgress(sent, total) says how it goes, and onDone(ok, reply) ends with the
|
||||
-- version number the service gave it and the state it starts in, which is "pending".
|
||||
function masterUpload(file, notes, onProgress, onDone)
|
||||
local endpoint = "/v1/upload"
|
||||
|
||||
if notes and notes ~= "" then
|
||||
endpoint = endpoint .. "?notes=" .. notes:gsub("[^%w%-%._~]", function(c)
|
||||
return string.format("%%%02X", c:byte())
|
||||
end)
|
||||
end
|
||||
request("POST", endpoint, nil, function(ok, reply)
|
||||
if ok and reply.version then
|
||||
reply.version = math.floor(reply.version)
|
||||
end
|
||||
onDone(ok, reply)
|
||||
end, { bodyFile = file, onProgress = onProgress })
|
||||
end
|
||||
|
||||
|
||||
-- Is there a newer Singe for this machine? Public, no sign-in: the answer is the newest release
|
||||
-- for the platform when it is newer than the version running, or newer = false.
|
||||
function masterRelease(platform, running, onDone)
|
||||
request("GET", "/v1/release/" .. platform .. "?version=" .. tostring(running), nil, function(ok, reply)
|
||||
onDone(ok, reply)
|
||||
end, { anonymous = true })
|
||||
end
|
||||
|
||||
|
||||
-- Fetch one release to a file, checked against the digest the service published, exactly as a
|
||||
-- game is; a binary that arrives wrong must not be put beside the one that works.
|
||||
function masterDownloadRelease(platform, version, toFile, expectSha, onProgress, onDone)
|
||||
request("GET", "/v1/release/" .. platform .. "/" .. version, nil, function(ok, reply, _, result)
|
||||
if not ok then
|
||||
return onDone(false, reply)
|
||||
end
|
||||
local file = io.open(toFile, "rb")
|
||||
if not file then
|
||||
return onDone(false, "the download did not land on disk")
|
||||
end
|
||||
local bytes = file:read("*a")
|
||||
file:close()
|
||||
if expectSha and utilSha256(bytes) ~= expectSha then
|
||||
os.remove(toFile)
|
||||
return onDone(false, "the download did not match its published checksum")
|
||||
end
|
||||
onDone(true, { bytes = #bytes, file = toFile })
|
||||
end, { toFile = toFile, onProgress = onProgress, anonymous = true })
|
||||
end
|
||||
|
||||
|
||||
-- This account's own games, every version with the review's word on it.
|
||||
function masterMine(onDone)
|
||||
request("GET", "/v1/catalogue/mine", nil, function(ok, reply)
|
||||
onDone(ok, ok and (reply.games or {}) or reply)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- One board. lowest asks for a board that ranks the lowest score first; a board keeps the direction
|
||||
-- its first score fixed, and the reply's order says which it is.
|
||||
function masterBoard(gameId, board, onDone, lowest)
|
||||
local query = {}
|
||||
|
||||
if board then
|
||||
query[#query + 1] = "board=" .. board
|
||||
end
|
||||
if lowest then
|
||||
query[#query + 1] = "order=lowest"
|
||||
end
|
||||
request("GET", "/v1/scores/" .. gameId .. (#query > 0 and ("?" .. table.concat(query, "&")) or ""), nil, function(ok, reply)
|
||||
-- JSON has one number type and Lua 5.4 has two, so a score that went out as 4242 comes back
|
||||
-- as 4242.0 and would be shown to a player with a decimal point on it.
|
||||
if ok then
|
||||
for _, row in ipairs(reply.top or {}) do
|
||||
row.value = math.floor(row.value)
|
||||
row.at = row.at and math.floor(row.at) or nil
|
||||
end
|
||||
if reply.standing then
|
||||
reply.standing.value = math.floor(reply.standing.value)
|
||||
reply.standing.rank = math.floor(reply.standing.rank)
|
||||
reply.standing.players = math.floor(reply.standing.players)
|
||||
end
|
||||
end
|
||||
onDone(ok, reply)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- Tell the service a play is starting. What comes back travels with the score, so the server can
|
||||
-- say how long the play took by its own clock; a cabinet that is offline simply has no play id and
|
||||
-- the score carries no time, which is honest rather than broken.
|
||||
function masterPlayStart(gameId, onDone)
|
||||
if not masterSignedIn() then
|
||||
MASTER.play = nil
|
||||
if onDone then onDone(false, "not signed in") end
|
||||
return
|
||||
end
|
||||
request("POST", "/v1/play/start", { game = gameId }, function(ok, reply)
|
||||
MASTER.play = ok and reply.play and math.floor(reply.play) or nil
|
||||
if onDone then onDone(ok, reply) end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- A score is queued first and sent afterwards. A cabinet is often offline, and a game must never
|
||||
-- wait on the wire to show its own board; the queue is written to disk so a power cut between the
|
||||
-- play and the send does not lose it. lowest sends it to a board that ranks the lowest first; one
|
||||
-- queued without it (as every score was before boards had a direction) goes the highest first.
|
||||
function masterSubmitScore(gameId, value, board, meta, lowest)
|
||||
MASTER.queue[#MASTER.queue + 1] = { game = gameId, value = math.floor(value),
|
||||
board = board or "default", meta = meta,
|
||||
play = MASTER.play, order = lowest and "lowest" or nil }
|
||||
masterSave()
|
||||
-- Send it on the next pump rather than waiting out the retry interval: a player who has just
|
||||
-- finished wants to see themselves on the board, and the interval exists for retrying what
|
||||
-- failed, not for delaying what has not been tried. This is a flag rather than a timestamp:
|
||||
-- singeGetTicks() counts from engine start, so "lastFlush = 0" would mean "once the engine has
|
||||
-- been up FLUSH_EVERY", which early in a run is never.
|
||||
flushDue = true
|
||||
end
|
||||
|
||||
|
||||
function masterQueueLength()
|
||||
return #MASTER.queue
|
||||
end
|
||||
|
||||
|
||||
-- Hand the queue to the server one at a time. A refusal that is the score's own fault (the game is
|
||||
-- unknown, the value is nonsense, the board ranks the other way) drops it, because retrying it
|
||||
-- forever would block everything behind it; anything else leaves it in place to try again.
|
||||
local function flushQueue()
|
||||
if flushing or #MASTER.queue == 0 or not MASTER.token then
|
||||
return
|
||||
end
|
||||
flushing = true
|
||||
local entry = MASTER.queue[1]
|
||||
request("POST", "/v1/scores/" .. entry.game, { value = entry.value, board = entry.board, meta = entry.meta, play = entry.play, order = entry.order },
|
||||
function(ok, reply, parsed, result)
|
||||
local status = result and result.status or 0
|
||||
if ok or (status >= 400 and status < 500 and status ~= 429) then
|
||||
table.remove(MASTER.queue, 1)
|
||||
masterSave()
|
||||
-- A backlog should drain, not trickle out one per interval.
|
||||
if #MASTER.queue > 0 then
|
||||
flushDue = true
|
||||
end
|
||||
end
|
||||
flushing = false
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function masterPump()
|
||||
netPump()
|
||||
if flushDue or (singeGetTicks() - lastFlush > FLUSH_EVERY) then
|
||||
flushDue = false
|
||||
lastFlush = singeGetTicks()
|
||||
flushQueue()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
masterLoad()
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<rml>
|
||||
<!--
|
||||
Singe 3
|
||||
Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
|
||||
The bundled game menu: the document Singe/Menu.singe fills and drives. Every region is placed
|
||||
by the script (it owns the geometry so the attract video, drawn into the overlay underneath,
|
||||
lands exactly in the #video hole); Singe/menu.rcss only says what the regions look like.
|
||||
Licensed under the GNU General Public License, version 3 or later.
|
||||
-->
|
||||
<head>
|
||||
<title>Menu</title>
|
||||
<link type="text/rcss" href="Singe/gui.rcss"/>
|
||||
<link type="text/rcss" href="Singe/menu.rcss"/>
|
||||
</head>
|
||||
<body id="menu">
|
||||
<div id="games">
|
||||
<div id="list" class="list"></div>
|
||||
<div id="poster" class="art"></div>
|
||||
<div id="marquee" class="art"></div>
|
||||
<div id="video"></div>
|
||||
<div id="info" class="panel">
|
||||
<div id="title"></div>
|
||||
<div id="credits" class="line"></div>
|
||||
<div id="developer" class="line"></div>
|
||||
<div id="publisher" class="line"></div>
|
||||
<div id="port" class="line"></div>
|
||||
<div id="notice" class="line"></div>
|
||||
<div id="description"></div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<div id="hints">Up / Down: select Left / Right: page PgUp / PgDn: details Start or button: play Service: tools</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Every service tool draws through this one page: Singe/Tools.singe hands the menu a list of
|
||||
rows and Singe/MenuDocument.singe turns them into the divs below it -- the rows into the
|
||||
body, the key hints into the footer, and on the list of tools the chosen tool's note into
|
||||
the column beside the list. -->
|
||||
<div id="tool" class="tool hidden">
|
||||
<div id="toolCard" class="card">
|
||||
<h1 id="toolTitle"></h1>
|
||||
<div id="toolColumns" class="columns">
|
||||
<div id="toolBody"></div>
|
||||
<div id="toolNote" class="hidden"></div>
|
||||
</div>
|
||||
<div id="toolKeys"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
|
|
@ -1,644 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- The bundled game menu. Everything the menu *is* lives here: the game list read from every
|
||||
-- games.dat, the selection and how it moves, the attract video, launching a game and coming back,
|
||||
-- and the service tools. Nothing here draws.
|
||||
--
|
||||
-- Drawing is a renderer, chosen once at startup by whether this machine has a GPU device:
|
||||
--
|
||||
-- Singe/MenuDocument.singe an RmlUi document, which needs the GPU device the 3D scene uses
|
||||
-- Singe/MenuOverlay.singe sprites and fontPrint straight into the overlay, which needs nothing
|
||||
--
|
||||
-- The two draw the same menu and take the same keys. A renderer provides the MENU_RENDER table;
|
||||
-- see the top of either file for what is in it.
|
||||
|
||||
dofile("Singe/Framework.singe")
|
||||
dofile("Singe/Master.singe")
|
||||
dofile("Singe/Tools.singe")
|
||||
|
||||
local lfs = require("lfs")
|
||||
|
||||
|
||||
function cleanTitle(a)
|
||||
local output = string.lower(a)
|
||||
output = string.gsub(output, '%p', ' ')
|
||||
output = string.gsub(output, '^ +', '')
|
||||
output = string.gsub(output, '^the ', '')
|
||||
output = string.gsub(output, '^a ', '')
|
||||
output = string.gsub(output, '^an ', '')
|
||||
return output
|
||||
end
|
||||
|
||||
|
||||
function compareTitles(a, b)
|
||||
return cleanTitle(a.TITLE) < cleanTitle(b.TITLE)
|
||||
end
|
||||
|
||||
|
||||
-- What the page shows when there is nothing to choose: the way to get something.
|
||||
MENU_EMPTY = {
|
||||
EMPTY = true,
|
||||
TITLE = "No games yet",
|
||||
DESCRIPTION = "Press SERVICE for the service tools. Sign in under Online Account, then choose " ..
|
||||
"what to fetch in Get Games. What you fetch is listed here as soon as you leave the tools.",
|
||||
}
|
||||
|
||||
|
||||
-- Makes a game the selection, wrapping at either end of the list, and shows it. An empty library
|
||||
-- has no selection, only MENU_EMPTY.
|
||||
function gameSelect(index)
|
||||
unloadGameAssets()
|
||||
if GAME_COUNT == 0 then
|
||||
GAME_SELECTED = nil
|
||||
MENU_RENDER.showGame(MENU_EMPTY)
|
||||
menuRefreshNotice()
|
||||
return
|
||||
end
|
||||
if index < 1 then
|
||||
index = GAME_COUNT
|
||||
elseif index > GAME_COUNT then
|
||||
index = 1
|
||||
end
|
||||
GAME_SELECTED = index
|
||||
loadGameAssets()
|
||||
end
|
||||
|
||||
|
||||
-- Starts the selected game. The menu comes back from the top when it ends. A game this machine
|
||||
-- cannot run says why instead of failing part way in.
|
||||
function gameStart()
|
||||
local game = GAME_LIST[GAME_SELECTED]
|
||||
|
||||
if game.CANNOT_RUN then
|
||||
menuNotice(game.MACHINE_FIT)
|
||||
return
|
||||
end
|
||||
SHUTDOWN_FROM_PUSH = true
|
||||
scriptPush(game)
|
||||
end
|
||||
|
||||
|
||||
function loadGameAssets()
|
||||
local game = GAME_LIST[GAME_SELECTED]
|
||||
|
||||
-- An entry without an attract clip, or naming one that is not there, shows its art and no
|
||||
-- video rather than taking the whole menu down with a videoLoad that cannot open it.
|
||||
if menuFileExists(game.ATTRACT) then
|
||||
VIDEO_ATTRACT = videoLoad(game.ATTRACT)
|
||||
if game.AUDIO_TRACK then
|
||||
videoSetAudioTrack(VIDEO_ATTRACT, game.AUDIO_TRACK)
|
||||
end
|
||||
videoPlay(VIDEO_ATTRACT)
|
||||
videoSeek(VIDEO_ATTRACT, game.ATTRACT_START or 0)
|
||||
else
|
||||
debugPrint("Menu: " .. tostring(game.TITLE) .. " has no attract clip to play.")
|
||||
end
|
||||
MENU_RENDER.showGame(game)
|
||||
menuRefreshNotice()
|
||||
end
|
||||
|
||||
|
||||
-- Where a game's art should be when it is not there. Extracted with the other support files, so
|
||||
-- it is always present whatever a game does or does not ship.
|
||||
MENU_MISSING_ART = "Singe/missing.png"
|
||||
|
||||
|
||||
-- Whether a name is a file the engine can open. Asked of the index rather than of the file:
|
||||
-- io.open, which this used to use, has to hand Lua a real file it can read with the C library, so
|
||||
-- every picture it was asked about was unpacked out of the .game into data/<game>/cache first --
|
||||
-- and the menu asks about two of them for every game in the library, none of which it then opens
|
||||
-- that way: the sprite loader and the document's texture loader both read straight out of the
|
||||
-- database. lfs.attributes goes through the same virtual filesystem and answers from the index,
|
||||
-- opening nothing.
|
||||
function menuFileExists(file)
|
||||
local path = tostring(file or "")
|
||||
local found = (path ~= "") and lfs.attributes(path) or nil
|
||||
|
||||
return (found ~= nil) and (found.mode ~= "directory")
|
||||
end
|
||||
|
||||
|
||||
-- A readable art path: the game's own when it can be opened, the missing-art picture when not.
|
||||
function menuArtPath(file)
|
||||
local path = tostring(file or "")
|
||||
|
||||
if not menuFileExists(path) then
|
||||
if path ~= "" then
|
||||
debugPrint("Menu: cannot read " .. path .. "; showing the missing-art picture.")
|
||||
end
|
||||
return MENU_MISSING_ART
|
||||
end
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
|
||||
-- How far left and right move. A page is a screenful, which is what the renderer answers, but in a
|
||||
-- library smaller than a screenful every press then lands on the first game or the last and nothing
|
||||
-- in between can be reached that way at all. A third of the library is never more than three
|
||||
-- presses from one end to the other, whatever size it is.
|
||||
function pageStep()
|
||||
return math.max(1, math.min(MENU_RENDER.pageSize(), math.floor(GAME_COUNT / 3)))
|
||||
end
|
||||
|
||||
|
||||
-- The game page takes input once the backdrop has finished the intro and settled on the grid.
|
||||
function menuActive()
|
||||
return (not toolsActive()) and MENU_RENDER.backdropReady()
|
||||
end
|
||||
|
||||
|
||||
-- In MODE_FULL every key arrives here as its keysym rather than as a switch, which is what the
|
||||
-- account tool's typing needs; the switches it still cares about are the pad's, which keep working.
|
||||
function onKeyPressed(keysym, scancode)
|
||||
if toolsActive() then
|
||||
toolsTyped(keysym)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function onInputPressed(what)
|
||||
|
||||
-- The service tools take every switch while they are up, including the one that closes them.
|
||||
if toolsActive() then
|
||||
toolsInput(what)
|
||||
if MENU_LIBRARY_CHANGED and not toolsActive() then
|
||||
menuRescanGames()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Are we displaying the grid background? With no games, SERVICE is all there is to press.
|
||||
if menuActive() then
|
||||
|
||||
if what == SWITCH_SERVICE then
|
||||
toolsBegin()
|
||||
elseif GAME_COUNT == 0 then
|
||||
return
|
||||
elseif what == SWITCH_UP then
|
||||
gameSelect(GAME_SELECTED - 1)
|
||||
elseif what == SWITCH_DOWN then
|
||||
gameSelect(GAME_SELECTED + 1)
|
||||
elseif what == SWITCH_LEFT then
|
||||
-- A page back, wrapping to the end from the first game.
|
||||
if GAME_SELECTED == 1 then
|
||||
gameSelect(GAME_COUNT)
|
||||
else
|
||||
gameSelect(math.max(1, GAME_SELECTED - pageStep()))
|
||||
end
|
||||
elseif what == SWITCH_RIGHT then
|
||||
if GAME_SELECTED == GAME_COUNT then
|
||||
gameSelect(1)
|
||||
else
|
||||
gameSelect(math.min(GAME_COUNT, GAME_SELECTED + pageStep()))
|
||||
end
|
||||
elseif what == SWITCH_BUTTON3 and menuUpdateFor(GAME_LIST[GAME_SELECTED]) then
|
||||
menuUpdateGame()
|
||||
elseif what == SWITCH_START1 or what == SWITCH_START2 or what == SWITCH_BUTTON1 or what == SWITCH_BUTTON2 or what == SWITCH_BUTTON3 or what == SWITCH_BUTTON4 then
|
||||
gameStart()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
|
||||
-- The intro, then the grid behind the menu, and the overlay cleared ready to be drawn into.
|
||||
MENU_RENDER.backdrop()
|
||||
|
||||
-- The network never blocks the frame; it makes progress here instead.
|
||||
masterPump()
|
||||
|
||||
if toolsActive() then
|
||||
toolsUpdate()
|
||||
MENU_RENDER.tools()
|
||||
return(OVERLAY_UPDATED)
|
||||
end
|
||||
|
||||
-- Are we displaying the grid background?
|
||||
if menuActive() then
|
||||
-- The page keys take the details over from the walk they do by themselves; the walk picks
|
||||
-- up from wherever they leave it once its pause is up. They arrive as a scancode rather
|
||||
-- than a switch because they are not one: a cabinet has no page keys and loses nothing.
|
||||
local key = keyboardGetLastDown()
|
||||
|
||||
if key == SCANCODE.PAGEDOWN.value then
|
||||
MENU_RENDER.scrollDetails(1)
|
||||
elseif key == SCANCODE.PAGEUP.value then
|
||||
MENU_RENDER.scrollDetails(-1)
|
||||
end
|
||||
|
||||
-- The attract clip runs between the two frames the game's entry names.
|
||||
if VIDEO_ATTRACT and GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
||||
if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
||||
videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START or 0)
|
||||
end
|
||||
end
|
||||
MENU_RENDER.frame()
|
||||
end
|
||||
|
||||
return(OVERLAY_UPDATED)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onShutdown()
|
||||
unloadGameAssets()
|
||||
saveConfig(not SHUTDOWN_FROM_PUSH)
|
||||
MENU_RENDER.finish()
|
||||
if SND_INTRO_VOICE ~= SOUND_ERROR_INVALID then
|
||||
-- A game started before the intro's sound had finished; it wants the speakers to itself.
|
||||
soundStop(SND_INTRO_VOICE)
|
||||
end
|
||||
if SND_INTRO then
|
||||
soundUnload(SND_INTRO)
|
||||
end
|
||||
if SND_CLICK then
|
||||
soundUnload(SND_CLICK)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
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")
|
||||
-- The file is written whole, so a renderer forced here has to be written back or it
|
||||
-- would last exactly one run.
|
||||
if MENU_OVERLAY then
|
||||
cfg:write("MENU_OVERLAY = true\n")
|
||||
end
|
||||
cfg:close()
|
||||
else
|
||||
debugPrint("Unable to write " .. CONFIG_FILE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function unloadGameAssets()
|
||||
-- Nothing is loaded when there are no games.
|
||||
if VIDEO_ATTRACT then
|
||||
videoUnload(VIDEO_ATTRACT)
|
||||
VIDEO_ATTRACT = nil
|
||||
end
|
||||
MENU_RENDER.hideGame()
|
||||
end
|
||||
|
||||
|
||||
-- What this machine can run (PLAN.md section 57), asked once, and what each capability is called
|
||||
-- when a game needs one the machine lacks.
|
||||
local MACHINE = singeCapabilities()
|
||||
local CAPABILITY_WORDS = { SCENE = "3D", GUI = "a GPU for its screens", SHADOWS = "shadows", POST = "post effects", SKIN = "skinned models", MORPH = "morph targets" }
|
||||
|
||||
|
||||
-- How a game suits this machine: a line for the notice, or nil when it simply runs, and whether it
|
||||
-- cannot run here at all. The list annotates; it does not hide. A game missing from a list gets
|
||||
-- asked about, and one that says why it will not run does not. The Get Games tool asks it of the
|
||||
-- catalogue's entries too, which carry the same REQUIRES and TIER.
|
||||
function menuMachineFit(game)
|
||||
local missing = {}
|
||||
|
||||
for _, name in ipairs(type(game.REQUIRES) == "table" and game.REQUIRES or {}) do
|
||||
if not MACHINE[name] then
|
||||
missing[#missing + 1] = CAPABILITY_WORDS[name] or name
|
||||
end
|
||||
end
|
||||
if #missing > 0 then
|
||||
return "Needs " .. table.concat(missing, " and ") .. ", and this machine has none.", true
|
||||
end
|
||||
if MACHINE.tier and ((tonumber(game.TIER) or 0) > MACHINE.tier) then
|
||||
return "Wants a faster machine than this one. It may run slowly.", false
|
||||
end
|
||||
|
||||
return nil, false
|
||||
end
|
||||
|
||||
|
||||
-- Adds the entries of one games.dat. container is the game database the file came from, or nil.
|
||||
local function loadGamesDat(source, container)
|
||||
GAMES = {}
|
||||
local ok, err = pcall(dofile, source)
|
||||
if not ok then
|
||||
debugPrint(source .. ": " .. tostring(err) .. " Skipped.")
|
||||
GAMES = {}
|
||||
return
|
||||
end
|
||||
for _, value in pairs(GAMES or {}) do
|
||||
if container then
|
||||
-- Packed game: the engine opens its files through the database, and so does the menu.
|
||||
value.CONTAINER = container
|
||||
for _, key in ipairs({ "CABINET", "MARQUEE", "ATTRACT" }) do
|
||||
if value[key] and value[key]:sub(1, 6):lower() ~= "singe/" then
|
||||
value[key] = container .. "/" .. value[key]
|
||||
end
|
||||
end
|
||||
end
|
||||
value.MACHINE_FIT, value.CANNOT_RUN = menuMachineFit(value)
|
||||
table.insert(GAME_LIST, value)
|
||||
GAME_COUNT = GAME_COUNT + 1
|
||||
end
|
||||
GAMES = {}
|
||||
end
|
||||
|
||||
-- Search for games.dat files in subdirectories and inside .game databases, starting the list over.
|
||||
--
|
||||
-- The extension is tested before the mode, and that order matters: the engine presents a packed
|
||||
-- game through the VFS, so lfs.attributes reports a .game as a *directory*, not a file. Testing
|
||||
-- the mode first sent every container down the loose-directory branch, which loads its games.dat
|
||||
-- with no container -- and then every CABINET, MARQUEE and ATTRACT path in it named a file that
|
||||
-- exists only inside the database. A packed library could not show a single piece of its art.
|
||||
--
|
||||
-- What this machine holds, by the id and version each games.dat carries, is rebuilt with it.
|
||||
-- VERSION is stamped into a packed game by the service when it is published; a game without one
|
||||
-- is version 0. MENU_VERSIONS is the only record of installed versions anywhere: the service
|
||||
-- keeps none.
|
||||
function menuScanGames()
|
||||
GAME_LIST = {}
|
||||
GAME_COUNT = 0
|
||||
MENU_VERSIONS = {}
|
||||
for dir in lfs.dir(".") do
|
||||
if dir ~= "." and dir ~= ".." then
|
||||
local dirattr = lfs.attributes(dir)
|
||||
if dir:sub(-5):lower() == ".game" then
|
||||
loadGamesDat(dir .. "/games.dat", dir)
|
||||
elseif dirattr and (dirattr.mode == "directory") then
|
||||
for file in lfs.dir(dir .. "/.") do
|
||||
if file == "games.dat" then
|
||||
loadGamesDat(dir .. "/games.dat", nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(GAME_LIST, compareTitles)
|
||||
-- A download holding several titles lists each of them here, all with the one GAME_ID. The
|
||||
-- highest VERSION any of them says is the download's, so a title that does not say one cannot
|
||||
-- make an up to date copy look old.
|
||||
for _, game in ipairs(GAME_LIST) do
|
||||
if game.GAME_ID then
|
||||
MENU_VERSIONS[game.GAME_ID] = math.max(MENU_VERSIONS[game.GAME_ID] or 0, math.floor(tonumber(game.VERSION) or 0))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- One games.dat entry, told apart from every other: the titles of one download share its GAME_ID,
|
||||
-- and a game offered twice shares its SCRIPT, so it takes all three.
|
||||
local function entryKey(game)
|
||||
return tostring(game.GAME_ID) .. "\n" .. tostring(game.SCRIPT) .. "\n" .. tostring(game.TITLE)
|
||||
end
|
||||
|
||||
|
||||
-- The Get Games tool added or removed a game: the list is read again once the tools close, and
|
||||
-- the game that was selected stays selected when it is still here.
|
||||
function menuRescanGames()
|
||||
local was = GAME_SELECTED and GAME_LIST[GAME_SELECTED] or nil
|
||||
local key = was and entryKey(was) or nil
|
||||
local at = 1
|
||||
|
||||
MENU_LIBRARY_CHANGED = false
|
||||
unloadGameAssets()
|
||||
GAME_SELECTED = nil
|
||||
menuScanGames()
|
||||
for index, game in ipairs(GAME_LIST) do
|
||||
if key and (entryKey(game) == key) then
|
||||
at = index
|
||||
break
|
||||
end
|
||||
end
|
||||
MENU_RENDER.buildList()
|
||||
gameSelect(at)
|
||||
menuCheckVersions()
|
||||
end
|
||||
|
||||
|
||||
menuScanGames()
|
||||
MENU_UPDATES = {}
|
||||
|
||||
|
||||
-- Asks the service which of the games here have a newer version. No sign-in is needed to ask;
|
||||
-- the answer arrives whenever it arrives and the renderers pick it up. Called once at startup
|
||||
-- and again whenever the Get Games tool opens.
|
||||
function menuCheckVersions()
|
||||
local held = {}
|
||||
|
||||
for id, version in pairs(MENU_VERSIONS) do
|
||||
held[#held + 1] = { id = id, version = version }
|
||||
end
|
||||
if #held == 0 then
|
||||
return
|
||||
end
|
||||
masterVersions(held, function(ok, updates)
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
MENU_UPDATES = {}
|
||||
for _, update in ipairs(updates) do
|
||||
MENU_UPDATES[update.id] = update
|
||||
end
|
||||
if MENU_RENDER.refreshList then
|
||||
MENU_RENDER.refreshList()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
-- The newer version the service offers for a game, or nil.
|
||||
function menuUpdateFor(game)
|
||||
return game.GAME_ID and MENU_UPDATES[game.GAME_ID] or nil
|
||||
end
|
||||
|
||||
|
||||
-- Fetches the newer version of the selected game into place, saying how it goes where the
|
||||
-- renderers show a notice. The download needs a sign-in, which the Online Account tool does.
|
||||
function menuUpdateGame()
|
||||
local game = GAME_LIST[GAME_SELECTED]
|
||||
local update = menuUpdateFor(game)
|
||||
|
||||
if not update then
|
||||
return false
|
||||
end
|
||||
if not masterSignedIn() then
|
||||
menuNotice("Sign in first, in the Online Account tool (SERVICE), to fetch the update.")
|
||||
return true
|
||||
end
|
||||
if MENU_UPDATING then
|
||||
return true
|
||||
end
|
||||
MENU_UPDATING = true
|
||||
shopInstall({ id = update.id, slug = update.slug, title = update.title or game.TITLE,
|
||||
version = update.latest, sha256 = update.sha256, target = game.CONTAINER },
|
||||
function(text)
|
||||
menuNotice("Updating: " .. text)
|
||||
end,
|
||||
function(ok, text)
|
||||
MENU_UPDATING = false
|
||||
-- The list first, since refreshing it resets the notice to whatever the selection
|
||||
-- warrants; then what happened, which stays until the selection moves.
|
||||
if MENU_RENDER.refreshList then
|
||||
MENU_RENDER.refreshList()
|
||||
end
|
||||
menuNotice(ok and (text .. " Version " .. update.latest .. " plays from now on.") or text)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- One line the renderers show under the game, for whatever the menu has to say.
|
||||
MENU_NOTICE = nil
|
||||
function menuNotice(text)
|
||||
MENU_NOTICE = text
|
||||
if MENU_RENDER.notice then
|
||||
MENU_RENDER.notice(text)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The notice for the selected game when nothing is in flight: how it suits this machine, or the
|
||||
-- update on offer, or, failing that, the newer Singe on offer, or nothing. A download in progress keeps its own line.
|
||||
function menuRefreshNotice()
|
||||
if MENU_UPDATING then
|
||||
return
|
||||
end
|
||||
local game = GAME_SELECTED and GAME_LIST[GAME_SELECTED] or nil
|
||||
local update = game and menuUpdateFor(game) or nil
|
||||
|
||||
if game and game.MACHINE_FIT then
|
||||
menuNotice(game.MACHINE_FIT)
|
||||
elseif update then
|
||||
menuNotice(string.format("Version %d is available. Button 3 fetches it.", update.latest))
|
||||
elseif MENU_RELEASE then
|
||||
menuNotice(string.format("Singe %s is available. SERVICE, then Engine Update.", MENU_RELEASE.version))
|
||||
else
|
||||
menuNotice(nil)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Once a day, unless the settings say never: is there a newer Singe for this machine? The
|
||||
-- answer is only a line on screen and a service tool; nothing is fetched or changed until a
|
||||
-- person opens that tool and says so. The last look is remembered in the save store.
|
||||
MENU_RELEASE = nil
|
||||
local RELEASE_CHECK_EVERY = 24 * 60 * 60
|
||||
|
||||
function menuCheckRelease(force)
|
||||
if not force and SINGE_UPDATE_CHECK == false then
|
||||
return
|
||||
end
|
||||
local info = singeGetSystemInfo()
|
||||
local last = tonumber(saveGet("releaseCheckedAt", 0)) or 0
|
||||
|
||||
if not force and (os.time() - last) < RELEASE_CHECK_EVERY then
|
||||
MENU_RELEASE = saveGet("releaseOffered", nil)
|
||||
if type(MENU_RELEASE) ~= "table" then
|
||||
MENU_RELEASE = nil
|
||||
end
|
||||
menuRefreshNotice()
|
||||
return
|
||||
end
|
||||
masterRelease(info.platform, info.version, function(ok, reply)
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
saveSet("releaseCheckedAt", os.time())
|
||||
MENU_RELEASE = reply.newer and reply or nil
|
||||
saveSet("releaseOffered", MENU_RELEASE or false)
|
||||
menuRefreshNotice()
|
||||
end)
|
||||
end
|
||||
|
||||
-- An empty library still gets the menu: the service tools are how games are fetched.
|
||||
do
|
||||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||
|
||||
-- No disc: the menu is laid out on the canvas, 720x480 unless --canvas says otherwise, and the
|
||||
-- renderer that needs Singe/menuBackground.mkv loads it itself.
|
||||
--
|
||||
-- Three frames of Singe/menuBackground.mkv: where the menu may take the screen, where the
|
||||
-- grid section it loops begins, and its last frame. The first two are not the same moment --
|
||||
-- the menu comes up while the music is still fading out behind it, and only then does the
|
||||
-- picture start repeating. All three come from util/renderMenuVideo.py, which prints them
|
||||
-- when it makes the video; the grid section is a whole number of turns of the grid, so
|
||||
-- playing it over and over has no seam in it. A renderer that draws its own grid uses none
|
||||
-- of them: it knows where its own logo went.
|
||||
DISC_MENU_FRAME = 222
|
||||
DISC_GRID_START = 300
|
||||
DISC_LAST_FRAME = 419
|
||||
|
||||
SHUTDOWN_FROM_PUSH = false
|
||||
|
||||
-- The service tools share this click with the audio delay tool.
|
||||
SND_CLICK = soundLoad("Singe/click.wav")
|
||||
|
||||
-- The intro's sound. It belongs to the menu rather than to either renderer: one draws the
|
||||
-- intro and the other plays a recording of it, and both want the same sound over the top at
|
||||
-- the same moment. Keeping it out of the video is also what stops it being stored twice.
|
||||
SND_INTRO = soundLoad("Singe/menuIntro.flac")
|
||||
SND_INTRO_VOICE = SOUND_ERROR_INVALID
|
||||
|
||||
-- Load configuration
|
||||
SHOW_INTRO = true
|
||||
CONFIG_FILE = singeGetDataPath() .. "menu.dat"
|
||||
local confattr = lfs.attributes(CONFIG_FILE)
|
||||
local wanted = 1
|
||||
if confattr then
|
||||
dofile(CONFIG_FILE)
|
||||
-- A damaged menu.dat must not take the menu down with it. What it asks for is held here
|
||||
-- until gameSelect places it: nothing is selected yet, so there is nothing to let go of.
|
||||
wanted = math.min(tonumber(GAME_SELECTED) or 1, GAME_COUNT)
|
||||
end
|
||||
GAME_SELECTED = nil
|
||||
|
||||
-- The renderer. A GUI document is drawn through the GPU device the 3D scene uses, so without
|
||||
-- one there is no document to build and guiNew would end the script rather than answer false;
|
||||
-- the overlay renderer needs nothing but the overlay every game already draws into. Set
|
||||
-- MENU_OVERLAY = true in menu.dat to use it on a machine that has a GPU, which is how the
|
||||
-- overlay path gets tested.
|
||||
if singeHasGpu() and not MENU_OVERLAY then
|
||||
dofile("Singe/MenuDocument.singe")
|
||||
else
|
||||
dofile("Singe/MenuOverlay.singe")
|
||||
end
|
||||
MENU_RENDER.begin()
|
||||
menuCheckVersions()
|
||||
menuCheckRelease(false)
|
||||
MENU_RENDER.layout()
|
||||
MENU_RENDER.buildList()
|
||||
|
||||
MENU_RENDER.backdropBegin(SHOW_INTRO)
|
||||
if SHOW_INTRO then
|
||||
SND_INTRO_VOICE = soundPlay(SND_INTRO)
|
||||
end
|
||||
|
||||
-- Prime the pump
|
||||
gameSelect(wanted)
|
||||
end
|
||||
|
|
@ -1,461 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- The menu drawn as an RmlUi document: Singe/Menu.rml styled by Singe/menu.rcss, composited over
|
||||
-- the overlay, with the attract video drawn into the overlay underneath the hole the document
|
||||
-- leaves for it. This needs the GPU device the 3D scene uses; Singe/MenuOverlay.singe is the one
|
||||
-- for a machine without one. Singe/Menu.singe loads exactly one of the two and calls it through
|
||||
-- MENU_RENDER:
|
||||
--
|
||||
-- begin() build what drawing needs
|
||||
-- finish() give it back
|
||||
-- backdropBegin(showIntro) start the background
|
||||
-- backdrop() advance it, and clear the overlay ready to be drawn into
|
||||
-- backdropReady() the intro is over and the menu may take input
|
||||
-- layout() place the regions, from the overlay size
|
||||
-- buildList() the whole game list is known
|
||||
-- showGame(game) this is the selection now, or MENU_EMPTY when there are no games
|
||||
-- hideGame() let go of the last selection
|
||||
-- pageSize() how many rows left and right should move by
|
||||
-- frame() draw the game page
|
||||
-- tools() draw the service tools page
|
||||
|
||||
|
||||
-- Layout, in overlay pixels: the columns' widths and the space around them; everything else
|
||||
-- follows from the overlay size in layout(). The marquee and video keep the overlay menu's
|
||||
-- 320x75 and 320x200 proportions.
|
||||
dofile("Singe/Backdrop.singe")
|
||||
|
||||
|
||||
-- Layout constants follow.
|
||||
local MARGIN = 10
|
||||
local GAP = 8
|
||||
local LIST_W = 200
|
||||
local POSTER_W = 180
|
||||
local FOOTER_H = 24
|
||||
local MARQUEE_ASPECT_W = 320
|
||||
local MARQUEE_ASPECT_H = 75
|
||||
local VIDEO_ASPECT_W = 320
|
||||
local VIDEO_ASPECT_H = 200
|
||||
|
||||
|
||||
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||
local scrollPending = false
|
||||
local detailScroll = 0 -- How far down the description has walked, in pixels,
|
||||
local detailDown = true -- and which way it is walking.
|
||||
local detailClock = 0
|
||||
local toolsCardShown = false -- The document shows the service tools' card, not the game list.
|
||||
local SCROLL_HOLD = 3.0 -- Seconds it rests at each end before moving, as in the overlay
|
||||
local SCROLL_STEP = 1.1 -- renderer, and seconds a step while it is moving.
|
||||
local SCROLL_LINE = 16 -- How far a step goes: a line of the description.
|
||||
|
||||
|
||||
function menuElement(id)
|
||||
return rmlui.contexts["gui" .. GUI].documents["menu"]:GetElementById(id)
|
||||
end
|
||||
|
||||
|
||||
-- Text on its way into the document, with RML's special characters made harmless.
|
||||
function menuEscape(text)
|
||||
local output = tostring(text or "")
|
||||
|
||||
output = string.gsub(output, "&", "&")
|
||||
output = string.gsub(output, "<", "<")
|
||||
output = string.gsub(output, ">", ">")
|
||||
return output
|
||||
end
|
||||
|
||||
|
||||
-- Fits a game's picture inside one of the art regions. The document lives in Singe/, so a game's
|
||||
-- own file is reached from there through "../"; the engine's files are named as they are.
|
||||
--
|
||||
-- Art that is not there shows as art that is not there, which menuArtPath decides -- a games.dat
|
||||
-- naming a file that was never packed, or a path with a typo in it, is otherwise a silent hole in
|
||||
-- the page, and it used to be worse than silent: an empty path took the whole engine down through
|
||||
-- the texture loader. MENU_MISSING_ART is an engine file, so the "singe/" test below leaves it
|
||||
-- alone.
|
||||
local function menuArt(id, file)
|
||||
local path = menuArtPath(file)
|
||||
|
||||
if path:sub(1, 6):lower() ~= "singe/" then
|
||||
path = "../" .. path
|
||||
end
|
||||
menuElement(id).style.decorator = 'image("' .. path .. '" contain)'
|
||||
end
|
||||
|
||||
|
||||
-- One "Label: value" fragment of a details line.
|
||||
local function menuLabel(label, value)
|
||||
return '<span class="label">' .. label .. ':</span> ' .. menuEscape(value) .. ' '
|
||||
end
|
||||
|
||||
|
||||
local function menuPlace(id, x, y, w, h)
|
||||
local element = menuElement(id)
|
||||
|
||||
element:SetClass("placed", true)
|
||||
element.style.left = x .. "px"
|
||||
element.style.top = y .. "px"
|
||||
element.style.width = w .. "px"
|
||||
element.style.height = h .. "px"
|
||||
end
|
||||
|
||||
|
||||
local function menuRow(index)
|
||||
return menuElement("game" .. index)
|
||||
end
|
||||
|
||||
|
||||
-- A click on a row selects its game; a click on the selected row starts it.
|
||||
local function menuRowClick(gui, document, id, event, value)
|
||||
local index = tonumber(id:sub(5))
|
||||
|
||||
if not menuActive() then
|
||||
return
|
||||
end
|
||||
if index == GAME_SELECTED then
|
||||
gameStart()
|
||||
else
|
||||
gameSelect(index)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Brings the selected row into view once the document has been laid out.
|
||||
local function menuScrollToSelection()
|
||||
local list = menuElement("list")
|
||||
local row = menuRow(GAME_SELECTED)
|
||||
local top = row.offset_top - list.scroll_top
|
||||
|
||||
if list.client_height <= 0 then
|
||||
return
|
||||
end
|
||||
if top < 0 then
|
||||
row:ScrollIntoView(true)
|
||||
elseif top + row.offset_height > list.client_height then
|
||||
row:ScrollIntoView(false)
|
||||
end
|
||||
scrollPending = false
|
||||
end
|
||||
|
||||
|
||||
-- A tool lines its columns up with runs of spaces, which RML would collapse to one, so they are
|
||||
-- made non-breaking on the way in. Only the tools do this: a game's description is meant to wrap.
|
||||
local function menuSpaces(text)
|
||||
return (string.gsub(menuEscape(text), " +", function(run) return string.rep(" ", #run) end))
|
||||
end
|
||||
|
||||
|
||||
-- One row of a service tool, as RML. The kinds are the ones Singe/Tools.singe builds.
|
||||
local function toolRow(row)
|
||||
if row.kind == "pair" then
|
||||
return string.format("<div class='pair'><span class='label'>%s</span><span class='detail'>%s</span></div>",
|
||||
menuEscape(row.label), menuSpaces(row.value))
|
||||
elseif row.kind == "sub" then
|
||||
return "<div class='sub'>" .. menuSpaces(row.text) .. "</div>"
|
||||
elseif row.kind == "keys" then
|
||||
return "<p class='keys'>" .. menuSpaces(row.text) .. "</p>"
|
||||
elseif row.kind == "item" then
|
||||
local detail = row.detail and ("<span class='detail'>" .. menuSpaces(tostring(row.detail)) .. "</span>") or ""
|
||||
|
||||
return string.format("<div class='%s'><span class='text'>%s</span>%s</div>", row.selected and "item selected" or "item", menuSpaces(row.text), detail)
|
||||
elseif row.kind == "blank" then
|
||||
return "<div class='blank'></div>"
|
||||
end
|
||||
return "<div class='row'>" .. menuSpaces(row.text) .. "</div>"
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER = {}
|
||||
|
||||
|
||||
MENU_RENDER.begin = function()
|
||||
-- The document: the size of the overlay, composited over it every frame, and first in line
|
||||
-- for the mouse and the switches so its rows can be clicked.
|
||||
GUI = guiNew(overlayGetWidth(), overlayGetHeight())
|
||||
DOCUMENT = guiLoad(GUI, "Singe/Menu.rml")
|
||||
guiSetInput(GUI, true)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.finish = function()
|
||||
if GUI then
|
||||
guiDelete(GUI)
|
||||
GUI = nil
|
||||
end
|
||||
backdropFinish()
|
||||
sceneEnable(false)
|
||||
end
|
||||
|
||||
|
||||
-- The backdrop is its own file, because util/renderMenuVideo.py draws the same thing to record it.
|
||||
MENU_RENDER.backdropBegin = function(showIntro)
|
||||
backdropBegin(showIntro)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdrop = function()
|
||||
-- Transparent, so the scene behind the overlay is what shows.
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
backdropFrame()
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdropReady = function()
|
||||
-- The menu takes the screen once the logo has left, not when a video frame number passes.
|
||||
return backdropReady()
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.layout = function()
|
||||
local width = overlayGetWidth()
|
||||
local height = overlayGetHeight()
|
||||
local sideX = MARGIN + LIST_W + GAP + POSTER_W + GAP
|
||||
local sideW = width - MARGIN - sideX
|
||||
local listH = height - MARGIN * 2 - FOOTER_H - GAP
|
||||
local posterX = MARGIN + LIST_W + GAP
|
||||
local footerY = height - MARGIN - FOOTER_H
|
||||
local marqueeH = math.floor(sideW * MARQUEE_ASPECT_H / MARQUEE_ASPECT_W)
|
||||
local infoY = 0
|
||||
|
||||
videoX = sideX
|
||||
videoY = MARGIN + marqueeH + GAP
|
||||
videoW = sideW
|
||||
videoH = math.floor(sideW * VIDEO_ASPECT_H / VIDEO_ASPECT_W)
|
||||
infoY = videoY + videoH + GAP
|
||||
|
||||
menuPlace("list", MARGIN, MARGIN, LIST_W, listH)
|
||||
menuPlace("poster", posterX, MARGIN, POSTER_W, videoY + videoH - MARGIN)
|
||||
menuPlace("marquee", sideX, MARGIN, sideW, marqueeH)
|
||||
menuPlace("video", videoX, videoY, videoW, videoH)
|
||||
menuPlace("info", posterX, infoY, width - MARGIN - posterX, MARGIN + listH - infoY)
|
||||
menuPlace("footer", MARGIN, footerY, width - MARGIN * 2, FOOTER_H)
|
||||
end
|
||||
|
||||
|
||||
-- Fills the game list with one row per game and routes their clicks here.
|
||||
MENU_RENDER.buildList = function()
|
||||
local rows = {}
|
||||
|
||||
for index, game in ipairs(GAME_LIST) do
|
||||
local badge = menuUpdateFor(game) and '<span class="badge">update</span>' or ""
|
||||
|
||||
-- The title in a span of its own, so a long one can give way to the mark (menu.rcss).
|
||||
rows[index] = '<div id="game' .. index .. '"><span class="title">' .. menuEscape(game.TITLE) .. '</span>' .. badge .. '</div>'
|
||||
end
|
||||
menuElement("list").inner_rml = table.concat(rows)
|
||||
for index = 1, GAME_COUNT do
|
||||
guiSetHandler(GUI, DOCUMENT, "game" .. index, "click", menuRowClick)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The service answered about versions, or an update landed: the badges and the notice change.
|
||||
MENU_RENDER.refreshList = function()
|
||||
MENU_RENDER.buildList()
|
||||
if GAME_SELECTED then
|
||||
menuRow(GAME_SELECTED):SetClass("selected", true)
|
||||
menuRefreshNotice()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.notice = function(text)
|
||||
guiSetValue(GUI, DOCUMENT, "notice", menuEscape(text or ""))
|
||||
end
|
||||
|
||||
|
||||
-- MENU_EMPTY, the page for an empty library, has no row, no art, and no facts: only its title and
|
||||
-- what to do.
|
||||
MENU_RENDER.showGame = function(game)
|
||||
if game.EMPTY then
|
||||
menuElement("poster").style.decorator = "none"
|
||||
menuElement("marquee").style.decorator = "none"
|
||||
guiSetValue(GUI, DOCUMENT, "credits", "")
|
||||
guiSetValue(GUI, DOCUMENT, "developer", "")
|
||||
guiSetValue(GUI, DOCUMENT, "publisher", "")
|
||||
guiSetValue(GUI, DOCUMENT, "port", "")
|
||||
else
|
||||
menuRow(GAME_SELECTED):SetClass("selected", true)
|
||||
scrollPending = true
|
||||
menuArt("poster", game.CABINET)
|
||||
menuArt("marquee", game.MARQUEE)
|
||||
guiSetValue(GUI, DOCUMENT, "credits", menuLabel("Year", game.YEAR) .. menuLabel("Genre", game.GENRE) .. menuLabel("Platform", game.PLATFORM))
|
||||
guiSetValue(GUI, DOCUMENT, "developer", menuLabel("Developer", game.DEVELOPER))
|
||||
guiSetValue(GUI, DOCUMENT, "publisher", menuLabel("Publisher", game.PUBLISHER))
|
||||
guiSetValue(GUI, DOCUMENT, "port", menuLabel("Singe port", game.CREATOR) .. menuLabel("Source", game.SOURCE))
|
||||
end
|
||||
guiSetValue(GUI, DOCUMENT, "title", menuEscape(game.TITLE))
|
||||
guiSetValue(GUI, DOCUMENT, "description", menuEscape(game.DESCRIPTION))
|
||||
menuElement("description").scroll_top = 0
|
||||
detailScroll = 0
|
||||
detailDown = true
|
||||
detailClock = 0
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.hideGame = function()
|
||||
-- The document keeps nothing of its own per game; only the highlight has to come off.
|
||||
if GAME_SELECTED then
|
||||
menuRow(GAME_SELECTED):SetClass("selected", false)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Rows that fit in the list at once: what left and right page by.
|
||||
MENU_RENDER.pageSize = function()
|
||||
local list = menuElement("list")
|
||||
local row = menuRow(GAME_SELECTED)
|
||||
|
||||
if row.offset_height <= 0 then
|
||||
return 1
|
||||
end
|
||||
return math.max(1, math.floor(list.client_height / row.offset_height))
|
||||
end
|
||||
|
||||
|
||||
-- The description walks by itself when there is more of it than fits -- down to the end, a rest,
|
||||
-- and back up to the start -- the same way the overlay renderer's details do. The style sheet
|
||||
-- gives this box a scroll bar, but nothing drives one from a joystick and up and down belong to the
|
||||
-- game list, so a long description was simply unreadable past its first few lines.
|
||||
local function menuScrollDetails()
|
||||
local element = menuElement("description")
|
||||
local room = element.scroll_height - element.client_height
|
||||
|
||||
if room <= 0 then
|
||||
return
|
||||
end
|
||||
-- The clock starts when there is something to scroll and the menu is on screen, not when the
|
||||
-- game was selected: the menu is behind the intro for the first seven seconds.
|
||||
if detailClock == 0 then
|
||||
detailClock = singeGetTicks()
|
||||
end
|
||||
|
||||
local resting = (detailScroll <= 0) or (detailScroll >= room)
|
||||
|
||||
if (singeGetTicks() - detailClock) >= ((resting and SCROLL_HOLD or SCROLL_STEP) * 1000) then
|
||||
if detailDown and (detailScroll >= room) then
|
||||
detailDown = false
|
||||
elseif not detailDown and (detailScroll <= 0) then
|
||||
detailDown = true
|
||||
end
|
||||
detailScroll = math.max(math.min(detailScroll + (detailDown and SCROLL_LINE or -SCROLL_LINE), room), 0)
|
||||
detailClock = singeGetTicks()
|
||||
element.scroll_top = detailScroll
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Page up and page down, by as much as is on show less a line to read across. The walk resumes
|
||||
-- from where this leaves it, in the direction it was sent, after its usual rest.
|
||||
MENU_RENDER.scrollDetails = function(direction)
|
||||
local element = menuElement("description")
|
||||
local room = element.scroll_height - element.client_height
|
||||
|
||||
if room > 0 then
|
||||
local page = math.max(element.client_height - SCROLL_LINE, SCROLL_LINE)
|
||||
|
||||
detailScroll = math.max(math.min(detailScroll + direction * page, room), 0)
|
||||
detailDown = (direction > 0)
|
||||
detailClock = singeGetTicks()
|
||||
element.scroll_top = detailScroll
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.frame = function()
|
||||
-- Back from the service tools: the document still shows their card, since only the tools page
|
||||
-- swaps the two, and it stops being drawn the moment the tools close. The game list comes back
|
||||
-- here, on the first frame of the game page.
|
||||
if toolsCardShown then
|
||||
toolsCardShown = false
|
||||
menuElement("tool"):SetClass("hidden", true)
|
||||
menuElement("games"):SetClass("hidden", false)
|
||||
end
|
||||
-- Attract Mode Video, under the hole the document leaves for it
|
||||
if VIDEO_ATTRACT then
|
||||
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
||||
end
|
||||
if scrollPending then
|
||||
menuScrollToSelection()
|
||||
end
|
||||
menuScrollDetails()
|
||||
guiDraw(GUI)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.tools = function()
|
||||
local rows = {}
|
||||
|
||||
-- The tools redraw only when they say something changed; the document keeps what it was given.
|
||||
if TOOL_DIRTY then
|
||||
TOOL_DIRTY = false
|
||||
-- The same window the overlay renderer draws, so the two show the same page rather than one
|
||||
-- of them quietly showing more. The style sheet can scroll this box, but nothing drives a
|
||||
-- scroll bar from a joystick: windowing is what actually keeps the chosen row on screen.
|
||||
local visible, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
||||
-- The key hints go to the card's footer and a note to the column beside the list;
|
||||
-- everything else is the body.
|
||||
local keys = {}
|
||||
local note = {}
|
||||
|
||||
if more then
|
||||
rows[#rows + 1] = "<div class='sub'>...</div>"
|
||||
end
|
||||
local seenItem = false
|
||||
for _, row in ipairs(visible) do
|
||||
-- The marker belongs under the last item rather than under the key hints below it.
|
||||
if below and seenItem and row.kind ~= "item" then
|
||||
rows[#rows + 1] = "<div class='sub'>...</div>"
|
||||
below = false
|
||||
end
|
||||
if row.kind == "item" then
|
||||
seenItem = true
|
||||
end
|
||||
if row.kind == "keys" then
|
||||
keys[#keys + 1] = toolRow(row)
|
||||
elseif row.kind == "note" then
|
||||
note[#note + 1] = toolRow(row)
|
||||
else
|
||||
rows[#rows + 1] = toolRow(row)
|
||||
end
|
||||
end
|
||||
if below then
|
||||
rows[#rows + 1] = "<div class='sub'>...</div>"
|
||||
end
|
||||
guiSetValue(GUI, DOCUMENT, "toolTitle", menuEscape(TOOL_TITLE))
|
||||
guiSetValue(GUI, DOCUMENT, "toolBody", table.concat(rows))
|
||||
guiSetValue(GUI, DOCUMENT, "toolNote", table.concat(note))
|
||||
guiSetValue(GUI, DOCUMENT, "toolKeys", table.concat(keys))
|
||||
menuElement("toolNote"):SetClass("hidden", #note == 0)
|
||||
menuElement("games"):SetClass("hidden", toolsActive())
|
||||
menuElement("tool"):SetClass("hidden", not toolsActive())
|
||||
toolsCardShown = toolsActive()
|
||||
menuElement("tool"):SetClass("clear", TOOL_CLEAR)
|
||||
menuElement("tool"):SetClass("flash", TOOL_FLASH)
|
||||
end
|
||||
guiDraw(GUI)
|
||||
end
|
||||
|
|
@ -1,724 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- The menu drawn with sprites and fontPrint straight into the overlay: the look Singe 2 had, and
|
||||
-- the one that works on a machine whose GPU device never came up, where there is no GUI document to
|
||||
-- build. Singe/MenuDocument.singe is the other renderer; Singe/Menu.singe loads exactly one of the
|
||||
-- two and calls it through MENU_RENDER. See the top of MenuDocument.singe for what is in it.
|
||||
--
|
||||
-- One game is on screen at a time, as it always was here, so there is no list to page through
|
||||
-- visually: up and down move one game, left and right move a page, and the line above the details
|
||||
-- says where in the library the selection is. An entry with more to say than fits walks down by
|
||||
-- itself a line at a time, rests, and walks back up again: up and down belong to the library, and
|
||||
-- a cabinet has no key to spare for scrolling a paragraph. Page up and page down override it for
|
||||
-- whoever does have a keyboard.
|
||||
|
||||
|
||||
local MARGIN = 10 -- Around the page, and between the regions on it.
|
||||
local GAP = 8
|
||||
local HINT_H = 24 -- The line of key hints along the bottom.
|
||||
local SIDE_FRAC = 0.40 -- How much of the width the marquee and the attract video take.
|
||||
local MARQUEE_ASPECT_W = 320 -- What the artwork and the clips are authored at; both regions keep
|
||||
local MARQUEE_ASPECT_H = 75 -- these proportions whatever the overlay's size turns out to be.
|
||||
local VIDEO_ASPECT_W = 320
|
||||
local VIDEO_ASPECT_H = 200
|
||||
local LINE_HEIGHT = 24
|
||||
local LABEL_W = 150
|
||||
local SUB_INDENT = 20
|
||||
local CARD_PAD = 14 -- The card the tools draw on: inside its border to its text,
|
||||
local TITLE_H = 36 -- its title bar, and its footer of key hints, one line each plus
|
||||
local KEY_LINE = 18 -- its padding. The colors are the game page's: the panel, the
|
||||
local KEY_PAD = 6 -- selection's gradient, the blue-gray border, the gold, the quiet
|
||||
local BORDER = 2 -- blue-gray of a label.
|
||||
local ITEM_INSET = 8
|
||||
local CLEAR_FRAC = 0.70 -- How wide a strip a page that lets the overlay through may take.
|
||||
local NOTE_FRAC = 0.42 -- How much of the card a note beside the list takes.
|
||||
local PANEL = { 24, 38, 64, 216 }
|
||||
local PANEL_DEEP = { 12, 20, 36, 216 }
|
||||
local FOOT = { 16, 24, 40, 208 }
|
||||
local TITLE_BAR = { 51, 80, 122, 255 }
|
||||
local TITLE_BAR_L = { 74, 110, 168, 255 }
|
||||
local EDGE = { 106, 134, 184, 255 }
|
||||
local GOLD = { 255, 211, 90, 255 }
|
||||
local INK = { 240, 240, 240, 255 }
|
||||
local QUIET = { 143, 162, 196, 255 }
|
||||
local SOFT = { 200, 210, 230, 255 }
|
||||
local PAGE_ROWS = 10
|
||||
local WRAP_BREAK = " [!wb!] "
|
||||
local HINTS = "Up / Down: select Left / Right: page PgUp / PgDn: details Start or button: play Service: tools"
|
||||
|
||||
local font = nil
|
||||
local hintFont = nil -- Smaller: the hints are a footnote, and at the body's size the line
|
||||
-- of them ran off the end of a 720 pixel overlay.
|
||||
local titleFont = nil -- Larger: a tool's title on its bar.
|
||||
local introDone = false
|
||||
local backdrop = nil -- Singe/menuBackground.mkv, played here rather than as the menu's disc
|
||||
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||
local marqueeX, marqueeY, marqueeW, marqueeH = 0, 0, 0, 0
|
||||
local textX, textY, textW, textH = 0, 0, 0, 0
|
||||
local hintsX, hintsY = 0, 0
|
||||
local cabinetX, cabinetY, cabinetW, cabinetH = 0, 0, 0, 0
|
||||
local spriteCabinet = nil
|
||||
local spriteMarquee = nil
|
||||
local textSprites = {}
|
||||
local textShadows = {}
|
||||
local noticeText = nil -- One line under the details: an update on offer, or how a fetch is going.
|
||||
local textFirst = 0 -- The first line of the details on show, which walks by itself.
|
||||
local textDown = true -- and which way it is walking.
|
||||
local textClock = 0
|
||||
local SHADOW = 2 -- How far the shadow sits down and across from the text.
|
||||
local SCROLL_HOLD = 3.0 -- Seconds the details rest at each end before moving.
|
||||
local SCROLL_STEP = 1.1 -- and seconds a line while they are moving.
|
||||
local textHeight = 0
|
||||
local textLimit = 0
|
||||
|
||||
|
||||
-- A games.dat entry need not carry every field, and one that is missing must not take the menu
|
||||
-- down; the document renderer is tolerant the same way, through menuEscape.
|
||||
local function field(value)
|
||||
return tostring(value or "")
|
||||
end
|
||||
|
||||
|
||||
-- Shrinks a sprite to fit inside a region, keeping its proportions. spriteDraw blits at the
|
||||
-- sprite's own size and a games.dat's artwork is whatever size the person who drew it chose, so
|
||||
-- without this a big cabinet picture is drawn straight over whatever the page put beside it. Only
|
||||
-- ever smaller: blowing a small picture up to fill the space makes it soft for nothing.
|
||||
local function fitSprite(handle, boxW, boxH)
|
||||
local width = spriteGetWidth(handle)
|
||||
local height = spriteGetHeight(handle)
|
||||
|
||||
if (width > 0) and (height > 0) then
|
||||
local scale = math.min(boxW / width, boxH / height)
|
||||
|
||||
if scale < 1.0 then
|
||||
spriteScale(handle, scale)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Splits text into the lines that fit the width given, measuring with the selected font. The only
|
||||
-- way to measure a string is to render it, so this is slow enough to be worth doing once when
|
||||
-- something changes rather than once a frame. WRAP_BREAK is a word the caller puts in to force a
|
||||
-- line ending.
|
||||
--
|
||||
-- The space between words is kept as it was written rather than squeezed to one, because a tool
|
||||
-- lines its columns up with runs of spaces and there is no other way here to say "a wide gap".
|
||||
local function fitLines(text, maxWidth)
|
||||
local lines = {}
|
||||
local line = ""
|
||||
local pending = ""
|
||||
local trimBreak = utilTrim(WRAP_BREAK)
|
||||
|
||||
for word, space in (text .. WRAP_BREAK):gmatch("(%S+)(%s*)") do
|
||||
if word == trimBreak then
|
||||
lines[#lines + 1] = line
|
||||
line = ""
|
||||
pending = ""
|
||||
else
|
||||
local candidate = line .. pending .. word
|
||||
local measured = fontToSprite(candidate)
|
||||
local tooWide = spriteGetWidth(measured) > maxWidth
|
||||
spriteUnload(measured)
|
||||
if tooWide and line ~= "" then
|
||||
lines[#lines + 1] = line
|
||||
line = word
|
||||
else
|
||||
line = candidate
|
||||
end
|
||||
pending = space
|
||||
end
|
||||
end
|
||||
if line ~= "" then
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
|
||||
-- The service tools, laid out. Each entry is one line of the page and each line is a list of
|
||||
-- pieces: where the piece starts, what it says and which color it wants. The colors are roles
|
||||
-- rather than values because the audio delay tool inverts the whole page for a frame, and fontPrint
|
||||
-- takes the color at the moment it draws -- unlike fontToSprite, which bakes it in.
|
||||
local toolLines = {}
|
||||
local bodyX, bodyW = 0, 0 -- The card's body, where wrapped text is measured against.
|
||||
|
||||
|
||||
local function toolLine(pieces)
|
||||
toolLines[#toolLines + 1] = pieces
|
||||
end
|
||||
|
||||
|
||||
-- Text too long for the page wraps under itself rather than running off the edge.
|
||||
local function toolWrapped(text, x, role)
|
||||
if text == nil or text == "" then
|
||||
-- Still a line: a tool that says nothing this frame should not have its page jump about.
|
||||
toolLine({})
|
||||
return
|
||||
end
|
||||
for _, line in ipairs(fitLines(text, bodyX + bodyW - x)) do
|
||||
toolLine({ { x = x, text = line, role = role } })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The card the tools draw on: its edges, and where its body and its columns begin. A page that
|
||||
-- lets the overlay through has no card; its strip is measured once its lines are known.
|
||||
local toolKeyLines = {} -- The key hints, for the card's footer.
|
||||
local toolNoteLines = {} -- What the chosen tool is for, beside the list of tools.
|
||||
local cardX, cardY, cardW, cardH = 0, 0, 0, 0
|
||||
local noteX, noteW = 0, 0
|
||||
|
||||
|
||||
local function toolNote(pieces)
|
||||
toolNoteLines[#toolNoteLines + 1] = pieces
|
||||
end
|
||||
|
||||
|
||||
local function toolsLayout()
|
||||
local width = overlayGetWidth()
|
||||
local height = overlayGetHeight()
|
||||
-- Only the rows that fit, with the chosen one kept in view; a catalogue is as long as the
|
||||
-- service is big and would otherwise run off the bottom of the screen.
|
||||
local rows, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
||||
|
||||
cardX, cardY, cardW, cardH = MARGIN, MARGIN, width - MARGIN * 2, height - MARGIN * 2
|
||||
if TOOL_CLEAR then
|
||||
cardW = math.floor(width * CLEAR_FRAC)
|
||||
cardX = math.floor((width - cardW) / 2)
|
||||
end
|
||||
bodyX = cardX + BORDER + CARD_PAD
|
||||
bodyW = cardW - (BORDER + CARD_PAD) * 2
|
||||
local fullW = bodyW
|
||||
-- A page with a note has a second column for it beside the list.
|
||||
noteX, noteW = nil, 0
|
||||
for _, row in ipairs(rows) do
|
||||
if row.kind == "note" then
|
||||
noteW = math.floor(cardW * NOTE_FRAC) - (BORDER + CARD_PAD) * 2
|
||||
noteX = cardX + cardW - BORDER - CARD_PAD - noteW
|
||||
bodyW = noteX - CARD_PAD - BORDER - bodyX
|
||||
end
|
||||
end
|
||||
local valueX = bodyX + LABEL_W
|
||||
|
||||
toolLines = {}
|
||||
toolKeyLines = {}
|
||||
toolNoteLines = {}
|
||||
if more then
|
||||
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
||||
end
|
||||
local seenItem = false
|
||||
for _, row in ipairs(rows) do
|
||||
-- The "more below" marker belongs immediately under the last item, not at the very bottom
|
||||
-- of the page under the key hints, which are not part of the list.
|
||||
if below and seenItem and row.kind ~= "item" then
|
||||
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
||||
below = false
|
||||
end
|
||||
if row.kind == "item" then
|
||||
seenItem = true
|
||||
end
|
||||
if row.kind == "blank" then
|
||||
toolLine({})
|
||||
elseif row.kind == "pair" then
|
||||
-- The label and the first line of the value share a line; the rest of the value wraps
|
||||
-- under itself, in its own column.
|
||||
local lines = fitLines(row.value, bodyX + bodyW - valueX)
|
||||
toolLine({ { x = bodyX, text = row.label, role = "quiet" },
|
||||
{ x = valueX, text = lines[1] or "", role = "ink" } })
|
||||
for index = 2, #lines do
|
||||
toolLine({ { x = valueX, text = lines[index], role = "ink" } })
|
||||
end
|
||||
elseif row.kind == "sub" then
|
||||
for _, line in ipairs(fitLines(row.text, bodyW - SUB_INDENT)) do
|
||||
toolLine({ { x = bodyX + SUB_INDENT, text = line, role = "soft" } })
|
||||
end
|
||||
elseif row.kind == "keys" then
|
||||
-- The footer, in the smaller font, across the whole card.
|
||||
fontSelect(hintFont)
|
||||
for _, line in ipairs(fitLines(row.text, fullW)) do
|
||||
toolKeyLines[#toolKeyLines + 1] = line
|
||||
end
|
||||
fontSelect(font)
|
||||
elseif row.kind == "item" then
|
||||
local pieces = { { x = bodyX + ITEM_INSET, text = row.text, role = row.selected and "pick" or "ink", bar = row.selected } }
|
||||
|
||||
-- The detail against the right edge of the row, so a column of them lines up. An
|
||||
-- empty one -- a field not filled in yet -- is nothing to measure or draw.
|
||||
local detail = row.detail and tostring(row.detail) or ""
|
||||
|
||||
if detail ~= "" then
|
||||
local measured = fontToSprite(detail)
|
||||
|
||||
pieces[#pieces + 1] = { x = bodyX + bodyW - ITEM_INSET - spriteGetWidth(measured), text = detail, role = pieces[1].role }
|
||||
spriteUnload(measured)
|
||||
end
|
||||
toolLine(pieces)
|
||||
elseif row.kind == "note" then
|
||||
for _, line in ipairs(fitLines(row.text, noteW)) do
|
||||
toolNote({ { x = noteX, text = line, role = "soft" } })
|
||||
end
|
||||
else
|
||||
toolWrapped(row.text, bodyX, "ink")
|
||||
end
|
||||
end
|
||||
-- A list that runs to the very end of the page has no row after it to hang the marker on.
|
||||
if below then
|
||||
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
||||
end
|
||||
-- A strip along the bottom is as tall as its lines.
|
||||
if TOOL_CLEAR then
|
||||
cardH = BORDER * 2 + TITLE_H + CARD_PAD + #toolLines * LINE_HEIGHT + CARD_PAD + KEY_PAD * 2 + #toolKeyLines * KEY_LINE
|
||||
cardY = height - MARGIN - cardH
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER = {}
|
||||
|
||||
|
||||
MENU_RENDER.begin = function()
|
||||
font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
hintFont = fontLoad("Singe/FreeSansBold.ttf", 13)
|
||||
titleFont = fontLoad("Singe/FreeSansBold.ttf", 22)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
fontSelect(font)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.finish = function()
|
||||
for _, handle in ipairs({ font, hintFont, titleFont }) do
|
||||
if handle then
|
||||
fontUnload(handle)
|
||||
end
|
||||
end
|
||||
font, hintFont, titleFont = nil, nil, nil
|
||||
if backdrop then
|
||||
videoUnload(backdrop)
|
||||
backdrop = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The backdrop is a recording: the engine intro, then the grid section played over and over. A
|
||||
-- machine with no GPU device has nothing to draw a grid with, so this renderer plays the one in
|
||||
-- Singe/menuBackground.mkv; Singe/MenuDocument.singe draws its own instead. It is loaded here
|
||||
-- rather than handed to the menu as its disc, so the renderer that draws its own never opens it.
|
||||
MENU_RENDER.backdropBegin = function(showIntro)
|
||||
introDone = not showIntro
|
||||
-- No GPU device, no 3D: tier 0, which is all this machine will ever measure (PLAN.md section 57).
|
||||
if singeCapabilities().tier ~= 0 then
|
||||
singeSaveTier(0)
|
||||
end
|
||||
backdrop = videoLoad("Singe/menuBackground.mkv")
|
||||
videoPlay(backdrop)
|
||||
if not showIntro then
|
||||
videoSeek(backdrop, DISC_GRID_START)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdrop = function()
|
||||
-- Loop the grid. A machine too slow to show every frame can step over the last one, after which
|
||||
-- the player starts the video again from the top; once the intro has played, that is a loop.
|
||||
local frame = videoGetFrame(backdrop)
|
||||
|
||||
if frame >= DISC_LAST_FRAME or (introDone and frame < DISC_GRID_START) then
|
||||
videoSeek(backdrop, DISC_GRID_START)
|
||||
end
|
||||
if frame >= DISC_GRID_START then
|
||||
introDone = true
|
||||
end
|
||||
|
||||
-- The recording fills the overlay, and everything else is drawn over it. Cleared see-through
|
||||
-- rather than black, which looks the same under a picture that covers everything, so the engine
|
||||
-- can have the GPU draw the recording instead of copying every frame of it into the overlay.
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
videoDraw(backdrop, 0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdropReady = function()
|
||||
-- The menu comes up where the drawn backdrop's logo leaves, which is before the picture starts
|
||||
-- repeating: the frames between the two are the grid running on while the music fades out.
|
||||
return videoGetFrame(backdrop) >= DISC_MENU_FRAME
|
||||
end
|
||||
|
||||
|
||||
-- The cabinet fills the left; the marquee, the attract video and the details stack down the right.
|
||||
-- The page: the cabinet on the left, the marquee and the attract video down the right, the game's
|
||||
-- details across the bottom and the key hints under them. The details used to have the video's
|
||||
-- column and whatever height was left over, which at 720x480 was four lines -- the description and
|
||||
-- about one field -- so most of what a games.dat says never reached the screen at all. They get
|
||||
-- the full width now, and the fields are grouped the way the document renderer groups them, so the
|
||||
-- whole entry fits. Everything is worked out from the overlay's size rather than written down,
|
||||
-- because a game may set any size it likes.
|
||||
MENU_RENDER.layout = function()
|
||||
local width = overlayGetWidth()
|
||||
local height = overlayGetHeight()
|
||||
local sideW = math.floor(width * SIDE_FRAC)
|
||||
local sideX = width - MARGIN - sideW
|
||||
|
||||
marqueeW = sideW
|
||||
marqueeH = math.floor(sideW * MARQUEE_ASPECT_H / MARQUEE_ASPECT_W)
|
||||
marqueeX = sideX
|
||||
marqueeY = MARGIN
|
||||
|
||||
videoW = sideW
|
||||
videoH = math.floor(sideW * VIDEO_ASPECT_H / VIDEO_ASPECT_W)
|
||||
videoX = sideX
|
||||
videoY = marqueeY + marqueeH + GAP
|
||||
|
||||
hintsX = MARGIN
|
||||
hintsY = height - MARGIN - HINT_H
|
||||
|
||||
textX = MARGIN
|
||||
textY = videoY + videoH + GAP
|
||||
textW = width - MARGIN - textX
|
||||
textH = hintsY - GAP - textY
|
||||
|
||||
cabinetX = MARGIN
|
||||
cabinetY = MARGIN
|
||||
cabinetW = sideX - GAP - cabinetX
|
||||
cabinetH = textY - GAP - cabinetY
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.buildList = function()
|
||||
-- One game is on screen at a time; there is no list to build.
|
||||
end
|
||||
|
||||
|
||||
-- The service answered about versions, or an update landed. The badge is read every frame, so
|
||||
-- only the notice has to be brought up to date.
|
||||
MENU_RENDER.refreshList = function()
|
||||
menuRefreshNotice()
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.notice = function(text)
|
||||
noticeText = text
|
||||
end
|
||||
|
||||
|
||||
-- MENU_EMPTY, the page for an empty library, has no art and no facts: only what to do.
|
||||
MENU_RENDER.showGame = function(game)
|
||||
local textBox = field(game.DESCRIPTION)
|
||||
|
||||
if not game.EMPTY then
|
||||
spriteCabinet = spriteLoad(menuArtPath(game.CABINET))
|
||||
spriteMarquee = spriteLoad(menuArtPath(game.MARQUEE))
|
||||
fitSprite(spriteCabinet, cabinetW, cabinetH)
|
||||
fitSprite(spriteMarquee, marqueeW, marqueeH)
|
||||
-- The details, grouped the way the document renderer groups them: the short fields share a
|
||||
-- line, and the description comes last because it is the one that can run on. It used to
|
||||
-- come first, which meant a long description pushed every fact about the game off the bottom.
|
||||
textBox = "Year: " .. field(game.YEAR) .. " Genre: " .. field(game.GENRE) ..
|
||||
" Platform: " .. field(game.PLATFORM) .. WRAP_BREAK ..
|
||||
"Developer: " .. field(game.DEVELOPER) .. WRAP_BREAK ..
|
||||
"Publisher: " .. field(game.PUBLISHER) .. WRAP_BREAK ..
|
||||
"Singe port: " .. field(game.CREATOR) .. " Source: " .. field(game.SOURCE) .. WRAP_BREAK ..
|
||||
WRAP_BREAK .. textBox
|
||||
end
|
||||
-- fontToSprite bakes the color in, so it is set before the sprites are made rather than
|
||||
-- before they are drawn.
|
||||
-- Twice over: black first, drawn a couple of pixels down and across behind the white, because
|
||||
-- this page sits on a moving grid and white on magenta is hard to read. The document renderer
|
||||
-- puts a translucent panel behind its text; nothing here can fill a rectangle, and a shadow
|
||||
-- costs one more blit a line rather than one more surface a frame.
|
||||
local lines = fitLines(textBox, textW)
|
||||
|
||||
colorForeground(0, 0, 0, 255)
|
||||
for _, line in ipairs(lines) do
|
||||
textShadows[#textShadows + 1] = (string.len(line) > 0) and fontToSprite(line) or -1
|
||||
end
|
||||
colorForeground(255, 255, 255, 255)
|
||||
for _, line in ipairs(lines) do
|
||||
textSprites[#textSprites + 1] = (string.len(line) > 0) and fontToSprite(line) or -1
|
||||
end
|
||||
|
||||
-- How tall a line is, and how many of them fit under the title. Measured here rather than
|
||||
-- part way through drawing, because the scroll has to know before the first line is drawn.
|
||||
textHeight = 0
|
||||
for _, handle in ipairs(textSprites) do
|
||||
if (textHeight == 0) and (handle >= 0) then
|
||||
textHeight = spriteGetHeight(handle) + 1
|
||||
end
|
||||
end
|
||||
textLimit = (textHeight > 0) and math.max(math.floor((textH - LINE_HEIGHT) / textHeight), 1) or 1
|
||||
textFirst = 0
|
||||
textDown = true
|
||||
textClock = 0
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.hideGame = function()
|
||||
if spriteCabinet then
|
||||
spriteUnload(spriteCabinet)
|
||||
spriteCabinet = nil
|
||||
end
|
||||
if spriteMarquee then
|
||||
spriteUnload(spriteMarquee)
|
||||
spriteMarquee = nil
|
||||
end
|
||||
for _, handle in ipairs(textSprites) do
|
||||
if handle >= 0 then
|
||||
spriteUnload(handle)
|
||||
end
|
||||
end
|
||||
for _, handle in ipairs(textShadows) do
|
||||
if handle >= 0 then
|
||||
spriteUnload(handle)
|
||||
end
|
||||
end
|
||||
textSprites = {}
|
||||
textShadows = {}
|
||||
end
|
||||
|
||||
|
||||
-- Page up and page down, by as much as is on show less a line to read across. The walk resumes
|
||||
-- from where this leaves it, in the direction it was sent, after its usual rest.
|
||||
MENU_RENDER.scrollDetails = function(direction)
|
||||
local room = #textSprites - textLimit
|
||||
|
||||
if room > 0 then
|
||||
textFirst = math.max(math.min(textFirst + direction * math.max(textLimit - 1, 1), room), 0)
|
||||
textDown = (direction > 0)
|
||||
textClock = singeGetTicks()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.pageSize = function()
|
||||
return PAGE_ROWS
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.frame = function()
|
||||
local x = 0
|
||||
local y = 0
|
||||
|
||||
-- Cabinet and marquee images, which an empty library has none of
|
||||
if spriteCabinet then
|
||||
x = cabinetX + (cabinetW - spriteGetWidth(spriteCabinet)) * 0.5
|
||||
y = cabinetY + (cabinetH - spriteGetHeight(spriteCabinet)) * 0.5
|
||||
spriteDraw(spriteCabinet, x, y)
|
||||
x = marqueeX + (marqueeW - spriteGetWidth(spriteMarquee)) * 0.5
|
||||
y = marqueeY + (marqueeH - spriteGetHeight(spriteMarquee)) * 0.5
|
||||
spriteDraw(spriteMarquee, x, y)
|
||||
end
|
||||
|
||||
-- Attract mode video
|
||||
if VIDEO_ATTRACT then
|
||||
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
||||
end
|
||||
|
||||
-- Which game this is, since there is no list to look at. It heads the details rather than
|
||||
-- sitting over the cabinet artwork, where it was unreadable against whatever the picture was.
|
||||
local title = MENU_EMPTY.TITLE
|
||||
|
||||
if GAME_SELECTED then
|
||||
title = string.format("%s (%d of %d)", field(GAME_LIST[GAME_SELECTED].TITLE), GAME_SELECTED, GAME_COUNT)
|
||||
if menuUpdateFor(GAME_LIST[GAME_SELECTED]) then
|
||||
title = title .. " [update]"
|
||||
end
|
||||
end
|
||||
|
||||
colorForeground(0, 0, 0, 255)
|
||||
fontPrint(textX + SHADOW, textY + SHADOW, title)
|
||||
colorForeground(255, 211, 90, 255)
|
||||
fontPrint(textX, textY, title)
|
||||
|
||||
-- What the game is. More of it than fits walks a line at a time, down to the end and back up
|
||||
-- to the start, so everything an entry says is seen without a key to scroll it -- and without
|
||||
-- the jump back to the top that reading the last line used to end in.
|
||||
local room = #textSprites - textLimit
|
||||
|
||||
-- The clock starts when the details are first drawn, not when they were built: the menu is
|
||||
-- behind the intro for the first seven seconds, and a page that had already scrolled itself by
|
||||
-- the time anybody saw it would appear to start half way down.
|
||||
if textClock == 0 then
|
||||
textClock = singeGetTicks()
|
||||
end
|
||||
if room > 0 then
|
||||
local resting = (textFirst <= 0) or (textFirst >= room)
|
||||
|
||||
if (singeGetTicks() - textClock) >= ((resting and SCROLL_HOLD or SCROLL_STEP) * 1000) then
|
||||
if textDown and (textFirst >= room) then
|
||||
textDown = false
|
||||
elseif not textDown and (textFirst <= 0) then
|
||||
textDown = true
|
||||
end
|
||||
textFirst = math.max(math.min(textFirst + (textDown and 1 or -1), room), 0)
|
||||
textClock = singeGetTicks()
|
||||
end
|
||||
end
|
||||
|
||||
y = textY + LINE_HEIGHT
|
||||
for index = textFirst + 1, math.min(textFirst + textLimit, #textSprites) do
|
||||
if textSprites[index] >= 0 then
|
||||
spriteDraw(textShadows[index], textX + SHADOW, y + SHADOW)
|
||||
spriteDraw(textSprites[index], textX, y)
|
||||
end
|
||||
y = y + textHeight
|
||||
end
|
||||
|
||||
-- Which key does what. The document renderer says the same thing along its footer; here
|
||||
-- there was nothing at all, so the only way to learn that up and down move one game and left
|
||||
-- and right move a page was to be told.
|
||||
fontSelect(hintFont)
|
||||
if noticeText and noticeText ~= "" then
|
||||
colorForeground(0, 0, 0, 255)
|
||||
fontPrint(hintsX + SHADOW, hintsY - LINE_HEIGHT + SHADOW, noticeText)
|
||||
colorForeground(255, 211, 90, 255)
|
||||
fontPrint(hintsX, hintsY - LINE_HEIGHT, noticeText)
|
||||
end
|
||||
colorForeground(0, 0, 0, 255)
|
||||
fontPrint(hintsX + SHADOW, hintsY + SHADOW, HINTS)
|
||||
colorForeground(178, 196, 224, 255)
|
||||
fontPrint(hintsX, hintsY, HINTS)
|
||||
fontSelect(font)
|
||||
end
|
||||
|
||||
|
||||
-- A filled rectangle: the overlay has no fill of its own, so it is lines, one a row. The color
|
||||
-- carries its alpha, which is what makes the card a panel over the moving grid and not a slab.
|
||||
-- Given a second color it runs from the first at the top to the second at the bottom: every
|
||||
-- row is its own line already, so the gradient costs nothing more.
|
||||
local function fillRect(x, y, w, h, color, toColor)
|
||||
local last = math.max(1, h - 1)
|
||||
|
||||
for row = 0, h - 1 do
|
||||
if toColor then
|
||||
local t = row / last
|
||||
|
||||
colorForeground(math.floor(color[1] + (toColor[1] - color[1]) * t + 0.5),
|
||||
math.floor(color[2] + (toColor[2] - color[2]) * t + 0.5),
|
||||
math.floor(color[3] + (toColor[3] - color[3]) * t + 0.5),
|
||||
math.floor(color[4] + (toColor[4] - color[4]) * t + 0.5))
|
||||
elseif row == 0 then
|
||||
colorForeground(color[1], color[2], color[3], color[4])
|
||||
end
|
||||
overlayLine(x, y + row, x + w - 1, y + row)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function ink(color)
|
||||
colorForeground(color[1], color[2], color[3], color[4])
|
||||
end
|
||||
|
||||
|
||||
-- The page: the card with its title bar, the rows in their columns, the chosen row on its bar,
|
||||
-- and the key hints in the footer. A page that draws its own picture gets a strip along the
|
||||
-- bottom instead; the audio delay tool's flash is the whole screen white and nothing else.
|
||||
MENU_RENDER.tools = function()
|
||||
if TOOL_FLASH then
|
||||
colorBackground(255, 255, 255, 255)
|
||||
overlayClear()
|
||||
return
|
||||
end
|
||||
if not TOOL_CLEAR then
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
end
|
||||
|
||||
-- Measuring text means rendering it, so the page is laid out only when a tool says it changed.
|
||||
if TOOL_DIRTY then
|
||||
TOOL_DIRTY = false
|
||||
toolsLayout()
|
||||
end
|
||||
|
||||
local footH = KEY_PAD * 2 + #toolKeyLines * KEY_LINE
|
||||
local footY = cardY + cardH - BORDER - footH
|
||||
local y = cardY + BORDER + TITLE_H + CARD_PAD
|
||||
|
||||
-- The card: the panel, its border twice for the width, the title bar, and the footer.
|
||||
fillRect(cardX, cardY, cardW, cardH, PANEL, PANEL_DEEP)
|
||||
fillRect(cardX + BORDER, cardY + BORDER, cardW - BORDER * 2, TITLE_H, TITLE_BAR_L, TITLE_BAR)
|
||||
fillRect(cardX + BORDER, cardY + BORDER + TITLE_H, cardW - BORDER * 2, BORDER, EDGE)
|
||||
if #toolKeyLines > 0 then
|
||||
fillRect(cardX + BORDER, footY, cardW - BORDER * 2, footH, FOOT)
|
||||
fillRect(cardX + BORDER, footY - BORDER, cardW - BORDER * 2, BORDER, EDGE)
|
||||
end
|
||||
ink(EDGE)
|
||||
overlayBox(cardX, cardY, cardX + cardW - 1, cardY + cardH - 1)
|
||||
overlayBox(cardX + 1, cardY + 1, cardX + cardW - 2, cardY + cardH - 2)
|
||||
if noteX then
|
||||
fillRect(noteX - CARD_PAD - BORDER, cardY + BORDER + TITLE_H + BORDER, BORDER, footY - BORDER - (cardY + BORDER + TITLE_H + BORDER), EDGE)
|
||||
end
|
||||
|
||||
fontSelect(titleFont)
|
||||
ink(GOLD)
|
||||
fontPrint(cardX + BORDER + CARD_PAD, cardY + BORDER + math.floor((TITLE_H - 26) / 2), TOOL_TITLE)
|
||||
fontSelect(font)
|
||||
|
||||
for _, line in ipairs(toolLines) do
|
||||
for _, piece in ipairs(line) do
|
||||
if piece.bar then
|
||||
fillRect(bodyX, y - 3, bodyW, LINE_HEIGHT - 2, TITLE_BAR_L, TITLE_BAR)
|
||||
end
|
||||
if piece.role == "quiet" then
|
||||
ink(QUIET)
|
||||
elseif piece.role == "soft" then
|
||||
ink(SOFT)
|
||||
elseif piece.role == "pick" then
|
||||
ink(GOLD)
|
||||
else
|
||||
ink(INK)
|
||||
end
|
||||
-- fontPrint refuses an empty string, and a tool legitimately produces one: a status
|
||||
-- line with nothing to say yet is still a line, and it still takes up its height.
|
||||
if piece.text ~= "" then
|
||||
fontPrint(piece.x, y, piece.text)
|
||||
end
|
||||
end
|
||||
y = y + LINE_HEIGHT
|
||||
end
|
||||
y = cardY + BORDER + TITLE_H + CARD_PAD
|
||||
for _, line in ipairs(toolNoteLines) do
|
||||
for _, piece in ipairs(line) do
|
||||
ink(SOFT)
|
||||
if piece.text ~= "" then
|
||||
fontPrint(piece.x, y, piece.text)
|
||||
end
|
||||
end
|
||||
y = y + LINE_HEIGHT
|
||||
end
|
||||
|
||||
fontSelect(hintFont)
|
||||
ink(QUIET)
|
||||
y = footY + KEY_PAD
|
||||
for _, line in ipairs(toolKeyLines) do
|
||||
if line ~= "" then
|
||||
fontPrint(bodyX, y, line)
|
||||
end
|
||||
y = y + KEY_LINE
|
||||
end
|
||||
fontSelect(font)
|
||||
end
|
||||
600
assets/Net.singe
600
assets/Net.singe
|
|
@ -1,600 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- Talking to the master service without stopping the game.
|
||||
--
|
||||
-- Lua in Singe is not threaded, and ssl.https.request blocks until it finishes: a JSON call would
|
||||
-- stutter the frame and a several-hundred-megabyte download would look like a hang. So every
|
||||
-- request here is a state machine driven a slice at a time from netPump(), which the menu calls
|
||||
-- once a frame. A caller hands in a function and gets called back when the answer arrives.
|
||||
--
|
||||
-- On trust. LuaSec's own https.lua ships with verify = "none", which encrypts the connection and
|
||||
-- authenticates nobody -- anything on the path could read the password going up, the token coming
|
||||
-- back, and alter the game being downloaded. Nothing here uses that module. A connection is
|
||||
-- verified against a pinned public key: Singe ships to five platforms and LuaSec can read neither
|
||||
-- the Windows certificate store nor the macOS keychain, and this client only ever talks to one
|
||||
-- service, so pinning is both simpler and stronger than carrying a list of every public CA.
|
||||
--
|
||||
-- The pin is of the *public key*, not the certificate, so an ordinary renewal that keeps the key
|
||||
-- does not lock out every cabinet; two pins are carried so a key can actually be rotated.
|
||||
--
|
||||
-- Without a pin the chain is verified against the union of two CA lists: Mozilla's, which the
|
||||
-- engine extracts as Singe/cacerts.pem beside the other support files, and the machine's own
|
||||
-- (the distribution's file on Linux, the root store on Windows, the system anchors on macOS),
|
||||
-- which netTrustSystem adds, with a cacerts.local.pem from the data directory for a self-hosted
|
||||
-- server's CA. A connection verifies when either list knows the server's root, so a cabinet
|
||||
-- whose bundle has gone stale can still reach the release that brings a new one, as long as the
|
||||
-- machine's own list is current (PLAN.md section 61).
|
||||
|
||||
|
||||
local socket = require("socket")
|
||||
local ssl = require("ssl")
|
||||
|
||||
NET_TIMEOUT = 30000 -- ms with no progress at all before a request is abandoned
|
||||
NET_CHUNK = 65536 -- bytes read per pump slice
|
||||
NET_REDIRECT_MAX = 5
|
||||
|
||||
-- master.singeengine.com. Two pins: the one in use and its successor, so a key rotation is a
|
||||
-- release that knows both, then a server that starts using the second. An empty list means the
|
||||
-- host's own trust store is used instead, which is what a self-hosted server without a pin gets.
|
||||
NET_PINS = {}
|
||||
|
||||
NET_CA_SHIPPED = "Singe/cacerts.pem" -- The list Singe carries; the machine's own is added to it.
|
||||
|
||||
local active = {}
|
||||
local nextId = 1
|
||||
local trust = nil -- The one client context every request shares, and what it was built from.
|
||||
|
||||
|
||||
-- The SHA-256 of a peer's public key, in the form the pins are written in.
|
||||
local function keyPin(connection)
|
||||
local cert = connection:getpeercertificate()
|
||||
|
||||
if not cert then
|
||||
return nil
|
||||
end
|
||||
local pem = cert:pubkey()
|
||||
if not pem then
|
||||
return nil
|
||||
end
|
||||
return utilSha256(pem)
|
||||
end
|
||||
|
||||
|
||||
local function finish(request, ok, result)
|
||||
active[request.id] = nil
|
||||
if request.file then
|
||||
request.file:close()
|
||||
request.file = nil
|
||||
end
|
||||
if request.socket then
|
||||
pcall(function() request.socket:close() end)
|
||||
request.socket = nil
|
||||
end
|
||||
if request.onDone then
|
||||
request.onDone(ok, result)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function fail(request, message)
|
||||
finish(request, false, { error = message, url = request.url })
|
||||
end
|
||||
|
||||
|
||||
-- scheme, host, port, path
|
||||
local function parseUrl(url)
|
||||
local scheme, rest = url:match("^(%a[%w+.-]*)://(.*)$")
|
||||
|
||||
if not scheme then
|
||||
return nil
|
||||
end
|
||||
local hostPort, path = rest:match("^([^/]+)(.*)$")
|
||||
if not hostPort then
|
||||
return nil
|
||||
end
|
||||
local host, port = hostPort:match("^(.-):(%d+)$")
|
||||
host = host or hostPort
|
||||
port = tonumber(port) or ((scheme == "https") and 443 or 80)
|
||||
return scheme, host, port, (path ~= "" and path or "/")
|
||||
end
|
||||
|
||||
|
||||
local function beginConnect(request)
|
||||
local sock, err = socket.tcp()
|
||||
|
||||
if not sock then
|
||||
return fail(request, "no socket: " .. tostring(err))
|
||||
end
|
||||
sock:settimeout(0)
|
||||
request.socket = sock
|
||||
-- A non-blocking connect answers "timeout" immediately and finishes in the background; the
|
||||
-- socket becoming writable is what says it arrived.
|
||||
local ok, connectErr = sock:connect(request.host, request.port)
|
||||
if not ok and connectErr ~= "timeout" and connectErr ~= "already connected" then
|
||||
return fail(request, "cannot reach " .. request.host .. ": " .. tostring(connectErr))
|
||||
end
|
||||
request.state = (request.scheme == "https") and "connecting" or "sending"
|
||||
request.sent = 0
|
||||
end
|
||||
|
||||
|
||||
local function buildRequest(request)
|
||||
local lines = {
|
||||
string.format("%s %s HTTP/1.1", request.method, request.path),
|
||||
"Host: " .. request.host,
|
||||
"Connection: close",
|
||||
"Accept-Encoding: identity",
|
||||
"User-Agent: Singe/" .. SINGE_VERSION_STRING,
|
||||
}
|
||||
|
||||
for name, value in pairs(request.headers or {}) do
|
||||
lines[#lines + 1] = name .. ": " .. value
|
||||
end
|
||||
if request.bodyFile then
|
||||
lines[#lines + 1] = "Content-Length: " .. request.bodySize
|
||||
elseif request.body then
|
||||
lines[#lines + 1] = "Content-Length: " .. #request.body
|
||||
end
|
||||
return table.concat(lines, "\r\n") .. "\r\n\r\n" .. (request.body or "")
|
||||
end
|
||||
|
||||
|
||||
-- The shipped bundle's own name for its version, from its header, so a failure can say how old it
|
||||
-- is without a second copy of the date to keep in step.
|
||||
local function bundleVersion()
|
||||
local file = io.open(NET_CA_SHIPPED, "r")
|
||||
local line = nil
|
||||
local seen = nil
|
||||
|
||||
if not file then
|
||||
return "missing"
|
||||
end
|
||||
for _ = 1, 5 do
|
||||
line = file:read("l")
|
||||
if not line then
|
||||
break
|
||||
end
|
||||
seen = seen or line:match("ca%-certificates (%d+)")
|
||||
end
|
||||
file:close()
|
||||
|
||||
return seen or "of unknown date"
|
||||
end
|
||||
|
||||
|
||||
-- What the clock has to do with a failed verification. Certificates are checked against the date,
|
||||
-- and a board with no battery-backed clock that has not had network time reads a date long past:
|
||||
-- when the clock reads earlier than the shipped bundle was made, it is certainly wrong, whatever
|
||||
-- OpenSSL called the failure. Otherwise a "not valid yet" certificate still points at the clock.
|
||||
local function clockNote(err)
|
||||
local now = os.time()
|
||||
local reads = os.date("%Y-%m-%d %H:%M", now)
|
||||
local y, m, d = bundleVersion():match("^(%d%d%d%d)(%d%d)(%d%d)$")
|
||||
|
||||
if y and (now < os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = 0 })) then
|
||||
return ". This machine's clock reads " .. reads .. ", before the certificate list Singe ships was made (" .. y .. "-" .. m .. "-" .. d .. "): set the date and time"
|
||||
end
|
||||
if tostring(err):find("not yet valid", 1, true) then
|
||||
return ". The certificate is not valid yet by this machine's clock, which reads " .. reads .. ": check the date and time"
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
|
||||
-- The client context, built once: the shipped bundle, then the machine's own roots and the local
|
||||
-- file on top. Rebuilding it per request parsed the whole bundle every connection.
|
||||
local function trustContext()
|
||||
if trust then
|
||||
return trust.context
|
||||
end
|
||||
local params = {
|
||||
mode = "client",
|
||||
protocol = "any",
|
||||
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1" },
|
||||
-- With a pin, the chain is checked against the key rather than a CA list, and the pin is
|
||||
-- what decides. Without one, the lists are asked -- and if they have nothing to say, the
|
||||
-- request fails rather than proceeding unverified.
|
||||
verify = (#NET_PINS > 0) and "none" or "peer",
|
||||
cafile = NET_CA_SHIPPED,
|
||||
}
|
||||
local context, err = ssl.newcontext(params)
|
||||
|
||||
if not context then
|
||||
return nil, err
|
||||
end
|
||||
local _, report = netTrustSystem(context)
|
||||
|
||||
trust = { context = context, report = report }
|
||||
|
||||
return context
|
||||
end
|
||||
|
||||
|
||||
-- What a failed verification was checked against, for the message a person reads.
|
||||
local function trustChecked()
|
||||
local lists = { "shipped " .. NET_CA_SHIPPED .. " (" .. bundleVersion() .. ")" }
|
||||
|
||||
for line in ((trust and trust.report) or ""):gmatch("[^\n]+") do
|
||||
lists[#lists + 1] = line
|
||||
end
|
||||
|
||||
return table.concat(lists, "; ")
|
||||
end
|
||||
|
||||
|
||||
local function startTls(request)
|
||||
local context, err = trustContext()
|
||||
|
||||
if not context then
|
||||
return fail(request, "TLS setup failed: " .. tostring(err))
|
||||
end
|
||||
local wrapped, why = ssl.wrap(request.socket, context)
|
||||
|
||||
if not wrapped then
|
||||
return fail(request, "TLS setup failed: " .. tostring(why))
|
||||
end
|
||||
wrapped:settimeout(0)
|
||||
if wrapped.sni then
|
||||
wrapped:sni(request.host)
|
||||
end
|
||||
request.socket = wrapped
|
||||
request.state = "handshake"
|
||||
end
|
||||
|
||||
|
||||
local function pumpHandshake(request)
|
||||
local ok, err = request.socket:dohandshake()
|
||||
|
||||
if ok then
|
||||
if #NET_PINS > 0 then
|
||||
local seen = keyPin(request.socket)
|
||||
local good = false
|
||||
for _, pin in ipairs(NET_PINS) do
|
||||
if seen == pin then
|
||||
good = true
|
||||
end
|
||||
end
|
||||
if not good then
|
||||
-- The one failure that must never be shrugged off: a wrong key is either a
|
||||
-- mis-deployed server or somebody in the middle, and both want a person.
|
||||
return fail(request, "the server's key is not one this build trusts")
|
||||
end
|
||||
end
|
||||
request.state = "sending"
|
||||
return
|
||||
end
|
||||
if err ~= "wantread" and err ~= "wantwrite" and err ~= "timeout" then
|
||||
fail(request, "TLS handshake failed: " .. tostring(err) .. ". Checked: " .. trustChecked() .. clockNote(err) .. ".")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The head goes first as one string. A body from a file follows it a piece at a time, read as
|
||||
-- it is sent, so a game is never held in memory whole: the same reason a download goes straight
|
||||
-- to disk.
|
||||
local NET_SEND_CHUNK = 65536
|
||||
|
||||
local function pumpSending(request)
|
||||
if not request.outgoing then
|
||||
request.outgoing = buildRequest(request)
|
||||
end
|
||||
if request.sent < #request.outgoing then
|
||||
local sent, err, partial = request.socket:send(request.outgoing, request.sent + 1)
|
||||
|
||||
if sent then
|
||||
request.sent = sent
|
||||
elseif err == "timeout" then
|
||||
request.sent = partial or request.sent
|
||||
else
|
||||
return fail(request, "send failed: " .. tostring(err))
|
||||
end
|
||||
if request.sent < #request.outgoing then
|
||||
return
|
||||
end
|
||||
request.lastProgress = singeGetTicks()
|
||||
end
|
||||
if not request.bodyFile then
|
||||
request.state = "reading"
|
||||
return
|
||||
end
|
||||
-- The body, from the file.
|
||||
if not request.bodyHandle then
|
||||
request.bodyHandle = io.open(request.bodyFile, "rb")
|
||||
request.bodySent = 0
|
||||
request.chunk = nil
|
||||
request.chunkSent = 0
|
||||
if not request.bodyHandle then
|
||||
return fail(request, "cannot read " .. request.bodyFile)
|
||||
end
|
||||
end
|
||||
if not request.chunk then
|
||||
request.chunk = request.bodyHandle:read(NET_SEND_CHUNK)
|
||||
request.chunkSent = 0
|
||||
if not request.chunk then
|
||||
request.bodyHandle:close()
|
||||
request.bodyHandle = nil
|
||||
request.state = "reading"
|
||||
return
|
||||
end
|
||||
end
|
||||
local sent, err, partial = request.socket:send(request.chunk, request.chunkSent + 1)
|
||||
|
||||
if sent then
|
||||
request.chunkSent = sent
|
||||
elseif err == "timeout" then
|
||||
request.chunkSent = partial or request.chunkSent
|
||||
else
|
||||
return fail(request, "send failed: " .. tostring(err))
|
||||
end
|
||||
if request.chunkSent > 0 then
|
||||
request.lastProgress = singeGetTicks()
|
||||
end
|
||||
if request.chunkSent >= #request.chunk then
|
||||
request.bodySent = request.bodySent + #request.chunk
|
||||
request.chunk = nil
|
||||
if request.onProgress then
|
||||
request.onProgress(request.bodySent, request.bodySize)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Split the headers off once the blank line arrives, so the body can start going to disk rather
|
||||
-- than accumulating in memory: a game is far too big to hold twice.
|
||||
local function splitHead(request)
|
||||
local at = request.buffer:find("\r\n\r\n", 1, true)
|
||||
|
||||
if not at then
|
||||
return false
|
||||
end
|
||||
local head = request.buffer:sub(1, at - 1)
|
||||
request.buffer = request.buffer:sub(at + 4)
|
||||
request.status = tonumber(head:match("^HTTP/%d%.%d (%d%d%d)")) or 0
|
||||
request.responseHeaders = {}
|
||||
for name, value in head:gmatch("\r\n([^:\r\n]+):%s*([^\r\n]*)") do
|
||||
request.responseHeaders[name:lower()] = value
|
||||
end
|
||||
request.length = tonumber(request.responseHeaders["content-length"])
|
||||
request.got = 0
|
||||
-- HTTP/1.1 may send the body in sized pieces instead of announcing a length; those are
|
||||
-- unwrapped as they arrive and the last, empty, piece is the end of the body.
|
||||
request.chunked = (request.responseHeaders["transfer-encoding"] or ""):lower():find("chunked", 1, true) ~= nil
|
||||
request.chunkBuffer = ""
|
||||
request.chunkLeft = nil
|
||||
request.chunkDone = false
|
||||
if request.toFile then
|
||||
local file, err = io.open(request.toFile, "wb")
|
||||
if not file then
|
||||
fail(request, "cannot write " .. request.toFile .. ": " .. tostring(err))
|
||||
return false
|
||||
end
|
||||
request.file = file
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local function consume(request, data)
|
||||
request.got = request.got + #data
|
||||
if request.file then
|
||||
request.file:write(data)
|
||||
else
|
||||
request.parts[#request.parts + 1] = data
|
||||
end
|
||||
if request.onProgress then
|
||||
request.onProgress(request.got, request.length)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Body bytes arriving under chunked transfer encoding: a hexadecimal size on a line, that many
|
||||
-- bytes, a blank line, again until a size of zero. Whatever is left over waits for the next read.
|
||||
local function consumeChunked(request, data)
|
||||
request.chunkBuffer = request.chunkBuffer .. data
|
||||
while not request.chunkDone do
|
||||
if request.chunkLeft == nil then
|
||||
local at = request.chunkBuffer:find("\r\n", 1, true)
|
||||
if not at then
|
||||
return
|
||||
end
|
||||
local size = tonumber(request.chunkBuffer:sub(1, at - 1):match("^%x+"), 16)
|
||||
request.chunkBuffer = request.chunkBuffer:sub(at + 2)
|
||||
if size == nil then
|
||||
return fail(request, "the server sent a chunk with no size")
|
||||
end
|
||||
if size == 0 then
|
||||
request.chunkDone = true
|
||||
return
|
||||
end
|
||||
request.chunkLeft = size
|
||||
elseif request.chunkLeft > 0 then
|
||||
local take = request.chunkBuffer:sub(1, request.chunkLeft)
|
||||
if #take == 0 then
|
||||
return
|
||||
end
|
||||
request.chunkBuffer = request.chunkBuffer:sub(#take + 1)
|
||||
request.chunkLeft = request.chunkLeft - #take
|
||||
consume(request, take)
|
||||
else
|
||||
-- The blank line after a chunk's bytes.
|
||||
if #request.chunkBuffer < 2 then
|
||||
return
|
||||
end
|
||||
request.chunkBuffer = request.chunkBuffer:sub(3)
|
||||
request.chunkLeft = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function complete(request)
|
||||
local body = request.file and "" or table.concat(request.parts)
|
||||
|
||||
-- A redirect is followed only for a plain GET, and only within the same scheme and host: the
|
||||
-- point of a pin is lost if a 302 can walk the client somewhere else.
|
||||
local location = request.responseHeaders["location"]
|
||||
if location and request.status >= 300 and request.status < 400 and request.redirects < NET_REDIRECT_MAX then
|
||||
local scheme, host = parseUrl(location)
|
||||
if scheme == request.scheme and host == request.host then
|
||||
request.redirects = request.redirects + 1
|
||||
request.url = location
|
||||
local _, _, port, path = parseUrl(location)
|
||||
request.port, request.path = port, path
|
||||
request.buffer, request.parts, request.outgoing, request.sent = "", {}, nil, 0
|
||||
request.chunked, request.chunkBuffer, request.chunkLeft, request.chunkDone = false, "", nil, false
|
||||
if request.socket then pcall(function() request.socket:close() end) end
|
||||
request.socket = nil
|
||||
return beginConnect(request)
|
||||
end
|
||||
end
|
||||
finish(request, true, { status = request.status, body = body, headers = request.responseHeaders,
|
||||
bytes = request.got, file = request.toFile })
|
||||
end
|
||||
|
||||
|
||||
local function pumpReading(request)
|
||||
local data, err, partial = request.socket:receive(NET_CHUNK)
|
||||
local got = data or partial
|
||||
|
||||
if got and #got > 0 then
|
||||
request.lastProgress = singeGetTicks()
|
||||
if not request.responseHeaders then
|
||||
request.buffer = request.buffer .. got
|
||||
if splitHead(request) and #request.buffer > 0 then
|
||||
local rest = request.buffer
|
||||
request.buffer = ""
|
||||
if request.chunked then
|
||||
consumeChunked(request, rest)
|
||||
else
|
||||
consume(request, rest)
|
||||
end
|
||||
end
|
||||
elseif request.chunked then
|
||||
consumeChunked(request, got)
|
||||
else
|
||||
consume(request, got)
|
||||
end
|
||||
end
|
||||
if not active[request.id] then
|
||||
return -- A bad chunk failed the request from inside consumeChunked.
|
||||
end
|
||||
if request.responseHeaders and ((request.length and request.got >= request.length) or request.chunkDone) then
|
||||
return complete(request)
|
||||
end
|
||||
if err == "closed" then
|
||||
if request.responseHeaders then
|
||||
return complete(request)
|
||||
end
|
||||
return fail(request, "the server closed before answering")
|
||||
end
|
||||
if err and err ~= "timeout" and err ~= "wantread" then
|
||||
fail(request, "read failed: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- One slice of work for every request in flight. Cheap when nothing is happening.
|
||||
--
|
||||
-- The list is snapshotted first because a request that finishes calls back into the caller, and the
|
||||
-- caller very reasonably starts another one from there -- a download that reports itself installed,
|
||||
-- say. Adding to a table while pairs() walks it is not allowed, and the failure ("invalid key to
|
||||
-- 'next'") names neither the table nor the callback that did it.
|
||||
function netPump()
|
||||
local running = {}
|
||||
|
||||
for id in pairs(active) do
|
||||
running[#running + 1] = id
|
||||
end
|
||||
for _, id in ipairs(running) do
|
||||
local request = active[id]
|
||||
if request then
|
||||
if singeGetTicks() - request.lastProgress > NET_TIMEOUT then
|
||||
fail(request, "the server stopped responding")
|
||||
elseif request.state == "connecting" then
|
||||
-- Writable means the connect finished; LuaSec is wrapped around it only then.
|
||||
local _, writable = socket.select(nil, { request.socket }, 0)
|
||||
if writable and #writable > 0 then
|
||||
request.lastProgress = singeGetTicks()
|
||||
startTls(request)
|
||||
end
|
||||
elseif request.state == "handshake" then
|
||||
pumpHandshake(request)
|
||||
elseif request.state == "sending" then
|
||||
pumpSending(request)
|
||||
elseif request.state == "reading" then
|
||||
pumpReading(request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Start a request. opts: url, method, headers, body, toFile, onProgress. onDone(ok, result) is
|
||||
-- called once, with result.status/body/headers on success or result.error on failure.
|
||||
function netRequest(opts, onDone)
|
||||
local scheme, host, port, path = parseUrl(opts.url)
|
||||
|
||||
if not scheme or (scheme ~= "http" and scheme ~= "https") then
|
||||
onDone(false, { error = "not a URL this understands: " .. tostring(opts.url) })
|
||||
return nil
|
||||
end
|
||||
local request = {
|
||||
id = nextId, url = opts.url, scheme = scheme, host = host, port = port, path = path,
|
||||
method = opts.method or "GET", headers = opts.headers, body = opts.body,
|
||||
bodyFile = opts.bodyFile, toFile = opts.toFile, onProgress = opts.onProgress, onDone = onDone,
|
||||
buffer = "", parts = {}, redirects = 0, lastProgress = singeGetTicks(), sent = 0,
|
||||
}
|
||||
-- A body sent from a file: its length has to be known before the head goes out.
|
||||
if request.bodyFile then
|
||||
local handle = io.open(request.bodyFile, "rb")
|
||||
if not handle then
|
||||
onDone(false, { error = "cannot read " .. tostring(request.bodyFile) })
|
||||
return nil
|
||||
end
|
||||
request.bodySize = handle:seek("end")
|
||||
handle:close()
|
||||
end
|
||||
nextId = nextId + 1
|
||||
active[request.id] = request
|
||||
beginConnect(request)
|
||||
return request.id
|
||||
end
|
||||
|
||||
|
||||
function netCancel(id)
|
||||
local request = active[id]
|
||||
|
||||
if request then
|
||||
request.onDone = nil
|
||||
finish(request, false, { error = "canceled" })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function netBusy()
|
||||
return next(active) ~= nil
|
||||
end
|
||||
BIN
assets/SingeText.glb
(Stored with Git LFS)
BIN
assets/SingeText.glb
(Stored with Git LFS)
Binary file not shown.
1668
assets/Tools.singe
1668
assets/Tools.singe
File diff suppressed because it is too large
Load diff
3004
assets/cacerts.pem
3004
assets/cacerts.pem
File diff suppressed because it is too large
Load diff
|
|
@ -1,77 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
-- Default Mappings
|
||||
|
||||
-- How far an axis must move before it counts as pressed, in raw axis units: SDL reports 32767 at
|
||||
-- full deflection, so 15000 is a little under half way. It covers every axis of every gamepad,
|
||||
-- the two analog triggers included.
|
||||
DEAD_ZONE = 15000
|
||||
|
||||
-- One axis at a time, for a stiff stick or a worn trigger, in the same raw units. Each entry is
|
||||
-- { axis, dead zone }, where the axis is any GAMEPAD_N axis entry -- the axis itself or either of
|
||||
-- its two directions, which all name the same axis. Anything not listed keeps DEAD_ZONE.
|
||||
DEAD_ZONES = {
|
||||
-- { GAMEPAD_0.AXIS_LEFT_X, 8000 },
|
||||
-- { GAMEPAD_0.AXIS_LEFT_Y, 8000 },
|
||||
}
|
||||
|
||||
-- How far an analog trigger must travel before it counts as a button, as a per cent of full
|
||||
-- travel: Hypseus's unit, where its own default is 99.5. Zero leaves the triggers on DEAD_ZONE,
|
||||
-- which is what Singe has always given them, and --trigger_threshold on the command line wins over
|
||||
-- whatever is set here. 15000 raw units is 45.8 per cent, for comparison.
|
||||
TRIGGER_THRESHOLD = 0
|
||||
|
||||
-- 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, or a
|
||||
-- chord: a list of two to four of them held together, such as
|
||||
-- { GAMEPAD_0.BUTTON_BACK, GAMEPAD_0.BUTTON_START }. Each keeps its own meaning as well.
|
||||
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_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_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 } -- 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 }
|
||||
INPUT_RECORD = { SCANCODE.F10 } -- Starts and stops recording the picture and the sound to singeNNN.mp4.
|
||||
-- Not a control: the engine raises this one itself when a mouse or a light gun is unplugged, so a
|
||||
-- gun game can say so on screen. Bind a key to it as well if you want to test what the game does.
|
||||
INPUT_MOUSE_DISCONNECT = { }
|
||||
BIN
assets/font.xcf
(Stored with Git LFS)
BIN
assets/font.xcf
(Stored with Git LFS)
Binary file not shown.
243
assets/gui.rcss
243
assets/gui.rcss
|
|
@ -1,243 +0,0 @@
|
|||
/*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* The shipped GUI theme: what a document looks like before a game styles it. Link it from a
|
||||
* document with <link type="text/rcss" href="Singe/gui.rcss"/> and override what you like.
|
||||
* Licensed under the GNU General Public License, version 3 or later.
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: FreeSans;
|
||||
font-weight: bold;
|
||||
font-size: 22dp;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: block;
|
||||
font-size: 40dp;
|
||||
margin: 0 0 12dp 0;
|
||||
color: #ffd35a;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: block;
|
||||
font-size: 28dp;
|
||||
margin: 0 0 8dp 0;
|
||||
color: #ffd35a;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 0 0 8dp 0;
|
||||
}
|
||||
|
||||
div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: block;
|
||||
background-color: #101828e0;
|
||||
decorator: linear-gradient(180deg, #182640e0, #0c1424e0);
|
||||
box-shadow: 0 6dp 14dp #00000090;
|
||||
border: 2dp #ffd35a;
|
||||
border-radius: 8dp;
|
||||
padding: 16dp;
|
||||
}
|
||||
|
||||
button,
|
||||
input.submit {
|
||||
display: inline-block;
|
||||
padding: 8dp 20dp;
|
||||
margin: 4dp;
|
||||
background-color: #24344e;
|
||||
decorator: linear-gradient(180deg, #2e4260, #1c2a42);
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 6dp;
|
||||
color: #f0f0f0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
tab-index: auto;
|
||||
transition: background-color border-color 0.12s;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
input.submit:hover {
|
||||
background-color: #33507a;
|
||||
decorator: linear-gradient(180deg, #3f5f8e, #2a4470);
|
||||
border-color: #ffd35a;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
input.submit:focus {
|
||||
border-color: #ffd35a;
|
||||
}
|
||||
|
||||
button:active,
|
||||
input.submit:active {
|
||||
background-color: #ffd35a;
|
||||
decorator: linear-gradient(180deg, #ffe08a, #f0c040);
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
input.text,
|
||||
input.password,
|
||||
textarea {
|
||||
display: inline-block;
|
||||
padding: 6dp 10dp;
|
||||
margin: 4dp;
|
||||
background-color: #0a1220;
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 4dp;
|
||||
color: #f0f0f0;
|
||||
cursor: text;
|
||||
tab-index: auto;
|
||||
}
|
||||
|
||||
input.text:focus,
|
||||
input.password:focus,
|
||||
textarea:focus {
|
||||
border-color: #ffd35a;
|
||||
}
|
||||
|
||||
input.checkbox,
|
||||
input.radio {
|
||||
display: inline-block;
|
||||
width: 22dp;
|
||||
height: 22dp;
|
||||
margin: 0 8dp 0 0;
|
||||
background-color: #0a1220;
|
||||
border: 2dp #6a86b8;
|
||||
vertical-align: middle;
|
||||
tab-index: auto;
|
||||
}
|
||||
|
||||
input.radio {
|
||||
border-radius: 11dp;
|
||||
}
|
||||
|
||||
input.checkbox:checked,
|
||||
input.radio:checked {
|
||||
background-color: #ffd35a;
|
||||
}
|
||||
|
||||
input.range {
|
||||
display: inline-block;
|
||||
height: 24dp;
|
||||
width: 240dp;
|
||||
margin: 4dp;
|
||||
tab-index: auto;
|
||||
}
|
||||
|
||||
input.range slidertrack {
|
||||
height: 6dp;
|
||||
margin: 9dp 0;
|
||||
background-color: #24344e;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
input.range sliderbar {
|
||||
width: 22dp;
|
||||
height: 22dp;
|
||||
margin: -8dp 0;
|
||||
background-color: #ffd35a;
|
||||
border-radius: 11dp;
|
||||
}
|
||||
|
||||
select {
|
||||
display: inline-block;
|
||||
width: 240dp;
|
||||
padding: 6dp 10dp;
|
||||
margin: 4dp;
|
||||
background-color: #0a1220;
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 4dp;
|
||||
tab-index: auto;
|
||||
}
|
||||
|
||||
select selectvalue {
|
||||
width: auto;
|
||||
margin-right: 30dp;
|
||||
}
|
||||
|
||||
select selectarrow {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
background-color: #6a86b8;
|
||||
}
|
||||
|
||||
select selectbox {
|
||||
background-color: #101828;
|
||||
border: 2dp #6a86b8;
|
||||
}
|
||||
|
||||
select selectbox option {
|
||||
padding: 4dp 10dp;
|
||||
}
|
||||
|
||||
select selectbox option:hover,
|
||||
select selectbox option:checked {
|
||||
background-color: #33507a;
|
||||
}
|
||||
|
||||
progress {
|
||||
display: block;
|
||||
height: 18dp;
|
||||
margin: 4dp 0;
|
||||
background-color: #0a1220;
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 9dp;
|
||||
}
|
||||
|
||||
progress fill {
|
||||
background-color: #ffd35a;
|
||||
decorator: linear-gradient(180deg, #ffe08a, #e6b830);
|
||||
border-radius: 7dp;
|
||||
}
|
||||
|
||||
scrollbarvertical {
|
||||
width: 14dp;
|
||||
background-color: #0a1220;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar {
|
||||
background-color: #6a86b8;
|
||||
border-radius: 7dp;
|
||||
min-height: 24dp;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar:hover,
|
||||
scrollbarvertical sliderbar:active {
|
||||
background-color: #ffd35a;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderarrowdec,
|
||||
scrollbarvertical sliderarrowinc {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
background-color: #0a1220;
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 4dp;
|
||||
}
|
||||
|
||||
.list div {
|
||||
padding: 6dp 10dp;
|
||||
tab-index: auto;
|
||||
}
|
||||
|
||||
.list div:hover,
|
||||
.list div:focus,
|
||||
.list div.selected {
|
||||
background-color: #33507a;
|
||||
color: #ffd35a;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #8fa2c4;
|
||||
}
|
||||
BIN
assets/icon.xcf
(Stored with Git LFS)
BIN
assets/icon.xcf
(Stored with Git LFS)
Binary file not shown.
BIN
assets/kangarooPunchLogo.xcf
(Stored with Git LFS)
BIN
assets/kangarooPunchLogo.xcf
(Stored with Git LFS)
Binary file not shown.
354
assets/menu.rcss
354
assets/menu.rcss
|
|
@ -1,354 +0,0 @@
|
|||
/*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* The bundled menu's look on top of the shipped theme. Nothing here has a position or a size:
|
||||
* Singe/Menu.singe places every region from the overlay size, so the attract video it draws
|
||||
* underneath fills the #video hole exactly.
|
||||
* Licensed under the GNU General Public License, version 3 or later.
|
||||
*/
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 14dp;
|
||||
}
|
||||
|
||||
/* Every region the script places; the border sits inside the rectangle it is given. */
|
||||
.placed {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The game list: the panel look (the theme's gradient and shadow), one row per game, the
|
||||
selected one gold on a gradient. */
|
||||
#list {
|
||||
background-color: #0a1220d0;
|
||||
decorator: linear-gradient(180deg, #182640d0, #0c1424d0);
|
||||
box-shadow: 0 6dp 14dp #00000090;
|
||||
padding: 4dp 0;
|
||||
font-size: 13dp;
|
||||
}
|
||||
|
||||
#list div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3dp 8dp;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* A title takes whatever the update mark leaves and ends in an ellipsis when that is not enough, so
|
||||
a long title can never push the mark out of sight. The whole title heads the details. */
|
||||
#list div .title {
|
||||
display: block;
|
||||
flex: 1 1 0dp;
|
||||
min-width: 0dp;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#list div:hover {
|
||||
background-color: #24344e;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
#list div:focus {
|
||||
background-color: transparent;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
#list div.selected,
|
||||
#list div.selected:hover,
|
||||
#list div.selected:focus {
|
||||
background-color: #33507a;
|
||||
decorator: linear-gradient(90deg, #4a6ea8, #33507a);
|
||||
color: #ffd35a;
|
||||
}
|
||||
|
||||
/* Poster and marquee: the script sets an image decorator that fits the picture inside. No border:
|
||||
cabinet art and a marquee are pictures in their own right and a frame round them fights with
|
||||
whatever frame the artwork already has. */
|
||||
.art {
|
||||
background-color: #0a122080;
|
||||
}
|
||||
|
||||
/* The attract video shows through here; nothing is drawn over it. */
|
||||
#video {
|
||||
}
|
||||
|
||||
#info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6dp 10dp;
|
||||
background-color: #101828d0;
|
||||
border-color: #6a86b8;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 18dp;
|
||||
color: #ffd35a;
|
||||
margin-bottom: 2dp;
|
||||
}
|
||||
|
||||
.line {
|
||||
font-size: 11dp;
|
||||
color: #c8d2e6;
|
||||
}
|
||||
|
||||
.line .label {
|
||||
color: #8fa2c4;
|
||||
}
|
||||
|
||||
#description {
|
||||
flex: 1;
|
||||
margin-top: 4dp;
|
||||
font-size: 13dp;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #101828d0;
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 4dp;
|
||||
padding: 0 8dp;
|
||||
}
|
||||
|
||||
#hints {
|
||||
font-size: 12dp;
|
||||
color: #8fa2c4;
|
||||
}
|
||||
|
||||
/* A game with a newer version on the service wears a small mark after its title, and the line
|
||||
under its details says which version and which button. Gold, like the selection, since it is
|
||||
the one thing on the page asking for a press. */
|
||||
#list div .badge {
|
||||
flex: none;
|
||||
margin-left: 6dp;
|
||||
padding: 0 4dp;
|
||||
font-size: 10dp;
|
||||
color: #101828;
|
||||
background-color: #ffd35a;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
#notice {
|
||||
color: #ffd35a;
|
||||
}
|
||||
|
||||
|
||||
/* .hidden and .tool are one class each, and the pages that let the overlay through wear two, so a
|
||||
page taken out of view has to be said again here or its display wins. */
|
||||
.tool.hidden,
|
||||
.tool.clear.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The service tools: one page over everything, the backdrop still moving behind it, and on it
|
||||
a card in the game page's own dress -- the panel gradient, the blue-gray border, a title bar in
|
||||
the selection's gradient with the title in the game list's gold, and the key hints in a footer
|
||||
strip like the game page's. Nothing on it says black. */
|
||||
.tool {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
padding: 10dp;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #f0f0f0;
|
||||
font-size: 15dp;
|
||||
}
|
||||
|
||||
.tool .card {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #0a1220d8;
|
||||
decorator: linear-gradient(180deg, #182640d8, #0c1424d8);
|
||||
border: 2dp #6a86b8;
|
||||
border-radius: 6dp;
|
||||
box-shadow: 0 6dp 14dp #00000090;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* The audio delay tool alone turns the whole screen white for the frame of its flash; the card
|
||||
steps out of the way so the flash is the flash and nothing else. */
|
||||
.tool.flash {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.tool.flash .card {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tool h1 {
|
||||
font-size: 20dp;
|
||||
color: #ffd35a;
|
||||
margin: 0;
|
||||
padding: 8dp 14dp;
|
||||
background-color: #33507a;
|
||||
decorator: linear-gradient(90deg, #4a6ea8, #33507a);
|
||||
border-bottom: 2dp #6a86b8;
|
||||
}
|
||||
|
||||
.tool .columns {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* The style sheet can scroll this box, but nothing drives a scroll bar from a joystick: the
|
||||
windowing in Singe/Tools.singe is what keeps the chosen row on screen. */
|
||||
.tool #toolBody {
|
||||
flex: 1 1 0;
|
||||
padding: 12dp 14dp;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* The list of tools has a second column: what the chosen tool is for, beside the list. */
|
||||
.tool #toolNote {
|
||||
flex: 0 0 42%;
|
||||
padding: 12dp 16dp;
|
||||
border-left: 2dp #6a86b8;
|
||||
color: #c8d2e6;
|
||||
font-size: 14dp;
|
||||
}
|
||||
|
||||
.tool #toolNote.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tool #toolKeys {
|
||||
padding: 6dp 14dp;
|
||||
border-top: 2dp #6a86b8;
|
||||
background-color: #101828d0;
|
||||
color: #8fa2c4;
|
||||
font-size: 13dp;
|
||||
}
|
||||
|
||||
.tool p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* The key hints, close together when a tool needs more than one line of them. */
|
||||
.tool p.keys {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tool p.keys + p.keys {
|
||||
margin-top: 2dp;
|
||||
}
|
||||
|
||||
/* The rows a tool fills in for itself: a heading row and an indented detail row under it. Text
|
||||
with nowhere to break -- a long path -- wraps mid-word rather than running off the edge, but an
|
||||
ordinary word is left whole. */
|
||||
.tool .row {
|
||||
margin-bottom: 4dp;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tool .sub {
|
||||
margin-left: 20dp;
|
||||
margin-bottom: 3dp;
|
||||
font-size: 13dp;
|
||||
color: #c8d2e6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* A label beside its value. The two are columns rather than one run of text, so a value too long
|
||||
for the line wraps under itself instead of under the label. */
|
||||
.tool .pair {
|
||||
display: flex;
|
||||
margin-bottom: 4dp;
|
||||
}
|
||||
|
||||
.tool .pair .label {
|
||||
flex: 0 0 130dp;
|
||||
color: #8fa2c4;
|
||||
}
|
||||
|
||||
.tool .pair .detail {
|
||||
flex: 1 1 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* A line a tool offers to choose between, and the chosen one the way the game list shows its
|
||||
selection: gold on the gradient bar. A detail the row carries -- a size, a state, a value --
|
||||
sits against the right edge, so a column of them lines up whatever the text beside it. */
|
||||
.tool .item {
|
||||
display: flex;
|
||||
padding: 3dp 8dp;
|
||||
margin: 0 0 1dp -8dp;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
.tool .item .text {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.tool .item .detail {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 16dp;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tool .item.selected {
|
||||
background-color: #33507a;
|
||||
decorator: linear-gradient(90deg, #4a6ea8, #33507a);
|
||||
color: #ffd35a;
|
||||
}
|
||||
|
||||
/* A gap a tool asked for. */
|
||||
.tool .blank {
|
||||
height: 12dp;
|
||||
}
|
||||
|
||||
/* Two tools draw their own picture into the overlay, which is underneath the document: the light
|
||||
gun's targets and the display's test pattern. Those pages let it through, and the card becomes
|
||||
a strip along the bottom between the two lower targets, so nothing worth aiming at is covered.
|
||||
Its width is fixed: left to fit its content, the value column of a pair had no room and broke
|
||||
its words one letter to a line. */
|
||||
.tool.clear .card {
|
||||
flex: 0 0 auto;
|
||||
margin-top: auto;
|
||||
align-self: center;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.tool.clear h1 {
|
||||
font-size: 16dp;
|
||||
padding: 4dp 12dp;
|
||||
}
|
||||
|
||||
/* The strip is as tall as its rows: the columns, which fill the page otherwise, take what they
|
||||
need here. The body inside them keeps its flex, which in their row is its width; without it
|
||||
a pair's value column had no room. */
|
||||
.tool.clear .columns {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tool.clear #toolBody {
|
||||
padding: 6dp 12dp;
|
||||
}
|
||||
|
||||
.tool.clear #toolKeys {
|
||||
padding: 4dp 12dp;
|
||||
}
|
||||
|
||||
/* The help under the list of tools, away from the list itself. */
|
||||
#toolHelp {
|
||||
margin-top: 12dp;
|
||||
}
|
||||
BIN
assets/menuBackground.mkv
(Stored with Git LFS)
BIN
assets/menuBackground.mkv
(Stored with Git LFS)
Binary file not shown.
BIN
assets/menuIntro.flac
(Stored with Git LFS)
BIN
assets/menuIntro.flac
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* The score panel's look: an arcade scoreboard's seven segment readouts, each a row of dim
|
||||
* "all segments lit" digits with the live value glowing over them, so an unused digit still
|
||||
* shows its ghost the way a real display does. This copy is rewritten at every start: a game
|
||||
* restyles the panel with its own copies of scoreBezel.rml and a sheet beside its script.
|
||||
* Licensed under the GNU General Public License, version 3 or later.
|
||||
*/
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: FreeSans;
|
||||
font-weight: bold;
|
||||
color: #ff3018;
|
||||
}
|
||||
|
||||
/* The cabinet the readouts are set into. */
|
||||
#board {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 12dp 24dp;
|
||||
background-color: #060606;
|
||||
decorator: linear-gradient(180deg, #1a1a1c, #050505);
|
||||
border: 3dp #3a3a40;
|
||||
border-radius: 10dp;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 0 10dp;
|
||||
}
|
||||
|
||||
/* An empty second player cell leaves the row to the other two. */
|
||||
.cell:empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 20dp;
|
||||
letter-spacing: 3dp;
|
||||
color: #c8b070;
|
||||
margin: 0 0 4dp 0;
|
||||
}
|
||||
|
||||
/* A readout stacks the live digits exactly over the dim ones, so both are the same size and
|
||||
the same shape whatever the value is. The slant is the LED look. */
|
||||
.readout {
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 54dp;
|
||||
letter-spacing: 6dp;
|
||||
transform: skewX(-7deg);
|
||||
}
|
||||
|
||||
.readout.small {
|
||||
font-size: 34dp;
|
||||
letter-spacing: 3dp;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
display: block;
|
||||
color: #4d1108;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
color: #ff3018;
|
||||
font-effect: glow(2dp 5dp #ff6a3cc0);
|
||||
}
|
||||
|
||||
/* The life count sits under the score, smaller, with its own label beside it. */
|
||||
.lives {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 6dp 0 0 0;
|
||||
}
|
||||
|
||||
.lives .label {
|
||||
font-size: 16dp;
|
||||
margin: 0 8dp 0 0;
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<rml>
|
||||
<!--
|
||||
Singe 3
|
||||
Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
|
||||
The score panel: the document behind the scoreBezel calls, an arcade scoreboard with credits
|
||||
and a score and a life count for each of two players. The engine writes the digits into the
|
||||
.value elements and empties #player2 when the second score is switched off, so everything the
|
||||
panel looks like lives in Singe/scoreBezel.rcss and everything it says comes from the game. The
|
||||
Singe folder is rewritten at every start, so a game wanting another look ships its own copy of
|
||||
this file as scoreBezel.rml beside its script, linking a sheet of its own, and the engine uses
|
||||
that instead.
|
||||
Licensed under the GNU General Public License, version 3 or later.
|
||||
-->
|
||||
<head>
|
||||
<title>Score</title>
|
||||
<link type="text/rcss" href="Singe/scoreBezel.rcss"/>
|
||||
</head>
|
||||
<body id="score">
|
||||
<div id="board">
|
||||
<div class="cell" id="player1">
|
||||
<div class="label">1UP</div>
|
||||
<div class="readout"><div class="ghost">888888</div><div class="value" id="score1">000000</div></div>
|
||||
<div class="lives"><div class="label">LIVES</div><div class="readout small"><div class="ghost">8</div><div class="value" id="lives1">0</div></div></div>
|
||||
</div>
|
||||
<div class="cell" id="middle">
|
||||
<div class="label">CREDITS</div>
|
||||
<div class="readout"><div class="ghost">88</div><div class="value" id="credits">00</div></div>
|
||||
</div>
|
||||
<div class="cell" id="player2">
|
||||
<div class="label">2UP</div>
|
||||
<div class="readout"><div class="ghost">888888</div><div class="value" id="score2">000000</div></div>
|
||||
<div class="lives"><div class="label">LIVES</div><div class="readout small"><div class="ghost">8</div><div class="value" id="lives2">0</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
-- settings.cfg: the options you would otherwise type on every command line.
|
||||
--
|
||||
-- Copy this file to settings.cfg and uncomment what you want. Singe reads it
|
||||
-- from the same four places it reads controls.cfg, in this order, and a later
|
||||
-- one wins key by key:
|
||||
--
|
||||
-- 1. the game's own script directory: what the game ships
|
||||
-- 2. the directory Singe was started in: the machine's own
|
||||
-- 3. the directory above the game's data directory
|
||||
-- 4. the game's data directory: yours, for that one game
|
||||
--
|
||||
-- Every key is the long name of a command line option without the leading
|
||||
-- dashes, so there is no second vocabulary to learn, and the command line
|
||||
-- always wins over anything set here. A games.dat entry for the game does
|
||||
-- too, because what is here is a default and what is there describes the game.
|
||||
-- A key that is not an option, or one that names the game or a path, is an
|
||||
-- error naming this file and the key.
|
||||
--
|
||||
-- Options that take no value on the command line take true or false here.
|
||||
-- Setting one to false is the same as leaving it out.
|
||||
--
|
||||
-- The value shown against each key is its default, or an example where the
|
||||
-- default is worked out from your display or from the game.
|
||||
|
||||
|
||||
-- The window ------------------------------------------------------------
|
||||
|
||||
-- screen = 1 -- which display, counting from 1; without it, the primary one
|
||||
-- xresolution = 1280 -- window width in pixels; without it, one that fits the display
|
||||
-- yresolution = 960 -- window height in pixels; without it, one that fits the width
|
||||
-- aspect = "16:9" -- which window size table to pick from: 4:3, 16:9 or 16:10
|
||||
-- fullscreen = false -- exclusive full screen at the desktop resolution
|
||||
-- fullscreen_window = false -- borderless window covering the desktop
|
||||
-- nologos = false -- skip the splash screens
|
||||
-- canvas = "720x480" -- world size for a game with no disc
|
||||
-- scenequality = "high" -- 3D detail: high, or low for a small GPU (half resolution, no antialiasing, no bloom)
|
||||
|
||||
|
||||
-- Where the picture sits -------------------------------------------------
|
||||
|
||||
-- stretch = false -- fill the window instead of keeping the video's shape
|
||||
-- scalefactor = 100 -- per cent of the window the picture fills, 25 to 100
|
||||
-- shiftx = 0 -- move it inside the room scalefactor left, -100 to 100
|
||||
-- shifty = 0
|
||||
-- rotate = 0 -- turn the whole presentation clockwise: 0, 90, 180 or 270
|
||||
-- linearscale = true -- smooth the overlay as the window scales it, or take the nearest pixel
|
||||
-- monochrome = false -- start with the disc picture in gray
|
||||
|
||||
|
||||
-- Cabinet artwork --------------------------------------------------------
|
||||
|
||||
-- bezel = "cabinet.png" -- artwork in the bezels folder, drawn around the picture
|
||||
-- bezeldir = "bezels" -- the folder holding it
|
||||
-- bezelflip = false -- draw the artwork in front of the picture instead of behind it
|
||||
-- sindenedge = "video" -- where a Sinden border sits: video, or window with artwork loaded
|
||||
|
||||
|
||||
-- Sound ------------------------------------------------------------------
|
||||
|
||||
-- nosound = false -- mute everything for the whole run
|
||||
-- startsilent = false -- start muted until the first input of any kind
|
||||
-- volume_vldp = 100 -- laserdisc volume in per cent
|
||||
-- volume_nonvldp = 100 -- sound effect and extra video volume in per cent
|
||||
-- audio = 0 -- which audio track of the disc video to play
|
||||
-- altaudio = "" -- play <base><suffix>.ogg beside the disc video instead
|
||||
-- audiodelay = 0 -- milliseconds the audio is heard late (negative if early)
|
||||
-- soundfont = "" -- the .sf2 a MIDI file is synthesized with; without it, a search
|
||||
|
||||
|
||||
-- Input ------------------------------------------------------------------
|
||||
|
||||
-- nomouse = false -- ignore mice and light guns
|
||||
-- nogamepad = false -- ignore gamepads
|
||||
-- nocrosshair = false -- ask the game not to draw its own crosshair
|
||||
-- haptic = 4 -- strongest rumble step a game may use; 0 turns rumble off
|
||||
-- keymapfile = "controls.cfg" -- read the mappings from this file and make no search
|
||||
-- sindengun = "10 5" -- the Sinden light gun border
|
||||
-- xratio = 0 -- gun coordinate scale a game reads with ratioGetX()
|
||||
-- yratio = 0 -- ... and ratioGetY()
|
||||
|
||||
-- mapjoysticks = true -- write a gamepad mapping for a device SDL does not recognize
|
||||
-- gamepad_reorder = "3210" -- which pad fills which slot, as enumeration positions from 0
|
||||
-- trigger_threshold = 0 -- per cent of full travel a trigger counts as pressed at; 0 uses DEAD_ZONE
|
||||
-- joymouse = false -- let the first gamepad's left stick drive the mouse cursor
|
||||
-- js_range = 5 -- how fast it drives it, 1 to 20
|
||||
-- manymouse = false -- tell the mice apart whatever the game asks for
|
||||
-- absolutes_only = false -- keep only the mice that report absolute positions, which light guns do
|
||||
|
||||
|
||||
-- Cabinets and front ends ------------------------------------------------
|
||||
|
||||
-- idleexit = 0 -- quit after this many seconds with no input; 0 never does
|
||||
-- no_update_check = false -- never look for a newer Singe from the menu (Engine Update still works by hand)
|
||||
-- quitchord = "back+start" -- first gamepad buttons held together that quit whatever a game maps
|
||||
-- fvalue = 0 -- one number handed to the game, which reads it with getFValue()
|
||||
|
||||
|
||||
-- Working on a game ------------------------------------------------------
|
||||
|
||||
-- reload = false -- rerun the game when one of its loose script files changes
|
||||
-- debug = false -- connect every game to ZeroBrane Studio's debugger; true for this
|
||||
-- machine's, or "HOST:PORT" for one elsewhere, as a cabinet would.
|
||||
-- With nothing listening, each start waits up to two seconds
|
||||
-- program = false -- trace engine activity to trace.txt
|
||||
-- perfsync = false -- with program, show the GPU's time in each stage of the perf lines; slows everything
|
||||
-- trace = false -- trace every Lua call to trace.txt
|
||||
-- showcalculated = false -- print the frame ranges of every framefile segment
|
||||
-- softwarevideo = false -- decode video in software even when the machine can do it in hardware
|
||||
-- zerocopy = true -- hand hardware decoded frames to the GPU without copying them, where it can
|
||||
-- deinterlace = "auto" -- what an interlaced picture gets: off, auto or on. A laserdisc held
|
||||
-- interlaced fields, and a rip that kept them combs on a progressive
|
||||
-- display; auto touches only the frames the file marks interlaced
|
||||
-- deterministic = 15 -- testing only: ignore real time, run on a virtual clock stepped this
|
||||
-- many milliseconds a frame and seed the random generators with the same
|
||||
-- number, so a run repeats to the pixel. true takes the default 15.
|
||||
-- Audio and pacing are meaningless in this mode.
|
||||
BIN
assets/singeLogo.xcf
(Stored with Git LFS)
BIN
assets/singeLogo.xcf
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* What a subtitle looks like: centered lines of light text on a dark bar just wide enough to hold
|
||||
* them, at whatever height the game asked for, over the picture. The engine only ever sets the
|
||||
* contents of #slot and the top of .cue. This copy is rewritten at every start: a game restyles
|
||||
* its subtitles with its own copies of subtitle.rml and a sheet beside its script.
|
||||
* Licensed under the GNU General Public License, version 3 or later.
|
||||
*/
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: FreeSans;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* The document covers the picture, so a cue is placed down it as a per cent. */
|
||||
#slot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cue {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* The bar hugs the text rather than spanning the picture, as a subtitle's does. */
|
||||
.cue .text {
|
||||
display: inline-block;
|
||||
padding: 4dp 18dp;
|
||||
font-size: 30dp;
|
||||
line-height: 36dp;
|
||||
color: #ffffff;
|
||||
background-color: #141414e0;
|
||||
border-radius: 4dp;
|
||||
font-effect: shadow(1dp 1dp #000000ff);
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<rml>
|
||||
<!--
|
||||
Singe 3
|
||||
Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
|
||||
Subtitles: the document behind srtLoad, srtEnable, srtPosition and overlayBanner. The engine
|
||||
writes one cue, or one banner, into #slot as a .cue whose inline style carries the height it was
|
||||
asked for, so everything a subtitle looks like lives in Singe/subtitle.rcss and everything it
|
||||
says comes from the game or from its .srt file. The Singe folder is rewritten at every start, so
|
||||
a game wanting another look ships its own copy of this file as subtitle.rml beside its script,
|
||||
linking a sheet of its own, and the engine uses that instead.
|
||||
Licensed under the GNU General Public License, version 3 or later.
|
||||
-->
|
||||
<head>
|
||||
<title>Subtitles</title>
|
||||
<link type="text/rcss" href="Singe/subtitle.rcss"/>
|
||||
</head>
|
||||
<body id="subtitle">
|
||||
<div id="slot"></div>
|
||||
</body>
|
||||
</rml>
|
||||
231
build-all.sh
231
build-all.sh
|
|
@ -1,231 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# Builds Singe and every vendored library for one platform, or all of them. The work is done
|
||||
# by the CMake superbuild (cmake/Superbuild.cmake, one preset per platform in
|
||||
# CMakePresets.json); this script installs the host packages and calls it:
|
||||
#
|
||||
# ./build-all.sh every supported platform
|
||||
# ./build-all.sh linux x86_64 one platform
|
||||
#
|
||||
# Anything cmake --build accepts can follow the platform, so a single library can be redone:
|
||||
#
|
||||
# ./build-all.sh linux x86_64 --target rebuild-ffmpeg
|
||||
|
||||
G_BUILDDIR=.builddir
|
||||
G_INSTALL="$(dirname "$0")/INSTALL"
|
||||
|
||||
# The one copy of the host package list. Everything else that names these packages derives from it.
|
||||
# Grouped by what wants them, because the SDL3 half is long and a bare list gives no clue why a
|
||||
# package is here: SDL3 compiles a backend whenever it finds the headers, so a package missing from
|
||||
# this list does not fail the build, it quietly drops a backend from the binary.
|
||||
G_HOSTPACKAGES=(
|
||||
# Toolchain
|
||||
build-essential
|
||||
cmake
|
||||
git-lfs
|
||||
pkg-config
|
||||
perl
|
||||
nasm
|
||||
llvm
|
||||
autoconf
|
||||
automake
|
||||
libtool
|
||||
# dav1d, the AV1 decoder, is the one vendored library whose own build system is Meson
|
||||
meson
|
||||
ninja-build
|
||||
# Content the build generates: embedded images, the menu video, the LuaSec table, the manual
|
||||
imagemagick
|
||||
ffmpeg
|
||||
lua5.4
|
||||
asciidoctor
|
||||
ruby-asciidoctor-pdf
|
||||
# Video decode and display
|
||||
libva-dev
|
||||
libvdpau-dev
|
||||
libdrm-dev
|
||||
libgl-dev
|
||||
libegl-dev
|
||||
libgles-dev
|
||||
libgbm-dev
|
||||
# SDL3 audio backends
|
||||
libasound2-dev
|
||||
libpulse-dev
|
||||
libpipewire-0.3-dev
|
||||
libjack-jackd2-dev
|
||||
libsndio-dev
|
||||
# SDL3 device hotplug, desktop integration and input methods
|
||||
libudev-dev
|
||||
libdbus-1-dev
|
||||
libibus-1.0-dev
|
||||
libxkbcommon-dev
|
||||
# SDL3 X11 video backend and the extensions it uses
|
||||
libx11-dev
|
||||
libxext-dev
|
||||
libxfixes-dev
|
||||
libxi-dev
|
||||
libxcursor-dev
|
||||
libxrandr-dev
|
||||
libxss-dev
|
||||
libxtst-dev
|
||||
# SDL3 Wayland video backend
|
||||
libwayland-dev
|
||||
wayland-protocols
|
||||
libdecor-0-dev
|
||||
)
|
||||
|
||||
G_INSTALLLEAD="Host packages (what build-all.sh installs):"
|
||||
G_INSTALLWIDTH=72
|
||||
|
||||
|
||||
function buildAll() {
|
||||
local OS=$1
|
||||
local ARCH=$2
|
||||
shift 2
|
||||
|
||||
# Every platform builds with the zig the build fetches itself; the Pi preset also fetches its
|
||||
# platform packages and macOS needs the Apple SDK tarball (see INSTALL).
|
||||
# "macos universal" builds both macOS presets and joins them with llvm-lipo.
|
||||
if [[ "${OS}" == "macos" && "${ARCH}" == "universal" ]]; then
|
||||
buildAll macos aarch64 "$@"
|
||||
buildAll macos x86_64 "$@"
|
||||
cmake -P cmake/zig/macosUniversal.cmake
|
||||
return
|
||||
fi
|
||||
|
||||
cmake --preset ${OS}-${ARCH}
|
||||
cmake --build --preset ${OS}-${ARCH} "$@"
|
||||
}
|
||||
|
||||
|
||||
# assets/ and media/ hold the artwork, the font, the models, and the menu video in Git LFS. A clone made without git-lfs
|
||||
# leaves pointer stubs -- a hundred and thirty bytes of text where a video should be -- and the
|
||||
# resource generators embed them without complaining, so the failure only shows up at run time as a
|
||||
# menu with no background. One sentinel is enough to tell the two apart.
|
||||
function checkoutHydrated() {
|
||||
local SENTINEL="$(dirname "$0")/assets/menuBackground.mkv"
|
||||
|
||||
# A sentinel that is not there at all is not a hydrated checkout either, and saying so beats
|
||||
# passing quietly because the path was wrong.
|
||||
[[ -f "${SENTINEL}" ]] || return 1
|
||||
! head -c 64 "${SENTINEL}" | grep -q "git-lfs.github.com/spec"
|
||||
}
|
||||
|
||||
|
||||
# The package list as INSTALL wants to read it: one sentence, wrapped, ending in a full stop.
|
||||
function installParagraph() {
|
||||
local LINE="${G_INSTALLLEAD}"
|
||||
local SEP=" "
|
||||
local WORD
|
||||
|
||||
for WORD in "${G_HOSTPACKAGES[@]}"; do
|
||||
if (( ${#LINE} + ${#SEP} + ${#WORD} > G_INSTALLWIDTH )); then
|
||||
printf '%s\n' "${LINE}"
|
||||
LINE="${WORD}"
|
||||
else
|
||||
LINE="${LINE}${SEP}${WORD}"
|
||||
fi
|
||||
SEP=" "
|
||||
done
|
||||
printf '%s.\n' "${LINE}"
|
||||
}
|
||||
|
||||
|
||||
# What INSTALL says today: from the lead line to the first line that ends the sentence.
|
||||
function installCurrent() {
|
||||
awk -v lead="${G_INSTALLLEAD}" '
|
||||
index($0, lead) == 1 { found = 1 }
|
||||
found { print; if (/\.$/) exit }
|
||||
' "${G_INSTALL}"
|
||||
}
|
||||
|
||||
|
||||
function installMatches() {
|
||||
# A checkout without INSTALL is not worth nagging about; there is nothing to disagree with.
|
||||
[[ ! -f "${G_INSTALL}" ]] || [[ "$(installParagraph)" == "$(installCurrent)" ]]
|
||||
}
|
||||
|
||||
|
||||
# Writes the paragraph back into INSTALL, replacing whatever sentence is there now.
|
||||
function installSync() {
|
||||
local TEMP
|
||||
|
||||
TEMP=$(mktemp)
|
||||
awk -v lead="${G_INSTALLLEAD}" -v replacement="$(installParagraph)" '
|
||||
index($0, lead) == 1 && !done {
|
||||
print replacement
|
||||
skip = 1
|
||||
}
|
||||
skip { if (/\.$/) { skip = 0; done = 1 } next }
|
||||
{ print }
|
||||
' "${G_INSTALL}" > "${TEMP}"
|
||||
mv "${TEMP}" "${G_INSTALL}"
|
||||
}
|
||||
|
||||
|
||||
# -e = stop script on errors
|
||||
# -u = stop script on undefined variable
|
||||
# -o pipefail = stop pipeline if any step fails
|
||||
set -euo pipefail
|
||||
|
||||
case "${1:-}" in
|
||||
--packages)
|
||||
printf '%s\n' "${G_HOSTPACKAGES[@]}"
|
||||
exit 0
|
||||
;;
|
||||
--sync-install)
|
||||
installSync
|
||||
echo "INSTALL rewritten from build-all.sh."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p ${G_BUILDDIR}
|
||||
|
||||
|
||||
# Host packages the build needs on a Debian based system. INSTALL prints the same list for a
|
||||
# reader who has not run anything yet, so it is written from this one: --sync-install rewrites
|
||||
# that paragraph and every build checks it, which is why there is no second list to keep in step.
|
||||
if ! installMatches; then
|
||||
echo "warning: INSTALL's host package list no longer matches this script."
|
||||
echo " Run ./build-all.sh --sync-install to write it from here."
|
||||
fi
|
||||
sudo apt-get install -y "${G_HOSTPACKAGES[@]}"
|
||||
|
||||
|
||||
if ! checkoutHydrated; then
|
||||
echo "error: assets/ and media/ hold git-lfs pointer files, not the real artwork, font and video."
|
||||
echo " The build would embed those stubs and produce a broken binary."
|
||||
echo " Fix the checkout with: git lfs install && git lfs pull"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [[ $# -ge 2 ]]; then
|
||||
buildAll "$@" 2>&1 | tee ${G_BUILDDIR}/$1-$2.log
|
||||
else
|
||||
buildAll linux x86_64 2>&1 | tee ${G_BUILDDIR}/linux-x86_64.log
|
||||
buildAll macos universal 2>&1 | tee ${G_BUILDDIR}/macos-universal.log
|
||||
buildAll linux aarch64 2>&1 | tee ${G_BUILDDIR}/linux-aarch64.log
|
||||
buildAll windows x86_64 2>&1 | tee ${G_BUILDDIR}/windows-x86_64.log
|
||||
buildAll windows aarch64 2>&1 | tee ${G_BUILDDIR}/windows-aarch64.log
|
||||
fi
|
||||
131
build-docs.sh
131
build-docs.sh
|
|
@ -1,131 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build the three books in every language they are kept in: the user manual (docs/Manual.adoc,
|
||||
# which ships inside the binary), the programming reference (docs/Reference.adoc), and the
|
||||
# beginner's course (docs/Learn.adoc), in English and in each translation, a folder under docs/
|
||||
# holding a locale.adoc (docs/fr, docs/it, docs/de, docs/es). Produces .builddir/<Book>.html and
|
||||
# .pdf for English and .builddir/<language>/<Book>.html and .pdf for each translation (a language's
|
||||
# books sit together, so the links between them work), and then packs the two books that do not
|
||||
# ship in the binary, with the lesson files and the license, into .builddir/Singe-Docs-v<version>.zip
|
||||
# and .builddir/Singe-Docs-v<version>-<language>.zip, the optional documentation downloads. The
|
||||
# version comes from CMakeLists.txt. Last, the ZeroBrane Studio package's API in every language,
|
||||
# zbstudio/packages/singe/api*.lua, is made again from the Reference (util/zbstudioApi.py).
|
||||
#
|
||||
# ./build-docs.sh the books as they ship
|
||||
# ./build-docs.sh preview the same, with PREVIEW across every page, for proofreaders, all of it
|
||||
# under .builddir/preview so neither a release nor the binary's own
|
||||
# copies of the manual (rendered by CMake into .builddir) are touched;
|
||||
# the downloads are named -preview so they cannot be mistaken for a
|
||||
# release, and they carry the Manual too, which a release download
|
||||
# leaves to the binary.
|
||||
set -euo pipefail
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
preview=
|
||||
suffix=
|
||||
if [[ "${1:-}" == "preview" ]]; then
|
||||
preview=1
|
||||
suffix=-preview
|
||||
elif [[ -n "${1:-}" ]]; then
|
||||
echo "usage: $0 [preview]" >&2
|
||||
exit 2
|
||||
fi
|
||||
books=(Manual Learn Reference)
|
||||
|
||||
# English is docs/ itself; every other language is a folder of docs/ with its captions in it.
|
||||
languages=(en)
|
||||
for locale in "$here"/docs/*/locale.adoc; do
|
||||
languages+=("$(basename "$(dirname "$locale")")")
|
||||
done
|
||||
|
||||
for language in "${languages[@]}"; do
|
||||
for book in "${books[@]}"; do
|
||||
doc=$here/docs/$book.adoc
|
||||
if [[ $language != en ]]; then
|
||||
doc=$here/docs/$language/$book.adoc
|
||||
fi
|
||||
if [[ ! -f $doc ]]; then
|
||||
echo "error: $doc not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
for tool in asciidoctor asciidoctor-pdf zip; do
|
||||
if ! command -v "$tool" > /dev/null; then
|
||||
echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The version, from the four numbers of the project() line: patch 1 means a beta and tweak says
|
||||
# which, so 3.00.1.2 is the books' "3.00b2", exactly as the binary is named.
|
||||
read -r major minor patch tweak <<< "$(sed -n 's/^project(singe2 VERSION \([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1 \2 \3 \4/p' "$here/CMakeLists.txt")"
|
||||
if [[ $patch == 1 ]]; then
|
||||
version="${major}.${minor}b${tweak}"
|
||||
else
|
||||
version="${major}.${minor}"
|
||||
fi
|
||||
|
||||
# compress deflates the PDFs' streams, which takes most of them to under half their size. The
|
||||
# watermark, when asked for: a page background for the PDFs, a head snippet for the HTML.
|
||||
pdfOpts=(-a compress)
|
||||
# The HTML pages carry their pictures inside them (data-uri), so a page is one file wherever it is
|
||||
# copied and the download needs no images folder beside it.
|
||||
htmlOpts=(-a data-uri)
|
||||
out=$here/.builddir
|
||||
if [[ -n $preview ]]; then
|
||||
pdfOpts+=(-a "page-background-image=image:$here/docs/images/preview.svg[fit=fill]")
|
||||
htmlOpts+=(-a docinfo=shared -a "docinfodir=$here/docs/preview")
|
||||
out=$here/.builddir/preview
|
||||
fi
|
||||
|
||||
built=()
|
||||
for language in "${languages[@]}"; do
|
||||
src=$here/docs
|
||||
dest=$out
|
||||
name=Singe-Docs-v$version
|
||||
if [[ $language != en ]]; then
|
||||
src=$here/docs/$language
|
||||
dest=$out/$language
|
||||
name=Singe-Docs-v$version-$language
|
||||
fi
|
||||
mkdir -p "$dest"
|
||||
for book in "${books[@]}"; do
|
||||
asciidoctor "${htmlOpts[@]}" -a revnumber="$version" "$src/$book.adoc" -o "$dest/$book.html"
|
||||
asciidoctor-pdf "${pdfOpts[@]}" -a revnumber="$version" "$src/$book.adoc" -o "$dest/$book.pdf"
|
||||
built+=("$dest/$book.html" "$dest/$book.pdf")
|
||||
done
|
||||
|
||||
# The documentation download: everything but the user manual, which the binary carries, except
|
||||
# in a preview, where the Manual goes to the proofreaders with the rest. The README's lines marked
|
||||
# @PREVIEW@ are kept for a preview and dropped for a release. Built in a staging folder so the zip
|
||||
# has one top-level directory and no stray build files.
|
||||
stage=$out/$name$suffix
|
||||
rm -rf "$stage" "$stage.zip"
|
||||
mkdir -p "$stage/learn"
|
||||
cp "$dest/Reference.pdf" "$dest/Reference.html" "$dest/Learn.pdf" "$dest/Learn.html" "$stage/"
|
||||
previewLines=/^@PREVIEW@/d
|
||||
if [[ -n $preview ]]; then
|
||||
cp "$dest/Manual.pdf" "$dest/Manual.html" "$stage/"
|
||||
previewLines=s/^@PREVIEW@//
|
||||
fi
|
||||
cp -r "$here/docs/learn/." "$stage/learn/"
|
||||
cp "$here/COPYING" "$stage/"
|
||||
sed -e "s/@VERSION@/$version/" -e "$previewLines" "$src/downloadReadme.txt" > "$stage/README.txt"
|
||||
(cd "$out" && zip -q -r "$name$suffix.zip" "$name$suffix")
|
||||
rm -rf "$stage"
|
||||
built+=("$stage.zip")
|
||||
done
|
||||
|
||||
# ZeroBrane Studio's autocompletion and tooltips are made from the Reference, so they are made again
|
||||
# with it, from the names a Singe built here defines; the next build embeds them. Without that
|
||||
# Singe or python3 they stay as they are, and this says so. A preview leaves them alone.
|
||||
if [[ -z $preview ]]; then
|
||||
if command -v python3 > /dev/null && [[ -x $here/.builddir/Singe-v$version-Linux-x86_64 ]]; then
|
||||
python3 "$here/util/zbstudioApi.py"
|
||||
else
|
||||
echo "note: ZeroBrane Studio API left as it was (needs python3 and .builddir/Singe-v$version-Linux-x86_64)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "built:"
|
||||
ls -la "${built[@]}"
|
||||
|
|
@ -1,478 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# Superbuild: builds every vendored library into BUILD_DIR, then Singe against them.
|
||||
# Entered from CMakeLists.txt when SINGE_SUPERBUILD is ON (the presets set it). Each
|
||||
# library is an ExternalProject with its own configure, build and install steps, so a
|
||||
# change to one rebuilds only it and what depends on it:
|
||||
# cmake --build --preset linux-x86_64 --target rebuild-ffmpeg
|
||||
# The toolchain environment (PATH, SYSROOT, osxcross variables) comes from
|
||||
# ../toolchains/toolchains.sh; build-all.sh sources it before calling cmake.
|
||||
|
||||
include(ExternalProject)
|
||||
include(ProcessorCount)
|
||||
|
||||
ProcessorCount(SB_JOBS)
|
||||
if(SB_JOBS EQUAL 0)
|
||||
set(SB_JOBS 4)
|
||||
endif()
|
||||
|
||||
# Target description: the presets pass these; a plain cmake invocation can take them from
|
||||
# the environment toolchains.sh exports.
|
||||
foreach(pair "SINGE_TRIPLE;TRIPLE" "SINGE_CROSS_OS;CROSS_OS" "SINGE_SUFFIX;SUFFIX")
|
||||
list(GET pair 0 var)
|
||||
list(GET pair 1 env)
|
||||
if(NOT DEFINED ${var})
|
||||
set(${var} "$ENV{${env}}")
|
||||
endif()
|
||||
endforeach()
|
||||
if(NOT SINGE_TRIPLE OR NOT SINGE_CROSS_OS)
|
||||
message(FATAL_ERROR "SINGE_TRIPLE and SINGE_CROSS_OS must be set (use a preset or source toolchains.sh).")
|
||||
endif()
|
||||
|
||||
set(SB_PREFIX ${BUILD_DIR})
|
||||
set(SB_THIRDPARTY ${CMAKE_SOURCE_DIR}/thirdparty)
|
||||
file(MAKE_DIRECTORY ${SB_PREFIX}/include ${SB_PREFIX}/lib)
|
||||
|
||||
# Compilers as the toolchain resolved them; the configure based libraries get them by name.
|
||||
set(SB_CC "${CMAKE_C_COMPILER}")
|
||||
if(CMAKE_CXX_COMPILER)
|
||||
set(SB_CXX "${CMAKE_CXX_COMPILER}")
|
||||
elseif(DEFINED ENV{CXX})
|
||||
set(SB_CXX "$ENV{CXX}")
|
||||
else()
|
||||
string(REGEX REPLACE "gcc$" "g++" SB_CXX "${SB_CC}")
|
||||
string(REGEX REPLACE "clang$" "clang++" SB_CXX "${SB_CXX}")
|
||||
endif()
|
||||
if(CMAKE_RANLIB)
|
||||
set(SB_RANLIB "${CMAKE_RANLIB}")
|
||||
elseif(DEFINED ENV{RANLIB})
|
||||
set(SB_RANLIB "$ENV{RANLIB}")
|
||||
else()
|
||||
set(SB_RANLIB ranlib)
|
||||
endif()
|
||||
|
||||
# The flags build-all.sh exported for every library: our install tree first, then the
|
||||
# platform's. The Pi sysroot and the osxcross linker come from the toolchain.
|
||||
set(SB_CFLAGS_BASE "$ENV{CFLAGS}")
|
||||
set(SB_LDFLAGS "-L${SB_PREFIX}/lib $ENV{LDFLAGS}")
|
||||
if(CMAKE_SYSROOT)
|
||||
set(SB_CFLAGS_BASE "--sysroot=${CMAKE_SYSROOT} ${SB_CFLAGS_BASE}")
|
||||
endif()
|
||||
set(SB_CFLAGS "-I${SB_PREFIX}/include ${SB_CFLAGS_BASE}")
|
||||
set(SB_PKG_CONFIG_LIBDIR "${SB_PREFIX}/lib/pkgconfig")
|
||||
# A cross build looks inside its sysroot; a build for this machine looks at this machine. The
|
||||
# sysroot is what tells them apart, not the platform's name.
|
||||
if(SINGE_SYSROOT)
|
||||
set(SB_PKG_CONFIG_LIBDIR "${SB_PKG_CONFIG_LIBDIR}:${SINGE_SYSROOT}/usr/lib/${SINGE_TRIPLE}/pkgconfig:${SINGE_SYSROOT}/usr/lib/pkgconfig:${SINGE_SYSROOT}/usr/share/pkgconfig")
|
||||
elseif(KANGAROO_OS STREQUAL "linux")
|
||||
set(SB_PKG_CONFIG_LIBDIR "${SB_PKG_CONFIG_LIBDIR}:/usr/lib/${SINGE_TRIPLE}/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig")
|
||||
elseif(DEFINED ENV{PKG_CONFIG_LIBDIR})
|
||||
set(SB_PKG_CONFIG_LIBDIR "${SB_PKG_CONFIG_LIBDIR}:$ENV{PKG_CONFIG_LIBDIR}")
|
||||
endif()
|
||||
|
||||
# Environment every step runs under. CFLAGS is a parameter because SDL3_image's vendored
|
||||
# libpng must not see the zlib headers under the install prefix.
|
||||
function(singeStepEnv outVar cflags)
|
||||
set(env ${CMAKE_COMMAND} -E env
|
||||
"CC=${SB_CC}"
|
||||
"CXX=${SB_CXX}"
|
||||
"RANLIB=${SB_RANLIB}"
|
||||
"CFLAGS=${cflags}"
|
||||
"CXXFLAGS=${cflags}"
|
||||
"LDFLAGS=${SB_LDFLAGS}"
|
||||
"LD_LIBRARY_PATH=${SB_PREFIX}/lib"
|
||||
"PKG_CONFIG_LIBDIR=${SB_PKG_CONFIG_LIBDIR}"
|
||||
"MAKEFLAGS=-j${SB_JOBS}"
|
||||
)
|
||||
if(CMAKE_AR)
|
||||
list(APPEND env "AR=${CMAKE_AR}")
|
||||
endif()
|
||||
if(SINGE_SYSROOT)
|
||||
list(APPEND env "PKG_CONFIG_SYSROOT_DIR=${SINGE_SYSROOT}")
|
||||
endif()
|
||||
set(${outVar} ${env} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
singeStepEnv(SB_ENV "${SB_CFLAGS}")
|
||||
singeStepEnv(SB_ENV_NO_PREFIX_INCLUDE "${SB_CFLAGS_BASE}")
|
||||
|
||||
set(SB_CMAKE_ARGS
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_INSTALL_PREFIX=${SB_PREFIX}
|
||||
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
|
||||
)
|
||||
|
||||
# A vendored CMake project: name, source directory, its dependencies, extra cache arguments,
|
||||
# and the step environment to use.
|
||||
function(singeCmakeProject name source deps args env)
|
||||
set(binary ${SB_PREFIX}/build/${name})
|
||||
ExternalProject_Add(${name}
|
||||
SOURCE_DIR ${source}
|
||||
BINARY_DIR ${binary}
|
||||
DEPENDS ${deps}
|
||||
CONFIGURE_COMMAND ${env} ${CMAKE_COMMAND} -S ${source} -B ${binary} ${SB_CMAKE_ARGS} ${args}
|
||||
BUILD_COMMAND ${env} ${CMAKE_COMMAND} --build ${binary} --parallel ${SB_JOBS}
|
||||
INSTALL_COMMAND ${env} ${CMAKE_COMMAND} --build ${binary} --target install
|
||||
LOG_CONFIGURE ON LOG_BUILD ON LOG_INSTALL ON LOG_OUTPUT_ON_FAILURE ON
|
||||
)
|
||||
singeRebuildTarget(${name})
|
||||
endfunction()
|
||||
|
||||
# rebuild-<name> forgets the stamps and the build directory and builds the library again, then whatever depends on it
|
||||
# is rebuilt by the normal build.
|
||||
function(singeRebuildTarget name)
|
||||
ExternalProject_Get_Property(${name} STAMP_DIR BINARY_DIR)
|
||||
add_custom_target(rebuild-${name}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf ${STAMP_DIR} ${BINARY_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target ${name}
|
||||
COMMENT "Rebuilding ${name}"
|
||||
)
|
||||
endfunction()
|
||||
|
||||
|
||||
# ===== zlib (configure script; the generated zconf.h lives in the build directory) =====
|
||||
|
||||
set(zlibBinary ${SB_PREFIX}/build/zlib)
|
||||
ExternalProject_Add(zlib
|
||||
SOURCE_DIR ${SB_THIRDPARTY}/zlib
|
||||
BINARY_DIR ${zlibBinary}
|
||||
CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env "CC=${SB_CC}" "AR=${CMAKE_AR}" "RANLIB=${SB_RANLIB}" "CFLAGS=-I${zlibBinary} ${SB_CFLAGS_BASE}" ${SB_THIRDPARTY}/zlib/configure --prefix=${SB_PREFIX} --static
|
||||
BUILD_COMMAND ${SB_ENV} make
|
||||
INSTALL_COMMAND ${SB_ENV} make install
|
||||
LOG_CONFIGURE ON LOG_BUILD ON LOG_INSTALL ON LOG_OUTPUT_ON_FAILURE ON
|
||||
)
|
||||
singeRebuildTarget(zlib)
|
||||
|
||||
|
||||
# ===== zstd =====
|
||||
|
||||
singeCmakeProject(zstd ${SB_THIRDPARTY}/zstd/build/cmake "" "-DZSTD_BUILD_SHARED=off;-DZSTD_BUILD_STATIC=on;-DZSTD_BUILD_PROGRAMS=off" "${SB_ENV}")
|
||||
|
||||
|
||||
# ===== SDL3 and its satellites =====
|
||||
|
||||
singeCmakeProject(SDL3 ${SB_THIRDPARTY}/SDL3 "" "-DSDL_SHARED=off;-DSDL_STATIC=on;-DSDL_TESTS=off;-DSDL_EXAMPLES=off" "${SB_ENV}")
|
||||
|
||||
# ===== Host tools: the shader compiler (see cmake/hostTools.cmake) =====
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/hostTools.cmake)
|
||||
|
||||
# ===== Jolt Physics (C++; static, single precision, no tests or samples) =====
|
||||
# The x86 baseline is SSE4.1/4.2 so a shipped binary runs on anything from 2008 on; AVX and the
|
||||
# bit-count instructions would silently raise it. No link-time optimization through the cross
|
||||
# compiler, no warnings-as-errors with a clang Jolt has not met, nothing but the library; the GPU
|
||||
# compute paths (hair and soft bodies on DX12, Vulkan, Metal or a CPU fallback) would need a shader
|
||||
# compiler at build time and are not used.
|
||||
set(joltOptions
|
||||
-DTARGET_UNIT_TESTS=OFF -DTARGET_HELLO_WORLD=OFF -DTARGET_PERFORMANCE_TEST=OFF -DTARGET_SAMPLES=OFF -DTARGET_VIEWER=OFF
|
||||
-DJPH_BUILD_SHARED_LIBS=OFF -DDOUBLE_PRECISION=OFF -DCROSS_PLATFORM_DETERMINISTIC=OFF
|
||||
-DUSE_SSE4_1=ON -DUSE_SSE4_2=ON -DUSE_AVX=OFF -DUSE_AVX2=OFF -DUSE_AVX512=OFF -DUSE_F16C=OFF -DUSE_FMADD=OFF -DUSE_LZCNT=OFF -DUSE_TZCNT=OFF
|
||||
-DINTERPROCEDURAL_OPTIMIZATION=OFF -DENABLE_ALL_WARNINGS=OFF -DGENERATE_DEBUG_SYMBOLS=OFF
|
||||
-DDEBUG_RENDERER_IN_DEBUG_AND_RELEASE=OFF -DDEBUG_RENDERER_IN_DISTRIBUTION=ON -DFLOATING_POINT_EXCEPTIONS_ENABLED=OFF -DCPP_EXCEPTIONS_ENABLED=OFF -DCPP_RTTI_ENABLED=OFF
|
||||
-DJPH_USE_DX12=OFF -DJPH_USE_VK=OFF -DJPH_USE_MTL=OFF -DJPH_USE_CPU_COMPUTE=OFF
|
||||
)
|
||||
singeCmakeProject(Jolt ${SB_THIRDPARTY}/JoltPhysics/Build "" "${joltOptions}" "${SB_ENV}")
|
||||
|
||||
# ===== Recast Navigation (C++; static, the libraries only) =====
|
||||
singeCmakeProject(recastnavigation ${SB_THIRDPARTY}/recastnavigation ""
|
||||
"-DRECASTNAVIGATION_DEMO=OFF;-DRECASTNAVIGATION_TESTS=OFF;-DRECASTNAVIGATION_EXAMPLES=OFF;-DBUILD_SHARED_LIBS=OFF"
|
||||
"${SB_ENV}")
|
||||
|
||||
set(sdl3Dir -DSDL3_DIR=${SB_PREFIX}/lib/cmake/SDL3)
|
||||
|
||||
# Brotli, for the WOFF2 web fonts FreeType reads only when it is present. It is built before
|
||||
# SDL3_image and SDL3_ttf so that it is in the prefix when FreeType looks for it.
|
||||
#
|
||||
# libjxl vendors brotli as well, and its install writes libbrotlidec.a and libbrotlicommon.a over
|
||||
# the ones built here. That is harmless and is left alone: it is the same upstream library, the
|
||||
# two FreeType links come from libjxl's tree as a matched pair, and only the decoder is ever linked
|
||||
# -- libbrotlienc.a is the one file of this project's own build that survives, and nothing asks for
|
||||
# it. Telling libjxl to use the copy already installed (JPEGXL_FORCE_SYSTEM_BROTLI) does work, but
|
||||
# SDL3_image then tries to install brotli targets that no longer exist, and patching its install
|
||||
# list is a worse trade than one duplicated build.
|
||||
singeCmakeProject(brotli ${SB_THIRDPARTY}/brotli "" "-DBUILD_SHARED_LIBS=off;-DBROTLI_BUILD_TOOLS=off;-DBROTLI_DISABLE_TESTS=on" "${SB_ENV}")
|
||||
|
||||
singeCmakeProject(SDL3_image ${SB_THIRDPARTY}/SDL3_image "SDL3;zlib;brotli"
|
||||
"-DBUILD_SHARED_LIBS=off;-DSDLIMAGE_DEPS_SHARED=off;-DSDLIMAGE_SAMPLES=off;-DSDLIMAGE_TESTS=off;-DSDLIMAGE_VENDORED=on;-DSDLIMAGE_BACKEND_STB=off;-DSDLIMAGE_AVIF=off;-DSDLIMAGE_JXL=on;-DSDLIMAGE_JXL_SHARED=off;-DBUILD_TESTING=OFF;-DSDLIMAGE_TIF=off;-DSDLIMAGE_WEBP=on;${sdl3Dir};-DWEBP_BUILD_ANIM_UTILS=off;-DWEBP_BUILD_CWEBP=off;-DWEBP_BUILD_DWEBP=off;-DWEBP_BUILD_GIF2WEBP=off;-DWEBP_BUILD_IMG2WEBP=off;-DWEBP_BUILD_VWEBP=off;-DWEBP_BUILD_WEBPINFO=off;-DWEBP_BUILD_WEBPMUX=off;-DWEBP_BUILD_EXTRAS=off"
|
||||
"${SB_ENV_NO_PREFIX_INCLUDE}")
|
||||
|
||||
# SDL3_image builds libpng and installs the archive but not its headers, and FreeType needs both
|
||||
# before it will read a font's color bitmap glyphs. pnglibconf.h is generated, so it comes from
|
||||
# the build tree rather than the source one.
|
||||
ExternalProject_Add_Step(SDL3_image pngHeaders
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SB_THIRDPARTY}/SDL3_image/external/libpng/png.h ${SB_THIRDPARTY}/SDL3_image/external/libpng/pngconf.h ${SB_PREFIX}/build/SDL3_image/external/libpng-build/pnglibconf.h ${SB_PREFIX}/include
|
||||
DEPENDEES install
|
||||
COMMENT "Installing the libpng headers"
|
||||
)
|
||||
|
||||
# Opus's NEON code on ARM comes with a run time check for NEON, and on Windows that check is written
|
||||
# for MSVC only; MinGW gets "no CPU detection method available". Opus's CMake cannot presume NEON
|
||||
# without the check either (it declares the NEON functions only alongside it), so on ARM64 Windows
|
||||
# Opus is built from its plain C, which voice and .opus music need little of.
|
||||
set(mixerOpusOptions "")
|
||||
if((KANGAROO_OS STREQUAL "windows") AND (KANGAROO_ARCH STREQUAL "aarch64"))
|
||||
set(mixerOpusOptions ";-DOPUS_DISABLE_INTRINSICS=ON")
|
||||
endif()
|
||||
singeCmakeProject(SDL3_mixer ${SB_THIRDPARTY}/SDL3_mixer "SDL3"
|
||||
"-DBUILD_SHARED_LIBS=off;-DSDLMIXER_DEPS_SHARED=off;-DSDLMIXER_VENDORED=on;-DSDLMIXER_EXAMPLES=off;-DSDLMIXER_TESTS=off;-DSDLMIXER_FLAC_LIBFLAC=off;-DSDLMIXER_MP3_MPG123=off;-DSDLMIXER_VORBIS_STB=on;-DSDLMIXER_VORBIS_VORBISFILE=off;-DSDLMIXER_VORBIS_TREMOR=off;-DSDLMIXER_GME=on;-DSDLMIXER_GME_SHARED=off;-DGME_BUILD_EXAMPLES=off;-DGME_BUILD_TESTING=off;-DGME_YM2612_EMU=MAME;-DSDLMIXER_MIDI_FLUIDSYNTH=off;${sdl3Dir};-DWAVPACK_ENABLE_ASM=no${mixerOpusOptions}"
|
||||
"${SB_ENV}")
|
||||
|
||||
singeCmakeProject(SDL3_ttf ${SB_THIRDPARTY}/SDL3_ttf "SDL3;zlib;brotli;SDL3_image"
|
||||
"-DBUILD_SHARED_LIBS=off;-DSDLTTF_VENDORED=on;-DSDLTTF_HARFBUZZ=on;-DSDLTTF_PLUTOSVG=on;-DHB_HAVE_CORETEXT=off;-DSDLTTF_SAMPLES=off;-DFT_DISABLE_ZLIB=off;-DFT_DISABLE_BROTLI=off;-DZLIB_INCLUDE_DIR=${SB_PREFIX}/include;-DZLIB_LIBRARY=${SB_PREFIX}/lib/libz.a;-DBROTLIDEC_INCLUDE_DIRS=${SB_PREFIX}/include;-DBROTLIDEC_LIBRARIES=${SB_PREFIX}/lib/libbrotlidec.a;-DFT_DISABLE_PNG=off;-DPNG_PNG_INCLUDE_DIR=${SB_PREFIX}/include;-DPNG_LIBRARY=${SB_PREFIX}/lib/libpng16.a;${sdl3Dir}"
|
||||
"${SB_ENV}")
|
||||
# SDL3_ttf builds its vendored FreeType but does not install it, and Singe links it directly.
|
||||
ExternalProject_Add_Step(SDL3_ttf freetype
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${SB_PREFIX}/build/SDL3_ttf/external/freetype/libfreetype.a ${SB_PREFIX}/lib/libfreetype.a
|
||||
DEPENDEES install
|
||||
COMMENT "Installing libfreetype.a"
|
||||
)
|
||||
|
||||
# ===== RmlUi (C++; static, the core, the Lua plugin and the debugger; no samples) =====
|
||||
# FreeType is the copy SDL3_ttf builds (installed as libfreetype.a below, headers from its tree);
|
||||
# Lua is the engine's own, handed over as a header-only target by cmake/rmluiLua.cmake.
|
||||
set(rmluiOptions
|
||||
-DBUILD_SHARED_LIBS=OFF -DRMLUI_SAMPLES=OFF -DRMLUI_PRECOMPILED_HEADERS=OFF
|
||||
-DRMLUI_FONT_ENGINE=freetype -DRMLUI_LUA_BINDINGS=ON -DRMLUI_LUA_BINDINGS_LIBRARY=lua
|
||||
-DFREETYPE_LIBRARY=${SB_PREFIX}/lib/libfreetype.a
|
||||
-DFREETYPE_INCLUDE_DIR_ft2build=${SB_THIRDPARTY}/SDL3_ttf/external/freetype/include
|
||||
-DFREETYPE_INCLUDE_DIR_freetype2=${SB_THIRDPARTY}/SDL3_ttf/external/freetype/include
|
||||
-DSINGE_LUA_INCLUDE=${SB_THIRDPARTY}/lua/src
|
||||
-DCMAKE_PROJECT_RmlUi_INCLUDE=${CMAKE_CURRENT_LIST_DIR}/rmluiLua.cmake
|
||||
)
|
||||
singeCmakeProject(RmlUi ${SB_THIRDPARTY}/RmlUi "SDL3_ttf" "${rmluiOptions}" "${SB_ENV}")
|
||||
|
||||
|
||||
# ===== OpenSSL (its own Configure; the target name follows the platform) =====
|
||||
|
||||
if(KANGAROO_OS STREQUAL "windows")
|
||||
if(KANGAROO_ARCH STREQUAL "x86")
|
||||
set(opensslTarget mingw)
|
||||
elseif(KANGAROO_ARCH STREQUAL "aarch64")
|
||||
# OpenSSL has no MinGW target for ARM64 Windows; Singe's own configuration file adds one.
|
||||
set(opensslTarget --config=${CMAKE_SOURCE_DIR}/cmake/opensslMingwArm64.conf mingw-arm64)
|
||||
else()
|
||||
set(opensslTarget mingw64)
|
||||
endif()
|
||||
elseif(KANGAROO_OS STREQUAL "linux")
|
||||
if(KANGAROO_ARCH STREQUAL "aarch64")
|
||||
set(opensslTarget linux-aarch64)
|
||||
elseif(KANGAROO_ARCH STREQUAL "armhf")
|
||||
set(opensslTarget linux-armv4)
|
||||
elseif(SINGE_ZIG_TARGET)
|
||||
set(opensslTarget linux-x86_64)
|
||||
else()
|
||||
set(opensslTarget "")
|
||||
endif()
|
||||
elseif(KANGAROO_OS STREQUAL "macos")
|
||||
if(KANGAROO_ARCH STREQUAL "aarch64")
|
||||
set(opensslTarget darwin64-arm64)
|
||||
else()
|
||||
set(opensslTarget darwin64-x86_64)
|
||||
endif()
|
||||
elseif(SINGE_ZIG_TARGET)
|
||||
set(opensslTarget linux-x86_64)
|
||||
else()
|
||||
set(opensslTarget "")
|
||||
endif()
|
||||
# OpenSSL's generated assembly uses GNU as directives clang's assembler rejects; under zig the
|
||||
# portable C paths serve, which is all LuaSec's traffic needs.
|
||||
if(SINGE_ZIG_TARGET)
|
||||
set(opensslAsm no-asm)
|
||||
else()
|
||||
set(opensslAsm "")
|
||||
endif()
|
||||
# OpenSSL's Configure derives the source tree from the build directory's position and only
|
||||
# copes with a build directory nested inside its own tree, so this one lives there, one per target
|
||||
# so that switching presets never links another architecture's objects.
|
||||
set(opensslBinary ${SB_THIRDPARTY}/openssl/.builddir/${KANGAROO_OS}-${KANGAROO_ARCH})
|
||||
ExternalProject_Add(openssl
|
||||
SOURCE_DIR ${SB_THIRDPARTY}/openssl
|
||||
BINARY_DIR ${opensslBinary}
|
||||
DEPENDS zlib zstd
|
||||
CONFIGURE_COMMAND ${SB_ENV} perl ../../Configure ${opensslTarget} --prefix=${SB_PREFIX} --libdir=lib --with-zlib-include=${SB_PREFIX}/include --with-zlib-lib=${SB_PREFIX}/lib --with-zstd-include=${SB_PREFIX}/include --with-zstd-lib=${SB_PREFIX}/lib ${opensslAsm} no-apps no-docs no-dso no-dynamic-engine no-module no-shared no-tests zlib enable-zstd
|
||||
BUILD_COMMAND ${SB_ENV} make build_sw
|
||||
INSTALL_COMMAND ${SB_ENV} make install_sw
|
||||
LOG_CONFIGURE ON LOG_BUILD ON LOG_INSTALL ON LOG_OUTPUT_ON_FAILURE ON
|
||||
)
|
||||
singeRebuildTarget(openssl)
|
||||
|
||||
|
||||
# ===== FFmpeg (hardware decoding per platform; CMakeLists.txt reads its .pc files back) =====
|
||||
|
||||
# ===== Rockchip Media Process Platform (the vendor decode library for Rockchip SoCs) =====
|
||||
|
||||
# Rockchip's decoders are aarch64 only. Because the library links statically it costs nothing on a
|
||||
# board that is not a Rockchip, so it is built for the aarch64 targets rather than needing one of
|
||||
# its own; set SINGE_ROCKCHIP_MPP to override either way.
|
||||
if(NOT DEFINED SINGE_ROCKCHIP_MPP)
|
||||
if((KANGAROO_OS STREQUAL "linux") AND (KANGAROO_ARCH STREQUAL "aarch64"))
|
||||
set(SINGE_ROCKCHIP_MPP ON)
|
||||
else()
|
||||
set(SINGE_ROCKCHIP_MPP OFF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# MPP is packaged by no distribution, so it is vendored and built here like everything else. Only
|
||||
# the static library is kept: linked statically the Singe binary carries no runtime dependency on
|
||||
# it, so one ARM build runs on a Rockchip board and on anything else, where the decoder simply fails
|
||||
# to open a device and the player falls back. A shared build would put librockchip_mpp.so in the
|
||||
# binary's DT_NEEDED and stop it starting anywhere the library is absent.
|
||||
set(rkmppFlag "")
|
||||
set(rkmppDep "")
|
||||
if(SINGE_ROCKCHIP_MPP)
|
||||
# MPP strips its merged archive with ${CMAKE_STRIP}, and no cross strip here understands an ar
|
||||
# archive: zig's objcopy is ELF only. The step only drops debug symbols from an intermediate
|
||||
# library, so it is skipped; the release binary is stripped at its own link.
|
||||
singeCmakeProject(rockchipMpp ${SB_THIRDPARTY}/rockchip-mpp "" "-DBUILD_SHARED_LIBS=OFF;-DBUILD_TEST=OFF;-DHAVE_DRM=ON;-DCMAKE_STRIP=/bin/true" "${SB_ENV}")
|
||||
# Its install puts both the archive and the shared object down; the shared one is removed so
|
||||
# that -lrockchip_mpp can only resolve to the archive.
|
||||
ExternalProject_Add_Step(rockchipMpp staticOnly
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -f ${SB_PREFIX}/lib/librockchip_mpp.so ${SB_PREFIX}/lib/librockchip_mpp.so.0 ${SB_PREFIX}/lib/librockchip_mpp.so.1 ${SB_PREFIX}/lib/librockchip_vpu.so ${SB_PREFIX}/lib/librockchip_vpu.so.0 ${SB_PREFIX}/lib/librockchip_vpu.so.1
|
||||
DEPENDEES install
|
||||
COMMENT "Keeping only the static Rockchip MPP library"
|
||||
)
|
||||
set(rkmppFlag --enable-rkmpp)
|
||||
set(rkmppDep rockchipMpp)
|
||||
message(STATUS "Rockchip MPP will be built and linked statically; the rkmpp decoders are enabled")
|
||||
endif()
|
||||
|
||||
# ===== dav1d, the AV1 decoder =====
|
||||
|
||||
# FFmpeg's own AV1 decoder needs hardware behind it: with no hwaccel it says "Your platform doesn't
|
||||
# support hardware accelerated AV1 decoding" and decodes nothing, so an AV1 video does not play at
|
||||
# all on a machine without an AV1 capable GPU. dav1d is the software decoder, and it brings AVIF
|
||||
# stills with it, an AVIF being an AV1 picture in a HEIF box. It is the one vendored library whose
|
||||
# own build system is Meson rather than CMake or a configure script.
|
||||
set(dav1dBinary ${SB_PREFIX}/build/dav1d)
|
||||
set(dav1dCross "")
|
||||
if(SINGE_ZIG_TARGET)
|
||||
# Meson learns a cross build from a file rather than from the environment, and names both the
|
||||
# system and some architectures its own way. The system name is not cosmetic: Meson picks
|
||||
# nasm's output format from it, and calling Windows "mingw32" -- which is what FFmpeg calls it
|
||||
# -- leaves nasm emitting ELF objects that the Windows linker will not read.
|
||||
if(KANGAROO_OS STREQUAL "windows")
|
||||
set(dav1dSystem windows)
|
||||
elseif(KANGAROO_OS STREQUAL "macos")
|
||||
set(dav1dSystem darwin)
|
||||
else()
|
||||
set(dav1dSystem linux)
|
||||
endif()
|
||||
if(KANGAROO_ARCH STREQUAL "armhf")
|
||||
set(dav1dCpuFamily arm)
|
||||
else()
|
||||
set(dav1dCpuFamily ${KANGAROO_ARCH})
|
||||
endif()
|
||||
set(dav1dCrossFile ${SB_PREFIX}/build/dav1dCross.txt)
|
||||
file(WRITE ${dav1dCrossFile}
|
||||
"[binaries]\nc = '${SB_CC}'\ncpp = '${SB_CXX}'\nar = '${CMAKE_AR}'\nranlib = '${SB_RANLIB}'\nstrip = 'true'\npkg-config = 'pkg-config'\nnasm = 'nasm'\n\n[host_machine]\nsystem = '${dav1dSystem}'\ncpu_family = '${dav1dCpuFamily}'\ncpu = '${KANGAROO_ARCH}'\nendian = 'little'\n")
|
||||
set(dav1dCross --cross-file ${dav1dCrossFile})
|
||||
endif()
|
||||
ExternalProject_Add(dav1d
|
||||
SOURCE_DIR ${SB_THIRDPARTY}/dav1d
|
||||
BINARY_DIR ${dav1dBinary}
|
||||
CONFIGURE_COMMAND ${SB_ENV} meson setup --wipe --prefix=${SB_PREFIX} --libdir=lib --buildtype=release --default-library=static -Denable_tools=false -Denable_tests=false ${dav1dCross} ${dav1dBinary} ${SB_THIRDPARTY}/dav1d
|
||||
BUILD_COMMAND ${SB_ENV} ninja -C ${dav1dBinary}
|
||||
INSTALL_COMMAND ${SB_ENV} ninja -C ${dav1dBinary} install
|
||||
LOG_CONFIGURE ON LOG_BUILD ON LOG_INSTALL ON LOG_OUTPUT_ON_FAILURE ON
|
||||
)
|
||||
singeRebuildTarget(dav1d)
|
||||
|
||||
|
||||
# Recording (src/record.c) encodes with the platform's own H.264 hardware where there is one, and
|
||||
# with FFmpeg's built-in MPEG-4 everywhere; sound is AAC, and the file is MP4.
|
||||
set(recordEncoders mpeg4,aac)
|
||||
if(KANGAROO_OS STREQUAL "linux")
|
||||
# ARM boards decode through a V4L2 memory-to-memory device; desktops have VAAPI or VDPAU.
|
||||
if(KANGAROO_ARCH MATCHES "^(aarch64|armhf)$")
|
||||
set(hwaccel --enable-v4l2-m2m --enable-libdrm ${rkmppFlag})
|
||||
string(APPEND recordEncoders ,h264_v4l2m2m)
|
||||
else()
|
||||
set(hwaccel --enable-vaapi --enable-vdpau --enable-libdrm ${rkmppFlag})
|
||||
string(APPEND recordEncoders ,h264_vaapi)
|
||||
endif()
|
||||
elseif(KANGAROO_OS STREQUAL "macos")
|
||||
set(hwaccel --enable-videotoolbox)
|
||||
string(APPEND recordEncoders ,h264_videotoolbox)
|
||||
elseif(KANGAROO_OS STREQUAL "windows")
|
||||
set(hwaccel --enable-d3d11va --enable-dxva2 --enable-mediafoundation)
|
||||
string(APPEND recordEncoders ,h264_mf)
|
||||
else()
|
||||
set(hwaccel "")
|
||||
endif()
|
||||
# With a classic cross toolchain every tool is <triple>-<tool>; with zig each is named outright.
|
||||
if(SINGE_ZIG_TARGET)
|
||||
set(ffmpegTools --enable-cross-compile --cc=${SB_CC} --cxx=${SB_CXX} --ar=${CMAKE_AR} --ranlib=${SB_RANLIB} --windres=${CMAKE_RC_COMPILER})
|
||||
# FFmpeg only uses nm to learn the symbol prefix. The host's binutils nm reads x86 ELF and PE
|
||||
# objects but not aarch64 or Mach-O ones; the prefix is empty on Linux either way, and on macOS
|
||||
# (where it is an underscore) and on ARM64 Windows llvm-nm from Debian's llvm package reads the
|
||||
# objects.
|
||||
if((KANGAROO_OS STREQUAL "macos") OR ((KANGAROO_OS STREQUAL "windows") AND (KANGAROO_ARCH STREQUAL "aarch64")))
|
||||
find_program(SINGE_LLVM_NM NAMES llvm-nm llvm-nm-21 llvm-nm-20 llvm-nm-19 llvm-nm-18 llvm-nm-17 llvm-nm-16 llvm-nm-15)
|
||||
if(NOT SINGE_LLVM_NM)
|
||||
message(FATAL_ERROR "llvm-nm is needed for the ${KANGAROO_OS} ${KANGAROO_ARCH} FFmpeg build (apt-get install llvm).")
|
||||
endif()
|
||||
list(APPEND ffmpegTools --nm=${SINGE_LLVM_NM})
|
||||
elseif(KANGAROO_ARCH STREQUAL "x86_64")
|
||||
list(APPEND ffmpegTools --nm=nm)
|
||||
endif()
|
||||
else()
|
||||
set(ffmpegTools --cross-prefix=${SINGE_TRIPLE}- --cc=${SB_CC} --cxx=${SB_CXX} --ranlib=${SB_RANLIB})
|
||||
endif()
|
||||
# FFmpeg's configure sees sys/sysctl.h in the Pi sysroot and in zig's glibc header set, then its
|
||||
# build cannot include it; an empty stand-in satisfies both (Linux uses sysconf, not sysctl).
|
||||
if((KANGAROO_OS STREQUAL "linux") AND SINGE_ZIG_TARGET)
|
||||
file(WRITE ${SB_PREFIX}/include/sys/sysctl.h "/* File no longer used */\n")
|
||||
endif()
|
||||
set(ffmpegBinary ${SB_PREFIX}/build/ffmpeg)
|
||||
ExternalProject_Add(ffmpeg
|
||||
SOURCE_DIR ${SB_THIRDPARTY}/ffmpeg
|
||||
BINARY_DIR ${ffmpegBinary}
|
||||
DEPENDS dav1d ${rkmppDep}
|
||||
CONFIGURE_COMMAND ${SB_ENV} ${SB_THIRDPARTY}/ffmpeg/configure --enable-static --disable-shared --disable-debug --disable-muxers --enable-muxer=mp4 ${hwaccel} --disable-encoders --enable-encoder=${recordEncoders} --disable-filters --enable-filter=bwdif --enable-filter=yadif --enable-filter=buffer --enable-filter=buffersink --enable-filter=format --disable-network --disable-devices --disable-vulkan --disable-d3d12va --disable-bzlib --disable-lzma --disable-doc --disable-programs --enable-libdav1d --enable-gpl --enable-version3 --extra-ldflags=-L${SB_PREFIX}/lib --prefix=${SB_PREFIX} --arch=${KANGAROO_ARCH} --target-os=${SINGE_CROSS_OS} ${ffmpegTools}
|
||||
BUILD_COMMAND ${SB_ENV} make
|
||||
INSTALL_COMMAND ${SB_ENV} make install
|
||||
LOG_CONFIGURE ON LOG_BUILD ON LOG_INSTALL ON LOG_OUTPUT_ON_FAILURE ON
|
||||
)
|
||||
singeRebuildTarget(ffmpeg)
|
||||
|
||||
|
||||
# ===== Singe itself, against everything above =====
|
||||
|
||||
string(SUBSTRING ${KANGAROO_OS} 0 1 osInitial)
|
||||
string(SUBSTRING ${KANGAROO_OS} 1 -1 osRest)
|
||||
string(TOUPPER ${osInitial} osInitial)
|
||||
# The built file carries the platform's executable suffix; the copy in .builddir/ carries the
|
||||
# preset's, which is the same thing for Windows and lets an experimental build live beside another.
|
||||
# SINGE_VERSION_FULL, not PROJECT_VERSION: the four numbers CMake needs are not the version
|
||||
# anybody sees, and a beta build is named v3.00b2.
|
||||
set(singeBinaryBase "Singe-v${SINGE_VERSION_FULL}-${osInitial}${osRest}-${KANGAROO_ARCH}")
|
||||
set(singeBinaryName "${singeBinaryBase}${SINGE_SUFFIX}")
|
||||
set(singeBinary ${SB_PREFIX}/singe)
|
||||
ExternalProject_Add(singe
|
||||
SOURCE_DIR ${CMAKE_SOURCE_DIR}
|
||||
BINARY_DIR ${singeBinary}
|
||||
DEPENDS zlib zstd SDL3 SDL3_image SDL3_mixer SDL3_ttf openssl ffmpeg Jolt recastnavigation RmlUi shadercross
|
||||
CONFIGURE_COMMAND ${SB_ENV} ${CMAKE_COMMAND} -S ${CMAKE_SOURCE_DIR} -B ${singeBinary} ${SB_CMAKE_ARGS} -DSINGE_SHADERCROSS=${SINGE_SHADERCROSS} -DSINGE_SPIRV_CROSS=${SINGE_SPIRV_CROSS} -DSINGE_FROM_SUPERBUILD=ON -DSINGE_SUPERBUILD=OFF -DKANGAROO_OS=${KANGAROO_OS} -DKANGAROO_ARCH=${KANGAROO_ARCH} -DSINGE_TREE=${SINGE_TREE}
|
||||
BUILD_COMMAND ${SB_ENV} ${CMAKE_COMMAND} --build ${singeBinary} --parallel ${SB_JOBS}
|
||||
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy ${singeBinary}/${singeBinaryBase}${CMAKE_EXECUTABLE_SUFFIX} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName}
|
||||
BUILD_ALWAYS ON
|
||||
)
|
||||
|
||||
message(STATUS "Superbuild for ${KANGAROO_OS}/${KANGAROO_ARCH}: libraries into ${SB_PREFIX}, binary ${singeBinaryName}")
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
|
||||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# Embeds a file as a C array. Invoked in script mode by CMakeLists.txt:
|
||||
# cmake -DINPUT=<file> -DOUTPUT=<header> -DGUARD=<macro> -DSYMBOL=<name> -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 3
|
||||
* Copyright (C) 2006-${COPYRIGHT_END_YEAR} Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
#ifndef ${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}
|
||||
")
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
# Host tools the build needs at build time, built once for the host machine and shared by every
|
||||
# preset: SDL_shadercross (the scene shaders are HLSL, compiled to SPIR-V, DXIL and MSL when Singe
|
||||
# is built, the way the icon and the other embedded files are made) with SPIRV-Cross, a minimal
|
||||
# host SDL3 for its command line tool, and Microsoft's prebuilt DirectX Shader Compiler, which
|
||||
# SDL_shadercross drives and which is fetched like zig (building it from source is a half-hour
|
||||
# job nobody should need). Everything lives under .builddir/toolchains.
|
||||
#
|
||||
# DXC's prebuilt release exists for x86_64 Linux hosts. On another host put an unpacked DXC
|
||||
# (lib/libdxcompiler.so, lib/libdxil.so, include/dxc/dxcapi.h) at .builddir/toolchains/dxc.
|
||||
|
||||
set(SINGE_DXC_VERSION "v1.10.2605.37")
|
||||
set(SINGE_DXC_FILE "linux_dxc_2026_08_11.x86_64.tar.gz")
|
||||
set(SINGE_DXC_SHA256 "dea02d61c0548a6163a5fb45daa9263faeb5334e35d25a30b0293a32d089ceae")
|
||||
|
||||
get_filename_component(hostToolchains ${CMAKE_CURRENT_LIST_DIR}/../.builddir/toolchains ABSOLUTE)
|
||||
set(hostPrefix ${hostToolchains}/host)
|
||||
set(hostBuild ${hostToolchains}/hostBuild)
|
||||
set(hostDxc ${hostToolchains}/dxc)
|
||||
file(MAKE_DIRECTORY ${hostPrefix} ${hostBuild})
|
||||
|
||||
# ----- DirectX Shader Compiler, prebuilt -----
|
||||
if(NOT EXISTS ${hostDxc}/lib/libdxcompiler.so)
|
||||
if((CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") AND (CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64"))
|
||||
set(dxcUrl "https://github.com/microsoft/DirectXShaderCompiler/releases/download/${SINGE_DXC_VERSION}/${SINGE_DXC_FILE}")
|
||||
message(STATUS "Downloading the DirectX Shader Compiler ${SINGE_DXC_VERSION}")
|
||||
file(DOWNLOAD ${dxcUrl} ${hostToolchains}/${SINGE_DXC_FILE} EXPECTED_HASH SHA256=${SINGE_DXC_SHA256} STATUS status)
|
||||
list(GET status 0 code)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "DirectX Shader Compiler download failed: ${status}")
|
||||
endif()
|
||||
file(MAKE_DIRECTORY ${hostDxc})
|
||||
file(ARCHIVE_EXTRACT INPUT ${hostToolchains}/${SINGE_DXC_FILE} DESTINATION ${hostDxc})
|
||||
else()
|
||||
message(FATAL_ERROR "No prebuilt DirectX Shader Compiler for this host; put one at ${hostDxc} (see cmake/hostTools.cmake).")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ----- The host builds run with the host's own compilers, never the cross toolchain -----
|
||||
set(hostEnv ${CMAKE_COMMAND} -E env --unset=CC --unset=CXX --unset=AR --unset=RANLIB --unset=CFLAGS --unset=CXXFLAGS --unset=LDFLAGS --unset=PKG_CONFIG_LIBDIR --unset=PKG_CONFIG_SYSROOT_DIR "MAKEFLAGS=-j${SB_JOBS}")
|
||||
# A | inside an argument becomes a ; (a CMake list) on the way to the child, see LIST_SEPARATOR.
|
||||
set(hostArgs -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${hostPrefix} "-DCMAKE_INSTALL_RPATH=${hostPrefix}/lib|${hostDxc}/lib" -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON)
|
||||
|
||||
# A host CMake project; its stamps and build tree live with the tools, so every preset shares them.
|
||||
function(singeHostProject name source deps args)
|
||||
ExternalProject_Add(${name}
|
||||
SOURCE_DIR ${source}
|
||||
BINARY_DIR ${hostBuild}/${name}
|
||||
STAMP_DIR ${hostBuild}/${name}-stamp
|
||||
TMP_DIR ${hostBuild}/${name}-tmp
|
||||
DEPENDS ${deps}
|
||||
LIST_SEPARATOR |
|
||||
CONFIGURE_COMMAND ${hostEnv} ${CMAKE_COMMAND} -S ${source} -B ${hostBuild}/${name} ${hostArgs} ${args}
|
||||
BUILD_COMMAND ${hostEnv} ${CMAKE_COMMAND} --build ${hostBuild}/${name} --parallel ${SB_JOBS}
|
||||
INSTALL_COMMAND ${hostEnv} ${CMAKE_COMMAND} --build ${hostBuild}/${name} --target install
|
||||
LOG_CONFIGURE ON LOG_BUILD ON LOG_INSTALL ON LOG_OUTPUT_ON_FAILURE ON
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# SDL3 for the host, shared, with nothing SDL_shadercross's tool does not use.
|
||||
singeHostProject(hostSDL3 ${SB_THIRDPARTY}/SDL3 "" "-DSDL_SHARED=ON;-DSDL_STATIC=OFF;-DSDL_TESTS=OFF;-DSDL_EXAMPLES=OFF;-DSDL_AUDIO=OFF;-DSDL_VIDEO=OFF;-DSDL_GPU=OFF;-DSDL_RENDER=OFF;-DSDL_CAMERA=OFF;-DSDL_JOYSTICK=OFF;-DSDL_HAPTIC=OFF;-DSDL_HIDAPI=OFF;-DSDL_POWER=OFF;-DSDL_SENSOR=OFF;-DSDL_DIALOG=OFF;-DSDL_UNIX_CONSOLE_BUILD=ON")
|
||||
# SPIRV-Cross as the shared C library SDL_shadercross looks for, and as the command line tool that
|
||||
# turns SPIR-V into GLSL ES for the GLES backend -- SDL_shadercross emits DXBC, DXIL, MSL, SPIRV,
|
||||
# HLSL and JSON and no GLSL of any kind, so the ES shaders come from this one (PLAN.md section 56).
|
||||
# The CLI needs the static library as well as the shared one.
|
||||
singeHostProject(spirvCross ${SB_THIRDPARTY}/SPIRV-Cross "" "-DSPIRV_CROSS_SHARED=ON;-DSPIRV_CROSS_STATIC=ON;-DSPIRV_CROSS_CLI=ON;-DSPIRV_CROSS_ENABLE_TESTS=OFF")
|
||||
# SDL_shadercross, unvendored: SPIRV-Cross and SDL3 from the host prefix, DXC from its download.
|
||||
singeHostProject(shadercross ${SB_THIRDPARTY}/SDL_shadercross "hostSDL3;spirvCross" "-DSDLSHADERCROSS_VENDORED=OFF;-DSDLSHADERCROSS_DXC=ON;-DSDLSHADERCROSS_SHARED=OFF;-DSDLSHADERCROSS_STATIC=ON;-DSDLSHADERCROSS_CLI=ON;-DSDLSHADERCROSS_INSTALL=ON;-DSDLSHADERCROSS_INSTALL_CPACK=OFF;-DCMAKE_PREFIX_PATH=${hostPrefix}|${hostDxc};-DSDL3_DIR=${hostPrefix}/lib/cmake/SDL3")
|
||||
|
||||
set(SINGE_SHADERCROSS ${hostPrefix}/bin/shadercross)
|
||||
set(SINGE_SPIRV_CROSS ${hostPrefix}/bin/spirv-cross)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# Linux x86_64 built on a Linux x86_64 host with its own gcc and development packages.
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(KANGAROO_OS linux)
|
||||
set(KANGAROO_ARCH x86_64)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# OpenSSL's Configure, given this with --config, knows MinGW on 64-bit ARM Windows. OpenSSL has no
|
||||
# such target of its own: its "mingw64" is x86_64 and passes -m64, which an ARM64 compiler refuses.
|
||||
# This is that target without the flag, with the integer layout of OpenSSL's own ARM64 Windows target
|
||||
# (VC-WIN64-ARM). The Singe build turns the assembly off for every zig target, so the C paths serve.
|
||||
|
||||
my %targets = (
|
||||
"mingw-arm64" => {
|
||||
inherit_from => [ "mingw-common" ],
|
||||
sys_id => "MINGW64",
|
||||
bn_ops => add("SIXTY_FOUR_BIT RC4_CHAR"),
|
||||
uplink_arch => undef,
|
||||
multilib => "64",
|
||||
},
|
||||
);
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# Injected into RmlUi's configure through CMAKE_PROJECT_RmlUi_INCLUDE (cmake/Superbuild.cmake).
|
||||
# RmlUi's Lua plugin wants a Lua::Lua target; Singe compiles Lua into its own binary, so the plugin
|
||||
# only needs the headers here and links through the engine. Defining the target first keeps RmlUi
|
||||
# from running find_package(Lua) and looking for a system Lua.
|
||||
|
||||
if(NOT TARGET Lua::Lua)
|
||||
add_library(Lua::Lua INTERFACE IMPORTED)
|
||||
set_target_properties(Lua::Lua PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${SINGE_LUA_INCLUDE}")
|
||||
endif()
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# Runs a command and captures its standard output in a file. Invoked in script mode:
|
||||
# cmake -DOUTPUT=<file> -DCOMMAND=<program> -DARGS=<semicolon list> -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()
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
# Compiles src/shaders/scene.hlsl (which includes src/sceneShared.h) into a C header of SPIR-V, DXIL, MSL and GLSL ES
|
||||
# with SDL_shadercross and SPIRV-Cross. Run by the build as
|
||||
# cmake -DSHADERCROSS=<tool> -DSPIRVCROSS=<tool> -DSOURCE=<scene.hlsl> -DOUTPUT=<sceneShaders.h> -P shaderHeader.cmake
|
||||
# so the header is generated into the build tree like the icon and the other embedded files.
|
||||
#
|
||||
# SDL_shadercross has no GLSL destination (DXBC, DXIL, MSL, SPIRV, HLSL, JSON), so the ES form is a second step over the
|
||||
# SPIR-V it already produced: one pipeline, four outputs. SPIRVCROSS may be empty, and then the ES strings are null and
|
||||
# the GLES backend refuses to start -- which is what a build without the host tool should do rather than fail.
|
||||
|
||||
# Invoked in script mode: -DSHADERCROSS -DSOURCE=<file.hlsl> -DOUTPUT=<header> -DENTRIES=<name:stage;...>
|
||||
# -DPREFIX=<lowerCamel prefix> -DTYPE=<struct name>. ENTRIES, PREFIX and TYPE default to the scene's.
|
||||
if(NOT ENTRIES)
|
||||
set(ENTRIES vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment depthCutoutMain:fragment particleVertex:vertex particleFragment:fragment lineVertex:vertex lineFragment:fragment postVertex:vertex postFragment:fragment skyFragment:fragment bloomDown:fragment bloomUp:fragment)
|
||||
endif()
|
||||
if(NOT PREFIX)
|
||||
set(PREFIX sceneShader)
|
||||
endif()
|
||||
if(NOT TYPE)
|
||||
set(TYPE SceneShaderT)
|
||||
endif()
|
||||
get_filename_component(sourceName ${SOURCE} NAME)
|
||||
string(TOUPPER "${PREFIX}" guard)
|
||||
string(REPLACE "SHADER" "_SHADERS_H" guard "${guard}")
|
||||
set(entries ${ENTRIES})
|
||||
set(formats SPIRV DXIL MSL)
|
||||
|
||||
# GLSL ES 3.10 for the GLES backend. Three switches matter and each is load bearing:
|
||||
# --es --version 310 ES 3.1, the floor the scene needs. The vertex stage's storage buffers become texture
|
||||
# buffers in this form (SINGE_ESSL below), since ES lets a vertex shader have no
|
||||
# storage blocks and Mali drivers give it none; renderGles.c adjusts the extension.
|
||||
# --combined-samplers-inherit-bindings
|
||||
# HLSL's separate Texture2D and SamplerState have no ES equivalent, so SPIRV-Cross folds
|
||||
# each pair into one sampler and keeps the HLSL t-register as the binding, which is the
|
||||
# slot the engine already binds by.
|
||||
# --remove-unused-variables Without it every entry point declares every cbuffer and both storage buffers in the file:
|
||||
# fragmentMain came out with twelve blocks instead of two, and a fragment shader that
|
||||
# merely declares an unused SSBO can fail to link where MAX_FRAGMENT_SHADER_STORAGE_BLOCKS
|
||||
# is zero, which ES 3.1 permits.
|
||||
# --flip-vert-y SDL_GPU puts the framebuffer origin at the top left, as Vulkan, D3D12 and Metal do, and
|
||||
# GL puts it at the bottom left. Without this every render to a texture arrives upside
|
||||
# down -- the GUI came out mirrored the first time this ran. Inverting gl_Position.y is
|
||||
# the same fix as a negative viewport height, and it also reverses triangle winding, which
|
||||
# renderGles.c undoes by flipping the front face it sets.
|
||||
# --fixup-clipspace HLSL puts clip-space Z in [0, w] and GL in [-w, w], and GL then stores (z/w + 1) / 2.
|
||||
# Without the rewrite a depth written as d lands in the buffer as (d + 1) / 2, while the
|
||||
# shader's own shadow comparison still computes d -- so everything in the near half of a
|
||||
# light's range read as lit. Sponza showed it as a 2.6x too bright mid-shadow band with
|
||||
# correct highlights and correct deep shadow, which is what a half-range comparison does.
|
||||
set(esslFlags --es --version 310 --combined-samplers-inherit-bindings --remove-unused-variables --flip-vert-y --fixup-clipspace)
|
||||
get_filename_component(sourceDir ${SOURCE} DIRECTORY)
|
||||
get_filename_component(includeDir ${sourceDir} DIRECTORY)
|
||||
get_filename_component(outputDir ${OUTPUT} DIRECTORY)
|
||||
set(work ${outputDir}/work)
|
||||
file(MAKE_DIRECTORY ${work})
|
||||
|
||||
set(header "// Generated from ${sourceName} by cmake/shaderHeader.cmake with SDL_shadercross; do not edit.\n")
|
||||
string(APPEND header "// SPIR-V for Vulkan, DXIL for Direct3D 12, MSL for Metal, GLSL ES for OpenGL ES, one set per entry point.\n\n")
|
||||
string(APPEND header "#ifndef ${guard}\n#define ${guard}\n\n#include <stddef.h>\n#include <stdlib.h>\n\n")
|
||||
string(REGEX REPLACE "T$" "S" structName "${TYPE}")
|
||||
string(APPEND header "typedef struct ${structName} {\n\tconst char *entryPoint;\n\tconst unsigned char *spirv;\n\tsize_t spirvSize;\n\tconst unsigned char *dxil;\n\tsize_t dxilSize;\n\tconst unsigned char *msl;\n\tsize_t mslSize;\n\tconst char *essl; // GLSL ES 3.10 source, or NULL when the host tool was absent\n} ${TYPE};\n\n")
|
||||
foreach(entry IN LISTS entries)
|
||||
string(REPLACE ":" ";" parts ${entry})
|
||||
list(GET parts 0 name)
|
||||
list(GET parts 1 stage)
|
||||
foreach(format IN LISTS formats)
|
||||
string(TOLOWER ${format} extension)
|
||||
set(blob ${work}/${name}.${extension})
|
||||
execute_process(COMMAND ${SHADERCROSS} ${SOURCE} -s HLSL -d ${format} -t ${stage} -e ${name} -I ${includeDir} -o ${blob} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "Shader ${name} (${format}) failed to compile:\n${out}\n${err}")
|
||||
endif()
|
||||
file(READ ${blob} hex HEX)
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," bytes "${hex}")
|
||||
string(APPEND header "static const unsigned char _${name}${format}[] = {\n\t${bytes}\n};\n\n")
|
||||
endforeach()
|
||||
# The ES form, cross compiled from its own SPIR-V: the same source with SINGE_ESSL defined, which reads the
|
||||
# vertex stage's storage buffers as texture buffers. Emitted as a C string rather than bytes because that is
|
||||
# what glShaderSource takes; the escaping is only quotes and backslashes, neither of which GLSL ES has a use for.
|
||||
set(esslSymbol "NULL")
|
||||
if(SPIRVCROSS)
|
||||
set(esslSpirv ${work}/${name}.essl.spirv)
|
||||
execute_process(COMMAND ${SHADERCROSS} ${SOURCE} -s HLSL -d SPIRV -t ${stage} -e ${name} -I ${includeDir} -DSINGE_ESSL -o ${esslSpirv} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "Shader ${name} (SPIR-V for GLSL ES) failed to compile:\n${out}\n${err}")
|
||||
endif()
|
||||
set(essl ${work}/${name}.essl)
|
||||
execute_process(COMMAND ${SPIRVCROSS} ${esslFlags} ${esslSpirv} --output ${essl} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "Shader ${name} (GLSL ES) failed to cross compile:\n${out}\n${err}")
|
||||
endif()
|
||||
file(READ ${essl} esslText)
|
||||
# SPIRV-Cross defaults an ES fragment shader to "precision mediump float", and a struct
|
||||
# declared inside a uniform block carries no qualifier of its own, so Light's members inherit
|
||||
# mediump -- fp16 on a real tiler. The scene's own values do not fit that: a camera or a
|
||||
# light tens of units from the origin loses whole units of position. Ask for highp instead.
|
||||
string(REPLACE "precision mediump float;" "precision highp float;" esslText "${esslText}")
|
||||
string(REPLACE "\\" "\\\\" esslText "${esslText}")
|
||||
string(REPLACE "\"" "\\\"" esslText "${esslText}")
|
||||
string(REPLACE "\n" "\\n\"\n\t\"" esslText "${esslText}")
|
||||
string(APPEND header "static const char _${name}ESSL[] =\n\t\"${esslText}\";\n\n")
|
||||
set(esslSymbol "_${name}ESSL")
|
||||
endif()
|
||||
string(SUBSTRING ${name} 0 1 initial)
|
||||
string(SUBSTRING ${name} 1 -1 rest)
|
||||
string(TOUPPER ${initial} initial)
|
||||
string(APPEND header "static const ${TYPE} ${PREFIX}${initial}${rest} = { \"${name}\", _${name}SPIRV, sizeof(_${name}SPIRV), _${name}DXIL, sizeof(_${name}DXIL), _${name}MSL, sizeof(_${name}MSL), ${esslSymbol} };\n\n")
|
||||
endforeach()
|
||||
string(APPEND header "#endif\n")
|
||||
file(WRITE ${OUTPUT} "${header}")
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# Raspberry Pi (64-bit Raspberry Pi OS) with zig as the compiler. zig supplies glibc pinned to the
|
||||
# version below plus the kernel headers; the platform libraries Singe, SDL3 and FFmpeg build against
|
||||
# (libdrm, ALSA, X11, GBM/EGL/GLES, udev, ...) come from the Debian packages listed in
|
||||
# arm64Packages.cmake, downloaded and unpacked into a sysroot under .builddir/toolchains on first use.
|
||||
# Nothing outside the source tree is touched; dpkg-deb from the host does the unpacking.
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/arm64Packages.cmake)
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
# Raspberry Pi OS bullseye shipped glibc 2.31; a binary built against it runs there and on everything since.
|
||||
set(SINGE_GLIBC_VERSION 2.31)
|
||||
|
||||
get_filename_component(armToolchains ${CMAKE_CURRENT_LIST_DIR}/../../.builddir/toolchains ABSOLUTE)
|
||||
set(SINGE_SYSROOT ${armToolchains}/sysroot-aarch64-linux-gnu CACHE PATH "ARM64 platform headers and libraries" FORCE)
|
||||
set(armDebs ${armToolchains}/debs-aarch64-linux-gnu)
|
||||
set(armStamp ${SINGE_SYSROOT}/.complete)
|
||||
|
||||
# The stamp holds the manifest that built the sysroot; a changed manifest rebuilds it.
|
||||
string(SHA256 arm64Manifest "${arm64Packages}")
|
||||
set(piHave "")
|
||||
if(EXISTS ${armStamp})
|
||||
file(READ ${armStamp} piHave)
|
||||
string(STRIP "${piHave}" piHave)
|
||||
endif()
|
||||
if(NOT piHave STREQUAL arm64Manifest)
|
||||
find_program(SINGE_DPKG_DEB dpkg-deb)
|
||||
if(NOT SINGE_DPKG_DEB)
|
||||
message(FATAL_ERROR "dpkg-deb is needed to unpack the Pi sysroot packages (apt-get install dpkg).")
|
||||
endif()
|
||||
message(STATUS "Building the ARM64 sysroot in ${SINGE_SYSROOT}")
|
||||
file(REMOVE_RECURSE ${SINGE_SYSROOT})
|
||||
file(MAKE_DIRECTORY ${SINGE_SYSROOT} ${armDebs})
|
||||
foreach(entry IN LISTS arm64Packages)
|
||||
string(REGEX MATCH "^([^|]*)\\|(.*)$" unused "${entry}")
|
||||
set(url ${CMAKE_MATCH_1})
|
||||
set(sha ${CMAKE_MATCH_2})
|
||||
get_filename_component(deb ${url} NAME)
|
||||
set(debPath ${armDebs}/${deb})
|
||||
if(NOT EXISTS ${debPath})
|
||||
message(STATUS "Downloading ${deb}")
|
||||
file(DOWNLOAD ${url} ${debPath} EXPECTED_HASH SHA256=${sha} STATUS status)
|
||||
list(GET status 0 code)
|
||||
if(NOT code EQUAL 0)
|
||||
file(REMOVE ${debPath})
|
||||
message(FATAL_ERROR "Download of ${url} failed: ${status}")
|
||||
endif()
|
||||
endif()
|
||||
execute_process(COMMAND ${SINGE_DPKG_DEB} -x ${debPath} ${SINGE_SYSROOT} RESULT_VARIABLE code)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "Unpacking ${deb} failed.")
|
||||
endif()
|
||||
endforeach()
|
||||
file(WRITE ${armStamp} "${arm64Manifest}\n")
|
||||
endif()
|
||||
|
||||
# The sysroot's headers come after zig's own; its libraries are linked by name and left unresolved
|
||||
# at link time (they are only stubs for what the Pi provides at run time).
|
||||
set(armLibs ${SINGE_SYSROOT}/usr/lib/aarch64-linux-gnu)
|
||||
singeZigToolchain(aarch64-linux-gnu.${SINGE_GLIBC_VERSION} "-idirafter ${SINGE_SYSROOT}/usr/include/aarch64-linux-gnu -idirafter ${SINGE_SYSROOT}/usr/include -L${armLibs} -L${SINGE_SYSROOT}/lib/aarch64-linux-gnu -Wl,--allow-shlib-undefined")
|
||||
|
||||
# find_package and pkg-config look only in the sysroot; programs come from the host.
|
||||
set(CMAKE_FIND_ROOT_PATH ${SINGE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu CACHE STRING "" FORCE)
|
||||
set(CMAKE_C_LIBRARY_ARCHITECTURE aarch64-linux-gnu CACHE STRING "" FORCE)
|
||||
set(CMAKE_LIBRARY_PATH ${armLibs} CACHE PATH "" FORCE)
|
||||
set(KANGAROO_OS linux)
|
||||
set(KANGAROO_ARCH aarch64)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# macOS on Apple silicon; everything is in macos.cmake.
|
||||
|
||||
set(SINGE_MACOS_ARCH aarch64)
|
||||
set(CMAKE_SYSTEM_PROCESSOR arm64)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/macos.cmake)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# Windows on 64-bit ARM with zig as the cross compiler: zig ships the mingw-w64 headers and import
|
||||
# libraries, so nothing else has to be installed.
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
singeZigToolchain(aarch64-windows-gnu "")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(KANGAROO_OS windows)
|
||||
set(KANGAROO_ARCH aarch64)
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Debian bookworm arm64 packages that give the Pi build its platform headers and stub libraries
|
||||
# (libdrm, ALSA, X11, GBM/EGL/GLES, udev, dbus, Wayland, PulseAudio). zig supplies libc and the
|
||||
# kernel headers itself, so no libc6 package is here. Every line is a permanent snapshot.debian.org
|
||||
# URL and the SHA-256 Debian publishes for it; regenerate against a newer snapshot when needed.
|
||||
# Format: one "url|sha256" pair per list element.
|
||||
|
||||
set(arm64PackagesSnapshot "https://snapshot.debian.org/archive/debian/20250901T000000Z")
|
||||
set(arm64Packages
|
||||
# Kernel headers, for the V4L2 stateless decoders: linux/media.h, videodev2.h, v4l2-controls.h.
|
||||
"${arm64PackagesSnapshot}/pool/main/l/linux/linux-libc-dev_6.1.148-1_arm64.deb|55baaddd21bcaa7fed8894c89e4d65b684e85abcb48acf443519438185e1153f"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdrm/libdrm-dev_2.4.114-1+b1_arm64.deb|26d7d0dcbbda8ed023b57989e0fca7f0ff8372daaf654daf8b3a362557be5e21"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdrm/libdrm2_2.4.114-1+b1_arm64.deb|f5f15a46d02cf5d9fa52d4f1c54b8cf80c398711ad771a9938b12399b8d8090c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdrm/libdrm-amdgpu1_2.4.114-1+b1_arm64.deb|24692e933b98b2b8b80d87c45728efa2bc7176120ce9efcb8793407e508b5a32"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdrm/libdrm-nouveau2_2.4.114-1+b1_arm64.deb|2acf0db609e8569dd89eea5f3e6f27968de50474d31213665901ad1c6d1133de"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdrm/libdrm-radeon1_2.4.114-1+b1_arm64.deb|e53c2b4be53b4a8e6413887525c9172bdc95ed3a140a52ce23d6e1a68789aaa0"
|
||||
"${arm64PackagesSnapshot}/pool/main/a/alsa-lib/libasound2-dev_1.2.8-1+b1_arm64.deb|263668e5ce831d3a8426bd342beba7e94925e893a4a5a285252b8f3ed6b3f5ee"
|
||||
"${arm64PackagesSnapshot}/pool/main/a/alsa-lib/libasound2_1.2.8-1+b1_arm64.deb|9fa889400fcee4b92c8f4a2fafbb7f2cd33444d9ec1665a71002ab67c06114bb"
|
||||
"${arm64PackagesSnapshot}/pool/main/a/alsa-lib/libasound2-data_1.2.8-1_all.deb|fe0780d2d3674b2977e0acb0d48b448ad72ba1642564b7dc537f55e839984c2d"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libx11/libx11-dev_1.8.4-2+deb12u2_arm64.deb|34bfc833561de983cfb4475523fedc13abb4faee14a876300de659591779add8"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libx11/libx11-6_1.8.4-2+deb12u2_arm64.deb|d1d533e983582282a9ea82c87ac5ce715a9b67bd6d1acbd2439a11c63c36549b"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxcb/libxcb1-dev_1.15-1_arm64.deb|87b5c3f1bb130727df68295ecfaadc305ee10c150afeb1a183e95b9150e18db5"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxcb/libxcb1_1.15-1_arm64.deb|041d9a68415c3ccf3ce8f4f8b88e4bbfb5dc1f0d97013c6ef8423e620ea50f84"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxau/libxau-dev_1.0.9-1_arm64.deb|21db3761a8f388fc83707c00fac645d4332737a859dd727571e1e6ede003b048"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxau/libxau6_1.0.9-1_arm64.deb|36c2bf400641a80521093771dc2562c903df4065f9eb03add50d90564337ea6c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxdmcp/libxdmcp-dev_1.1.2-3_arm64.deb|6fd0bc51f3a4e6b7f944f24f15524a4c4ba68a4b0aa136c18bdb96a2d36988f2"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxdmcp/libxdmcp6_1.1.2-3_arm64.deb|e92569ac33247261aa09adfadc34ced3994ac301cf8b58de415a2d5dbf15ccfc"
|
||||
"${arm64PackagesSnapshot}/pool/main/x/xorgproto/x11proto-dev_2022.1-1_all.deb|3cab66a6591774188a568f9b54c4e7956f18da76e0e0fc4a779db5f6bcc3f148"
|
||||
"${arm64PackagesSnapshot}/pool/main/x/xorg-sgml-doctools/xorg-sgml-doctools_1.11-1.1_all.deb|168345058319094e475a87ace66f5fb6ae802109650ea8434d672117982b5d0a"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxext/libxext-dev_1.3.4-1+b1_arm64.deb|07f5e3efeba9564071885b2d0d2ba277692fa309a9f05ef862688af0022b1fcf"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxext/libxext6_1.3.4-1+b1_arm64.deb|5e9c0ad606eb4674c645fe8e0e64330c47d2729f7a59ed569848610efd5d5b62"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxi/libxi-dev_1.8-1+b1_arm64.deb|4a5e7bc3b4e55c23281b2e500b572a61ae8869b1bc03fe14b4ae5a4c9e3c295c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxi/libxi6_1.8-1+b1_arm64.deb|b26383c9929d8bfba051eee22301e064d17039ed022bbf31990feb0d7a03b36c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxfixes/libxfixes-dev_6.0.0-2_arm64.deb|25266e1bd10c7624210abf3aa3eb85e3886b6c8c08b2ecc396b8a04f24c10082"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxfixes/libxfixes3_6.0.0-2_arm64.deb|d47bda8fed01b19b41d503e2df05d9166c58e30e2376f2f8784ceb7a834befe6"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxrandr/libxrandr-dev_1.5.2-2+b1_arm64.deb|f951a3813c525bc950e21e801837dfe447bdbef4d2610eddbd833d7d41f92c7c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxrandr/libxrandr2_1.5.2-2+b1_arm64.deb|f0bc7f79fee182caae176652fdb1bf349d13b9591b10ad4dc1c896521da8e49f"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxrender/libxrender-dev_0.9.10-1.1_arm64.deb|26a3ef50910cae3422c20af90a4a4deb85e7da3680c9192e50efc01b7e1b2f8f"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxrender/libxrender1_0.9.10-1.1_arm64.deb|f260193deb4eaf0b410a1c61e1f84804366ee3372b8bad9f38005f7dbcd89aae"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxcursor/libxcursor-dev_1.2.1-1_arm64.deb|5d0a0e62fcb202d9afc08c95d6b44e28211e63a6f0763e05741561ac2580aaa1"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxcursor/libxcursor1_1.2.1-1_arm64.deb|a93dd172bdd940de4ac6fd2b7a3f0213604c975f9f2f51eb3013f50886aaa87c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxtst/libxtst-dev_1.2.3-1.1_arm64.deb|4e205710dd04f9e9ee360da0161c155be01086e36462fead392b707b3857b4f2"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxtst/libxtst6_1.2.3-1.1_arm64.deb|ef2a1b9ac3f2620d96e17744e41c4eefb7f491eee3bcc5bc1e592180b2d94128"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxss/libxss-dev_1.2.3-1_arm64.deb|970b39c57df5c1307f905c6dd89967d548e9e2711a2099241de7b8e8831e40ca"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxss/libxss1_1.2.3-1_arm64.deb|e0ff80e309eacda5face68b9a3bd56718fed2750af429324c35d7c9491c335f4"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxinerama/libxinerama-dev_1.1.4-3_arm64.deb|64c758aef152615d99bceead07c86b03f3737dca190230b5dde1d79403288341"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxinerama/libxinerama1_1.1.4-3_arm64.deb|1dc5f458f6b3245b1b128897f905717d3cf7777a5697577b246042e25311d0df"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxkbcommon/libxkbcommon-dev_1.5.0-1_arm64.deb|7d39b34b202fe58e18992b439c79c149ebb1b821b58dfb863ff4898e06420699"
|
||||
"${arm64PackagesSnapshot}/pool/main/libx/libxkbcommon/libxkbcommon0_1.5.0-1_arm64.deb|ec518d8a19796a399ab95e7bc4dfbb6bd2ed8e151f77b222df26208db412d852"
|
||||
"${arm64PackagesSnapshot}/pool/main/m/mesa/libgbm-dev_22.3.6-1+deb12u1_arm64.deb|ba3f413f6f428b45e21d93fc5eb213bbc31ca1c15a7b265da3700100fd2475c8"
|
||||
"${arm64PackagesSnapshot}/pool/main/m/mesa/libgbm1_22.3.6-1+deb12u1_arm64.deb|c76790e11aac46e328b6a13b34cccd3ef01fad79cfe80d2c4aa2384ac9ddb1f8"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libegl-dev_1.6.0-1_arm64.deb|8a3004f892785a5fa4c9f969c116f567a36d1c8e9b3d5fd0403055151e36c7d3"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libegl1_1.6.0-1_arm64.deb|707097a275155c600e2e9251c4ee7cdfdb2d8f50a678a850a2526a7bc9664166"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libgles-dev_1.6.0-1_arm64.deb|941e9963d64479544691f7183fe0eeae32b53b633df71c4c51263517ab2ea8f3"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libgles2_1.6.0-1_arm64.deb|efeb2717380f8411d66b3f669bb99bbd83ff09db3d4a1661533aa31af659608c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libgl-dev_1.6.0-1_arm64.deb|56add9e710c072cf56504beed50a639f9f7c64f4e31e66958fa65a566084888c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libgl1_1.6.0-1_arm64.deb|f4043ab47f52325973d2b608514b3469b055b65c744861c7f378755a32e799d6"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libglvnd-dev_1.6.0-1_arm64.deb|86cba4e243d10ae07cc6e769f23ecd097b2f87b722dc5ac48a48a3794cd29513"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libglvnd0_1.6.0-1_arm64.deb|d5063205fab5322ee686f33f87c6315b3fcd1335ea4e7bc8202b48ed4f9c9e83"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libglx-dev_1.6.0-1_arm64.deb|f3d6fca16c259743208df6f845aee3189eb54fed00e159811135089c16d0ad9c"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libglx0_1.6.0-1_arm64.deb|523f9b71f75e48ae605d8ef298a4b03ade26ade9a808a85329a3bd82a54b9b0d"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libopengl0_1.6.0-1_arm64.deb|4b3c6b3bc25530290cf231acac4c85b8806dda53ab6376ab22b3ec511b4fb7a0"
|
||||
"${arm64PackagesSnapshot}/pool/main/libg/libglvnd/libopengl-dev_1.6.0-1_arm64.deb|b4ddc2d38103e9f0c3637d46334ccbf40c30e851c85f5348abc0aa37e3926822"
|
||||
"${arm64PackagesSnapshot}/pool/main/s/systemd/libudev-dev_252.36-1~deb12u1_arm64.deb|62760a9e8fb87c2827333163efadc36c5ae66be59af18e7286d19813b8a72e03"
|
||||
"${arm64PackagesSnapshot}/pool/main/s/systemd/libudev1_252.36-1~deb12u1_arm64.deb|5abfa6d6960f1b6a19c3d35373958d47a3a2f1221a5fe4652143fea0e0adc2ab"
|
||||
"${arm64PackagesSnapshot}/pool/main/d/dbus/libdbus-1-dev_1.14.10-1~deb12u1_arm64.deb|817caa440feee88d53dd74432ce633af8d351552cd71014bcc8c98380c9ed88c"
|
||||
"${arm64PackagesSnapshot}/pool/main/d/dbus/libdbus-1-3_1.14.10-1~deb12u1_arm64.deb|2a423794f44ee756f70fda67c6e47b15afe3dd22cd69e51f5d14d2ec9538f806"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-dev_1.21.0-1_arm64.deb|932405c58195d299495230792e374ad304adadf3d3ffdc07eda3466b756adc33"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-client0_1.21.0-1_arm64.deb|ae390cc04c2eb1de90b9a6373505b22c730ada5e72daa50c507b7f99c12faf06"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-egl1_1.21.0-1_arm64.deb|c08124a4d9af24f058b45c88867a84697882e755ea94965795d152fe26fe57c9"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-cursor0_1.21.0-1_arm64.deb|8c8d1a3942bc3fc9dd3ea7c679c1a314e59cdaaa700883d54d1bb3ec89db256b"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-server0_1.21.0-1_arm64.deb|4d99e7e0503fdaa999e67d47d8612c0465e775eafa47819573da50bdb09064f2"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-bin_1.21.0-1_arm64.deb|02e9361617b43b130ff032873a910258884160c169e89a1ca45d34d1150a2615"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdecor-0/libdecor-0-dev_0.1.1-2_arm64.deb|e52fbcbbf6e755be2c642fcecc6154d9d071978d0797e7cf9ccf93d131bf4fc6"
|
||||
"${arm64PackagesSnapshot}/pool/main/libd/libdecor-0/libdecor-0-0_0.1.1-2_arm64.deb|e88d3c2dd3336a97b8cf10ef270a264388fce81cedf8032b18f2a36b021bc6a3"
|
||||
"${arm64PackagesSnapshot}/pool/main/p/pulseaudio/libpulse-dev_16.1+dfsg1-2+b1_arm64.deb|dac8f94dc214a77654cc62d0f1611cd4dee9cdefdc816e3056dd9fae5aa5d4a8"
|
||||
"${arm64PackagesSnapshot}/pool/main/p/pulseaudio/libpulse0_16.1+dfsg1-2+b1_arm64.deb|26f17e3457c5fce0104a6b2f0efb75a3256b24b32ca7cfda6266373758a930cb"
|
||||
"${arm64PackagesSnapshot}/pool/main/w/wayland/libwayland-egl-backend-dev_1.21.0-1_arm64.deb|0974da94059371e67e93d61398853c9967d0cc34cd7745b6d47d3aa47b88815d"
|
||||
"${arm64PackagesSnapshot}/pool/main/libf/libffi/libffi-dev_3.4.4-1_arm64.deb|8ff6335f76752ef8659762435d0d88129ef0090a587520853238809bd10ef392"
|
||||
"${arm64PackagesSnapshot}/pool/main/libf/libffi/libffi8_3.4.4-1_arm64.deb|80b5c36177dc0e29d531c7eddbed3cc7355cb490e49f8cfa5959572d161f27b3"
|
||||
)
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# macOS with zig as the compiler, shared by the aarch64 and x86_64 toolchain files (which set
|
||||
# SINGE_MACOS_ARCH and CMAKE_SYSTEM_PROCESSOR before including this). zig carries the macOS libc
|
||||
# headers and the libSystem stub itself; everything else (Cocoa, CoreAudio, VideoToolbox, Metal and the rest of
|
||||
# the frameworks, libc++, libiconv) comes from an Apple SDK, which Apple does not let a build
|
||||
# script fetch. The SDK is expected, in this order:
|
||||
# 1. an unpacked SDK at SINGE_MACOS_SDK (default .builddir/toolchains/MacOSX.sdk);
|
||||
# 2. a tarball at .builddir/toolchains/MacOSX.sdk.tar.xz, unpacked on first use;
|
||||
# 3. SINGE_MACOS_SDK_URL (with SINGE_MACOS_SDK_SHA256), downloaded and unpacked on first use;
|
||||
# the default points at the community mirror osxcross users share (github.com/joseluisq/
|
||||
# macosx-sdks), pinned to one SDK release and its published SHA-256.
|
||||
# See INSTALL for how to make the tarball from Xcode instead.
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Darwin)
|
||||
|
||||
# Oldest macOS the binary runs on (Ventura: Intel Macs from 2017 and every Apple silicon Mac).
|
||||
set(SINGE_MACOS_VERSION 13.0)
|
||||
|
||||
get_filename_component(sdkToolchains ${CMAKE_CURRENT_LIST_DIR}/../../.builddir/toolchains ABSOLUTE)
|
||||
set(SINGE_MACOS_SDK ${sdkToolchains}/MacOSX.sdk CACHE PATH "Unpacked Apple SDK for the macOS build")
|
||||
set(SINGE_MACOS_SDK_URL "https://github.com/joseluisq/macosx-sdks/releases/download/15.5/MacOSX15.5.sdk.tar.xz" CACHE STRING "Where to fetch the Apple SDK tarball from; empty to never download")
|
||||
set(SINGE_MACOS_SDK_SHA256 "c15cf0f3f17d714d1aa5a642da8e118db53d79429eb015771ba816aa7c6c1cbd" CACHE STRING "SHA-256 of that tarball; empty to skip the check")
|
||||
set(sdkTarball ${sdkToolchains}/MacOSX.sdk.tar.xz)
|
||||
if(NOT EXISTS ${SINGE_MACOS_SDK}/System/Library/Frameworks/Cocoa.framework)
|
||||
if((NOT EXISTS ${sdkTarball}) AND SINGE_MACOS_SDK_URL)
|
||||
message(STATUS "Downloading the Apple SDK from ${SINGE_MACOS_SDK_URL}")
|
||||
file(MAKE_DIRECTORY ${sdkToolchains})
|
||||
if(SINGE_MACOS_SDK_SHA256)
|
||||
file(DOWNLOAD ${SINGE_MACOS_SDK_URL} ${sdkTarball} EXPECTED_HASH SHA256=${SINGE_MACOS_SDK_SHA256} STATUS status)
|
||||
else()
|
||||
file(DOWNLOAD ${SINGE_MACOS_SDK_URL} ${sdkTarball} STATUS status)
|
||||
endif()
|
||||
list(GET status 0 code)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "Apple SDK download failed: ${status}")
|
||||
endif()
|
||||
endif()
|
||||
if(EXISTS ${sdkTarball})
|
||||
message(STATUS "Unpacking ${sdkTarball}")
|
||||
set(sdkUnpack ${sdkToolchains}/MacOSX.sdk.unpack)
|
||||
file(REMOVE_RECURSE ${sdkUnpack})
|
||||
file(ARCHIVE_EXTRACT INPUT ${sdkTarball} DESTINATION ${sdkUnpack})
|
||||
file(GLOB sdkFound LIST_DIRECTORIES true ${sdkUnpack}/*.sdk ${sdkUnpack}/*/*.sdk)
|
||||
list(LENGTH sdkFound sdkCount)
|
||||
if(NOT sdkCount EQUAL 1)
|
||||
message(FATAL_ERROR "${sdkTarball} should hold exactly one MacOSX*.sdk directory; found ${sdkCount}.")
|
||||
endif()
|
||||
file(RENAME ${sdkFound} ${SINGE_MACOS_SDK})
|
||||
file(REMOVE_RECURSE ${sdkUnpack})
|
||||
endif()
|
||||
endif()
|
||||
if(NOT EXISTS ${SINGE_MACOS_SDK}/System/Library/Frameworks/Cocoa.framework)
|
||||
message(FATAL_ERROR "No Apple SDK at ${SINGE_MACOS_SDK}. Put an SDK tarball at ${sdkTarball} (see INSTALL), or set SINGE_MACOS_SDK to an unpacked SDK, or SINGE_MACOS_SDK_URL to fetch one.")
|
||||
endif()
|
||||
|
||||
# No --sysroot: zig would then prefix every -L path with it, the superbuild's own lib directory
|
||||
# included. The SDK is named outright instead: its usr/include (libDER and other non-libc pieces)
|
||||
# after zig's own libc headers, its frameworks, and its stub libraries. The SDK's CMTag.h uses two
|
||||
# macOS 14 types without an availability guard, and FFmpeg's configure makes that an error with
|
||||
# -Werror=partial-availability; the trailing -Wno-unguarded-availability-new wins over it.
|
||||
singeZigToolchain(${SINGE_MACOS_ARCH}-macos.${SINGE_MACOS_VERSION} "-idirafter ${SINGE_MACOS_SDK}/usr/include -F${SINGE_MACOS_SDK}/System/Library/Frameworks -L${SINGE_MACOS_SDK}/usr/lib -Wno-unguarded-availability-new")
|
||||
# SDL3 compiles Objective-C, which the same zig wrappers handle, and CMake's Darwin support insists
|
||||
# on an install_name_tool even though nothing here links dynamically; llvm's stands in.
|
||||
set(CMAKE_OBJC_COMPILER ${CMAKE_C_COMPILER} CACHE FILEPATH "" FORCE)
|
||||
set(CMAKE_OBJCXX_COMPILER ${CMAKE_CXX_COMPILER} CACHE FILEPATH "" FORCE)
|
||||
find_program(SINGE_INSTALL_NAME_TOOL NAMES llvm-install-name-tool llvm-install-name-tool-21 llvm-install-name-tool-20 llvm-install-name-tool-19 llvm-install-name-tool-18 llvm-install-name-tool-17 llvm-install-name-tool-16 llvm-install-name-tool-15)
|
||||
if(NOT SINGE_INSTALL_NAME_TOOL)
|
||||
message(FATAL_ERROR "llvm-install-name-tool is needed for the macOS build (apt-get install llvm).")
|
||||
endif()
|
||||
set(CMAKE_INSTALL_NAME_TOOL ${SINGE_INSTALL_NAME_TOOL} CACHE FILEPATH "" FORCE)
|
||||
set(CMAKE_OSX_SYSROOT "" CACHE PATH "Left empty; the wrappers name the SDK themselves" FORCE)
|
||||
set(CMAKE_OSX_ARCHITECTURES "" CACHE STRING "Left empty; the zig target names the architecture" FORCE)
|
||||
set(CMAKE_FIND_ROOT_PATH ${SINGE_MACOS_SDK})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(KANGAROO_OS macos)
|
||||
set(KANGAROO_ARCH ${SINGE_MACOS_ARCH})
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# Joins the Apple silicon and Intel Singe binaries into one universal binary with llvm-lipo.
|
||||
# Run as: cmake -P cmake/zig/macosUniversal.cmake (after both macOS presets have built)
|
||||
|
||||
get_filename_component(buildDir ${CMAKE_CURRENT_LIST_DIR}/../../.builddir ABSOLUTE)
|
||||
# The binaries by name, from the project() line as CMakeLists.txt makes SINGE_VERSION_FULL (patch 1
|
||||
# is a beta and tweak says which), so one an older version left in .builddir is never joined.
|
||||
file(STRINGS ${CMAKE_CURRENT_LIST_DIR}/../../CMakeLists.txt projectLine REGEX "^project\\(singe2 VERSION ")
|
||||
if(NOT projectLine MATCHES "VERSION ([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)")
|
||||
message(FATAL_ERROR "CMakeLists.txt has no four-number project() version.")
|
||||
endif()
|
||||
if(CMAKE_MATCH_3 EQUAL 1)
|
||||
set(version "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}b${CMAKE_MATCH_4}")
|
||||
else()
|
||||
set(version "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}")
|
||||
endif()
|
||||
set(armBinary ${buildDir}/Singe-v${version}-Macos-aarch64)
|
||||
set(intelBinary ${buildDir}/Singe-v${version}-Macos-x86_64)
|
||||
if(NOT EXISTS ${armBinary} OR NOT EXISTS ${intelBinary})
|
||||
message(FATAL_ERROR "Build the macos-aarch64 and macos-x86_64 presets first.")
|
||||
endif()
|
||||
set(universalBinary ${buildDir}/Singe-v${version}-Macos-universal)
|
||||
find_program(SINGE_LIPO NAMES llvm-lipo llvm-lipo-21 llvm-lipo-20 llvm-lipo-19 llvm-lipo-18 llvm-lipo-17 llvm-lipo-16 llvm-lipo-15)
|
||||
if(NOT SINGE_LIPO)
|
||||
message(FATAL_ERROR "llvm-lipo is needed to make the universal binary (apt-get install llvm).")
|
||||
endif()
|
||||
execute_process(COMMAND ${SINGE_LIPO} -create ${armBinary} ${intelBinary} -output ${universalBinary} RESULT_VARIABLE code)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "llvm-lipo failed.")
|
||||
endif()
|
||||
message(STATUS "Wrote ${universalBinary}")
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# Linux x86_64 with zig as the compiler, targeting glibc 2.28 (Debian 10, Ubuntu 18.04) so one
|
||||
# release binary runs on more distributions than the build host's own glibc allows. zig supplies
|
||||
# libc and its headers; the platform libraries (ALSA, X11, DRM, VA-API, VDPAU, GL) and their
|
||||
# headers come from the host's development packages, searched after zig's own so the host's
|
||||
# newer glibc headers never shadow the pinned ones.
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
|
||||
|
||||
set(SINGE_GLIBC_VERSION 2.28)
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
# The linker may see the host's platform libraries but never its libc, libm, libpthread or gcc
|
||||
# runtime, or symbols get bound to the host's newer glibc versions. A directory of links to
|
||||
# everything else under the host's library directory keeps the two apart.
|
||||
set(hostLibs ${CMAKE_BINARY_DIR}/hostlibs)
|
||||
file(MAKE_DIRECTORY ${hostLibs})
|
||||
file(GLOB hostSharedLibs /usr/lib/x86_64-linux-gnu/lib*.so)
|
||||
foreach(lib ${hostSharedLibs})
|
||||
get_filename_component(name ${lib} NAME)
|
||||
if(NOT name MATCHES "^lib(c|m|dl|pthread|rt|resolv|util|anl|nsl|crypt|gcc_s|stdc\\+\\+|BrokenLocale|mvec|memusage|pcprofile|SegFault|thread_db|c_malloc_debug|asan|ubsan|tsan|lsan|atomic|gomp|itm|quadmath)\\.so$")
|
||||
if(NOT EXISTS ${hostLibs}/${name})
|
||||
file(CREATE_LINK ${lib} ${hostLibs}/${name} SYMBOLIC)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# --allow-shlib-undefined: the host's platform libraries reference its newer glibc; at link time only
|
||||
# their symbols matter, and the machine that runs the binary brings its own copies.
|
||||
singeZigToolchain(x86_64-linux-gnu.${SINGE_GLIBC_VERSION} "-idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include -L${hostLibs} -Wl,--allow-shlib-undefined")
|
||||
# No find-root restrictions: the host's development packages are the platform packages here, so
|
||||
# SDL3 must find X11, Wayland, ALSA and the rest where they are. CMake learns the Debian multiarch
|
||||
# library directory from gcc, not from a clang-style compiler, and its compiler detection overwrites
|
||||
# a plain variable, so the settings are forced into the cache or FindX11 never sees libX11.
|
||||
set(CMAKE_LIBRARY_ARCHITECTURE x86_64-linux-gnu CACHE STRING "Debian multiarch library directory name" FORCE)
|
||||
set(CMAKE_C_LIBRARY_ARCHITECTURE x86_64-linux-gnu CACHE STRING "Debian multiarch library directory name" FORCE)
|
||||
set(CMAKE_LIBRARY_PATH /usr/lib/x86_64-linux-gnu CACHE PATH "Where the host platform libraries live" FORCE)
|
||||
set(KANGAROO_OS linux)
|
||||
set(KANGAROO_ARCH x86_64)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
# macOS on Intel; everything is in macos.cmake.
|
||||
|
||||
set(SINGE_MACOS_ARCH x86_64)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/macos.cmake)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# Windows x86_64 with zig as the cross compiler: zig ships the mingw-w64 headers and import
|
||||
# libraries, so nothing else has to be installed.
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
singeZigToolchain(x86_64-windows-gnu "")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(KANGAROO_OS windows)
|
||||
set(KANGAROO_ARCH x86_64)
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
#
|
||||
# Singe 3
|
||||
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 3
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
#
|
||||
|
||||
# The one place the zig version lives. Toolchain files include this and name a target; the
|
||||
# compiler, archiver and resource compiler are small wrapper scripts so every tool that wants a
|
||||
# single program name (FFmpeg's configure, OpenSSL's, CMake itself) gets one.
|
||||
|
||||
# Pinned release, fetched into .builddir/toolchains on first use and checked against its hash.
|
||||
set(ZIG_VERSION 0.15.2)
|
||||
set(ZIG_SHA256 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239)
|
||||
set(ZIG_NAME zig-x86_64-linux-${ZIG_VERSION})
|
||||
set(ZIG_URL https://ziglang.org/download/${ZIG_VERSION}/${ZIG_NAME}.tar.xz)
|
||||
get_filename_component(ZIG_TOOLCHAINS ${CMAKE_CURRENT_LIST_DIR}/../../.builddir/toolchains ABSOLUTE)
|
||||
set(ZIG_ROOT ${ZIG_TOOLCHAINS}/${ZIG_NAME})
|
||||
set(ZIG_EXECUTABLE ${ZIG_ROOT}/zig)
|
||||
if(NOT EXISTS ${ZIG_EXECUTABLE})
|
||||
message(STATUS "Downloading zig ${ZIG_VERSION} from ${ZIG_URL}")
|
||||
file(MAKE_DIRECTORY ${ZIG_TOOLCHAINS})
|
||||
file(DOWNLOAD ${ZIG_URL} ${ZIG_TOOLCHAINS}/${ZIG_NAME}.tar.xz EXPECTED_HASH SHA256=${ZIG_SHA256} STATUS status)
|
||||
list(GET status 0 code)
|
||||
if(NOT code EQUAL 0)
|
||||
message(FATAL_ERROR "zig download failed: ${status}")
|
||||
endif()
|
||||
file(ARCHIVE_EXTRACT INPUT ${ZIG_TOOLCHAINS}/${ZIG_NAME}.tar.xz DESTINATION ${ZIG_TOOLCHAINS})
|
||||
file(REMOVE ${ZIG_TOOLCHAINS}/${ZIG_NAME}.tar.xz)
|
||||
if(NOT EXISTS ${ZIG_EXECUTABLE})
|
||||
message(FATAL_ERROR "zig ${ZIG_VERSION} did not unpack to ${ZIG_ROOT}.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Writes the wrapper scripts for a target into the build tree and points CMake at them. extra is
|
||||
# appended to every compiler invocation (a target's include and library directories).
|
||||
function(singeZigToolchain target extra)
|
||||
set(dir ${CMAKE_BINARY_DIR}/zig-${target})
|
||||
file(MAKE_DIRECTORY ${dir})
|
||||
# The same triple without its glibc version. zig cc refuses the versioned form when it is only
|
||||
# preprocessing -- "version '.2.28' in target triple 'x86_64-unknown-linux-gnu.2.28' is invalid"
|
||||
# -- and asking a compiler for its predefined macros that way is exactly how Meson identifies
|
||||
# one, so dav1d would not configure. Preprocessing generates no code, so the version it loses
|
||||
# there cannot reach anything built.
|
||||
string(REGEX REPLACE "\\.[0-9].*$" "" targetBase "${target}")
|
||||
# The compiler wrappers drop two flags FFmpeg's configure adds to every test link and zig's linker
|
||||
# driver refuses: binutils' --pic-executable ASLR workaround on Windows and Apple's
|
||||
# -dynamic,-search_paths_first on macOS. They also add
|
||||
# -fno-sanitize=undefined: zig cc instruments for UBSan by default and expects its runtime at
|
||||
# link time; a release build wants neither.
|
||||
# "zig cc -Wl,--version" prints zig's own banner rather than the linker's, and Meson reads that
|
||||
# answer to decide which linker it is driving. The linker really is LLD, and asking it directly
|
||||
# says so in the words Meson knows, so the probe is passed straight through to it.
|
||||
set(version "[[ $a == -Wl,--version ]] && exec \"${ZIG_EXECUTABLE}\" ld.lld --version;")
|
||||
set(filter "for a in \"$@\"; do ${version} [[ $a == -Wl,--pic-executable* || $a == -Wl,-dynamic* ]] || args+=(\"$a\"); [[ $a == -E ]] && target=\"-target ${targetBase}\"; done")
|
||||
# extra goes after the caller's arguments so the caller's -L directories are searched first.
|
||||
# ld.lld and objcopy are here for vendored projects that shell out to a linker or a strip of
|
||||
# their own rather than going through the compiler driver: Rockchip's MPP merges its archives
|
||||
# with "ld -r" and then strips the result, and without these it would reach for the host's.
|
||||
foreach(tool "cc;$target -fno-sanitize=undefined;${extra};yes" "c++;$target -fno-sanitize=undefined;${extra};yes" "ar;;;no" "ranlib;;;no" "rc;;;no" "ld.lld;;;no")
|
||||
list(GET tool 0 name)
|
||||
list(GET tool 1 args)
|
||||
list(GET tool 2 after)
|
||||
list(GET tool 3 compiler)
|
||||
if(compiler STREQUAL "yes")
|
||||
file(WRITE ${dir}/zig-${name} "#!/bin/bash\nargs=()\ntarget=\"-target ${target}\"\n${filter}\nexec \"${ZIG_EXECUTABLE}\" ${name} ${args} \"$\{args[@]}\" ${after}\n")
|
||||
else()
|
||||
file(WRITE ${dir}/zig-${name} "#!/bin/bash\nargs=()\n${filter}\nexec \"${ZIG_EXECUTABLE}\" ${name} ${args} \"$\{args[@]}\" ${after}\n")
|
||||
endif()
|
||||
file(CHMOD ${dir}/zig-${name} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
endforeach()
|
||||
set(CMAKE_C_COMPILER ${dir}/zig-cc CACHE FILEPATH "C compiler" FORCE)
|
||||
set(CMAKE_CXX_COMPILER ${dir}/zig-c++ CACHE FILEPATH "C++ compiler" FORCE)
|
||||
set(CMAKE_AR ${dir}/zig-ar CACHE FILEPATH "archiver" FORCE)
|
||||
set(CMAKE_RANLIB ${dir}/zig-ranlib CACHE FILEPATH "ranlib" FORCE)
|
||||
set(CMAKE_RC_COMPILER ${dir}/zig-rc CACHE FILEPATH "resource compiler" FORCE)
|
||||
set(CMAKE_LINKER ${dir}/zig-ld.lld CACHE FILEPATH "linker" FORCE)
|
||||
set(SINGE_ZIG_TARGET ${target} CACHE STRING "zig target triple" FORCE)
|
||||
endfunction()
|
||||
170
demos/README.md
170
demos/README.md
|
|
@ -1,170 +0,0 @@
|
|||
# Singe demos
|
||||
|
||||
Three demos in the old demoscene way: a song each, and everything on screen timed to it. Every
|
||||
one runs live, nothing on screen is a recording of the demo itself, and every motion takes its time
|
||||
from the music's clock (`musicGetPosition`), so a demo stays with its song on any machine at any
|
||||
frame rate.
|
||||
|
||||
| Demo | Song | Length |
|
||||
|---|---|---|
|
||||
| Neon Coast | "Neons in Napoli" by eliza_music | 3:08 |
|
||||
| Mirrorball | "Vintage Funky Groove" by miiira | 4:05 |
|
||||
| Insert Coin | "Game Is On" by poorartistt | 4:10 |
|
||||
|
||||
Each folder has its own `games.dat`, so Singe runs a demo like any game: from this folder,
|
||||
`Singe neonCoast/neonCoast.singe`. They need the 3D scene. `kit.lua` is the code all three share,
|
||||
and each demo carries its own copy of it (and of the logos), so a demo packs into a `.game` with
|
||||
nothing outside its folder; a change to one copy belongs in all three. The kit holds
|
||||
the music clock, beats and bars, the envelopes and drum hits, the scene runner, and the effects
|
||||
more than one demo uses: vector bobs, scrollers of five kinds (sine, bounce, zoom, spin, round a
|
||||
circle), a 3D scroller, and a perspective crawl, flashes and strobes, chrome and glass materials, a camera-filling
|
||||
quad, a torus knot, and three effects worked out a pixel at a time in Lua every frame and handed to
|
||||
a sprite with `spriteSetPixels`: a plasma with a cycling palette, a lookup-table tunnel, and a
|
||||
tape's snow. Nothing in any of the three demos is a video.
|
||||
|
||||
The demos run at 1920 by 1080, the canvas and window in each `games.dat`. They are laid out in
|
||||
design units, 480 tall and as wide as the screen's shape makes them (853 at 16:9), and the kit turns
|
||||
those into the window's pixels as it draws, `K.S` (2.25) to a unit: `K.draw` and `K.drawGrid` for
|
||||
sprites, `K.text` and `K.font` for lettering rendered at full resolution, `K.width` and `K.height`
|
||||
for sizes. Art the util scripts draw is made 2.25 pixels to a unit and loaded with `K.sprite(path,
|
||||
density)`, so it draws pixel for pixel; Insert Coin's pixel art is drawn a whole number of screen
|
||||
pixels to each of its own instead, so every art pixel is the same size. A whole-screen 2D layer
|
||||
would cost the CPU milliseconds a frame on a 1920 by 1080 overlay, so those are quads on the GPU:
|
||||
`K.fill` (a background), `K.under` (a sprite or a color), and `K.turn` (a sprite turned and
|
||||
scaled, which on the overlay would be rebuilt every frame) between the 3D world and the overlay,
|
||||
`K.backdrop` behind the world, and `K.flash` and `K.cover` (scanlines) over everything. Each demo
|
||||
calls `K.screen(camera, fov, far)` once its camera is made.
|
||||
|
||||
Every demo opens on the Singe dragon and lettering, faded up on black, held, and faded out, before
|
||||
its music starts, and closes the same way on the Kangaroo Punch logo after the music ends and the
|
||||
picture fades to black. util/demoLogos.sh copies the logos' SVGs from media/ into every demo's art/ (the
|
||||
lettering turned near-white for black), and the kit draws them from the SVG at the screen's
|
||||
resolution (`K.vector`).
|
||||
They are on the demo's own clock (the opening card runs from -3.5 s to 0), so util/demoCapture.sh
|
||||
takes them with a negative start or a stretch past the end.
|
||||
|
||||
Scroller text can be changed freely. Each scroller is given the bars it runs between, not a speed:
|
||||
`K.scroller(t, font, text, y, amp, startTime, endTime, style)` works its speed out from the text so
|
||||
the first letter enters as the scene starts and the last has left a second and a half before it
|
||||
ends, so the words are finished before the cut. Text too short to fill
|
||||
the time at a readable speed is repeated until it does; text too long to read at its speed still
|
||||
ends on time and puts a line in the trace (`DEMO scroller too long to read`, with about how many
|
||||
characters would fit). `K.scroller3D(t, blocks, text, y, startTime, endTime)` is timed the same
|
||||
way, with solid letters from `K.blockFont(glyphs, camera, fov, cell, alpha)`: a pixel font
|
||||
extruded into blocks, hung off the camera in the scene's front layer (`nodeSetLayer`), so they
|
||||
stand over 2D and 3D alike. Its letters are larger, so it is allowed to go slower before it
|
||||
repeats. The text round the circle is written in over the first half of its scene,
|
||||
and the crawl spaces whatever lines it is given over its scene, the same way.
|
||||
|
||||
## How the timing works
|
||||
|
||||
`util/demoSync.py` listens to a song once, offline, and writes `sync.lua` beside the demo:
|
||||
|
||||
- **Beats.** A song on a fixed tempo gets a grid, found by folding the onsets of the whole track at
|
||||
every candidate period and keeping the sharpest; Neon Coast's is 122.000 beats a minute and
|
||||
Insert Coin's 135.000, with the tracked beats within 4 ms of them. Mirrorball's song speeds up
|
||||
from about 111 to 117, so it gets the tracker's beats, each moved to the strongest onset near it
|
||||
and smoothed through its neighbors, and carried on past where the tracker gave up.
|
||||
- **Bars.** Beat one is the beat where the loudness and the bass jump most often, since songs
|
||||
change section on a downbeat.
|
||||
- **Envelopes.** Kick, snare, and hat (the change in each drum's frequency band), bass, loudness,
|
||||
and brightness, sixty samples a second, each 0 to 1, packed one character a sample.
|
||||
- **Hits.** The times of the kicks, snares, and hats, for effects that fire once per hit.
|
||||
|
||||
A scene in a demo says which bars it spans, so cuts land on downbeats, and inside it effects read
|
||||
the envelopes and hits at the current time: a flash that decays from every kick, a formation that
|
||||
steps on every beat, a tunnel whose speed the kick pushes.
|
||||
|
||||
## The songs, and what happens in them
|
||||
|
||||
**Neon Coast** (bar n starts at 0.1008 + (n - 1) x 1.96721 s)
|
||||
|
||||
| Bars | Song | Demo |
|
||||
|---|---|---|
|
||||
| 1-3 | pads only | a tape starts: snow, the deck's lettering, the title typed out |
|
||||
| 4-16 | bass in | the sunset grid; the chrome logotype rises at 9, the title slams in at 13 |
|
||||
| 17-18 | bass out, snare build | the logotype turns, the fog closes in, strobes on the snare roll |
|
||||
| 19-28 | full groove | the night drive, fast: two streetlights pass the car on every beat, a 3D city on the horizon; a bouncing scroller |
|
||||
| 29-30 | bass out | the car flies off the crest in slow motion, the road dropping away below, and lands on bar 31 |
|
||||
| 31-38 | main section | the chrome knot and glass, a cut on every downbeat; a spinning scroller |
|
||||
| 39-46 | main section | slaloming round the towers of a neon city, a swerve a bar and a barrel roll every two |
|
||||
| 47-48 | filtered drop | the tape rewinds the flight |
|
||||
| 49-58 | busy hats | the tunnel, scrubbed by the kick; vector bobs and the scroller |
|
||||
| 59-60 | bass out | the tunnel stops, the bobs fall in, ARE YOU READY |
|
||||
| 61-64 | loud | plasma, a rotozoomed logo, a ring of palms, text written round a circle |
|
||||
| 65-68 | loud | low and fast over neon mountains toward the sun, under a perspective crawl of Singe's features |
|
||||
| 69-76 | breakdown, then the kick | a cloth flag waving on the night grid, gusting with the kicks once they return; a zooming scroller |
|
||||
| 77-78 | riser, dead stop | into hyperspace, white, and black for the stop |
|
||||
| 79-94 | finale | road, city, knot, and mountains, cut every two bars, fireworks on the snares; over the road the logotype and the dragon, beating its wings on the beat |
|
||||
| 95- | ending | the set switches off |
|
||||
|
||||
**Mirrorball** (eight-bar phrases from bar 1 at 0.79 s; the bars shorten as the song speeds up)
|
||||
|
||||
| Bars | Song | Demo |
|
||||
|---|---|---|
|
||||
| 1-8 | choppy drums and bass | copper bars gated by the groove, the title dropping a letter at a time |
|
||||
| 9-16 | chords | the club: the ball comes down, spots sweep, the floor swaps on every beat |
|
||||
| 17-24 | lead | a rotozoomer, copper bars colored by the note being played, the scroller |
|
||||
| 25-28 | full | a mirrored kaleidoscope of plasma under vector bobs |
|
||||
| 29-32 | | a cloud of glowing dots that takes a new shape every bar |
|
||||
| 33-40 | choppy again | the club, cut on every beat, black in the gaps, the lasers changing pattern every bar |
|
||||
| 41-48 | denser hats | the twister, drawn a scanline at a time, over copper bars |
|
||||
| 49-52 | horns | a checkered ball bouncing on the beat; a ball drops on every snare and piles up |
|
||||
| 53-56 | | an equalizer city, a band of the music to each column and its last moments running back |
|
||||
| 57-60 | | glenz glass in a tunnel scrubbed by the kick |
|
||||
| 61-64 | | a turntable, the record scratched on every snare, the camera cutting every two beats |
|
||||
| 65-72 | dense | lasers in the haze: 24 beams in six patterns, a new one every two beats |
|
||||
| 73-80 | | plasma and a rotozoomed logo |
|
||||
| 81-88 | breakdown | the dark club; the floor relights a tile a beat when the bass returns |
|
||||
| 89-96 | build | everything back on, strobing |
|
||||
| 97-112 | full, then final | the party, cutting faster: club, dots, equalizer, club spinning, turntable, club |
|
||||
| 113- | the last downbeat | the ball shatters and the lights go down |
|
||||
|
||||
**Insert Coin** (bar n starts at 0.0067 + (n - 1) x 1.77778 s)
|
||||
|
||||
From bar 1 to bar 121, a large scroller of translucent 3D block letters (`FEATURE_TEXT` near the
|
||||
top of insertCoin.singe, in the demo's own pixel font from `art/font.lua`) runs over every era,
|
||||
saying what Singe is and what it can do.
|
||||
|
||||
| Bars | Song | Demo |
|
||||
|---|---|---|
|
||||
| 1-4 | intro | a tube warms up; INSERT COIN, and a coin drops on the downbeat of 5 |
|
||||
| 5-8 | drums in | analog: bat and ball in phosphor, the ball meeting a bat on every beat |
|
||||
| 9-16 | groove | 8-bit: a formation that steps on the beat; the cannon fires on the snare |
|
||||
| 17-24 | lead melody | 16-bit: a runner over parallax hills, jumping on the backbeat |
|
||||
| 25-32 | lead melody | 16-bit: a kart round a track seen in perspective |
|
||||
| 33-40 | breakdown, riser | a tape loading |
|
||||
| 41-48 | full | vector: glowing wireframe asteroids, split by the ship's aimed shot on every snare |
|
||||
| 49-56 | full | 32-bit: a flat-shaded ship weaving a winding path through gates, one a beat, banking into the turns |
|
||||
| 57-64 | heavy | light gun: targets slide along three rails, and each snare flashes the screen and drops the one under the crosshair |
|
||||
| 65-72 | heavy | now: chrome balls into a wall of glossy bricks, real physics, on the kick |
|
||||
| 73-80 | breakdown | GAME OVER, and a countdown to continue, a number a bar |
|
||||
| 81-88 | melody returns | versus: two fighters trade blows on the kicks and snares, ROUND 1, FIGHT!, K.O. |
|
||||
| 89-96 | melody | the boss, firing on the kick, the hats, and the snare |
|
||||
| 97-104 | full | the bonus stage: coins rain down far along the tunnel and ride its floor off the screen |
|
||||
| 105-112 | full | rhythm: a note highway whose notes are the song's own kicks, snares, and hats |
|
||||
| 113-120 | melody | the high score table, which is the credits |
|
||||
| 121-136 | final | every era again, a bar each, then GAME ON |
|
||||
| 137- | outro | GAME OVER, and the set switched off on the last hit |
|
||||
|
||||
## Making them again
|
||||
|
||||
Everything in `art/` and `sync.lua` is generated:
|
||||
|
||||
- `util/neonCoastArt.py`, `util/mirrorballArt.py`, `util/insertCoinArt.py` draw each demo's
|
||||
pictures (PIL and numpy).
|
||||
- `util/neonCoastCar.sh` renders Neon Coast's car with Singe's own 3D renderer
|
||||
(`util/neonCoastCar.singe` builds the model): seven steering angles, each drawn over black and
|
||||
over white so every pixel's true alpha comes out of the pair.
|
||||
- `util/demoSync.py` writes `sync.lua` from `music.mp3`. It needs librosa, which Singe does not:
|
||||
`python3 -m venv env && env/bin/pip install librosa`.
|
||||
|
||||
`util/demoRecord.sh neonCoast out.mp4` records a whole demo with its music, using Singe's own
|
||||
recorder: `--deterministic` and `--record` together keep every frame and generate the sound in
|
||||
step with the picture, and Neon Coast's three minutes take about two and a half on a desktop.
|
||||
|
||||
`util/demoCapture.sh neonCoast 0 190 [every] [out.mp4]` runs a stretch of a demo headless on a
|
||||
clock that moves exactly one 20 ms step a frame, saves the frames, and with `every` = 1 puts the
|
||||
same stretch of the music under them in an MP4. The engine plays the music on real time, so a
|
||||
capture is silent and the song is added afterwards; a demo run normally plays it itself. Set
|
||||
`DEMO_STATS = true` before loading a demo to have it print its frame rate every second.
|
||||
BIN
demos/insertCoin/art/bossArm.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/bossArm.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/bossCore.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/bossCore.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/bullet.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/bullet.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/cannon.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/cannon.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/coin.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/coin.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/coinDragon.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/coinDragon.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/coinSinge.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/coinSinge.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="193 22 889 749" width="889" height="749"><g transform="translate(1275 0) scale(-1 1)">
|
||||
<!-- Singe dragon, reconstructed as polygons from Dragon.jpeg; transparent background. -->
|
||||
<defs>
|
||||
<linearGradient id="g0" gradientUnits="userSpaceOnUse" x1="698.7" y1="96.1" x2="629.6" y2="137.3"><stop offset="0" stop-color="#6f7277"/><stop offset="1" stop-color="#97969a"/></linearGradient>
|
||||
<linearGradient id="g1" gradientUnits="userSpaceOnUse" x1="503.6" y1="307.1" x2="382.6" y2="159.3"><stop offset="0" stop-color="#66686f"/><stop offset="1" stop-color="#8a8b90"/></linearGradient>
|
||||
<linearGradient id="g2" gradientUnits="userSpaceOnUse" x1="845.5" y1="159.5" x2="866.9" y2="156.5"><stop offset="0" stop-color="#73777b"/><stop offset="1" stop-color="#73767c"/></linearGradient>
|
||||
<linearGradient id="g3" gradientUnits="userSpaceOnUse" x1="835.5" y1="229.1" x2="842.3" y2="253.1"><stop offset="0" stop-color="#54575d"/><stop offset="1" stop-color="#5a5d63"/></linearGradient>
|
||||
<linearGradient id="g4" gradientUnits="userSpaceOnUse" x1="738.8" y1="402.4" x2="778.4" y2="373.8"><stop offset="0" stop-color="#4e5357"/><stop offset="1" stop-color="#55595e"/></linearGradient>
|
||||
<linearGradient id="g5" gradientUnits="userSpaceOnUse" x1="502.9" y1="620.5" x2="456.0" y2="520.4"><stop offset="0" stop-color="#52575e"/><stop offset="1" stop-color="#5a6166"/></linearGradient>
|
||||
<linearGradient id="g6" gradientUnits="userSpaceOnUse" x1="854.9" y1="616.3" x2="901.1" y2="622.7"><stop offset="0" stop-color="#33393e"/><stop offset="1" stop-color="#373c42"/></linearGradient>
|
||||
<linearGradient id="g7" gradientUnits="userSpaceOnUse" x1="364.2" y1="664.4" x2="360.7" y2="746.1"><stop offset="0" stop-color="#353b40"/><stop offset="1" stop-color="#40474d"/></linearGradient>
|
||||
<linearGradient id="g8" gradientUnits="userSpaceOnUse" x1="965.4" y1="697.5" x2="906.6" y2="706.0"><stop offset="0" stop-color="#63686c"/><stop offset="1" stop-color="#676c70"/></linearGradient>
|
||||
<linearGradient id="g9" gradientUnits="userSpaceOnUse" x1="894.1" y1="714.4" x2="875.2" y2="729.2"><stop offset="0" stop-color="#43474a"/><stop offset="1" stop-color="#45474b"/></linearGradient>
|
||||
<linearGradient id="g10" gradientUnits="userSpaceOnUse" x1="682.0" y1="67.3" x2="737.5" y2="95.0"><stop offset="0" stop-color="#d5c9bc"/><stop offset="1" stop-color="#dbcdc1"/></linearGradient>
|
||||
<linearGradient id="g11" gradientUnits="userSpaceOnUse" x1="979.3" y1="183.9" x2="958.7" y2="176.5"><stop offset="0" stop-color="#9b9187"/><stop offset="1" stop-color="#b9aea5"/></linearGradient>
|
||||
<linearGradient id="g12" gradientUnits="userSpaceOnUse" x1="924.1" y1="150.5" x2="852.7" y2="205.4"><stop offset="0" stop-color="#c2b6a8"/><stop offset="1" stop-color="#f1e7d9"/></linearGradient>
|
||||
<linearGradient id="g13" gradientUnits="userSpaceOnUse" x1="663.8" y1="225.3" x2="726.2" y2="233.1"><stop offset="0" stop-color="#d4c4b2"/><stop offset="1" stop-color="#dccebd"/></linearGradient>
|
||||
<linearGradient id="g14" gradientUnits="userSpaceOnUse" x1="945.6" y1="245.5" x2="885.2" y2="257.5"><stop offset="0" stop-color="#837b70"/><stop offset="1" stop-color="#b0a292"/></linearGradient>
|
||||
<linearGradient id="g15" gradientUnits="userSpaceOnUse" x1="889.0" y1="403.3" x2="817.3" y2="411.5"><stop offset="0" stop-color="#a1998c"/><stop offset="1" stop-color="#e4d7c8"/></linearGradient>
|
||||
<linearGradient id="g16" gradientUnits="userSpaceOnUse" x1="886.4" y1="506.4" x2="918.1" y2="507.5"><stop offset="0" stop-color="#857f74"/><stop offset="1" stop-color="#8b8279"/></linearGradient>
|
||||
<linearGradient id="g17" gradientUnits="userSpaceOnUse" x1="881.7" y1="569.7" x2="827.2" y2="506.1"><stop offset="0" stop-color="#888175"/><stop offset="1" stop-color="#a49b8d"/></linearGradient>
|
||||
<linearGradient id="g18" gradientUnits="userSpaceOnUse" x1="208.2" y1="559.7" x2="242.4" y2="555.4"><stop offset="0" stop-color="#aa9c8d"/><stop offset="1" stop-color="#b3a693"/></linearGradient>
|
||||
<linearGradient id="g19" gradientUnits="userSpaceOnUse" x1="852.5" y1="644.9" x2="736.7" y2="633.3"><stop offset="0" stop-color="#928a7d"/><stop offset="1" stop-color="#daccbc"/></linearGradient>
|
||||
<linearGradient id="g20" gradientUnits="userSpaceOnUse" x1="224.4" y1="631.6" x2="303.9" y2="662.7"><stop offset="0" stop-color="#8d8376"/><stop offset="1" stop-color="#b0a396"/></linearGradient>
|
||||
<linearGradient id="g21" gradientUnits="userSpaceOnUse" x1="1017.2" y1="708.7" x2="974.7" y2="752.6"><stop offset="0" stop-color="#726b60"/><stop offset="1" stop-color="#867e73"/></linearGradient>
|
||||
<linearGradient id="g22" gradientUnits="userSpaceOnUse" x1="560.7" y1="705.9" x2="600.9" y2="763.2"><stop offset="0" stop-color="#898072"/><stop offset="1" stop-color="#a89b8b"/></linearGradient>
|
||||
<linearGradient id="g23" gradientUnits="userSpaceOnUse" x1="801.0" y1="120.8" x2="796.8" y2="140.0"><stop offset="0" stop-color="#e5e4e4"/><stop offset="1" stop-color="#eff0ef"/></linearGradient>
|
||||
<linearGradient id="g24" gradientUnits="userSpaceOnUse" x1="795.2" y1="187.5" x2="744.9" y2="264.6"><stop offset="0" stop-color="#d6dad6"/><stop offset="1" stop-color="#e7e8e7"/></linearGradient>
|
||||
<linearGradient id="g25" gradientUnits="userSpaceOnUse" x1="444.3" y1="360.6" x2="343.6" y2="248.4"><stop offset="0" stop-color="#eaeaea"/><stop offset="1" stop-color="#f1f1f0"/></linearGradient>
|
||||
<linearGradient id="g26" gradientUnits="userSpaceOnUse" x1="774.0" y1="291.5" x2="756.7" y2="328.6"><stop offset="0" stop-color="#e0e1de"/><stop offset="1" stop-color="#e7e8e5"/></linearGradient>
|
||||
<linearGradient id="g27" gradientUnits="userSpaceOnUse" x1="504.5" y1="428.2" x2="600.5" y2="343.0"><stop offset="0" stop-color="#e6e8e7"/><stop offset="1" stop-color="#e9eae8"/></linearGradient>
|
||||
<linearGradient id="g28" gradientUnits="userSpaceOnUse" x1="270.4" y1="455.9" x2="305.1" y2="499.0"><stop offset="0" stop-color="#e7e8e7"/><stop offset="1" stop-color="#ececeb"/></linearGradient>
|
||||
<linearGradient id="g29" gradientUnits="userSpaceOnUse" x1="780.3" y1="469.6" x2="741.5" y2="534.9"><stop offset="0" stop-color="#dbdfdb"/><stop offset="1" stop-color="#e6e7e4"/></linearGradient>
|
||||
<linearGradient id="g30" gradientUnits="userSpaceOnUse" x1="572.4" y1="460.6" x2="554.8" y2="532.2"><stop offset="0" stop-color="#e1e3e0"/><stop offset="1" stop-color="#eaeae9"/></linearGradient>
|
||||
<linearGradient id="g31" gradientUnits="userSpaceOnUse" x1="373.0" y1="591.4" x2="450.6" y2="588.1"><stop offset="0" stop-color="#e1e4e0"/><stop offset="1" stop-color="#e5e7e4"/></linearGradient>
|
||||
<linearGradient id="g32" gradientUnits="userSpaceOnUse" x1="634.8" y1="522.2" x2="694.0" y2="646.0"><stop offset="0" stop-color="#cfd5d1"/><stop offset="1" stop-color="#d8ddd9"/></linearGradient>
|
||||
<linearGradient id="g33" gradientUnits="userSpaceOnUse" x1="488.1" y1="692.3" x2="494.0" y2="725.6"><stop offset="0" stop-color="#d5dad6"/><stop offset="1" stop-color="#e2e5e2"/></linearGradient>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="130%" height="130%"><feDropShadow dx="3" dy="5" stdDeviation="4" flood-color="#000" flood-opacity="0.18"/></filter>
|
||||
<filter id="brush" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.003 0.7" numOctaves="2" seed="7" result="noise"/><feColorMatrix in="noise" type="saturate" values="0" result="gray"/><feComponentTransfer in="gray" result="grain"><feFuncA type="linear" slope="0.22"/></feComponentTransfer><feComposite in="grain" in2="SourceGraphic" operator="in" result="clipped"/><feBlend in="clipped" in2="SourceGraphic" mode="overlay"/></filter>
|
||||
</defs>
|
||||
<title>Singe dragon</title>
|
||||
<g filter="url(#shadow)">
|
||||
<polygon points="620.3,450.0 420.1,406.2 575.1,277.9 680.5,456.9" fill="url(#g27)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="718.9,692.9 547.2,565.9 642.9,498.6 688.8,474.3 721.0,511.5" fill="url(#g32)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="832.0,437.8 829.7,543.9 705.5,374.0 673.4,303.6 704.9,256.9" fill="url(#g4)" stroke="#aaacae" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="573.2,269.1 519.7,314.7 404.5,263.3 302.7,135.5" fill="url(#g1)" stroke="#c4c5c7" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="281.8,224.7 375.5,258.3 510.4,318.9 414.4,400.8" fill="url(#g25)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="732.1,520.7 894.5,701.6 730.0,691.8" fill="url(#g19)" stroke="#ece5dd" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="412.5,697.6 353.4,656.7 383.4,543.9 473.3,459.7" fill="url(#g31)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="320.8,754.9 288.6,624.8 486.2,759.6" fill="url(#g7)" stroke="#9fa3a6" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="481.8,461.2 539.9,571.7 415.1,701.2" fill="url(#g5)" stroke="#acb0b2" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="785.2,280.5 915.9,436.6 874.0,506.8 815.2,407.5" fill="url(#g15)" stroke="#f1ebe3" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="674.6,467.0 539.5,557.8 484.1,450.4" fill="url(#g30)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="829.1,553.8 825.4,615.9 692.3,464.8 719.1,401.9" fill="url(#g29)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="346.0,386.3 325.7,487.9 266.2,553.8 224.8,479.9" fill="url(#g28)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="266.0,555.8 310.0,745.6 202.7,639.4" fill="url(#g20)" stroke="#d7d1ca" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="772.9,160.4 839.1,206.4 782.1,265.6 730.4,279.6 711.1,253.1" fill="url(#g24)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="481.7,755.7 587.6,673.5 660.8,760.2" fill="url(#g22)" stroke="#d3cdc5" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="656.2,206.4 767.6,153.0 644.6,335.1" fill="url(#g13)" stroke="#ede6de" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="837.1,618.3 834.8,444.4 906.2,561.9" fill="url(#g17)" stroke="#d1cdc6" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="956.0,158.3 956.0,190.3 862.2,209.3 774.5,150.0 819.0,146.0" fill="url(#g12)" stroke="#f8f3ec" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="933.1,751.6 995.8,671.8 1073.3,758.9" fill="url(#g21)" stroke="#c2beb9" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="778.2,149.8 640.3,144.9 552.5,57.2" fill="url(#g0)" stroke="#cbcacc" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="928.4,545.6 882.6,680.7 833.3,626.6" fill="url(#g6)" stroke="#9b9da0" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="421.4,698.7 579.0,670.2 483.0,746.1" fill="url(#g33)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="883.2,680.4 990.9,673.4 926.5,753.0" fill="url(#g8)" stroke="#b3b5b7" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="930.3,539.5 906.2,561.1 871.3,508.2 912.0,435.2" fill="url(#g16)" stroke="#c5c0bc" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="257.8,560.4 201.9,632.0 214.1,482.6" fill="url(#g18)" stroke="#d9d2c9" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="783.1,149.1 712.8,111.3 830.2,116.0 874.7,144.4" fill="url(#g23)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="940.3,291.2 842.3,211.4 966.9,239.2" fill="url(#g14)" stroke="#d7d0c8" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="887.2,248.1 785.9,269.7 841.6,212.7" fill="url(#g3)" stroke="#acaeb1" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="771.8,272.5 797.9,379.7 733.9,288.2" fill="url(#g26)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="700.8,100.8 653.0,30.8 778.0,107.7" fill="url(#g10)" stroke="#ede6e0" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="826.6,702.0 893.1,702.0 934.9,762.2" fill="url(#g9)" stroke="#a2a3a5" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="954.9,193.0 958.8,135.4 989.6,179.7 970.4,218.2" fill="url(#g11)" stroke="#dcd6d2" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="825.0,149.0 887.9,149.0 854.9,174.1" fill="url(#g2)" stroke="#b9bbbd" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
</g>
|
||||
</g></svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
BIN
demos/insertCoin/art/duskSky.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/duskSky.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/fighterA.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/fighterA.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/fighterB.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/fighterB.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,67 +0,0 @@
|
|||
-- Insert Coin's pixel font, written by util/insertCoinArt.py from its GLYPHS: rows top down, "#" lit.
|
||||
return {
|
||||
[" "] = { " ", " ", " ", " ", " ", " ", " " },
|
||||
["!"] = { " # ", " # ", " # ", " # ", " # ", " ", " # " },
|
||||
["\""] = { " # # ", " # # ", " ", " ", " ", " ", " " },
|
||||
["#"] = { " # # ", "#####", " # # ", " # # ", " # # ", "#####", " # # " },
|
||||
["$"] = { " # ", " ####", "# # ", " ### ", " # #", "#### ", " # " },
|
||||
["%"] = { "## ", "## #", " # ", " # ", " # ", "# ##", " ##" },
|
||||
["&"] = { " ## ", "# # ", "# # ", " # ", "# # #", "# # ", " ## #" },
|
||||
["'"] = { " # ", " # ", " ", " ", " ", " ", " " },
|
||||
["("] = { " # ", " # ", " # ", " # ", " # ", " # ", " # " },
|
||||
[")"] = { " # ", " # ", " # ", " # ", " # ", " # ", " # " },
|
||||
["*"] = { " ", " # ", "# # #", " ### ", "# # #", " # ", " " },
|
||||
["+"] = { " ", " # ", " # ", "#####", " # ", " # ", " " },
|
||||
[","] = { " ", " ", " ", " ", " ## ", " # ", " # " },
|
||||
["-"] = { " ", " ", " ", "#####", " ", " ", " " },
|
||||
["."] = { " ", " ", " ", " ", " ", " ## ", " ## " },
|
||||
["/"] = { " ", " #", " # ", " # ", " # ", "# ", " " },
|
||||
["0"] = { " ### ", "# #", "# ##", "# # #", "## #", "# #", " ### " },
|
||||
["1"] = { " # ", " ## ", " # ", " # ", " # ", " # ", " ### " },
|
||||
["2"] = { " ### ", "# #", " #", " # ", " # ", " # ", "#####" },
|
||||
["3"] = { "#####", " # ", " # ", " # ", " #", "# #", " ### " },
|
||||
["4"] = { " # ", " ## ", " # # ", "# # ", "#####", " # ", " # " },
|
||||
["5"] = { "#####", "# ", "#### ", " #", " #", "# #", " ### " },
|
||||
["6"] = { " ## ", " # ", "# ", "#### ", "# #", "# #", " ### " },
|
||||
["7"] = { "#####", " #", " # ", " # ", " # ", " # ", " # " },
|
||||
["8"] = { " ### ", "# #", "# #", " ### ", "# #", "# #", " ### " },
|
||||
["9"] = { " ### ", "# #", "# #", " ####", " #", " # ", " ## " },
|
||||
[":"] = { " ", " ## ", " ## ", " ", " ## ", " ## ", " " },
|
||||
[";"] = { " ", " ## ", " ## ", " ", " ## ", " # ", " # " },
|
||||
["<"] = { " # ", " # ", " # ", "# ", " # ", " # ", " # " },
|
||||
["="] = { " ", " ", "#####", " ", "#####", " ", " " },
|
||||
[">"] = { " # ", " # ", " # ", " #", " # ", " # ", " # " },
|
||||
["?"] = { " ### ", "# #", " #", " # ", " # ", " ", " # " },
|
||||
["@"] = { " ### ", "# #", "# ###", "# # #", "# ###", "# ", " ####" },
|
||||
["A"] = { " ### ", "# #", "# #", "#####", "# #", "# #", "# #" },
|
||||
["B"] = { "#### ", "# #", "# #", "#### ", "# #", "# #", "#### " },
|
||||
["C"] = { " ### ", "# #", "# ", "# ", "# ", "# #", " ### " },
|
||||
["D"] = { "#### ", "# #", "# #", "# #", "# #", "# #", "#### " },
|
||||
["E"] = { "#####", "# ", "# ", "#### ", "# ", "# ", "#####" },
|
||||
["F"] = { "#####", "# ", "# ", "#### ", "# ", "# ", "# " },
|
||||
["G"] = { " ### ", "# #", "# ", "# ###", "# #", "# #", " ####" },
|
||||
["H"] = { "# #", "# #", "# #", "#####", "# #", "# #", "# #" },
|
||||
["I"] = { " ### ", " # ", " # ", " # ", " # ", " # ", " ### " },
|
||||
["J"] = { " ###", " # ", " # ", " # ", " # ", "# # ", " ## " },
|
||||
["K"] = { "# #", "# # ", "# # ", "## ", "# # ", "# # ", "# #" },
|
||||
["L"] = { "# ", "# ", "# ", "# ", "# ", "# ", "#####" },
|
||||
["M"] = { "# #", "## ##", "# # #", "# # #", "# #", "# #", "# #" },
|
||||
["N"] = { "# #", "# #", "## #", "# # #", "# ##", "# #", "# #" },
|
||||
["O"] = { " ### ", "# #", "# #", "# #", "# #", "# #", " ### " },
|
||||
["P"] = { "#### ", "# #", "# #", "#### ", "# ", "# ", "# " },
|
||||
["Q"] = { " ### ", "# #", "# #", "# #", "# # #", "# # ", " ## #" },
|
||||
["R"] = { "#### ", "# #", "# #", "#### ", "# # ", "# # ", "# #" },
|
||||
["S"] = { " ####", "# ", "# ", " ### ", " #", " #", "#### " },
|
||||
["T"] = { "#####", " # ", " # ", " # ", " # ", " # ", " # " },
|
||||
["U"] = { "# #", "# #", "# #", "# #", "# #", "# #", " ### " },
|
||||
["V"] = { "# #", "# #", "# #", "# #", "# #", " # # ", " # " },
|
||||
["W"] = { "# #", "# #", "# #", "# # #", "# # #", "# # #", " # # " },
|
||||
["X"] = { "# #", "# #", " # # ", " # ", " # # ", "# #", "# #" },
|
||||
["Y"] = { "# #", "# #", " # # ", " # ", " # ", " # ", " # " },
|
||||
["Z"] = { "#####", " #", " # ", " # ", " # ", "# ", "#####" },
|
||||
["["] = { " ### ", " # ", " # ", " # ", " # ", " # ", " ### " },
|
||||
["\\"] = { " ", "# ", " # ", " # ", " # ", " #", " " },
|
||||
["]"] = { " ### ", " # ", " # ", " # ", " # ", " # ", " ### " },
|
||||
["^"] = { " # ", " # # ", "# #", " ", " ", " ", " " },
|
||||
["_"] = { " ", " ", " ", " ", " ", " ", "#####" },
|
||||
}
|
||||
BIN
demos/insertCoin/art/font.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/font.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/ground.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/ground.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/hills.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/hills.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/invaderA.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/invaderA.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/invaderB.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/invaderB.png
(Stored with Git LFS)
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 427 KiB |
BIN
demos/insertCoin/art/kart.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/kart.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/kart1.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/kart1.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/kart2.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/kart2.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/kart3.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/kart3.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/kart4.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/kart4.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/kart5.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/kart5.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/mountains.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/mountains.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/raceSky.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/raceSky.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/runner.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/runner.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/scanlines.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/scanlines.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/ship.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/ship.png
(Stored with Git LFS)
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
BIN
demos/insertCoin/art/target.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/target.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/track.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/track.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/insertCoin/art/turret.png
(Stored with Git LFS)
BIN
demos/insertCoin/art/turret.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,16 +0,0 @@
|
|||
-- Insert Coin: a demo for Singe, to "Game Is On" by poorartistt.
|
||||
GAMES = {
|
||||
{
|
||||
TITLE = "Insert Coin",
|
||||
SCRIPT = "insertCoin/insertCoin.singe",
|
||||
CANVAS_X = 1920,
|
||||
CANVAS_Y = 1080,
|
||||
RESOLUTION_X = 1920,
|
||||
RESOLUTION_Y = 1080,
|
||||
REQUIRES = { "SCENE", "POST", "SHADOWS" },
|
||||
GAME_ID = "07678fa2-6d97-450f-ac8c-a7483d3ce9c1",
|
||||
YEAR = 2026,
|
||||
GENRE = "Demo",
|
||||
DESCRIPTION = "A demo for Singe, to \"Game Is On\" by poorartistt."
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
BIN
demos/insertCoin/music.mp3
(Stored with Git LFS)
BIN
demos/insertCoin/music.mp3
(Stored with Git LFS)
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
demos/mirrorball/art/bob.png
(Stored with Git LFS)
BIN
demos/mirrorball/art/bob.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/mirrorball/art/boing.png
(Stored with Git LFS)
BIN
demos/mirrorball/art/boing.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/mirrorball/art/club.png
(Stored with Git LFS)
BIN
demos/mirrorball/art/club.png
(Stored with Git LFS)
Binary file not shown.
BIN
demos/mirrorball/art/copper.png
(Stored with Git LFS)
BIN
demos/mirrorball/art/copper.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="193 22 889 749" width="889" height="749"><g transform="translate(1275 0) scale(-1 1)">
|
||||
<!-- Singe dragon, reconstructed as polygons from Dragon.jpeg; transparent background. -->
|
||||
<defs>
|
||||
<linearGradient id="g0" gradientUnits="userSpaceOnUse" x1="698.7" y1="96.1" x2="629.6" y2="137.3"><stop offset="0" stop-color="#6f7277"/><stop offset="1" stop-color="#97969a"/></linearGradient>
|
||||
<linearGradient id="g1" gradientUnits="userSpaceOnUse" x1="503.6" y1="307.1" x2="382.6" y2="159.3"><stop offset="0" stop-color="#66686f"/><stop offset="1" stop-color="#8a8b90"/></linearGradient>
|
||||
<linearGradient id="g2" gradientUnits="userSpaceOnUse" x1="845.5" y1="159.5" x2="866.9" y2="156.5"><stop offset="0" stop-color="#73777b"/><stop offset="1" stop-color="#73767c"/></linearGradient>
|
||||
<linearGradient id="g3" gradientUnits="userSpaceOnUse" x1="835.5" y1="229.1" x2="842.3" y2="253.1"><stop offset="0" stop-color="#54575d"/><stop offset="1" stop-color="#5a5d63"/></linearGradient>
|
||||
<linearGradient id="g4" gradientUnits="userSpaceOnUse" x1="738.8" y1="402.4" x2="778.4" y2="373.8"><stop offset="0" stop-color="#4e5357"/><stop offset="1" stop-color="#55595e"/></linearGradient>
|
||||
<linearGradient id="g5" gradientUnits="userSpaceOnUse" x1="502.9" y1="620.5" x2="456.0" y2="520.4"><stop offset="0" stop-color="#52575e"/><stop offset="1" stop-color="#5a6166"/></linearGradient>
|
||||
<linearGradient id="g6" gradientUnits="userSpaceOnUse" x1="854.9" y1="616.3" x2="901.1" y2="622.7"><stop offset="0" stop-color="#33393e"/><stop offset="1" stop-color="#373c42"/></linearGradient>
|
||||
<linearGradient id="g7" gradientUnits="userSpaceOnUse" x1="364.2" y1="664.4" x2="360.7" y2="746.1"><stop offset="0" stop-color="#353b40"/><stop offset="1" stop-color="#40474d"/></linearGradient>
|
||||
<linearGradient id="g8" gradientUnits="userSpaceOnUse" x1="965.4" y1="697.5" x2="906.6" y2="706.0"><stop offset="0" stop-color="#63686c"/><stop offset="1" stop-color="#676c70"/></linearGradient>
|
||||
<linearGradient id="g9" gradientUnits="userSpaceOnUse" x1="894.1" y1="714.4" x2="875.2" y2="729.2"><stop offset="0" stop-color="#43474a"/><stop offset="1" stop-color="#45474b"/></linearGradient>
|
||||
<linearGradient id="g10" gradientUnits="userSpaceOnUse" x1="682.0" y1="67.3" x2="737.5" y2="95.0"><stop offset="0" stop-color="#d5c9bc"/><stop offset="1" stop-color="#dbcdc1"/></linearGradient>
|
||||
<linearGradient id="g11" gradientUnits="userSpaceOnUse" x1="979.3" y1="183.9" x2="958.7" y2="176.5"><stop offset="0" stop-color="#9b9187"/><stop offset="1" stop-color="#b9aea5"/></linearGradient>
|
||||
<linearGradient id="g12" gradientUnits="userSpaceOnUse" x1="924.1" y1="150.5" x2="852.7" y2="205.4"><stop offset="0" stop-color="#c2b6a8"/><stop offset="1" stop-color="#f1e7d9"/></linearGradient>
|
||||
<linearGradient id="g13" gradientUnits="userSpaceOnUse" x1="663.8" y1="225.3" x2="726.2" y2="233.1"><stop offset="0" stop-color="#d4c4b2"/><stop offset="1" stop-color="#dccebd"/></linearGradient>
|
||||
<linearGradient id="g14" gradientUnits="userSpaceOnUse" x1="945.6" y1="245.5" x2="885.2" y2="257.5"><stop offset="0" stop-color="#837b70"/><stop offset="1" stop-color="#b0a292"/></linearGradient>
|
||||
<linearGradient id="g15" gradientUnits="userSpaceOnUse" x1="889.0" y1="403.3" x2="817.3" y2="411.5"><stop offset="0" stop-color="#a1998c"/><stop offset="1" stop-color="#e4d7c8"/></linearGradient>
|
||||
<linearGradient id="g16" gradientUnits="userSpaceOnUse" x1="886.4" y1="506.4" x2="918.1" y2="507.5"><stop offset="0" stop-color="#857f74"/><stop offset="1" stop-color="#8b8279"/></linearGradient>
|
||||
<linearGradient id="g17" gradientUnits="userSpaceOnUse" x1="881.7" y1="569.7" x2="827.2" y2="506.1"><stop offset="0" stop-color="#888175"/><stop offset="1" stop-color="#a49b8d"/></linearGradient>
|
||||
<linearGradient id="g18" gradientUnits="userSpaceOnUse" x1="208.2" y1="559.7" x2="242.4" y2="555.4"><stop offset="0" stop-color="#aa9c8d"/><stop offset="1" stop-color="#b3a693"/></linearGradient>
|
||||
<linearGradient id="g19" gradientUnits="userSpaceOnUse" x1="852.5" y1="644.9" x2="736.7" y2="633.3"><stop offset="0" stop-color="#928a7d"/><stop offset="1" stop-color="#daccbc"/></linearGradient>
|
||||
<linearGradient id="g20" gradientUnits="userSpaceOnUse" x1="224.4" y1="631.6" x2="303.9" y2="662.7"><stop offset="0" stop-color="#8d8376"/><stop offset="1" stop-color="#b0a396"/></linearGradient>
|
||||
<linearGradient id="g21" gradientUnits="userSpaceOnUse" x1="1017.2" y1="708.7" x2="974.7" y2="752.6"><stop offset="0" stop-color="#726b60"/><stop offset="1" stop-color="#867e73"/></linearGradient>
|
||||
<linearGradient id="g22" gradientUnits="userSpaceOnUse" x1="560.7" y1="705.9" x2="600.9" y2="763.2"><stop offset="0" stop-color="#898072"/><stop offset="1" stop-color="#a89b8b"/></linearGradient>
|
||||
<linearGradient id="g23" gradientUnits="userSpaceOnUse" x1="801.0" y1="120.8" x2="796.8" y2="140.0"><stop offset="0" stop-color="#e5e4e4"/><stop offset="1" stop-color="#eff0ef"/></linearGradient>
|
||||
<linearGradient id="g24" gradientUnits="userSpaceOnUse" x1="795.2" y1="187.5" x2="744.9" y2="264.6"><stop offset="0" stop-color="#d6dad6"/><stop offset="1" stop-color="#e7e8e7"/></linearGradient>
|
||||
<linearGradient id="g25" gradientUnits="userSpaceOnUse" x1="444.3" y1="360.6" x2="343.6" y2="248.4"><stop offset="0" stop-color="#eaeaea"/><stop offset="1" stop-color="#f1f1f0"/></linearGradient>
|
||||
<linearGradient id="g26" gradientUnits="userSpaceOnUse" x1="774.0" y1="291.5" x2="756.7" y2="328.6"><stop offset="0" stop-color="#e0e1de"/><stop offset="1" stop-color="#e7e8e5"/></linearGradient>
|
||||
<linearGradient id="g27" gradientUnits="userSpaceOnUse" x1="504.5" y1="428.2" x2="600.5" y2="343.0"><stop offset="0" stop-color="#e6e8e7"/><stop offset="1" stop-color="#e9eae8"/></linearGradient>
|
||||
<linearGradient id="g28" gradientUnits="userSpaceOnUse" x1="270.4" y1="455.9" x2="305.1" y2="499.0"><stop offset="0" stop-color="#e7e8e7"/><stop offset="1" stop-color="#ececeb"/></linearGradient>
|
||||
<linearGradient id="g29" gradientUnits="userSpaceOnUse" x1="780.3" y1="469.6" x2="741.5" y2="534.9"><stop offset="0" stop-color="#dbdfdb"/><stop offset="1" stop-color="#e6e7e4"/></linearGradient>
|
||||
<linearGradient id="g30" gradientUnits="userSpaceOnUse" x1="572.4" y1="460.6" x2="554.8" y2="532.2"><stop offset="0" stop-color="#e1e3e0"/><stop offset="1" stop-color="#eaeae9"/></linearGradient>
|
||||
<linearGradient id="g31" gradientUnits="userSpaceOnUse" x1="373.0" y1="591.4" x2="450.6" y2="588.1"><stop offset="0" stop-color="#e1e4e0"/><stop offset="1" stop-color="#e5e7e4"/></linearGradient>
|
||||
<linearGradient id="g32" gradientUnits="userSpaceOnUse" x1="634.8" y1="522.2" x2="694.0" y2="646.0"><stop offset="0" stop-color="#cfd5d1"/><stop offset="1" stop-color="#d8ddd9"/></linearGradient>
|
||||
<linearGradient id="g33" gradientUnits="userSpaceOnUse" x1="488.1" y1="692.3" x2="494.0" y2="725.6"><stop offset="0" stop-color="#d5dad6"/><stop offset="1" stop-color="#e2e5e2"/></linearGradient>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="130%" height="130%"><feDropShadow dx="3" dy="5" stdDeviation="4" flood-color="#000" flood-opacity="0.18"/></filter>
|
||||
<filter id="brush" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.003 0.7" numOctaves="2" seed="7" result="noise"/><feColorMatrix in="noise" type="saturate" values="0" result="gray"/><feComponentTransfer in="gray" result="grain"><feFuncA type="linear" slope="0.22"/></feComponentTransfer><feComposite in="grain" in2="SourceGraphic" operator="in" result="clipped"/><feBlend in="clipped" in2="SourceGraphic" mode="overlay"/></filter>
|
||||
</defs>
|
||||
<title>Singe dragon</title>
|
||||
<g filter="url(#shadow)">
|
||||
<polygon points="620.3,450.0 420.1,406.2 575.1,277.9 680.5,456.9" fill="url(#g27)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="718.9,692.9 547.2,565.9 642.9,498.6 688.8,474.3 721.0,511.5" fill="url(#g32)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="832.0,437.8 829.7,543.9 705.5,374.0 673.4,303.6 704.9,256.9" fill="url(#g4)" stroke="#aaacae" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="573.2,269.1 519.7,314.7 404.5,263.3 302.7,135.5" fill="url(#g1)" stroke="#c4c5c7" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="281.8,224.7 375.5,258.3 510.4,318.9 414.4,400.8" fill="url(#g25)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="732.1,520.7 894.5,701.6 730.0,691.8" fill="url(#g19)" stroke="#ece5dd" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="412.5,697.6 353.4,656.7 383.4,543.9 473.3,459.7" fill="url(#g31)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="320.8,754.9 288.6,624.8 486.2,759.6" fill="url(#g7)" stroke="#9fa3a6" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="481.8,461.2 539.9,571.7 415.1,701.2" fill="url(#g5)" stroke="#acb0b2" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="785.2,280.5 915.9,436.6 874.0,506.8 815.2,407.5" fill="url(#g15)" stroke="#f1ebe3" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="674.6,467.0 539.5,557.8 484.1,450.4" fill="url(#g30)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="829.1,553.8 825.4,615.9 692.3,464.8 719.1,401.9" fill="url(#g29)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="346.0,386.3 325.7,487.9 266.2,553.8 224.8,479.9" fill="url(#g28)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="266.0,555.8 310.0,745.6 202.7,639.4" fill="url(#g20)" stroke="#d7d1ca" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="772.9,160.4 839.1,206.4 782.1,265.6 730.4,279.6 711.1,253.1" fill="url(#g24)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="481.7,755.7 587.6,673.5 660.8,760.2" fill="url(#g22)" stroke="#d3cdc5" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="656.2,206.4 767.6,153.0 644.6,335.1" fill="url(#g13)" stroke="#ede6de" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="837.1,618.3 834.8,444.4 906.2,561.9" fill="url(#g17)" stroke="#d1cdc6" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="956.0,158.3 956.0,190.3 862.2,209.3 774.5,150.0 819.0,146.0" fill="url(#g12)" stroke="#f8f3ec" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="933.1,751.6 995.8,671.8 1073.3,758.9" fill="url(#g21)" stroke="#c2beb9" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="778.2,149.8 640.3,144.9 552.5,57.2" fill="url(#g0)" stroke="#cbcacc" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="928.4,545.6 882.6,680.7 833.3,626.6" fill="url(#g6)" stroke="#9b9da0" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="421.4,698.7 579.0,670.2 483.0,746.1" fill="url(#g33)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="883.2,680.4 990.9,673.4 926.5,753.0" fill="url(#g8)" stroke="#b3b5b7" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="930.3,539.5 906.2,561.1 871.3,508.2 912.0,435.2" fill="url(#g16)" stroke="#c5c0bc" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="257.8,560.4 201.9,632.0 214.1,482.6" fill="url(#g18)" stroke="#d9d2c9" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="783.1,149.1 712.8,111.3 830.2,116.0 874.7,144.4" fill="url(#g23)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="940.3,291.2 842.3,211.4 966.9,239.2" fill="url(#g14)" stroke="#d7d0c8" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="887.2,248.1 785.9,269.7 841.6,212.7" fill="url(#g3)" stroke="#acaeb1" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="771.8,272.5 797.9,379.7 733.9,288.2" fill="url(#g26)" fill-opacity="0.88" stroke="#c2ccc6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<polygon points="700.8,100.8 653.0,30.8 778.0,107.7" fill="url(#g10)" stroke="#ede6e0" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="826.6,702.0 893.1,702.0 934.9,762.2" fill="url(#g9)" stroke="#a2a3a5" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="954.9,193.0 958.8,135.4 989.6,179.7 970.4,218.2" fill="url(#g11)" stroke="#dcd6d2" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
<polygon points="825.0,149.0 887.9,149.0 854.9,174.1" fill="url(#g2)" stroke="#b9bbbd" stroke-width="1.5" stroke-linejoin="round" filter="url(#brush)"/>
|
||||
</g>
|
||||
</g></svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue