3454 lines
112 KiB
Text
3454 lines
112 KiB
Text
= Singe Manual
|
|
Scott Duensing <scott@kangaroopunch.com>
|
|
:revnumber: 2.20
|
|
:revdate: 2026
|
|
:doctype: book
|
|
:toc: left
|
|
:toclevels: 3
|
|
:sectnums:
|
|
:sectnumlevels: 3
|
|
:source-highlighter: rouge
|
|
:icons: font
|
|
:experimental:
|
|
|
|
[preface]
|
|
== About Singe
|
|
|
|
Singe, the Somewhat Interactive Nostalgic Game Engine (named after the dragon in
|
|
Dragon's Lair) is a Lua-based scripting system that allows for rapid prototyping
|
|
of new laserdisc games, or the creation of entirely new games. The language is
|
|
easy to learn, very powerful, and fast. All the features needed to develop your
|
|
own game are made available through a simple application programming interface
|
|
(API).
|
|
|
|
Singe provides numerous features to the game developer. Some of the more
|
|
interesting ones are:
|
|
|
|
* Object-oriented programming language
|
|
* Animated sprites
|
|
* TrueType font support
|
|
* 32-bit color space with transparency
|
|
* Multi-channel, overlapping, stereo sound
|
|
* Analog and digital input device support
|
|
* Wide support for video and audio formats
|
|
|
|
For players, Singe allows many unemulated and new games to be enjoyed using any
|
|
desired front end or via the included menu system.
|
|
|
|
Since the original release of Singe back in 2006, several revisions have been
|
|
released, both officially and unofficially. In 2020, Singe 2.00 was released.
|
|
This was a total rewrite from the ground up, adding numerous features while
|
|
staying compatible with existing 1.xx games. As of the writing of this manual,
|
|
the latest release is {revnumber}.
|
|
|
|
== Installation and Upgrading
|
|
|
|
=== Installing Singe
|
|
|
|
To install Singe, simply download the appropriate binary for your system. Place
|
|
it in a new folder by itself and run it. On the first run, Singe will create
|
|
all the necessary directories and support files needed. You are now ready to
|
|
install some games.
|
|
|
|
=== Upgrading Singe
|
|
|
|
To upgrade Singe to a newer version, place the new binary in the installation
|
|
folder and rename (or delete) the existing `<InstallDir>/Singe` folder. As with
|
|
the initial installation, run the new binary with no command line arguments to
|
|
generate the new support files.
|
|
|
|
WARNING: Do not run a new Singe binary against an out-of-date set of support
|
|
files. Always refresh the `Singe/` directory when upgrading.
|
|
|
|
=== Things to Know
|
|
|
|
Do not touch the `Singe/` folder. Files in this folder belong to Singe and may
|
|
be updated or deleted at any time.
|
|
|
|
== Usage
|
|
|
|
=== Installing Games
|
|
|
|
Games designed for Singe 2.xx and later simply need to be unpacked and copied
|
|
into the same folder where you placed the Singe binary. The included menu
|
|
system will automatically detect them and add them to the menu.
|
|
|
|
=== Customizing the Controls
|
|
|
|
By default, Singe maps controllers as if they were Xbox gamepads. If your
|
|
controller is not recognized or you wish to change the default mappings as they
|
|
appear to Singe, you can use the free SDL2 Gamepad Tool from
|
|
http://generalarcade.com/gamepadtool/.
|
|
|
|
In addition to configuring your controller, you can remap all the input options
|
|
for Singe as a whole or for any individual game by using `controls.cfg` files.
|
|
When determining how inputs are mapped, Singe reads configuration settings in
|
|
the following order:
|
|
|
|
. The defaults from inside Singe.
|
|
. `controls.cfg` from the directory Singe was started in (the `Singe` directory holds a `controls.cfg.example` template you can copy).
|
|
. `controls.cfg` from the root of the data directory.
|
|
. `controls.cfg` from the game's data directory.
|
|
. `controls.cfg` from the game's main script directory.
|
|
|
|
You do not have to specify every option in a custom `controls.cfg` file. Feel
|
|
free to just provide the entries you want changed.
|
|
|
|
After installing Singe, you can find a sample input configuration file at
|
|
`<InstallDir>/Singe/controls.cfg.example`. To see the available configuration
|
|
options available to be used in this file, read through
|
|
`<InstallDir>/Singe/Framework.singe`.
|
|
|
|
=== Command Line Options
|
|
|
|
----
|
|
Singe [OPTIONS] scriptName{.singe}
|
|
----
|
|
|
|
The script name is the only required argument. It may be a `.singe` file, or
|
|
a directory containing a script of the same name (`ActionMax` finds
|
|
`ActionMax/ActionMax.singe`). When no `--framefile` is given, Singe looks for
|
|
a video next to the script with the same base name and any extension FFmpeg
|
|
can demux, then for a `.txt` framefile.
|
|
|
|
[cols="1,2",options="header"]
|
|
|===
|
|
| Option | Purpose
|
|
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
|
|
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
|
|
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory.
|
|
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
|
|
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
|
|
| `-h`, `--help` | Show the option summary and exit.
|
|
| `-k`, `--nologos` | Skip the splash screens.
|
|
| `-l`, `--volume_vldp=PERCENT` | Laserdisc volume, `0` to `100`.
|
|
| `-m`, `--nomouse` | Disable mouse and lightgun input entirely.
|
|
| `-n`, `--nocrosshair` | Ask the game not to draw its own crosshair. Exposed to scripts as `singeWantsCrosshairs()`.
|
|
| `-o`, `--audio=TRACK` | Default audio track (zero based) for videos with several, such as multi-language releases.
|
|
| `-p`, `--program` | Trace engine activity to the console and to `trace.txt` in the data directory.
|
|
| `-s`, `--nosound` | Mute all audio.
|
|
| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`.
|
|
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
|
|
| `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script.
|
|
| `-w`, `--fullscreen_window` | Borderless window covering the desktop.
|
|
| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted.
|
|
| `-y`, `--yresolution=VALUE` | Window height.
|
|
| `-z`, `--noconsole` | Print nothing (and open no console window on Windows).
|
|
|===
|
|
|
|
Options may be given as `-x 640`, `-x640`, or `--xresolution=640`. Numeric
|
|
values are validated; a bad value prints the usage text and exits.
|
|
|
|
== Frequently Asked Questions
|
|
|
|
*Why is it named Singe?*
|
|
|
|
Singe is the name of the dragon in Dragon's Lair. As Singe (the program) began
|
|
as an add-on to Daphne (the princess in Dragon's Lair) the Dragon's Lair theme
|
|
was kept.
|
|
|
|
*What is the difference between Daphne, Singe, Hypseus, and Singe 2.xx?*
|
|
|
|
Daphne is an actual laserdisc game emulator that can run a handful of classic
|
|
laser games. Daphne/Singe (Singe 1.xx) is the original version of Singe that
|
|
was an add-on for the Daphne emulator -- originally shipped as a game DLL for
|
|
Daphne, then later combined into a single binary with the Daphne emulation
|
|
features removed. Hypseus-Singe is an enhanced fork of Daphne with Singe 1.xx
|
|
and some Singe 2.xx support. Finally, Singe 2.xx is an all-new, built-from-
|
|
scratch upgrade of the original Singe. In short, if you want accuracy, use
|
|
Daphne or Hypseus if they support emulating the desired game. If it is a Singe
|
|
game, use Singe 2.xx, or try Hypseus.
|
|
|
|
*Why should we not call it an "emulator"?*
|
|
|
|
Emulators use the real software or ROMs from the original game and pretend to
|
|
be the machine they were originally intended to run on. From the game's
|
|
perspective, it is business as usual. Singe, on the other hand, requires that
|
|
every game be re-implemented (ported) to run on Singe. The end result may be
|
|
very similar, but how it works is entirely different.
|
|
|
|
*Can I run Singe 1.xx games on Singe 2.xx?*
|
|
|
|
Yes, probably. But you likely will not want to. Almost everything from Singe
|
|
1.xx has been converted and enhanced for Singe 2.xx.
|
|
|
|
*Why does the game I just installed not show in the menu?*
|
|
|
|
Whoever packaged the game failed to include a proper `games.dat` file.
|
|
|
|
*Why is HD video slow?*
|
|
|
|
Due to the way Singe accesses video files to provide frame seeking, it is
|
|
unable to offload video decoding to the video card. High definition video
|
|
requires a lot of CPU.
|
|
|
|
*Why does my audio stutter on Windows?*
|
|
|
|
Users have discovered that most Singe audio and stuttering problems are related
|
|
to their installed audio driver -- especially Realtek-based devices. Try
|
|
updating your sound drivers or switching to the generic Windows HD Audio
|
|
driver. Disable surround sound -- use stereo, not 5.1 or 7.1. If your drivers
|
|
do not allow you to use stereo, you can use Virtual Audio Cable to fix this:
|
|
|
|
. Install VB-CABLE (https://vb-audio.com/Cable/).
|
|
. Go to Windows sound settings (right-click sound on the taskbar, then
|
|
*Sounds*, then *Playback*).
|
|
. Right-click the *Cable Input* device and choose *Set as default device*.
|
|
(You will lose your current sound output temporarily.)
|
|
. Go to the *Recording* tab.
|
|
. Right-click *Cable Output*, then *Properties*, then *Listen*, and finally
|
|
*Enable Listen to This Device*. In the drop-down list, choose your actual
|
|
listening device.
|
|
. You should have sound again. Load up Singe and your audio will be working
|
|
and lag-free.
|
|
|
|
*Why does my audio stutter on the Raspberry Pi?*
|
|
|
|
The Raspberry Pi OS now uses Pipewire as the default audio backend. Switching
|
|
to PulseAudio seems to fix the issue:
|
|
|
|
. Run `sudo raspi-config` from a terminal window.
|
|
. Select *Advanced Options*.
|
|
. Select *Audio Config*.
|
|
. Select *PulseAudio*.
|
|
. Press kbd:[Tab], select *Ok*, and let it reboot.
|
|
|
|
== Game Development
|
|
|
|
=== Lua
|
|
|
|
Singe uses the Lua programming language (http://www.lua.org) for scripting game
|
|
logic. Lua is fast, lightweight, object-oriented, easy to use, and actually
|
|
used in the games industry. A tutorial in Lua is beyond the scope of this
|
|
document. You can find video tutorials for Lua on the Kangaroo Punch YouTube
|
|
channel and additional documentation by searching the web.
|
|
|
|
=== Basic Rules
|
|
|
|
* Singe is cross-platform. Things you do not normally need to worry about on
|
|
Windows are important. Filenames are case-sensitive -- `MyScript.singe` and
|
|
`myscript.singe` are not the same file. You should also use UNIX path
|
|
separators -- `/` rather than `\`. This both makes your code cleaner (no
|
|
double-escaped backslashes) and works everywhere Singe is supported.
|
|
* Pick a distinctive name for your game folder. Do not use spaces.
|
|
* Use the included Singe Framework. Do not copy the framework into your game
|
|
scripts. By including the provided framework you help with future
|
|
compatibility as Singe is updated. Begin (or end) your game script with:
|
|
+
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
* Stay out of the `Singe/` folder. This is managed by Singe and anything added
|
|
or changed here is subject to future deletion.
|
|
* Include a `games.dat`. This is extremely important for new users. While
|
|
other front ends and Singe-based menu systems exist, it is vital that you
|
|
include data for the built-in menu system. Almost all support issues are due
|
|
to missing or malformed `games.dat` files.
|
|
* Do not distribute non-essential files. Never ship Singe binaries or
|
|
Singe-provided scripts with your game. Do not ship index files generated
|
|
from the videos.
|
|
|
|
=== Game Directory Layout
|
|
|
|
A Singe installation is a directory containing the Singe executable, the
|
|
`Singe` support directory it creates on first run, one directory per game,
|
|
and a `data` directory when the menu or a front end passes `--datadir`.
|
|
|
|
----
|
|
Singe/ Support files extracted by the engine
|
|
Framework.singe Loaded by every game (dofile it)
|
|
Menu.singe The bundled game menu
|
|
controls.cfg.example Template for input mappings
|
|
Manual.pdf This manual
|
|
ActionMax/ One game
|
|
games.dat Menu entries for the games in this directory
|
|
38AmbushAlley.singe A script
|
|
frame_38AmbushAlley.txt Its framefile (or a video with the same base name)
|
|
sprite_*.png, sound_*.wav, font_*.ttf
|
|
data/
|
|
ActionMax/ Indexes, trace.txt, screenshots for that game
|
|
----
|
|
|
|
Paths inside a script are relative to the directory Singe was started from,
|
|
not to the script. Use the `DIR` global, which `Framework.singe` sets to the
|
|
script's own directory, so a game works no matter where it is installed:
|
|
|
|
[source,lua]
|
|
----
|
|
crosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
|
|
----
|
|
|
|
Everything Singe writes goes to the data directory (`singeGetDataPath()`),
|
|
so a game can live on read-only media.
|
|
|
|
=== Packaging Your Game
|
|
|
|
Singe installs games itself. Drop an archive into the installation directory,
|
|
start Singe, and the archive is validated, unpacked, and deleted. Any format
|
|
libarchive reads is accepted (zip, 7z, tar.gz, and so on); the extension
|
|
tells Singe what kind of package it is:
|
|
|
|
[cols="1,3",options="header"]
|
|
|===
|
|
| Extension | Rules
|
|
| `.game` | Everything must live inside one top level directory, which must contain a `games.dat`.
|
|
| `.tool` | Like a game, but no `games.dat` is required.
|
|
| `.patch` | Files may live anywhere; used to update an installed game in place.
|
|
|===
|
|
|
|
Every package is rejected if it contains `controls.dat`, `Framework.singe`,
|
|
a file whose extension is `exe`, `sh`, `bat`, `cmd`, or `index`, or (for
|
|
games and tools) an extensionless file whose name starts with `singe`. Paths
|
|
that escape the installation directory are refused as well. Do not ship video
|
|
index files; Singe rebuilds them on first run.
|
|
|
|
To make a package, archive your game directory so the directory itself is the
|
|
top level entry, and rename the result:
|
|
|
|
----
|
|
zip -r ActionMax.game ActionMax
|
|
----
|
|
|
|
=== Event Driven... Or Not?
|
|
|
|
Traditionally, Singe used an event-driven programming model -- Singe handles
|
|
everything and only calls your code when it needs to tell you something or
|
|
update the screen. This is efficient and is how most Lua-based game engines
|
|
work, but it can be verbose. As of Singe 2.10 there is a new _threaded_ model
|
|
that allows you to write procedural code.
|
|
|
|
==== Event Driven
|
|
|
|
With the event-driven programming model, Singe controls the main program loop
|
|
and is in charge of the order of execution. Singe automatically handles all
|
|
the details of decoding and presenting video and audio. It manages
|
|
controllers, mice, and keyboard input. When Singe needs something
|
|
game-specific, it calls part of your script. The most basic Singe script that
|
|
demonstrates all the existing callbacks looks like this:
|
|
|
|
[source,lua]
|
|
----
|
|
-- Singe Game Skeleton.
|
|
|
|
-- Load the Singe Framework.
|
|
dofile("Singe/Framework.singe")
|
|
|
|
-- Declare any global variables you need here.
|
|
|
|
function onControllerMoved(axis, value, which)
|
|
--[[
|
|
Reports which controller axis was moved as well as its current value.
|
|
(Range: -32768 to 32767.) This is used for analog devices. Digital
|
|
input is handled by onInput and onKey.
|
|
--]]
|
|
end
|
|
|
|
function onInputPressed(what)
|
|
--[[
|
|
When in keyboard MODE_NORMAL, input events are reported here when the
|
|
key or button is first depressed. For a full list of keys, buttons,
|
|
and controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
|
will be called with the keysym of the key pressed.
|
|
--]]
|
|
end
|
|
|
|
function onInputReleased(what)
|
|
--[[
|
|
When in keyboard MODE_NORMAL, input events are reported here when the
|
|
key or button is released. For a full list of keys, buttons, and
|
|
controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
|
will be called with the keysym of the key released.
|
|
--]]
|
|
end
|
|
|
|
function onKeyPressed(key, scancode)
|
|
--[[
|
|
When in keyboard MODE_FULL, input events are reported here when the
|
|
key is pressed. Both the keysym and scancode are returned. For a list
|
|
of available scancodes, see Singe/Framework.singe.
|
|
--]]
|
|
end
|
|
|
|
function onKeyReleased(key, scancode)
|
|
--[[
|
|
When in keyboard MODE_FULL, input events are reported here when the
|
|
key is released. Both the keysym and scancode are returned. For a
|
|
list of available scancodes, see Singe/Framework.singe.
|
|
--]]
|
|
end
|
|
|
|
function onMouseMoved(x, y, xr, yr, which)
|
|
--[[
|
|
Called when the mouse is moved. When in SINGLE_MOUSE mode, absolute
|
|
X and Y values as well as the relative change in position are
|
|
returned. For MANY_MOUSE mode, only the relative change is available
|
|
as well as which mouse was moved.
|
|
--]]
|
|
end
|
|
|
|
function onOverlayUpdate()
|
|
--[[
|
|
This is the only place you can safely perform drawing operations.
|
|
If you wish to display a targeting cursor, you will need to save the
|
|
mouse position from onMouseMoved in global variables and then use
|
|
those here to render the cursor.
|
|
--]]
|
|
|
|
-- Tell Singe if we changed the display or not.
|
|
return(OVERLAY_UPDATED) -- Or OVERLAY_NOT_UPDATED if no drawing was done.
|
|
end
|
|
|
|
function onShutdown()
|
|
-- Called when the user exits your game. Free loaded resources here.
|
|
end
|
|
|
|
function onSoundCompleted(id)
|
|
-- The sound "id" just finished playing.
|
|
end
|
|
|
|
-- Note: There is no "onStartup" event.
|
|
-- Any startup code you need can be placed here.
|
|
----
|
|
|
|
[#threaded]
|
|
==== Threaded
|
|
|
|
As of Singe 2.10, you can use the classic procedural programming model. To
|
|
use this model, declare a `singeMain()` function and include the Singe
|
|
framework at the *end* of your program rather than at the beginning. Lua is
|
|
not truly multithreaded, so this model relies on the game developer to
|
|
cooperatively multitask by calling `singeYield()` anywhere code consumes a
|
|
substantial amount of time. As with the event-driven model, Singe still
|
|
manages all input, video decoding, and audio decoding. A sample threaded
|
|
program looks like this:
|
|
|
|
[source,lua]
|
|
----
|
|
function singeMain()
|
|
local x = 10
|
|
local y = 10
|
|
|
|
while true do
|
|
colorBackground(0, 0, 0, 255)
|
|
overlayClear()
|
|
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayPrint(x, y, "+")
|
|
|
|
if keyboardGetLastUp() == SCANCODE.LEFT.value then x = x - 1 end
|
|
if keyboardGetLastUp() == SCANCODE.RIGHT.value then x = x + 1 end
|
|
if keyboardGetLastUp() == SCANCODE.UP.value then y = y - 1 end
|
|
if keyboardGetLastUp() == SCANCODE.DOWN.value then y = y + 1 end
|
|
|
|
singeYield()
|
|
end
|
|
end
|
|
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
==== Hybrid
|
|
|
|
The two models combine freely. When `singeMain` exists, `Framework.singe`
|
|
installs its own `onOverlayUpdate`, but every other callback still fires:
|
|
define `onInputPressed`, `onSoundCompleted`, or `onShutdown` next to
|
|
`singeMain` and they are called between the coroutine's yields. A typical
|
|
split keeps the game's flow procedural while reacting to input and audio
|
|
events:
|
|
|
|
[source,lua]
|
|
----
|
|
local fired = false
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
fired = true
|
|
end
|
|
end
|
|
|
|
function onSoundCompleted(channel)
|
|
if channel == gunshotChannel then
|
|
soundPlay(reloadSound)
|
|
end
|
|
end
|
|
|
|
function singeMain()
|
|
while true do
|
|
if fired then
|
|
fired = false
|
|
gunshotChannel = soundPlay(gunshotSound)
|
|
end
|
|
singeYield()
|
|
end
|
|
end
|
|
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
Do not define `onOverlayUpdate` yourself in this model; draw from inside
|
|
`singeMain` instead. An error raised inside `singeMain` ends the game with a
|
|
traceback, and returning from `singeMain` quits cleanly.
|
|
|
|
[#pausing]
|
|
=== Pausing
|
|
|
|
Every game gets a working pause for free. While the pause key is enabled
|
|
(the default), pressing the key mapped to `INPUT_PAUSE` makes the engine
|
|
pause the disc, every loaded video, and every sound, draw a PAUSED
|
|
indicator over the last frame, and stop running the script: no
|
|
`onOverlayUpdate`, no `singeMain` resumption, no input, mouse, controller,
|
|
or sound callbacks. Timers built on Lua libraries stop with it because
|
|
nothing runs them. Pressing the key again resumes everything that was
|
|
playing and the script continues where it left off. Only the engine's own
|
|
switches (pause, quit, screenshot, grab) still work while frozen.
|
|
|
|
Inputs are kept truthful across the freeze. Anything the script believes is
|
|
held down is released with `onInputReleased` (or `onKeyReleased` in
|
|
`MODE_FULL`) when the pause starts, and whatever is still physically held
|
|
when the pause ends is pressed again. A joystick held through a pause never
|
|
sticks.
|
|
|
|
A game that wants its own pause behavior (a pause menu, a story freeze that
|
|
the player may not skip) calls `singeSetPauseKeyEnabled(false)`. The key is
|
|
then delivered to the script as `SWITCH_PAUSE` and the game drives
|
|
`singeSetPauseFlag` itself, which pauses the media without freezing the
|
|
script.
|
|
|
|
In `MODE_FULL` the keyboard belongs entirely to the game, so keys mapped to
|
|
the engine's switches do nothing there (a `p` typed into a high score entry
|
|
must stay a `p`). Gamepad and mouse buttons mapped to those switches keep
|
|
working in either mode, since they cannot be typed.
|
|
|
|
=== games.dat
|
|
|
|
The `games.dat` file allows Singe to automatically locate new games when they
|
|
are installed by the end user. This file is extremely important and must be
|
|
included with every Singe game. Place `games.dat` in the top-most directory
|
|
of your game. An example containing multiple games:
|
|
|
|
[source,lua]
|
|
----
|
|
GAMES = {
|
|
{
|
|
TITLE = ".38 Ambush Alley",
|
|
SCRIPT = "ActionMax/38AmbushAlley.singe",
|
|
VIDEO = "ActionMax/frame_38AmbushAlley.txt",
|
|
DATA = "ActionMax",
|
|
STRETCH = false,
|
|
NO_MOUSE = false,
|
|
RESOLUTION_X = 720,
|
|
RESOLUTION_Y = 480,
|
|
SINDEN_GUN = "",
|
|
CABINET = "ActionMax/cabinet_38AmbushAlley.png",
|
|
MARQUEE = "ActionMax/marquee_ActionMax.png",
|
|
ATTRACT = "ActionMax/video_38AmbushAlley.mkv",
|
|
ATTRACT_START = 3000,
|
|
ATTRACT_END = 3500,
|
|
YEAR = 1987,
|
|
PLATFORM = "ActionMax",
|
|
DEVELOPER = "Sourcing International, Ltd.",
|
|
PUBLISHER = "Worlds of Wonder, Inc.",
|
|
GENRE = "Shooter",
|
|
DESCRIPTION = "Get your target practice in with real police officers then hit the streets.",
|
|
CREATOR = "Scott Duensing",
|
|
SOURCE = "http://kangaroopunch.com"
|
|
},
|
|
-- ... additional entries ...
|
|
}
|
|
----
|
|
|
|
The keys `SCRIPT`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`,
|
|
`RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and `LEGACY_SPRITE_ARGS` are
|
|
read by the engine when the menu (or your own script, through
|
|
`scriptExecute` / `scriptPush`) launches the entry; they override the
|
|
command line. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10
|
|
with the old sprite argument order (see <<migrating,Migrating from Singe
|
|
2.10>>). The remaining keys are read by the menu for display.
|
|
|
|
[#migrating]
|
|
=== Migrating from Singe 2.10
|
|
|
|
Singe 2.20 moved the sprite handle to the first argument of `spriteDraw`,
|
|
`spriteLoop`, `spriteQuality`, `spriteRotate`, `spriteRotateAndScale`,
|
|
`spriteScale`, and `spriteSetFrame`, so every sprite call now matches the
|
|
`video*` family. To update a game, move the last argument of each of those
|
|
calls to the front:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(x, y, cursor) -- 2.10
|
|
spriteDraw(cursor, x, y) -- 2.20
|
|
spriteRotate(angle, cursor) -- 2.10
|
|
spriteRotate(cursor, angle) -- 2.20
|
|
----
|
|
|
|
A game you cannot edit can opt into the old order instead. Either set the
|
|
global before loading the framework:
|
|
|
|
[source,lua]
|
|
----
|
|
SINGE_LEGACY_SPRITE_ARGS = true
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
or add `LEGACY_SPRITE_ARGS = true` to its `games.dat` entry. Nothing else
|
|
changed shape, but a few behaviors did:
|
|
|
|
* Handles and counts are returned as Lua integers; `videoIsPlaying` returns
|
|
a boolean; `mouseSetMode` returns nothing.
|
|
* Errors inside callbacks now end the game with a traceback instead of being
|
|
printed and ignored, matching how argument errors have always behaved.
|
|
* Held keys no longer repeat in `MODE_NORMAL`.
|
|
* The disc is parked on frame 1 by the engine before your script runs;
|
|
`Framework.singe` no longer seeks when it is loaded.
|
|
* `onMouseMoved` in `MANY_MOUSE` mode passes real movement in its third and
|
|
fourth arguments, and `onControllerMoved` passes the controller index
|
|
(`0` to `3`) instead of SDL's instance ID.
|
|
* `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` /
|
|
`singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and
|
|
`singeSetPauseKeyEnabled`.
|
|
* The pause key now pauses the whole game, not just the media: the script
|
|
is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered
|
|
to the script only when the key has been disabled. It acts on the key
|
|
press rather than the release. See <<pausing,Pausing>>.
|
|
|
|
=== Engine Constants
|
|
|
|
The engine defines these globals before any script runs, so they are
|
|
available to `controls.cfg` and to `Framework.singe` alike:
|
|
|
|
[cols="2,3",options="header"]
|
|
|===
|
|
| Constant | Meaning
|
|
| `SWITCH_UP` ... `SWITCH_GRAB` | Values passed to `onInputPressed` / `onInputReleased` in `MODE_NORMAL`.
|
|
| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Arguments for `fontQuality`.
|
|
| `MODE_NORMAL`, `MODE_FULL` | Arguments for `keyboardSetMode`.
|
|
| `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`.
|
|
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`.
|
|
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`.
|
|
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED` | Return values of `discGetState`.
|
|
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free.
|
|
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v2.20"`), and as the number `singeVersion()` returns.
|
|
| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`.
|
|
| `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order.
|
|
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them.
|
|
|===
|
|
|
|
`Framework.singe` adds the `SCANCODE` and `MODIFIER` tables (SDL's key and
|
|
modifier values), `GAMEPAD_0` to `GAMEPAD_3`, `MOUSE_0` to `MOUSE_3`, the
|
|
`GAMEPAD_AXIS_*` indexes for `controllerGetAxis`, and `DIR`.
|
|
|
|
=== Included Libraries
|
|
|
|
In addition to the standard Lua libraries and the Singe API, the following
|
|
libraries are also available for use in Singe programs without the need to
|
|
install any additional software:
|
|
|
|
[cols="1,3",options="header"]
|
|
|===
|
|
| Library | Purpose
|
|
| Copas | Asynchronous networking
|
|
| LuaFileSystem | Expanded filesystem support
|
|
| json.lua | JavaScript Object Notation encoding and decoding
|
|
| binaryheap | Binary heap data structure
|
|
| timerwheel | Efficient timer scheduling
|
|
| LuaSec | TLS / SSL communication
|
|
| LuaSocket | TCP and UDP
|
|
| LuaRS232 | RS232 serial port access
|
|
|===
|
|
|
|
Their usage is beyond the scope of this document.
|
|
|
|
=== Video, Audio, and Container Formats
|
|
|
|
Singe decodes video with FFmpeg through FFMS2, so any container and codec the
|
|
bundled FFmpeg can demux and decode will play: MP4, MKV, MPEG program streams,
|
|
AVI, and the classic Daphne `.m2v` elementary streams with a matching `.ogg`
|
|
audio file next to them. Every audio track in the file is available to
|
|
`discSetAudioTrack` / `videoSetAudioTrack`; all tracks must share one sample
|
|
format, channel count, and rate.
|
|
|
|
The first time a video is opened, Singe indexes it and stores the index next
|
|
to the game's other data (`<name>.index`). Indexing takes a while for large
|
|
files and happens again if the video changes.
|
|
|
|
For laserdisc footage the constraints are frame accuracy and seek speed, not
|
|
compression. H.264 in MP4 or MKV with a short keyframe interval (one or two
|
|
seconds) seeks quickly and plays on every supported platform; long keyframe
|
|
intervals make `discSearch` visibly slow because the decoder must walk from
|
|
the previous keyframe. Standard definition (720x480 or 720x576) is the sweet
|
|
spot. High definition sources work but cost proportionally more CPU on
|
|
Raspberry Pi class hardware, and the overlay defaults to half the video
|
|
resolution, so oversized video buys little.
|
|
|
|
Sound effects go through SDL_mixer: WAV, OGG, FLAC, MP3, Opus, WavPack, and
|
|
tracker modules are supported. Short uncompressed WAV files give the lowest
|
|
latency.
|
|
|
|
== Lua API Reference
|
|
|
|
This chapter documents the Lua API exposed by the Singe engine -- every
|
|
function your game script can call, and every callback Singe will invoke on
|
|
you. It is aimed at developers writing new games or porting video-heavy games
|
|
from other engines.
|
|
|
|
[#conventions]
|
|
=== Conventions
|
|
|
|
Every API entry below follows the same structure:
|
|
|
|
* *Signature(s)* in a code fence, showing every accepted form.
|
|
* *Description* of what the function does and any side effects.
|
|
* *Parameters* listed inline, or as a table when there are more than two.
|
|
* *Returns* line describing the value pushed back to Lua (omitted when the function returns nothing).
|
|
* *Notes*, if there are gotchas worth flagging.
|
|
* *Since* -- the Singe version the function first appeared in.
|
|
* *See also* -- related functions you will likely use in the same call site.
|
|
* *Example* -- a short, realistic snippet in the shape of real game code.
|
|
|
|
A few rules apply across the whole API:
|
|
|
|
* *Colors* are 8-bit per channel: red, green, blue, and opacity are each integers from `0` to `255`.
|
|
* *Overlay coordinates* are in the current overlay resolution (set by `overlaySetResolution`), not screen pixels. The default overlay resolution is half the video resolution in each dimension; call `overlaySetResolution` to change it.
|
|
* *Handles* returned by `*Load` functions (sprites, sounds, fonts, videos) are opaque integers. Pass them back unchanged to the other functions in that namespace. Do not do math on them.
|
|
* *Invalid arguments abort the script.* If a function is called with the wrong number of arguments, wrong types, or a handle that has already been unloaded, Singe terminates the running script with an error -- it does not return `nil` or a failure code. Validate user input in your script before handing it to the API.
|
|
* *Drawing must happen from `onOverlayUpdate`.* Calls to any `overlay*`, `spriteDraw`, `videoDraw`, or `fontPrint` function from other callbacks will not be reflected on screen and may corrupt the overlay. This is the one rule that catches every new Singe developer.
|
|
|
|
[#color]
|
|
=== Color
|
|
|
|
The foreground and background colors are *engine globals*, not per-operation parameters. Set them once with `colorForeground` / `colorBackground`, then every subsequent drawing call (`overlayPrint`, `overlayBox`, `overlayLine`, etc.) uses those colors until you change them again.
|
|
|
|
[#colorbackground]
|
|
==== colorBackground
|
|
|
|
[source,text]
|
|
----
|
|
colorBackground(r, g, b)
|
|
colorBackground(r, g, b, a)
|
|
----
|
|
|
|
Sets the background color used by subsequent drawing operations (primarily `overlayPrint` when it fills the text background).
|
|
|
|
* `r`, `g`, `b` -- red, green, blue channels, `0` to `255`.
|
|
* `a` -- optional opacity, `0` to `255`. Defaults to `0` (fully transparent) so that text drawn with `overlayPrint` shows through to whatever is behind it.
|
|
|
|
The three-argument form exists for compatibility with pre-2.00 scripts and produces the same result as passing `a = 0`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<colorforeground,colorForeground>>, <<overlayprint,overlayPrint>> *(documented in the Overlay section)*
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw a status line with a solid black background behind the text.
|
|
function onOverlayUpdate()
|
|
colorBackground(0, 0, 0, 255) -- Opaque black.
|
|
colorForeground(255, 255, 0, 255) -- Yellow text.
|
|
overlayPrint(8, 8, "Score: " .. score)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#colorforeground]
|
|
==== colorForeground
|
|
|
|
[source,text]
|
|
----
|
|
colorForeground(r, g, b)
|
|
colorForeground(r, g, b, a)
|
|
----
|
|
|
|
Sets the foreground color used by subsequent drawing operations -- text rendered by `overlayPrint` and `fontPrint`, all primitives in the `overlay*` family, and the tint used by `fontToSprite`.
|
|
|
|
* `r`, `g`, `b` -- red, green, blue channels, `0` to `255`.
|
|
* `a` -- optional opacity, `0` to `255`. Defaults to `255` (fully opaque). This is the opposite default from `colorBackground`, because foreground drawing almost always wants to be visible.
|
|
|
|
The three-argument form exists for compatibility with pre-2.00 scripts and produces the same result as passing `a = 255`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<colorbackground,colorBackground>>, <<overlaybox,overlayBox>>, <<overlayline,overlayLine>>, <<overlayprint,overlayPrint>> *(all documented in the Overlay section)*
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Flash the crosshair red when the player is hit, white otherwise.
|
|
function onOverlayUpdate()
|
|
if hitTimer > 0 then
|
|
colorForeground(255, 0, 0, 255)
|
|
hitTimer = hitTimer - 1
|
|
else
|
|
colorForeground(255, 255, 255, 255)
|
|
end
|
|
overlayCircle(crosshairX, crosshairY, 8)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#controller]
|
|
=== Controller
|
|
|
|
Singe supports up to *four* simultaneous game controllers (indices `0` through `3`). Controllers are detected automatically at startup; there is no `controllerLoad`. Inputs reach your game in two ways:
|
|
|
|
* *Analog axes* (sticks and triggers) -- reported in real time through the `onControllerMoved` callback *or* polled on demand with `controllerGetAxis`. Values range from `-32768` to `32767`.
|
|
* *Digital buttons* (A/B/X/Y, shoulders, D-pad, etc.) -- reach your game through the generic input system only when they are mapped to a switch in `controls.cfg`. Map a button code from the `GAMEPAD_0` / `GAMEPAD_1` / `GAMEPAD_2` / `GAMEPAD_3` tables (for example `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`) and `onInputPressed` / `onInputReleased` then receive the matching `SWITCH_*` value in keyboard `MODE_NORMAL`. Unmapped buttons are ignored in `MODE_NORMAL`; in `MODE_FULL` the raw button code arrives as the `scancode` argument of `onKeyPressed` / `onKeyReleased`. `controllerGetButton` is for polling the live state of a specific button.
|
|
|
|
The global `SINGE_DEAD_ZONE` (set from the `DEAD_ZONE` entry in `controls.cfg`) is the recommended threshold below which you should treat axis motion as noise.
|
|
|
|
[#controllergetaxis]
|
|
==== controllerGetAxis
|
|
|
|
[source,text]
|
|
----
|
|
value = controllerGetAxis(controller, axis)
|
|
----
|
|
|
|
Polls the current position of an analog axis on a controller.
|
|
|
|
* `controller` -- controller index, `0` through `3`.
|
|
* `axis` -- axis index. Use the named constants: `GAMEPAD_AXIS_LEFT_X`, `GAMEPAD_AXIS_LEFT_Y`, `GAMEPAD_AXIS_RIGHT_X`, `GAMEPAD_AXIS_RIGHT_Y`, `GAMEPAD_AXIS_LEFT_TRIGGER`, `GAMEPAD_AXIS_RIGHT_TRIGGER`.
|
|
|
|
*Returns:* integer from `-32768` to `32767`. Triggers use `0` to `32767` (they are one-sided).
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<controllergetbutton,controllerGetButton>>, the `onControllerMoved` callback
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Steer left/right based on the left analog stick, ignoring the dead zone.
|
|
function onOverlayUpdate()
|
|
local x = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
|
|
if math.abs(x) > SINGE_DEAD_ZONE then
|
|
playerX = playerX + (x / 32768) * steerSpeed
|
|
end
|
|
drawPlayer()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#controllergetbutton]
|
|
==== controllerGetButton
|
|
|
|
[source,text]
|
|
----
|
|
down = controllerGetButton(controller, button)
|
|
----
|
|
|
|
Polls the current state of a digital button on a controller. Use this when you need to know *right now* whether a button is held, rather than waiting for an `onInputPressed` event.
|
|
|
|
* `controller` -- controller index, `0` through `3`.
|
|
* `button` -- one of the `BUTTON_*` values from the `GAMEPAD_N` table for that controller (e.g. `GAMEPAD_0.BUTTON_A.value`, `GAMEPAD_0.DPAD_UP.value`).
|
|
|
|
*Returns:* boolean. `true` while the button is held.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<controllergetaxis,controllerGetAxis>>, the `onInputPressed` callback
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Charge a shot while the A button is held, fire when released.
|
|
function onOverlayUpdate()
|
|
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
|
|
chargeLevel = math.min(chargeLevel + 1, MAX_CHARGE)
|
|
elseif chargeLevel > 0 then
|
|
fireShot(chargeLevel)
|
|
chargeLevel = 0
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#debug]
|
|
=== Debug
|
|
|
|
[#debugprint]
|
|
==== debugPrint
|
|
|
|
[source,text]
|
|
----
|
|
debugPrint(message)
|
|
----
|
|
|
|
Writes a string to Singe's trace log (stdout and the trace file when tracing is enabled with `-t`/`--trace`). Useful for step-printf debugging when the ZeroBrane Studio integration is not available.
|
|
|
|
* `message` -- any string.
|
|
|
|
*Since:* 1.x
|
|
*See also:* command-line options `-t` (script trace) and `-p` (engine trace) in the manual
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Check why the player isn't taking damage.
|
|
debugPrint("hit check: player=" .. tostring(playerX) .. "," .. tostring(playerY) ..
|
|
" enemy=" .. tostring(enemyX) .. "," .. tostring(enemyY))
|
|
----
|
|
|
|
[#disc]
|
|
=== Disc
|
|
|
|
The *disc* is the main laserdisc video -- the one Singe was originally built to emulate. A Singe game always has exactly one disc, loaded at startup from the video file declared in `games.dat` (or the command line).
|
|
|
|
All `disc*` functions operate on this single, implicit disc. They exist in addition to the more general <<video,`video*`>> family, which handles additional video assets you load yourself. Use `disc*` for the main gameplay video; use `video*` for extra clips layered on top.
|
|
|
|
A handful of `disc*` functions are *unimplemented no-ops* retained for script-level compatibility with very old Singe games. They are listed in <<unimplementedandlegacy,Unimplemented and Legacy>>.
|
|
|
|
[#discaudio]
|
|
==== discAudio
|
|
|
|
[source,text]
|
|
----
|
|
discAudio(channel, onOff)
|
|
----
|
|
|
|
Toggles one audio channel of the disc's soundtrack on or off. Often used to mute the left or right channel when a game originally used one channel for audio and the other for a data track.
|
|
|
|
* `channel` -- `1` for left, `2` for right.
|
|
* `onOff` -- boolean. `true` restores the channel to the configured VLDP volume; `false` silences it.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsetaudiotrack,discSetAudioTrack>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Some titles bake narration into the right channel only.
|
|
discAudio(1, false) -- Silence the music on the left.
|
|
discAudio(2, true) -- Keep narration on the right.
|
|
----
|
|
|
|
[#discchangespeed]
|
|
==== discChangeSpeed
|
|
|
|
WARNING: *Unimplemented.* This function is a no-op retained for backward compatibility with pre-2.00 scripts.
|
|
|
|
[#discgetaudiotrack]
|
|
==== discGetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
track = discGetAudioTrack()
|
|
----
|
|
|
|
Returns the index of the currently selected audio track for the disc.
|
|
|
|
*Returns:* integer track index.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>
|
|
|
|
[#discgetaudiotracks]
|
|
==== discGetAudioTracks
|
|
|
|
[source,text]
|
|
----
|
|
count = discGetAudioTracks()
|
|
----
|
|
|
|
Returns how many audio tracks the disc's video file contains. Multiple tracks are typically alternate language dubs.
|
|
|
|
*Returns:* integer count (>= 1).
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Build a language-select menu from the disc's audio tracks.
|
|
for i = 0, discGetAudioTracks() - 1 do
|
|
local code = discGetLanguage(i)
|
|
local name = discGetLanguageDescription(code)
|
|
table.insert(languages, { track = i, label = name })
|
|
end
|
|
----
|
|
|
|
[#discgetframe]
|
|
==== discGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = discGetFrame()
|
|
----
|
|
|
|
Returns the current frame number on the disc. This is the fundamental time reference for laserdisc games -- almost every interactive decision hinges on "what frame are we on right now?"
|
|
|
|
*Returns:* integer. `0` when the disc is stopped.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsearch,discSearch>>, <<discpauseatframe,discPauseAtFrame>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Trigger a prompt at the moment the player must duck.
|
|
function onOverlayUpdate()
|
|
local f = discGetFrame()
|
|
if f >= 2847 and f <= 2882 and not promptShown then
|
|
showPrompt("DUCK!")
|
|
promptShown = true
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#discgetheight]
|
|
==== discGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = discGetHeight()
|
|
----
|
|
|
|
Returns the height of the disc's video in pixels. Useful if you want to build an overlay resolution that matches the native video.
|
|
|
|
*Returns:* integer pixel height.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<discgetwidth,discGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
[#discgetlanguage]
|
|
==== discGetLanguage
|
|
|
|
[source,text]
|
|
----
|
|
code = discGetLanguage(track)
|
|
----
|
|
|
|
Returns the ISO language code (e.g. `"eng"`, `"jpn"`, `"fre"`) for an audio track on the disc, as stored in the video file's metadata.
|
|
|
|
* `track` -- audio track index, `0` through `discGetAudioTracks() - 1`.
|
|
|
|
*Returns:* three-letter language code string, or an empty string if no language tag is set on that track.
|
|
|
|
*Since:* 2.10
|
|
*See also:* `discGetLanguageDescription` (aliased in `Framework.singe` to <<videogetlanguagedescription,videoGetLanguageDescription>>), <<discgetaudiotracks,discGetAudioTracks>>
|
|
|
|
[#discgetstate]
|
|
==== discGetState
|
|
|
|
[source,text]
|
|
----
|
|
state = discGetState()
|
|
----
|
|
|
|
Returns an integer describing the playback state of the disc.
|
|
|
|
*Returns:* one of the following constants (defined by the engine and available to every script):
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Constant | Value | Meaning
|
|
| `DISC_STOPPED` | `2` | Stopped (`discStop` was called)
|
|
| `DISC_PLAYING` | `3` | Playing
|
|
| `DISC_PAUSED` | `4` | Paused
|
|
|===
|
|
|
|
Singe's lightweight player has no distinct searching state; a disc that is seeking reports `DISC_PAUSED`.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discstop,discStop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Don't let the pause key resume from a story-driven freeze.
|
|
if discGetState() == DISC_PAUSED and not storyPaused then
|
|
discPlay()
|
|
end
|
|
----
|
|
|
|
[#discgetwidth]
|
|
==== discGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = discGetWidth()
|
|
----
|
|
|
|
Returns the width of the disc's video in pixels.
|
|
|
|
*Returns:* integer pixel width.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<discgetheight,discGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
[#discpause]
|
|
==== discPause
|
|
|
|
[source,text]
|
|
----
|
|
discPause()
|
|
----
|
|
|
|
Pauses playback on the current frame. A paused disc keeps displaying the frame it stopped on. Has no effect if the disc is already stopped (see `discStop` for the difference).
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discplay,discPlay>>, <<discstop,discStop>>, <<singesetpauseflag,singeSetPauseFlag>>
|
|
|
|
[#discpauseatframe]
|
|
==== discPauseAtFrame
|
|
|
|
[source,text]
|
|
----
|
|
discPauseAtFrame(frame)
|
|
----
|
|
|
|
Seeks to the specified frame and pauses on it. In the current engine this is *identical to `discSearch`* -- the two names exist for historical reasons. Prefer `discSearch` in new code; keep `discPauseAtFrame` if you are porting old scripts.
|
|
|
|
* `frame` -- target frame number.
|
|
|
|
*Since:* 1.18
|
|
*See also:* <<discsearch,discSearch>>
|
|
|
|
[#discplay]
|
|
==== discPlay
|
|
|
|
[source,text]
|
|
----
|
|
discPlay()
|
|
----
|
|
|
|
Starts or resumes playback from the current frame. If the disc was stopped (via `discStop`), playback restarts and the disc is marked running again.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discpause,discPause>>, <<discstop,discStop>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Standard attract-mode loop.
|
|
function attractLoop()
|
|
discSearch(attractStart) -- Seek and pause on the first frame.
|
|
discPlay() -- Start rolling.
|
|
end
|
|
----
|
|
|
|
[#discsearch]
|
|
==== discSearch
|
|
|
|
[source,text]
|
|
----
|
|
discSearch(frame)
|
|
----
|
|
|
|
Seeks the disc to the specified frame and pauses on it. The disc will display that frame until you call `discPlay`, `discSkipToFrame`, or another transport function. This is the workhorse frame-positioning call for laserdisc games -- every branch point in an interactive video game is some variation of "seek to the next scene, show me the frame, wait for my decision."
|
|
|
|
* `frame` -- target frame number.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discskiptoframe,discSkipToFrame>>, <<discpauseatframe,discPauseAtFrame>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Player picked "go left" -- cue the left-corridor scene and wait.
|
|
function goLeft()
|
|
discSearch(sceneLeftStartFrame)
|
|
waitingForPlayer = true
|
|
end
|
|
----
|
|
|
|
[#discsearchblanking]
|
|
==== discSearchBlanking
|
|
|
|
WARNING: *Unimplemented.* No-op retained for backward compatibility with pre-2.00 scripts. On real laserdisc hardware this controlled whether the screen blanked during a seek; Singe's software player does not need this.
|
|
|
|
[#discsetaudiotrack]
|
|
==== discSetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
discSetAudioTrack(track)
|
|
----
|
|
|
|
Switches the active audio track on the disc. Use this to implement a language-select menu.
|
|
|
|
* `track` -- audio track index, `0` through `discGetAudioTracks() - 1`. Out-of-range values terminate the script.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<discgetaudiotracks,discGetAudioTracks>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
[#discsetfps]
|
|
==== discSetFPS
|
|
|
|
WARNING: *Unimplemented.* No-op retained for backward compatibility. `Framework.singe` still calls `discSetFPS(29.97)` at startup for the benefit of old scripts that read the framerate elsewhere. The actual framerate comes from the video file.
|
|
|
|
[#discskipbackward]
|
|
==== discSkipBackward
|
|
|
|
[source,text]
|
|
----
|
|
discSkipBackward(delta)
|
|
----
|
|
|
|
Seeks backward by a number of frames from the current position. Does *not* change the play/pause state -- a playing disc keeps playing, a paused disc stays paused on the new frame.
|
|
|
|
* `delta` -- number of frames to subtract from the current frame. Must be positive.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discskipforward,discSkipForward>>, <<discstepbackward,discStepBackward>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Rewind 30 frames (about one second at 30fps) when the player misses a prompt.
|
|
discSkipBackward(30)
|
|
----
|
|
|
|
[#discskipblanking]
|
|
==== discSkipBlanking
|
|
|
|
WARNING: *Unimplemented.* No-op retained for backward compatibility with pre-2.00 scripts.
|
|
|
|
[#discskipforward]
|
|
==== discSkipForward
|
|
|
|
[source,text]
|
|
----
|
|
discSkipForward(delta)
|
|
----
|
|
|
|
Seeks forward by a number of frames from the current position. Does not change the play/pause state. Mirror of `discSkipBackward`.
|
|
|
|
* `delta` -- number of frames to add to the current frame.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discskipbackward,discSkipBackward>>, <<discstepforward,discStepForward>>
|
|
|
|
[#discskiptoframe]
|
|
==== discSkipToFrame
|
|
|
|
[source,text]
|
|
----
|
|
discSkipToFrame(frame)
|
|
----
|
|
|
|
Seeks to a specific frame and *starts playing* from that frame. Contrast with `discSearch`, which seeks and pauses. Use `discSkipToFrame` when you want continuous video playback from a new location.
|
|
|
|
* `frame` -- target frame number.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discsearch,discSearch>>, <<discplay,discPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Skip the intro on a replay.
|
|
if hasPlayedBefore then
|
|
discSkipToFrame(gameplayStart)
|
|
else
|
|
discSkipToFrame(introStart)
|
|
end
|
|
----
|
|
|
|
[#discstepbackward]
|
|
==== discStepBackward
|
|
|
|
[source,text]
|
|
----
|
|
discStepBackward()
|
|
----
|
|
|
|
Moves the disc back by exactly one frame. Always pauses on the new frame, regardless of previous play state. Intended for frame-accurate debugging and service-mode screens.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>
|
|
|
|
[#discstepforward]
|
|
==== discStepForward
|
|
|
|
[source,text]
|
|
----
|
|
discStepForward()
|
|
----
|
|
|
|
Moves the disc forward by exactly one frame. Always pauses on the new frame, regardless of previous play state. Mirror of `discStepBackward`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discstepbackward,discStepBackward>>
|
|
|
|
[#discstop]
|
|
==== discStop
|
|
|
|
[source,text]
|
|
----
|
|
discStop()
|
|
----
|
|
|
|
Stops the disc. Stopped is a stronger state than paused: the display is marked for refresh, and `discGetState` returns `2` rather than `4`. Transport calls that require a running disc (like `discSkipForward` and `discSkipBackward`) will fail silently while stopped. Resume with `discPlay` or any `discSearch` / `discSkipToFrame`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
|
|
|
|
[#font]
|
|
=== Font
|
|
|
|
Singe has *two* text rendering systems. Know which one you're using:
|
|
|
|
* *Console font* -- a fixed 8-bit bitmap font baked into the engine. Accessed through <<overlayprint,`overlayPrint`>>. Fast, predictable, grid-aligned, and the only way to print text without loading any assets.
|
|
* *TrueType fonts* -- arbitrary `.ttf` files you load at runtime with `fontLoad`, then render with `fontPrint` (direct to overlay) or `fontToSprite` (to a reusable sprite). Scalable, anti-aliased, colorable, but require managing the lifecycle yourself.
|
|
|
|
The TrueType system keeps a global *currently selected font*. `fontLoad` selects the font it just loaded; to switch between several loaded fonts, use `fontSelect`.
|
|
|
|
Three render qualities are available, set with `fontQuality`:
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Constant | Value | Appearance
|
|
| `FONT_QUALITY_SOLID` | `1` | Fastest. 1-bit alpha, jagged edges. Use for dev text.
|
|
| `FONT_QUALITY_SHADED` | `2` | Anti-aliased text on a solid rectangle of `colorBackground`. Good for UI panels.
|
|
| `FONT_QUALITY_BLENDED` | `3` | Smooth anti-aliased text with per-pixel alpha -- blends cleanly over any background. Highest quality, most common choice.
|
|
|===
|
|
|
|
[#fontload]
|
|
==== fontLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = fontLoad(filename, pointSize)
|
|
----
|
|
|
|
Loads a TrueType font at the given point size and selects it as the current font. Different point sizes of the same typeface must be loaded as separate fonts -- `pointSize` is baked into the loaded handle.
|
|
|
|
* `filename` -- path to a `.ttf` file. Prepend `DIR` for files shipped with your game.
|
|
* `pointSize` -- integer point size. Typical values range from `12` (small UI text) to `72` (large title text).
|
|
|
|
*Returns:* integer font handle.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontselect,fontSelect>>, <<fontunload,fontUnload>>, <<fontprint,fontPrint>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Load two sizes of the same face: big for the title, small for scores.
|
|
titleFont = fontLoad(DIR .. "fonts/BreatheFire.ttf", 48)
|
|
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 14)
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
----
|
|
|
|
[#fontprint]
|
|
==== fontPrint
|
|
|
|
[source,text]
|
|
----
|
|
fontPrint(x, y, text)
|
|
----
|
|
|
|
Renders a string onto the overlay using the currently selected TrueType font, the current foreground color, and (for `FONT_QUALITY_SHADED`) the current background color. Must be called from `onOverlayUpdate`.
|
|
|
|
* `x`, `y` -- overlay coordinates of the top-left corner of the rendered text.
|
|
* `text` -- the string to render.
|
|
|
|
`fontPrint` creates a fresh text surface on every call and frees it after blitting. For text that changes only occasionally (a score, a status line, menu labels), `fontToSprite` is significantly faster since it caches the surface.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fonttosprite,fontToSprite>>, <<fontselect,fontSelect>>, <<overlayprint,overlayPrint>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Render a live-updating score with the currently selected font.
|
|
function onOverlayUpdate()
|
|
colorForeground(255, 255, 255, 255)
|
|
fontPrint(20, 20, "SCORE: " .. score)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#fontquality]
|
|
==== fontQuality
|
|
|
|
[source,text]
|
|
----
|
|
fontQuality(mode)
|
|
----
|
|
|
|
Sets the render quality used by subsequent `fontPrint` and `fontToSprite` calls. This is a *global setting*, not per-font.
|
|
|
|
* `mode` -- one of `FONT_QUALITY_SOLID` (`1`), `FONT_QUALITY_SHADED` (`2`), or `FONT_QUALITY_BLENDED` (`3`).
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
|
|
|
[#fontselect]
|
|
==== fontSelect
|
|
|
|
[source,text]
|
|
----
|
|
fontSelect(id)
|
|
----
|
|
|
|
Makes a previously loaded font the current one. All subsequent `fontPrint` / `fontToSprite` calls will use this font until you call `fontSelect` again.
|
|
|
|
* `id` -- font handle returned by `fontLoad`.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontload,fontLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
fontSelect(titleFont)
|
|
fontPrint(centerX, 40, "DRAGON'S LAIR")
|
|
|
|
fontSelect(hudFont)
|
|
fontPrint(8, 8, "Score: " .. score)
|
|
----
|
|
|
|
[#fonttosprite]
|
|
==== fontToSprite
|
|
|
|
[source,text]
|
|
----
|
|
id = fontToSprite(text)
|
|
----
|
|
|
|
Renders a string to a new sprite using the currently selected font, quality, and foreground color. Returns a sprite handle that can be used with every <<sprite,sprite>> function (draw, scale, rotate, unload).
|
|
|
|
This is the recommended path for text that doesn't change often -- a menu label, a level name, a scoreboard header. Render once, cache the sprite, draw it repeatedly. For text that changes every frame (a timer, a live score), either re-render with `fontToSprite` and unload the previous sprite, or use `fontPrint` directly.
|
|
|
|
* `text` -- the string to render.
|
|
|
|
*Returns:* integer sprite handle.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontprint,fontPrint>>, <<spritedraw,spriteDraw>>, <<spriteunload,spriteUnload>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Pre-render menu labels once at startup.
|
|
menuLabels = {}
|
|
for i, entry in ipairs(menuEntries) do
|
|
menuLabels[i] = fontToSprite(entry.name)
|
|
end
|
|
|
|
function onOverlayUpdate()
|
|
for i, label in ipairs(menuLabels) do
|
|
spriteDraw(label, menuX, menuY + i * lineHeight)
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#fontunload]
|
|
==== fontUnload
|
|
|
|
[source,text]
|
|
----
|
|
fontUnload(id)
|
|
----
|
|
|
|
Releases a loaded font. After this call the handle is invalid.
|
|
|
|
* `id` -- font handle.
|
|
|
|
If you unload the currently selected font, the engine does not automatically pick another -- do not call `fontPrint` or `fontToSprite` until you have `fontSelect`ed a different loaded font.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<fontload,fontLoad>>
|
|
|
|
[#keyboard]
|
|
=== Keyboard
|
|
|
|
Singe exposes keyboard input in two modes, selected with `keyboardSetMode`:
|
|
|
|
* *`MODE_NORMAL`* (default, value `0`) -- Singe translates raw keys into the high-level "switches" defined in `controls.cfg` (jump, coin, start, etc.). Your script only sees `onInputPressed(switch)` / `onInputReleased(switch)` with the `SWITCH_*` values from `Framework.singe`. Use this for gameplay.
|
|
* *`MODE_FULL`* (value `1`) -- Singe also delivers every physical keypress through `onKeyPressed(keysym, scancode)` / `onKeyReleased(...)`. Use this when you need raw text entry (high score initials, a debug console, typing player names).
|
|
|
|
Either mode, you can poll keyboard state directly with `keyboardIsDown`, `keyboardGetLastDown`, and `keyboardGetLastUp` -- those always work regardless of mode.
|
|
|
|
*Scancodes vs. keysyms:* a *scancode* is the physical key position (the `A` key is always scancode `4`, no matter the layout). A *keysym* is the logical character that key produces (`'a'` on US QWERTY, `'q'` on AZERTY). The `SCANCODE` table in `Framework.singe` gives you named scancodes. `keyboardIsDown` works in scancodes. `onKeyPressed` gives you both.
|
|
|
|
[#keyboardgetlastdown]
|
|
==== keyboardGetLastDown
|
|
|
|
[source,text]
|
|
----
|
|
scancode = keyboardGetLastDown()
|
|
----
|
|
|
|
Returns the SDL scancode of the most recently pressed key (compare against `SCANCODE.*.value`), or `0` if nothing has been pressed since the last read. The value is cleared on every `onOverlayUpdate` pass, making this a one-shot "was a key just pressed?" poll -- ideal for the threaded model.
|
|
|
|
*Returns:* integer scancode.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<keyboardgetlastup,keyboardGetLastUp>>, <<keyboardisdown,keyboardIsDown>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Threaded-model main loop reacts to one keypress per iteration.
|
|
function singeMain()
|
|
while true do
|
|
local k = keyboardGetLastDown()
|
|
if k == SCANCODE.ESCAPE.value then singeQuit() end
|
|
if k == SCANCODE.SPACE.value then jump() end
|
|
singeYield()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#keyboardgetlastup]
|
|
==== keyboardGetLastUp
|
|
|
|
[source,text]
|
|
----
|
|
scancode = keyboardGetLastUp()
|
|
----
|
|
|
|
Returns the SDL scancode of the most recently released key (compare against `SCANCODE.*.value`), or `0` if nothing has been released since the last read. Cleared each frame, like `keyboardGetLastDown`.
|
|
|
|
*Returns:* integer scancode.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<keyboardgetlastdown,keyboardGetLastDown>>
|
|
|
|
[#keyboardgetmode]
|
|
==== keyboardGetMode
|
|
|
|
[source,text]
|
|
----
|
|
mode = keyboardGetMode()
|
|
----
|
|
|
|
Returns the current keyboard mode: `MODE_NORMAL` (`0`) or `MODE_FULL` (`1`).
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<keyboardsetmode,keyboardSetMode>>
|
|
|
|
[#keyboardgetmodifiers]
|
|
==== keyboardGetModifiers
|
|
|
|
[source,text]
|
|
----
|
|
mods = keyboardGetModifiers()
|
|
----
|
|
|
|
Returns a bitmask of the currently held modifier keys. Test bits against values in the `MODIFIER` table -- for example `mods & MODIFIER.SHIFT.value ~= 0` for either shift key.
|
|
|
|
*Returns:* integer bitmask.
|
|
|
|
*Since:* 2.10
|
|
*See also:* the `MODIFIER` table in `Framework.singe`
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Shift+tab opens the debug overlay.
|
|
if keyboardGetLastDown() == SCANCODE.TAB.value then
|
|
if keyboardGetModifiers() & MODIFIER.SHIFT.value ~= 0 then
|
|
debugOverlayVisible = not debugOverlayVisible
|
|
end
|
|
end
|
|
----
|
|
|
|
[#keyboardisdown]
|
|
==== keyboardIsDown
|
|
|
|
[source,text]
|
|
----
|
|
down = keyboardIsDown(scancode)
|
|
----
|
|
|
|
Tests whether a specific key is physically held right now. Unlike the `on*` callbacks, this does not wait for events -- it reads the live keyboard state.
|
|
|
|
* `scancode` -- a value from the `SCANCODE` table (e.g. `SCANCODE.UP.value`). Out-of-range values quietly return `false`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 2.10
|
|
*See also:* the `SCANCODE` table in `Framework.singe`
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Diagonal movement by checking multiple keys each frame.
|
|
local dx, dy = 0, 0
|
|
if keyboardIsDown(SCANCODE.LEFT.value) then dx = -1 end
|
|
if keyboardIsDown(SCANCODE.RIGHT.value) then dx = 1 end
|
|
if keyboardIsDown(SCANCODE.UP.value) then dy = -1 end
|
|
if keyboardIsDown(SCANCODE.DOWN.value) then dy = 1 end
|
|
playerX = playerX + dx * speed
|
|
playerY = playerY + dy * speed
|
|
----
|
|
|
|
[#keyboardsetmode]
|
|
==== keyboardSetMode
|
|
|
|
[source,text]
|
|
----
|
|
keyboardSetMode(mode)
|
|
----
|
|
|
|
Switches the keyboard between the two input models. Most games should call this once at startup and leave it alone.
|
|
|
|
* `mode` -- `MODE_NORMAL` (`0`) for mapped switches only, `MODE_FULL` (`1`) to also receive raw key events.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<keyboardgetmode,keyboardGetMode>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Enter high-score entry, capture raw keys, then restore gameplay mode.
|
|
keyboardSetMode(MODE_FULL)
|
|
collectInitials()
|
|
keyboardSetMode(MODE_NORMAL)
|
|
----
|
|
|
|
[#mouse]
|
|
=== Mouse
|
|
|
|
Singe supports up to *four* simultaneous mice (or lightguns -- Singe treats them identically). One-mouse mode is the default; switch to many-mouse mode only if your game genuinely needs to tell the devices apart (e.g. two-player lightgun cabinets).
|
|
|
|
* Enable / disable event dispatch with `mouseSetEnabled`.
|
|
* Poll position with `mouseGetPosition`. Event-driven games get position updates through `onMouseMoved`.
|
|
* `mouseSetCaptured(true)` grabs the cursor and hides it -- typical for fullscreen gameplay.
|
|
|
|
Digital mouse buttons reach `onInputPressed` (keyboard `MODE_NORMAL`) only when mapped to a switch in `controls.cfg`, using the codes in the `MOUSE_0` / `MOUSE_1` / `MOUSE_2` / `MOUSE_3` tables. The shipped defaults map the left button to `SWITCH_BUTTON3`, the right button to `SWITCH_BUTTON1`, and the middle button to `SWITCH_BUTTON2`.
|
|
|
|
[#mousedisable]
|
|
==== mouseDisable
|
|
|
|
[source,text]
|
|
----
|
|
mouseDisable()
|
|
----
|
|
|
|
Legacy alias for `mouseSetEnabled(false)`, defined in `Framework.singe`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousesetenabled,mouseSetEnabled>>
|
|
|
|
[#mouseenable]
|
|
==== mouseEnable
|
|
|
|
[source,text]
|
|
----
|
|
mouseEnable()
|
|
----
|
|
|
|
Legacy alias for `mouseSetEnabled(true)`, defined in `Framework.singe`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousesetenabled,mouseSetEnabled>>
|
|
|
|
[#mousegetposition]
|
|
==== mouseGetPosition
|
|
|
|
[source,text]
|
|
----
|
|
x, y = mouseGetPosition(index)
|
|
----
|
|
|
|
Returns the current overlay-coordinate position of a mouse. Use this for polling instead of tracking `onMouseMoved` events if that's more convenient for your architecture (especially in the threaded model).
|
|
|
|
* `index` -- mouse index, `0` through `3`. Passing an out-of-range index terminates the script.
|
|
|
|
*Returns:* two values, `x` and `y`, in overlay units.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<mousehowmany,mouseHowMany>>, the `onMouseMoved` callback
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw a crosshair at mouse 0's position every frame.
|
|
function onOverlayUpdate()
|
|
local x, y = mouseGetPosition(0)
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayCircle(x, y, 10)
|
|
overlayPlot(x, y)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#mousehowmany]
|
|
==== mouseHowMany
|
|
|
|
[source,text]
|
|
----
|
|
count = mouseHowMany()
|
|
----
|
|
|
|
Returns the number of mice currently connected. Capped at 4. Useful for deciding whether a second-player cursor should be drawn.
|
|
|
|
*Returns:* integer, `0` to `4`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousesetmode,mouseSetMode>>
|
|
|
|
[#mousesetcaptured]
|
|
==== mouseSetCaptured
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetCaptured(grabbed)
|
|
----
|
|
|
|
Grabs or releases the mouse cursor. When grabbed, the cursor is hidden and confined to the Singe window -- the right default for fullscreen gameplay with a targeting reticle.
|
|
|
|
* `grabbed` -- boolean. `true` captures; `false` releases.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<mousesetenabled,mouseSetEnabled>>
|
|
|
|
[#mousesetenabled]
|
|
==== mouseSetEnabled
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetEnabled(enabled)
|
|
----
|
|
|
|
Turns mouse event dispatch to your script on or off. When off, the cursor still moves on screen; your callbacks just stop being called. Enabling honors the `--nomouse` command-line flag -- if the user launched Singe with `--nomouse`, the mouse stays disabled.
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 2.20
|
|
*See also:* <<mouseenable,mouseEnable>>, <<mousedisable,mouseDisable>>, <<mousesetcaptured,mouseSetCaptured>>
|
|
|
|
[#mousesetmode]
|
|
==== mouseSetMode
|
|
|
|
[source,text]
|
|
----
|
|
ok = mouseSetMode(mode)
|
|
----
|
|
|
|
Switches between single-mouse and many-mouse input modes.
|
|
|
|
* `mode` -- `SINGLE_MOUSE` (`100`) for games that treat all mice as one virtual pointer, or `MANY_MOUSE` (`200`) to distinguish multiple mice. `MOUSE_SINGLE` and `MOUSE_MANY` are accepted as aliases.
|
|
|
|
Switch to `MANY_MOUSE` when you need to tell two lightguns or two mice apart -- typically a two-player cabinet or a cooperative lightgun game. In `SINGLE_MOUSE` mode, `onMouseMoved` reports absolute and relative positions; in `MANY_MOUSE` mode, only relative motion and the source device index are reliable.
|
|
|
|
*Returns:* boolean. `true` if the mode was set successfully.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<mousehowmany,mouseHowMany>>, the `onMouseMoved` callback
|
|
|
|
[#overlay]
|
|
=== Overlay
|
|
|
|
The overlay is a transparent 32-bit RGBA surface composited over the disc/video every frame. Everything your script draws -- shapes, text, sprites, videos -- goes onto the overlay, not directly to the screen.
|
|
|
|
Overlay coordinates are *not* screen pixels. They are in the resolution set by `overlaySetResolution`, which the engine scales to the window size at display time. By convention most games set the overlay to match the disc's native resolution.
|
|
|
|
All drawing must happen inside `onOverlayUpdate`. Calls from other callbacks will not be reflected on screen and may corrupt the overlay surface.
|
|
|
|
Two things that behave differently from the rest of the overlay API:
|
|
|
|
* *`overlayBox` draws outlines only.* There is no filled-rectangle primitive. Build filled regions out of sprites or pre-rendered images.
|
|
* *`overlayPrint` uses character-cell coordinates*, not overlay units. The `x` and `y` arguments are multiplied by the built-in console font's cell size internally -- so `overlayPrint(0, 0, "HI")` prints at the top-left in character cell `(0, 0)`, not pixel `(0, 0)`. Every other overlay function uses pixel coordinates.
|
|
|
|
All primitives draw in the current <<colorforeground,foreground color>>. `overlayClear` fills with the current <<colorbackground,background color>>.
|
|
|
|
[#overlaybox]
|
|
==== overlayBox
|
|
|
|
[source,text]
|
|
----
|
|
overlayBox(x1, y1, x2, y2)
|
|
----
|
|
|
|
Draws the outline of an axis-aligned rectangle. *Outline only -- there is no fill.*
|
|
|
|
* `x1`, `y1`, `x2`, `y2` -- corners of the rectangle in overlay units.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>
|
|
|
|
[#overlaycircle]
|
|
==== overlayCircle
|
|
|
|
[source,text]
|
|
----
|
|
overlayCircle(x, y, radius)
|
|
----
|
|
|
|
Draws the outline of a circle using the midpoint algorithm.
|
|
|
|
* `x`, `y` -- center of the circle in overlay units.
|
|
* `radius` -- radius in overlay units.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayellipse,overlayEllipse>>, <<overlayplot,overlayPlot>>
|
|
|
|
[#overlayclear]
|
|
==== overlayClear
|
|
|
|
[source,text]
|
|
----
|
|
overlayClear()
|
|
----
|
|
|
|
Fills the entire overlay with the current background color (`colorBackground`). Typically called first in `onOverlayUpdate` to wipe last frame's drawing -- unless your game composites over a dynamic video and prefers to redraw only dirty regions.
|
|
|
|
Note: because `colorBackground` defaults to transparent (`a = 0`), `overlayClear()` without first setting a solid background is the standard way to erase the overlay back to "invisible."
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<colorbackground,colorBackground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear() -- Erase previous frame (back to transparent).
|
|
drawHUD()
|
|
drawPlayerIcon()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#overlayellipse]
|
|
==== overlayEllipse
|
|
|
|
[source,text]
|
|
----
|
|
overlayEllipse(x1, y1, x2, y2)
|
|
----
|
|
|
|
Draws the outline of an axis-aligned ellipse inscribed in the given bounding rectangle.
|
|
|
|
* `x1`, `y1`, `x2`, `y2` -- corners of the bounding box.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlaycircle,overlayCircle>>
|
|
|
|
[#overlaygetheight]
|
|
==== overlayGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = overlayGetHeight()
|
|
----
|
|
|
|
Returns the height of the overlay surface in overlay units (whatever `overlaySetResolution` set it to).
|
|
|
|
*Returns:* integer height.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<overlaygetwidth,overlayGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
[#overlaygetwidth]
|
|
==== overlayGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = overlayGetWidth()
|
|
----
|
|
|
|
Returns the width of the overlay surface in overlay units.
|
|
|
|
*Returns:* integer width.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<overlaygetheight,overlayGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
[#overlayline]
|
|
==== overlayLine
|
|
|
|
[source,text]
|
|
----
|
|
overlayLine(x1, y1, x2, y2)
|
|
----
|
|
|
|
Draws a line between two points.
|
|
|
|
* `x1`, `y1` -- first endpoint.
|
|
* `x2`, `y2` -- second endpoint.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayplot,overlayPlot>>, <<overlaybox,overlayBox>>
|
|
|
|
[#overlayplot]
|
|
==== overlayPlot
|
|
|
|
[source,text]
|
|
----
|
|
overlayPlot(x, y)
|
|
----
|
|
|
|
Plots a single pixel in the foreground color. Use sparingly -- plotting many individual pixels in Lua is slow; consider a sprite or a line for anything with structure.
|
|
|
|
* `x`, `y` -- pixel coordinates.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlayline,overlayLine>>
|
|
|
|
[#overlayprint]
|
|
==== overlayPrint
|
|
|
|
[source,text]
|
|
----
|
|
overlayPrint(column, row, text)
|
|
----
|
|
|
|
Prints text using the engine's built-in fixed-width console font. *Coordinates are character cells, not pixels* -- `column = 0, row = 0` is the top-left character position, `column = 1` is one font-width to the right. This is different from every other overlay function.
|
|
|
|
* `column`, `row` -- character cell coordinates.
|
|
* `text` -- the string to print. Text that would extend past the right edge of the overlay is truncated silently.
|
|
|
|
Rendered in the current foreground color on a background filled with the current background color (so `colorBackground(0, 0, 0, 0)` gives you background-transparent text).
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<fontprint,fontPrint>> (for proportional TrueType text), <<colorforeground,colorForeground>>, <<colorbackground,colorBackground>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Quick dev HUD using the built-in font.
|
|
function onOverlayUpdate()
|
|
colorBackground(0, 0, 0, 0) -- Transparent background.
|
|
colorForeground(255, 255, 0, 255)
|
|
overlayPrint(0, 0, "FRAME: " .. discGetFrame())
|
|
overlayPrint(0, 1, "STATE: " .. discGetState())
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#overlaysetresolution]
|
|
==== overlaySetResolution
|
|
|
|
[source,text]
|
|
----
|
|
overlaySetResolution(width, height)
|
|
----
|
|
|
|
Resizes the overlay surface. All previously drawn content is discarded. Call this once at startup to pick the coordinate space your game will draw in; don't change it during gameplay.
|
|
|
|
* `width`, `height` -- overlay dimensions in pixels.
|
|
|
|
Higher resolutions give finer control over sprite placement and sharper text at the cost of more per-frame compositing work. Matching the disc's native resolution (via `discGetWidth` / `discGetHeight`) is a reasonable default.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<overlaygetwidth,overlayGetWidth>>, <<overlaygetheight,overlayGetHeight>>, <<discgetwidth,discGetWidth>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Use the disc's native resolution as the overlay coordinate system.
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
----
|
|
|
|
[#script]
|
|
=== Script
|
|
|
|
WARNING: These functions let a Singe script hand control to another Singe script -- the mechanism behind the built-in `Menu.singe` launcher. Game developers should not reach for these; build your game as a single script and let the menu system handle chaining. They are documented here for completeness and for anyone maintaining the menu itself.
|
|
|
|
[#scriptexecute]
|
|
==== scriptExecute
|
|
|
|
[source,text]
|
|
----
|
|
scriptExecute(config)
|
|
----
|
|
|
|
Replaces the currently running script with a new one described by a games.dat-style table. The current script exits; control does not return.
|
|
|
|
* `config` -- a Lua table with the same fields as a `GAMES[]` entry in `games.dat` (at minimum `SCRIPT`, and usually `VIDEO`, `DATA`, and resolution fields).
|
|
|
|
*Since:* 2.00
|
|
|
|
[#scriptpush]
|
|
==== scriptPush
|
|
|
|
[source,text]
|
|
----
|
|
scriptPush(config)
|
|
----
|
|
|
|
Starts a new script but remembers the current one on an internal stack. When the new script calls `singeQuit`, the previously running script resumes from startup (it is re-run, not unfrozen in place). This is how `Menu.singe` launches a game and comes back to the menu when the game exits.
|
|
|
|
* `config` -- a Lua table describing the script to launch (as for `scriptExecute`).
|
|
|
|
*Since:* 2.00
|
|
|
|
[#singe]
|
|
=== Singe
|
|
|
|
Functions in the `singe*` namespace control the engine itself -- window properties, screenshots, the pause system, quitting, and a handful of useful paths and flags exposed from the command line and `games.dat`.
|
|
|
|
[#singedisablepausekey]
|
|
==== singeDisablePauseKey
|
|
|
|
[source,text]
|
|
----
|
|
singeDisablePauseKey()
|
|
----
|
|
|
|
Legacy alias for `singeSetPauseKeyEnabled(false)`, defined in `Framework.singe`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
|
|
|
[#singeenablepausekey]
|
|
==== singeEnablePauseKey
|
|
|
|
[source,text]
|
|
----
|
|
singeEnablePauseKey()
|
|
----
|
|
|
|
Legacy alias for `singeSetPauseKeyEnabled(true)`, defined in `Framework.singe`.
|
|
|
|
*Since:* 1.18 (RDG)
|
|
*See also:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
|
|
|
[#singegetdatapath]
|
|
==== singeGetDataPath
|
|
|
|
[source,text]
|
|
----
|
|
path = singeGetDataPath()
|
|
----
|
|
|
|
Returns the absolute path to the writable data directory for the running game (the directory specified by the `DATA` field in `games.dat`, resolved under Singe's data root). Use this for save games, high scores, and any other per-game persistent state.
|
|
|
|
*Returns:* string path, ending in a platform-appropriate separator.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<singegetscriptpath,singeGetScriptPath>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Read and write high scores alongside the game's data.
|
|
local highScoreFile = singeGetDataPath() .. "highscores.json"
|
|
----
|
|
|
|
[#singegetheight]
|
|
==== singeGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = singeGetHeight()
|
|
----
|
|
|
|
Returns the height of the Singe window in *screen pixels*, not overlay units. Useful if you want to know the actual display size (for a configuration screen, or to letterbox the overlay).
|
|
|
|
*Returns:* integer pixel height.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<singegetwidth,singeGetWidth>>, <<overlaygetheight,overlayGetHeight>>
|
|
|
|
[#singegetpauseflag]
|
|
==== singeGetPauseFlag
|
|
|
|
[source,text]
|
|
----
|
|
paused = singeGetPauseFlag()
|
|
----
|
|
|
|
Returns the engine's pause flag: `true` while the player has paused with the pause key, or while the script has set it with `singeSetPauseFlag`. This is separate from whether the disc is sitting on a paused frame (`discGetState`); read it when you want to know whether a pause was deliberately requested. Note that while the pause key holds the game frozen your script is not running, so from inside a callback the flag can only be seen as `true` after your own `singeSetPauseFlag(true)`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<singesetpauseflag,singeSetPauseFlag>>
|
|
|
|
[#singegetscriptpath]
|
|
==== singeGetScriptPath
|
|
|
|
[source,text]
|
|
----
|
|
path = singeGetScriptPath()
|
|
----
|
|
|
|
Returns the absolute path of the currently running script file. `Framework.singe` derives the global `DIR` variable from this -- you will usually use `DIR` directly rather than calling `singeGetScriptPath` yourself.
|
|
|
|
*Returns:* string path.
|
|
|
|
*Since:* 1.15 (RDG)
|
|
*See also:* <<singegetdatapath,singeGetDataPath>>, the `DIR` global
|
|
|
|
[#singegetwidth]
|
|
==== singeGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = singeGetWidth()
|
|
----
|
|
|
|
Returns the width of the Singe window in screen pixels. Mirror of `singeGetHeight`.
|
|
|
|
*Returns:* integer pixel width.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<singegetheight,singeGetHeight>>, <<overlaygetwidth,overlayGetWidth>>
|
|
|
|
[#singequit]
|
|
==== singeQuit
|
|
|
|
[source,text]
|
|
----
|
|
singeQuit()
|
|
----
|
|
|
|
Exits the currently running script. If the script was pushed via `scriptPush` (typically, launched from the menu), control returns to the calling script; otherwise Singe itself terminates.
|
|
|
|
Call this from `onInputPressed` or a game-over flow when the user chooses to exit. Do not call it from `onShutdown` -- that callback is already the exit path.
|
|
|
|
*Since:* 1.x (RDG)
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_QUIT then singeQuit() end
|
|
end
|
|
----
|
|
|
|
[#singescreenshot]
|
|
==== singeScreenshot
|
|
|
|
[source,text]
|
|
----
|
|
singeScreenshot()
|
|
----
|
|
|
|
Captures a PNG screenshot of the currently composited frame (disc + overlay) and writes it under the Singe screenshots directory. The capture is asynchronous; the file is written on the next rendering pass.
|
|
|
|
*Since:* 1.x
|
|
|
|
[#singesetgamename]
|
|
==== singeSetGameName
|
|
|
|
[source,text]
|
|
----
|
|
singeSetGameName(title)
|
|
----
|
|
|
|
Sets the OS window title for the Singe window. Call this once at startup with your game's display name.
|
|
|
|
* `title` -- any string.
|
|
|
|
*Since:* 1.15 (RDG)
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
singeSetGameName("Dragon's Lair Remastered")
|
|
----
|
|
|
|
[#singesetpauseflag]
|
|
==== singeSetPauseFlag
|
|
|
|
[source,text]
|
|
----
|
|
singeSetPauseFlag(paused)
|
|
----
|
|
|
|
Sets the engine's pause flag from the script. `true` pauses the disc, every loaded video, and every sound channel, and remembers which of them were playing; `false` resumes exactly those. Unlike the pause key, this does not freeze your script: callbacks keep firing so the script can decide when to clear the flag again. Use it for story freezes and menus in games that manage their own pause; disable the engine's pause key (`singeSetPauseKeyEnabled(false)`) so the two do not fight.
|
|
|
|
* `paused` -- boolean.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<singegetpauseflag,singeGetPauseFlag>>, <<singedisablepausekey,singeDisablePauseKey>>
|
|
|
|
[#singesetpausekeyenabled]
|
|
==== singeSetPauseKeyEnabled
|
|
|
|
[source,text]
|
|
----
|
|
singeSetPauseKeyEnabled(enabled)
|
|
----
|
|
|
|
Chooses who owns the pause key mapped to `INPUT_PAUSE` in `controls.cfg`. While enabled (the default), the engine owns it: pressing the key freezes the game completely (see <<pausing,Pausing>>) and the script never receives `SWITCH_PAUSE`. While disabled, the engine ignores the key and delivers `SWITCH_PAUSE` to `onInputPressed` / `onInputReleased` like any other switch, so a game can run its own pause logic with `singeSetPauseFlag`.
|
|
|
|
* `enabled` -- boolean.
|
|
|
|
*Since:* 2.20
|
|
*See also:* <<singeenablepausekey,singeEnablePauseKey>>, <<singedisablepausekey,singeDisablePauseKey>>, <<singesetpauseflag,singeSetPauseFlag>>
|
|
|
|
[#singeversion]
|
|
==== singeVersion
|
|
|
|
[source,text]
|
|
----
|
|
version = singeVersion()
|
|
----
|
|
|
|
Returns the running Singe engine version as a floating-point number (e.g. `2.10`). Script compatibility shims often branch on this -- look at `Framework.singe` for the pattern.
|
|
|
|
*Returns:* number.
|
|
|
|
*Since:* 1.x (RDG)
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
if singeVersion() < 2.10 then
|
|
error("This game requires Singe 2.10 or newer.")
|
|
end
|
|
----
|
|
|
|
[#singewantscrosshairs]
|
|
==== singeWantsCrosshairs
|
|
|
|
[source,text]
|
|
----
|
|
wants = singeWantsCrosshairs()
|
|
----
|
|
|
|
Returns `true` if the user launched Singe without the `--nocrosshair` flag. Your game should draw its targeting reticle only when this returns `true`; this lets users with physical Sinden-style lightguns (which display their own on-screen reticle in hardware) turn the software crosshair off.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 2.00
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
drawScene()
|
|
if singeWantsCrosshairs() then
|
|
local x, y = mouseGetPosition(0)
|
|
drawCrosshair(x, y)
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#sound]
|
|
=== Sound
|
|
|
|
Sound effects are short audio clips (typical format: WAV) loaded into memory and played through SDL_mixer. Distinct from the disc's own audio (which rides with the video) and from `video*`-asset audio.
|
|
|
|
*Handles and channels -- the single most confusing thing about this API:*
|
|
|
|
* `soundLoad` returns a *sound handle*. Keep it; pass it to `soundPlay` and `soundUnload`.
|
|
* `soundPlay` returns a *channel number* -- the mixer slot the sound is playing in. This is *not* the same value as the sound handle.
|
|
* `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` all take the *channel number* returned by `soundPlay`, not the sound handle.
|
|
|
|
In short: the handle identifies the *data* (the loaded clip); the channel identifies an *instance* of that clip playing right now. One sound can be playing in multiple channels simultaneously (N overlapping footsteps, etc.).
|
|
|
|
There is a master effects volume (`soundSetVolume`) that applies to all channels.
|
|
|
|
[#soundfullstop]
|
|
==== soundFullStop
|
|
|
|
[source,text]
|
|
----
|
|
soundFullStop()
|
|
----
|
|
|
|
Stops every currently playing sound channel at once. Does not unload any sounds.
|
|
|
|
*Since:* 1.16
|
|
*See also:* <<soundstop,soundStop>>
|
|
|
|
[#soundgetvolume]
|
|
==== soundGetVolume
|
|
|
|
[source,text]
|
|
----
|
|
volume = soundGetVolume()
|
|
----
|
|
|
|
Returns the current master effects volume, `0` through `63`.
|
|
|
|
*Returns:* integer.
|
|
|
|
*Since:* 1.16
|
|
*See also:* <<soundsetvolume,soundSetVolume>>
|
|
|
|
[#soundisplaying]
|
|
==== soundIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = soundIsPlaying(channel)
|
|
----
|
|
|
|
Tests whether a specific mixer channel is currently playing audio.
|
|
|
|
* `channel` -- the channel number returned by `soundPlay`.
|
|
|
|
*Returns:* boolean.
|
|
|
|
*Since:* 1.16 (RDG)
|
|
*See also:* <<soundplay,soundPlay>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Don't re-trigger the alarm while a previous instance is still playing.
|
|
if not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmSound)
|
|
end
|
|
----
|
|
|
|
[#soundload]
|
|
==== soundLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = soundLoad(filename)
|
|
----
|
|
|
|
Loads an audio clip from disk.
|
|
|
|
* `filename` -- path to a sound file (WAV is safest; other SDL_mixer-supported formats work but are not officially supported).
|
|
|
|
*Returns:* integer sound handle (a data handle, *not* a playing-channel number).
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<soundplay,soundPlay>>, <<soundunload,soundUnload>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
gunshot = soundLoad(DIR .. "sounds/shot.wav")
|
|
miss = soundLoad(DIR .. "sounds/miss.wav")
|
|
----
|
|
|
|
[#soundpause]
|
|
==== soundPause
|
|
|
|
[source,text]
|
|
----
|
|
wasPlaying = soundPause(channel)
|
|
----
|
|
|
|
Pauses a playing channel. The channel keeps its position; call `soundResume` to continue.
|
|
|
|
* `channel` -- channel number from `soundPlay`.
|
|
|
|
*Returns:* boolean indicating whether the channel was playing at the moment of the call.
|
|
|
|
*Since:* 1.16 (RDG)
|
|
*See also:* <<soundresume,soundResume>>
|
|
|
|
[#soundplay]
|
|
==== soundPlay
|
|
|
|
[source,text]
|
|
----
|
|
channel = soundPlay(id)
|
|
----
|
|
|
|
Plays a loaded sound on the next available mixer channel.
|
|
|
|
* `id` -- sound handle from `soundLoad`.
|
|
|
|
*Returns:* integer channel number you will pass to `soundPause`, `soundResume`, `soundStop`, and `soundIsPlaying`. Returns `-1` (`SOUND_ERROR_INVALID`) if all channels are in use -- check for this if your game can fire lots of concurrent sounds.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<soundload,soundLoad>>, <<soundstop,soundStop>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fire and remember the channel so we can cut it off early if needed.
|
|
shotChannel = soundPlay(gunshot)
|
|
if shotChannel < 0 then
|
|
debugPrint("All sound channels in use -- dropping gunshot")
|
|
end
|
|
----
|
|
|
|
[#soundresume]
|
|
==== soundResume
|
|
|
|
[source,text]
|
|
----
|
|
wasPaused = soundResume(channel)
|
|
----
|
|
|
|
Resumes a paused channel from the position where `soundPause` left it.
|
|
|
|
* `channel` -- channel number from `soundPlay`.
|
|
|
|
*Returns:* boolean indicating whether the channel was paused at the moment of the call.
|
|
|
|
*Since:* 1.16 (RDG)
|
|
*See also:* <<soundpause,soundPause>>
|
|
|
|
[#soundsetvolume]
|
|
==== soundSetVolume
|
|
|
|
[source,text]
|
|
----
|
|
soundSetVolume(volume)
|
|
----
|
|
|
|
Sets the master effects volume. Applied immediately to every currently playing channel.
|
|
|
|
* `volume` -- integer `0` (silent) through `63` (loudest). Out-of-range values terminate the script.
|
|
|
|
*Since:* 1.16
|
|
*See also:* <<soundgetvolume,soundGetVolume>>
|
|
|
|
[#soundstop]
|
|
==== soundStop
|
|
|
|
[source,text]
|
|
----
|
|
wasPlaying = soundStop(channel)
|
|
----
|
|
|
|
Halts a channel immediately. The channel is released back to the pool and the previous channel number is invalidated -- discard it on the Lua side.
|
|
|
|
* `channel` -- channel number from `soundPlay`.
|
|
|
|
*Returns:* boolean indicating whether the channel was playing when the call was made.
|
|
|
|
*Since:* 1.x (RDG)
|
|
*See also:* <<soundfullstop,soundFullStop>>
|
|
|
|
[#soundunload]
|
|
==== soundUnload
|
|
|
|
[source,text]
|
|
----
|
|
soundUnload(id)
|
|
----
|
|
|
|
Frees a loaded sound. Stop any channels playing this sound before unloading; unloading audio data that the mixer is still reading from is undefined behavior.
|
|
|
|
* `id` -- sound handle from `soundLoad` (*not* a channel number).
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<soundload,soundLoad>>
|
|
|
|
[#sprite]
|
|
=== Sprite
|
|
|
|
Sprites are 2D bitmap assets loaded from disk. They support:
|
|
|
|
* *Any SDL_image-supported format* -- PNG, JPG, BMP, GIF, WEBP, and more.
|
|
* *Animation* when the source file is an animated GIF or WEBP. Single-frame images are treated as static sprites; animation functions still work on them but have no effect.
|
|
* *Per-sprite rotation and scaling* with optional bilinear filtering.
|
|
* *Transparency via color-key* -- pixels whose raw value is `0` in the image's own pixel format are drawn as fully transparent: palette index `0` for indexed images, pure black for 24-bit RGB images, and fully transparent black for images with an alpha channel. Alpha channels are honored as well, so a PNG with real transparency needs no color key.
|
|
|
|
Every sprite you load must eventually be released with `spriteUnload`, or from `onShutdown` for assets that live for the lifetime of the game.
|
|
|
|
[#spritedraw]
|
|
==== spriteDraw
|
|
|
|
[source,text]
|
|
----
|
|
spriteDraw(id, x, y) -- Natural size, top-left anchor
|
|
spriteDraw(id, x, y, centered) -- Natural size, centered anchor
|
|
spriteDraw(id, x, y, x2, y2) -- Stretched to rectangle, top-left anchor
|
|
spriteDraw(id, x, y, x2, y2, centered) -- Stretched to rectangle, centered anchor
|
|
----
|
|
|
|
Draws a previously loaded sprite onto the overlay. Must be called from `onOverlayUpdate`.
|
|
|
|
* `x`, `y` -- anchor coordinates in overlay units.
|
|
* `x2`, `y2` -- opposite corner for the stretched forms. The sprite is scaled to fit `(x, y)`-`(x2, y2)`.
|
|
* `centered` -- boolean. When `true`, `(x, y)` (and the center of the `(x, y)`-`(x2, y2)` rectangle for the stretched forms) is treated as the *center* of the sprite. When `false`, it is the top-left corner. Using `centered = true` is strongly recommended when the sprite has been rotated with `spriteRotate`, because rotated sprites change size and a top-left anchor will appear to drift as the sprite spins.
|
|
* `id` -- sprite handle returned by `spriteLoad` or `fontToSprite`.
|
|
|
|
Animation advances automatically each frame the sprite is drawn, based on real time elapsed since the previous `spriteDraw` of that sprite. A sprite that is not drawn does not advance its animation.
|
|
|
|
*Since:* 1.x. Centered and stretched-centered forms added in 2.10.
|
|
*See also:* <<spriteload,spriteLoad>>, <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw a menu cursor that rotates slowly as a visual flourish.
|
|
cursor = spriteLoad(DIR .. "cursor.png")
|
|
cursorAngle = 0
|
|
|
|
function onOverlayUpdate()
|
|
cursorAngle = (cursorAngle + 2) % 360
|
|
spriteRotate(cursor, cursorAngle)
|
|
spriteDraw(cursor, selectionX, selectionY, true) -- Center-anchored.
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#spritegetframe]
|
|
==== spriteGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = spriteGetFrame(id)
|
|
----
|
|
|
|
Returns the index of the currently displayed frame of an animated sprite. Frames are numbered starting at `0`.
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Returns:* integer frame index, or `0` for a static sprite that has no animation.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritesetframe,spriteSetFrame>>, <<spriteisplaying,spriteIsPlaying>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fire the gun only on the frame where the muzzle flash is visible.
|
|
if spriteGetFrame(muzzleFlash) == 2 and not shotFired then
|
|
soundPlay(gunshot)
|
|
shotFired = true
|
|
end
|
|
----
|
|
|
|
[#spritegetheight]
|
|
==== spriteGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
h = spriteGetHeight(id)
|
|
----
|
|
|
|
Returns the current height of the sprite in overlay units. This reflects any scaling or rotation previously applied via `spriteScale`, `spriteRotate`, or `spriteRotateAndScale` -- it is the *drawn* height, not the height of the source image.
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Returns:* integer height.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<spritegetwidth,spriteGetWidth>>, <<spritescale,spriteScale>>
|
|
|
|
[#spritegetwidth]
|
|
==== spriteGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
w = spriteGetWidth(id)
|
|
----
|
|
|
|
Returns the current width of the sprite in overlay units. Reflects any scaling or rotation currently applied, like `spriteGetHeight`.
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Returns:* integer width.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<spritegetheight,spriteGetHeight>>, <<spritescale,spriteScale>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Center a cabinet image inside a fixed drawing area.
|
|
function drawCabinet()
|
|
local x = cabinetX + (cabinetW - spriteGetWidth(cabinetSprite)) * 0.5
|
|
local y = cabinetY + (cabinetH - spriteGetHeight(cabinetSprite)) * 0.5
|
|
spriteDraw(cabinetSprite, x, y)
|
|
end
|
|
----
|
|
|
|
[#spriteisplaying]
|
|
==== spriteIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = spriteIsPlaying(id)
|
|
----
|
|
|
|
Reports whether an animated sprite is currently advancing its animation.
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Returns:* boolean. `true` if the animation is playing, `false` if it has been paused (`spritePause`), has finished a non-looping animation, or the sprite has no animation data at all.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteloop,spriteLoop>>
|
|
|
|
[#spriteload]
|
|
==== spriteLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = spriteLoad(filename)
|
|
----
|
|
|
|
Loads a bitmap from disk and returns an opaque integer handle. The file is resolved relative to the working directory Singe launched from -- in practice, prepend `DIR` (the running script's directory) for files shipped with your game.
|
|
|
|
* `filename` -- path to a PNG, JPG, BMP, GIF, WEBP, or other SDL_image-supported format. Animated GIF and WEBP files load as animated sprites; all other formats load as static sprites.
|
|
|
|
*Returns:* integer sprite handle. Store it; you will pass it to every other `sprite*` function.
|
|
|
|
*Notes:*
|
|
|
|
* There is no `nil` return on failure. If the file cannot be read or decoded, the script is terminated with an error.
|
|
* Loading is synchronous and can take noticeable time for large images. In the threaded model, preload sprites before entering your main loop.
|
|
|
|
*Since:* 1.x. Animated GIF/WEBP support added in 2.10.
|
|
*See also:* <<spriteunload,spriteUnload>>, <<fonttosprite,fontToSprite>> *(Font section)*
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Preload all the game's sprites once at startup.
|
|
cabinetSprite = spriteLoad(DIR .. "cabinet.png")
|
|
marqueeSprite = spriteLoad(DIR .. "marquee.png")
|
|
explosion = spriteLoad(DIR .. "explosion.gif") -- Animated.
|
|
|
|
spriteLoop(explosion, false) -- Play once, do not loop.
|
|
----
|
|
|
|
[#spriteloop]
|
|
==== spriteLoop
|
|
|
|
[source,text]
|
|
----
|
|
spriteLoop(id, shouldLoop)
|
|
----
|
|
|
|
Sets whether an animated sprite loops when it reaches the last frame. The default for newly loaded animated sprites is to loop.
|
|
|
|
* `id` -- sprite handle.
|
|
* `shouldLoop` -- boolean. `true` restarts from frame `0`; `false` stops on the last frame and `spriteIsPlaying` begins returning `false`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteisplaying,spriteIsPlaying>>
|
|
|
|
[#spritepause]
|
|
==== spritePause
|
|
|
|
[source,text]
|
|
----
|
|
spritePause(id)
|
|
----
|
|
|
|
Pauses animation on an animated sprite. The current frame keeps being drawn. Call `spritePlay` to resume from the same frame.
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>
|
|
|
|
[#spriteplay]
|
|
==== spritePlay
|
|
|
|
[source,text]
|
|
----
|
|
spritePlay(id)
|
|
----
|
|
|
|
Starts or resumes animation on an animated sprite. If the animation is already playing, this is a no-op (the frame timer is not reset).
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritepause,spritePause>>, <<spriteisplaying,spriteIsPlaying>>
|
|
|
|
[#spritequality]
|
|
==== spriteQuality
|
|
|
|
[source,text]
|
|
----
|
|
spriteQuality(id, smooth)
|
|
----
|
|
|
|
Selects the filtering mode used when the sprite is scaled or rotated.
|
|
|
|
* `id` -- sprite handle.
|
|
* `smooth` -- `RENDER_PIXELATED` (`0`) for nearest-neighbor (sharp, pixelated -- right for low-res art that should stay crunchy), `RENDER_SMOOTH` (`1`) for bilinear filtering (smooth -- right for photographic art and large decorative elements).
|
|
|
|
Quality is applied immediately: the cached rotated/scaled surface is rebuilt, so the next `spriteDraw` reflects the new setting. Changing quality on every frame is wasteful; set it once after `spriteLoad`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>
|
|
|
|
[#spriterotate]
|
|
==== spriteRotate
|
|
|
|
[source,text]
|
|
----
|
|
spriteRotate(id, angle)
|
|
----
|
|
|
|
Rotates a sprite by the given angle.
|
|
|
|
* `id` -- sprite handle.
|
|
* `angle` -- rotation in degrees, clockwise. The value is wrapped into `0`-`360` automatically, so `spriteRotate(s, 450)` and `spriteRotate(s, 90)` are equivalent.
|
|
|
|
Rotation changes the sprite's bounding box, so `spriteGetWidth` and `spriteGetHeight` return different values after a rotation. Use `spriteDraw(id, x, y, true)` -- the centered form -- to keep the visual center fixed as the sprite rotates.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Spin the "PRESS START" indicator to draw the eye during attract mode.
|
|
function onOverlayUpdate()
|
|
attractAngle = (attractAngle + 3) % 360
|
|
spriteRotate(pressStart, attractAngle)
|
|
spriteDraw(pressStart, screenCenterX, screenCenterY, true)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#spriterotateandscale]
|
|
==== spriteRotateAndScale
|
|
|
|
[source,text]
|
|
----
|
|
spriteRotateAndScale(id, angle, scale)
|
|
spriteRotateAndScale(id, angle, scaleX, scaleY)
|
|
----
|
|
|
|
Applies both rotation and scaling in a single call. Cheaper than calling `spriteRotate` and `spriteScale` separately, because the intermediate surface only has to be rebuilt once.
|
|
|
|
* `angle` -- rotation in degrees, wrapped to `0`-`360`.
|
|
* `scale` -- uniform scale factor (`1.0` = natural size, `2.0` = double size, `0.5` = half size).
|
|
* `scaleX`, `scaleY` -- separate horizontal and vertical scale factors for stretching or squishing.
|
|
* `id` -- sprite handle.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
|
|
|
|
[#spritescale]
|
|
==== spriteScale
|
|
|
|
[source,text]
|
|
----
|
|
spriteScale(id, scale)
|
|
spriteScale(id, scaleX, scaleY)
|
|
----
|
|
|
|
Scales a sprite uniformly or along each axis independently.
|
|
|
|
* `scale` -- uniform scale factor.
|
|
* `scaleX`, `scaleY` -- separate horizontal and vertical factors.
|
|
* `id` -- sprite handle.
|
|
|
|
The scaled surface is cached, so repeated `spriteDraw` calls are cheap -- but each new `spriteScale` rebuilds that cache. Avoid changing the scale every frame if you do not need to.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritequality,spriteQuality>>
|
|
|
|
[#spritesetframe]
|
|
==== spriteSetFrame
|
|
|
|
[source,text]
|
|
----
|
|
spriteSetFrame(id, frame)
|
|
----
|
|
|
|
Jumps an animated sprite directly to a specific frame. The per-frame delay timer is reset, so the sprite will sit on the new frame for its full duration before advancing.
|
|
|
|
* `id` -- sprite handle.
|
|
* `frame` -- zero-based frame index. Silently ignored if out of range, and for sprites that are not animations.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<spritegetframe,spriteGetFrame>>, <<spriteplay,spritePlay>>
|
|
|
|
[#spriteunload]
|
|
==== spriteUnload
|
|
|
|
[source,text]
|
|
----
|
|
spriteUnload(id)
|
|
----
|
|
|
|
Releases all memory associated with a sprite -- the source image, the cached rotated/scaled surface, and animation frames for animated sprites. After this call the handle is invalid; passing it to any other sprite function will terminate the script.
|
|
|
|
* `id` -- sprite handle.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<spriteload,spriteLoad>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Free everything we loaded, called by Singe when the user exits.
|
|
function onShutdown()
|
|
spriteUnload(cabinetSprite)
|
|
spriteUnload(marqueeSprite)
|
|
spriteUnload(explosion)
|
|
end
|
|
----
|
|
|
|
[#video]
|
|
=== Video
|
|
|
|
The `video*` family handles *additional video assets* on top of the main laserdisc video. Typical uses: a side-video of a character talking in a corner of the screen, a pre-rendered animation that plays once at a story beat, multiple concurrent video cues. You can load any number of videos, unlike the disc (which is a singleton).
|
|
|
|
Key differences from the `disc*` family:
|
|
|
|
* Videos are loaded by filename at runtime with `videoLoad`; the disc comes from `games.dat`.
|
|
* Videos can be *drawn at arbitrary positions on the overlay* (with `videoDraw`), rotated, and scaled. The disc fills the background behind the overlay; videos composite through the overlay like sprites do.
|
|
* Each video has its own audio track selection and volume, independent of the disc.
|
|
* Argument order is *different* from sprites: `videoDraw(id, x, y, ...)` takes the handle first. Every other `video*` function also takes the handle first.
|
|
|
|
Like sprites, videos must be drawn from `onOverlayUpdate` and eventually freed with `videoUnload`.
|
|
|
|
[#videodraw]
|
|
==== videoDraw
|
|
|
|
[source,text]
|
|
----
|
|
videoDraw(id, x, y, x2, y2) -- Stretched to rectangle, top-left anchor
|
|
videoDraw(id, x, y, centered) -- Scaled/rotated per videoScale/videoRotate, optional center anchor
|
|
----
|
|
|
|
Draws the current frame of a video onto the overlay, advancing the video's decode by one frame of wall time.
|
|
|
|
* `id` -- video handle from `videoLoad`.
|
|
* `x`, `y` -- anchor coordinates (overlay units).
|
|
* `x2`, `y2` -- opposite corner for the stretched form. Video is scaled to fit the rectangle.
|
|
* `centered` -- boolean (for the scaled/rotated form). When `true`, `(x, y)` is the center of the drawn video; when `false`, it is the top-left. Use `true` for rotated videos.
|
|
|
|
To use the non-stretched form with rotation or scaling, apply `videoScale` and/or `videoRotate` first, then pass the boolean form: `videoDraw(id, x, y, true)`.
|
|
|
|
*Since:* 2.00. The scaled/rotated form with `centered` flag added in 2.10.
|
|
*See also:* <<videoload,videoLoad>>, <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Draw a picture-in-picture video of the NPC in the upper right.
|
|
function onOverlayUpdate()
|
|
videoDraw(npcVideo, 900, 40, 1260, 240) -- Stretched to a 360x200 box.
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#videogetaudiotrack]
|
|
==== videoGetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
track = videoGetAudioTrack(id)
|
|
----
|
|
|
|
Returns the index of the currently selected audio track for a loaded video.
|
|
|
|
*Returns:* integer track index.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>
|
|
|
|
[#videogetaudiotracks]
|
|
==== videoGetAudioTracks
|
|
|
|
[source,text]
|
|
----
|
|
count = videoGetAudioTracks(id)
|
|
----
|
|
|
|
Returns the number of audio tracks available on a loaded video.
|
|
|
|
*Returns:* integer count.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguage,videoGetLanguage>>
|
|
|
|
[#videogetframe]
|
|
==== videoGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = videoGetFrame(id)
|
|
----
|
|
|
|
Returns the current frame number of a loaded video.
|
|
|
|
*Returns:* integer frame.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoseek,videoSeek>>, <<videogetframecount,videoGetFrameCount>>
|
|
|
|
[#videogetframecount]
|
|
==== videoGetFrameCount
|
|
|
|
[source,text]
|
|
----
|
|
total = videoGetFrameCount(id)
|
|
----
|
|
|
|
Returns the total number of frames in a loaded video. Useful for progress bars, for wrapping a loop, or for detecting when a one-shot clip is almost done.
|
|
|
|
*Returns:* integer total frame count.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetframe,videoGetFrame>>, <<videoisplaying,videoIsPlaying>>
|
|
|
|
[#videogetheight]
|
|
==== videoGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = videoGetHeight(id)
|
|
----
|
|
|
|
Returns the source height of a loaded video in pixels (the native resolution of the file, not how big it's drawn on the overlay).
|
|
|
|
*Returns:* integer height.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetwidth,videoGetWidth>>
|
|
|
|
[#videogetlanguage]
|
|
==== videoGetLanguage
|
|
|
|
[source,text]
|
|
----
|
|
code = videoGetLanguage(id, track)
|
|
----
|
|
|
|
Returns the ISO language code for an audio track on a loaded video.
|
|
|
|
* `id` -- video handle.
|
|
* `track` -- audio track index.
|
|
|
|
*Returns:* three-letter language code string (e.g. `"eng"`, `"jpn"`), or empty if no language tag is set.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<videogetaudiotracks,videoGetAudioTracks>>
|
|
|
|
[#videogetlanguagedescription]
|
|
==== videoGetLanguageDescription
|
|
|
|
[source,text]
|
|
----
|
|
name = videoGetLanguageDescription(code)
|
|
----
|
|
|
|
Looks up a human-readable name for an ISO language code. Unlike the other `video*` functions this one does *not* take a video handle -- it is a pure lookup table.
|
|
|
|
`Framework.singe` aliases this as `discGetLanguageDescription` for symmetry with the disc API.
|
|
|
|
* `code` -- three-letter code like `"eng"` or `"jpn"`.
|
|
|
|
*Returns:* English language name (e.g. `"English"`, `"Japanese"`), or `"Unknown"` for unrecognized codes.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetlanguage,videoGetLanguage>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Build a language-select menu from a video's tracks.
|
|
for i = 0, videoGetAudioTracks(cutscene) - 1 do
|
|
local code = videoGetLanguage(cutscene, i)
|
|
local name = videoGetLanguageDescription(code)
|
|
table.insert(options, { track = i, label = name })
|
|
end
|
|
----
|
|
|
|
[#videogetvolume]
|
|
==== videoGetVolume
|
|
|
|
[source,text]
|
|
----
|
|
left, right = videoGetVolume(id)
|
|
----
|
|
|
|
Returns the current per-channel volume of a loaded video. Note this returns *two values* -- left and right.
|
|
|
|
*Returns:* two integers, each `0` to `100`.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videosetvolume,videoSetVolume>>
|
|
|
|
[#videogetwidth]
|
|
==== videoGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = videoGetWidth(id)
|
|
----
|
|
|
|
Returns the source width of a loaded video in pixels.
|
|
|
|
*Returns:* integer width.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetheight,videoGetHeight>>
|
|
|
|
[#videoisplaying]
|
|
==== videoIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = videoIsPlaying(id)
|
|
----
|
|
|
|
Reports whether a loaded video is currently playing.
|
|
|
|
*Returns:* boolean (pushed as a number, `0` or `1` -- treat as boolean in Lua: `videoIsPlaying(id) ~= 0`).
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoplay,videoPlay>>, <<videopause,videoPause>>, <<videogetframe,videoGetFrame>>
|
|
|
|
[#videoload]
|
|
==== videoLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = videoLoad(filename)
|
|
----
|
|
|
|
Loads a video file. Supported formats are everything FFmpeg can decode that Singe ships a codec for (MP4, MKV, WEBM, MPEG, and more). Loading a video creates a per-game data subdirectory under Singe's data root (used for things like seek indexes); this is managed automatically.
|
|
|
|
* `filename` -- path to a video file.
|
|
|
|
*Returns:* integer video handle.
|
|
|
|
The default audio track and volume are taken from the command line or `games.dat`. Change them afterwards with `videoSetAudioTrack` and `videoSetVolume` if needed.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videounload,videoUnload>>, <<videoplay,videoPlay>>, <<videodraw,videoDraw>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Load a cutscene video to play over the paused laserdisc.
|
|
cutscene = videoLoad(DIR .. "videos/intro.mp4")
|
|
videoSetVolume(cutscene, 80, 80)
|
|
videoPlay(cutscene)
|
|
----
|
|
|
|
[#videopause]
|
|
==== videoPause
|
|
|
|
[source,text]
|
|
----
|
|
videoPause(id)
|
|
----
|
|
|
|
Pauses decoding and audio on a video. The last-decoded frame keeps being drawn by `videoDraw`.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoplay,videoPlay>>
|
|
|
|
[#videoplay]
|
|
==== videoPlay
|
|
|
|
[source,text]
|
|
----
|
|
videoPlay(id)
|
|
----
|
|
|
|
Starts or resumes a video.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videopause,videoPause>>, <<videoseek,videoSeek>>
|
|
|
|
[#videoquality]
|
|
==== videoQuality
|
|
|
|
[source,text]
|
|
----
|
|
videoQuality(id, smooth)
|
|
----
|
|
|
|
Selects the filtering mode used when the video is scaled or rotated. Applied on the next frame.
|
|
|
|
* `id` -- video handle.
|
|
* `smooth` -- `RENDER_PIXELATED` (`0`) for nearest-neighbor, `RENDER_SMOOTH` (`1`) for bilinear filtering.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videoscale,videoScale>>, <<videorotate,videoRotate>>
|
|
|
|
[#videorotate]
|
|
==== videoRotate
|
|
|
|
[source,text]
|
|
----
|
|
videoRotate(id, angle)
|
|
----
|
|
|
|
Sets the rotation applied to the video by `videoDraw` (in the non-stretched form).
|
|
|
|
* `id` -- video handle.
|
|
* `angle` -- degrees clockwise. Wrapped to `0`-`360`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videorotateandscale,videoRotateAndScale>>, <<videodraw,videoDraw>>
|
|
|
|
[#videorotateandscale]
|
|
==== videoRotateAndScale
|
|
|
|
[source,text]
|
|
----
|
|
videoRotateAndScale(id, angle, scale)
|
|
videoRotateAndScale(id, angle, scaleX, scaleY)
|
|
----
|
|
|
|
Applies both rotation and scaling in one call.
|
|
|
|
* `id` -- video handle.
|
|
* `angle` -- degrees clockwise.
|
|
* `scale` -- uniform factor.
|
|
* `scaleX`, `scaleY` -- separate horizontal/vertical factors.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videorotate,videoRotate>>, <<videoscale,videoScale>>
|
|
|
|
[#videoscale]
|
|
==== videoScale
|
|
|
|
[source,text]
|
|
----
|
|
videoScale(id, scale)
|
|
videoScale(id, scaleX, scaleY)
|
|
----
|
|
|
|
Scales the video for subsequent `videoDraw` calls in the non-stretched form.
|
|
|
|
* `id` -- video handle.
|
|
* `scale` -- uniform factor.
|
|
* `scaleX`, `scaleY` -- separate horizontal/vertical factors.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videorotateandscale,videoRotateAndScale>>, <<videoquality,videoQuality>>
|
|
|
|
[#videoseek]
|
|
==== videoSeek
|
|
|
|
[source,text]
|
|
----
|
|
videoSeek(id, frame)
|
|
----
|
|
|
|
Jumps a loaded video to a specific frame. Does not change play/pause state.
|
|
|
|
* `id` -- video handle.
|
|
* `frame` -- target frame number.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetframe,videoGetFrame>>, <<videogetframecount,videoGetFrameCount>>
|
|
|
|
[#videosetaudiotrack]
|
|
==== videoSetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
videoSetAudioTrack(id, track)
|
|
----
|
|
|
|
Switches the active audio track on a loaded video. Out-of-range tracks terminate the script.
|
|
|
|
* `id` -- video handle.
|
|
* `track` -- audio track index, `0` through `videoGetAudioTracks(id) - 1`.
|
|
|
|
*Since:* 2.10
|
|
*See also:* <<videogetaudiotrack,videoGetAudioTrack>>
|
|
|
|
[#videosetvolume]
|
|
==== videoSetVolume
|
|
|
|
[source,text]
|
|
----
|
|
videoSetVolume(id, left, right)
|
|
----
|
|
|
|
Sets per-channel volume for a loaded video. Silently clamps values to `0`-`100`.
|
|
|
|
* `id` -- video handle.
|
|
* `left`, `right` -- channel volumes, `0` (silent) to `100` (loudest).
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videogetvolume,videoGetVolume>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Fade the cutscene audio down to 20% as we return control to the player.
|
|
for v = 100, 20, -4 do
|
|
videoSetVolume(cutscene, v, v)
|
|
coroutine.yield() -- In threaded mode; or spread across frames otherwise.
|
|
end
|
|
----
|
|
|
|
[#videounload]
|
|
==== videoUnload
|
|
|
|
[source,text]
|
|
----
|
|
videoUnload(id)
|
|
----
|
|
|
|
Releases a loaded video and all associated resources (decoder, surfaces, cached frames).
|
|
|
|
* `id` -- video handle.
|
|
|
|
*Since:* 2.00
|
|
*See also:* <<videoload,videoLoad>>
|
|
|
|
[#vldp]
|
|
=== VLDP
|
|
|
|
WARNING: *Legacy.* The `vldp*` namespace predates Singe 2.00 and exists to keep pre-2.00 scripts running. For new code, prefer the modern equivalents: - `vldpGetHeight` -> <<discgetheight,`discGetHeight`>> - `vldpGetWidth` -> <<discgetwidth,`discGetWidth`>> - `vldpSetVerbose` -- unimplemented; ignore. `vldpGetPixel` is the one function in this namespace without a modern replacement and is still useful.
|
|
|
|
[#vldpgetheight]
|
|
==== vldpGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = vldpGetHeight()
|
|
----
|
|
|
|
Returns the height of the main laserdisc video in pixels. Equivalent to `discGetHeight`.
|
|
|
|
*Returns:* integer height.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetheight,discGetHeight>>
|
|
|
|
[#vldpgetpixel]
|
|
==== vldpGetPixel
|
|
|
|
[source,text]
|
|
----
|
|
r, g, b = vldpGetPixel(x, y)
|
|
----
|
|
|
|
Reads the color of a single pixel from the current laserdisc frame at overlay coordinates `(x, y)`. The coordinates are internally scaled from overlay units to video pixels, so you pass the same coordinates you use everywhere else on the overlay.
|
|
|
|
* `x`, `y` -- overlay coordinates.
|
|
|
|
*Returns:* three integers: red, green, blue, each `0` to `255`.
|
|
|
|
This is the preferred technique for *hit detection on pre-recorded video*. Rather than hand-authoring hit boxes for every frame, many laserdisc games tag targets with a specific color in the source footage (e.g. a bright magenta outline invisible to the eye) and check the pixel under the crosshair on a trigger pull.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetframe,discGetFrame>>
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- Lightgun hit check: the target is drawn in pure magenta on enemy frames.
|
|
function onInputPressed(what)
|
|
-- The default controls.cfg maps MOUSE_0.BUTTON_LEFT to SWITCH_BUTTON3.
|
|
if what == SWITCH_BUTTON3 then
|
|
local x, y = mouseGetPosition(0)
|
|
local r, g, b = vldpGetPixel(x, y)
|
|
if r > 200 and g < 50 and b > 200 then
|
|
scoreHit()
|
|
else
|
|
scoreMiss()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vldpgetwidth]
|
|
==== vldpGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = vldpGetWidth()
|
|
----
|
|
|
|
Returns the width of the main laserdisc video in pixels. Equivalent to `discGetWidth`.
|
|
|
|
*Returns:* integer width.
|
|
|
|
*Since:* 1.x
|
|
*See also:* <<discgetwidth,discGetWidth>>
|
|
|
|
[#vldpsetverbose]
|
|
==== vldpSetVerbose
|
|
|
|
WARNING: *Unimplemented.* No-op. Retained for backward compatibility.
|
|
|
|
[#enginecallbacks]
|
|
=== Engine Callbacks
|
|
|
|
These are functions *you define* in your script. Singe calls them when the corresponding event happens. All are optional -- define only the ones your game needs.
|
|
|
|
In the threaded programming model, the `singeMain` function replaces most of them (see <<threaded,Threaded>>), but the callbacks still exist and still fire, with one exception: `Framework.singe` installs its own `onOverlayUpdate` to drive the `singeMain` coroutine. Most threaded games define only `singeMain` and `onShutdown`.
|
|
|
|
[#oncontrollermoved]
|
|
==== onControllerMoved
|
|
|
|
[source,text]
|
|
----
|
|
function onControllerMoved(axis, value, which)
|
|
-- axis: physical axis index on the controller
|
|
-- value: -32768 to 32767
|
|
-- which: SDL internal controller instance ID
|
|
end
|
|
----
|
|
|
|
Called when an analog axis on any connected controller moves. Not throttled -- a continuously moved stick will fire many times per second. Apply the `SINGE_DEAD_ZONE` threshold before reacting.
|
|
|
|
The `which` parameter is the controller index, `0` through `3`, matching the index used by `controllerGetAxis` and the `GAMEPAD_0` through `GAMEPAD_3` tables. Most games ignore it and treat all controllers equivalently.
|
|
|
|
*See also:* <<controllergetaxis,controllerGetAxis>>, the `GAMEPAD_AXIS_*` constants
|
|
|
|
[#oninputpressedoninputreleased]
|
|
==== onInputPressed / onInputReleased
|
|
|
|
[source,text]
|
|
----
|
|
function onInputPressed(what)
|
|
-- what: an integer code for the logical input
|
|
end
|
|
|
|
function onInputReleased(what)
|
|
-- what: same as above
|
|
end
|
|
----
|
|
|
|
Called when a mapped input fires. The value passed depends on the current keyboard mode:
|
|
|
|
* *`MODE_NORMAL`*: one of the `SWITCH_*` constants (`SWITCH_UP`, `SWITCH_START1`, `SWITCH_COIN1`, etc.). Keys, controller buttons, and mouse buttons all arrive this way, translated through the mappings in `controls.cfg`; an input that is not mapped to any switch is ignored in this mode. `SWITCH_PAUSE` arrives only when the engine's pause key has been disabled with `singeSetPauseKeyEnabled(false)`; otherwise the engine handles it and freezes the game (see <<pausing,Pausing>>).
|
|
* *`MODE_FULL`*: the SDL keysym of the key pressed (the character value, which is not the same as the scancode). Controller and mouse buttons pass `0` here. In this mode every raw event also comes through `onKeyPressed` / `onKeyReleased` (see below), which is where the scancode or button code lives.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_COIN1 then addCredit()
|
|
elseif what == SWITCH_START1 then startGame()
|
|
elseif what == SWITCH_QUIT then singeQuit()
|
|
elseif what == SWITCH_BUTTON3 then fireShot() -- Left mouse button by default.
|
|
end
|
|
end
|
|
----
|
|
|
|
[#onkeypressedonkeyreleased]
|
|
==== onKeyPressed / onKeyReleased
|
|
|
|
[source,text]
|
|
----
|
|
function onKeyPressed(keysym, scancode)
|
|
end
|
|
|
|
function onKeyReleased(keysym, scancode)
|
|
end
|
|
----
|
|
|
|
*`MODE_FULL` only.* Called when any keyboard key is pressed or released, with both the logical keysym (what character the key produces) and the physical scancode (what key position was pressed).
|
|
|
|
For text entry, use `keysym`. For key-as-button controls, use `scancode` so the binding works across layouts.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
-- High score initial entry -- text, so use keysym.
|
|
function onKeyPressed(keysym, scancode)
|
|
if keysym >= string.byte('A') and keysym <= string.byte('Z') then
|
|
initials = initials .. string.char(keysym)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#onmousemoved]
|
|
==== onMouseMoved
|
|
|
|
[source,text]
|
|
----
|
|
function onMouseMoved(x, y, xRelative, yRelative, which)
|
|
end
|
|
----
|
|
|
|
Called on every mouse-movement event. Parameters depend on the mouse mode:
|
|
|
|
* *`SINGLE_MOUSE`* (default): `x` and `y` are absolute overlay coordinates; `xRelative` and `yRelative` are deltas since the previous event; `which` is always `0`.
|
|
* *`MANY_MOUSE`*: only `xRelative` / `yRelative` are reliable (they are the raw deltas from the physical device); `which` identifies which mouse moved.
|
|
|
|
For a persistently visible crosshair, track the latest `(x, y)` in globals from this callback and render from `onOverlayUpdate`.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
cursorX, cursorY = 640, 360
|
|
function onMouseMoved(x, y, xr, yr, which)
|
|
cursorX, cursorY = x, y
|
|
end
|
|
----
|
|
|
|
[#onoverlayupdate]
|
|
==== onOverlayUpdate
|
|
|
|
[source,text]
|
|
----
|
|
function onOverlayUpdate()
|
|
-- Draw your frame here.
|
|
return OVERLAY_UPDATED -- Or OVERLAY_NOT_UPDATED if nothing changed.
|
|
end
|
|
----
|
|
|
|
The single callback where drawing is allowed. Singe calls it once per rendering pass. Any `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint`, or similar call outside this callback will not appear on screen and may corrupt the overlay.
|
|
|
|
*Return value:*
|
|
|
|
* `OVERLAY_UPDATED` (`1`) -- the overlay changed; Singe re-composites it on top of the video.
|
|
* `OVERLAY_NOT_UPDATED` (`0`) -- you drew nothing (or the overlay looks the same as last frame); Singe reuses the previous composite. Optimization only; when in doubt, return `OVERLAY_UPDATED`.
|
|
|
|
In the threaded model, `Framework.singe` auto-defines `onOverlayUpdate` to resume `singeMain`'s coroutine and always returns `OVERLAY_UPDATED`. Don't define it yourself when using threaded mode.
|
|
|
|
[#onshutdown]
|
|
==== onShutdown
|
|
|
|
[source,text]
|
|
----
|
|
function onShutdown()
|
|
-- Free loaded sprites, sounds, videos, fonts here.
|
|
end
|
|
----
|
|
|
|
Called once, just before Singe exits or switches to the next script (via `scriptExecute` / `scriptPush` / `singeQuit`). Use it to free every handle you loaded.
|
|
|
|
[#onsoundcompleted]
|
|
==== onSoundCompleted
|
|
|
|
[source,text]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
-- channel: the channel number returned by the soundPlay call that just finished.
|
|
end
|
|
----
|
|
|
|
Called each time a sound finishes playing on its own (not when it is stopped via `soundStop`). Useful for chaining music stingers or reusing channel numbers.
|
|
|
|
.Example
|
|
[source,lua]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
if channel == musicChannel then
|
|
musicChannel = soundPlay(nextTrack)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singemain]
|
|
==== singeMain
|
|
|
|
[source,text]
|
|
----
|
|
function singeMain()
|
|
while true do
|
|
-- Your game logic here.
|
|
singeYield() -- Cooperatively yield once per frame.
|
|
end
|
|
end
|
|
----
|
|
|
|
*Threaded model entry point.* Define this function and `Framework.singe` sets up a coroutine that runs it, scheduled from the engine's overlay-update tick. See the Manual's "Threaded" section for the full pattern.
|
|
|
|
* `singeYield()` is an alias for `coroutine.yield` -- call it anywhere your code would otherwise consume substantial time, to let the engine continue servicing input, audio, and video.
|
|
* Do not also define `onOverlayUpdate` -- `Framework.singe` installs its own when `singeMain` is defined.
|
|
|
|
[#unimplementedandlegacy]
|
|
=== Unimplemented and Legacy
|
|
|
|
A handful of API functions are retained for script-level compatibility but do nothing. They are safe to call; they simply log a trace message and return. Prefer the listed replacements for new code.
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Function | Status | Replacement
|
|
| `discChangeSpeed` | No-op | -- (was for variable-speed playback on real LD hardware)
|
|
| `discSearchBlanking` | No-op | -- (was screen-blanking during seek on real LD hardware)
|
|
| `discSkipBlanking` | No-op | -- (same reason)
|
|
| `discSetFPS` | No-op | Framerate is read from the video file.
|
|
| `vldpGetHeight` | Works | <<discgetheight,`discGetHeight`>>
|
|
| `vldpGetWidth` | Works | <<discgetwidth,`discGetWidth`>>
|
|
| `vldpSetVerbose` | No-op | --
|
|
| `colorBackground(r,g,b)` | Works | Prefer the 4-argument form, `colorBackground(r, g, b, a)`.
|
|
| `colorForeground(r,g,b)` | Works | Prefer the 4-argument form, `colorForeground(r, g, b, a)`.
|
|
| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Aliases in `Framework.singe` | Prefer the `singe*` equivalents.
|
|
| `discPauseAtFrame` | Works (alias) | <<discsearch,`discSearch`>>.
|
|
|===
|
|
|
|
In addition, `Framework.singe` rebinds `random = { new = math.random }` so that pre-2.00 scripts using `random.new()` still work. New code should use `math.random` directly.
|