Compare commits
No commits in common. "master" and "ffms-video-player" have entirely different histories.
master
...
ffms-video
32082 changed files with 50619 additions and 9945365 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
|
||||
|
|
|
|||
892
CMakeLists.txt
892
CMakeLists.txt
|
|
@ -1,892 +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 Licens
|
||||
# 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)
|
||||
project(singe2 VERSION 3.00 LANGUAGES C CXX)
|
||||
|
||||
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()
|
||||
|
||||
# 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 licence, 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/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 "")
|
||||
|
||||
# Forge, the authoring tool, is built from assets/Forge but is NOT embedded: it is distributed on
|
||||
# its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime
|
||||
# a game it builds loads -- belongs inside Singe.
|
||||
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.
|
||||
set(MANUAL_DIR ${CMAKE_SOURCE_DIR}/.builddir)
|
||||
add_custom_command(
|
||||
OUTPUT ${MANUAL_DIR}/Manual.pdf
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
|
||||
COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${PROJECT_VERSION} ${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 "")
|
||||
|
||||
# The licence 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 "")
|
||||
|
||||
# Forge's manual, rendered the same way. It is not embedded: the superbuild packs it into
|
||||
# Forge.game (cmake/Superbuild.cmake), which is the one thing that ships Forge.
|
||||
# Its vocabulary tables are generated from the manifest in Author.singe, so the manual cannot
|
||||
# drift from it; the generated file is kept in the tree for a host without a Lua interpreter.
|
||||
find_program(LUA_INTERPRETER NAMES lua5.4 lua)
|
||||
if(LUA_INTERPRETER)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
|
||||
COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_SOURCE_DIR} ${LUA_INTERPRETER} util/forgeVocabulary.lua > ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
|
||||
DEPENDS assets/Forge/Author.singe util/forgeVocabulary.lua
|
||||
COMMENT "Writing the Forge vocabulary"
|
||||
)
|
||||
endif()
|
||||
# The book embeds the tutorials' pictures and includes their shipped descriptions, so any of
|
||||
# those changing renders it again; the globs are re-run on every build.
|
||||
file(GLOB_RECURSE FORGE_BOOK_PARTS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/docs/images/tutorials/* ${CMAKE_SOURCE_DIR}/assets/Forge/tutorials/*.game)
|
||||
add_custom_command(
|
||||
OUTPUT ${MANUAL_DIR}/Forge.pdf
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
|
||||
COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Forge.adoc -o ${MANUAL_DIR}/Forge.pdf
|
||||
DEPENDS docs/Forge.adoc docs/ForgeVocabulary.adoc ${FORGE_BOOK_PARTS}
|
||||
COMMENT "Rendering Forge.pdf"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(forgeManual ALL DEPENDS ${MANUAL_DIR}/Forge.pdf)
|
||||
|
||||
# Optional HTML manual for browsing: cmake --build . --target docs
|
||||
if(ASCIIDOCTOR)
|
||||
add_custom_target(docs
|
||||
COMMAND ${ASCIIDOCTOR} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Manual.adoc -o ${MANUAL_DIR}/Manual.html
|
||||
DEPENDS ${MANUAL_DIR}/Manual.pdf
|
||||
COMMENT "Rendering Manual.html"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
# ===== Sources =====
|
||||
|
||||
set(SINGE_SOURCE
|
||||
src/collide.c
|
||||
src/collide.h
|
||||
src/common.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
|
||||
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/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}\"")
|
||||
|
||||
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 behaviour.
|
||||
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}
|
||||
)
|
||||
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${PROJECT_VERSION}-${osInitial}${osRest}-${KANGAROO_ARCH}")
|
||||
|
||||
# Warnings apply to our code only; the vendored libraries are not ours to fix.
|
||||
set_source_files_properties(${SINGE_SOURCE} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra")
|
||||
# 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
|
||||
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
|
||||
)
|
||||
|
||||
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,-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). Unlike
|
||||
# shadercross this one is not fatal when missing: the header then carries null ES strings, the GLES
|
||||
# backend declines to start, and every other platform builds exactly as before.
|
||||
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()
|
||||
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,119 +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": "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": "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>.
|
||||
145
INSTALL
145
INSTALL
|
|
@ -1,145 +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! SINGE Is Not a Game Emulator!
|
||||
|
||||
|
||||
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 and the menu video in assets/ 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
|
||||
|
||||
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, linux-aarch64,
|
||||
macos-aarch64 (CMakePresets.json).
|
||||
75
LICENSES
75
LICENSES
|
|
@ -1,75 +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
|
||||
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)
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
Data
|
||||
----
|
||||
|
||||
Mozilla CA list 20260601 (Debian ca-certificates) MPL-2.0 https://wiki.mozilla.org/CA (assets/cacerts.pem)
|
||||
|
|
@ -1,626 +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 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.
|
||||
|
||||
-- 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 centred 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,
|
||||
-- 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 = { 3.55, 5.60, 0.00 } -- Where the flame leaves, in model coordinates.
|
||||
local HEAD_AT = { 0.62, 5.69, 0.00 } -- The head's baked joint; see assets/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 REAR_BACK = 3.05 -- Seconds: the head starts to rear.
|
||||
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 = -32.0 -- The dragon alone is turned toward the camera, so the wingbeat is
|
||||
-- seen three quarters on rather than edge on; the name stays square
|
||||
-- to the camera because it has to be read.
|
||||
local TAGLINE = "SINGE Is Not a Game Emulator"
|
||||
|
||||
|
||||
local 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 colours are final.
|
||||
local backdropFont = nil
|
||||
|
||||
-- ===== 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
|
||||
-- assets/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 colour 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.
|
||||
local wingLeft = joint(dragon, "wingL")
|
||||
local wingRite = joint(dragon, "wingR")
|
||||
local neck = joint(dragon, "neck")
|
||||
-- The head hangs off the neck so that rearing the neck carries the head with it.
|
||||
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 colours 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 colour 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 grey 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
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 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)
|
||||
|
||||
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 colours 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 back, snaps forward, and settles; the flame goes with the strike.
|
||||
local pitch = 0
|
||||
|
||||
if 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 * 0.35)
|
||||
|
||||
-- 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.
|
||||
local aim = math.rad(pitch)
|
||||
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.
|
||||
backdropFont = fontLoad("Singe/FreeSansBold.ttf", 30)
|
||||
fontSelect(backdropFont)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
|
||||
-- fontPrint has no centring, 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 colour'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
|
||||
gridLit = false
|
||||
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
|
||||
BIN
assets/Dragon.jpeg
(Stored with Git LFS)
BIN
assets/Dragon.jpeg
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,34 +0,0 @@
|
|||
# Materials for Dragon.obj
|
||||
newmtl dark
|
||||
Ka 0.060 0.062 0.066
|
||||
Kd 0.300 0.310 0.330
|
||||
Ks 0.700 0.700 0.700
|
||||
Ns 180.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.35
|
||||
|
||||
newmtl champagne
|
||||
Ka 0.144 0.128 0.108
|
||||
Kd 0.720 0.640 0.540
|
||||
Ks 0.850 0.800 0.700
|
||||
Ns 120.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.40
|
||||
|
||||
newmtl glass
|
||||
Ka 0.176 0.184 0.180
|
||||
Kd 0.880 0.920 0.900
|
||||
Ks 0.950 0.950 0.950
|
||||
Ns 300.0
|
||||
d 0.55
|
||||
Ni 1.50
|
||||
illum 4
|
||||
Pm 0.00
|
||||
Pr 0.15
|
||||
|
||||
1339
assets/Dragon.obj
1339
assets/Dragon.obj
File diff suppressed because it is too large
Load diff
|
|
@ -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">
|
||||
<!-- 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="grey"/><feComponentTransfer in="grey" 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>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
BIN
assets/DragonModel.glb
(Stored with Git LFS)
BIN
assets/DragonModel.glb
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,34 +0,0 @@
|
|||
# Materials for DragonModel.obj
|
||||
newmtl dark
|
||||
Ka 0.060 0.062 0.066
|
||||
Kd 0.300 0.310 0.330
|
||||
Ks 0.700 0.700 0.700
|
||||
Ns 180.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.35
|
||||
|
||||
newmtl champagne
|
||||
Ka 0.144 0.128 0.108
|
||||
Kd 0.720 0.640 0.540
|
||||
Ks 0.850 0.800 0.700
|
||||
Ns 120.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.40
|
||||
|
||||
newmtl glass
|
||||
Ka 0.176 0.184 0.180
|
||||
Kd 0.880 0.920 0.900
|
||||
Ks 0.950 0.950 0.950
|
||||
Ns 300.0
|
||||
d 0.55
|
||||
Ni 1.50
|
||||
illum 4
|
||||
Pm 0.00
|
||||
Pr 0.15
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"_comment": [
|
||||
"Where each of the dragon's parts turns, for util/objToGlb.py --pivots.",
|
||||
"Each entry is min, mid or max of that part's own bounding box, per axis, or a number.",
|
||||
"The dragon faces +X, +Y is up, and the wings reach out along -Z and +Z.",
|
||||
"Derived from the anatomy in util/dragonModel.py: a joint sits where the part meets the",
|
||||
"one it hangs from, so a wing turns at the shoulder and the head at the top of the neck.",
|
||||
"Without these every part's node sits at the model's origin and rotating one swings it",
|
||||
"around the middle of the animal."
|
||||
],
|
||||
|
||||
"head": ["min", "mid", "mid"],
|
||||
"neck": ["mid", "min", "mid"],
|
||||
"tail": ["max", "mid", "mid"],
|
||||
|
||||
"wingL": ["max", "mid", "max"],
|
||||
"wingR": ["max", "mid", "min"],
|
||||
|
||||
"foreLegL": ["mid", "max", "mid"],
|
||||
"foreLegR": ["mid", "max", "mid"],
|
||||
"hindLegL": ["mid", "max", "mid"],
|
||||
"hindLegR": ["mid", "max", "mid"],
|
||||
|
||||
"hornL": ["mid", "min", "mid"],
|
||||
"hornR": ["mid", "min", "mid"],
|
||||
"crest": ["mid", "min", "mid"]
|
||||
}
|
||||
BIN
assets/DragonPrint-champagne.stl
(Stored with Git LFS)
BIN
assets/DragonPrint-champagne.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/DragonPrint-dark.stl
(Stored with Git LFS)
BIN
assets/DragonPrint-dark.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/DragonPrint-frame.stl
(Stored with Git LFS)
BIN
assets/DragonPrint-frame.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/DragonPrint-glass.stl
(Stored with Git LFS)
BIN
assets/DragonPrint-glass.stl
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,45 +0,0 @@
|
|||
# Materials for DragonPrint.obj
|
||||
newmtl dark
|
||||
Ka 0.060 0.062 0.066
|
||||
Kd 0.300 0.310 0.330
|
||||
Ks 0.700 0.700 0.700
|
||||
Ns 180.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.35
|
||||
|
||||
newmtl champagne
|
||||
Ka 0.144 0.128 0.108
|
||||
Kd 0.720 0.640 0.540
|
||||
Ks 0.850 0.800 0.700
|
||||
Ns 120.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.40
|
||||
|
||||
newmtl glass
|
||||
Ka 0.176 0.184 0.180
|
||||
Kd 0.880 0.920 0.900
|
||||
Ks 0.950 0.950 0.950
|
||||
Ns 300.0
|
||||
d 0.55
|
||||
Ni 1.50
|
||||
illum 4
|
||||
Pm 0.00
|
||||
Pr 0.15
|
||||
|
||||
newmtl frame
|
||||
Ka 0.024 0.022 0.020
|
||||
Kd 0.120 0.110 0.100
|
||||
Ks 0.300 0.300 0.300
|
||||
Ns 40.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.00
|
||||
Pr 0.70
|
||||
|
||||
File diff suppressed because it is too large
Load diff
BIN
assets/DragonRelief.glb
(Stored with Git LFS)
BIN
assets/DragonRelief.glb
(Stored with Git LFS)
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -1,864 +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 compiler: a game description to a Singe game (FORGE.md).
|
||||
--
|
||||
-- A description is data -- types, rooms, and rules -- and what comes out is a Lua script that
|
||||
-- calls the runtime in Author.singe. The rules become real functions: a condition is a Lua
|
||||
-- expression, an action a statement, and a rule with an action that takes time a coroutine. The
|
||||
-- description travels with the game so it can be opened again; nothing is lost in either
|
||||
-- direction.
|
||||
--
|
||||
-- Written in Lua rather than as a util/ script because the editor is itself a Singe game and has to
|
||||
-- compile what it is editing without leaving the engine. It lives with Forge rather than in Singe/
|
||||
-- because only building needs it; Author.singe, the runtime a built game loads, travels with the
|
||||
-- game -- a copy is written beside it when it is built -- so a game handed to someone who has never
|
||||
-- installed Forge still runs.
|
||||
|
||||
-- Where the runtime is taken from when a game is built. Forge carries it; a game gets a copy.
|
||||
AUTHOR_RUNTIME = "Forge/Author.singe"
|
||||
|
||||
dofile(AUTHOR_RUNTIME)
|
||||
|
||||
|
||||
local INDENT = "\t"
|
||||
|
||||
|
||||
-- ===== Expressions ============================================================================
|
||||
--
|
||||
-- A small, safe language where a number goes: numbers, strings, arithmetic, comparison, and, or,
|
||||
-- not, self.var, other.var, event.field, an entity's field by name, the game's vars by name, and a
|
||||
-- few functions. Anything else is a compile error naming the expression, and the lua action is
|
||||
-- the way past it.
|
||||
|
||||
local FUNCTIONS = {
|
||||
count = "authorCount",
|
||||
random = "authorRandom",
|
||||
distance = "authorDistance",
|
||||
has = "authorHas",
|
||||
abs = "math.abs",
|
||||
min = "math.min",
|
||||
max = "math.max",
|
||||
floor = "math.floor",
|
||||
time = "authorTime",
|
||||
frame = "discGetFrame"
|
||||
}
|
||||
|
||||
local NAMES = {
|
||||
self = "self",
|
||||
other = "other",
|
||||
event = "event",
|
||||
time = "authorTime()",
|
||||
frame = "discGetFrame()"
|
||||
}
|
||||
|
||||
local OPERATORS = {
|
||||
["=="] = "==", ["~="] = "~=", ["!="] = "~=", ["<="] = "<=", [">="] = ">=", ["<"] = "<", [">"] = ">",
|
||||
["+"] = "+", ["-"] = "-", ["*"] = "*", ["/"] = "/", ["%"] = "%", ["^"] = "^", [".."] = ".."
|
||||
}
|
||||
|
||||
|
||||
-- The keys of a table in name order, so anything written from one is written the same way each time.
|
||||
local function sortedKeys(t)
|
||||
local keys = {}
|
||||
|
||||
for key in pairs(t or {}) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
|
||||
return keys
|
||||
end
|
||||
|
||||
|
||||
local function tokenize(text)
|
||||
local tokens = {}
|
||||
local at = 1
|
||||
|
||||
while at <= #text do
|
||||
local c = text:sub(at, at)
|
||||
|
||||
if c:match("%s") then
|
||||
at = at + 1
|
||||
elseif c:match("[%d%.]") and text:sub(at):match("^%d") or text:sub(at):match("^%.%d") then
|
||||
local number = text:sub(at):match("^%d*%.?%d+[eE][-+]?%d+") or text:sub(at):match("^%d*%.?%d+") or text:sub(at):match("^%d+")
|
||||
|
||||
tokens[#tokens + 1] = { kind = "number", value = number }
|
||||
at = at + #number
|
||||
elseif c:match("[%a_]") then
|
||||
local name = text:sub(at):match("^[%a_][%w_]*")
|
||||
|
||||
tokens[#tokens + 1] = { kind = "name", value = name }
|
||||
at = at + #name
|
||||
elseif (c == '"') or (c == "'") then
|
||||
-- A backslash escapes the next character, so a line can hold its own quote marks.
|
||||
local value = {}
|
||||
local scan = at + 1
|
||||
|
||||
while (scan <= #text) and (text:sub(scan, scan) ~= c) do
|
||||
local ch = text:sub(scan, scan)
|
||||
|
||||
if ch == "\\" then
|
||||
local escaped = text:sub(scan + 1, scan + 1)
|
||||
|
||||
value[#value + 1] = (escaped == "n") and "\n" or escaped
|
||||
scan = scan + 2
|
||||
else
|
||||
value[#value + 1] = ch
|
||||
scan = scan + 1
|
||||
end
|
||||
end
|
||||
if scan > #text then
|
||||
error("unterminated string in: " .. text)
|
||||
end
|
||||
tokens[#tokens + 1] = { kind = "string", value = table.concat(value) }
|
||||
at = scan + 1
|
||||
else
|
||||
local two = text:sub(at, at + 1)
|
||||
|
||||
if OPERATORS[two] then
|
||||
tokens[#tokens + 1] = { kind = "op", value = two }
|
||||
at = at + 2
|
||||
elseif OPERATORS[c] or c == "(" or c == ")" or c == "," or c == "." then
|
||||
tokens[#tokens + 1] = { kind = "op", value = c }
|
||||
at = at + 1
|
||||
else
|
||||
error("cannot read '" .. c .. "' in: " .. text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return tokens
|
||||
end
|
||||
|
||||
|
||||
-- Recursive descent, lowest precedence first. Every level returns Lua source.
|
||||
local function parser(text)
|
||||
local tokens = tokenize(text)
|
||||
local at = 1
|
||||
local p = {}
|
||||
|
||||
local function peek(kind, value)
|
||||
local token = tokens[at]
|
||||
|
||||
return token and (token.kind == kind) and ((value == nil) or (token.value == value))
|
||||
end
|
||||
|
||||
local function take()
|
||||
at = at + 1
|
||||
return tokens[at - 1]
|
||||
end
|
||||
|
||||
local function expect(kind, value)
|
||||
if not peek(kind, value) then
|
||||
error("expected " .. (value or kind) .. " in: " .. text)
|
||||
end
|
||||
return take()
|
||||
end
|
||||
|
||||
function p.primary()
|
||||
local token = take()
|
||||
|
||||
if token == nil then
|
||||
error("expression ends early: " .. text)
|
||||
end
|
||||
if token.kind == "number" then
|
||||
return token.value
|
||||
elseif token.kind == "string" then
|
||||
return string.format("%q", token.value)
|
||||
elseif token.kind == "op" and token.value == "(" then
|
||||
local inner = p.expression()
|
||||
|
||||
expect("op", ")")
|
||||
return "(" .. inner .. ")"
|
||||
elseif token.kind == "op" and token.value == "-" then
|
||||
return "(-" .. p.unary() .. ")"
|
||||
elseif token.kind == "name" then
|
||||
local name = token.value
|
||||
|
||||
if name == "true" or name == "false" or name == "nil" then
|
||||
return name
|
||||
end
|
||||
if name == "not" then
|
||||
return "(not " .. p.unary() .. ")"
|
||||
end
|
||||
if peek("op", "(") then
|
||||
local target = FUNCTIONS[name]
|
||||
local args = {}
|
||||
|
||||
if target == nil then
|
||||
error("no function called " .. name .. " in: " .. text)
|
||||
end
|
||||
take()
|
||||
if not peek("op", ")") then
|
||||
args[#args + 1] = p.expression()
|
||||
while peek("op", ",") do
|
||||
take()
|
||||
args[#args + 1] = p.expression()
|
||||
end
|
||||
end
|
||||
expect("op", ")")
|
||||
return target .. "(" .. table.concat(args, ", ") .. ")"
|
||||
end
|
||||
if peek("op", ".") then
|
||||
take()
|
||||
local field = expect("name").value
|
||||
|
||||
if name == "event" then
|
||||
return "event[" .. string.format("%q", field) .. "]"
|
||||
elseif NAMES[name] and (name == "self" or name == "other") then
|
||||
return "authorField(" .. name .. ", " .. string.format("%q", field) .. ")"
|
||||
end
|
||||
return "authorField(authorEntity(" .. string.format("%q", name) .. "), " .. string.format("%q", field) .. ")"
|
||||
end
|
||||
if NAMES[name] then
|
||||
return NAMES[name]
|
||||
end
|
||||
return "AUTHOR_VARS[" .. string.format("%q", name) .. "]"
|
||||
end
|
||||
error("unexpected '" .. tostring(token.value) .. "' in: " .. text)
|
||||
end
|
||||
|
||||
function p.unary()
|
||||
return p.primary()
|
||||
end
|
||||
|
||||
function p.power()
|
||||
local left = p.unary()
|
||||
|
||||
while peek("op", "^") do
|
||||
take()
|
||||
left = "(" .. left .. " ^ " .. p.unary() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.product()
|
||||
local left = p.power()
|
||||
|
||||
while peek("op", "*") or peek("op", "/") or peek("op", "%") do
|
||||
local op = take().value
|
||||
|
||||
left = "(" .. left .. " " .. op .. " " .. p.power() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.sum()
|
||||
local left = p.product()
|
||||
|
||||
while peek("op", "+") or peek("op", "-") or peek("op", "..") do
|
||||
local op = take().value
|
||||
|
||||
left = "(" .. left .. " " .. op .. " " .. p.product() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.comparison()
|
||||
local left = p.sum()
|
||||
|
||||
while peek("op") and OPERATORS[tokens[at].value] and tokens[at].value:match("[=<>~!]") do
|
||||
local op = OPERATORS[take().value]
|
||||
|
||||
left = "(" .. left .. " " .. op .. " " .. p.sum() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.conjunction()
|
||||
local left = p.comparison()
|
||||
|
||||
while peek("name", "and") do
|
||||
take()
|
||||
left = "(" .. left .. " and " .. p.comparison() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
function p.expression()
|
||||
local left = p.conjunction()
|
||||
|
||||
while peek("name", "or") do
|
||||
take()
|
||||
left = "(" .. left .. " or " .. p.conjunction() .. ")"
|
||||
end
|
||||
return left
|
||||
end
|
||||
|
||||
local out = p.expression()
|
||||
|
||||
if at <= #tokens then
|
||||
error("unexpected '" .. tostring(tokens[at].value) .. "' in: " .. text)
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
-- An expression as Lua. A number or a boolean given as a value is itself.
|
||||
function authorExpression(value)
|
||||
if type(value) == "number" then
|
||||
return tostring(value)
|
||||
elseif type(value) == "boolean" then
|
||||
return tostring(value)
|
||||
elseif value == nil then
|
||||
return "nil"
|
||||
end
|
||||
|
||||
return parser(tostring(value))
|
||||
end
|
||||
|
||||
|
||||
-- ===== Parameters =============================================================================
|
||||
--
|
||||
-- A parameter's value as a Lua fragment, by the type the manifest declares for it.
|
||||
|
||||
local function fragment(kind, value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
if kind == "number" or kind == "expression" then
|
||||
return authorExpression(value)
|
||||
elseif kind == "boolean" then
|
||||
return value and "true" or "false"
|
||||
elseif kind == "entity" then
|
||||
if value == "self" or value == "other" then
|
||||
return value
|
||||
end
|
||||
return "authorEntity(" .. string.format("%q", value) .. ")"
|
||||
elseif kind == "scancode" then
|
||||
return "SCANCODE." .. value .. ".value"
|
||||
elseif kind == "switch" then
|
||||
return value
|
||||
elseif kind == "lua" then
|
||||
return value
|
||||
end
|
||||
|
||||
return string.format("%q", tostring(value))
|
||||
end
|
||||
|
||||
|
||||
-- A part's parameters as Lua. One the author left empty takes the manifest's default for it,
|
||||
-- and one with neither becomes nil, which the checker has already pointed out.
|
||||
local function fragments(entry, item)
|
||||
local out = {}
|
||||
|
||||
for key, kind in pairs(entry.params or {}) do
|
||||
local value = item[key]
|
||||
|
||||
if (value == nil) and entry.defaults then
|
||||
value = entry.defaults[key]
|
||||
end
|
||||
out[key] = fragment(kind, value) or "nil"
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
-- ===== Rules ==================================================================================
|
||||
|
||||
local function emitOne(set, item, what)
|
||||
local name = item[1]
|
||||
local entry = set[name]
|
||||
|
||||
if entry == nil then
|
||||
debugPrint("Author: no " .. what .. " called '" .. tostring(name) .. "'; skipped")
|
||||
return nil
|
||||
end
|
||||
|
||||
return entry.emit(fragments(entry, item))
|
||||
end
|
||||
|
||||
|
||||
-- A condition list is an AND; an entry named any is an OR of what it holds, and an entry named
|
||||
-- all an AND, nesting as deep as the author likes.
|
||||
local function emitConditions(list)
|
||||
local parts = {}
|
||||
|
||||
for _, item in ipairs(list or {}) do
|
||||
local test = emitOne(AUTHOR.conditions, item, "condition")
|
||||
|
||||
if test ~= nil then
|
||||
parts[#parts + 1] = test
|
||||
end
|
||||
end
|
||||
if list and list.any then
|
||||
local inner = {}
|
||||
|
||||
for _, item in ipairs(list.any) do
|
||||
if item[1] then
|
||||
local test = emitOne(AUTHOR.conditions, item, "condition")
|
||||
|
||||
if test then
|
||||
inner[#inner + 1] = test
|
||||
end
|
||||
else
|
||||
inner[#inner + 1] = emitConditions(item)
|
||||
end
|
||||
end
|
||||
if #inner > 0 then
|
||||
parts[#parts + 1] = "(" .. table.concat(inner, " or ") .. ")"
|
||||
end
|
||||
end
|
||||
if list and list.all then
|
||||
parts[#parts + 1] = emitConditions(list.all)
|
||||
end
|
||||
if #parts == 0 then
|
||||
return "true"
|
||||
end
|
||||
|
||||
return table.concat(parts, " and ")
|
||||
end
|
||||
|
||||
|
||||
local function ruleWaits(rule)
|
||||
for _, item in ipairs(rule.act or {}) do
|
||||
local entry = AUTHOR.actions[item[1]]
|
||||
|
||||
if entry and entry.waits then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function emitRule(out, rule, index)
|
||||
local event = AUTHOR.events[rule.on or "frame"]
|
||||
local filter = {}
|
||||
|
||||
if event == nil then
|
||||
debugPrint("Author: no event called '" .. tostring(rule.on) .. "'; rule " .. index .. " skipped")
|
||||
return
|
||||
end
|
||||
for key, kind in pairs(event.filter) do
|
||||
if rule[key] ~= nil then
|
||||
filter[#filter + 1] = key .. " = " .. fragment(kind, rule[key])
|
||||
end
|
||||
end
|
||||
out[#out + 1] = INDENT .. "{"
|
||||
out[#out + 1] = INDENT .. INDENT .. string.format("note = %q, on = %q,", rule.note or ("rule " .. index), rule.on or "frame")
|
||||
out[#out + 1] = INDENT .. INDENT .. "filter = { " .. table.concat(filter, ", ") .. " },"
|
||||
out[#out + 1] = INDENT .. INDENT .. string.format("each = %s, room = %s, waits = %s, controls = %s, interrupt = %s, guarded = %s,",
|
||||
rule.each and string.format("%q", rule.each) or "nil",
|
||||
rule.room and string.format("%q", rule.room) or "nil",
|
||||
tostring(ruleWaits(rule)),
|
||||
tostring(rule.controls ~= false),
|
||||
tostring(rule.interrupt == true),
|
||||
tostring((rule.when ~= nil) and ((#rule.when > 0) or (rule.when.any ~= nil) or (rule.when.all ~= nil))))
|
||||
out[#out + 1] = INDENT .. INDENT .. "run = function(self, other, event)"
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "if " .. emitConditions(rule.when) .. " then"
|
||||
for _, item in ipairs(rule.act or {}) do
|
||||
local statement = emitOne(AUTHOR.actions, item, "action")
|
||||
|
||||
if statement ~= nil then
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. INDENT .. statement
|
||||
end
|
||||
end
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. INDENT .. "return true"
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "end"
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "return false"
|
||||
out[#out + 1] = INDENT .. INDENT .. "end"
|
||||
out[#out + 1] = INDENT .. "},"
|
||||
end
|
||||
|
||||
|
||||
-- A dialogue: its start and its nodes; a choice's when is an expression and its act a list of
|
||||
-- actions, compiled as a rule's are, with self and other absent.
|
||||
local function emitDialogue(out, name, dialogue)
|
||||
out[#out + 1] = INDENT .. string.format("[%q] = { start = %q, nodes = {", name, dialogue.start or "start")
|
||||
-- Nodes in name order, so the same description compiles to the same text every time.
|
||||
for _, nodeName in ipairs(sortedKeys(dialogue.nodes or {})) do
|
||||
local node = dialogue.nodes[nodeName]
|
||||
|
||||
out[#out + 1] = INDENT .. INDENT .. string.format("[%q] = { name = %q, who = %s, text = %s, seconds = %s, next = %s, choices = {",
|
||||
nodeName, nodeName,
|
||||
node.who and string.format("%q", node.who) or "nil",
|
||||
node.text and string.format("%q", node.text) or "nil",
|
||||
node.seconds and tostring(node.seconds) or "nil",
|
||||
node.next and string.format("%q", node.next) or "nil")
|
||||
for _, choice in ipairs(node.choices or {}) do
|
||||
local parts = { string.format("text = %q", choice.text or "..."), "next = " .. (choice.next and string.format("%q", choice.next) or "nil"), "once = " .. tostring(choice.once == true) }
|
||||
|
||||
if choice.when then
|
||||
parts[#parts + 1] = "when = function() local self, other, event = nil, nil, {} return " .. emitConditions(type(choice.when) == "table" and choice.when or { { "test", expr = choice.when } }) .. " end"
|
||||
end
|
||||
if choice.act then
|
||||
local body = {}
|
||||
|
||||
for _, item in ipairs(choice.act) do
|
||||
local statement = emitOne(AUTHOR.actions, item, "action")
|
||||
|
||||
if statement then
|
||||
body[#body + 1] = statement
|
||||
end
|
||||
end
|
||||
parts[#parts + 1] = "run = function() local self, other, event = nil, nil, {} " .. table.concat(body, " ") .. " end"
|
||||
end
|
||||
out[#out + 1] = INDENT .. INDENT .. INDENT .. "{ " .. table.concat(parts, ", ") .. " },"
|
||||
end
|
||||
out[#out + 1] = INDENT .. INDENT .. "} },"
|
||||
end
|
||||
out[#out + 1] = INDENT .. "} },"
|
||||
end
|
||||
|
||||
|
||||
-- ===== Reading and writing a description ======================================================
|
||||
--
|
||||
-- The editor produces descriptions, so the format has to survive a round trip: load, change
|
||||
-- nothing, save, and the result must compile to the same game.
|
||||
|
||||
local function literal(value)
|
||||
if type(value) == "string" then
|
||||
return string.format("%q", value)
|
||||
end
|
||||
|
||||
return tostring(value)
|
||||
end
|
||||
|
||||
|
||||
local function indentOf(depth)
|
||||
return string.rep(INDENT, depth)
|
||||
end
|
||||
|
||||
|
||||
-- Whether a table holds only leaves, in which case it is written on one line. A condition reads
|
||||
-- far better as { "keyHeld", key = "LEFT" } than as six lines, and it is what an author typed.
|
||||
local function isLeaf(t)
|
||||
for _, value in pairs(t) do
|
||||
if type(value) == "table" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- A value written back as Lua source. A condition or an action is a mixed table -- an array part
|
||||
-- naming it, plus named parameters -- so both parts have to be written or the name survives and
|
||||
-- the parameters do not. The array part keeps its order; the named keys are sorted, because a
|
||||
-- description that reorders itself on every save makes every diff unreadable.
|
||||
local function valueSource(value, depth)
|
||||
local parts = {}
|
||||
local keys = {}
|
||||
local count = 0
|
||||
|
||||
if type(value) ~= "table" then
|
||||
return literal(value)
|
||||
end
|
||||
count = #value
|
||||
for _, item in ipairs(value) do
|
||||
parts[#parts + 1] = valueSource(item, depth + 1)
|
||||
end
|
||||
for key in pairs(value) do
|
||||
-- Skip the array part, which has already been written in order.
|
||||
if not (type(key) == "number" and key >= 1 and key <= count and key == math.floor(key)) then
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
end
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
for _, key in ipairs(keys) do
|
||||
local name = tostring(key)
|
||||
|
||||
if not name:match("^[%a_][%w_]*$") then
|
||||
name = "[" .. literal(key) .. "]"
|
||||
end
|
||||
parts[#parts + 1] = string.format("%s = %s", name, valueSource(value[key], depth + 1))
|
||||
end
|
||||
if #parts == 0 then
|
||||
return "{}"
|
||||
end
|
||||
if isLeaf(value) then
|
||||
return "{ " .. table.concat(parts, ", ") .. " }"
|
||||
end
|
||||
|
||||
return "{\n" .. indentOf(depth + 1) .. table.concat(parts, ",\n" .. indentOf(depth + 1)) .. "\n" .. indentOf(depth) .. "}"
|
||||
end
|
||||
|
||||
|
||||
function authorLoad(path)
|
||||
local chunk, problem = loadfile(path)
|
||||
|
||||
if chunk == nil then
|
||||
debugPrint("Author: cannot read " .. tostring(path) .. ": " .. tostring(problem))
|
||||
return nil
|
||||
end
|
||||
|
||||
return chunk()
|
||||
end
|
||||
|
||||
|
||||
function authorSave(game, path)
|
||||
local file = assert(io.open(path, "w"))
|
||||
local copy = {}
|
||||
|
||||
-- source says where a description came from and is set by the loader, so writing it back out
|
||||
-- would make a description that had been through the editor differ from one that had not.
|
||||
for key in pairs(game) do
|
||||
if key ~= "source" then
|
||||
copy[key] = game[key]
|
||||
end
|
||||
end
|
||||
file:write("-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.\n")
|
||||
file:write("return " .. valueSource(copy, 0) .. "\n")
|
||||
file:close()
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
|
||||
function authorCopy(fromPath, toPath)
|
||||
local input = io.open(fromPath, "rb")
|
||||
local output
|
||||
|
||||
if input == nil then
|
||||
return false
|
||||
end
|
||||
output = io.open(toPath, "wb")
|
||||
if output == nil then
|
||||
input:close()
|
||||
return false
|
||||
end
|
||||
output:write(input:read("a"))
|
||||
input:close()
|
||||
output:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- ===== Checking ===============================================================================
|
||||
--
|
||||
-- What the compiler can tell an author before the game runs: a type that is not declared, a
|
||||
-- condition the manifest does not have, an expression that does not parse. Returns a list of
|
||||
-- messages, empty when the description is sound.
|
||||
|
||||
function authorCheck(game)
|
||||
local problems = {}
|
||||
local types = game.types or {}
|
||||
|
||||
local function problem(text)
|
||||
problems[#problems + 1] = text
|
||||
end
|
||||
|
||||
local function checkItems(set, list, what, where)
|
||||
for _, item in ipairs(list or {}) do
|
||||
local entry = set[item[1]]
|
||||
|
||||
if entry == nil then
|
||||
problem(where .. ": no " .. what .. " called '" .. tostring(item[1]) .. "'")
|
||||
else
|
||||
for key, kind in pairs(entry.params) do
|
||||
if (kind == "number" or kind == "expression") and (type(item[key]) == "string") then
|
||||
local ok, err = pcall(parser, item[key])
|
||||
|
||||
if not ok then
|
||||
problem(where .. ": " .. tostring(err))
|
||||
end
|
||||
elseif (kind == "type") and (item[key] ~= nil) and (types[item[key]] == nil) then
|
||||
problem(where .. ": no type called '" .. tostring(item[key]) .. "'")
|
||||
elseif (item[key] == nil) and (kind ~= "entity") and ((entry.defaults or {})[key] == nil) and not (entry.optional or {})[key] then
|
||||
problem(where .. ": " .. key .. " is empty")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A field the manifest does not declare is a misspelling nine times in ten, and it is
|
||||
-- otherwise silently ignored.
|
||||
local function checkFields(t, declared, skip, where)
|
||||
for key in pairs(t or {}) do
|
||||
if (type(key) == "string") and (declared[key] == nil) and not skip[key] then
|
||||
problem(where .. ": '" .. key .. "' is not something it takes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for name, entityType in pairs(types) do
|
||||
local look = entityType.look and AUTHOR.looks[entityType.look.kind] or nil
|
||||
|
||||
if entityType.look and (look == nil) then
|
||||
problem("type " .. name .. ": no look called '" .. tostring(entityType.look.kind) .. "'")
|
||||
elseif look then
|
||||
checkFields(entityType.look, look.params, { kind = true }, "type " .. name .. "'s look")
|
||||
end
|
||||
for _, b in ipairs(entityType.behaviours or {}) do
|
||||
local behaviour = AUTHOR.behaviours[b.kind]
|
||||
|
||||
if behaviour == nil then
|
||||
problem("type " .. name .. ": no behaviour called '" .. tostring(b.kind) .. "'")
|
||||
else
|
||||
checkFields(b, behaviour.params, { kind = true }, "type " .. name .. "'s " .. b.kind)
|
||||
end
|
||||
end
|
||||
checkFields(entityType, { look = true, vars = true, behaviours = true }, {}, "type " .. name)
|
||||
end
|
||||
if #(game.rooms or {}) == 0 then
|
||||
problem("the game has no rooms")
|
||||
end
|
||||
for _, room in ipairs(game.rooms or {}) do
|
||||
for _, entry in ipairs(room.entities or {}) do
|
||||
if types[entry.type] == nil then
|
||||
problem("room " .. tostring(room.name) .. ": no type called '" .. tostring(entry.type) .. "'")
|
||||
end
|
||||
end
|
||||
-- A waves track's keys name a type, and are at a moment -- or at a stop, when keyed by one.
|
||||
for _, track in ipairs(room.tracks or {}) do
|
||||
for index, key in ipairs(track.spawns or {}) do
|
||||
local where = "room " .. tostring(room.name) .. ", track " .. tostring(track.name) .. ", wave " .. index
|
||||
|
||||
if types[key.type] == nil then
|
||||
problem(where .. ": no type called '" .. tostring(key.type) .. "'")
|
||||
end
|
||||
if (track.key == "stop") and (type(key.at) ~= "string") then
|
||||
problem(where .. ": at should name a stop")
|
||||
elseif (track.key ~= "stop") and (type(key.at) ~= "number") then
|
||||
problem(where .. ": at should be a number")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for index, rule in ipairs(game.rules or {}) do
|
||||
local where = "rule " .. index .. " (" .. tostring(rule.note or "") .. ")"
|
||||
|
||||
if rule.on and (AUTHOR.events[rule.on] == nil) then
|
||||
problem(where .. ": no event called '" .. tostring(rule.on) .. "'")
|
||||
elseif AUTHOR.events[rule.on or "frame"] then
|
||||
checkFields(rule, AUTHOR.events[rule.on or "frame"].filter, { note = true, on = true, each = true, room = true, when = true, act = true, controls = true, interrupt = true }, where)
|
||||
end
|
||||
if rule.each and (types[rule.each] == nil) then
|
||||
problem(where .. ": no type called '" .. tostring(rule.each) .. "'")
|
||||
end
|
||||
checkItems(AUTHOR.conditions, rule.when, "condition", where)
|
||||
checkItems(AUTHOR.conditions, rule.when and rule.when.any, "condition", where)
|
||||
checkItems(AUTHOR.actions, rule.act, "action", where)
|
||||
end
|
||||
|
||||
return problems
|
||||
end
|
||||
|
||||
|
||||
-- ===== The whole game =========================================================================
|
||||
|
||||
-- The whole game, as source. Returns a string; the caller decides where it goes.
|
||||
--
|
||||
-- Every built game loads the runtime sitting beside it. There is no other copy to load: nothing
|
||||
-- of Forge ships inside Singe, so Author.singe travels with the game that needs it. Framework is
|
||||
-- the engine's, as it is for every game ever written for Singe.
|
||||
function authorCompile(game)
|
||||
local out = {}
|
||||
local data = {}
|
||||
|
||||
for key, value in pairs(game) do
|
||||
if (key ~= "rules") and (key ~= "source") and (key ~= "dialogues") then
|
||||
data[key] = value
|
||||
end
|
||||
end
|
||||
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
|
||||
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
||||
out[#out + 1] = "--"
|
||||
out[#out + 1] = "-- This is an ordinary Singe game and can be edited by hand. Doing so and then"
|
||||
out[#out + 1] = "-- recompiling the description will overwrite it, so keep one or the other."
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = 'dofile("Singe/Framework.singe")'
|
||||
-- The game finds its own directory rather than trusting DIR. Framework sets DIR from the
|
||||
-- script the *engine was launched with*, which is this file only when the game is played
|
||||
-- directly; a game reached by dofile -- a test, a launcher, a menu that previews it -- would
|
||||
-- otherwise look for its runtime beside the caller and not find it. debug.getinfo names the
|
||||
-- chunk actually running, either way.
|
||||
out[#out + 1] = 'local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\\\])") or "")'
|
||||
out[#out + 1] = 'dofile(here .. "Author.singe")'
|
||||
out[#out + 1] = "AUTHOR_DIR = here -- Files the description names are looked for here first."
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "-- The description, as the runtime wants it: everything but the rules, which follow as code."
|
||||
out[#out + 1] = "authorBegin(" .. valueSource(data, 0) .. ")"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "-- The rules. Each is a trigger, its filter, and a function that tests the conditions and"
|
||||
out[#out + 1] = "-- does the actions; one with an action that waits runs as a coroutine."
|
||||
out[#out + 1] = "authorRules({"
|
||||
for index, rule in ipairs(game.rules or {}) do
|
||||
emitRule(out, rule, index)
|
||||
end
|
||||
out[#out + 1] = "})"
|
||||
out[#out + 1] = ""
|
||||
if game.dialogues then
|
||||
out[#out + 1] = "-- The dialogues: nodes of a line and choices, each choice's condition and actions as code."
|
||||
out[#out + 1] = "authorDialogues({"
|
||||
for _, name in ipairs(sortedKeys(game.dialogues)) do
|
||||
emitDialogue(out, name, game.dialogues[name])
|
||||
end
|
||||
out[#out + 1] = "})"
|
||||
out[#out + 1] = ""
|
||||
end
|
||||
out[#out + 1] = "onKeyPressed = authorKeyDown"
|
||||
out[#out + 1] = "onKeyReleased = authorKeyUp"
|
||||
out[#out + 1] = "onInputPressed = authorSwitchDown"
|
||||
out[#out + 1] = "onInputReleased = authorSwitchUp"
|
||||
out[#out + 1] = "onMouseMoved = authorMouseMoved"
|
||||
out[#out + 1] = "onCollision = authorCollision"
|
||||
out[#out + 1] = "onTrigger = authorTrigger"
|
||||
out[#out + 1] = "onNavArrived = authorNavArrived"
|
||||
out[#out + 1] = "onMidiMessage = authorMidi"
|
||||
out[#out + 1] = "onSoundCompleted = authorSoundDone"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = "function onOverlayUpdate()"
|
||||
out[#out + 1] = INDENT .. "authorFrame()"
|
||||
out[#out + 1] = ""
|
||||
out[#out + 1] = INDENT .. "return OVERLAY_UPDATED"
|
||||
out[#out + 1] = "end"
|
||||
out[#out + 1] = ""
|
||||
|
||||
return table.concat(out, "\n")
|
||||
end
|
||||
|
||||
|
||||
local function directoryOf(path)
|
||||
return (string.match(path, "^(.*[/\\])") or "")
|
||||
end
|
||||
|
||||
|
||||
-- Compiles a description file to a game beside it, with the runtime beside that, because that is
|
||||
-- what the result loads. Returns the path written.
|
||||
function authorBuild(descriptionFile, outputFile)
|
||||
local chunk = assert(loadfile(descriptionFile))
|
||||
local game = chunk()
|
||||
local file
|
||||
|
||||
game.source = descriptionFile
|
||||
for _, problem in ipairs(authorCheck(game)) do
|
||||
debugPrint("Author: " .. problem)
|
||||
end
|
||||
file = assert(io.open(outputFile, "w"))
|
||||
file:write(authorCompile(game))
|
||||
file:close()
|
||||
authorCopy(AUTHOR_RUNTIME, directoryOf(outputFile) .. "Author.singe")
|
||||
|
||||
return outputFile
|
||||
end
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<rml>
|
||||
<!--
|
||||
Singe 3
|
||||
Copyright (C) 2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
|
||||
Forge's chrome. Only the panels are RmlUi; the canvas beside them is drawn
|
||||
into the overlay, because that is where Singe already tracks a pointer against rectangles.
|
||||
pointer-events keeps the canvas clickable through the document.
|
||||
Licensed under the GNU General Public License, version 3 or later.
|
||||
-->
|
||||
<head>
|
||||
<title>forge</title>
|
||||
<link type="text/rcss" href="Singe/gui.rcss"/>
|
||||
<style>
|
||||
/* The body is the whole overlay, so the panel's 100% is a height: without it the panel
|
||||
was nothing tall, everything in it overflow, and overflow: auto on it hid it all. */
|
||||
body { pointer-events: none; width: 100%; height: 100%; font-family: FreeSans; font-size: 13px; color: #e6e6ee; }
|
||||
/* Rows stay focusable: RmlUi delivers a click to the nearest focusable element under the
|
||||
pointer, so a row that could not take focus could not be clicked either. ENTER and SPACE
|
||||
on a focused element only become a click when its tab-index is auto, and no row's is. */
|
||||
/* The panel is a column: the grip and the title, then the list and the fields box, which
|
||||
share what is left and each scroll (the wheel, or the bar) rather than pushing the other
|
||||
off the screen. */
|
||||
#panel { pointer-events: auto; position: absolute; left: 0px; top: 0px; width: 190px; height: 100%; background-color: #161620ff; display: flex; flex-direction: column; }
|
||||
#grip, #title { flex: 0 0 auto; }
|
||||
#list { flex: 0 1 auto; min-height: 0px; overflow-y: auto; }
|
||||
scrollbarvertical { width: 5px; }
|
||||
scrollbarvertical slidertrack { background-color: #1b1b28ff; }
|
||||
scrollbarvertical sliderbar { background-color: #6a5a9aff; }
|
||||
#grip { pointer-events: auto; background-color: #2a2a3cff; padding: 6px 10px; color: #ffcf4a; font-size: 15px; }
|
||||
#grip:hover { background-color: #3a3a52ff; }
|
||||
#title { margin: 0px 10px 8px 10px; color: #9aa0b4; }
|
||||
.row { margin: 2px 8px; padding: 4px 6px; background-color: #242433ff; }
|
||||
.row:hover { background-color: #32324aff; }
|
||||
.row.selected { background-color: #c03c96ff; color: #ffffff; }
|
||||
.thumb { display: inline-block; vertical-align: middle; margin-right: 6px; width: 24px; text-align: center; color: #ffcf4a; }
|
||||
.thumb.dim { color: #6a6a80; }
|
||||
.thumb.swatch { height: 16px; width: 16px; margin-left: 4px; margin-right: 10px; }
|
||||
.part { margin: 1px 8px 1px 18px; padding: 2px 5px; background-color: #1b1b28ff; color: #aeb4c8; font-size: 12px; }
|
||||
.part:hover { background-color: #2a2a40ff; color: #e6e6ee; }
|
||||
.picked { background-color: #6a5a9aff; color: #ffffff; }
|
||||
#title:hover { color: #ffcf4a; }
|
||||
.part.here { background-color: #4a3a6aff; color: #ffffff; }
|
||||
/* A field's list, unfolded under its row and floating over the rows beneath, as a dropdown
|
||||
does: absolute with no top or left takes the place it would have had in the flow. */
|
||||
.drop { position: absolute; z-index: 2; clip: none; width: 140px; margin: 0px 0px 0px 26px; padding: 2px 0px; background-color: #0b0b12ff; border-width: 1px; border-color: #6a5a9aff; }
|
||||
.choice { margin: 0px; padding: 2px 8px; color: #aeb4c8; font-size: 12px; }
|
||||
.choice:hover { background-color: #32324aff; color: #e6e6ee; }
|
||||
.choice.selected { background-color: #c03c96ff; color: #ffffff; }
|
||||
.choice.more { color: #6a6a80; }
|
||||
.choice.more:hover { background-color: #0b0b12ff; color: #6a6a80; }
|
||||
#detail { flex: 0 1 auto; min-height: 0px; overflow-y: auto; margin: 14px 8px; padding: 6px; background-color: #10101aff; color: #b9bfd4; }
|
||||
</style>
|
||||
</head>
|
||||
<body id="forge">
|
||||
<div id="panel">
|
||||
<div id="grip">≡ Entities</div>
|
||||
<div id="title"></div>
|
||||
<div id="list"></div>
|
||||
<div id="detail"></div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
|||
-- Forge appears in the menu like anything else, so it is reached the same way a game is and packs
|
||||
-- to Forge.game with -P. It needs no disc of its own; the menu background stands in, because the
|
||||
-- engine wants a video and Forge draws over all of it anyway.
|
||||
GAMES = {
|
||||
{
|
||||
TITLE = "Forge",
|
||||
SCRIPT = "Forge/Forge.singe",
|
||||
LEGACY_SPRITE_ARGS = false,
|
||||
VIDEO = "Singe/menuBackground.mkv",
|
||||
STRETCH = false,
|
||||
NO_MOUSE = false,
|
||||
RESOLUTION_X = 720,
|
||||
RESOLUTION_Y = 480,
|
||||
SINDEN_GUN = "",
|
||||
ATTRACT = "Singe/menuBackground.mkv",
|
||||
ATTRACT_START = 180,
|
||||
ATTRACT_END = 359,
|
||||
YEAR = 2026,
|
||||
PLATFORM = "Singe",
|
||||
DEVELOPER = "Kangaroo Punch Studios",
|
||||
PUBLISHER = "Kangaroo Punch Studios",
|
||||
GENRE = "Tool",
|
||||
DESCRIPTION = "Build a game by describing it: place things, write rules, press play. Forge writes ordinary Singe Lua, so nothing it makes is locked inside it."
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
The Forge tutorial kit.
|
||||
|
||||
Everything here except fox.glb was drawn or synthesised by util/forgeKit.py in the
|
||||
Singe source tree and is released under CC0: use it in your own games as you like.
|
||||
|
||||
fox.glb is the Fox from the Khronos glTF sample models, CC-BY 4.0: model by PixelMannen,
|
||||
rigging and animation by tomkranis, glTF conversion by AsoboStudio and scurest.
|
||||
A game that ships it should carry this credit.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
The Forge tutorial kit, addressed from a game as Forge/kit/<name>.
|
||||
|
||||
hero.png a sprite sheet, 8 columns of 32 x 48, facing right: idle, walk x4, jump, fall, hit
|
||||
enemy.png a sheet, 4 columns of 32 x 32, flapping
|
||||
tiles.png a tile sheet, 4 columns of 32 x 32: grass, dirt, stone, brick
|
||||
coin.png key.png crate.png door.png spike.png ship.png shot.png rock.png target.png stills
|
||||
yard.png hall.png painted rooms, 720 x 480, for the adventures
|
||||
jump.wav coin.wav shot.wav hit.wav click.wav door.wav clips
|
||||
loop.ogg eight seconds of music that loops
|
||||
clip.mkv twelve seconds of 720 x 480 video at 24 frames a second: a door opens at 3 s, a target rises at 6 s, a hand reaches at 9 s
|
||||
fox.glb an animated model (see LICENSE)
|
||||
BIN
assets/Forge/kit/click.wav
(Stored with Git LFS)
BIN
assets/Forge/kit/click.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/clip.mkv
(Stored with Git LFS)
BIN
assets/Forge/kit/clip.mkv
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/coin.png
(Stored with Git LFS)
BIN
assets/Forge/kit/coin.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/coin.wav
(Stored with Git LFS)
BIN
assets/Forge/kit/coin.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/crate.png
(Stored with Git LFS)
BIN
assets/Forge/kit/crate.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/door.png
(Stored with Git LFS)
BIN
assets/Forge/kit/door.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/door.wav
(Stored with Git LFS)
BIN
assets/Forge/kit/door.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/enemy.png
(Stored with Git LFS)
BIN
assets/Forge/kit/enemy.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/fox.glb
(Stored with Git LFS)
BIN
assets/Forge/kit/fox.glb
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/hall.png
(Stored with Git LFS)
BIN
assets/Forge/kit/hall.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/hero.png
(Stored with Git LFS)
BIN
assets/Forge/kit/hero.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/hit.wav
(Stored with Git LFS)
BIN
assets/Forge/kit/hit.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/jump.wav
(Stored with Git LFS)
BIN
assets/Forge/kit/jump.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/key.png
(Stored with Git LFS)
BIN
assets/Forge/kit/key.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/loop.ogg
(Stored with Git LFS)
BIN
assets/Forge/kit/loop.ogg
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/rock.png
(Stored with Git LFS)
BIN
assets/Forge/kit/rock.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/ship.png
(Stored with Git LFS)
BIN
assets/Forge/kit/ship.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/shot.png
(Stored with Git LFS)
BIN
assets/Forge/kit/shot.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/shot.wav
(Stored with Git LFS)
BIN
assets/Forge/kit/shot.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/spike.png
(Stored with Git LFS)
BIN
assets/Forge/kit/spike.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/target.png
(Stored with Git LFS)
BIN
assets/Forge/kit/target.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/tiles.png
(Stored with Git LFS)
BIN
assets/Forge/kit/tiles.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Forge/kit/yard.png
(Stored with Git LFS)
BIN
assets/Forge/kit/yard.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,76 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
layers = {
|
||||
{ gravity = 1500, kind = "world2d" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "hero", type = "hero", x = 200, y = 420 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = -1, entity = "hero" }
|
||||
},
|
||||
note = "Run left",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "LEFT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = 1, entity = "hero" }
|
||||
},
|
||||
note = "Run right",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "RIGHT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "jump", entity = "hero" }
|
||||
},
|
||||
note = "Jump, with ground underfoot",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "SPACE" },
|
||||
{ "onGround", entity = "hero" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
}
|
||||
},
|
||||
title = "New game",
|
||||
types = {
|
||||
ground = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 90, g = 70, h = 40, kind = "box", r = 60, w = 720 }
|
||||
},
|
||||
hero = {
|
||||
behaviours = {
|
||||
{ jump = 620, kind = "platformer", speed = 260 }
|
||||
},
|
||||
look = { anchor = "feet", b = 240, g = 90, h = 44, kind = "box", r = 90, w = 24 }
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
}
|
||||
},
|
||||
vars = { score = 0 }
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
layers = {
|
||||
{ gravity = 1500, kind = "world2d" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "hero", type = "hero", x = 200, y = 420 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "coin", type = "coin", x = 300, y = 392 },
|
||||
{ id = "coin1", type = "coin", x = 420, y = 392 },
|
||||
{ id = "coin2", type = "coin", x = 560, y = 300 },
|
||||
{ id = "spike", type = "spike", x = 640, y = 420 },
|
||||
{ id = "door", type = "door", x = 700, y = 420 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
},
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "coin", type = "coin", x = 400, y = 392 }
|
||||
},
|
||||
name = "cave",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = -1, entity = "hero" }
|
||||
},
|
||||
note = "Run left",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "LEFT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = 1, entity = "hero" }
|
||||
},
|
||||
note = "Run right",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "RIGHT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "jump", entity = "hero" }
|
||||
},
|
||||
note = "Jump, with ground underfoot",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "SPACE" },
|
||||
{ "onGround", entity = "hero" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
a = "coin",
|
||||
act = {
|
||||
{ "addScore", amount = 10 },
|
||||
{ "destroy", entity = "self" },
|
||||
{ "playSound", file = "Forge/kit/coin.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Take a coin",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "spike",
|
||||
act = {
|
||||
{ "moveTo", entity = "hero", x = 200, y = 380 },
|
||||
{ "playSound", file = "Forge/kit/hit.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Spikes hurt",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "door",
|
||||
act = {
|
||||
{ "goTo", entity = "hero", room = "cave", x = 60, y = 380 }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Through the door",
|
||||
on = "enter",
|
||||
when = {}
|
||||
}
|
||||
},
|
||||
title = "New game",
|
||||
types = {
|
||||
coin = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { file = "Forge/kit/coin.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
door = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/door.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
ground = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 90, g = 70, h = 40, kind = "box", r = 60, w = 720 }
|
||||
},
|
||||
hero = {
|
||||
behaviours = {
|
||||
{ jump = 620, kind = "platformer", speed = 260 }
|
||||
},
|
||||
look = { anchor = "feet", b = 240, g = 90, h = 44, kind = "box", r = 90, w = 24 }
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
spike = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/spike.png", kind = "sprite" },
|
||||
vars = {}
|
||||
}
|
||||
},
|
||||
vars = { score = 0 }
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
layers = {
|
||||
{ gravity = 1500, kind = "world2d" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "hero", type = "hero", x = 200, y = 420 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "coin", type = "coin", x = 300, y = 392 },
|
||||
{ id = "coin1", type = "coin", x = 420, y = 392 },
|
||||
{ id = "coin2", type = "coin", x = 560, y = 300 },
|
||||
{ id = "spike", type = "spike", x = 640, y = 420 },
|
||||
{ id = "door", type = "door", x = 700, y = 420 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
},
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "coin", type = "coin", x = 400, y = 392 }
|
||||
},
|
||||
name = "cave",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = -1, entity = "hero" }
|
||||
},
|
||||
note = "Run left",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "LEFT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = 1, entity = "hero" }
|
||||
},
|
||||
note = "Run right",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "RIGHT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "jump", entity = "hero" },
|
||||
{ "playSound", file = "Forge/kit/jump.wav" }
|
||||
},
|
||||
note = "Jump, with ground underfoot",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "onGround", entity = "hero" },
|
||||
{ "keyPressed", key = "SPACE" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
a = "coin",
|
||||
act = {
|
||||
{ "addScore", amount = 10 },
|
||||
{ "destroy", entity = "self" },
|
||||
{ "playSound", file = "Forge/kit/coin.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Take a coin",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "spike",
|
||||
act = {
|
||||
{ "moveTo", entity = "hero", x = 200, y = 380 },
|
||||
{ "playSound", file = "Forge/kit/hit.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Spikes hurt",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "door",
|
||||
act = {
|
||||
{ "goTo", entity = "hero", room = "cave", x = 60, y = 380 }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Through the door",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "playMusic", file = "Forge/kit/loop.ogg" }
|
||||
},
|
||||
note = "Music",
|
||||
on = "roomStart",
|
||||
when = {}
|
||||
}
|
||||
},
|
||||
title = "New game",
|
||||
types = {
|
||||
coin = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" },
|
||||
{ kind = "spin", rate = 180 }
|
||||
},
|
||||
look = { file = "Forge/kit/coin.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
door = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/door.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
ground = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 90, g = 70, h = 40, kind = "box", r = 60, w = 720 }
|
||||
},
|
||||
hero = {
|
||||
behaviours = {
|
||||
{ jump = 620, kind = "platformer", speed = 260 },
|
||||
{ fps = 10, kind = "frames", states = "idle=1-1, walk=2-5, jump=6-6, fall=7-7" }
|
||||
},
|
||||
look = { file = "Forge/kit/hero.png", frames = 8, kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
spike = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/spike.png", kind = "sprite" },
|
||||
vars = {}
|
||||
}
|
||||
},
|
||||
vars = { score = 0 }
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
types = {
|
||||
hive = {
|
||||
behaviours = {
|
||||
{ every = 0.6, kind = "spawner", max = 6, spawns = "rock", total = 12 }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
rock = {
|
||||
behaviours = {
|
||||
{ kind = "drift", vy = 90 },
|
||||
{ kind = "health", max = 2 },
|
||||
{ death = "Forge/kit/hit.wav", kind = "sound" }
|
||||
},
|
||||
look = { file = "Forge/kit/rock.png", kind = "sprite" },
|
||||
vars = { health = 2 }
|
||||
},
|
||||
ship = {
|
||||
behaviours = {
|
||||
{ down = "DOWN", fire = "SPACE", kind = "keys", left = "LEFT", right = "RIGHT", up = "UP" },
|
||||
{ clamp = true, kind = "mover", speed = 260 },
|
||||
{ kind = "shooter", offsetY = -14, rate = 0.15, spawns = "shot" },
|
||||
{ keep = true, kind = "health", max = 1 },
|
||||
{ hit = "Forge/kit/hit.wav", kind = "sound" }
|
||||
},
|
||||
look = { file = "Forge/kit/ship.png", kind = "sprite" },
|
||||
vars = { health = 1 }
|
||||
},
|
||||
shot = {
|
||||
behaviours = {
|
||||
{ kind = "projectile", life = 3, vy = -520 },
|
||||
{ kind = "sound", spawn = "Forge/kit/shot.wav" }
|
||||
},
|
||||
look = { file = "Forge/kit/shot.png", kind = "sprite" },
|
||||
vars = {}
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
{ kind = "overlay" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "ship", type = "ship", x = 360, y = 420 },
|
||||
{ id = "hive", type = "hive", x = 360, y = 20 },
|
||||
{ id = "hive1", type = "hive", x = 560, y = 20 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score .. \" lives \" .. lives" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
a = "shot",
|
||||
act = {
|
||||
{ "destroy", entity = "self" },
|
||||
{ "damage", amount = 1, entity = "other" },
|
||||
{ "addScore", amount = 10 }
|
||||
},
|
||||
b = "rock",
|
||||
note = "A shot lands on a rock",
|
||||
on = "collision",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "emit", b = 60, count = 24, entity = "self", g = 120, r = 255 },
|
||||
{ "addScore", amount = 50 }
|
||||
},
|
||||
type = "rock",
|
||||
note = "A rock dies",
|
||||
on = "death",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "rock",
|
||||
act = {
|
||||
{ "destroy", entity = "self" },
|
||||
{ "damage", amount = 1, entity = "other" }
|
||||
},
|
||||
b = "ship",
|
||||
note = "A rock rams the ship",
|
||||
on = "collision",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addVar", amount = -1, name = "lives" },
|
||||
{ "flash", b = 60, g = 60, r = 255, seconds = 0.3 },
|
||||
{ "setVar", entity = "self", name = "health", value = "1" },
|
||||
{ "moveTo", entity = "self", x = 360, y = 420 }
|
||||
},
|
||||
type = "ship",
|
||||
note = "The ship is lost",
|
||||
on = "death",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "gameOver" }
|
||||
},
|
||||
note = "Out of lives",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "lives <= 0" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "destroy", entity = "self" }
|
||||
},
|
||||
each = "rock",
|
||||
note = "A rock slips past the bottom",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "self.y > 500" }
|
||||
}
|
||||
}
|
||||
},
|
||||
title = "Rocks",
|
||||
vars = { lives = 3, score = 0 }
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
types = {
|
||||
prompt = {
|
||||
behaviours = {},
|
||||
look = { b = 80, g = 220, kind = "text", r = 255 },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
verdict = {
|
||||
behaviours = {},
|
||||
look = { b = 180, g = 255, kind = "text", r = 160 },
|
||||
vars = {}
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
{ file = "Forge/kit/clip.mkv", kind = "disc" },
|
||||
{ kind = "overlay" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "prompt", type = "prompt", x = 250, y = 60 },
|
||||
{ id = "verdict", type = "verdict", x = 250, y = 100 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score .. \" lives \" .. lives" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "prompt", text = "\"PRESS!\"" }
|
||||
},
|
||||
note = "Window one is open",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "discBetween", from = 72, to = 120 }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVar", name = "hitone", value = true },
|
||||
{ "addScore", amount = 250 },
|
||||
{ "setText", entity = "verdict", text = "\"HIT\"" }
|
||||
},
|
||||
note = "Window one answered in time",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "discBetween", from = 72, to = 120 },
|
||||
{ "keyPressed", key = "SPACE" },
|
||||
{ "once", tag = "one" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addVar", amount = -1, name = "lives" },
|
||||
{ "setText", entity = "verdict", text = "\"MISS\"" },
|
||||
{ "setText", entity = "prompt", text = "\"\"" }
|
||||
},
|
||||
frame = 120,
|
||||
note = "Window one closed unanswered",
|
||||
on = "frameReached",
|
||||
when = {
|
||||
{ "test", expr = "not hitone" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "prompt", text = "\"UP!\"" }
|
||||
},
|
||||
note = "Window two is open",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "discBetween", from = 150, to = 200 }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVar", name = "hittwo", value = true },
|
||||
{ "addScore", amount = 250 },
|
||||
{ "setText", entity = "verdict", text = "\"HIT\"" }
|
||||
},
|
||||
note = "Window two answered in time",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "discBetween", from = 150, to = 200 },
|
||||
{ "keyPressed", key = "UP" },
|
||||
{ "once", tag = "two" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addVar", amount = -1, name = "lives" },
|
||||
{ "setText", entity = "verdict", text = "\"MISS\"" },
|
||||
{ "setText", entity = "prompt", text = "\"\"" }
|
||||
},
|
||||
frame = 200,
|
||||
note = "Window two closed unanswered",
|
||||
on = "frameReached",
|
||||
when = {
|
||||
{ "test", expr = "not hittwo" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVar", name = "hitone", value = false },
|
||||
{ "setVar", name = "hittwo", value = false },
|
||||
{ "setText", entity = "verdict", text = "\"\"" },
|
||||
{ "discTo", frame = 1 }
|
||||
},
|
||||
frame = 280,
|
||||
note = "The clip runs out: round again",
|
||||
on = "frameReached",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "gameOver" }
|
||||
},
|
||||
note = "Out of lives",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "lives <= 0" }
|
||||
}
|
||||
}
|
||||
},
|
||||
title = "Moments",
|
||||
vars = { lives = 3, score = 0 }
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
types = {
|
||||
gun = {
|
||||
behaviours = {
|
||||
{ ammo = 6, kind = "gun", player = 1, reload = "offscreen" }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = { ammo = 6 }
|
||||
},
|
||||
hand = {
|
||||
behaviours = {
|
||||
{ kind = "hitbox", track = "hand" }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
target = {
|
||||
behaviours = {
|
||||
{ kind = "hitbox", track = "target" },
|
||||
{ kind = "health", max = 1 }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = { health = 1 }
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
{ file = "Forge/kit/clip.mkv", kind = "disc" },
|
||||
{ kind = "overlay" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "gun", type = "gun", x = 30, y = 450 },
|
||||
{ id = "target", type = "target", x = 450, y = 210 },
|
||||
{ id = "hand", type = "hand", x = 600, y = 210 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {
|
||||
{
|
||||
boxes = {
|
||||
{ at = 150, h = 90, w = 90, x = 405, y = 165 },
|
||||
{ at = 210, h = 90, w = 90, x = 405, y = 165 }
|
||||
},
|
||||
key = "frame",
|
||||
name = "target"
|
||||
},
|
||||
{
|
||||
boxes = {
|
||||
{ at = 220, h = 105, w = 240, x = 480, y = 157 },
|
||||
{ at = 260, h = 105, w = 240, x = 480, y = 157 }
|
||||
},
|
||||
key = "frame",
|
||||
name = "hand"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score .. \" ammo \" .. gun.ammo .. \" misses \" .. misses" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addScore", amount = 100 },
|
||||
{ "damage", amount = 1, entity = "self" },
|
||||
{ "playSound", file = "Forge/kit/hit.wav" }
|
||||
},
|
||||
type = "target",
|
||||
note = "The target is hit",
|
||||
on = "hit",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addScore", amount = -200 },
|
||||
{ "flash", b = 40, g = 40, r = 255, seconds = 0.3 }
|
||||
},
|
||||
type = "hand",
|
||||
note = "The hand is hit",
|
||||
on = "hit",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addVar", amount = 1, name = "misses" },
|
||||
{ "playSound", file = "Forge/kit/shot.wav" }
|
||||
},
|
||||
note = "A shot at nothing",
|
||||
on = "miss",
|
||||
when = {
|
||||
{ "test", expr = "not event.offscreen" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "say", seconds = 1, text = "\"DRAW!\"" }
|
||||
},
|
||||
frame = 150,
|
||||
note = "Draw!",
|
||||
on = "frameReached",
|
||||
when = {}
|
||||
}
|
||||
},
|
||||
title = "Draw",
|
||||
vars = { misses = 0, score = 0 }
|
||||
}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
dialogues = {
|
||||
guard = {
|
||||
nodes = {
|
||||
ok = {
|
||||
choices = {},
|
||||
text = "Then go ahead.",
|
||||
who = "Guard"
|
||||
},
|
||||
start = {
|
||||
choices = {
|
||||
{ next = "why", text = "Why not?" },
|
||||
{ text = "Fine." }
|
||||
},
|
||||
text = "Nobody passes.",
|
||||
who = "Guard"
|
||||
},
|
||||
why = {
|
||||
choices = {
|
||||
{
|
||||
act = {
|
||||
{ "setVar", name = "allowed", value = true }
|
||||
},
|
||||
next = "ok",
|
||||
text = "I have a key.",
|
||||
when = "has(\"key\")"
|
||||
},
|
||||
{ text = "I see." }
|
||||
},
|
||||
text = "The door is locked, and I have no key.",
|
||||
who = "Guard"
|
||||
}
|
||||
},
|
||||
start = "start"
|
||||
}
|
||||
},
|
||||
types = {
|
||||
backdrop = {
|
||||
behaviours = {},
|
||||
look = { file = "Forge/kit/yard.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
door = {
|
||||
behaviours = {
|
||||
{ kind = "hotspot", name = "door", walkX = 590, walkY = 330 }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/door.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
guard = {
|
||||
behaviours = {
|
||||
{ kind = "hotspot", name = "guard", walkX = 480, walkY = 360 }
|
||||
},
|
||||
look = { anchor = "feet", faces = "left", file = "Forge/kit/hero.png", frames = 8, kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
hero = {
|
||||
behaviours = {
|
||||
{ kind = "walker", speed = 160 },
|
||||
{ fps = 10, kind = "frames", states = "idle=1-1, walk=2-5" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/hero.png", frames = 8, kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
key = {
|
||||
behaviours = {
|
||||
{ kind = "hotspot", name = "key", walkX = 200, walkY = 400 }
|
||||
},
|
||||
look = { file = "Forge/kit/key.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
{ kind = "overlay" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
depthSort = true,
|
||||
entities = {
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "backdrop", type = "backdrop", x = 360, y = 240 },
|
||||
{ id = "hero", type = "hero", x = 150, y = 440 },
|
||||
{ id = "door", type = "door", x = 590, y = 300 },
|
||||
{ id = "key", type = "key", x = 200, y = 380 },
|
||||
{ id = "guard", type = "guard", x = 520, y = 340 }
|
||||
},
|
||||
name = "yard",
|
||||
scaleBy = {
|
||||
{ scale = 0.6, y = 300 },
|
||||
{ scale = 1.0, y = 460 }
|
||||
},
|
||||
tracks = {},
|
||||
walk = {
|
||||
{ 60, 320, 660, 320, 700, 460, 40, 460 }
|
||||
}
|
||||
},
|
||||
{
|
||||
depthSort = true,
|
||||
entities = {
|
||||
{ id = "backdrop", type = "backdrop", x = 360, y = 240 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 }
|
||||
},
|
||||
name = "hall",
|
||||
tracks = {},
|
||||
walk = {
|
||||
{ 40, 300, 680, 300, 700, 460, 40, 460 }
|
||||
}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "(sentence or \"\") .. \" score \" .. score" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "walkToPointer", entity = "hero", player = 1 }
|
||||
},
|
||||
interrupt = true,
|
||||
note = "A click on the floor walks there",
|
||||
on = "pressed",
|
||||
switch = "SWITCH_BUTTON3",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "say", seconds = 1, text = "\"It is a \" .. event.target .. \".\"" }
|
||||
},
|
||||
note = "Look at anything",
|
||||
on = "verb",
|
||||
verb = "look",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "walkToHotspot", entity = "hero", target = "self" },
|
||||
{ "give", item = "key" },
|
||||
{ "addScore", amount = 1 },
|
||||
{ "destroy", entity = "self" }
|
||||
},
|
||||
note = "Take the key",
|
||||
on = "verb",
|
||||
target = "key",
|
||||
verb = "take",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "walkToHotspot", entity = "hero", target = "self" },
|
||||
{ "face", entity = "hero", target = "self" },
|
||||
{ "talk", dialogue = "guard" }
|
||||
},
|
||||
note = "Talk to the guard",
|
||||
on = "verb",
|
||||
target = "guard",
|
||||
verb = "talk",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "say", seconds = 1, text = "\"The guard shakes his head.\"" }
|
||||
},
|
||||
note = "The door, while the guard objects",
|
||||
on = "verb",
|
||||
target = "door",
|
||||
verb = "open",
|
||||
when = {
|
||||
{ "test", expr = "not allowed" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "walkToHotspot", entity = "hero", target = "self" },
|
||||
{ "playSound", file = "Forge/kit/door.wav" },
|
||||
{ "fade", out = true, seconds = 0.5 },
|
||||
{ "goTo", entity = "hero", room = "hall", x = 120, y = 420 },
|
||||
{ "fade", out = false, seconds = 0.5 },
|
||||
{ "addScore", amount = 1 }
|
||||
},
|
||||
controls = false,
|
||||
note = "The door opens",
|
||||
on = "verb",
|
||||
target = "door",
|
||||
verb = "open",
|
||||
when = {
|
||||
{ "test", expr = "allowed" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "say", seconds = 1, text = "\"That does not work.\"" }
|
||||
},
|
||||
note = "Anything else",
|
||||
on = "verb",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVerb", verb = "look" }
|
||||
},
|
||||
key = "L",
|
||||
note = "The verb keys",
|
||||
on = "pressed",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVerb", verb = "take" }
|
||||
},
|
||||
key = "T",
|
||||
note = "Take",
|
||||
on = "pressed",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVerb", verb = "open" }
|
||||
},
|
||||
key = "O",
|
||||
note = "Open",
|
||||
on = "pressed",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setVerb", verb = "talk" }
|
||||
},
|
||||
key = "K",
|
||||
note = "Talk",
|
||||
on = "pressed",
|
||||
when = {}
|
||||
}
|
||||
},
|
||||
title = "The Yard",
|
||||
vars = { allowed = false, score = 0 },
|
||||
verbs = { "walk", "look", "take", "use", "open", "talk" }
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
types = {
|
||||
camera = {
|
||||
behaviours = {
|
||||
{ kind = "camera", lag = 0.2, lookY = 0.6, mode = "follow", target = "hero", x = 0, y = 3, z = 7 }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = {}
|
||||
},
|
||||
floor = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 90, d = 12, g = 110, h = 0.4, kind = "mesh", r = 90, shape = "box", w = 30 },
|
||||
vars = {}
|
||||
},
|
||||
hero = {
|
||||
behaviours = {
|
||||
{ down = "DOWN", kind = "keys", left = "LEFT", right = "RIGHT", up = "UP" },
|
||||
{ height = 0.9, jump = 6.5, kind = "character", radius = 0.3, speed = 4 },
|
||||
{ idle = "Survey", kind = "animator", walk = "Walk" }
|
||||
},
|
||||
look = { d = 0.6, file = "Forge/kit/fox.glb", h = 0.9, kind = "model", scale = 0.008, w = 0.6 },
|
||||
vars = {}
|
||||
},
|
||||
ledge = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 70, d = 4, g = 90, h = 0.4, kind = "mesh", r = 110, shape = "box", w = 4 },
|
||||
vars = {}
|
||||
},
|
||||
prize = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { b = 60, g = 200, kind = "mesh", r = 255, shape = "sphere", w = 0.6 },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
{ fov = 55, kind = "scene3d" },
|
||||
{ kind = "overlay" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "floor", type = "floor", x = 0.0, y = -0.2, z = 0.0 },
|
||||
{ id = "ledge", type = "ledge", x = 6.0, y = 1.2, z = 0.0 },
|
||||
{ id = "prize", type = "prize", x = 6.0, y = 2.0, z = 0.0 },
|
||||
{ id = "hero", type = "hero", x = -4.0, y = 0.5, z = 0.0 },
|
||||
{ id = "camera", type = "camera", x = -4.0, y = 3.0, z = 7.0 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setState", entity = "self", state = "walk" }
|
||||
},
|
||||
each = "hero",
|
||||
note = "Walking is a state the animator plays",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "self.dx != 0 or self.dy != 0" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setState", entity = "self", state = "idle" }
|
||||
},
|
||||
each = "hero",
|
||||
note = "Standing still",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "self.dx == 0 and self.dy == 0" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "jump", entity = "hero" }
|
||||
},
|
||||
note = "Jump, with ground underfoot",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyPressed", key = "SPACE" },
|
||||
{ "onGround", entity = "hero" }
|
||||
}
|
||||
},
|
||||
{
|
||||
a = "prize",
|
||||
act = {
|
||||
{ "destroy", entity = "self" },
|
||||
{ "addScore", amount = 100 },
|
||||
{ "playSound", file = "Forge/kit/coin.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "The prize is taken",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "moveTo", entity = "hero", x = -4, y = 0.5, z = 0 }
|
||||
},
|
||||
note = "Fallen off the world",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "hero.y < -5" }
|
||||
}
|
||||
}
|
||||
},
|
||||
title = "Field",
|
||||
vars = { score = 0 }
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
types = {
|
||||
camera = {
|
||||
behaviours = {
|
||||
{ kind = "camera", mode = "rail", track = "rail" }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = {}
|
||||
},
|
||||
floor = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 90, d = 60, g = 75, h = 0.2, kind = "mesh", r = 70, shape = "box", w = 40 },
|
||||
vars = {}
|
||||
},
|
||||
gun = {
|
||||
behaviours = {
|
||||
{ ammo = 6, kind = "gun", player = 1, reload = "offscreen" }
|
||||
},
|
||||
look = { kind = "none" },
|
||||
vars = { ammo = 6 }
|
||||
},
|
||||
painting = {
|
||||
behaviours = {},
|
||||
look = { b = 255, d = 0.2, g = 255, h = 16, kind = "mesh", r = 255, shape = "box", texture = "Forge/kit/yard.png", unlit = true, w = 24 },
|
||||
vars = {}
|
||||
},
|
||||
pillar = {
|
||||
behaviours = {},
|
||||
look = { d = 1.2, h = 8, kind = "mesh", occluder = true, shape = "box", w = 1.2 },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
zombie = {
|
||||
behaviours = {
|
||||
{ kind = "health", linger = 2, max = 2, ragdoll = true },
|
||||
{ kind = "seek", speed = 1.6, stopAt = 2.2, target = "camera" },
|
||||
{ attack = "Run", idle = "Survey", kind = "animator", walk = "Walk" },
|
||||
{ kind = "target" },
|
||||
{ after = 1.5, kind = "timer", name = "bite", start = false }
|
||||
},
|
||||
look = { d = 1.4, file = "Forge/kit/fox.glb", h = 0.9, kind = "model", scale = 0.01, w = 0.8 },
|
||||
vars = { health = 2 }
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
{ fov = 60, kind = "scene3d" },
|
||||
{ kind = "overlay" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "floor", type = "floor", x = 0.0, y = -0.1, z = -20.0 },
|
||||
{ id = "painting", type = "painting", x = 0.0, y = 6.0, z = -30.0 },
|
||||
{ id = "pillar", type = "pillar", x = 0.0, y = 4.0, z = -22.0 },
|
||||
{ id = "camera", type = "camera", x = 0.0, y = 1.6, z = 0.0 },
|
||||
{ id = "gun", type = "gun", x = 0.0, y = 0.0, z = 0.0 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {
|
||||
{
|
||||
key = "time",
|
||||
name = "rail",
|
||||
points = {
|
||||
{
|
||||
at = 0,
|
||||
look = { 0, 1.2, -10 },
|
||||
stop = "nil",
|
||||
x = 0,
|
||||
y = 1.6,
|
||||
z = 0
|
||||
},
|
||||
{
|
||||
at = 4,
|
||||
look = { 0, 1.2, -16 },
|
||||
stop = "gate",
|
||||
x = 0,
|
||||
y = 1.6,
|
||||
z = -6
|
||||
},
|
||||
{
|
||||
at = 10,
|
||||
look = { 2, 1.2, -24 },
|
||||
stop = "pillar",
|
||||
x = 2,
|
||||
y = 1.6,
|
||||
z = -14
|
||||
},
|
||||
{
|
||||
at = 14,
|
||||
look = { 2, 1.2, -28 },
|
||||
stop = "nil",
|
||||
x = 2,
|
||||
y = 1.6,
|
||||
z = -18
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score .. \" health \" .. health .. \" ammo \" .. gun.ammo" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "spawn", type = "zombie", x = -2, y = 0, z = -14 },
|
||||
{ "spawn", type = "zombie", x = 2, y = 0, z = -15 }
|
||||
},
|
||||
name = "gate",
|
||||
note = "The gate: two come out of the dark",
|
||||
on = "stopped",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "spawn", type = "zombie", x = -1, y = 0, z = -23 },
|
||||
{ "spawn", type = "zombie", x = 0, y = 0, z = -26 },
|
||||
{ "spawn", type = "zombie", x = 4, y = 0, z = -24 }
|
||||
},
|
||||
name = "pillar",
|
||||
note = "The pillar: three more, one from behind it",
|
||||
on = "stopped",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setState", entity = "self", state = "walk" }
|
||||
},
|
||||
type = "zombie",
|
||||
note = "A zombie walks the moment it is made",
|
||||
on = "spawn",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "damage", amount = 1, entity = "self" },
|
||||
{ "addScore", amount = 100 },
|
||||
{ "playSound", file = "Forge/kit/hit.wav" }
|
||||
},
|
||||
type = "zombie",
|
||||
note = "A shot lands",
|
||||
on = "hit",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setState", entity = "self", state = "attack" },
|
||||
{ "timerStart", after = 1.5, entity = "self", name = "bite" }
|
||||
},
|
||||
type = "zombie",
|
||||
note = "It reaches the camera and rears up",
|
||||
on = "arrived",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addVar", amount = -1, name = "health" },
|
||||
{ "flash", b = 40, g = 40, r = 255, seconds = 0.3 },
|
||||
{ "shake", amount = 0.3, seconds = 0.4 },
|
||||
{ "timerStart", after = 1.5, entity = "self", name = "bite" }
|
||||
},
|
||||
type = "zombie",
|
||||
name = "bite",
|
||||
note = "The bite lands unless it was killed in time",
|
||||
on = "timer",
|
||||
when = {
|
||||
{ "inState", entity = "self", state = "attack" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "pathNext", entity = "camera" }
|
||||
},
|
||||
note = "The stop is clear: move on",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "waiting", entity = "camera" },
|
||||
{ "test", expr = "count(\"zombie\") == 0" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "say", seconds = 2, text = "\"YOU ARE DEAD\"" },
|
||||
{ "gameOver" }
|
||||
},
|
||||
note = "Out of health",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "health <= 0" },
|
||||
{ "once", scope = "game", tag = "over" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "say", seconds = 2, text = "\"STAGE CLEAR\"" }
|
||||
},
|
||||
note = "The end of the rail",
|
||||
on = "railEnd",
|
||||
when = {}
|
||||
}
|
||||
},
|
||||
title = "The Dead Yard",
|
||||
vars = { health = 3, score = 0 }
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||
return {
|
||||
layers = {
|
||||
{ gravity = 1500, kind = "world2d" },
|
||||
{ kind = "bezel" }
|
||||
},
|
||||
rooms = {
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "hero", type = "hero", x = 200, y = 420 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "coin", type = "coin", x = 300, y = 392 },
|
||||
{ id = "coin1", type = "coin", x = 420, y = 392 },
|
||||
{ id = "coin2", type = "coin", x = 560, y = 300 },
|
||||
{ id = "spike", type = "spike", x = 640, y = 420 },
|
||||
{ id = "door", type = "door", x = 700, y = 420 }
|
||||
},
|
||||
name = "start",
|
||||
tracks = {}
|
||||
},
|
||||
{
|
||||
entities = {
|
||||
{ id = "ground", type = "ground", x = 360, y = 440 },
|
||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||
{ id = "coin", type = "coin", x = 400, y = 392 }
|
||||
},
|
||||
name = "cave",
|
||||
tracks = {}
|
||||
}
|
||||
},
|
||||
rules = {
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = -1, entity = "hero" }
|
||||
},
|
||||
note = "Run left",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "LEFT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "run", direction = 1, entity = "hero" }
|
||||
},
|
||||
note = "Run right",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "keyHeld", key = "RIGHT" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "jump", entity = "hero" },
|
||||
{ "playSound", file = "Forge/kit/jump.wav" }
|
||||
},
|
||||
note = "Jump, with ground underfoot",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "onGround", entity = "hero" },
|
||||
{ "keyPressed", key = "SPACE" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "setText", entity = "readout", text = "\"score \" .. score .. \" lives \" .. lives" }
|
||||
},
|
||||
note = "The readout, every frame",
|
||||
on = "frame"
|
||||
},
|
||||
{
|
||||
a = "coin",
|
||||
act = {
|
||||
{ "addScore", amount = 10 },
|
||||
{ "destroy", entity = "self" },
|
||||
{ "playSound", file = "Forge/kit/coin.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Take a coin",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "spike",
|
||||
act = {
|
||||
{ "moveTo", entity = "hero", x = 200, y = 380 },
|
||||
{ "playSound", file = "Forge/kit/hit.wav" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Spikes hurt",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
a = "door",
|
||||
act = {
|
||||
{ "goTo", entity = "hero", room = "cave", x = 60, y = 380 },
|
||||
{ "addVar", amount = -1, name = "lives" }
|
||||
},
|
||||
b = "hero",
|
||||
note = "Through the door",
|
||||
on = "enter",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "playMusic", file = "Forge/kit/loop.ogg" }
|
||||
},
|
||||
note = "Music",
|
||||
on = "roomStart",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "submitScore", board = "default" },
|
||||
{ "say", seconds = 2, text = "\"GAME OVER\"" },
|
||||
{ "gameOver" }
|
||||
},
|
||||
note = "Out of lives",
|
||||
on = "frame",
|
||||
when = {
|
||||
{ "test", expr = "lives <= 0" },
|
||||
{ "once", scope = "game", tag = "over" }
|
||||
}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "credit" }
|
||||
},
|
||||
note = "A coin",
|
||||
on = "pressed",
|
||||
switch = "SWITCH_COIN1",
|
||||
when = {}
|
||||
},
|
||||
{
|
||||
act = {
|
||||
{ "addVar", amount = -1, name = "credits" },
|
||||
{ "setVar", name = "lives", value = "3" },
|
||||
{ "restart" }
|
||||
},
|
||||
note = "Continue",
|
||||
on = "pressed",
|
||||
switch = "SWITCH_START1",
|
||||
when = {
|
||||
{ "test", expr = "credits > 0 and lives <= 0" }
|
||||
}
|
||||
}
|
||||
},
|
||||
title = "Coin Run",
|
||||
types = {
|
||||
coin = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" },
|
||||
{ kind = "spin", rate = 180 }
|
||||
},
|
||||
look = { file = "Forge/kit/coin.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
door = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/door.png", kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
ground = {
|
||||
behaviours = {
|
||||
{ kind = "solid" }
|
||||
},
|
||||
look = { b = 90, g = 70, h = 40, kind = "box", r = 60, w = 720 }
|
||||
},
|
||||
hero = {
|
||||
behaviours = {
|
||||
{ jump = 620, kind = "platformer", speed = 260 },
|
||||
{ fps = 10, kind = "frames", states = "idle=1-1, walk=2-5, jump=6-6, fall=7-7" }
|
||||
},
|
||||
look = { file = "Forge/kit/hero.png", frames = 8, kind = "sprite" },
|
||||
vars = {}
|
||||
},
|
||||
readout = {
|
||||
look = { b = 245, g = 235, kind = "text", r = 235, text = "score 0" }
|
||||
},
|
||||
spike = {
|
||||
behaviours = {
|
||||
{ kind = "trigger" }
|
||||
},
|
||||
look = { anchor = "feet", file = "Forge/kit/spike.png", kind = "sprite" },
|
||||
vars = {}
|
||||
}
|
||||
},
|
||||
vars = { credits = 0, lives = 3, score = 0 }
|
||||
}
|
||||
|
|
@ -1,704 +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, sourceX, sourceY, width, height)
|
||||
return newSpriteDrawGrid(id, x, y, sourceX, sourceY, width, height)
|
||||
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 function scoreReady()
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
-- 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()
|
||||
|
||||
if not id or not scoreReady() then
|
||||
return false
|
||||
end
|
||||
masterSubmitScore(id, value, board, meta)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- The board, as a callback: onDone(ok, result) where result.top is the list and result.standing is
|
||||
-- this player's place.
|
||||
function scoreBoard(onDone, board)
|
||||
local id = scoreGameId()
|
||||
|
||||
if not id or not scoreReady() then
|
||||
onDone(false, "this cabinet is not signed in")
|
||||
return false
|
||||
end
|
||||
masterBoard(id, board, onDone)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function scorePlayerName()
|
||||
return scoreReady() and MASTER.name or nil
|
||||
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,349 +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
|
||||
|
||||
|
||||
local function path()
|
||||
if not settingsPath then
|
||||
settingsPath = singeGetSystemInfo().dataRoot .. "master.dat"
|
||||
end
|
||||
return settingsPath
|
||||
end
|
||||
|
||||
|
||||
function masterLoad()
|
||||
local file = io.open(path(), "r")
|
||||
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
local text = file:read("*a")
|
||||
file:close()
|
||||
local 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 {}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function masterSave()
|
||||
local file = io.open(path(), "w")
|
||||
|
||||
if not file then
|
||||
return false
|
||||
end
|
||||
file:write(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
|
||||
netRequest({
|
||||
url = MASTER.url .. endpoint, method = method, headers = headers,
|
||||
body = body and cjson.encode(body) or nil,
|
||||
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
|
||||
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 recognise.
|
||||
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
|
||||
|
||||
|
||||
function masterTellInstalled(gameId, version, onDone)
|
||||
request("POST", "/v1/installed", { game = gameId, version = version and math.floor(version) or nil },
|
||||
onDone or function() end)
|
||||
end
|
||||
|
||||
|
||||
function masterBoard(gameId, board, onDone)
|
||||
request("GET", "/v1/scores/" .. gameId .. (board and ("?board=" .. board) 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
|
||||
|
||||
|
||||
-- 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.
|
||||
-- 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
|
||||
|
||||
|
||||
function masterSubmitScore(gameId, value, board, meta)
|
||||
MASTER.queue[#MASTER.queue + 1] = { game = gameId, value = math.floor(value),
|
||||
board = board or "default", meta = meta,
|
||||
play = MASTER.play }
|
||||
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) 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 },
|
||||
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,41 +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="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. -->
|
||||
<div id="tool" class="tool hidden">
|
||||
<h1 id="toolTitle"></h1>
|
||||
<div id="toolBody"></div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
|
|
@ -1,404 +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
|
||||
|
||||
|
||||
-- Makes a game the selection, wrapping at either end of the list, and shows it.
|
||||
function gameSelect(index)
|
||||
if index < 1 then
|
||||
index = GAME_COUNT
|
||||
elseif index > GAME_COUNT then
|
||||
index = 1
|
||||
end
|
||||
unloadGameAssets()
|
||||
GAME_SELECTED = index
|
||||
loadGameAssets()
|
||||
end
|
||||
|
||||
|
||||
-- Starts the selected game. The menu comes back from the top when it ends.
|
||||
function gameStart()
|
||||
SHUTDOWN_FROM_PUSH = true
|
||||
scriptPush(GAME_LIST[GAME_SELECTED])
|
||||
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)
|
||||
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)
|
||||
return
|
||||
end
|
||||
|
||||
-- Are we displaying the grid background?
|
||||
if menuActive() then
|
||||
|
||||
if what == SWITCH_SERVICE then
|
||||
toolsBegin()
|
||||
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_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()
|
||||
-- With no games found there is no renderer and nothing was loaded; quitting is all there is.
|
||||
if MENU_RENDER == nil then
|
||||
return
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
-- 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
|
||||
table.insert(GAME_LIST, value)
|
||||
GAME_COUNT = GAME_COUNT + 1
|
||||
end
|
||||
GAMES = {}
|
||||
end
|
||||
|
||||
-- Search for games.dat files in subdirectories and inside .game databases.
|
||||
--
|
||||
-- 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.
|
||||
GAME_LIST = {}
|
||||
GAME_COUNT = 0
|
||||
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)
|
||||
|
||||
if GAME_COUNT == 0 then
|
||||
debugPrint("No games found! Exiting.")
|
||||
singeQuit()
|
||||
else
|
||||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||
|
||||
-- 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)
|
||||
if confattr then
|
||||
dofile(CONFIG_FILE)
|
||||
-- A damaged menu.dat must not take the menu down with it.
|
||||
GAME_SELECTED = tonumber(GAME_SELECTED) or 1
|
||||
if GAME_SELECTED > GAME_COUNT then
|
||||
GAME_SELECTED = GAME_COUNT
|
||||
end
|
||||
else
|
||||
GAME_SELECTED = 1
|
||||
end
|
||||
|
||||
-- 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()
|
||||
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(GAME_SELECTED)
|
||||
end
|
||||
|
|
@ -1,406 +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
|
||||
-- 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 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
|
||||
return string.format("<div class='%s'>%s</div>", row.selected and "item selected" or "item", menuSpaces(row.text))
|
||||
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
|
||||
rows[index] = '<div id="game' .. index .. '">' .. menuEscape(game.TITLE) .. '</div>'
|
||||
end
|
||||
menuElement("list").inner_rml = table.concat(rows)
|
||||
for index = 1, GAME_COUNT do
|
||||
guiSetHandler(GUI, DOCUMENT, "game" .. index, "click", menuRowClick)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.showGame = function(game)
|
||||
menuRow(GAME_SELECTED):SetClass("selected", true)
|
||||
scrollPending = true
|
||||
menuArt("poster", game.CABINET)
|
||||
menuArt("marquee", game.MARQUEE)
|
||||
guiSetValue(GUI, DOCUMENT, "title", menuEscape(game.TITLE))
|
||||
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))
|
||||
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()
|
||||
-- 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())
|
||||
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
|
||||
rows[#rows + 1] = toolRow(row)
|
||||
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))
|
||||
menuElement("games"):SetClass("hidden", toolsActive())
|
||||
menuElement("tool"):SetClass("hidden", not toolsActive())
|
||||
menuElement("tool"):SetClass("clear", TOOL_CLEAR)
|
||||
menuElement("tool"):SetClass("flash", TOOL_FLASH)
|
||||
end
|
||||
guiDraw(GUI)
|
||||
end
|
||||
|
|
@ -1,544 +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_X = 25
|
||||
local MARGIN_Y = 25
|
||||
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 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 introDone = false
|
||||
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 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 colour it wants. The colours are roles
|
||||
-- rather than values because the audio delay tool inverts the whole page for a frame, and fontPrint
|
||||
-- takes the colour at the moment it draws -- unlike fontToSprite, which bakes it in.
|
||||
local toolLines = {}
|
||||
|
||||
|
||||
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, overlayGetWidth() - MARGIN_X - x)) do
|
||||
toolLine({ { x = x, text = line, role = role } })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function toolsLayout()
|
||||
local valueX = MARGIN_X + LABEL_W
|
||||
local wasKeys = false
|
||||
-- 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())
|
||||
|
||||
toolLines = {}
|
||||
if more then
|
||||
toolLine({ { x = MARGIN_X, 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 = MARGIN_X, text = " ...", role = "quiet" } })
|
||||
below = false
|
||||
end
|
||||
if row.kind == "item" then
|
||||
seenItem = true
|
||||
end
|
||||
-- The key hints stand away from whatever the tool put above them, as they do in the
|
||||
-- document, but sit together when a tool needs more than one line of them.
|
||||
if row.kind == "keys" and not wasKeys then
|
||||
toolLine({})
|
||||
end
|
||||
wasKeys = (row.kind == "keys")
|
||||
|
||||
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, overlayGetWidth() - MARGIN_X - valueX)
|
||||
toolLine({ { x = MARGIN_X, 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
|
||||
toolWrapped(row.text, MARGIN_X + SUB_INDENT, "quiet")
|
||||
elseif row.kind == "keys" then
|
||||
toolWrapped(row.text, MARGIN_X, "quiet")
|
||||
elseif row.kind == "item" then
|
||||
toolLine({ { x = MARGIN_X, text = (row.selected and "> " or " ") .. row.text,
|
||||
role = row.selected and "pick" or "ink" } })
|
||||
else
|
||||
toolWrapped(row.text, MARGIN_X, "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 = MARGIN_X, text = " ...", role = "quiet" } })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER = {}
|
||||
|
||||
|
||||
MENU_RENDER.begin = function()
|
||||
font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
hintFont = fontLoad("Singe/FreeSansBold.ttf", 13)
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
fontSelect(font)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.finish = function()
|
||||
if font then
|
||||
fontUnload(font)
|
||||
font = nil
|
||||
end
|
||||
if hintFont then
|
||||
fontUnload(hintFont)
|
||||
hintFont = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The backdrop is the disc: 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.
|
||||
MENU_RENDER.backdropBegin = function(showIntro)
|
||||
introDone = not showIntro
|
||||
discPlay()
|
||||
if not showIntro then
|
||||
discSkipToFrame(DISC_GRID_START)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.backdrop = function()
|
||||
-- Loop the grid. A machine too slow to show every disc frame can step over the last one, after
|
||||
-- which the engine starts the disc again from the top; once the intro has played, that is a loop.
|
||||
if discGetFrame() >= DISC_LAST_FRAME or (introDone and discGetFrame() < DISC_GRID_START) then
|
||||
discSkipToFrame(DISC_GRID_START)
|
||||
end
|
||||
if discGetFrame() >= DISC_GRID_START then
|
||||
introDone = true
|
||||
end
|
||||
|
||||
-- Transparent, so the disc behind the overlay is what shows.
|
||||
colorBackground(0, 0, 0, 0)
|
||||
overlayClear()
|
||||
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 discGetFrame() >= 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
|
||||
|
||||
|
||||
MENU_RENDER.showGame = function(game)
|
||||
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.
|
||||
local 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 .. field(game.DESCRIPTION)
|
||||
-- fontToSprite bakes the colour 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 image
|
||||
x = cabinetX + (cabinetW - spriteGetWidth(spriteCabinet)) * 0.5
|
||||
y = cabinetY + (cabinetH - spriteGetHeight(spriteCabinet)) * 0.5
|
||||
spriteDraw(spriteCabinet, x, y)
|
||||
|
||||
-- Marquee image
|
||||
x = marqueeX + (marqueeW - spriteGetWidth(spriteMarquee)) * 0.5
|
||||
y = marqueeY + (marqueeH - spriteGetHeight(spriteMarquee)) * 0.5
|
||||
spriteDraw(spriteMarquee, x, y)
|
||||
|
||||
-- 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 = string.format("%s (%d of %d)", field(GAME_LIST[GAME_SELECTED].TITLE), GAME_SELECTED, GAME_COUNT)
|
||||
|
||||
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)
|
||||
colorForeground(0, 0, 0, 255)
|
||||
fontPrint(hintsX + SHADOW, hintsY + SHADOW, HINTS)
|
||||
colorForeground(178, 196, 224, 255)
|
||||
fontPrint(hintsX, hintsY, HINTS)
|
||||
fontSelect(font)
|
||||
end
|
||||
|
||||
|
||||
MENU_RENDER.tools = function()
|
||||
local y = MARGIN_Y
|
||||
|
||||
-- A tool that draws its own picture into the overlay has already done so, and the page has to
|
||||
-- let it through; every other tool gets a page painted over everything.
|
||||
if not TOOL_CLEAR then
|
||||
if TOOL_FLASH then
|
||||
colorBackground(255, 255, 255, 255)
|
||||
else
|
||||
colorBackground(0, 0, 0, 255)
|
||||
end
|
||||
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 ink = TOOL_FLASH and 0 or 255
|
||||
local quiet = TOOL_FLASH and 90 or 160
|
||||
|
||||
-- A page that lets the overlay through gathers its text along the bottom, out of the way of
|
||||
-- whatever the tool drew: the light gun's corner targets in particular.
|
||||
if TOOL_CLEAR then
|
||||
y = overlayGetHeight() - MARGIN_Y - (#toolLines + 1) * LINE_HEIGHT
|
||||
end
|
||||
colorForeground(ink, ink, ink, 255)
|
||||
fontPrint(MARGIN_X, y, TOOL_TITLE)
|
||||
y = y + LINE_HEIGHT
|
||||
if not TOOL_CLEAR then
|
||||
y = y + LINE_HEIGHT
|
||||
end
|
||||
|
||||
for _, line in ipairs(toolLines) do
|
||||
for _, piece in ipairs(line) do
|
||||
if piece.role == "quiet" then
|
||||
colorForeground(quiet, quiet, quiet, 255)
|
||||
elseif piece.role == "pick" then
|
||||
colorForeground(255, 211, 90, 255)
|
||||
else
|
||||
colorForeground(ink, ink, ink, 255)
|
||||
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
|
||||
end
|
||||
467
assets/Net.singe
467
assets/Net.singe
|
|
@ -1,467 +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 a CA list. Linux has one; Windows, macOS and the
|
||||
-- handhelds have nothing LuaSec can read, so the engine extracts Mozilla's list as
|
||||
-- Singe/cacerts.pem beside the other support files and that is used wherever the platform's own
|
||||
-- is missing.
|
||||
|
||||
|
||||
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_SYSTEM = "/etc/ssl/certs/ca-certificates.crt" -- Linux's list, when it is there,
|
||||
NET_CA_SHIPPED = "Singe/cacerts.pem" -- and the one Singe carries otherwise.
|
||||
|
||||
local active = {}
|
||||
local nextId = 1
|
||||
|
||||
|
||||
-- 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.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 CA list a connection without a pin is verified against.
|
||||
local function caFile()
|
||||
local lfs = require("lfs")
|
||||
|
||||
if lfs.attributes(NET_CA_SYSTEM, "mode") == "file" then
|
||||
return NET_CA_SYSTEM
|
||||
end
|
||||
|
||||
return NET_CA_SHIPPED
|
||||
end
|
||||
|
||||
|
||||
local function startTls(request)
|
||||
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, a CA list is asked -- and if it has nothing to say, the
|
||||
-- request fails rather than proceeding unverified.
|
||||
verify = (#NET_PINS > 0) and "none" or "peer",
|
||||
cafile = caFile(),
|
||||
}
|
||||
local wrapped, err = ssl.wrap(request.socket, params)
|
||||
|
||||
if not wrapped then
|
||||
return fail(request, "TLS setup failed: " .. tostring(err))
|
||||
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))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function pumpSending(request)
|
||||
if not request.outgoing then
|
||||
request.outgoing = buildRequest(request)
|
||||
end
|
||||
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
|
||||
request.state = "reading"
|
||||
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,
|
||||
toFile = opts.toFile, onProgress = opts.onProgress, onDone = onDone,
|
||||
buffer = "", parts = {}, redirects = 0, lastProgress = singeGetTicks(),
|
||||
}
|
||||
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 = "cancelled" })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function netBusy()
|
||||
return next(active) ~= nil
|
||||
end
|
||||
BIN
assets/Plaque-champagne.stl
(Stored with Git LFS)
BIN
assets/Plaque-champagne.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Plaque-dark.stl
(Stored with Git LFS)
BIN
assets/Plaque-dark.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Plaque-frame.stl
(Stored with Git LFS)
BIN
assets/Plaque-frame.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Plaque-glass.stl
(Stored with Git LFS)
BIN
assets/Plaque-glass.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/Plaque-plate.stl
(Stored with Git LFS)
BIN
assets/Plaque-plate.stl
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,56 +0,0 @@
|
|||
# Materials for Plaque.obj
|
||||
newmtl dark
|
||||
Ka 0.060 0.062 0.066
|
||||
Kd 0.300 0.310 0.330
|
||||
Ks 0.700 0.700 0.700
|
||||
Ns 180.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.35
|
||||
|
||||
newmtl champagne
|
||||
Ka 0.144 0.128 0.108
|
||||
Kd 0.720 0.640 0.540
|
||||
Ks 0.850 0.800 0.700
|
||||
Ns 120.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.40
|
||||
|
||||
newmtl glass
|
||||
Ka 0.176 0.184 0.180
|
||||
Kd 0.880 0.920 0.900
|
||||
Ks 0.950 0.950 0.950
|
||||
Ns 300.0
|
||||
d 0.55
|
||||
Ni 1.50
|
||||
illum 4
|
||||
Pm 0.00
|
||||
Pr 0.15
|
||||
|
||||
newmtl frame
|
||||
Ka 0.024 0.022 0.020
|
||||
Kd 0.120 0.110 0.100
|
||||
Ks 0.300 0.300 0.300
|
||||
Ns 40.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.00
|
||||
Pr 0.70
|
||||
|
||||
newmtl plate
|
||||
Ka 0.110 0.106 0.100
|
||||
Kd 0.550 0.530 0.500
|
||||
Ks 0.200 0.200 0.200
|
||||
Ns 30.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.00
|
||||
Pr 0.80
|
||||
|
||||
7040
assets/Plaque.obj
7040
assets/Plaque.obj
File diff suppressed because it is too large
Load diff
|
|
@ -1,117 +0,0 @@
|
|||
--[[
|
||||
*
|
||||
* Singe 3
|
||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*
|
||||
*
|
||||
--]]
|
||||
|
||||
|
||||
local socket = require("socket")
|
||||
local copas = require("copas")
|
||||
local https = require("copas.http").request
|
||||
|
||||
|
||||
dofile("Singe/Framework.singe")
|
||||
|
||||
|
||||
function get(url)
|
||||
local download = {}
|
||||
download.url = url
|
||||
download.finished = false
|
||||
copas.addthread(function(d)
|
||||
d.result, d.error = https(d.url)
|
||||
d.finished = true
|
||||
end,
|
||||
download)
|
||||
return download
|
||||
end
|
||||
|
||||
|
||||
function onInputPressed(what)
|
||||
|
||||
if what == SWITCH_LEFT then
|
||||
discStepBackward()
|
||||
end
|
||||
|
||||
if what == SWITCH_RIGHT then
|
||||
discStepForward()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onOverlayUpdate()
|
||||
|
||||
copas.step()
|
||||
|
||||
colorBackground(0, 0, 0, 127) -- this dims the background 50%
|
||||
overlayClear()
|
||||
|
||||
overlayPrint(1, 1, dldGameList.url.." "..(dldGameList.finished and 'true' or 'false'))
|
||||
if dldGameList.finished then
|
||||
overlayPrint(1, 2, dldGameList.error)
|
||||
overlayPrint(1, 3, dldGameList.result)
|
||||
end
|
||||
|
||||
colorForeground(255, 0, 0, 255)
|
||||
overlayBox(0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1)
|
||||
|
||||
spriteDraw(sprServiceMenu, (overlayGetWidth() - spriteGetWidth(sprServiceMenu)) / 2, 25)
|
||||
|
||||
return(OVERLAY_UPDATED)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function onShutdown()
|
||||
|
||||
spriteUnload(sprServiceMenu)
|
||||
fontUnload(fntSans18)
|
||||
fontUnload(fntSans32)
|
||||
|
||||
end
|
||||
|
||||
|
||||
copas.running = true
|
||||
|
||||
dldGameList = get("https://kangaroopunch.com/api/singeSoftware")
|
||||
|
||||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||
|
||||
DISC_SINGE_LOGO = 140
|
||||
|
||||
discSkipToFrame(DISC_SINGE_LOGO)
|
||||
discPause()
|
||||
|
||||
fontQuality(FONT_QUALITY_BLENDED)
|
||||
fntSans18 = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||
fntSans32 = fontLoad("Singe/FreeSansBold.ttf", 32)
|
||||
|
||||
fontSelect(fntSans32)
|
||||
sprServiceMenu = fontToSprite("Singe Service Menu")
|
||||
|
||||
|
||||
|
||||
--[[
|
||||
|
||||
Game Management
|
||||
|
||||
|
||||
--]]
|
||||
|
||||
BIN
assets/SingeText.glb
(Stored with Git LFS)
BIN
assets/SingeText.glb
(Stored with Git LFS)
Binary file not shown.
BIN
assets/SingeText.jpeg
(Stored with Git LFS)
BIN
assets/SingeText.jpeg
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,12 +0,0 @@
|
|||
# Materials for SingeText.obj
|
||||
newmtl logo
|
||||
Ka 0.060 0.062 0.066
|
||||
Kd 0.300 0.310 0.330
|
||||
Ks 0.700 0.700 0.700
|
||||
Ns 180.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.35
|
||||
|
||||
1735
assets/SingeText.obj
1735
assets/SingeText.obj
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
BIN
assets/SingeTextPrint-face.stl
(Stored with Git LFS)
BIN
assets/SingeTextPrint-face.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/SingeTextPrint-plate.stl
(Stored with Git LFS)
BIN
assets/SingeTextPrint-plate.stl
(Stored with Git LFS)
Binary file not shown.
BIN
assets/SingeTextPrint-walls.stl
(Stored with Git LFS)
BIN
assets/SingeTextPrint-walls.stl
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,34 +0,0 @@
|
|||
# Materials for SingeTextPrint.obj
|
||||
newmtl dark
|
||||
Ka 0.060 0.062 0.066
|
||||
Kd 0.300 0.310 0.330
|
||||
Ks 0.700 0.700 0.700
|
||||
Ns 180.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.35
|
||||
|
||||
newmtl frame
|
||||
Ka 0.024 0.022 0.020
|
||||
Kd 0.120 0.110 0.100
|
||||
Ks 0.300 0.300 0.300
|
||||
Ns 40.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.00
|
||||
Pr 0.70
|
||||
|
||||
newmtl champagne
|
||||
Ka 0.144 0.128 0.108
|
||||
Kd 0.720 0.640 0.540
|
||||
Ks 0.850 0.800 0.700
|
||||
Ns 120.0
|
||||
d 1.00
|
||||
Ni 1.00
|
||||
illum 2
|
||||
Pm 0.80
|
||||
Pr 0.40
|
||||
|
||||
File diff suppressed because it is too large
Load diff
1365
assets/Tools.singe
1365
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,74 +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 analogue 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 analogue 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.
|
||||
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 }
|
||||
-- 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.
256
assets/menu.rcss
256
assets/menu.rcss
|
|
@ -1,256 +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 {
|
||||
padding: 3dp 8dp;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
/* .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 opaque page over everything, the same for all of them. The audio delay
|
||||
tool alone turns the page white for the frame of its flash, so the colours are named twice. */
|
||||
.tool {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
padding: 25dp;
|
||||
box-sizing: border-box;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tool.flash {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.tool h1 {
|
||||
font-size: 24dp;
|
||||
color: #ffffff;
|
||||
margin-bottom: 20dp;
|
||||
}
|
||||
|
||||
.tool.flash h1 {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.tool p {
|
||||
margin-bottom: 20dp;
|
||||
}
|
||||
|
||||
.tool p.value {
|
||||
margin-bottom: 4dp;
|
||||
}
|
||||
|
||||
/* The key hints stand away from whatever the tool put above them, and close together among
|
||||
themselves when a tool needs more than one line of them. */
|
||||
.tool p.keys {
|
||||
margin-top: 20dp;
|
||||
margin-bottom: 4dp;
|
||||
color: #8fa2c4;
|
||||
}
|
||||
|
||||
.tool p.keys + p.keys {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 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: 2dp;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tool .sub {
|
||||
margin-left: 20dp;
|
||||
margin-bottom: 2dp;
|
||||
font-size: 12dp;
|
||||
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: 2dp;
|
||||
}
|
||||
|
||||
.tool .pair .label {
|
||||
flex: 0 0 110dp;
|
||||
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 in the game list's gold -- the same
|
||||
way the game list itself shows a selection, since RCSS has no generated content to mark it with. */
|
||||
.tool .item {
|
||||
margin-bottom: 2dp;
|
||||
}
|
||||
|
||||
.tool .item.selected {
|
||||
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 gather their text
|
||||
into a strip along the bottom, between the two lower targets, so nothing worth aiming at is
|
||||
covered. */
|
||||
.tool.clear {
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tool.clear h1 {
|
||||
font-size: 16dp;
|
||||
margin-bottom: 4dp;
|
||||
}
|
||||
|
||||
.tool.clear h1,
|
||||
.tool.clear .row,
|
||||
.tool.clear .pair,
|
||||
.tool.clear .sub,
|
||||
.tool.clear .item,
|
||||
.tool.clear p {
|
||||
background-color: #000000c0;
|
||||
padding: 1dp 8dp;
|
||||
margin-bottom: 4dp;
|
||||
}
|
||||
|
||||
/* 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.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue