singe/src/singe.c
2026-10-13 13:58:00 -05:00

19775 lines
706 KiB
C

/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#include <string.h>
#include <math.h>
#include <errno.h>
#include <ctype.h>
#ifdef __linux__
#include <dirent.h>
#include <unistd.h>
#endif
// SHA-256 for utilSha256, from the OpenSSL already linked in for LuaSec.
#include <openssl/sha.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <SDL3_ttf/SDL_ttf.h>
#include "../thirdparty/lua/src/lua.h"
#include "../thirdparty/lua/src/lualib.h"
#include "../thirdparty/lua/src/lauxlib.h"
#include "../thirdparty/uthash/src/uthash.h"
#include "../thirdparty/manymouse/manymouse.h"
#include "rotoZoom.h"
#include "../thirdparty/luafilesystem/src/lfs.h"
#include "../thirdparty/luasec/src/context.h"
#include "../thirdparty/luasec/src/ssl.h"
#include "../thirdparty/luasec/src/x509.h"
#include "../thirdparty/luasocket/src/luasocket.h"
#include "../thirdparty/luasocket/src/mime.h"
#ifndef _WIN32
#include "../thirdparty/luasocket/src/unix.h"
// There is no serial.h, so fake it.
LUASOCKET_API int luaopen_socket_serial(lua_State *L);
#endif
// There is no header for rs232 binding. Make our own.
int luaopen_luars232(lua_State *L);
// Nor for lsqlite3.
int luaopen_lsqlite3(lua_State *L);
int luaopen_md5_core(lua_State *L);
int luaopen_bit(lua_State *L);
int luaopen_random(lua_State *L);
int luaopen_cjson(lua_State *L);
int luaopen_lpeg(lua_State *L);
// There is no header for ssl.config binding. Make our own.
LSEC_API int luaopen_ssl_config(lua_State *L);
// Nor for reaching the SSL_CTX inside a LuaSec context, which netTrustSystem needs.
SSL_CTX *lsec_checkcontext(lua_State *L, int idx);
#include "collide.h"
#include "decode.h"
#include "main.h"
#include "midi.h"
#include "mic.h"
#include "midiIo.h"
#include "netTrust.h"
#include "pack.h"
#include "record.h"
#include "util.h"
#include "frameFile.h"
#include "vfs.h"
#include "voice.h"
#include "sqlite3.h"
#include "persist.h"
#include "scene.h"
#include "render.h"
#include "scheduler.h"
#include "scriptWatch.h"
#include "stats.h"
#include "hdr.h"
#include "ktx2.h"
#include "nav.h"
#include "model.h"
#include "physics.h"
#include "particles.h"
#include "gui.h"
#include "videoPlayer.h"
#include "singe.h"
// We have to do the embedding here so the Lua module
// definitions can find their length properly. They
// can't be external to this source file.
#define EMBED_HERE
#include "embedded.h"
// soundSetVolume/soundGetVolume use this legacy scale; the mixer uses MIX_MAX_VOLUME.
#define AUDIO_MAX_VOLUME 63
#define MUSIC_MAX_VOLUME 128 // musicSetVolume keeps Hypseus's own scale, which is twice the one above
#define GAMES_DAT_LEVELS 4 // Directories, the script's and above, searched for its games.dat
#define GUI_DRAW_MAX 16 // guiDraw calls honoured per frame
#define OVERLAY_NO_LAYER -1 // A GUI draw with nothing drawn over it
#define GUI_VALUE_MAX 4096 // Longest value guiGetValue hands back
#define GUI_HANDLERS "singeGuiHandlers" // Registry table of guiSetHandler functions
#define MAX_MICE 4
#define MOUSE_AXIS_COUNT 2
#define MAX_CONTROLLERS 4
#define CONTROLLER_AXIS_COUNT 6
#define CONTROLLER_BUTTON_COUNT 15
#define CONTROLLER_DEAD_ZONE_DEFAULT 15000
#define HYPSEUS_VOLUME_MAX 64 // What Hypseus's videoGetVolume() answers at full volume
#define CONTROLLER_TRIGGER_FIRST 4 // Gamepad axes 4 and 5 are the two analogue triggers
#define AXIS_VALUE_MAX 32767 // What SDL reports at full deflection, and the unit DEAD_ZONE counts in
#define PERCENT_FULL 100.0 // TRIGGER_THRESHOLD is a per cent of that full deflection, as Hypseus writes it
#define GAMEPAD_MAPPING_MAX 640 // Longest synthetic mapping string a device can need
#define GAMEPAD_GUID_MAX 33 // SDL writes a joystick GUID as 32 hex digits and a terminator
#define GAMEPAD_NAME_MAX 96 // As much of a device's name as a mapping carries
#define GAMEPAD_NAME_UNKNOWN "Unknown Device" // What a device that will not say gets called
#define GAMEPAD_HAT_DIRECTIONS 4 // Up, right, down and left, in the order of SDL's hat bits
#define AXIS_CONTROLLER_COUNT (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT) // Controller axis slots first, then the mice
#define AXIS_COUNT (AXIS_CONTROLLER_COUNT + MAX_MICE * MOUSE_AXIS_COUNT)
#define AXIS_INDEX_CONTROLLER(c, a) ((c) * CONTROLLER_AXIS_COUNT + (a))
#define AXIS_INDEX_MOUSE(m, a) (AXIS_CONTROLLER_COUNT + (m) * MOUSE_AXIS_COUNT + (a))
// Input codes handed to scripts and controls.cfg. Framework.singe builds its tables from these.
#define CODE_GAMEPAD_BASE 500
#define CODE_GAMEPAD_STRIDE 100 // Per controller
#define CODE_AXIS_STRIDE 3 // Per axis: axis, negative direction, positive direction
#define CODE_AXIS_NEGATIVE 1
#define CODE_AXIS_POSITIVE 2
#define CODE_GAMEPAD_BUTTON_OFFSET (CONTROLLER_AXIS_COUNT * CODE_AXIS_STRIDE)
#define CODE_MOUSE_BASE 1000
#define CODE_MOUSE_STRIDE 100 // Per mouse
#define CODE_MOUSE_BUTTON_COUNT 5 // Left, right, middle, X1, X2
#define CODE_MOUSE_WHEEL_UP (CODE_MOUSE_BUTTON_COUNT)
#define CODE_MOUSE_WHEEL_DOWN (CODE_MOUSE_BUTTON_COUNT + 1)
#define SCANCODE_DYNAMIC_COUNT 100 // SDL hands SDL_SCANCODE_RESERVED onward to keys it has no scancode for
// Codes below the gamepad range are keyboard scancodes, so every scancode SDL can hand out must sit under it.
SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE >= SDL_SCANCODE_RESERVED + SCANCODE_DYNAMIC_COUNT);
#define IDLE_SLEEP_MS 1
#define OVERLAY_SCALE_DEFAULT 0.5
#define OVERLAY_TEXTURES 3 // Overlay textures uploads take turns in, so none is rewritten while a frame still reads it
#define OVERLAY_VIDEOS_MAX 8 // videoDraw calls the GPU draws under the overlay; more go through the CPU
#define REGION_RECTS_MAX 16 // Separate rectangles a RegionT keeps; more are merged
#define CONSOLE_FONT_GLYPHS 256
#define DEGREES_PER_CIRCLE 360.0
#define ANIMATION_MIN_DELAY_MS 10 // GIFs often carry a zero delay
#define SCREENSHOT_MAX 10000
#define VIDEO_SCALE_THROTTLE_MS 15 // vldpSetScale accepts one change this often, as Hypseus does
#define BEZEL_DIRECTORY "bezels" // Where --bezel looks for its artwork unless --bezeldir names another
#define BEZEL_SIDECAR_EXTENSION ".cfg" // The cutout beside the artwork, named after it
#define BEZEL_CUTOUT_TABLE "CUTOUT" // The one table the sidecar sets
#define BEZEL_CUTOUT_FIELDS 4 // x, y, width and height, in the artwork's own pixels
#define BEZEL_HASH_BASIS 14695981039346656037ULL // FNV-1a, so mainBezelLoaded answers Hypseus's own number
#define BEZEL_HASH_PRIME 1099511628211ULL
#define BEZEL_HASH_NONE "0" // What Hypseus reports with no bezel loaded
#define SCORE_PANEL_RML "Singe/scoreBezel.rml"
#define SCORE_PANEL_PLAYER2 "player2" // The element holding the second player's readouts
#define SCORE_PANEL_WIDTH 960 // The score panel document's own pixels; the draw scales it
#define SCORE_PANEL_HEIGHT 220
#define SCORE_PANEL_BAND_MIN 10 // It needs a band this fraction of the window deep to sit outside the picture
#define SCORE_PANEL_BAND_MAX 4 // ... and never takes more than this fraction of it
#define SCORE_PANEL_PLAYERS 2
#define SCORE_PANEL_SCORE_DIGITS 6 // The readout sizes Hypseus's scoreboard has: six for a score ...
#define SCORE_PANEL_CREDIT_DIGITS 2 // ... two for the credits ...
#define SCORE_PANEL_LIVES_DIGITS 1 // ... and one for a life count
#define SCORE_PANEL_TEXT_MAX 16 // Longest readout, with room to spare
#define SCORE_CODE_OFF -1 // Hypseus's negative values, shown on every digit of the readout
#define SCORE_CODE_DASH -2
#define SCORE_CODE_BLANK -3
#define SCORE_CODE_H -4
#define SCORE_CODE_A -5
// The MIDI wire format, for the midi* sending calls. Status bytes carry the channel in their low
// four bits; every data byte after one is seven bits.
#define MIDI_NOTE_OFF 0x80
#define MIDI_NOTE_ON 0x90
#define MIDI_CONTROL_CHANGE 0xB0
#define MIDI_PROGRAM_CHANGE 0xC0
#define MIDI_PITCH_BEND 0xE0
#define MIDI_CHANNEL_MAX 15
#define MIDI_DATA_MAX 127
#define MIDI_DATA_BITS 7
#define MIDI_BEND_MAX 16383 // Two seven bit halves, 8192 being no bend at all
#define MIDI_BYTE_MAX 255
#define MIDI_MESSAGE_BYTES 3 // The longest message the numeric form of midiSend takes
#define SAVE_TABLE "singeSave" // The save table, in the Lua registry
#define SAVE_CALLBACKS "singeSchedule" // Timer and tween functions, by handle
#define SAVE_DEPTH_MAX 32 // Nested tables a save may hold
#define SAVE_POLYGON_MIN 3 // Corners a hitbox needs before it holds anything
#define SAVE_POLYGON_MAX 256
#define SCHEDULE_UPDATE 1 // Slots of a schedule entry's callback table
#define SCHEDULE_DONE 2
#define SPRITE_VECTOR_MAX 8192 // Pixels a side an SVG may be asked for
#define SUBTITLE_RML "Singe/subtitle.rml"
#define SUBTITLE_SLOT "slot" // The element the engine writes a cue or a banner into
#define SUBTITLE_WIDTH 960 // The subtitle document's own pixels; the draw scales it over the picture
#define SUBTITLE_HEIGHT 540
#define SUBTITLE_NONE -1 // No cue is on screen
#define SUBTITLE_POSITION_MIN 1 // srtPosition is a per cent down the picture, as Hypseus takes it
#define SUBTITLE_POSITION_MAX 95
#define SUBTITLE_POSITION_DEFAULT 80 // Hypseus's own starting height, 0x50
#define SUBTITLE_MARKUP_MAX 4096 // Longest cue the document is handed, once escaped
#define SUBTITLE_WRAPPER_MAX 128 // Room for the element and the inline height around it
#define SUBTITLE_TIME_FIELDS 4 // hours, minutes, seconds and milliseconds either side of the arrow
#define SUBTITLE_ARROW "-->" // What separates a cue's two timestamps
#define SUBTITLE_CUES_FIRST 64 // Cues the parser makes room for before it doubles
#define SUBTITLE_CUES_MAX 65536 // A film's worth of dialogue, and a guard against a runaway file
#define SECONDS_PER_HOUR 3600.0
#define SECONDS_PER_MINUTE 60.0
#define BANNER_TEXT_MAX 60 // overlayBanner shows no more than this, as Hypseus refuses more
#define BANNER_FRAMES 30 // ... for about this many drawn frames, as Hypseus times it
#define BANNER_POSITION_DEFAULT 47 // Hypseus's own default height, 0x2f
#define SINDEN_MIN_VIDEO_PIXELS 2 // The border is squeezed rather than swallow the picture
#define COLOR_BYTE_MAX 255
#define NAV_DRAW_VERTICES 4096 // Starting room for navDraw, doubled until the mesh fits
#define SOUND_QUEUE_SIZE 64
#define MS_PER_SECOND_NUMBER 1000.0
#define MS_PER_SECOND 1000 // --idleexit counts in whole seconds
#define PERF_TRACE_NS (5 * SDL_NS_PER_SECOND) // How often -p says where the frame loop's time went
#define PERF_TOP_CALLS 8 // Engine calls the -p script line names, the costliest first
#define PERF_LINE_BYTES 1024 // Room for that line
#define INPUT_GRACE_MS 1000 // Presses this soon after a script starts or focus arrives were held over from before
#define EFFECT_TRACKS 16 // Sound effect "channels" scripts can play at once
#define SOUND_DEFAULT_NEAR 1.0f // A positioned sound is at full volume within this distance ...
#define SOUND_DEFAULT_FAR 30.0f // ... and silent beyond this
#define SOUND_FADE_FRACTION 0.2f // Of the far distance, over which it fades to silence
#define SOUND_LOOP_FOREVER -1
#define SOUND_DIRECTION_EPSILON 0.0001f // Closer than this to the listener has no direction
#define HEIGHTMAP_MAX 1025 // Pixels per side of a heightmap image
#define WATCH_INTERVAL_MS 1000 // How often --reload checks the script files
#define MACHINE_FILE "machine.cfg" // Per-machine audio delay and picture geometry, in the data root
#define CONTROLS_FILE "controls.cfg" // Input mappings, built in and overridden per game
#define SETTINGS_FILE "settings.cfg" // User options, found the same four ways controls.cfg is
#define SETTINGS_PLACES 4 // The four places both of those are looked for
#define GAMEPAD_DATABASE_FILE "gamecontrollerdb.txt" // Extra SDL controller mappings, found the same four ways
#define SOUND_CHANNEL_NONE -1 // soundPlay's answer when every channel is busy (SOUND_ERROR_INVALID)
#define COLOR_COMPONENTS 4 // r, g, b, a
#define BYTES_PER_KIB 1024
#define SHAPE_SIZES 3 // a, b, c sizing a primitive shape
#define VEHICLE_DRIVE_INPUTS 4 // forward, right, brake, hand brake
#define MESH_SEGMENTS_DEFAULT 24 // Around a cone or cylinder
#define MESH_SPHERE_SEGMENTS_DEFAULT 32 // Around a sphere or torus
#define EMITTER_DEFAULT_BOUNCE 0.5f // emitterSetCollide's optional arguments
#define EMITTER_DEFAULT_FRICTION 0.2f
#define EMITTER_DEFAULT_FLOOR 0.0f
#define WATER_DEFAULT_LINEAR_DRAG 0.5f // bodySetWater's optional arguments
#define WATER_DEFAULT_ANGULAR_DRAG 0.1f
#define LISTENER_CAMERA -1 // soundSetListener's default: the scene camera
#define EFFECT_TAG "effects"
#define MUSIC_TAG "music" // Music carries its own gain, so musicSetVolume never moves a sound effect
#define MUSIC_LOOP_MAX 64 // Hypseus clamps musicPlay's loop count to this
#define MUSIC_GLIDE_MAX_MS 50 // musicGetPosition runs on with the clock between mixer updates, no further than this
#define HELD_KEYS_MAX 64 // Keys physically down at once
#define CHORD_CODES_MAX 4 // Buttons held together in one chord
#define INPUT_DEVICE_DIR "/dev/input"
#define INPUT_EVENT_PREFIX "event" // The input devices ManyMouse reads
#define INPUT_PATH_MAX 288 // /dev/input/ and the longest name a directory entry holds
#define QUIT_CHORD_WAIT_MS 250 // How long a quit chord button's own press waits for the rest of the chord
#define TEXT_CACHE_KEEP_MS 1000 // How long fontPrint keeps a string it has stopped printing
#define TEXT_CACHE_KEY_HEADER 48 // Room in a text cache key for everything ahead of the text
#define PAUSE_TEXT "PAUSED"
#define PAUSE_TEXT_SCALE 3 // Console font is small; scale the indicator up
#define QUAD_VERTICES 4 // A textured quad for SDL_RenderGeometry ...
#define QUAD_INDICES 6 // ... as two triangles
#define TRAIL_POINT_FLOATS 3 // x, y, z per recorded trail point
#define COLOR_KEY_VALUE 0
#define SPRITE_SHEET_FRAMES_MIN 2 // spriteLoadFrames strip bounds, as Hypseus has them
#define SPRITE_SHEET_FRAMES_MAX 512
#define SPRITE_NEW_MAX 4096 // spriteNew's widest and tallest
#define PALETTE_COLOURS 256 // spriteSetPixels' palette: one colour for each byte value
#define RUMBLE_BASE_MS 150 // Duration step 1; every step doubles it
#define LUMA_WEIGHT_RED 77 // BT.601 luma weights in 1/256ths, for overlaySetMonochrome
#define LUMA_WEIGHT_GREEN 151
#define LUMA_WEIGHT_BLUE 28
#define LUMA_WEIGHT_SHIFT 8
#define BGRA_BLUE 0 // Byte order of the overlay's pixels
#define BGRA_GREEN 1
#define BGRA_RED 2
#define BGRA_ALPHA 3
#define BLUE_SCREEN_BLUE 255
#define PLAYER_DISC -2 // materialSetVideo's "the disc": resolved every frame, since a frame file swaps players per segment
#define PACKED_PERMISSIONS "r--r--r--" // lfs.attributes on a packed entry: read only
#define PACKED_BLOCK_SIZE 512 // ... and the block size it reports
#define LUA_VFS_FILE "SingeVfsFile" // Metatable of a Lua file that reads through the vfs
#define LUA_VFS_BUFFER 4096 // ... and how much of it that file holds at a time
#define LUA_VFS_INPUT "SingeVfsInput" // Registry slot holding one as the default input
#define LOGO_FADE_STEPS 256
#define LOGO_MARGIN 40 // Pixels of breathing room around a logo in the logo space
#define LOGO_SPACE_WIDTH 1920 // Logical size the splash screens are laid out in
#define LOGO_SPACE_HEIGHT 1080
#define LOGO_FADE_STEP_MS 3
#define LOGO_HOLD_MS 750
// Decoded video frames are BGRA in memory; this is the matching alpha-less surface format.
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
#define VIDEO_SURFACE_FORMAT SDL_PIXELFORMAT_XRGB8888
#else
#define VIDEO_SURFACE_FORMAT SDL_PIXELFORMAT_BGRX8888
#endif
typedef enum KeyboardModeE {
KEYBOARD_NORMAL = 0,
KEYBOARD_FULL = 1
} KeyboardModeE;
typedef enum MouseModeE {
MOUSE_SINGLE = 100,
MOUSE_MANY = 200
} MouseModeE;
// Values match the Daphne LDP states scripts have always compared against.
typedef enum DiscStateE {
DISC_STOPPED = 2,
DISC_PLAYING = 3,
DISC_PAUSED = 4,
DISC_EJECTED = 5 // The game has no disc at all
} DiscStateE;
typedef enum FontQualityE {
FONT_QUALITY_SOLID = 1,
FONT_QUALITY_SHADED = 2,
FONT_QUALITY_BLENDED = 3
} FontQualityE;
typedef enum RenderQualityE {
RENDER_PIXELATED = 0,
RENDER_SMOOTH = 1
} RenderQualityE;
// A recording the engine is to start or stop at the next frame it draws.
typedef enum RecordRequestE {
RECORD_REQUEST_NONE = 0,
RECORD_REQUEST_START = 1,
RECORD_REQUEST_STOP = 2
} RecordRequestE;
// How spriteSetBlend lays a sprite over the overlay.
typedef enum SpriteBlendE {
SPRITE_ALPHA = 0, // Over what is there, by the sprite's alpha
SPRITE_ADD = 1, // Added to what is there, so it can only brighten
SPRITE_MULTIPLY = 2 // What is there, darkened by the sprite's colour
} SpriteBlendE;
// How an io function hooked through the vfs uses its file name.
typedef enum IoHookModeE {
IO_HOOK_INPUT = 0, // io.input: reads, and becomes the default input
IO_HOOK_LINES = 1, // io.lines: reads, and hands back an iterator
IO_HOOK_OPEN = 2, // The mode string decides
IO_HOOK_WRITE = 3
} IoHookModeE;
typedef enum OverlayResultE {
OVERLAY_NOT_UPDATED = 0,
OVERLAY_UPDATED = 1
} OverlayResultE;
// Index into _global.controlMappings and the SWITCH_* value handed to scripts.
typedef enum InputE {
INPUT_UP = 0,
INPUT_LEFT,
INPUT_DOWN,
INPUT_RIGHT,
INPUT_1P_START,
INPUT_2P_START,
INPUT_ACTION_1,
INPUT_ACTION_2,
INPUT_ACTION_3,
INPUT_1P_COIN,
INPUT_2P_COIN,
INPUT_SKILL_EASY,
INPUT_SKILL_MEDIUM,
INPUT_SKILL_HARD,
INPUT_SERVICE,
INPUT_TEST_MODE,
INPUT_RESET_CPU,
INPUT_SCREENSHOT,
INPUT_QUIT,
INPUT_PAUSE,
INPUT_CONSOLE,
// Added in Singe 2.00
INPUT_ACTION_4,
INPUT_TILT,
INPUT_GRAB,
// Added in Singe 3.00. A new switch goes on the end so the numbers a game already knows do not move.
INPUT_MOUSE_DISCONNECT,
INPUT_RECORDING,
INPUT_COUNT
} InputE;
typedef struct LuaModuleS {
const char *name;
union {
const char *source;
lua_CFunction openf;
};
size_t length;
} LuaModuleT;
// A Lua file handle that reads through the vfs.
// Lua's own file handles are a FILE * and nothing else, so anything a script opens by name has to
// exist as a file the C library can read. For a packed game that meant writing the asset out to
// data/<game>/cache first and handing back a path to the copy -- a game's own artwork, its level
// data, anything it opened -- which is a second copy of the game on disk, written the first time
// each file is touched, and stale the moment the .game is rebuilt.
//
// Everything else in the engine already reads a packed asset in place: the sprite loader, the
// decoder, the GUI's file interface. Only Lua could not, because the io library is not ours. So
// the io library is patched here rather than worked around: io.open, io.lines and io.input hand
// back the object below when the name lives only inside a database, and it reads through
// vfsStreamOpen like everything else does. It answers to the same methods Lua's own handle does --
// read, lines, seek, close, flush, setvbuf -- and io.type, io.read and io.close know it, so a
// script cannot tell the difference except that write refuses: these are only ever opened to read.
//
// What is NOT covered, deliberately: a handle handed to a C module that expects a FILE *. Nothing
// in the engine does that, and a script that manages it gets a clear error rather than a crash.
typedef struct LuaFileS {
VfsStreamT *stream;
uint8_t buffer[LUA_VFS_BUFFER];
int64_t have; // Bytes in the buffer
int64_t at; // and how far through it the reader is.
int64_t pos; // Where the reader is in the file, buffering included.
bool closed;
} LuaFileT;
typedef struct InputNameS {
const char *configName; // Table name in controls.cfg
const char *switchName; // Constant name in scripts
} InputNameT;
typedef struct HeldKeyS {
int32_t keysym;
int32_t scancode;
} HeldKeyT;
typedef struct MouseS {
int32_t x;
int32_t y;
float absX; // An absolute device's last reported position, in presentation coordinates
float absY;
char name[64];
} MouseT;
// Renderer textures made from an emitter's frames for drawing 2D particles, refreshed when they change.
typedef struct ParticleTexturesS {
int32_t id;
uint32_t version;
int32_t count;
SDL_Texture **textures;
ParticleBlendE blend; // The blend mode the textures were last set to
UT_hash_handle hh;
} ParticleTexturesT;
typedef struct SpriteS {
int32_t id;
IMG_Animation *animation; // NULL for still images
SDL_Surface *originalSurface; // Owned unless it points into animation->frames
SDL_Surface *surface; // What gets drawn: originalSurface, or a transformed copy
SDL_Surface *rotatedFrame; // spriteRotateFrame's turned copy of one sheet frame, drawn by spriteDrawRotatedFrame
SDL_Surface *flippedFrame; // spriteFlip's mirrored copy of the sheet frame spriteDrawFrame last drew
int32_t flippedIndex; // Which frame that is
SDL_FlipMode flip; // How spriteFlip mirrors every draw
SpriteBlendE blend; // spriteSetBlend
uint8_t alpha; // spriteSetAlpha: every pixel's alpha is multiplied by this
uint8_t tintR; // spriteSetTint: every pixel's colour is multiplied by this
uint8_t tintG;
uint8_t tintB;
bool surfaceOwned; // True when surface is a transformed copy to free
int32_t frameWidth; // Unscaled size of one frame (the image, for a still)
int32_t frameHeight;
double angle;
double scaleX;
double scaleY;
int32_t smooth;
int32_t currentFrame;
bool loop;
bool animating;
uint64_t lastTick;
uint64_t ticks;
uint64_t loopMs; // One pass through every frame's (clamped) delay
UT_hash_handle hh;
} SpriteT;
// A loose script file --reload watches.
typedef struct WatchedFileS {
char *name;
int64_t modified;
} WatchedFileT;
typedef struct SoundS {
int32_t id;
MIX_Audio *audio;
UT_hash_handle hh;
} SoundT;
// A music track. Music keeps its own track and its own tag so its gain is independent of the
// sound effects', which is what Hypseus gets from running music on a mixer of its own.
typedef struct MusicS {
int32_t id;
MIX_Audio *audio;
MIX_Track *track;
Sint64 heardFrames; // musicGetPosition: the mixer's position when it last moved
uint64_t heardTicks; // and when that was
UT_hash_handle hh;
} MusicT;
// One cue of a loaded .srt, its timestamps already turned into disc frames.
typedef struct SubtitleCueS {
int64_t startFrame;
int64_t endFrame;
char *text;
} SubtitleCueT;
// Where a playing effect channel sits: in the scene (following a node or placed by hand), or
// panned across the speakers, or neither.
typedef struct EffectS {
int32_t node; // Followed while it exists; LISTENER_CAMERA when the position is given by hand
Vec3T position; // World position when not following a node
float nearBy; // Full volume within this distance ...
float farOff; // ... silent beyond this
float relative[3]; // The last position handed to the mixer, listener space
float gain; // The last distance gain
float volume; // The channel's own volume, 0 to 1, on top of the master
bool positioned;
} EffectT;
typedef struct FontS {
int32_t id;
TTF_Font *font;
UT_hash_handle hh;
} FontT;
// A string fontPrint has already rendered, ready to blit: SDL_ttf, the unpacking and the colour key
// are the costly part of printing, and a game that reprints its HUD every frame asks for the same
// strings over and over. No fixed number of them: a high score table prints dozens a frame, and a
// cache smaller than that misses every time. Strings not printed for TEXT_CACHE_KEEP_MS go instead.
typedef struct TextCacheS {
char *key; // Font, quality, colours and text: everything that changes the picture
SDL_Surface *surface;
int32_t font; // Dropped with it
uint64_t usedMS; // Last printed, by utilTicks
UT_hash_handle hh;
} TextCacheT;
// Part of the overlay: a few rectangles, or all of it. What was drawn since overlayClear, what
// changed since the last upload, and what each of the overlay's textures has yet to be sent. More
// rectangles than it keeps are merged, so a region only ever errs on the large side.
typedef struct RegionS {
SDL_Rect rect[REGION_RECTS_MAX];
int32_t count;
bool all;
} RegionT;
typedef struct VideoS {
int32_t id;
int32_t handle;
int64_t lastFrame;
bool wasPlayingBeforePause;
bool transformChanged;
SDL_Texture *texture;
SDL_Surface *transformedSurface;
double angle;
double scaleX;
double scaleY;
int32_t smooth;
UT_hash_handle hh;
} VideoT;
// Buttons held together for one switch: controls.cfg's nested lists, such as Select and Start for quit
// on a handheld with no Escape key. It fires on the press that completes it and lets go when any of
// its buttons does. Each button keeps its own meaning as well.
typedef struct ChordS {
int32_t code[CHORD_CODES_MAX];
int32_t count;
bool active;
} ChordT;
typedef struct MappingS {
int32_t inputCount;
int32_t *input;
int32_t chordCount;
ChordT *chord;
} MappingT;
// One guiDraw this frame: a GUI composited over the overlay, in overlay coordinates or covering
// it. A screen space one takes the same coordinates across the whole window instead, so it can sit
// on the bezel artwork beside the picture rather than inside it.
typedef struct GuiDrawS {
int32_t gui;
SDL_FRect dest;
bool whole;
bool screen;
int32_t layer; // The overlay layer drawn over this GUI, or OVERLAY_NO_LAYER
} GuiDrawT;
// What a script draws on the overlay after a guiDraw goes on a layer of its own, composited over
// that GUI, so the order of the script's own drawing says what is under a document and what is
// over it: a HUD under the pointer, the pointer over the HUD.
typedef struct OverlayLayerS {
SDL_Surface *surface;
SDL_Texture *texture;
bool dirty;
} OverlayLayerT;
// A videoDraw the GPU does instead: the frame's texture, drawn under the base overlay where the
// overlay is still transparent, which looks the same as the frame copied into it.
typedef struct OverlayVideoS {
SDL_Texture *texture;
SDL_Rect dest; // Overlay coordinates
} OverlayVideoT;
typedef struct GlobalS {
MouseT mice[MAX_MICE];
lua_State *luaContext;
SDL_Color colorForeground;
SDL_Color colorBackground;
SDL_Surface *overlay; // What overlay drawing lands on: the base, or the layer the last guiDraw opened
SDL_Surface *overlayBase; // The overlay itself, under every GUI
OverlayLayerT overlayLayers[GUI_DRAW_MAX]; // The layers this turn's guiDraw calls opened, in order
int32_t overlayLayerCount;
SDL_Surface *overlayGrey; // The overlay in luma only, for overlaySetMonochrome
SDL_Surface *blitScratch; // The overlay's size and format: a stretched or tinted sprite is made here first (_spriteBlitFast)
SDL_Texture *overlayTexture; // The one holding the current overlay, one of overlayTextures
SDL_Texture *overlayTextures[OVERLAY_TEXTURES];
int32_t overlayTextureIndex;
SDL_FRect focusArea; // vldpFocusArea's part of the disc frame, in disc pixels
bool focused;
bool overlayMonochrome;
bool overlayLinearScale; // setOverlayLinearScale's filter for the overlay texture; linear is Singe's long-standing look
char *discAudioSuffix; // discAudioSuffix's last accepted suffix, NULL until one is
GuiDrawT guiDraws[GUI_DRAW_MAX]; // This frame's guiDraw calls
int32_t guiDrawCount;
GuiDrawT guiShown[GUI_DRAW_MAX]; // Last frame's, for pointing at them
int32_t guiShownCount;
int32_t guiHover; // The GUI under the pointer, GUI_NO_HANDLE for none
bool guiTextActive; // A GUI taking input has a text field focused
bool textInputOn; // SDL text input is running: for that field, or for a MODE_FULL script
SDL_Window *window;
SDL_Renderer *renderer;
SDL_GPUDevice *device;
bool reloadRequested; // singeReload, F5 or a watched file changing
char *relaunchPath; // singeRelaunch: the executable to start once this one has ended
char *relaunchExtra; // and one argument added to the ones this run was given
WatchedFileT *watched;
int32_t watchedCount;
uint64_t watchTick; // When the watched files were last checked
SDL_Texture *videoTexture;
SDL_Surface *consoleFontSurface;
SDL_Gamepad *controllers[MAX_CONTROLLERS];
int32_t controllerDeadZone; // DEAD_ZONE: the fallback for every axis
int32_t triggerThreshold; // --trigger_threshold in raw units; 0 leaves the triggers on the dead zone
int32_t axisDeadZone[AXIS_COUNT]; // What each axis actually uses, DEAD_ZONES having had its say
bool joyMouseEnabled; // A stick drives the mouse cursor
int32_t joyMouseX; // Where it has driven it, in video coordinates
int32_t joyMouseY;
uint64_t joyMouseClock; // Next tick it may move on
bool mouseAbsoluteSeen[MAX_MICE]; // --absolutes_only: this device has reported an absolute position
bool fullScreen; // The mode Alt-Enter last put the window in
int32_t consoleFontWidth;
int32_t consoleFontHeight;
int32_t nextSpriteId;
int32_t nextSoundId;
int32_t nextMusicId;
int32_t nextFontId;
int32_t nextVideoId;
int32_t nextScreenshot; // First index worth checking; lower ones are taken
int32_t effectsVolume;
int32_t musicVolume; // 0 to MUSIC_MAX_VOLUME, Hypseus's scale, over the music tag alone
int32_t listenerNode; // For positioned sounds; LISTENER_CAMERA for the scene camera
KeyboardModeE keyboardMode;
bool keyboardState[SDL_SCANCODE_COUNT];
bool keySuppressed[SDL_SCANCODE_COUNT]; // Held before this script started; not a press for it
bool buttonSuppressed[MAX_CONTROLLERS][CONTROLLER_BUTTON_COUNT];
uint64_t inputGraceUntil; // Until then, new presses are treated as held over too
int32_t keyboardLastDown;
int32_t keyboardLastUp;
int32_t frameFileHandle;
int32_t videoHandle; // -1 when the game has no disc
int32_t canvasWidth; // World size: the disc's, or the configured canvas
int32_t canvasHeight;
FontQualityE fontQuality;
bool spriteKeyed; // drawTransparent: colour 0 keyed on the sprites and text made from now on
MouseModeE mouseMode;
int32_t mouseCount;
int32_t axisCache[AXIS_COUNT];
int32_t axisCode[AXIS_COUNT]; // Direction code currently pressed per axis, 0 when none
int32_t soundQueue[SOUND_QUEUE_SIZE]; // Channels finished since the last frame (audio thread writes)
int32_t soundQueueCount;
bool frozen; // Engine pause: the script is not being run
bool switchHeld[INPUT_COUNT]; // Switches the script believes are down (MODE_NORMAL)
HeldKeyT heldKeys[HELD_KEYS_MAX]; // Keys the script believes are down (MODE_FULL)
int32_t heldKeyCount;
HeldKeyT physicalKeys[HELD_KEYS_MAX]; // Keys and buttons physically down right now
int32_t physicalKeyCount;
SDL_Texture *pauseTexture;
int32_t pauseTextureWidth;
int32_t pauseTextureHeight;
double overlayScaleX; // Overlay size / video size
double overlayScaleY;
SDL_FRect videoRect; // Where the game is drawn, in video coordinates: _computeVideoRect() builds it
double drawScaleX; // Video size / videoRect size: mouse positions map through it
double drawScaleY;
SDL_FRect sindenBlackRect; // The Sinden rings, built by the same routine so a runtime change moves them
SDL_FRect sindenWhiteRect;
SDL_FRect sindenInnerRect; // What the two rings enclose, which is the picture unless they are at the window's edge
bool sindenAtWindow; // The rings are at the window's edge, around the bezel artwork rather than the picture
SDL_Texture *bezelTexture; // --bezel's artwork, drawn over the whole logical rectangle
char *bezelHash; // Its identifier for mainBezelLoaded: the file's FNV-1a hash as decimal
SDL_FRect bezelCutout; // The hole the picture goes in, in video coordinates
bool bezelHasCutout; // A sidecar declared one; without it the picture keeps the window
bool bezelInFront; // --bezelflip: the artwork draws over the picture, not behind it
bool overlayOnTop; // setOverlayOnTop: the overlay and the GUIs draw above the artwork
int32_t subtitleGui; // The subtitle document, GUI_NO_HANDLE until a cue or a banner needs it
int32_t subtitleDocument;
SubtitleCueT *subtitleCues; // srtLoad's parsed cues, in file order
int32_t subtitleCueCount;
int32_t subtitleCue; // The cue whose text is in the document, SUBTITLE_NONE for none
int32_t subtitlePosition; // srtPosition, a per cent down the picture
bool subtitleEnabled; // srtEnable
char *bannerText; // overlayBanner's message while it is up, NULL otherwise
int32_t bannerFrames; // Drawn frames it has left
int32_t bannerPosition;
int32_t scoreGui; // The score panel, GUI_NO_HANDLE until a game asks for it
int32_t scoreDocument;
char *scoreTwinMarkup; // The second player's cell, kept while it is switched off
bool scoreEnabled; // scoreBezelEnable
bool scoreTwin; // scoreBezelTwinScoreOn
bool scoreTwinShown; // Whether the document is carrying that cell right now
bool scoreBlank; // scoreBezelClear, until the next value arrives
int32_t scoreCredits;
int32_t scoreValue[SCORE_PANEL_PLAYERS];
int32_t scoreLives[SCORE_PANEL_PLAYERS];
SDL_Color sindenBlackColor;
SDL_Color sindenWhiteColor;
int32_t sindenBlackWidth; // In video pixels, as the user gave them
int32_t sindenWhiteWidth;
bool sindenBorder;
int32_t videoScale; // Live --scalefactor per cent; vldpSetScale moves it
int32_t videoShiftX; // Live --shiftx and --shifty per cent
int32_t videoShiftY;
int32_t videoRotate; // Live --rotate degrees; vldpSetRotate moves it
SDL_Texture *rotateTexture; // The frame, drawn unrotated, when videoRotate is not zero
uint64_t videoScaleClock; // vldpSetScale is throttled to one change per VIDEO_SCALE_THROTTLE_MS
bool saveDirty; // The save table changed this frame and wants writing
double statsFrameMs; // How long the last frame took, for the overlay
bool discMonochrome; // vldpSetMonochrome: the disc picture is shown in luma only
bool discBlend; // vldpSetBlend: the disc picture is smoothed down its rows
bool discLumaOn; // vldpSetLuma, and the level it was given
int32_t discLumaLevel;
uint8_t overlayOpacity; // setOverlayOpacity, over the whole overlay
bool overlayDirty;
RegionT overlayPainted; // Drawn on the base overlay since overlayClear; the rest is overlayClearColor
uint32_t overlayClearColor;
uint8_t overlayClearAlpha; // Of overlayClearColor: videos go under the overlay only where it is see-through
OverlayVideoT overlayVideos[OVERLAY_VIDEOS_MAX]; // Under the base overlay until the next overlayClear
int32_t overlayVideoCount;
RegionT overlayChanged; // Changed on the base overlay since it was last uploaded
RegionT overlayStale[OVERLAY_TEXTURES]; // What each texture is behind the base overlay by
bool pauseState; // by RDG2010
bool pauseEnabled; // by RDG2010
bool quitEnabled; // The engine quits on the quit switch
ChordT quitChord; // --quitchord: the machine's own way out, whatever the game maps
TextCacheT *textCache;
char *textCacheKey; // Scratch for building a key to look up
size_t textCacheKeyBytes;
uint64_t textCacheSweepMS; // When unused strings were last dropped
int32_t quitChordRelease; // The button that completed it, whose release the script is not told of either, or -1
int32_t quitChordHeld[CHORD_CODES_MAX]; // Its buttons pressed while it waits to see whether the rest follow
int32_t quitChordHeldCount;
uint64_t quitChordHeldMS; // When the first of them was pressed, by utilTicks
bool refreshDisplay;
bool running;
bool discStopped;
bool mouseEnabled;
bool mouseGrabbed;
bool audioMuted; // --nosound, or --startsilent until the first input
uint64_t idleClock; // Ticks of the last input, for --idleexit
bool requestScreenShot;
char *screenshotName; // singeScreenshot's chosen file name, or NULL for the next singeNNN.png
RecordRequestE recordRequest; // A recording to start or stop when the next frame is drawn
char *recordName; // singeRecordStart's chosen file name, or NULL for the next singeNNN.mp4
int32_t nextRecording; // First singeNNN.mp4 worth checking
bool generatedAudio; // A recorded deterministic run: the sound is mixed a step at a time
double generatedCarry; // Fractions of a sample frame the steps have not yet mixed
uint8_t *generatedBuffer; // Where it is mixed to; the recorder takes it from the mixer
int32_t generatedBufferSize;
bool wasPlayingBeforePause;
VideoT *videoList;
SpriteT *spriteList;
ParticleTexturesT *particleTextures; // 2D particle textures by emitter
Vec3T *navDrawVertices; // navDraw's triangle list, kept between frames
int32_t navDrawCapacity;
SDL_Vertex *quadVertices; // Scratch for 2D particle quads and trails, grown as needed
int32_t *quadIndices;
int32_t quadCapacity; // In quads
int32_t *frameStarts; // Per emitter frame, where its particles' quads begin in the index scratch
int32_t frameStartCapacity;
SoundT *soundList;
MusicT *musicList;
FontT *fontList;
FontT *fontCurrent;
MappingT controlMappings[INPUT_COUNT];
uint32_t capabilitiesUsed; // CapabilityE bits this game has used so far
int32_t machineTier; // Measured performance class from machine.cfg, or TIER_UNMEASURED
bool sceneMeasuring; // sceneMeasure: each frame waits for the scene to finish and adds its time
uint64_t sceneMeasureNS;
uint64_t sceneMeasureFrames;
ConfigT *conf; // Local copy of command line options
} GlobalT;
// -p's account of the frame loop: where the time went, summed until PERF_TRACE_NS has passed and
// then traced as averages per frame. Only the stages that can plausibly be slow are timed; "other"
// is the remainder, input and the 2D layers mostly.
typedef struct PerfS {
uint64_t windowStartNS;
uint64_t scriptStartNS;
uint64_t loops;
uint64_t presents;
uint64_t discNS;
uint64_t scriptNS;
uint64_t sceneNS;
uint64_t guiNS;
uint64_t overlayNS;
uint64_t composeNS; // --perfsync: SDL_Renderer's own drawing, finished on the GPU before the swap
uint64_t presentNS;
uint64_t totalNS;
uint64_t worstNS;
bool firstPresented;
} PerfT;
// -p's account of the script's time: every engine call a script can make, wrapped while tracing so
// its time and count are kept, which splits the script's share of the loop into the engine's work
// on its behalf and the Lua around it. Kept apart from PerfT, because the calls are wrapped when a
// script's Lua state is made and PerfT is cleared after that.
typedef struct PerfCallS {
char *name;
uint64_t ns;
uint64_t calls;
} PerfCallT;
typedef struct PerfCallsS {
PerfCallT *call;
int32_t count;
uint64_t ns; // In every wrapped call, wherever it was made
uint64_t scriptNS; // Of that, inside onOverlayUpdate, the loop's "script" stage
bool inScript;
} PerfCallsT;
static GlobalT _global;
// The capability names games.dat's REQUIRES and singeCapabilities use, one per CapabilityE bit.
typedef struct CapabilityNameS {
uint32_t bit;
const char *name;
} CapabilityNameT;
static const CapabilityNameT CAPABILITY_NAMES[] = {
{ CAPABILITY_SCENE, "SCENE" },
{ CAPABILITY_GUI, "GUI" },
{ CAPABILITY_SHADOWS, "SHADOWS" },
{ CAPABILITY_POST, "POST" },
{ CAPABILITY_SKIN, "SKIN" },
{ CAPABILITY_MORPH, "MORPH" }
};
static PerfT _perf;
static PerfCallsT _perfCalls;
static MIX_Track *_effectTracks[EFFECT_TRACKS];
static EffectT _effects[EFFECT_TRACKS];
// One entry per InputE, in order. The config name is the controls.cfg table; the switch name is the Lua constant.
static const InputNameT _inputNames[INPUT_COUNT] = {
{ "INPUT_UP", "SWITCH_UP" },
{ "INPUT_LEFT", "SWITCH_LEFT" },
{ "INPUT_DOWN", "SWITCH_DOWN" },
{ "INPUT_RIGHT", "SWITCH_RIGHT" },
{ "INPUT_1P_START", "SWITCH_START1" },
{ "INPUT_2P_START", "SWITCH_START2" },
{ "INPUT_ACTION_1", "SWITCH_BUTTON1" },
{ "INPUT_ACTION_2", "SWITCH_BUTTON2" },
{ "INPUT_ACTION_3", "SWITCH_BUTTON3" },
{ "INPUT_1P_COIN", "SWITCH_COIN1" },
{ "INPUT_2P_COIN", "SWITCH_COIN2" },
{ "INPUT_SKILL_EASY", "SWITCH_SKILL1" },
{ "INPUT_SKILL_MEDIUM", "SWITCH_SKILL2" },
{ "INPUT_SKILL_HARD", "SWITCH_SKILL3" },
{ "INPUT_SERVICE", "SWITCH_SERVICE" },
{ "INPUT_TEST_MODE", "SWITCH_TEST" },
{ "INPUT_RESET_CPU", "SWITCH_RESET" },
{ "INPUT_SCREENSHOT", "SWITCH_SCREENSHOT" },
{ "INPUT_QUIT", "SWITCH_QUIT" },
{ "INPUT_PAUSE", "SWITCH_PAUSE" },
{ "INPUT_CONSOLE", "SWITCH_CONSOLE" },
{ "INPUT_ACTION_4", "SWITCH_BUTTON4" },
{ "INPUT_TILT", "SWITCH_TILT" },
{ "INPUT_GRAB", "SWITCH_GRAB" },
{ "INPUT_MOUSE_DISCONNECT", "SWITCH_MOUSE_DISCONNECT" },
{ "INPUT_RECORD", "SWITCH_RECORD" }
};
// SDL numbers mouse buttons left, middle, right; scripts number them left, right, middle.
static const int32_t _sdlMouseButtonToCode[] = { 0, 0, 2, 1, 3, 4 };
static void _actOnKey(bool down, int32_t keysym, int32_t scancode);
static int32_t _apiUnimplemented(lua_State *L, const char *method);
static int32_t _argAnimation(lua_State *L, const char *method, int32_t model, int32_t index);
static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t index);
static int32_t _argBody(lua_State *L, const char *method, int32_t index);
static bool _argBoolean(lua_State *L, const char *method, int32_t index);
static int32_t _argChannel(lua_State *L, const char *method, int32_t index);
static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum);
static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index);
static const char *_argData(lua_State *L, const char *method, int32_t index, size_t *length);
static int32_t _argEmitter(lua_State *L, const char *method, int32_t index);
static QuatT _argEuler(lua_State *L, const char *method, int32_t index);
static float *_argFloatTable(lua_State *L, const char *method, int32_t index, int32_t *count);
static FontT *_argFont(lua_State *L, const char *method, int32_t index);
static int32_t _argGui(lua_State *L, const char *method, int32_t index);
static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun);
static int32_t _argInteger(lua_State *L, const char *method, int32_t index);
static int64_t _argInteger64(lua_State *L, const char *method, int32_t index);
static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Surface **surface, Ktx2ImageT *ktx2);
static int32_t _argMaterial(lua_State *L, const char *method, int32_t index);
static int32_t _argMesh(lua_State *L, const char *method, int32_t index);
static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index);
static MusicT *_argMusic(lua_State *L, const char *method, int32_t index);
static int32_t _argNav(lua_State *L, const char *method, int32_t index);
static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index);
static int32_t _argNode(lua_State *L, const char *method, int32_t index);
static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun);
static double _argNumber(lua_State *L, const char *method, int32_t index);
static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b);
static int32_t _argPlayer(lua_State *L, const char *method, int32_t index);
static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index);
static int32_t _argSoft(lua_State *L, const char *method, int32_t index);
static SoundT *_argSound(lua_State *L, const char *method, int32_t index);
static SpriteT *_argSprite(lua_State *L, const char *method, int32_t index);
static const char *_argString(lua_State *L, const char *method, int32_t index);
static Vec3T _argVec3(lua_State *L, const char *method, int32_t index);
static int32_t _argVehicle(lua_State *L, const char *method, int32_t index);
static VideoT *_argVideo(lua_State *L, const char *method, int32_t index);
static int32_t _argView(lua_State *L, const char *method, int32_t index);
static int32_t _axisIndexForCode(int32_t code);
static void _bezelDraw(void);
static void _bezelLoad(void);
static bool _bezelReadCutout(const char *path, int32_t width, int32_t height, SDL_FRect *cutout);
static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base, const char *method);
static void _callLua(const char *func, const char *sig, ...);
static uint32_t _capabilitiesFromList(lua_State *L, int32_t index, const char *method);
static void _capabilityUsed(uint32_t bit);
static bool _chordChanged(ChordT *chord, bool down, int32_t scancode);
static bool _chordsUpdate(bool down, int32_t scancode);
static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2);
static void _computeVideoRect(void);
static void _confFail(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn));
static void _configPlaces(const char *file, const char *dataDir, const char *scriptFile, char **places);
static int32_t _controllerSlot(SDL_JoystickID which);
static void _createScriptContext(void);
static int32_t _defaultVolume(int32_t maximum);
static bool _delayAndPump(uint32_t ms);
static void _deliverKey(bool down, int32_t keysym, int32_t scancode);
static void _deliverSwitch(InputE move, bool down);
static void _discApplyPicture(void);
static int64_t _discGetFrame(void);
static void _discPark(void);
static void _discSeek(int64_t frame);
static bool _discSetAudioSuffix(const char *suffix);
static void _doLogos(void);
static void _drawGuis(const SDL_FRect *target, bool screen);
static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel);
static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target);
static void _drawPauseIndicator(const SDL_FRect *target);
static void _drawRing(const SDL_FRect *outer, const SDL_FRect *inner, const SDL_Color *color);
static void _drawSindenBorder(void);
static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY);
static void _effectApplyGain(int32_t channel);
static float _effectGain(const EffectT *effect, float distance);
static void _effectReset(int32_t channel);
static void _effectStopped(void *userdata, MIX_Track *track);
static int32_t _effectTrackFree(void);
static void _engineAction(InputE engine);
static InputE _engineSwitch(int32_t scancode);
static void _fireMouseMoved(int32_t device, int32_t x, int32_t y, int32_t xr, int32_t yr);
static void _fitRect(int32_t width, int32_t height, int32_t spaceWidth, int32_t spaceHeight, int32_t margin, SDL_FRect *rect);
static void _fontDestroy(FontT *font);
static void _freezeGame(bool freeze);
static bool _gamepadMappingFor(SDL_JoystickID id, char *mapping, size_t size);
static void _gamepadOrder(int32_t *order, int32_t count);
static void _generateAudio(void);
static void _guiEvent(int32_t gui, int32_t document, const char *id, const char *event, const char *value);
static void _guiHandlerKey(char *buffer, size_t size, int32_t gui, int32_t document, const char *id, const char *event);
static bool _guiKey(SDL_Keycode key, SDL_Keymod modifiers, bool down);
static bool _guiMouseButton(int32_t button, bool down);
static void _guiMouseMove(int32_t x, int32_t y);
static bool _guiMouseWheel(float delta);
static bool _guiPointer(int32_t x, int32_t y, int32_t *gui, int32_t *guiX, int32_t *guiY);
static int32_t _guiQueueDraw(lua_State *L, const char *method, bool screen);
static void _guisShown(void);
static bool _guiSwitch(InputE move, bool down);
static bool _guiText(const char *text);
static void _guiTextInput(void);
static void _heldListUpdate(HeldKeyT *list, int32_t *count, bool down, int32_t keysym, int32_t scancode);
static bool _inputDevicesDenied(void);
static void _installFileHooks(lua_State *L);
static bool _isInputEvent(uint32_t type);
static void _joyMouseUpdate(void);
static void _joystickButtonTrace(const SDL_JoyButtonEvent *event);
static int32_t _lfsAttributes(lua_State *L);
static int32_t _lfsDir(lua_State *L);
static int32_t _lfsDirIterator(lua_State *L);
static void _lfsFillAttributes(lua_State *L, bool directory, int64_t size, int64_t modified);
static int32_t _lfsMkdir(lua_State *L);
static int32_t _lfsRmdir(lua_State *L);
static void _loadControlMappings(void);
static void _loadControlsFile(const char *path);
static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length);
static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface);
static void _loadGamepadDatabase(void);
static void _loadMachineSettings(void);
static void _logicalRect(SDL_FRect *rect);
static int32_t _luaCallOriginal(lua_State *L);
static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn));
static int32_t _luaDofile(lua_State *L);
static int32_t _luaFileClose(lua_State *L);
static int32_t _luaFileGc(lua_State *L);
static int32_t _luaFileGetc(LuaFileT *file);
static int32_t _luaFileIterate(lua_State *L);
static int32_t _luaFileLines(lua_State *L);
static int32_t _luaFileLinesFrom(lua_State *L, int32_t index, bool closing);
static int32_t _luaFileNoop(lua_State *L);
static LuaFileT *_luaFileOpen(lua_State *L, int32_t index);
static bool _luaFilePush(lua_State *L, const char *name);
static int32_t _luaFileRead(lua_State *L);
static void _luaFileReadAll(lua_State *L, LuaFileT *file);
static bool _luaFileReadCount(lua_State *L, LuaFileT *file, int64_t count);
static bool _luaFileReadFormat(lua_State *L, LuaFileT *file, int32_t index);
static bool _luaFileReadLine(lua_State *L, LuaFileT *file, bool keepEnding);
static bool _luaFileReadNumber(lua_State *L, LuaFileT *file);
static void _luaFileRegister(lua_State *L);
static int32_t _luaFileSearcher(lua_State *L);
static int32_t _luaFileSeek(lua_State *L);
static void _luaFileShut(LuaFileT *file);
static LuaFileT *_luaFileTest(lua_State *L, int32_t index);
static int32_t _luaFileToString(lua_State *L);
static void _luaFileUngetc(LuaFileT *file);
static int32_t _luaFileWrite(lua_State *L);
static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args);
static int32_t _luaIoClose(lua_State *L);
static void _luaIoForgetInput(lua_State *L);
static int32_t _luaIoHook(lua_State *L);
static LuaFileT *_luaIoInput(lua_State *L);
static int32_t _luaIoRead(lua_State *L);
static int32_t _luaIoSetInput(lua_State *L);
static int32_t _luaIoType(lua_State *L);
static int32_t _luaLoadfile(lua_State *L);
static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bool watch);
static int32_t _luaopenLfs(lua_State *L);
static int32_t _luaopenSqlite3(lua_State *L);
static int32_t _luaPanic(lua_State *L);
static int32_t _luaSearcher(lua_State *L);
static int32_t _luaSqliteOpen(lua_State *L);
static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
static int32_t _luaTraceback(lua_State *L);
static char *_machineFilePath(void);
static void _mapPointer(float px, float py, int32_t *x, int32_t *y);
static void _mapPointerDelta(float dx, float dy, int32_t *xr, int32_t *yr);
static void _mapUnknownJoysticks(void);
static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength);
static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index);
static uint8_t _midiData(lua_State *L, const char *method, int32_t index);
static void _midiReceived(void *context, const uint8_t *bytes, size_t size);
static float _mixerGain(int32_t volume, int32_t maximum);
static int32_t _mouseCode(int32_t device, int32_t button);
static void _musicDestroy(MusicT *music);
static void _navCallbacks(void);
static void _noteInput(void);
static void _overlayApplyOpacity(void);
static void _overlayApplyScaleMode(void);
static SDL_Rect _overlayArea(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t margin);
static uint32_t _overlayColor(const SDL_Color *color);
static SDL_Surface *_overlayGrey(void);
static void _overlayLayerDraw(int32_t index, const SDL_FRect *target);
static int32_t _overlayLayerOpen(void);
static void _overlayLayersFree(void);
static void _overlayResize(int32_t width, int32_t height);
static void _overlayTexturesDestroy(void);
static void _overlayTouched(const SDL_Rect *area);
static void _overlayUpload(SDL_Texture *texture, SDL_Surface *surface, const RegionT *region);
static bool _overlayVideoUnder(SDL_Texture *texture, const SDL_Rect *dest);
static void _overlayVideosDraw(const SDL_FRect *target);
static void _particleTexturesDestroy(ParticleTexturesT *cache);
static void _particleTexturesFree(int32_t emitter);
static void _particleTexturesFreeAll(void);
static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view);
static void _pauseAllVideos(bool pause);
static int _perfCall(lua_State *L); // lua_CFunction. Not changing int.
static int _perfCompare(const void *a, const void *b); // qsort callback. Not changing int.
static void _perfSync(void);
static void _perfTrace(uint64_t loopNS);
static void _perfWrapApi(lua_State *L);
static bool _physicallyHeld(int32_t scancode);
static void _physicsCallbacks(void);
static void _presentSize(int32_t *width, int32_t *height);
static void _processKey(bool down, int32_t keysym, int32_t scancode);
static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void _pushConstants(lua_State *L);
static int32_t _pushVec3(lua_State *L, Vec3T v);
static void _putPixel(int32_t x, int32_t y, uint32_t pixel);
static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad);
static void _quadScratch(int32_t quads, int32_t frames);
static bool _quitChordHold(int32_t scancode);
static void _quitChordLetGo(int32_t scancode);
static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha);
static SDL_Surface *_readWindow(void);
static void _recordUpdate(void);
static void _regionAdd(RegionT *region, const SDL_Rect *area);
static void _regionAddRegion(RegionT *region, const RegionT *other);
static void _regionClear(RegionT *region);
static void _registerApi(lua_State *L);
static void _releaseAxis(int32_t axisIndex);
static void _reloadScript(void);
static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message);
static SDL_Surface *_renderTextCached(lua_State *L, const char *method, const char *message);
static void _resetScriptState(void);
static void _rmlEscape(char *out, size_t size, const char *text);
static void _runScript(bool fatal);
static bool _sameLoosePath(const char *a, const char *b);
static void _saveCheckValue(lua_State *L, const char *method, int32_t index);
static bool _saveCjson(lua_State *L, const char *name);
static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth);
static void _saveMachineSettings(int32_t milliseconds);
static void _saveNormalise(lua_State *L, int32_t depth);
static void _saveTable(lua_State *L);
static void _saveWrite(void);
static SDL_Texture *_sceneVideoSource(int32_t player);
static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished);
static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex);
static void _scheduleReset(lua_State *L);
static void _scorePanelDigits(char *text, size_t size, int32_t value, int32_t digits, bool blank);
static bool _scorePanelEnable(bool enabled);
static int32_t _scorePanelPlayer(int32_t player);
static void _scorePanelQueue(void);
static void _scorePanelRect(SDL_FRect *dest);
static void _scorePanelReset(void);
static void _scorePanelUpdate(void);
static ConfigT *_scriptConfFromTable(lua_State *L, const char *method);
static void _selectDefaultAudioTrack(int32_t handle);
static void _setAudioMuted(bool muted);
static void _setMouseCaptured(bool captured);
static void _setPause(bool paused, bool fromKey);
static void _settingsAdd(SettingT **list, int32_t *count, const char *key, const char *value, const char *source);
static int32_t _settingsCompare(const void *a, const void *b);
static void _settingsReadFile(const char *path, SettingT **list, int32_t *count);
static bool _setVideoRotate(int32_t degrees);
static const char *_skipDotSlash(const char *path);
static void _soundDestroy(SoundT *sound);
static SoundT *_soundLoadFromIO(lua_State *L, const char *method, SDL_IOStream *io);
static int32_t _soundQueueDrain(int32_t *finished);
static void _spriteAdoptAnimation(SpriteT *sprite);
static void _spriteBlit(SpriteT *sprite, SDL_Surface *surface, const SDL_Rect *source, SDL_Rect *dest, bool stretched);
static bool _spriteBlitFast(SpriteT *sprite, SDL_Surface *surface, const SDL_Rect *source, const SDL_Rect *dest, SDL_BlendMode mode);
static void _spriteDestroy(SpriteT *sprite);
static SDL_Surface *_spriteFlippedFrame(SpriteT *sprite, SDL_Surface *source, int32_t frame);
static void _spriteFreeSurface(SpriteT *sprite);
static SpriteT *_spriteLoadFromIO(lua_State *L, const char *method, SDL_IOStream *io, int32_t width, int32_t height);
static SpriteT *_spriteNew(lua_State *L, const char *method);
static void _spriteRebuildSurface(SpriteT *sprite);
static void _spriteRegister(SpriteT *sprite);
static SDL_Surface *_spriteTransform(SpriteT *sprite, SDL_Surface *source);
static const char *_startAudioSuffix(void);
static void _startControllers(void);
static void _startLuaContext(lua_State *L);
static void _statsDraw(void);
static void _stopControllers(void);
static void _subsystemsInit(void);
static void _subsystemsQuit(void);
static void _subtitleClearCues(void);
static int32_t _subtitleCueNow(void);
static bool _subtitleEnsureGui(void);
static char *_subtitleNextLine(char **cursor);
static bool _subtitleParse(const char *text);
static bool _subtitleParseTime(const char *text, int64_t *frame);
static void _subtitleQueue(void);
static void _subtitleReset(void);
static void _subtitleShow(const char *text, int32_t position);
static void _subtitleUpdate(void);
static void _suppressHeldInput(void);
static uint32_t _surfaceCornerKey(SDL_Surface *surface);
static void _surfaceKey(SDL_Surface *surface);
static void _surfaceUnpack(SDL_Surface **surface);
static void _takeScreenshot(void);
static void _textCacheDrop(int32_t font);
static void _textCacheRemove(TextCacheT *entry);
static void _toggleFullScreen(void);
static void _unloadScriptResources(void);
static void _unrotateDelta(float dx, float dy, float *vx, float *vy);
static void _unrotatePoint(float px, float py, float *vx, float *vy);
static void _updatePauseState(void);
static void _updateSounds(void);
static void _videoDestroy(VideoT *video);
static bool _voiceIsAny(int32_t handle);
static bool _voiceIsDecoder(int32_t handle);
static bool _voiceIsEncoder(int32_t handle);
static bool _voiceIsJitter(int32_t handle);
static bool _voiceIsProcessor(int32_t handle);
static bool _watchedChanged(void);
static void _watchFile(const char *name);
static int32_t apiAllowSocketCall(lua_State *L);
static int32_t apiAnimationGetTime(lua_State *L);
static int32_t apiAnimationIsPlaying(lua_State *L);
static int32_t apiAnimationPause(lua_State *L);
static int32_t apiAnimationPlay(lua_State *L);
static int32_t apiAnimationPlayLayer(lua_State *L);
static int32_t apiAnimationResume(lua_State *L);
static int32_t apiAnimationSetLayerMask(lua_State *L);
static int32_t apiAnimationSetLayerWeight(lua_State *L);
static int32_t apiAnimationSetTime(lua_State *L);
static int32_t apiAnimationStop(lua_State *L);
static int32_t apiBodyApplyForce(lua_State *L);
static int32_t apiBodyApplyImpulse(lua_State *L);
static int32_t apiBodyDelete(lua_State *L);
static int32_t apiBodyGetAngularVelocity(lua_State *L);
static int32_t apiBodyGetVelocity(lua_State *L);
static int32_t apiBodyIsResting(lua_State *L);
static int32_t apiBodyNew(lua_State *L);
static int32_t apiBodySetAngularVelocity(lua_State *L);
static int32_t apiBodySetBounce(lua_State *L);
static int32_t apiBodySetBuoyancy(lua_State *L);
static int32_t apiBodySetCurrent(lua_State *L);
static int32_t apiBodySetEnabled(lua_State *L);
static int32_t apiBodySetFriction(lua_State *L);
static int32_t apiBodySetMass(lua_State *L);
static int32_t apiBodySetTrigger(lua_State *L);
static int32_t apiBodySetVelocity(lua_State *L);
static int32_t apiBodySetWater(lua_State *L);
static int32_t apiCameraSet(lua_State *L);
static int32_t apiCameraSetOrthographic(lua_State *L);
static int32_t apiCameraSetPerspective(lua_State *L);
static int32_t apiCollideCircles(lua_State *L);
static int32_t apiCollidePointCircle(lua_State *L);
static int32_t apiCollidePointPolygon(lua_State *L);
static int32_t apiCollidePointRect(lua_State *L);
static int32_t apiCollideRectCircle(lua_State *L);
static int32_t apiCollideRects(lua_State *L);
static int32_t apiCollideSegments(lua_State *L);
static int32_t apiColorBackground(lua_State *L);
static int32_t apiColorForeground(lua_State *L);
static int32_t apiControllerDoRumble(lua_State *L);
static int32_t apiControllerGetAxis(lua_State *L);
static int32_t apiControllerGetButton(lua_State *L);
static int32_t apiControllerGetName(lua_State *L);
static int32_t apiControllerGetPadding(lua_State *L);
static int32_t apiControllerHowMany(lua_State *L);
static int32_t apiControllerIsValid(lua_State *L);
static int32_t apiControllerSetPadding(lua_State *L);
static int32_t apiDebugPrint(lua_State *L);
static int32_t apiDiscAudio(lua_State *L);
static int32_t apiDiscAudioSuffix(lua_State *L);
static int32_t apiDiscChangeSpeed(lua_State *L);
static int32_t apiDiscGetAudioTrack(lua_State *L);
static int32_t apiDiscGetAudioTracks(lua_State *L);
static int32_t apiDiscGetSubtitleLanguage(lua_State *L);
static int32_t apiDiscGetSubtitleTracks(lua_State *L);
static int32_t apiDiscGetFrame(lua_State *L);
static int32_t apiDiscGetFrameCount(lua_State *L);
static int32_t apiDiscGetHeight(lua_State *L);
static int32_t apiDiscGetLanguage(lua_State *L);
static int32_t apiDiscGetState(lua_State *L);
static int32_t apiDiscGetWidth(lua_State *L);
static int32_t apiDiscPause(lua_State *L);
static int32_t apiDiscPlay(lua_State *L);
static int32_t apiDiscSearch(lua_State *L);
static int32_t apiDiscSearchBlanking(lua_State *L);
static int32_t apiDiscSetAudioTrack(lua_State *L);
static int32_t apiDiscSetFPS(lua_State *L);
static int32_t apiDiscSkipBackward(lua_State *L);
static int32_t apiDiscSkipBlanking(lua_State *L);
static int32_t apiDiscSkipForward(lua_State *L);
static int32_t apiDiscSkipToFrame(lua_State *L);
static int32_t apiDiscStepBackward(lua_State *L);
static int32_t apiDiscStepForward(lua_State *L);
static int32_t apiDiscStop(lua_State *L);
static int32_t apiDrawTransparent(lua_State *L);
static int32_t apiEmitterBurst(lua_State *L);
static int32_t apiEmitterClear(lua_State *L);
static int32_t apiEmitterDelete(lua_State *L);
static int32_t apiEmitterDraw(lua_State *L);
static int32_t apiEmitterGetCount(lua_State *L);
static int32_t apiEmitterIsActive(lua_State *L);
static int32_t apiEmitterNew(lua_State *L);
static int32_t apiEmitterSetBlend(lua_State *L);
static int32_t apiEmitterSetCollide(lua_State *L);
static int32_t apiEmitterSetColor(lua_State *L);
static int32_t apiEmitterSetDirection(lua_State *L);
static int32_t apiEmitterSetDrag(lua_State *L);
static int32_t apiEmitterSetFrames(lua_State *L);
static int32_t apiEmitterSetGravity(lua_State *L);
static int32_t apiEmitterSetLayer(lua_State *L);
static int32_t apiEmitterSetLife(lua_State *L);
static int32_t apiEmitterSetLit(lua_State *L);
static int32_t apiEmitterSetLocal(lua_State *L);
static int32_t apiEmitterSetMax(lua_State *L);
static int32_t apiEmitterSetPosition(lua_State *L);
static int32_t apiEmitterSetRadius(lua_State *L);
static int32_t apiEmitterSetRate(lua_State *L);
static int32_t apiEmitterSetSize(lua_State *L);
static int32_t apiEmitterSetSoftness(lua_State *L);
static int32_t apiEmitterSetSpeed(lua_State *L);
static int32_t apiEmitterSetSpin(lua_State *L);
static int32_t apiEmitterSetSpread(lua_State *L);
static int32_t apiEmitterSetTexture(lua_State *L);
static int32_t apiEmitterSetTrail(lua_State *L);
static int32_t apiEmitterStart(lua_State *L);
static int32_t apiEmitterStop(lua_State *L);
static int32_t apiFontLoad(lua_State *L);
static int32_t apiFontPrint(lua_State *L);
static int32_t apiFontQuality(lua_State *L);
static int32_t apiFontSelect(lua_State *L);
static int32_t apiFontToSprite(lua_State *L);
static int32_t apiFontUnload(lua_State *L);
static int32_t apiGetFValue(lua_State *L);
static int32_t apiGetUserString(lua_State *L);
static int32_t apiGuiClose(lua_State *L);
static int32_t apiGuiDelete(lua_State *L);
static int32_t apiGuiDraw(lua_State *L);
static int32_t apiGuiDrawScreen(lua_State *L);
static int32_t apiGuiGetHeight(lua_State *L);
static int32_t apiGuiGetStats(lua_State *L);
static int32_t apiGuiGetValue(lua_State *L);
static int32_t apiGuiGetWidth(lua_State *L);
static int32_t apiGuiHide(lua_State *L);
static int32_t apiGuiLoad(lua_State *L);
static int32_t apiGuiLoadFont(lua_State *L);
static int32_t apiGuiNew(lua_State *L);
static int32_t apiGuiRefresh(lua_State *L);
static int32_t apiGuiSetHandler(lua_State *L);
static int32_t apiGuiSetInput(lua_State *L);
static int32_t apiGuiSetValue(lua_State *L);
static int32_t apiGuiShow(lua_State *L);
static int32_t apiJitterGet(lua_State *L);
static int32_t apiJitterNew(lua_State *L);
static int32_t apiJitterPut(lua_State *L);
static int32_t apiJointBall(lua_State *L);
static int32_t apiJointDelete(lua_State *L);
static int32_t apiJointHinge(lua_State *L);
static int32_t apiJointSetLimits(lua_State *L);
static int32_t apiJointSlider(lua_State *L);
static int32_t apiJoyMouseEnable(lua_State *L);
static int32_t apiJoyMouseIsEnabled(lua_State *L);
static int32_t apiKeyboardGetLastDown(lua_State *L);
static int32_t apiKeyboardGetLastUp(lua_State *L);
static int32_t apiKeyboardGetMode(lua_State *L);
static int32_t apiKeyboardGetModifiers(lua_State *L);
static int32_t apiKeyboardIsDown(lua_State *L);
static int32_t apiKeyboardSetMode(lua_State *L);
static int32_t apiLightNew(lua_State *L);
static int32_t apiLightSetColor(lua_State *L);
static int32_t apiLightSetCone(lua_State *L);
static int32_t apiLightSetIntensity(lua_State *L);
static int32_t apiLightSetRange(lua_State *L);
static int32_t apiLightSetShadow(lua_State *L);
static int32_t apiLineDraw(lua_State *L);
static int32_t apiMainBezelLoaded(lua_State *L);
static int32_t apiMaterialDelete(lua_State *L);
static int32_t apiMaterialNew(lua_State *L);
static int32_t apiMaterialSetBlend(lua_State *L);
static int32_t apiMaterialSetColor(lua_State *L);
static int32_t apiMaterialSetCutoff(lua_State *L);
static int32_t apiMaterialSetDoubleSided(lua_State *L);
static int32_t apiMaterialSetEmissive(lua_State *L);
static int32_t apiMaterialSetEmissiveMap(lua_State *L);
static int32_t apiMaterialSetFilter(lua_State *L);
static int32_t apiMaterialSetGui(lua_State *L);
static int32_t apiMaterialSetMetallic(lua_State *L);
static int32_t apiMaterialSetMetallicRoughnessMap(lua_State *L);
static int32_t apiMaterialSetNormalMap(lua_State *L);
static int32_t apiMaterialSetOccluder(lua_State *L);
static int32_t apiMaterialSetOcclusionMap(lua_State *L);
static int32_t apiMaterialSetRoughness(lua_State *L);
static int32_t apiMaterialSetTexture(lua_State *L);
static int32_t apiMaterialSetTiling(lua_State *L);
static int32_t apiMaterialSetUnlit(lua_State *L);
static int32_t apiMaterialSetVideo(lua_State *L);
static int32_t apiMaterialSetView(lua_State *L);
static int32_t apiMathRandomseed(lua_State *L);
static int32_t apiMeshBox(lua_State *L);
static int32_t apiMeshCone(lua_State *L);
static int32_t apiMeshCylinder(lua_State *L);
static int32_t apiMeshDelete(lua_State *L);
static int32_t apiMeshHeightmap(lua_State *L);
static int32_t apiMeshNew(lua_State *L);
static int32_t apiMeshPlane(lua_State *L);
static int32_t apiMeshSphere(lua_State *L);
static int32_t apiMeshTorus(lua_State *L);
static int32_t apiMicCalibrate(lua_State *L);
static int32_t apiMicCalibration(lua_State *L);
static int32_t apiMicClose(lua_State *L);
static int32_t apiMicIsOpen(lua_State *L);
static int32_t apiMicLatency(lua_State *L);
static int32_t apiMicLevel(lua_State *L);
static int32_t apiMicList(lua_State *L);
static int32_t apiMicMonitor(lua_State *L);
static int32_t apiMicOpen(lua_State *L);
static int32_t apiMicPitch(lua_State *L);
static int32_t apiMicRead(lua_State *L);
static int32_t apiMicRecordStart(lua_State *L);
static int32_t apiMicRecordStop(lua_State *L);
static int32_t apiMicSetLatency(lua_State *L);
static int32_t apiMidiCloseInput(lua_State *L);
static int32_t apiMidiCloseOutput(lua_State *L);
static int32_t apiMidiControlChange(lua_State *L);
static int32_t apiMidiInputCount(lua_State *L);
static int32_t apiMidiInputName(lua_State *L);
static int32_t apiMidiIsInputOpen(lua_State *L);
static int32_t apiMidiIsOutputOpen(lua_State *L);
static int32_t apiMidiNoteOff(lua_State *L);
static int32_t apiMidiNoteOn(lua_State *L);
static int32_t apiMidiOpenInput(lua_State *L);
static int32_t apiMidiOpenOutput(lua_State *L);
static int32_t apiMidiOutputCount(lua_State *L);
static int32_t apiMidiOutputName(lua_State *L);
static int32_t apiMidiPitchBend(lua_State *L);
static int32_t apiMidiProgramChange(lua_State *L);
static int32_t apiMidiRescan(lua_State *L);
static int32_t apiMidiSend(lua_State *L);
static int32_t apiModelDelete(lua_State *L);
static int32_t apiModelGetAnimations(lua_State *L);
static int32_t apiModelInstance(lua_State *L);
static int32_t apiModelLoad(lua_State *L);
static int32_t apiMouseGetPadding(lua_State *L);
static int32_t apiMusicGetPosition(lua_State *L);
static int32_t apiMusicIsPlaying(lua_State *L);
static int32_t apiMusicLoad(lua_State *L);
static int32_t apiMusicPause(lua_State *L);
static int32_t apiMusicPlay(lua_State *L);
static int32_t apiMusicResume(lua_State *L);
static int32_t apiMusicSetVolume(lua_State *L);
static int32_t apiMusicStop(lua_State *L);
static int32_t apiMusicUnload(lua_State *L);
static int32_t apiMouseGetName(lua_State *L);
static int32_t apiMouseGetPosition(lua_State *L);
static int32_t apiMouseHowMany(lua_State *L);
static int32_t apiMouseSetCaptured(lua_State *L);
static int32_t apiMouseSetEnabled(lua_State *L);
static int32_t apiMouseSetMode(lua_State *L);
static int32_t apiNavAddNode(lua_State *L);
static int32_t apiNavAgentDelete(lua_State *L);
static int32_t apiNavAgentGetVelocity(lua_State *L);
static int32_t apiNavAgentIsArrived(lua_State *L);
static int32_t apiNavAgentMoveTo(lua_State *L);
static int32_t apiNavAgentNew(lua_State *L);
static int32_t apiNavAgentSetPlayer(lua_State *L);
static int32_t apiNavAgentStop(lua_State *L);
static int32_t apiNavBuild(lua_State *L);
static int32_t apiNavDelete(lua_State *L);
static int32_t apiNavDraw(lua_State *L);
static int32_t apiNavLoad(lua_State *L);
static int32_t apiNavNearest(lua_State *L);
static int32_t apiNavNew(lua_State *L);
static int32_t apiNavPath(lua_State *L);
static int32_t apiNavRandomPoint(lua_State *L);
static int32_t apiNavRaycast(lua_State *L);
static int32_t apiNavSave(lua_State *L);
static int32_t apiNetTrustSystem(lua_State *L);
static int32_t apiNodeDelete(lua_State *L);
static int32_t apiNodeFind(lua_State *L);
static int32_t apiNodeGetChildren(lua_State *L);
static int32_t apiNodeGetMorph(lua_State *L);
static int32_t apiNodeGetMorphs(lua_State *L);
static int32_t apiNodeGetName(lua_State *L);
static int32_t apiNodeGetParent(lua_State *L);
static int32_t apiNodeGetPosition(lua_State *L);
static int32_t apiNodeGetQuaternion(lua_State *L);
static int32_t apiNodeGetRotation(lua_State *L);
static int32_t apiNodeGetScale(lua_State *L);
static int32_t apiNodeGetWorldPosition(lua_State *L);
static int32_t apiNodeLookAt(lua_State *L);
static int32_t apiNodeMove(lua_State *L);
static int32_t apiNodeNew(lua_State *L);
static int32_t apiNodeRotate(lua_State *L);
static int32_t apiNodeSetBillboard(lua_State *L);
static int32_t apiNodeSetLayer(lua_State *L);
static int32_t apiNodeSetMaterial(lua_State *L);
static int32_t apiNodeSetMesh(lua_State *L);
static int32_t apiNodeSetMorph(lua_State *L);
static int32_t apiNodeSetName(lua_State *L);
static int32_t apiNodeSetParent(lua_State *L);
static int32_t apiNodeSetPosition(lua_State *L);
static int32_t apiNodeSetQuaternion(lua_State *L);
static int32_t apiNodeSetRotation(lua_State *L);
static int32_t apiNodeSetScale(lua_State *L);
static int32_t apiNodeSetShadow(lua_State *L);
static int32_t apiNodeSetSprite(lua_State *L);
static int32_t apiNodeSetSpriteFrame(lua_State *L);
static int32_t apiNodeSetText(lua_State *L);
static int32_t apiNodeSetVisible(lua_State *L);
static int32_t apiOpusDecode(lua_State *L);
static int32_t apiOpusDecoderNew(lua_State *L);
static int32_t apiOpusEncode(lua_State *L);
static int32_t apiOpusEncoderNew(lua_State *L);
static int32_t apiOpusEncoderSet(lua_State *L);
static int32_t apiOsClock(lua_State *L);
static int32_t apiOsTime(lua_State *L);
static int32_t apiOverlayBanner(lua_State *L);
static int32_t apiOverlayBox(lua_State *L);
static int32_t apiOverlayCircle(lua_State *L);
static int32_t apiOverlayClear(lua_State *L);
static int32_t apiOverlayEllipse(lua_State *L);
static int32_t apiOverlayGetFontHeight(lua_State *L);
static int32_t apiOverlayGetFontWidth(lua_State *L);
static int32_t apiOverlayGetHeight(lua_State *L);
static int32_t apiOverlayGetWidth(lua_State *L);
static int32_t apiOverlayLine(lua_State *L);
static int32_t apiOverlayPlot(lua_State *L);
static int32_t apiOverlayPrint(lua_State *L);
static int32_t apiOverlaySetMonochrome(lua_State *L);
static int32_t apiOverlaySetResolution(lua_State *L);
static int32_t apiPhysicsRaycast(lua_State *L);
static int32_t apiPhysicsSet2D(lua_State *L);
static int32_t apiPhysicsSetDebug(lua_State *L);
static int32_t apiPhysicsSetEnabled(lua_State *L);
static int32_t apiPhysicsSetGravity(lua_State *L);
static int32_t apiPlayerDelete(lua_State *L);
static int32_t apiPlayerGetGround(lua_State *L);
static int32_t apiPlayerGetVelocity(lua_State *L);
static int32_t apiPlayerIsOnGround(lua_State *L);
static int32_t apiPlayerIsSwimming(lua_State *L);
static int32_t apiPlayerJump(lua_State *L);
static int32_t apiPlayerMove(lua_State *L);
static int32_t apiPlayerNew(lua_State *L);
static int32_t apiPlayerSetEnabled(lua_State *L);
static int32_t apiPlayerSetGravityScale(lua_State *L);
static int32_t apiPlayerSetMass(lua_State *L);
static int32_t apiPlayerSetPosition(lua_State *L);
static int32_t apiPlayerSetPush(lua_State *L);
static int32_t apiPlayerSetSlope(lua_State *L);
static int32_t apiPlayerSetStep(lua_State *L);
static int32_t apiPlayerSetSwim(lua_State *L);
static int32_t apiPlayerSetVelocity(lua_State *L);
static int32_t apiRagdollActivate(lua_State *L);
static int32_t apiRagdollApplyImpulse(lua_State *L);
static int32_t apiRagdollDeactivate(lua_State *L);
static int32_t apiRagdollDelete(lua_State *L);
static int32_t apiRagdollIsActive(lua_State *L);
static int32_t apiRagdollIsResting(lua_State *L);
static int32_t apiRagdollNew(lua_State *L);
static int32_t apiRagdollSetJoint(lua_State *L);
static int32_t apiRagdollSetStrength(lua_State *L);
static int32_t apiRatioGetX(lua_State *L);
static int32_t apiRatioGetY(lua_State *L);
static int32_t apiSaveClear(lua_State *L);
static int32_t apiSaveDelete(lua_State *L);
static int32_t apiSaveFlush(lua_State *L);
static int32_t apiSaveGet(lua_State *L);
static int32_t apiSaveGetAll(lua_State *L);
static int32_t apiSaveSet(lua_State *L);
static int32_t apiSaveSetAll(lua_State *L);
static int32_t apiSceneEnable(lua_State *L);
static int32_t apiSceneGetSize(lua_State *L);
static int32_t apiSceneGetStats(lua_State *L);
static int32_t apiSceneMeasure(lua_State *L);
static int32_t apiSceneProbeGui(lua_State *L);
static int32_t apiSceneProject(lua_State *L);
static int32_t apiSceneSetAmbient(lua_State *L);
static int32_t apiSceneSetAntialias(lua_State *L);
static int32_t apiSceneSetBackground(lua_State *L);
static int32_t apiSceneSetBloom(lua_State *L);
static int32_t apiSceneSetEnvironment(lua_State *L);
static int32_t apiSceneSetExposure(lua_State *L);
static int32_t apiSceneSetFog(lua_State *L);
static int32_t apiSceneSetShadowCascades(lua_State *L);
static int32_t apiSceneSetShadowDistance(lua_State *L);
static int32_t apiSceneSetShadowSize(lua_State *L);
static int32_t apiSceneSetSky(lua_State *L);
static int32_t apiSceneSetSkyIntensity(lua_State *L);
static int32_t apiSceneSetTonemap(lua_State *L);
static int32_t apiSceneUnproject(lua_State *L);
static int32_t apiScoreBezelClear(lua_State *L);
static int32_t apiScoreBezelCredits(lua_State *L);
static int32_t apiScoreBezelEnable(lua_State *L);
static int32_t apiScoreBezelGetState(lua_State *L);
static int32_t apiScoreBezelLives(lua_State *L);
static int32_t apiScoreBezelScore(lua_State *L);
static int32_t apiScoreBezelTwinScoreOn(lua_State *L);
static int32_t apiScriptExecute(lua_State *L);
static int32_t apiScriptPush(lua_State *L);
static int32_t apiSetOverlayFullAlpha(lua_State *L);
static int32_t apiSetOverlayLinearScale(lua_State *L);
static int32_t apiSetOverlayOnTop(lua_State *L);
static int32_t apiSetOverlayOpacity(lua_State *L);
static int32_t apiSingeCapabilities(lua_State *L);
static int32_t apiSingeGetAudioCalibration(lua_State *L);
static int32_t apiSingeGetAudioDelay(lua_State *L);
static int32_t apiSingeGetAudioLatency(lua_State *L);
static int32_t apiSingeGetDataPath(lua_State *L);
static int32_t apiSingeGetSystemInfo(lua_State *L);
static int32_t apiSingeSaveGeometry(lua_State *L);
static int32_t apiSingeSaveTier(lua_State *L);
static int32_t apiSingeGetGameId(lua_State *L);
static int32_t apiSingeGetHeight(lua_State *L);
static int32_t apiSingeGetPauseFlag(lua_State *L);
static int32_t apiSingeGetScriptPath(lua_State *L);
static int32_t apiSingeGetTicks(lua_State *L);
static int32_t apiSingeGetWidth(lua_State *L);
static int32_t apiSingeHasGpu(lua_State *L);
static int32_t apiSingeIsRecording(lua_State *L);
static int32_t apiSingePack(lua_State *L);
static int32_t apiSingeQuit(lua_State *L);
static int32_t apiSingeRecordStart(lua_State *L);
static int32_t apiSingeRecordStop(lua_State *L);
static int32_t apiSingeRelaunch(lua_State *L);
static int32_t apiSingeReload(lua_State *L);
static int32_t apiSingeScreenshot(lua_State *L);
static int32_t apiSingeSetAudioCalibration(lua_State *L);
static int32_t apiSingeSetAudioDelay(lua_State *L);
static int32_t apiSingeSetGameName(lua_State *L);
static int32_t apiSingeSetPauseFlag(lua_State *L);
static int32_t apiSingeSetPauseKeyEnabled(lua_State *L);
static int32_t apiSingeSetQuitKeyEnabled(lua_State *L);
static int32_t apiSingeVersion(lua_State *L);
static int32_t apiUtilSha256(lua_State *L);
static int32_t apiSingeWantsCrosshairs(lua_State *L);
static int32_t apiSoftDelete(lua_State *L);
static int32_t apiSoftNew(lua_State *L);
static int32_t apiSoftPin(lua_State *L);
static int32_t apiSoftSetDamping(lua_State *L);
static int32_t apiSoftSetMass(lua_State *L);
static int32_t apiSoftSetPressure(lua_State *L);
static int32_t apiSoftSetStiffness(lua_State *L);
static int32_t apiSoftUnpin(lua_State *L);
static int32_t apiSoundFullStop(lua_State *L);
static int32_t apiSoundGetPosition(lua_State *L);
static int32_t apiSoundGetVolume(lua_State *L);
static int32_t apiSoundIsPlaying(lua_State *L);
static int32_t apiSoundLoad(lua_State *L);
static int32_t apiSoundLoadData(lua_State *L);
static int32_t apiSoundPause(lua_State *L);
static int32_t apiSoundPlay(lua_State *L);
static int32_t apiSoundResume(lua_State *L);
static int32_t apiSoundSetChannelVolume(lua_State *L);
static int32_t apiSoundSetListener(lua_State *L);
static int32_t apiSoundSetNode(lua_State *L);
static int32_t apiSoundSetPan(lua_State *L);
static int32_t apiSoundSetPosition(lua_State *L);
static int32_t apiSoundSetRange(lua_State *L);
static int32_t apiSoundSetVolume(lua_State *L);
static int32_t apiSoundStop(lua_State *L);
static int32_t apiSoundUnload(lua_State *L);
static int32_t apiSpriteBlend(lua_State *L);
static int32_t apiSpriteDraw(lua_State *L);
static int32_t apiSpriteDrawFrame(lua_State *L);
static int32_t apiSpriteDrawGrid(lua_State *L);
static int32_t apiSpriteDrawRotatedFrame(lua_State *L);
static int32_t apiSpriteFlip(lua_State *L);
static int32_t apiSpriteFrameHeight(lua_State *L);
static int32_t apiSpriteFrameWidth(lua_State *L);
static int32_t apiSpriteGetFrame(lua_State *L);
static int32_t apiSpriteGetFrames(lua_State *L);
static int32_t apiSpriteGetHeight(lua_State *L);
static int32_t apiSpriteGetWidth(lua_State *L);
static int32_t apiSpriteIsPlaying(lua_State *L);
static int32_t apiSpriteLoad(lua_State *L);
static int32_t apiSpriteLoadData(lua_State *L);
static int32_t apiSpriteLoadFrames(lua_State *L);
static int32_t apiSpriteLoop(lua_State *L);
static int32_t apiSpriteNew(lua_State *L);
static int32_t apiSpritePause(lua_State *L);
static int32_t apiSpritePlay(lua_State *L);
static int32_t apiSpriteQuality(lua_State *L);
static int32_t apiSpriteResetColorKey(lua_State *L);
static int32_t apiSpriteRotate(lua_State *L);
static int32_t apiSpriteRotateAndScale(lua_State *L);
static int32_t apiSpriteRotateFrame(lua_State *L);
static int32_t apiSpriteScale(lua_State *L);
static int32_t apiSpriteSetAlpha(lua_State *L);
static int32_t apiSpriteSetBlend(lua_State *L);
static int32_t apiSpriteSetFrame(lua_State *L);
static int32_t apiSpriteSetPixels(lua_State *L);
static int32_t apiSpriteSetTint(lua_State *L);
static int32_t apiSpriteUnload(lua_State *L);
static int32_t apiSrtClear(lua_State *L);
static int32_t apiSrtEnable(lua_State *L);
static int32_t apiSrtLoad(lua_State *L);
static int32_t apiSrtLoadTrack(lua_State *L);
static int32_t apiSrtPosition(lua_State *L);
static int32_t apiStatsEnable(lua_State *L);
static int32_t apiStatsIsEnabled(lua_State *L);
static int32_t apiTerrainGetHeight(lua_State *L);
static int32_t apiTimerAfter(lua_State *L);
static int32_t apiTimerCancel(lua_State *L);
static int32_t apiTimerEvery(lua_State *L);
static int32_t apiTimerIsActive(lua_State *L);
static int32_t apiTweenCancel(lua_State *L);
static int32_t apiTweenIsActive(lua_State *L);
static int32_t apiTweenValue(lua_State *L);
static int32_t apiVehicleAddWheel(lua_State *L);
static int32_t apiVehicleDelete(lua_State *L);
static int32_t apiVehicleDrive(lua_State *L);
static int32_t apiVehicleGetGear(lua_State *L);
static int32_t apiVehicleGetRpm(lua_State *L);
static int32_t apiVehicleGetSpeed(lua_State *L);
static int32_t apiVehicleGetWheelSlip(lua_State *L);
static int32_t apiVehicleIsWheelOnGround(lua_State *L);
static int32_t apiVehicleNew(lua_State *L);
static int32_t apiVehicleSetAntiRoll(lua_State *L);
static int32_t apiVehicleSetBrakes(lua_State *L);
static int32_t apiVehicleSetEngine(lua_State *L);
static int32_t apiVehicleSetGears(lua_State *L);
static int32_t apiVehicleSetRudder(lua_State *L);
static int32_t apiVehicleSetSteering(lua_State *L);
static int32_t apiVehicleSetSuspension(lua_State *L);
static int32_t apiVehicleSetThrust(lua_State *L);
static int32_t apiVehicleSetWheel(lua_State *L);
static int32_t apiVideoDraw(lua_State *L);
static int32_t apiVideoGetAudioTrack(lua_State *L);
static int32_t apiVideoGetAudioTracks(lua_State *L);
static int32_t apiVideoGetFrame(lua_State *L);
static int32_t apiVideoGetFrameCount(lua_State *L);
static int32_t apiVideoGetHeight(lua_State *L);
static int32_t apiVideoGetLanguage(lua_State *L);
static int32_t apiVideoGetLanguageDescription(lua_State *L);
static int32_t apiVideoGetVolume(lua_State *L);
static int32_t apiVideoGetWidth(lua_State *L);
static int32_t apiVideoIsPlaying(lua_State *L);
static int32_t apiVideoLoad(lua_State *L);
static int32_t apiVideoPause(lua_State *L);
static int32_t apiVideoPlay(lua_State *L);
static int32_t apiVideoQuality(lua_State *L);
static int32_t apiVideoRotate(lua_State *L);
static int32_t apiVideoRotateAndScale(lua_State *L);
static int32_t apiVideoScale(lua_State *L);
static int32_t apiVideoSeek(lua_State *L);
static int32_t apiVideoSetAudioTrack(lua_State *L);
static int32_t apiVideoSetVolume(lua_State *L);
static int32_t apiVideoUnload(lua_State *L);
static int32_t apiViewDelete(lua_State *L);
static int32_t apiViewNew(lua_State *L);
static int32_t apiViewSetCamera(lua_State *L);
static int32_t apiVldpFlash(lua_State *L);
static int32_t apiVldpFocusArea(lua_State *L);
static int32_t apiVldpGetPixel(lua_State *L);
static int32_t apiVldpGetRotate(lua_State *L);
static int32_t apiVldpGetScale(lua_State *L);
static int32_t apiVldpGetYUVPixel(lua_State *L);
static int32_t apiVldpResetFocus(lua_State *L);
static int32_t apiVldpSetBlend(lua_State *L);
static int32_t apiVldpSetLuma(lua_State *L);
static int32_t apiVldpSetMonochrome(lua_State *L);
static int32_t apiVldpSetRotate(lua_State *L);
static int32_t apiVldpGetShift(lua_State *L);
static int32_t apiVldpSetScale(lua_State *L);
static int32_t apiVldpSetShift(lua_State *L);
static int32_t apiVldpSetVerbose(lua_State *L);
static int32_t apiVoiceFree(lua_State *L);
static int32_t apiVoiceProcess(lua_State *L);
static int32_t apiVoiceProcessorNew(lua_State *L);
#define MODL(name, array) { name, { (const char *)array }, sizeof(array) }
#define MODC(name, openf) { name, { (const char *)openf }, 0 }
// Lua Modules
static const LuaModuleT _luaModules[] = {
// LuaFileSystem
MODC("lfs", _luaopenLfs),
// md5, the PUC-Rio library Hypseus bundles: sum, hexsum, exor, crypt, decrypt
MODC("md5", luaopen_md5_core),
// The rest of what Hypseus exposes, so a game written against it finds the same modules here
MODC("bit", luaopen_bit),
MODC("random", luaopen_random),
MODC("cjson", luaopen_cjson),
MODC("lpeg", luaopen_lpeg),
// SQLite for script data
MODC("sqlite3", _luaopenSqlite3),
// LuaSocket
MODC("mime.core", luaopen_mime_core),
MODC("socket.core", luaopen_socket_core),
MODL("ltn12", ltn12_lua),
MODL("mbox", mbox_lua),
MODL("mime", mime_lua),
MODL("socket", socket_lua),
MODL("socket.ftp", ftp_lua),
MODL("socket.headers", headers_lua),
MODL("socket.http", http_lua),
MODL("socket.smtp", smtp_lua),
MODL("socket.tp", tp_lua),
MODL("socket.url", url_lua),
#ifndef _WIN32
MODC("socket.unix", luaopen_socket_unix),
MODC("socket.serial", luaopen_socket_serial),
#endif
// LuaSec
MODC("ssl.core", luaopen_ssl_core),
MODC("ssl.context", luaopen_ssl_context),
MODC("ssl.x509", luaopen_ssl_x509),
MODC("ssl.config", luaopen_ssl_config),
MODL("ssl.https", https_lua),
MODL("ssl", ssl_lua),
// LuaRS232
MODC("rs232.core", luaopen_luars232),
MODL("rs232", rs232_lua),
// binaryheap
MODL("binaryheap", binaryheap_lua),
// timerwheel
MODL("timerwheel", timerwheel_lua),
// json
MODL("json", json_lua),
// middleclass, for games that want classes to build their entities out of
MODL("middleclass", middleclass_lua),
// lume, the small things every game needs: lerp, clamp, round, shuffle, split, serialise
MODL("lume", lume_lua),
// inspect, for printing a table in a shape a person can read
MODL("inspect", inspect_lua),
// bump, which answers what collide* deliberately does not: where a mover ends up
MODL("bump", bump_lua),
// Copas
MODL("copas", copas_lua),
MODL("copas.ftp", copas_ftp_lua),
MODL("copas.future", copas_future_lua),
MODL("copas.http", copas_http_lua),
MODL("copas.lock", copas_lock_lua),
MODL("copas.queue", copas_queue_lua),
MODL("copas.semaphore", copas_semaphore_lua),
MODL("copas.smtp", copas_smtp_lua),
MODL("copas.timer", copas_timer_lua),
};
// ===== Internal helpers =====
// What a press or a release does once it counts: the engine's own switches, the chords, and the
// script. _processKey records it first, and may hold a press back for the quit chord.
static void _actOnKey(bool down, int32_t keysym, int32_t scancode) {
InputE engine = INPUT_COUNT;
bool keyboard = (scancode < CODE_GAMEPAD_BASE); // Every defined scancode is below the gamepad range
// Engine owned switches act on the press. Keyboard mappings only count in MODE_NORMAL, so
// full mode keeps every key for the game; gamepad and mouse buttons cannot be typed, so they
// always count.
if ((_global.keyboardMode == KEYBOARD_NORMAL) || !keyboard) {
engine = _engineSwitch(scancode);
}
if (down && _global.conf->reload && (scancode == SDL_SCANCODE_F5)) {
_global.reloadRequested = true;
}
if (down) {
_engineAction(engine);
}
// Chords, under the same rule: in full mode the keyboard belongs to the game. The press that
// completes the machine's quit chord, and later its release, go no further.
if (((_global.keyboardMode == KEYBOARD_NORMAL) || !keyboard) && _chordsUpdate(down, scancode)) {
return;
}
if (!down && (scancode == _global.quitChordRelease)) {
_global.quitChordRelease = -1;
return;
}
// The script never sees the pause key while the engine owns it, or anything while frozen.
if (_global.frozen || ((engine == INPUT_PAUSE) && _global.pauseEnabled)) {
return;
}
_deliverKey(down, keysym, scancode);
}
// Legacy Daphne functions that Singe never implemented. They trace and return nothing.
static int32_t _apiUnimplemented(lua_State *L, const char *method) {
_luaTrace(L, method, "Unimplemented");
return 0;
}
// An animation by name or by its number from 1, as modelGetAnimations lists them.
static int32_t _argAnimation(lua_State *L, const char *method, int32_t model, int32_t index) {
int32_t animation;
if (lua_type(L, index) == LUA_TSTRING) {
animation = modelAnimationIndex(model, lua_tostring(L, index));
if (animation < 0) {
_luaDie(L, method, "Model %d has no animation named %s.", model, lua_tostring(L, index));
}
return animation;
}
return _argInteger(L, method, index) - 1;
}
// A layer number above the base, 1 to ANIMATION_LAYERS.
static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t index) {
int32_t layer = _argInteger(L, method, index);
if ((layer < 1) || (layer > ANIMATION_LAYERS)) {
_luaDie(L, method, "Layer must be 1 to %d: %d", ANIMATION_LAYERS, layer);
}
return layer;
}
// A node that carries a physics body, checked.
static int32_t _argBody(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, bodyExists, "body");
}
static bool _argBoolean(lua_State *L, const char *method, int32_t index) {
if (!lua_isboolean(L, index)) {
_luaDie(L, method, "Argument %d must be a boolean.", index);
}
return lua_toboolean(L, index) != 0;
}
// An effect channel number as soundPlay returned it.
static int32_t _argChannel(lua_State *L, const char *method, int32_t index) {
int32_t channel = _argInteger(L, method, index);
if ((channel < 0) || (channel >= EFFECT_TRACKS)) {
_luaDie(L, method, "Invalid channel: %d", channel);
}
return channel;
}
// Dies unless the argument count is within [minimum, maximum].
static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum) {
int32_t n = lua_gettop(L);
if ((n < minimum) || (n > maximum)) {
if (minimum == maximum) {
_luaDie(L, method, "Expected %d argument(s), got %d.", minimum, n);
}
_luaDie(L, method, "Expected %d to %d arguments, got %d.", minimum, maximum, n);
}
}
// A colour component argument, clamped to 0..255.
static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) {
int32_t value = _argInteger(L, method, index);
return (uint8_t)SDL_clamp(value, 0, COLOR_BYTE_MAX);
}
// A block of bytes held in a Lua string, for the calls that build an asset from memory. A Lua
// string may hold a zero byte, so the length comes back beside the pointer.
static const char *_argData(lua_State *L, const char *method, int32_t index, size_t *length) {
const char *data = NULL;
if (lua_type(L, index) != LUA_TSTRING) {
_luaDie(L, method, "Argument %d must be a string of bytes.", index);
}
data = lua_tolstring(L, index, length);
if (*length == 0) {
_luaDie(L, method, "Argument %d is empty.", index);
}
return data;
}
// An emitter handle argument, checked.
static int32_t _argEmitter(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, emitterValid, "emitter");
}
// The scene's rotation arguments: three Euler angles in degrees.
static QuatT _argEuler(lua_State *L, const char *method, int32_t index) {
return quatFromEuler((float)_argNumber(L, method, index), (float)_argNumber(L, method, index + 1), (float)_argNumber(L, method, index + 2));
}
// A table of numbers as a float array (caller frees). A nil argument gives NULL and count 0.
static float *_argFloatTable(lua_State *L, const char *method, int32_t index, int32_t *count) {
float *values;
int32_t x;
*count = 0;
if (lua_isnoneornil(L, index)) {
return NULL;
}
if (!lua_istable(L, index)) {
_luaDie(L, method, "Argument %d must be a table of numbers.", index);
}
*count = (int32_t)lua_rawlen(L, index);
values = SDL_calloc((size_t)SDL_max(*count, 1), sizeof(float));
if (values == NULL) {
_luaDie(L, method, "Out of memory.");
}
for (x = 0; x < *count; x++) {
lua_rawgeti(L, index, x + 1);
if (!lua_isnumber(L, -1)) {
SDL_free(values);
_luaDie(L, method, "Argument %d, element %d is not a number.", index, x + 1);
}
values[x] = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
}
return values;
}
static FontT *_argFont(lua_State *L, const char *method, int32_t index) {
int32_t id = _argInteger(L, method, index);
FontT *font = NULL;
HASH_FIND_INT(_global.fontList, &id, font);
if (!font) {
_luaDie(L, method, "No font at index %d.", id);
}
return font;
}
static int32_t _argGui(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, guiValid, "GUI");
}
// A handle argument the given test accepts, named for the message: "No material 7."
static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun) {
int32_t handle = _argInteger(L, method, index);
if (!valid(handle)) {
_luaDie(L, method, "No %s %d.", noun, handle);
}
return handle;
}
static int32_t _argInteger(lua_State *L, const char *method, int32_t index) {
return (int32_t)_argNumber(L, method, index);
}
static int64_t _argInteger64(lua_State *L, const char *method, int32_t index) {
return (int64_t)_argNumber(L, method, index);
}
// A texture argument for a material map: a loaded sprite (its surface comes back), or the name of a
// KTX2 file (transcoded into ktx2, true comes back and the caller frees it).
static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Surface **surface, Ktx2ImageT *ktx2) {
const char *name;
char *bytes;
size_t size = 0;
bool ok;
*surface = NULL;
memset(ktx2, 0, sizeof(*ktx2));
if (lua_type(L, index) != LUA_TSTRING) {
*surface = _argSprite(L, method, index)->originalSurface;
return false;
}
name = lua_tostring(L, index);
bytes = vfsRead(name, &size);
if (bytes == NULL) {
_luaDie(L, method, "Unable to read %s", name);
}
if (!ktx2Is(bytes, size)) {
free(bytes);
_luaDie(L, method, "%s is not a KTX2 texture (load other pictures with spriteLoad).", name);
}
ok = ktx2Transcode(bytes, size, sceneCompressedFormat(), ktx2);
free(bytes);
if (!ok) {
_luaDie(L, method, "%s: %s", name, SDL_GetError());
}
return true;
}
// A material handle argument, checked.
static int32_t _argMaterial(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, materialValid, "material");
}
// A mesh handle argument, checked.
static int32_t _argMesh(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, meshValid, "mesh");
}
// A morph target of a node's mesh, by name or by number from 1, checked.
static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index) {
int32_t target;
if (lua_type(L, index) == LUA_TSTRING) {
target = meshFindMorph(nodeGetMesh(node), lua_tostring(L, index));
if (target < 0) {
_luaDie(L, method, "Node %d has no morph target named %s.", node, lua_tostring(L, index));
}
return target;
}
target = _argInteger(L, method, index) - 1;
if ((target < 0) || (target >= nodeGetMorphCount(node))) {
_luaDie(L, method, "Node %d has no morph target %d.", node, target + 1);
}
return target;
}
// A music handle argument, checked.
static MusicT *_argMusic(lua_State *L, const char *method, int32_t index) {
int32_t id = _argInteger(L, method, index);
MusicT *music = NULL;
HASH_FIND_INT(_global.musicList, &id, music);
if (!music) {
_luaDie(L, method, "No music at index %d.", id);
}
return music;
}
// A navigation mesh handle from navNew or navLoad.
static int32_t _argNav(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, navValid, "navigation mesh");
}
// An agent handle from navAgentNew.
static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, navAgentValid, "navigation agent");
}
// A scene node handle argument, checked.
static int32_t _argNode(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, nodeValid, "node");
}
// A node argument carrying the given kind of thing: "Node 7 has no body."
static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun) {
int32_t node = _argNode(L, method, index);
if (!exists(node)) {
_luaDie(L, method, "Node %d has no %s.", node, noun);
}
return node;
}
static double _argNumber(lua_State *L, const char *method, int32_t index) {
if (!lua_isnumber(L, index)) {
_luaDie(L, method, "Argument %d must be a number.", index);
}
return lua_tonumber(L, index);
}
// r, g, b at index onwards when the script gave them; the caller's defaults otherwise.
static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b) {
if (lua_gettop(L) < index + 2) {
return;
}
*r = _argColorByte(L, method, index);
*g = _argColorByte(L, method, index + 1);
*b = _argColorByte(L, method, index + 2);
}
// A node carrying a player, checked.
static int32_t _argPlayer(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, playerExists, "player");
}
// A node carrying a ragdoll, checked.
static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, ragdollExists, "ragdoll");
}
// A node carrying a soft body, checked.
static int32_t _argSoft(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, softExists, "soft body");
}
static SoundT *_argSound(lua_State *L, const char *method, int32_t index) {
int32_t id = _argInteger(L, method, index);
SoundT *sound = NULL;
HASH_FIND_INT(_global.soundList, &id, sound);
if (!sound) {
_luaDie(L, method, "No sound at index %d.", id);
}
return sound;
}
static SpriteT *_argSprite(lua_State *L, const char *method, int32_t index) {
int32_t id = _argInteger(L, method, index);
SpriteT *sprite = NULL;
HASH_FIND_INT(_global.spriteList, &id, sprite);
if (!sprite) {
_luaDie(L, method, "No sprite at index %d.", id);
}
return sprite;
}
static const char *_argString(lua_State *L, const char *method, int32_t index) {
if (!lua_isstring(L, index)) {
_luaDie(L, method, "Argument %d must be a string.", index);
}
return lua_tostring(L, index);
}
// Three numbers as a vector.
static Vec3T _argVec3(lua_State *L, const char *method, int32_t index) {
return vec3((float)_argNumber(L, method, index), (float)_argNumber(L, method, index + 1), (float)_argNumber(L, method, index + 2));
}
// A node carrying a vehicle, checked.
static int32_t _argVehicle(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, vehicleExists, "vehicle");
}
static VideoT *_argVideo(lua_State *L, const char *method, int32_t index) {
int32_t id = _argInteger(L, method, index);
VideoT *video = NULL;
HASH_FIND_INT(_global.videoList, &id, video);
if (!video) {
_luaDie(L, method, "No video at index %d.", id);
}
return video;
}
// A view handle from viewNew.
static int32_t _argView(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, viewValid, "view");
}
// The axis cache slot a controls.cfg code names, or -1 when the code is not an axis. An axis and
// its two direction codes all answer the same slot, so a DEAD_ZONES entry may be written either way.
static int32_t _axisIndexForCode(int32_t code) {
int32_t device = 0;
int32_t offset = 0;
if ((code >= CODE_GAMEPAD_BASE) && (code < CODE_GAMEPAD_BASE + MAX_CONTROLLERS * CODE_GAMEPAD_STRIDE)) {
device = (code - CODE_GAMEPAD_BASE) / CODE_GAMEPAD_STRIDE;
offset = (code - CODE_GAMEPAD_BASE) % CODE_GAMEPAD_STRIDE;
if (offset < CODE_GAMEPAD_BUTTON_OFFSET) {
return AXIS_INDEX_CONTROLLER(device, offset / CODE_AXIS_STRIDE);
}
}
return -1;
}
// The bezel artwork: always the whole logical rectangle, so a picture of a cabinet lands where a
// cabinet would be whatever shape the window is. It draws behind everything else, or in front of
// the picture with --bezelflip, in which case its own transparency is the hole the game shows
// through.
static void _bezelDraw(void) {
SDL_FRect rect;
_logicalRect(&rect);
SDL_RenderTexture(_global.renderer, _global.bezelTexture, NULL, &rect);
}
// Loads --bezel's artwork and the cutout beside it. The name is looked for in the bezels folder of
// the game directory (a packed game may carry its own) and then in the same folder of the data
// directory; --bezeldir replaces both with the one folder it names. Artwork the user asked for and
// Singe cannot find is fatal, since a missing frame would silently misplace the picture.
static void _bezelLoad(void) {
const char *directory = BEZEL_DIRECTORY;
const char separator = utilGetPathSeparator();
char *candidate[2];
char *sidecar = NULL;
char *stem = NULL;
char *extension = NULL;
char *data = NULL;
size_t bytes = 0;
size_t i = 0;
int32_t c = 0;
int32_t found = -1;
uint64_t hash = BEZEL_HASH_BASIS;
SDL_Surface *artwork = NULL;
if (_global.conf->bezelFile == NULL) {
return;
}
if (_global.conf->bezelDir != NULL) {
directory = _global.conf->bezelDir;
}
candidate[0] = utilCreateString("%s%c%s", directory, separator, _global.conf->bezelFile);
candidate[1] = (_global.conf->bezelDir != NULL) ? NULL : utilCreateString("%s%s%c%s", _global.conf->dataDir, directory, separator, _global.conf->bezelFile);
for (c = 0; (found < 0) && (c < (int32_t)SDL_arraysize(candidate)); c++) {
if ((candidate[c] != NULL) && vfsExists(candidate[c])) {
found = c;
}
}
if (found < 0) {
utilDie("Unable to find the bezel %s in %s.", _global.conf->bezelFile, directory);
}
data = vfsRead(candidate[found], &bytes);
if (data == NULL) {
utilDie("Unable to read the bezel %s.", candidate[found]);
}
artwork = decodeImageIO(SDL_IOFromConstMem(data, bytes), true);
if (artwork == NULL) {
utilDie("%s: %s", candidate[found], SDL_GetError());
}
_global.bezelTexture = SDL_CreateTextureFromSurface(_global.renderer, artwork);
if (_global.bezelTexture == NULL) {
utilDie("%s: %s", candidate[found], SDL_GetError());
}
// Hypseus identifies a bezel to the game by the hash of its bytes, and games compare that
// against numbers they were written with, so mainBezelLoaded has to answer the same number.
for (i = 0; i < bytes; i++) {
hash ^= (uint8_t)data[i];
hash *= BEZEL_HASH_PRIME;
}
free(data);
_global.bezelHash = utilCreateString("%" PRIu64, hash);
_global.bezelInFront = _global.conf->bezelFlip;
_progTrace("Bezel %s is %dx%d, id %s", candidate[found], artwork->w, artwork->h, _global.bezelHash);
// The cutout is declared in the artwork's own pixels, so it survives any window size; the
// artwork fills the logical rectangle, so the same ratio carries the hole into it.
stem = strdup(candidate[found]);
extension = strrchr(stem, '.');
if ((extension != NULL) && (strchr(extension, separator) == NULL)) {
*extension = 0;
}
sidecar = utilCreateString("%s%s", stem, BEZEL_SIDECAR_EXTENSION);
free(stem);
if (_bezelReadCutout(sidecar, artwork->w, artwork->h, &_global.bezelCutout)) {
_global.bezelCutout.x *= (float)_global.canvasWidth / (float)artwork->w;
_global.bezelCutout.w *= (float)_global.canvasWidth / (float)artwork->w;
_global.bezelCutout.y *= (float)_global.canvasHeight / (float)artwork->h;
_global.bezelCutout.h *= (float)_global.canvasHeight / (float)artwork->h;
_global.bezelHasCutout = true;
_progTrace("Bezel cutout is %.1f,%.1f %.1fx%.1f", _global.bezelCutout.x, _global.bezelCutout.y, _global.bezelCutout.w, _global.bezelCutout.h);
}
free(sidecar);
SDL_DestroySurface(artwork);
for (c = 0; c < (int32_t)SDL_arraysize(candidate); c++) {
free(candidate[c]);
}
}
// Reads the cutout a bezel declares beside its artwork: a Lua file, as controls.cfg is, setting
// CUTOUT = { x = , y = , width = , height = } in the artwork's own pixels, run in a throwaway state
// with no libraries because it is data and not a program. False when there is no sidecar or it
// declares no cutout; one that will not run, or that names a rectangle outside the artwork, is
// fatal.
static bool _bezelReadCutout(const char *path, int32_t width, int32_t height, SDL_FRect *cutout) {
static const char *fields[BEZEL_CUTOUT_FIELDS] = { "x", "y", "width", "height" };
lua_State *L = NULL;
char *source = NULL;
size_t bytes = 0;
int32_t status = 0;
int32_t i = 0;
bool found = false;
double value[BEZEL_CUTOUT_FIELDS];
if (!vfsExists(path)) {
return false;
}
source = vfsRead(path, &bytes);
if (source == NULL) {
utilDie("Unable to read the bezel cutout %s.", path);
}
L = luaL_newstate();
if (L == NULL) {
utilDie("Out of memory reading the bezel cutout %s.", path);
}
status = luaL_loadbuffer(L, source, bytes, path);
free(source);
if (status || lua_pcall(L, 0, 0, 0)) {
utilDie("%s", lua_tostring(L, -1));
}
lua_getglobal(L, BEZEL_CUTOUT_TABLE);
if (lua_istable(L, -1)) {
found = true;
for (i = 0; i < BEZEL_CUTOUT_FIELDS; i++) {
lua_getfield(L, -1, fields[i]);
if (!lua_isnumber(L, -1)) {
utilDie("The bezel cutout %s needs a numeric %s.", path, fields[i]);
}
value[i] = lua_tonumber(L, -1);
lua_pop(L, 1);
}
}
lua_close(L);
if (!found) {
return false;
}
cutout->x = (float)value[0];
cutout->y = (float)value[1];
cutout->w = (float)value[2];
cutout->h = (float)value[3];
if ((cutout->w < 1.0f) || (cutout->h < 1.0f) || (cutout->x < 0.0f) || (cutout->y < 0.0f) || ((cutout->x + cutout->w) > (float)width) || ((cutout->y + cutout->h) > (float)height)) {
utilDie("The bezel cutout %s is not inside the %dx%d artwork.", path, width, height);
}
return true;
}
// Builds a config for scriptExecute/scriptPush from the games.dat style table at stack index 1.
static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base, const char *method) {
const char *confKey = NULL;
const char *valueString = NULL;
bool valueBoolean = false;
int64_t valueNumber = 0;
ConfigT *c = NULL;
// Start with the given config, but every entry brings its own video, container and data directory.
c = cloneConf(base);
free(c->container);
c->container = NULL;
c->disc = false;
c->isFrameFile = false;
free(c->videoFile);
c->videoFile = NULL;
free(c->dataDir);
c->dataDir = NULL;
c->requires = 0;
c->tier = 0;
// Update with data in the table on the top of the Lua stack.
lua_pushnil(L);
while (lua_next(L, 1)) {
// Keys must be strings; converting other keys in place would confuse lua_next.
if (lua_type(L, 2) != LUA_TSTRING) {
lua_pop(L, 1);
continue;
}
confKey = lua_tostring(L, 2);
valueString = NULL;
valueBoolean = false;
valueNumber = 0;
// Get value
switch (lua_type(L, 3)) {
case LUA_TSTRING:
valueString = lua_tostring(L, 3);
break;
case LUA_TBOOLEAN:
valueBoolean = lua_toboolean(L, 3);
break;
case LUA_TNUMBER:
valueNumber = (int64_t)lua_tonumber(L, 3);
break;
default:
break;
}
// Update config with new data
if (strcmp(confKey, "SCRIPT") == 0) {
if (valueString == NULL) {
_confFail(L, method, "SCRIPT must be a string.");
}
free(c->scriptFile);
c->scriptFile = strdup(valueString);
utilFixPathSeparators(&c->scriptFile, false);
} else if (strcmp(confKey, "GAME_ID") == 0) {
// The master service's id for this game. Nothing in the engine uses it; it is carried
// so a script can ask singeGetGameId() which board its scores belong on.
if (valueString == NULL) {
_confFail(L, method, "GAME_ID must be a string.");
}
free(c->gameId);
c->gameId = strdup(valueString);
} else if (strcmp(confKey, "CONTAINER") == 0) {
if (valueString == NULL) {
_confFail(L, method, "CONTAINER must be a string.");
}
free(c->container);
c->container = strdup(valueString);
utilFixPathSeparators(&c->container, false);
} else if (strcmp(confKey, "VIDEO") == 0) {
if (valueString == NULL) {
_confFail(L, method, "VIDEO must be a string.");
}
free(c->videoFile);
c->videoFile = strdup(valueString);
utilFixPathSeparators(&c->videoFile, false);
c->isFrameFile = isFrameFileName(c->videoFile);
} else if (strcmp(confKey, "STRETCH") == 0) {
c->stretchVideo = valueBoolean;
} else if (strcmp(confKey, "NO_MOUSE") == 0) {
c->noMouse = valueBoolean;
} else if (strcmp(confKey, "RESOLUTION_X") == 0) {
c->xResolution = (int32_t)valueNumber;
c->resolutionWasCalculated = false;
} else if (strcmp(confKey, "RESOLUTION_Y") == 0) {
c->yResolution = (int32_t)valueNumber;
c->resolutionWasCalculated = false;
} else if (strcmp(confKey, "SINDEN_GUN") == 0) {
if ((valueString == NULL) || !parseSindenString(valueString, c)) {
c->sindenArgc = 0;
}
} else if (strcmp(confKey, "AUDIO_TRACK") == 0) {
c->audioOutputTrack = (int32_t)valueNumber;
} else if (strcmp(confKey, "AUDIO_DELAY") == 0) {
c->audioDelayMs = (int32_t)valueNumber;
} else if (strcmp(confKey, "AUDIO_SUFFIX") == 0) {
if (valueString == NULL) {
_confFail(L, method, "AUDIO_SUFFIX must be a string.");
}
free(c->audioSuffix);
c->audioSuffix = strdup(valueString);
} else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) {
c->legacySpriteArgs = valueBoolean;
} else if (strcmp(confKey, "REQUIRES") == 0) {
if (!lua_istable(L, -1)) {
_confFail(L, method, "REQUIRES must be a list of capability names.");
}
c->requires = _capabilitiesFromList(L, -1, method);
} else if (strcmp(confKey, "TIER") == 0) {
if ((valueNumber < 0) || (valueNumber > TIER_MAX)) {
_confFail(L, method, "TIER must be 0 to %d.", TIER_MAX);
}
c->tier = (int32_t)valueNumber;
} else if (strcmp(confKey, "CANVAS_X") == 0) {
c->canvasWidth = (int32_t)valueNumber;
} else if (strcmp(confKey, "CANVAS_Y") == 0) {
c->canvasHeight = (int32_t)valueNumber;
}
// Clean up for next pair
lua_pop(L, 1);
}
// The VIDEO line is the disc: present and not blank means a laserdisc game.
if ((c->videoFile != NULL) && (c->videoFile[0] == 0)) {
free(c->videoFile);
c->videoFile = NULL;
}
c->disc = (c->videoFile != NULL);
if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) {
_confFail(L, method, "%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile);
}
return c;
}
// Calls a global Lua function. sig lists argument types (d, i, s), then '>' and result types.
static void _callLua(const char *func, const char *sig, ...) {
va_list vl;
bool done = false;
int32_t narg = 0;
int32_t nres = 0;
int32_t handler = 0;
int32_t top = 0;
double d = 0;
// No state (shutting down) means nothing to call.
if (_global.luaContext == NULL) {
return;
}
top = lua_gettop(_global.luaContext);
// Get Function
lua_getglobal(_global.luaContext, func);
if (!lua_isfunction(_global.luaContext, -1)) {
// Function does not exist. Bail.
lua_settop(_global.luaContext, top);
return;
}
if (_global.conf->scriptTracing) {
utilTrace("%s", func);
}
// Traceback handler sits below the function.
lua_pushcfunction(_global.luaContext, _luaTraceback);
lua_insert(_global.luaContext, -2);
handler = lua_gettop(_global.luaContext) - 1;
// Push Arguments. Room for all of them (the results reuse it) is checked once, up front.
luaL_checkstack(_global.luaContext, (int)strlen(sig) + 1, "Too many arguments");
va_start(vl, sig);
while ((*sig) && (!done)) {
switch (*sig++) {
case 'd': // Double
lua_pushnumber(_global.luaContext, va_arg(vl, double));
break;
case 'i': // Int
lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Promoted type for varargs.
break;
case 'b': // Boolean (passed as an int)
lua_pushboolean(_global.luaContext, va_arg(vl, int32_t));
break;
case 's': // String
lua_pushstring(_global.luaContext, va_arg(vl, char *));
break;
case 'S': { // Counted string, for bytes that may hold a zero
const char *bytes = va_arg(vl, const char *);
lua_pushlstring(_global.luaContext, bytes, va_arg(vl, size_t));
break;
}
case '>':
done = true;
break;
default:
utilDie("Invalid argument option (%c)", *(sig - 1));
}
if (!done) {
narg++;
}
}
// Do the call. Script errors are fatal, like every other error in Singe.
nres = (int32_t)strlen(sig);
if (scriptWatchCall(_global.luaContext, narg, nres, handler) != 0) {
utilDie("Error executing function '%s': %s", func, lua_tostring(_global.luaContext, -1));
}
// Retrieve results
nres = -nres; // Stack index of first result
while (*sig) {
switch (*sig++) {
case 'd': // Double
*va_arg(vl, double *) = lua_tonumber(_global.luaContext, nres);
break;
case 'i': // Int - nil or non-numbers read as zero.
d = lua_tonumber(_global.luaContext, nres);
*va_arg(vl, int32_t *) = (int32_t)d;
break;
default:
utilDie("Invalid option (%c)", *(sig - 1));
}
nres++;
}
va_end(vl);
lua_settop(_global.luaContext, top);
}
// A REQUIRES list from games.dat: capability names as strings, turned into CapabilityE bits. A
// name that is not a capability is an error, as any other bad games.dat value is.
static uint32_t _capabilitiesFromList(lua_State *L, int32_t index, const char *method) {
const char *name = NULL;
uint32_t bits = 0;
int32_t at = lua_absindex(L, index);
size_t x = 0;
bool found = false;
lua_pushnil(L);
while (lua_next(L, at)) {
name = lua_tostring(L, -1);
found = false;
for (x = 0; (name != NULL) && (x < SDL_arraysize(CAPABILITY_NAMES)); x++) {
if (strcmp(name, CAPABILITY_NAMES[x].name) == 0) {
bits |= CAPABILITY_NAMES[x].bit;
found = true;
}
}
if (!found) {
_confFail(L, method, "REQUIRES names %s, which is not a capability (SCENE, GUI, SHADOWS, POST, SKIN, MORPH).", (name != NULL) ? name : "something that is not a name");
}
lua_pop(L, 1);
}
return bits;
}
// A game has used a capability. The first use is recorded in the game's data directory, where
// --pack reads it to write REQUIRES, and a game that did not declare it is told so in the trace:
// a hand-kept list is wrong within two releases, so the engine is what knows (PLAN.md section 57).
static void _capabilityUsed(uint32_t bit) {
char *path = NULL;
FILE *out = NULL;
size_t x = 0;
if ((_global.capabilitiesUsed & bit) != 0) {
return;
}
_global.capabilitiesUsed |= bit;
// The engine's own scripts (the menu, the service tools) are not games: nothing packs them.
if ((_global.conf->scriptFile != NULL) && (strncmp(_global.conf->scriptFile, VFS_ENGINE_DIRECTORY, strlen(VFS_ENGINE_DIRECTORY)) == 0) && ((_global.conf->scriptFile[strlen(VFS_ENGINE_DIRECTORY)] == '/') || (_global.conf->scriptFile[strlen(VFS_ENGINE_DIRECTORY)] == '\\'))) {
return;
}
for (x = 0; x < SDL_arraysize(CAPABILITY_NAMES); x++) {
if ((CAPABILITY_NAMES[x].bit == bit) && ((_global.conf->requires & bit) == 0)) {
utilSay("Warning: this game uses %s, which its games.dat does not list in REQUIRES.", CAPABILITY_NAMES[x].name);
}
}
if (_global.conf->dataDir == NULL) {
return;
}
path = utilCreateString("%s%s", _global.conf->dataDir, REQUIRES_RECORD_FILE);
out = fopen(path, "w");
if (out != NULL) {
for (x = 0; x < SDL_arraysize(CAPABILITY_NAMES); x++) {
if ((_global.capabilitiesUsed & CAPABILITY_NAMES[x].bit) != 0) {
fprintf(out, "%s\n", CAPABILITY_NAMES[x].name);
}
}
fclose(out);
}
free(path);
}
// Whether a press or a release changes a chord: a press that completes it, or the release of one of
// its buttons while it is down. The chord is updated to match.
static bool _chordChanged(ChordT *chord, bool down, int32_t scancode) {
bool member = false;
bool complete = true;
int32_t c = 0;
for (c = 0; c < chord->count; c++) {
member = member || (chord->code[c] == scancode);
complete = complete && _physicallyHeld(chord->code[c]);
}
if (!member || (chord->active == down) || (down && !complete)) {
return false;
}
chord->active = down;
return true;
}
// Chords after a press or a release. A press fires any chord it completes; a release lets go of any
// fired chord it belongs to. Firing does what a single button mapped to the same switch does: the
// engine's own action for pause, quit, screenshot and grab, and the switch handed to the script
// unless the engine keeps it, as it keeps pause. The machine's quit chord goes last, and quits even a
// game that turned its quit key off: a handheld with no keyboard has no other way out. It ends this
// script as any quit does, so a game started from the menu goes back to the menu. True when the press
// completed it: that press is the machine's, and the script is not told of it -- in the menu, the
// Start that completes Select and Start would otherwise also start the game under the cursor.
static bool _chordsUpdate(bool down, int32_t scancode) {
int32_t move = 0;
int32_t x = 0;
for (move = 0; move < INPUT_COUNT; move++) {
for (x = 0; x < _global.controlMappings[move].chordCount; x++) {
if (!_chordChanged(&_global.controlMappings[move].chord[x], down, scancode)) {
continue;
}
if (down) {
_engineAction((InputE)move);
}
if ((move != INPUT_PAUSE) || !_global.pauseEnabled) {
_deliverSwitch((InputE)move, down);
}
}
}
if ((_global.quitChord.count > 0) && _chordChanged(&_global.quitChord, down, scancode) && down) {
_progTrace("Quit chord pressed");
_global.running = false;
_global.quitChordRelease = scancode;
_global.quitChordHeldCount = 0;
return true;
}
return false;
}
// Liang-Barsky clip of a line to the overlay. False when none of it is on the overlay; otherwise
// the ends are moved onto it.
static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2) {
const double dx = *x2 - *x1;
const double dy = *y2 - *y1;
double t0 = 0.0;
double t1 = 1.0;
double r = 0.0;
double p[4];
double q[4];
int32_t i = 0;
// Each edge as the parametric distance to it: left, right, top, bottom.
p[0] = -dx;
q[0] = *x1;
p[1] = dx;
q[1] = (_global.overlay->w - 1) - *x1;
p[2] = -dy;
q[2] = *y1;
p[3] = dy;
q[3] = (_global.overlay->h - 1) - *y1;
for (i = 0; i < 4; i++) {
if (p[i] == 0.0) {
// Parallel to this edge: outside it means gone altogether.
if (q[i] < 0.0) {
return false;
}
continue;
}
r = q[i] / p[i];
if (p[i] < 0.0) {
if (r > t1) {
return false;
}
t0 = SDL_max(t0, r);
} else {
if (r < t0) {
return false;
}
t1 = SDL_min(t1, r);
}
}
*x2 = (int32_t)lround(*x1 + t1 * dx);
*y2 = (int32_t)lround(*y1 + t1 * dy);
*x1 = (int32_t)lround(*x1 + t0 * dx);
*y1 = (int32_t)lround(*y1 + t0 * dy);
return true;
}
// Builds the rectangle the game is drawn into and the Sinden rings around it, in video
// coordinates. The inputs apply in order: the whole logical rectangle, or the cutout when a bezel
// declares one; then --scalefactor, which sets the size, and --shiftx and --shifty, which place it
// in the room the scale left, exactly as Hypseus does, so at full scale there is no room and a
// shift does nothing; and last the Sinden border, so that a border is a border whatever the shift
// and the scale did. Rotation is not an input: the frame is drawn unrotated and turned as a whole
// when it is shown. Mouse positions map back into the rectangle through drawScaleX and drawScaleY.
static void _computeVideoRect(void) {
SDL_FRect rect;
SDL_FRect border;
float scaledW = 0.0f;
float scaledH = 0.0f;
float roomX = 0.0f;
float roomY = 0.0f;
int32_t black = _global.sindenBlackWidth;
int32_t white = _global.sindenWhiteWidth;
int32_t total = 0;
int32_t room = 0;
bool atWindow = false;
// The whole logical rectangle to start with, or the hole in the bezel artwork when one is
// declared: either way, everything after this places the picture inside it.
if (_global.bezelHasCutout) {
rect = _global.bezelCutout;
} else {
_logicalRect(&rect);
}
// Overscan compensation shrinks it, and the shift then moves it inside the room that leaves:
// at the extreme the picture is flush against one side of that room and can never be pushed
// off, and with no shift it is centred exactly where the scale factor alone would put it.
scaledW = rect.w * (float)_global.videoScale / (float)SCALE_FACTOR_MAX;
scaledH = rect.h * (float)_global.videoScale / (float)SCALE_FACTOR_MAX;
roomX = (rect.w - scaledW) / 2.0f;
roomY = (rect.h - scaledH) / 2.0f;
rect.x += roomX + roomX * (float)_global.videoShiftX / (float)SHIFT_MAX;
rect.y += roomY + roomY * (float)_global.videoShiftY / (float)SHIFT_MAX;
rect.w = scaledW;
rect.h = scaledH;
// The Sinden border is the outermost thing: the black ring takes the whole rectangle, the
// white ring sits inside it, and the game goes inside both. A scale small enough to leave no
// room squeezes the two rings rather than swallowing the picture. With bezel artwork around
// the picture the rings belong at the window's edge instead, because the gun's camera sees the
// whole screen and not just the picture; there they are painted over the artwork and leave the
// picture the size it already is. --sindenedge decides it either way.
if (_global.sindenBorder) {
atWindow = (_global.conf->sindenEdge == SINDEN_EDGE_WINDOW) || ((_global.conf->sindenEdge == SINDEN_EDGE_AUTO) && (_global.bezelTexture != NULL));
if (atWindow) {
_logicalRect(&border);
} else {
border = rect;
}
total = black + white;
room = (int32_t)(SDL_min(border.w, border.h) - SINDEN_MIN_VIDEO_PIXELS) / 2;
if (room < 0) {
room = 0;
}
if (total > room) {
black = (total > 0) ? (black * room / total) : 0;
white = room - black;
}
_global.sindenBlackRect = border;
_global.sindenWhiteRect.x = border.x + (float)black;
_global.sindenWhiteRect.y = border.y + (float)black;
_global.sindenWhiteRect.w = border.w - (float)(black * 2);
_global.sindenWhiteRect.h = border.h - (float)(black * 2);
_global.sindenInnerRect.x = border.x + (float)(black + white);
_global.sindenInnerRect.y = border.y + (float)(black + white);
_global.sindenInnerRect.w = border.w - (float)((black + white) * 2);
_global.sindenInnerRect.h = border.h - (float)((black + white) * 2);
_global.sindenAtWindow = atWindow;
if (!atWindow) {
rect = _global.sindenInnerRect;
}
}
_global.videoRect = rect;
_global.drawScaleX = (double)_global.canvasWidth / (double)rect.w;
_global.drawScaleY = (double)_global.canvasHeight / (double)rect.h;
_global.refreshDisplay = true;
_progTrace("Video rectangle is %.1f,%.1f %.1fx%.1f (shift %d,%d scale %d rotate %d)", rect.x, rect.y, rect.w, rect.h, _global.videoShiftX, _global.videoShiftY, _global.videoScale, _global.videoRotate);
}
// A bad games.dat value. From a script it is reported as an API argument error, with the line and
// the call named; at startup, before there is a script, it ends the run with the message alone.
static void _confFail(lua_State *L, const char *method, const char *fmt, ...) {
char message[256];
va_list args;
va_start(args, fmt);
vsnprintf(message, sizeof(message), fmt, args);
va_end(args);
if (method != NULL) {
_luaDie(L, method, "%s", message);
}
utilDie("%s", message);
}
// Maps an SDL joystick instance ID to our controller slot, or -1.
// The four places a configuration file is read from, in the order they are applied, so a later one
// wins: beside the game's script, the game author's defaults; the working directory, the
// machine's; above the data directory; and inside it, the player's own for that one game. A
// place that repeats an earlier one comes back NULL, because without -d the data directory is the
// script's own and the same file would otherwise be read twice; it then counts as the game's. The
// caller frees the strings.
static void _configPlaces(const char *file, const char *dataDir, const char *scriptFile, char **places) {
char *scriptDir = utilGetUpToLastPathComponent(scriptFile);
int32_t c = 0;
int32_t x = 0;
places[0] = utilCreateString("%s%s", scriptDir, file);
places[1] = strdup(file);
places[2] = utilCreateString("%s..%c%s", dataDir, utilGetPathSeparator(), file);
places[3] = utilCreateString("%s%s", dataDir, file);
free(scriptDir);
for (c = 0; c < SETTINGS_PLACES; c++) {
for (x = 0; x < c; x++) {
if ((places[x] != NULL) && (strcmp(places[x], places[c]) == 0)) {
free(places[c]);
places[c] = NULL;
break;
}
}
}
}
static int32_t _controllerSlot(SDL_JoystickID which) {
int32_t x = 0;
for (x = 0; x < MAX_CONTROLLERS; x++) {
if ((_global.controllers[x] != NULL) && (SDL_GetGamepadID(_global.controllers[x]) == which)) {
return x;
}
}
return -1;
}
// A fresh Lua state with the standard libraries, the vfs hooks, the constants and the whole API.
static void _createScriptContext(void) {
lua_State *L = NULL;
_progTrace("Creating Lua context for script");
L = luaL_newstate();
_global.luaContext = L;
_startLuaContext(L);
// -p times the engine calls: what was there before the API is noted, so only the API is wrapped.
if (_global.conf->programTracing) {
lua_newtable(L);
lua_pushglobaltable(L);
lua_pushnil(L);
while (lua_next(L, -2)) {
lua_pop(L, 1);
lua_pushvalue(L, -1);
lua_pushboolean(L, 1);
lua_rawset(L, -5);
}
lua_pop(L, 1);
}
_registerApi(L);
if (_global.conf->programTracing) {
_perfWrapApi(L);
lua_pop(L, 1);
_progTrace("Timing %d engine calls for the perf lines", _perfCalls.count);
}
}
// The volume a script starts a family of sounds at, from the non-disc volume setting, on that
// family's own scale. Sound effects and music therefore begin equally loud, which is what a user
// who set --volume_nonvldp meant; Hypseus instead starts its music mixer a third of the way up.
static int32_t _defaultVolume(int32_t maximum) {
return maximum * _global.conf->volumeNonVldp / VOLUME_MAX;
}
// Sleeps while keeping the window responsive. Returns false if the user asked to quit.
// The real clock, deliberately: this waits out a real sleep, and the virtual clock only moves in
// the frame loop, so a virtual deadline here would never arrive.
static bool _delayAndPump(uint32_t ms) {
SDL_Event event;
uint64_t until = SDL_GetTicks() + ms;
do {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) {
_global.running = false;
}
}
if (mainTerminated()) {
_global.running = false;
}
SDL_Delay(IDLE_SLEEP_MS);
} while (_global.running && (SDL_GetTicks() < until));
return _global.running;
}
// Hands a key, button, or axis direction to the script, tracking what it now believes is held.
static void _deliverKey(bool down, int32_t keysym, int32_t scancode) {
int32_t move = 0;
int32_t index = 0;
// Full mode hands the keyboard over whole: every key as its keysym, and as a key. A pad or
// mouse button cannot be typed, so it still arrives as the switch it is mapped to.
if ((_global.keyboardMode == KEYBOARD_FULL) && (scancode < CODE_GAMEPAD_BASE)) {
_heldListUpdate(_global.heldKeys, &_global.heldKeyCount, down, keysym, scancode);
_callLua(down ? "onInputPressed" : "onInputReleased", "i", keysym);
_callLua(down ? "onKeyPressed" : "onKeyReleased", "ii", keysym, scancode);
return;
}
// Mappable switches. The pause switch belongs to the engine while its key is enabled.
for (move = 0; move < INPUT_COUNT; move++) {
if ((move == INPUT_PAUSE) && _global.pauseEnabled) {
continue;
}
for (index = 0; index < _global.controlMappings[move].inputCount; index++) {
if (_global.controlMappings[move].input[index] == scancode) {
_global.switchHeld[move] = down;
// A pad or mouse switch steers a GUI that takes input; a key already reached it as a key.
if ((scancode < CODE_GAMEPAD_BASE) || !_guiSwitch((InputE)move, down)) {
// A switch from a mouse or gun names its device too, as Hypseus does, so a
// two-gun game can tell its players apart.
if (scancode >= CODE_MOUSE_BASE) {
_callLua(down ? "onInputPressed" : "onInputReleased", "ii", move, (scancode - CODE_MOUSE_BASE) / CODE_MOUSE_STRIDE);
} else {
_callLua(down ? "onInputPressed" : "onInputReleased", "i", move);
}
}
break;
}
}
}
}
// A mapped switch nothing pressed. SWITCH_MOUSE_DISCONNECT has no key, button or axis to arrive
// on, so it is handed to the script directly rather than through a code.
static void _deliverSwitch(InputE move, bool down) {
if (_global.frozen) {
return;
}
_global.switchHeld[move] = down;
_callLua(down ? "onInputPressed" : "onInputReleased", "i", (int32_t)move);
}
// Puts the disc picture controls on whichever player is showing the disc. A frame file crosses into
// another player as it plays, so this runs whenever a control changes and again once a frame, which
// is what keeps monochrome, the blend and the luma level on a multi segment disc.
static void _discApplyPicture(void) {
if (_global.videoHandle < 0) {
return;
}
videoSetMonochrome(_global.videoHandle, _global.discMonochrome);
videoSetBlend(_global.videoHandle, _global.discBlend);
videoSetLuma(_global.videoHandle, _global.discLumaOn, _global.discLumaLevel);
}
// The disc's current frame, whichever kind it is: a frame file counts across all its segments.
// Only with a disc (videoHandle >= 0).
static int64_t _discGetFrame(void) {
if (_global.conf->isFrameFile) {
return frameFileGetFrame(_global.frameFileHandle, _global.videoHandle);
}
return videoGetFrame(_global.videoHandle);
}
// The disc always starts parked on frame 1, paused, like discSearch(1). Only with a disc.
static void _discPark(void) {
_progTrace("Parking laserdisc on frame 1");
_discSeek(1);
videoPause(_global.videoHandle);
_global.discStopped = false;
_selectDefaultAudioTrack(_global.videoHandle);
}
// Seeks the laserdisc, whichever kind it is, held within its ends (a frame file clamps inside its
// segments itself) rather than wrapping around.
static void _discSeek(int64_t frame) {
int64_t actualFrame = 0;
int64_t count = 0;
if (_global.conf->isFrameFile) {
frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &actualFrame);
} else if (_global.videoHandle >= 0) {
count = videoGetFrameCount(_global.videoHandle);
if (count > 0) {
frame = SDL_clamp(frame, 0, count - 1);
}
videoSeek(_global.videoHandle, frame);
}
}
// Points the disc's audio at "<base><suffix>.ogg" beside its video (every segment of a frame
// file), reopened at the current frame, and remembers the suffix so a script reset can undo it.
// An empty suffix restores the default audio. False, with the audio unchanged, when the file is
// missing. Only with a disc.
static bool _discSetAudioSuffix(const char *suffix) {
bool opened = false;
char *audio = NULL;
char *copy = NULL;
if (_global.conf->isFrameFile) {
opened = frameFileSetAudioSuffix(_global.frameFileHandle, suffix);
} else {
audio = frameFileAudioName(_global.conf->videoFile, suffix);
opened = ((suffix[0] == 0) || (audio != NULL)) && videoReopenAudio(_global.videoHandle, audio);
free(audio);
}
if (opened) {
copy = strdup(suffix);
if (!copy) {
utilDie("Unable to allocate the audio suffix.");
}
free(_global.discAudioSuffix);
_global.discAudioSuffix = copy;
}
return opened;
}
// Splash screens: fade in the Kangaroo Punch logo, cross fade to the Singe logo, fade out.
static void _doLogos(void) {
int32_t i = 0;
int32_t w = 0;
int32_t h = 0;
bool keepGoing = true;
SDL_Surface *surfKangaroo = NULL;
SDL_Surface *surfSinge = NULL;
SDL_Texture *texKangaroo = NULL;
SDL_Texture *texSinge = NULL;
SDL_FRect rectKangaroo;
SDL_FRect rectSinge;
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED;
SDL_GetRenderLogicalPresentation(_global.renderer, &w, &h, &mode);
texKangaroo = _loadEmbeddedTexture(kangarooPunchLogo_png, kangarooPunchLogo_png_len, &surfKangaroo);
texSinge = _loadEmbeddedTexture(singeLogo_png, singeLogo_png_len, &surfSinge);
// Both logos are fitted into the same space, so the cross fade neither stretches nor jumps.
SDL_SetRenderLogicalPresentation(_global.renderer, LOGO_SPACE_WIDTH, LOGO_SPACE_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
_fitRect(surfKangaroo->w, surfKangaroo->h, LOGO_SPACE_WIDTH, LOGO_SPACE_HEIGHT, LOGO_MARGIN, &rectKangaroo);
_fitRect(surfSinge->w, surfSinge->h, LOGO_SPACE_WIDTH, LOGO_SPACE_HEIGHT, LOGO_MARGIN, &rectSinge);
// Fade in to white with Kangaroo logo
for (i = 0; keepGoing && (i < LOGO_FADE_STEPS); i++) {
SDL_SetRenderDrawColor(_global.renderer, (uint8_t)i, (uint8_t)i, (uint8_t)i, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
SDL_SetTextureAlphaMod(texKangaroo, (uint8_t)i);
SDL_RenderTexture(_global.renderer, texKangaroo, NULL, &rectKangaroo);
SDL_RenderPresent(_global.renderer);
keepGoing = _delayAndPump(LOGO_FADE_STEP_MS);
}
keepGoing = keepGoing && _delayAndPump(LOGO_HOLD_MS);
// Cross fade to Singe logo
for (i = 0; keepGoing && (i < LOGO_FADE_STEPS); i++) {
SDL_RenderClear(_global.renderer);
SDL_SetTextureAlphaMod(texKangaroo, (uint8_t)(LOGO_FADE_STEPS - 1 - i));
SDL_RenderTexture(_global.renderer, texKangaroo, NULL, &rectKangaroo);
SDL_SetTextureAlphaMod(texSinge, (uint8_t)i);
SDL_RenderTexture(_global.renderer, texSinge, NULL, &rectSinge);
SDL_RenderPresent(_global.renderer);
keepGoing = _delayAndPump(LOGO_FADE_STEP_MS);
}
keepGoing = keepGoing && _delayAndPump(LOGO_HOLD_MS);
// Fade to black
for (i = LOGO_FADE_STEPS - 1; keepGoing && (i >= 0); i--) {
SDL_SetRenderDrawColor(_global.renderer, (uint8_t)i, (uint8_t)i, (uint8_t)i, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
SDL_SetTextureAlphaMod(texSinge, (uint8_t)i);
SDL_RenderTexture(_global.renderer, texSinge, NULL, &rectSinge);
SDL_RenderPresent(_global.renderer);
keepGoing = _delayAndPump(LOGO_FADE_STEP_MS);
}
SDL_DestroyTexture(texSinge);
SDL_DestroyTexture(texKangaroo);
SDL_DestroySurface(surfSinge);
SDL_DestroySurface(surfKangaroo);
SDL_SetRenderLogicalPresentation(_global.renderer, w, h, mode);
}
// Bresenham line into the overlay, clipped to it first so a line from far off screen costs only
// its visible part. The overlay must be locked by the caller.
// This frame's guiDraw calls, over the overlay: each GUI's texture scaled into the target the way
// the overlay is (overlay coordinates map through the same ratio), or covering it. The frame makes
// two passes, one for the GUIs over the picture and one for the screen space ones over the whole
// window, and _guisShown() closes the queue after the second.
static void _drawGuis(const SDL_FRect *target, bool screen) {
float scaleX = target->w / (float)_global.overlay->w;
float scaleY = target->h / (float)_global.overlay->h;
SDL_FRect dest;
int32_t x;
for (x = 0; x < _global.guiDrawCount; x++) {
SDL_Texture *texture = guiTexture2D(_global.guiDraws[x].gui);
if ((texture == NULL) || (_global.guiDraws[x].screen != screen)) {
continue;
}
if (_global.guiDraws[x].whole) {
dest = *target;
} else {
dest.x = target->x + _global.guiDraws[x].dest.x * scaleX;
dest.y = target->y + _global.guiDraws[x].dest.y * scaleY;
dest.w = _global.guiDraws[x].dest.w * scaleX;
dest.h = _global.guiDraws[x].dest.h * scaleY;
}
SDL_RenderTexture(_global.renderer, texture, NULL, &dest);
// What the script drew after this GUI goes over it.
if (_global.guiDraws[x].layer != OVERLAY_NO_LAYER) {
_overlayLayerDraw(_global.guiDraws[x].layer, target);
}
}
}
static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel) {
int32_t x = 0;
int32_t y = 0;
int32_t dx = 0;
int32_t dy = 0;
int32_t incX = 0;
int32_t incY = 0;
int32_t balance = 0;
if (!_clipLine(&x1, &y1, &x2, &y2)) {
return;
}
x = x1;
y = y1;
dx = abs(x2 - x1);
dy = abs(y2 - y1);
incX = (x2 >= x1) ? 1 : -1;
incY = (y2 >= y1) ? 1 : -1;
if (dx >= dy) {
dy <<= 1;
balance = dy - dx;
dx <<= 1;
while (x != x2) {
_putPixel(x, y, pixel);
if (balance >= 0) {
y += incY;
balance -= dx;
}
balance += dy;
x += incX;
}
} else {
dx <<= 1;
balance = dx - dy;
dy <<= 1;
while (y != y2) {
_putPixel(x, y, pixel);
if (balance >= 0) {
x += incX;
balance -= dy;
}
balance += dx;
y += incY;
}
}
_putPixel(x, y, pixel);
}
// Draws the 2D emitters queued this frame on one layer: a textured, tinted, rotated quad per particle
// through SDL_RenderGeometry, in overlay pixels mapped onto the window target. Every quad is built
// in one pass; with several frames the indices are bucketed by frame so each texture is one call.
static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target) {
int32_t emitters[PARTICLES_QUEUE_MAX];
int32_t n = particlesQueued2D(layer, emitters, (int32_t)SDL_arraysize(emitters));
float scaleX = target->w / (float)_global.overlay->w;
float scaleY = target->h / (float)_global.overlay->h;
SDL_Vertex *vertices = NULL;
SDL_Vertex *v = NULL;
int32_t *indices = NULL;
int32_t *starts = NULL;
ParticleTexturesT *cache = NULL;
ParticleViewT *particle = NULL;
EmitterViewT view;
SDL_FColor color;
float c = 0.0f;
float s = 0.0f;
float half = 0.0f;
float dx = 0.0f;
float dy = 0.0f;
int32_t begin = 0;
int32_t count = 0;
int32_t e = 0;
int32_t i = 0;
int32_t f = 0;
int32_t frame = 0;
for (e = 0; e < n; e++) {
if (!particlesViewEmitter(emitters[e], &view) || (view.count == 0) || (view.frameCount == 0)) {
continue;
}
cache = _particleTexturesGet(&view);
if (cache->blend != view.blend) {
for (f = 0; f < cache->count; f++) {
SDL_SetTextureBlendMode(cache->textures[f], (view.blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND);
}
cache->blend = view.blend;
}
_quadScratch(view.count, cache->count);
vertices = _global.quadVertices;
indices = _global.quadIndices;
starts = _global.frameStarts;
memset(starts, 0, (size_t)cache->count * sizeof(int32_t));
for (i = 0; i < view.count; i++) {
particle = &view.particles[i];
v = &vertices[i * QUAD_VERTICES];
c = SDL_cosf(DEGREES_TO_RADIANS(particle->angle));
s = SDL_sinf(DEGREES_TO_RADIANS(particle->angle));
half = particle->size * 0.5f;
color.r = particle->colour[0];
color.g = particle->colour[1];
color.b = particle->colour[2];
color.a = particle->colour[3];
for (f = 0; f < QUAD_VERTICES; f++) {
dx = ((f == 1) || (f == 2)) ? half : -half;
dy = (f >= 2) ? half : -half;
v[f].position.x = target->x + (particle->position.x + dx * c - dy * s) * scaleX;
v[f].position.y = target->y + (particle->position.y + dx * s + dy * c) * scaleY;
v[f].color = color;
v[f].tex_coord.x = ((f == 1) || (f == 2)) ? 1.0f : 0.0f;
v[f].tex_coord.y = (f >= 2) ? 1.0f : 0.0f;
}
starts[SDL_clamp(particle->frame, 0, cache->count - 1)]++;
}
// Each frame's quads start where the earlier frames' end; filling advances each start to its end.
begin = 0;
for (f = 0; f < cache->count; f++) {
count = starts[f];
starts[f] = begin;
begin += count;
}
for (i = 0; i < view.count; i++) {
frame = SDL_clamp(view.particles[i].frame, 0, cache->count - 1);
_quadIndex(indices, starts[frame]++, i);
}
begin = 0;
for (f = 0; f < cache->count; f++) {
if (starts[f] > begin) {
SDL_RenderGeometry(_global.renderer, cache->textures[f], vertices, view.count * QUAD_VERTICES, indices + begin * QUAD_INDICES, (starts[f] - begin) * QUAD_INDICES);
}
begin = starts[f];
}
_drawTrails2D(&view, cache, target, scaleX, scaleY);
}
}
// Draws the PAUSED indicator, built from the console font on first use, centered on the target.
static void _drawPauseIndicator(const SDL_FRect *target) {
SDL_Surface *text = NULL;
SDL_Rect src;
SDL_Rect dest;
SDL_FRect where;
int32_t i = 0;
if (_global.pauseTexture == NULL) {
_global.pauseTextureWidth = (int32_t)strlen(PAUSE_TEXT) * _global.consoleFontWidth;
_global.pauseTextureHeight = _global.consoleFontHeight;
text = SDL_CreateSurface(_global.pauseTextureWidth, _global.pauseTextureHeight, SDL_PIXELFORMAT_BGRA32);
if (text == NULL) {
utilDie("%s", SDL_GetError());
}
src.y = 0;
src.w = _global.consoleFontWidth;
src.h = _global.consoleFontHeight;
dest.y = 0;
dest.w = _global.consoleFontWidth;
dest.h = _global.consoleFontHeight;
for (i = 0; PAUSE_TEXT[i] != 0; i++) {
src.x = (uint8_t)PAUSE_TEXT[i] * _global.consoleFontWidth;
dest.x = i * _global.consoleFontWidth;
SDL_BlitSurface(_global.consoleFontSurface, &src, text, &dest);
}
_global.pauseTexture = SDL_CreateTextureFromSurface(_global.renderer, text);
SDL_DestroySurface(text);
if (_global.pauseTexture == NULL) {
utilDie("%s", SDL_GetError());
}
}
where.w = (float)(_global.pauseTextureWidth * PAUSE_TEXT_SCALE);
where.h = (float)(_global.pauseTextureHeight * PAUSE_TEXT_SCALE);
where.x = target->x + (target->w - where.w) / 2.0f;
where.y = target->y + (target->h - where.h) / 2.0f;
SDL_RenderTexture(_global.renderer, _global.pauseTexture, NULL, &where);
}
// The four bands between two nested rectangles: a ring, with whatever is inside it left alone.
static void _drawRing(const SDL_FRect *outer, const SDL_FRect *inner, const SDL_Color *color) {
SDL_FRect band[4];
band[0].x = outer->x;
band[0].y = outer->y;
band[0].w = outer->w;
band[0].h = inner->y - outer->y;
band[1].x = outer->x;
band[1].y = inner->y + inner->h;
band[1].w = outer->w;
band[1].h = (outer->y + outer->h) - (inner->y + inner->h);
band[2].x = outer->x;
band[2].y = inner->y;
band[2].w = inner->x - outer->x;
band[2].h = inner->h;
band[3].x = inner->x + inner->w;
band[3].y = inner->y;
band[3].w = (outer->x + outer->w) - (inner->x + inner->w);
band[3].h = inner->h;
SDL_SetRenderDrawColor(_global.renderer, color->r, color->g, color->b, color->a);
SDL_RenderFillRects(_global.renderer, band, (int32_t)SDL_arraysize(band));
}
// The Sinden light gun border. Around the picture the two rings are filled rectangles the picture
// then covers, which is how Singe has always drawn them. At the window's edge, where bezel artwork
// puts them, they are drawn as rings instead and last of all, so the artwork between the border and
// the picture stays visible and nothing paints over the white ring the gun's camera looks for.
static void _drawSindenBorder(void) {
if (_global.sindenAtWindow) {
if (_global.sindenBlackWidth > 0) {
_drawRing(&_global.sindenBlackRect, &_global.sindenWhiteRect, &_global.sindenBlackColor);
}
_drawRing(&_global.sindenWhiteRect, &_global.sindenInnerRect, &_global.sindenWhiteColor);
return;
}
if (_global.sindenBlackWidth > 0) {
SDL_SetRenderDrawColor(_global.renderer, _global.sindenBlackColor.r, _global.sindenBlackColor.g, _global.sindenBlackColor.b, _global.sindenBlackColor.a);
SDL_RenderFillRect(_global.renderer, &_global.sindenBlackRect);
}
SDL_SetRenderDrawColor(_global.renderer, _global.sindenWhiteColor.r, _global.sindenWhiteColor.g, _global.sindenWhiteColor.b, _global.sindenWhiteColor.a);
SDL_RenderFillRect(_global.renderer, &_global.sindenWhiteRect);
}
// The 2D emitter's trails: a ribbon of quads through each particle's recorded points, fading
// toward the tail, textured by the middle column of the first frame so the disc's edge softens it.
static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY) {
SDL_Vertex *vertices = NULL;
SDL_Vertex *v = NULL;
int32_t *indices = NULL;
const float *points = NULL;
SDL_FColor color;
int32_t segments = 0;
int32_t count = 0;
int32_t i = 0;
int32_t j = 0;
int32_t k = 0;
float half = view->trailWidth * 0.5f;
float x0 = 0.0f;
float y0 = 0.0f;
float x1 = 0.0f;
float y1 = 0.0f;
float dx = 0.0f;
float dy = 0.0f;
float length = 0.0f;
float nx = 0.0f;
float ny = 0.0f;
float side = 0.0f;
bool head = false;
if ((view->trailLength < 2) || (view->trailCounts == NULL) || (view->count == 0)) {
return;
}
_quadScratch(view->count * (view->trailLength - 1), 0);
vertices = _global.quadVertices;
indices = _global.quadIndices;
for (i = 0; i < view->count; i++) {
points = view->trailPoints + (size_t)i * (size_t)view->trailLength * TRAIL_POINT_FLOATS;
count = view->trailCounts[i];
color.r = view->particles[i].colour[0];
color.g = view->particles[i].colour[1];
color.b = view->particles[i].colour[2];
for (j = 1; j < count; j++) {
x0 = view->trailOffset.x + points[(j - 1) * TRAIL_POINT_FLOATS];
y0 = view->trailOffset.y + points[(j - 1) * TRAIL_POINT_FLOATS + 1];
x1 = view->trailOffset.x + points[j * TRAIL_POINT_FLOATS];
y1 = view->trailOffset.y + points[j * TRAIL_POINT_FLOATS + 1];
dx = x1 - x0;
dy = y1 - y0;
length = SDL_sqrtf(dx * dx + dy * dy);
nx = (length > 0.0f) ? -dy / length * half : 0.0f;
ny = (length > 0.0f) ? dx / length * half : 0.0f;
v = &vertices[segments * QUAD_VERTICES];
for (k = 0; k < QUAD_VERTICES; k++) {
head = (k == 1) || (k == 2);
side = (k >= 2) ? 1.0f : -1.0f;
v[k].position.x = target->x + ((head ? x1 : x0) + nx * side) * scaleX;
v[k].position.y = target->y + ((head ? y1 : y0) + ny * side) * scaleY;
v[k].color = color;
v[k].color.a = view->particles[i].colour[3] * (float)(head ? j : j - 1) / (float)(count - 1);
v[k].tex_coord.x = 0.5f;
v[k].tex_coord.y = (side < 0.0f) ? 0.0f : 1.0f;
}
_quadIndex(indices, segments, segments);
segments++;
}
}
if (segments > 0) {
SDL_RenderGeometry(_global.renderer, cache->textures[0], vertices, segments * QUAD_VERTICES, indices, segments * QUAD_INDICES);
}
}
// The volume of a positioned sound at a distance: full within near, inverse distance beyond it,
// and fading out over the last fifth before far.
static float _effectGain(const EffectT *effect, float distance) {
float gain = 1.0f;
float fadeStart;
if (distance > effect->nearBy) {
gain = effect->nearBy / distance;
}
fadeStart = effect->farOff * (1.0f - SOUND_FADE_FRACTION);
if (distance > fadeStart) {
gain *= SDL_clamp((effect->farOff - distance) / (effect->farOff - fadeStart), 0.0f, 1.0f);
}
return gain;
}
// A channel about to play afresh: no position, no pan, the default range.
// What a channel's track plays at: the master effects volume, the channel's own volume, and its
// distance gain while it is in the scene. Every gain change goes through here so there is one
// answer to what a channel is worth, and the master can move without losing the rest.
static void _effectApplyGain(int32_t channel) {
EffectT *effect = &_effects[channel];
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME) * effect->volume * (effect->positioned ? effect->gain : 1.0f));
}
static void _effectReset(int32_t channel) {
EffectT *effect = &_effects[channel];
memset(effect, 0, sizeof(*effect));
effect->node = LISTENER_CAMERA;
effect->nearBy = SOUND_DEFAULT_NEAR;
effect->farOff = SOUND_DEFAULT_FAR;
effect->gain = 1.0f;
effect->volume = 1.0f;
MIX_SetTrack3DPosition(_effectTracks[channel], NULL);
_effectApplyGain(channel);
}
// SDL_mixer calls this from its mixing thread. Just queue the channel; the game loop reads it under the mixer lock.
static void _effectStopped(void *userdata, MIX_Track *track) {
(void)track;
if (_global.soundQueueCount < SOUND_QUEUE_SIZE) {
_global.soundQueue[_global.soundQueueCount++] = (int32_t)(intptr_t)userdata;
}
}
// First effect track that is neither playing nor paused, or SOUND_CHANNEL_NONE when all are busy.
static int32_t _effectTrackFree(void) {
int32_t x = 0;
for (x = 0; x < EFFECT_TRACKS; x++) {
if (!MIX_TrackPlaying(_effectTracks[x]) && !MIX_TrackPaused(_effectTracks[x])) {
return x;
}
}
return SOUND_CHANNEL_NONE;
}
// What the engine does itself for the switches it owns, on the press, whether it came from a button
// or a chord. Anything else is the script's alone.
static void _engineAction(InputE engine) {
switch (engine) {
case INPUT_PAUSE:
if (_global.pauseEnabled) {
_setPause(!_global.pauseState, true);
}
break;
case INPUT_GRAB:
_setMouseCaptured(!_global.mouseGrabbed);
break;
case INPUT_QUIT:
if (_global.quitEnabled) {
_global.running = false;
}
break;
case INPUT_SCREENSHOT:
// Force a redraw so the shot is taken now, even while paused.
_global.requestScreenShot = true;
_global.refreshDisplay = true;
break;
case INPUT_RECORDING:
// Starts the next singeNNN.mp4, or stops the one being written.
_global.recordRequest = recordIsRecording() ? RECORD_REQUEST_STOP : RECORD_REQUEST_START;
_global.refreshDisplay = true;
break;
default:
break;
}
}
// Which engine owned switch (pause, quit, screenshot, grab, record) a code is mapped to, or INPUT_COUNT.
static InputE _engineSwitch(int32_t scancode) {
static const InputE owned[] = { INPUT_PAUSE, INPUT_QUIT, INPUT_SCREENSHOT, INPUT_GRAB, INPUT_RECORDING };
int32_t i = 0;
int32_t index = 0;
for (i = 0; i < (int32_t)(sizeof(owned) / sizeof(owned[0])); i++) {
for (index = 0; index < _global.controlMappings[owned[i]].inputCount; index++) {
if (_global.controlMappings[owned[i]].input[index] == scancode) {
return owned[i];
}
}
}
return INPUT_COUNT;
}
// Caches a mouse position (overlay coordinates) and tells the script.
static void _fireMouseMoved(int32_t device, int32_t x, int32_t y, int32_t xr, int32_t yr) {
_global.axisCache[AXIS_INDEX_MOUSE(device, 0)] = x;
_global.axisCache[AXIS_INDEX_MOUSE(device, 1)] = y;
if (!_global.frozen) {
_callLua("onMouseMoved", "iiiii", x, y, xr, yr, device);
}
}
// Largest rectangle of the given aspect that fits inside the space with a margin, centred.
static void _fitRect(int32_t width, int32_t height, int32_t spaceWidth, int32_t spaceHeight, int32_t margin, SDL_FRect *rect) {
float scale = SDL_min((float)(spaceWidth - 2 * margin) / (float)width, (float)(spaceHeight - 2 * margin) / (float)height);
rect->w = (float)width * scale;
rect->h = (float)height * scale;
rect->x = ((float)spaceWidth - rect->w) * 0.5f;
rect->y = ((float)spaceHeight - rect->h) * 0.5f;
}
static void _fontDestroy(FontT *font) {
if (_global.fontCurrent == font) {
_global.fontCurrent = NULL;
}
_textCacheDrop(font->id);
HASH_DEL(_global.fontList, font);
TTF_CloseFont(font->font);
free(font);
}
// Engine pause. Freezing releases everything the script thinks is held so it never sees a
// stale button; thawing presses whatever is still physically down.
static void _freezeGame(bool freeze) {
int32_t i = 0;
HeldKeyT held[HELD_KEYS_MAX];
int32_t heldCount = 0;
if (freeze) {
for (i = 0; i < INPUT_COUNT; i++) {
if (_global.switchHeld[i]) {
_global.switchHeld[i] = false;
_callLua("onInputReleased", "i", i);
}
}
// Copy first: the callbacks may not touch the list, but keep it simple.
heldCount = _global.heldKeyCount;
memcpy(held, _global.heldKeys, sizeof(HeldKeyT) * (size_t)heldCount);
_global.heldKeyCount = 0;
for (i = 0; i < heldCount; i++) {
_callLua("onInputReleased", "i", held[i].keysym);
_callLua("onKeyReleased", "ii", held[i].keysym, held[i].scancode);
}
_global.frozen = true;
} else {
_global.frozen = false;
for (i = 0; i < _global.physicalKeyCount; i++) {
_deliverKey(true, _global.physicalKeys[i].keysym, _global.physicalKeys[i].scancode);
}
_global.keyboardLastDown = SDL_SCANCODE_UNKNOWN;
_global.keyboardLastUp = SDL_SCANCODE_UNKNOWN;
}
_global.refreshDisplay = true;
}
// Maintains a list of keys that are down.
// A gamepad mapping for a device SDL does not recognise. Its axes in order become the two sticks
// and then the two triggers, its buttons in order become the standard gamepad buttons, and its
// first hat becomes the D-pad (a device with no hat spends its last four buttons on the D-pad
// instead). A device with more controls than the gamepad model can name loses the extras, which is
// the price of every device reaching a game through one family rather than two, and the mapping is
// traced so a user can paste it into their own gamecontrollerdb.txt and rearrange it.
static bool _gamepadMappingFor(SDL_JoystickID id, char *mapping, size_t size) {
static const char *axisNames[] = { "leftx", "lefty", "rightx", "righty", "lefttrigger", "righttrigger" };
static const char *buttonNames[] = { "a", "b", "x", "y", "back", "guide", "start", "leftstick", "rightstick", "leftshoulder", "rightshoulder", "dpup", "dpdown", "dpleft", "dpright" };
static const char *hatNames[] = { "dpup", "dpright", "dpdown", "dpleft" }; // In the order of SDL's hat bits 1, 2, 4 and 8
SDL_Joystick *joystick = SDL_OpenJoystick(id);
char guid[GAMEPAD_GUID_MAX];
char name[GAMEPAD_NAME_MAX];
const char *reported = NULL;
size_t used = 0;
int32_t axes = 0;
int32_t buttons = 0;
int32_t hats = 0;
int32_t names = 0;
int32_t x = 0;
// The counts need the device open; the name and the GUID are copied out before it is closed
// again, since the name belongs to the joystick and goes with it.
if (joystick == NULL) {
return false;
}
axes = SDL_GetNumJoystickAxes(joystick);
buttons = SDL_GetNumJoystickButtons(joystick);
hats = SDL_GetNumJoystickHats(joystick);
reported = SDL_GetJoystickName(joystick);
SDL_strlcpy(name, (reported != NULL) ? reported : GAMEPAD_NAME_UNKNOWN, sizeof(name));
SDL_GUIDToString(SDL_GetJoystickGUID(joystick), guid, sizeof(guid));
SDL_CloseJoystick(joystick);
if ((axes <= 0) && (buttons <= 0) && (hats <= 0)) {
return false;
}
// A comma in the name would end the field, so any comma becomes a space.
for (x = 0; name[x] != '\0'; x++) {
if (name[x] == ',') {
name[x] = ' ';
}
}
used = (size_t)SDL_snprintf(mapping, size, "%s,%s,platform:%s", guid, name, SDL_GetPlatform());
names = (int32_t)(sizeof(buttonNames) / sizeof(buttonNames[0]));
if (hats > 0) {
names -= GAMEPAD_HAT_DIRECTIONS;
}
for (x = 0; (x < buttons) && (x < names) && (used < size); x++) {
used += (size_t)SDL_snprintf(mapping + used, size - used, ",%s:b%d", buttonNames[x], x);
}
names = (int32_t)(sizeof(axisNames) / sizeof(axisNames[0]));
for (x = 0; (x < axes) && (x < names) && (used < size); x++) {
used += (size_t)SDL_snprintf(mapping + used, size - used, ",%s:a%d", axisNames[x], x);
}
for (x = 0; (hats > 0) && (x < GAMEPAD_HAT_DIRECTIONS) && (used < size); x++) {
used += (size_t)SDL_snprintf(mapping + used, size - used, ",%s:h0.%d", hatNames[x], 1 << x);
}
if (used >= size - 1) {
utilSay("Warning: %s has too many controls to describe in one mapping; it is not opened.", name);
return false;
}
SDL_snprintf(mapping + used, size - used, ",");
return true;
}
// --gamepad_reorder decides which physical pad is player one. Its digits are enumeration positions
// counted from zero, as Hypseus counts them, one for each slot in turn; a position it does not name
// fills whichever slot is left, in the order SDL found the pads. Without the option every slot is
// SDL's own position, which is what Singe has always done.
static void _gamepadOrder(int32_t *order, int32_t count) {
const char *text = _global.conf->gamepadOrder;
bool used[GAMEPAD_ORDER_DIGITS];
int32_t slot = 0;
int32_t x = 0;
memset(used, 0, sizeof(used));
for (slot = 0; slot < MAX_CONTROLLERS; slot++) {
order[slot] = -1;
}
slot = 0;
while ((text != NULL) && (*text != '\0') && (slot < MAX_CONTROLLERS)) {
if ((*text >= '0') && (*text <= '9')) {
x = *text - '0';
if (!used[x]) {
used[x] = true;
order[slot] = x;
slot++;
}
}
text++;
}
for (x = 0; (x < count) && (slot < MAX_CONTROLLERS); x++) {
if ((x < GAMEPAD_ORDER_DIGITS) && used[x]) {
continue;
}
order[slot] = x;
slot++;
}
}
// A recorded deterministic run mixes its sound into memory, one clock step of it each loop, so the
// recording's sound is exactly what went with each frame. The recorder takes it from the mixer as
// it is mixed; what lands in the buffer is not heard by anyone.
static void _generateAudio(void) {
MIX_Mixer *mixer = videoGetMixer();
SDL_AudioSpec spec;
double want = 0.0;
int32_t bytes = 0;
if (!MIX_GetMixerFormat(mixer, &spec)) {
return;
}
want = (double)_global.conf->deterministicStep * (double)spec.freq / 1000.0 + _global.generatedCarry;
bytes = (int32_t)want * SDL_AUDIO_FRAMESIZE(spec);
_global.generatedCarry = want - (double)(int32_t)want;
if (bytes > _global.generatedBufferSize) {
free(_global.generatedBuffer);
_global.generatedBuffer = (uint8_t *)malloc((size_t)bytes);
_global.generatedBufferSize = bytes;
}
if (bytes > 0) {
MIX_Generate(mixer, _global.generatedBuffer, bytes);
}
}
// A GUI element's event, from RmlUi through gui.cpp: the function guiSetHandler registered for it.
static void _guiEvent(int32_t gui, int32_t document, const char *id, const char *event, const char *value) {
lua_State *L = _global.luaContext;
char key[GUI_ID_MAX * 2];
int32_t handler;
if (L == NULL) {
return;
}
_guiHandlerKey(key, sizeof(key), gui, document, id, event);
if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, GUI_HANDLERS)) {
lua_pop(L, 1);
return;
}
lua_getfield(L, -1, key);
lua_remove(L, -2);
if (!lua_isfunction(L, -1)) {
lua_pop(L, 1);
return;
}
lua_pushcfunction(L, _luaTraceback);
lua_insert(L, -2);
handler = lua_gettop(L) - 1;
lua_pushinteger(L, gui);
lua_pushinteger(L, document);
lua_pushstring(L, id);
lua_pushstring(L, event);
lua_pushstring(L, value);
if (scriptWatchCall(L, 5, 0, handler) != LUA_OK) {
utilDie("Error in GUI handler for \"%s\" %s: %s", id, event, lua_tostring(L, -1));
}
lua_settop(L, handler - 1);
}
static void _guiHandlerKey(char *buffer, size_t size, int32_t gui, int32_t document, const char *id, const char *event) {
snprintf(buffer, size, "%d:%d:%s:%s", gui, document, id, event);
}
// A key to every GUI taking input; true when one of them used it.
static bool _guiKey(SDL_Keycode key, SDL_Keymod modifiers, bool down) {
bool consumed = false;
int32_t x;
// RmlUi takes TAB for moving focus between fields and never hands it back, so a game whose
// keys include TAB would lose it to a document that has no field to focus. It goes to the
// GUI only while a text field is being typed in.
if ((key == SDLK_TAB) && !_global.guiTextActive) {
return false;
}
for (x = 0; x < GUI_MAX_CONTEXTS; x++) {
if (guiValid(x) && guiGetInput(x) && guiKey(x, key, modifiers, down)) {
consumed = true;
}
}
return consumed;
}
// A mouse button (SDL's numbering) to the GUI under the pointer; true when it landed on an element.
static bool _guiMouseButton(int32_t button, bool down) {
int32_t gui;
int32_t guiX;
int32_t guiY;
if (!_guiPointer(_global.axisCache[AXIS_INDEX_MOUSE(0, 0)], _global.axisCache[AXIS_INDEX_MOUSE(0, 1)], &gui, &guiX, &guiY)) {
return false;
}
return guiMouseButton(gui, button, down);
}
// The pointer moved: the GUI under it (if any) follows it, and the one it left forgets it.
static void _guiMouseMove(int32_t x, int32_t y) {
int32_t gui = GUI_NO_HANDLE;
int32_t guiX = 0;
int32_t guiY = 0;
_guiPointer(x, y, &gui, &guiX, &guiY);
if ((_global.guiHover != gui) && (_global.guiHover != GUI_NO_HANDLE) && guiValid(_global.guiHover)) {
guiMouseMove(_global.guiHover, -1, -1);
}
_global.guiHover = gui;
if (gui != GUI_NO_HANDLE) {
guiMouseMove(gui, guiX, guiY);
}
}
static bool _guiMouseWheel(float delta) {
int32_t gui;
int32_t guiX;
int32_t guiY;
if (!_guiPointer(_global.axisCache[AXIS_INDEX_MOUSE(0, 0)], _global.axisCache[AXIS_INDEX_MOUSE(0, 1)], &gui, &guiX, &guiY)) {
return false;
}
return guiMouseWheel(gui, delta);
}
// The GUI under an overlay position, taking input, and the position inside it in its own pixels:
// one drawn flat last frame (the last drawn is on top), else one on a surface in the scene.
static bool _guiPointer(int32_t x, int32_t y, int32_t *gui, int32_t *guiX, int32_t *guiY) {
SDL_FRect dest;
int32_t i;
float u;
float v;
float pointX;
float pointY;
// A screen space GUI was drawn across the whole window instead of into the picture, so the
// pointer, which arrives in overlay coordinates over the picture, is carried the same way.
float screenX = (_global.videoRect.x + (float)x * _global.videoRect.w / (float)_global.overlay->w) * (float)_global.overlay->w / (float)_global.canvasWidth;
float screenY = (_global.videoRect.y + (float)y * _global.videoRect.h / (float)_global.overlay->h) * (float)_global.overlay->h / (float)_global.canvasHeight;
for (i = _global.guiShownCount - 1; i >= 0; i--) {
const GuiDrawT *shown = &_global.guiShown[i];
if (!guiValid(shown->gui) || !guiGetInput(shown->gui)) {
continue;
}
pointX = shown->screen ? screenX : (float)x;
pointY = shown->screen ? screenY : (float)y;
if (shown->whole) {
dest.x = 0.0f;
dest.y = 0.0f;
dest.w = (float)_global.overlay->w;
dest.h = (float)_global.overlay->h;
} else {
dest = shown->dest;
}
if ((pointX < dest.x) || (pointY < dest.y) || (pointX >= dest.x + dest.w) || (pointY >= dest.y + dest.h) || (dest.w <= 0.0f) || (dest.h <= 0.0f)) {
continue;
}
*gui = shown->gui;
*guiX = (int32_t)((pointX - dest.x) * (float)guiGetWidth(shown->gui) / dest.w);
*guiY = (int32_t)((pointY - dest.y) * (float)guiGetHeight(shown->gui) / dest.h);
return true;
}
// Nothing drawn flat under it: a surface in the scene showing a GUI that takes input.
if (sceneIsEnabled() && sceneProbeGui((float)x, (float)y, gui, &u, &v) && guiGetInput(*gui)) {
*guiX = (int32_t)(u * (float)guiGetWidth(*gui));
*guiY = (int32_t)(v * (float)guiGetHeight(*gui));
return true;
}
*gui = GUI_NO_HANDLE;
return false;
}
// guiDraw and guiDrawScreen queue the same record; only the rectangle it lands in differs.
static int32_t _guiQueueDraw(lua_State *L, const char *method, bool screen) {
GuiDrawT *draw;
int32_t top = lua_gettop(L);
SDL_Rect nothing = { 0, 0, 0, 0 };
_argCheck(L, method, 1, 5);
if ((top == 2) || (top == 4)) {
_luaDie(L, method, "Expected 1, 3 or 5 arguments, got %d.", top);
}
if (_global.guiDrawCount >= GUI_DRAW_MAX) {
return 0;
}
draw = &_global.guiDraws[_global.guiDrawCount];
draw->gui = _argGui(L, method, 1);
draw->whole = (top == 1);
draw->screen = screen;
if (!draw->whole) {
draw->dest.x = (float)_argNumber(L, method, 2);
draw->dest.y = (float)_argNumber(L, method, 3);
draw->dest.w = (top == 5) ? (float)_argNumber(L, method, 4) : (float)guiGetWidth(draw->gui);
draw->dest.h = (top == 5) ? (float)_argNumber(L, method, 5) : (float)guiGetHeight(draw->gui);
}
draw->layer = screen ? OVERLAY_NO_LAYER : _overlayLayerOpen();
_global.guiDrawCount++;
_overlayTouched(&nothing);
return 0;
}
// Both passes of _drawGuis are done: this frame's GUIs become the ones a pointer may land on.
// The queue itself is not emptied here -- the script's next turn does that -- so the same GUIs are
// drawn again on the refreshes between script frames instead of vanishing.
static void _guisShown(void) {
memcpy(_global.guiShown, _global.guiDraws, sizeof(GuiDrawT) * (size_t)_global.guiDrawCount);
_global.guiShownCount = _global.guiDrawCount;
}
// A framework switch from a pad or mouse, as the navigation key a document understands: the
// directions, ACTION_1 as Return (activate) and ACTION_2 as Escape. True when a GUI used it.
static bool _guiSwitch(InputE move, bool down) {
SDL_Keycode key = SDLK_UNKNOWN;
switch (move) {
case INPUT_UP:
key = SDLK_UP;
break;
case INPUT_DOWN:
key = SDLK_DOWN;
break;
case INPUT_LEFT:
key = SDLK_LEFT;
break;
case INPUT_RIGHT:
key = SDLK_RIGHT;
break;
case INPUT_ACTION_1:
key = SDLK_RETURN;
break;
case INPUT_ACTION_2:
key = SDLK_ESCAPE;
break;
default:
return false;
}
return _guiKey(key, SDL_KMOD_NONE, down);
}
static bool _guiText(const char *text) {
bool consumed = false;
int32_t x;
for (x = 0; x < GUI_MAX_CONTEXTS; x++) {
if (guiValid(x) && guiGetInput(x) && guiText(x, text)) {
consumed = true;
}
}
return consumed;
}
// SDL's text input runs while a GUI taking input has a text field focused, and while the script
// has the keyboard whole (MODE_FULL), when what the keys produce -- shifted, on whatever layout --
// reaches it as onTextInput.
static void _guiTextInput(void) {
bool field = false;
bool wants = false;
int32_t x;
for (x = 0; x < GUI_MAX_CONTEXTS; x++) {
if (guiValid(x) && guiGetInput(x) && guiWantsText(x)) {
field = true;
}
}
_global.guiTextActive = field;
wants = field || (_global.keyboardMode == KEYBOARD_FULL);
if (wants != _global.textInputOn) {
_global.textInputOn = wants;
if (wants) {
SDL_StartTextInput(_global.window);
} else {
SDL_StopTextInput(_global.window);
}
}
}
static void _heldListUpdate(HeldKeyT *list, int32_t *count, bool down, int32_t keysym, int32_t scancode) {
int32_t i = 0;
for (i = 0; i < *count; i++) {
if (list[i].scancode == scancode) {
break;
}
}
if (down) {
if ((i == *count) && (*count < HELD_KEYS_MAX)) {
list[*count].keysym = keysym;
list[*count].scancode = scancode;
(*count)++;
}
} else {
if (i < *count) {
(*count)--;
list[i] = list[*count];
}
}
}
// Whether this user is refused the input devices: they are there but cannot be read, which is what
// an account outside the "input" group sees. ManyMouse finding no mice means only that on a machine
// that has none, a handheld for one, so that is no reason to mention the group.
static bool _inputDevicesDenied(void) {
#ifdef __linux__
DIR *dir = opendir(INPUT_DEVICE_DIR);
struct dirent *entry = NULL;
char path[INPUT_PATH_MAX];
bool denied = false;
if (dir == NULL) {
return false;
}
while (!denied && ((entry = readdir(dir)) != NULL)) {
if (strncmp(entry->d_name, INPUT_EVENT_PREFIX, sizeof(INPUT_EVENT_PREFIX) - 1) == 0) {
snprintf(path, sizeof(path), "%s/%s", INPUT_DEVICE_DIR, entry->d_name);
denied = (access(path, R_OK) != 0) && (errno == EACCES);
}
}
closedir(dir);
return denied;
#else
return false;
#endif
}
// Replaces dofile, loadfile, and the io functions that take a file name with versions that resolve
// the name through the vfs. Each closure keeps the original as its first upvalue.
static void _installFileHooks(lua_State *L) {
// The calls that take a file name, to find it inside the .game.
static const struct {
const char *name;
IoHookModeE mode;
} ioHooks[] = {
{ "input", IO_HOOK_INPUT },
{ "lines", IO_HOOK_LINES },
{ "open", IO_HOOK_OPEN },
{ "output", IO_HOOK_WRITE }
};
// The calls that take a handle, or none, and may be handed one of ours. They are not name hooks
// as well: read's string is a format, and a name hook took "*line" for a file to find.
static const struct {
const char *name;
lua_CFunction hook;
} handleHooks[] = { { "close", _luaIoClose }, { "read", _luaIoRead }, { "type", _luaIoType } };
size_t i = 0;
lua_getglobal(L, "dofile");
lua_pushcclosure(L, _luaDofile, 1);
lua_setglobal(L, "dofile");
lua_getglobal(L, "loadfile");
lua_pushcclosure(L, _luaLoadfile, 1);
lua_setglobal(L, "loadfile");
_luaFileRegister(L);
lua_getglobal(L, "io");
for (i = 0; i < sizeof(ioHooks) / sizeof(ioHooks[0]); i++) {
lua_getfield(L, -1, ioHooks[i].name);
lua_pushinteger(L, ioHooks[i].mode);
lua_pushcclosure(L, _luaIoHook, 2);
lua_setfield(L, -2, ioHooks[i].name);
}
for (i = 0; i < sizeof(handleHooks) / sizeof(handleHooks[0]); i++) {
lua_getfield(L, -1, handleHooks[i].name);
lua_pushcclosure(L, handleHooks[i].hook, 1);
lua_setfield(L, -2, handleHooks[i].name);
}
lua_pop(L, 1);
}
// Whether an SDL event is somebody touching a control, which is what --idleexit counts and what
// --startsilent waits for. Devices arriving and leaving are not.
static bool _isInputEvent(uint32_t type) {
switch (type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
case SDL_EVENT_TEXT_INPUT:
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return true;
default:
return false;
}
}
// --joymouse: a stick drives the mouse cursor, so a light gun game is playable on a cabinet that
// has a stick and no mouse. The left stick of gamepad slot 0 drives it -- every device Singe opens
// is a gamepad, an unrecognised one through a mapping Singe writes for it -- and --js_range is how
// many video pixels a frame full deflection is worth. The movement is turned back through the
// presentation exactly as a real mouse's is, so a rotated cabinet moves the cursor the way the
// player sees the stick pushed.
static void _joyMouseUpdate(void) {
int32_t xAxis = 0;
int32_t yAxis = 0;
int32_t deadX = 0;
int32_t deadY = 0;
int32_t index = 0;
float moveX = 0.0f;
float moveY = 0.0f;
if (!_global.joyMouseEnabled || !_global.mouseEnabled || (_global.mouseMode != MOUSE_SINGLE) || (_global.canvasWidth <= 0)) {
return;
}
if (utilTicks() < _global.joyMouseClock) {
return;
}
_global.joyMouseClock = utilTicks() + FRAME_TICK_MS;
if (_global.controllers[0] == NULL) {
return;
}
index = AXIS_INDEX_CONTROLLER(0, 0);
xAxis = _global.axisCache[index];
yAxis = _global.axisCache[index + 1];
deadX = _global.axisDeadZone[index];
deadY = _global.axisDeadZone[index + 1];
if (abs(xAxis) > deadX) {
moveX = (float)xAxis * (float)_global.conf->joyMouseRange / (float)AXIS_VALUE_MAX;
}
if (abs(yAxis) > deadY) {
moveY = (float)yAxis * (float)_global.conf->joyMouseRange / (float)AXIS_VALUE_MAX;
}
if ((moveX == 0.0f) && (moveY == 0.0f)) {
return;
}
_unrotateDelta(moveX, moveY, &moveX, &moveY);
_global.joyMouseX += (int32_t)moveX;
_global.joyMouseY += (int32_t)moveY;
_global.joyMouseX = SDL_clamp(_global.joyMouseX, 0, _global.canvasWidth - 1);
_global.joyMouseY = SDL_clamp(_global.joyMouseY, 0, _global.canvasHeight - 1);
_fireMouseMoved(0, (int32_t)(_global.joyMouseX * _global.overlayScaleX), (int32_t)(_global.joyMouseY * _global.overlayScaleY), (int32_t)moveX, (int32_t)moveY);
}
// -p: a button of a pad that its gamepad mapping leaves out never reaches a game or --quitchord, and
// nothing else would say so. Its raw number is traced, so a gamecontrollerdb.txt line can name it.
static void _joystickButtonTrace(const SDL_JoyButtonEvent *event) {
SDL_Gamepad *pad = SDL_GetGamepadFromID(event->which);
SDL_GamepadBinding **bindings = NULL;
int32_t count = 0;
int32_t x = 0;
bool mapped = false;
if (!_global.conf->programTracing || (pad == NULL)) {
return;
}
bindings = SDL_GetGamepadBindings(pad, &count);
for (x = 0; (bindings != NULL) && (x < count) && !mapped; x++) {
mapped = (bindings[x]->input_type == SDL_GAMEPAD_BINDTYPE_BUTTON) && (bindings[x]->input.button == event->button);
}
SDL_free(bindings);
if (!mapped) {
_progTrace("Pad %d: button b%d %s is not in its mapping; a gamecontrollerdb.txt line can name it", _controllerSlot(event->which), event->button, event->down ? "down" : "up");
}
}
// lfs.attributes through the vfs, with lfs's own forms: attributes(path) makes a table,
// attributes(path, name) answers one attribute (an unknown name raises), attributes(path, table)
// fills the table. A packed entry answers as a read only file; a plain filesystem path goes to
// the original (upvalue 1) untouched.
static int32_t _lfsAttributes(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
const char *attribute = NULL;
int64_t size = 0;
int64_t modified = 0;
bool directory = false;
if (vfsIsFilesystem(name)) {
return _luaCallOriginal(L);
}
directory = vfsIsDirectory(name);
if (!directory && !vfsStat(name, &size, &modified)) {
lua_pushnil(L);
lua_pushfstring(L, "cannot obtain information from file '%s': %s", name, strerror(ENOENT));
lua_pushinteger(L, ENOENT);
return 3;
}
if (lua_isstring(L, 2)) {
attribute = lua_tostring(L, 2);
lua_newtable(L);
_lfsFillAttributes(L, directory, size, modified);
lua_getfield(L, -1, attribute);
if (lua_isnil(L, -1)) {
return luaL_error(L, "invalid attribute name '%s'", attribute);
}
return 1;
}
lua_settop(L, 2);
if (!lua_istable(L, 2)) {
lua_newtable(L);
}
_lfsFillAttributes(L, directory, size, modified);
return 1;
}
// lfs.dir through the vfs: a packed directory iterates the union of loose, overlay and packed
// entries; a plain filesystem path goes to the original (upvalue 1).
static int32_t _lfsDir(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
char **list = NULL;
int32_t count = 0;
int32_t i = 0;
if (vfsIsFilesystem(name)) {
return _luaCallOriginal(L);
}
if (!vfsIsDirectory(name)) {
return luaL_error(L, "cannot open %s: No such file or directory", name);
}
list = vfsList(name, &count);
lua_createtable(L, count, 0);
for (i = 0; i < count; i++) {
lua_pushstring(L, list[i]);
lua_rawseti(L, -2, i + 1);
}
vfsListFree(list, count);
lua_pushinteger(L, 0);
lua_pushcclosure(L, _lfsDirIterator, 2);
return 1;
}
// The iterator lfs.dir returns for a packed directory: upvalues are the entry table and the index.
static int32_t _lfsDirIterator(lua_State *L) {
lua_Integer index = lua_tointeger(L, lua_upvalueindex(2)) + 1;
lua_pushinteger(L, index);
lua_copy(L, -1, lua_upvalueindex(2));
lua_pop(L, 1);
lua_rawgeti(L, lua_upvalueindex(1), index);
return 1;
}
// Every attribute real lfs reports, into the table on top of the stack, for a packed entry.
static void _lfsFillAttributes(lua_State *L, bool directory, int64_t size, int64_t modified) {
lua_pushstring(L, directory ? "directory" : "file");
lua_setfield(L, -2, "mode");
lua_pushinteger(L, 0);
lua_setfield(L, -2, "dev");
lua_pushinteger(L, 0);
lua_setfield(L, -2, "ino");
lua_pushinteger(L, 1);
lua_setfield(L, -2, "nlink");
lua_pushinteger(L, 0);
lua_setfield(L, -2, "uid");
lua_pushinteger(L, 0);
lua_setfield(L, -2, "gid");
lua_pushinteger(L, 0);
lua_setfield(L, -2, "rdev");
lua_pushinteger(L, (lua_Integer)modified);
lua_setfield(L, -2, "access");
lua_pushinteger(L, (lua_Integer)modified);
lua_setfield(L, -2, "modification");
lua_pushinteger(L, (lua_Integer)modified);
lua_setfield(L, -2, "change");
lua_pushinteger(L, (lua_Integer)size);
lua_setfield(L, -2, "size");
lua_pushstring(L, PACKED_PERMISSIONS);
lua_setfield(L, -2, "permissions");
lua_pushinteger(L, (lua_Integer)((size + PACKED_BLOCK_SIZE - 1) / PACKED_BLOCK_SIZE));
lua_setfield(L, -2, "blocks");
lua_pushinteger(L, PACKED_BLOCK_SIZE);
lua_setfield(L, -2, "blksize");
}
// lfs.mkdir through the vfs: inside a packed game the directory is made where its files will be
// written, the data overlay; a plain filesystem path goes to the original (upvalue 1).
static int32_t _lfsMkdir(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
char *path = NULL;
bool ok = false;
if (vfsIsFilesystem(name)) {
return _luaCallOriginal(L);
}
path = vfsFilePath(name, true);
if (path == NULL) {
lua_pushnil(L);
lua_pushfstring(L, "%s reaches outside the game", name);
return 2;
}
ok = utilMkDirP(path, DIRECTORY_MODE);
free(path);
if (!ok) {
lua_pushnil(L);
lua_pushfstring(L, "cannot create %s", name);
return 2;
}
lua_pushboolean(L, true);
return 1;
}
// lfs.rmdir through the vfs: removes the overlay directory of a packed name; the packed copy
// itself is read only and stays.
static int32_t _lfsRmdir(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
char *path = NULL;
bool ok = false;
if (vfsIsFilesystem(name)) {
return _luaCallOriginal(L);
}
path = vfsFilePath(name, true);
if (path == NULL) {
lua_pushnil(L);
lua_pushfstring(L, "%s reaches outside the game", name);
return 2;
}
ok = (rmdir(path) == 0);
free(path);
if (!ok) {
lua_pushnil(L);
lua_pushfstring(L, "cannot remove %s", name);
return 2;
}
lua_pushboolean(L, true);
return 1;
}
// The built-in controls.cfg, then every override in turn (the working directory, above and inside
// the data directory, beside the script), each place only once, in a throwaway Lua state that has
// the framework but not the API. Leaves the dead zone and the switch mappings behind.
static void _loadControlMappings(void) {
lua_State *L = NULL;
ChordT *chord = NULL;
char *places[SETTINGS_PLACES];
double threshold = 0.0;
int32_t code = 0;
int32_t zone = 0;
int32_t c = 0;
int32_t x = 0;
int32_t y = 0;
_progTrace("Creating Lua context for Singe setup");
L = luaL_newstate();
_global.luaContext = L;
_startLuaContext(L);
// Load framework - NOTE! SINGE API NOT AVAILABLE AT THIS POINT!
// Any calls in the framework need to be wrapped with nil checks!
_progTrace("Loading Singe framework");
if (luaL_loadbuffer(L, (const char *)Framework_singe, Framework_singe_len, "Framework.singe") || lua_pcall(L, 0, 0, 0)) {
utilDie("%s", lua_tostring(L, -1));
}
_progTrace("Loading default control mappings");
if (luaL_loadbuffer(L, (const char *)controls_cfg, controls_cfg_len, CONTROLS_FILE) || lua_pcall(L, 0, 0, 0)) {
utilDie("%s", lua_tostring(L, -1));
}
// --keymapfile names one file and the search is not made, so a front end can be certain
// which mappings a game got.
if (_global.conf->keymapFile != NULL) {
if (!vfsExists(_global.conf->keymapFile)) {
utilDie("Unable to find the control mappings %s.", _global.conf->keymapFile);
}
_loadControlsFile(_global.conf->keymapFile);
} else {
_configPlaces(CONTROLS_FILE, _global.conf->dataDir, _global.conf->scriptFile, places);
for (c = 0; c < SETTINGS_PLACES; c++) {
if (places[c] != NULL) {
_loadControlsFile(places[c]);
}
free(places[c]);
}
}
// Parse results
lua_getglobal(L, "DEAD_ZONE");
if (lua_isnumber(L, -1)) {
_global.controllerDeadZone = (int32_t)lua_tonumber(L, -1);
}
lua_pop(L, 1);
_progTrace("Controller dead zone is %d", _global.controllerDeadZone);
// TRIGGER_THRESHOLD is Hypseus's unit, per cent of full travel, and --trigger_threshold sets
// the same number from the command line, where it wins. Zero leaves the triggers on DEAD_ZONE,
// which is what every release so far has given them, so nothing moves for anyone not asking.
threshold = _global.conf->triggerThreshold;
if (threshold <= 0.0) {
lua_getglobal(L, "TRIGGER_THRESHOLD");
if (lua_isnumber(L, -1)) {
threshold = lua_tonumber(L, -1);
}
lua_pop(L, 1);
}
_global.triggerThreshold = (int32_t)(threshold * (double)AXIS_VALUE_MAX / PERCENT_FULL);
if (_global.triggerThreshold > 0) {
_progTrace("Trigger threshold is %.1f per cent (%d)", threshold, _global.triggerThreshold);
}
// Every axis starts on the global dead zone, the triggers take the threshold when there is one,
// and DEAD_ZONES then names the individual axes a stiff stick or a worn trigger needs moved.
for (x = 0; x < AXIS_COUNT; x++) {
_global.axisDeadZone[x] = _global.controllerDeadZone;
}
if (_global.triggerThreshold > 0) {
for (x = 0; x < MAX_CONTROLLERS; x++) {
for (y = CONTROLLER_TRIGGER_FIRST; y < CONTROLLER_AXIS_COUNT; y++) {
_global.axisDeadZone[AXIS_INDEX_CONTROLLER(x, y)] = _global.triggerThreshold;
}
}
}
lua_getglobal(L, "DEAD_ZONES");
if (lua_istable(L, -1)) {
y = (int32_t)lua_rawlen(L, -1);
for (x = 1; x <= y; x++) {
lua_rawgeti(L, -1, x);
if (lua_istable(L, -1)) {
lua_rawgeti(L, -1, 1);
lua_getfield(L, -1, "value");
code = lua_isnumber(L, -1) ? (int32_t)lua_tonumber(L, -1) : -1;
lua_pop(L, 2);
lua_rawgeti(L, -1, 2);
zone = lua_isnumber(L, -1) ? (int32_t)lua_tonumber(L, -1) : -1;
lua_pop(L, 1);
c = _axisIndexForCode(code);
if ((c < 0) || (zone < 0) || (zone > AXIS_VALUE_MAX)) {
utilSay("Warning: DEAD_ZONES entry %d names no axis, or a dead zone outside 0 to %d.", x, AXIS_VALUE_MAX);
} else {
_global.axisDeadZone[c] = zone;
_progTrace("Dead zone for input code %d is %d", code, zone);
}
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
for (x = 0; x < INPUT_COUNT; x++) {
// Each INPUT_* table holds { name = ..., value = ... } entries; collect the values.
lua_getglobal(L, _inputNames[x].configName);
if (!lua_istable(L, -1)) {
utilSay("Configuration option %s missing!", _inputNames[x].configName);
lua_pop(L, 1);
continue;
}
y = (int32_t)lua_rawlen(L, -1);
_global.controlMappings[x].input = (int32_t *)calloc((size_t)(y + 1), sizeof(int32_t));
_global.controlMappings[x].chord = (ChordT *)calloc((size_t)(y + 1), sizeof(ChordT));
if (!_global.controlMappings[x].input || !_global.controlMappings[x].chord) {
utilDie("Unable to allocate memory for control mappings.");
}
_global.controlMappings[x].inputCount = 0;
_global.controlMappings[x].chordCount = 0;
lua_pushnil(L);
while (lua_next(L, -2)) {
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "value");
if (lua_isnumber(L, -1) && (_global.controlMappings[x].inputCount < y)) {
_global.controlMappings[x].input[_global.controlMappings[x].inputCount++] = (int32_t)lua_tonumber(L, -1);
} else if (!lua_isnumber(L, -1) && (lua_rawlen(L, -2) > 1) && (_global.controlMappings[x].chordCount < y)) {
// No value of its own but entries inside: a chord, { GAMEPAD_0.BUTTON_BACK, GAMEPAD_0.BUTTON_START }.
chord = &_global.controlMappings[x].chord[_global.controlMappings[x].chordCount];
chord->count = 0;
for (c = 1; c <= (int32_t)lua_rawlen(L, -2); c++) {
lua_rawgeti(L, -2, c);
lua_getfield(L, -1, "value");
if (lua_isnumber(L, -1) && (chord->count < CHORD_CODES_MAX)) {
chord->code[chord->count++] = (int32_t)lua_tonumber(L, -1);
}
lua_pop(L, 2);
}
if (chord->count > 1) {
_global.controlMappings[x].chordCount++;
} else {
utilSay("Warning: a chord in %s needs two to %d buttons.", _inputNames[x].configName, CHORD_CODES_MAX);
}
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_close(L);
}
// Runs a controls.cfg if it exists.
static void _loadControlsFile(const char *path) {
if (vfsExists(path)) {
_progTrace("Loading %s", path);
if (_luaLoadFile(_global.luaContext, path, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, 0)) {
utilDie("%s", lua_tostring(_global.luaContext, -1));
}
}
}
static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length) {
SDL_Surface *surface = IMG_LoadTyped_IO(SDL_IOFromConstMem(data, length), true, "PNG");
if (surface == NULL) {
utilDie("%s", SDL_GetError());
}
_surfaceUnpack(&surface);
return surface;
}
// Loads an embedded PNG as a texture. The surface stays alive so callers can read its size.
static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface) {
SDL_Texture *texture = NULL;
*surface = _loadEmbeddedPng(data, length);
texture = SDL_CreateTextureFromSurface(_global.renderer, *surface);
if (texture == NULL) {
utilDie("%s", SDL_GetError());
}
return texture;
}
// SDL knows a great many pads already; a gamecontrollerdb.txt adds the rest. It is looked for in
// the four places controls.cfg is looked for and every one found is added in that order, so the
// machine's file wins over the game's, and a player's file in the game's data directory over both.
static void _loadGamepadDatabase(void) {
char *places[SETTINGS_PLACES];
char *data = NULL;
size_t bytes = 0;
int32_t added = 0;
int32_t c = 0;
_configPlaces(GAMEPAD_DATABASE_FILE, _global.conf->dataDir, _global.conf->scriptFile, places);
for (c = 0; c < SETTINGS_PLACES; c++) {
if ((places[c] != NULL) && vfsExists(places[c])) {
data = vfsRead(places[c], &bytes);
if (data != NULL) {
added = SDL_AddGamepadMappingsFromIO(SDL_IOFromConstMem(data, bytes), true);
if (added < 0) {
utilSay("Warning: %s could not be read: %s", places[c], SDL_GetError());
} else {
_progTrace("Added %d controller mappings from %s", added, places[c]);
}
free(data);
}
}
free(places[c]);
}
}
// The whole logical rectangle: the window, in the coordinates everything is drawn in.
// Everything the service tools keep for this machine, read in one pass: the audio delay and the
// picture geometry. A file missing a key leaves that setting alone, which is why each is looked
// for on its own rather than with one scan of the whole file.
//
// The geometry goes into the configuration rather than straight into the live values, because the
// live ones are set back to the configuration whenever a script starts or reloads; writing there
// makes "start from the command line" mean "start from the command line as this cabinet amends it".
// An option actually typed wins, which is what the GIVEN_ flags are for.
static void _loadMachineSettings(void) {
char *path = _machineFilePath();
size_t bytes = 0;
char *data = utilReadFile(path, &bytes);
const char *at = NULL;
int32_t delay = 0;
int32_t value = 0;
free(path);
_global.machineTier = TIER_UNMEASURED;
if (data != NULL) {
at = strstr(data, "tier = ");
if ((at != NULL) && (sscanf(at, "tier = %d", &value) == 1) && (value >= 0) && (value <= TIER_MAX)) {
_global.machineTier = value;
}
at = strstr(data, "mic = ");
if ((at != NULL) && (sscanf(at, "mic = %d", &value) == 1) && (value >= 0) && (value <= MIC_LATENCY_MAX)) {
micSetLatency(value);
}
at = strstr(data, "delay = ");
if ((at != NULL) && (sscanf(at, "delay = %d", &value) == 1) && (value >= -VIDEO_AUDIO_DELAY_MAX) && (value <= VIDEO_AUDIO_DELAY_MAX)) {
delay = value;
}
at = strstr(data, "scale = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_SCALE) && (sscanf(at, "scale = %d", &value) == 1) && (value >= SCALE_FACTOR_MIN) && (value <= SCALE_FACTOR_MAX)) {
_global.conf->scaleFactor = value;
}
at = strstr(data, "shiftx = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_SHIFT) && (sscanf(at, "shiftx = %d", &value) == 1) && (value >= SHIFT_MIN) && (value <= SHIFT_MAX)) {
_global.conf->shiftX = value;
}
at = strstr(data, "shifty = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_SHIFT) && (sscanf(at, "shifty = %d", &value) == 1) && (value >= SHIFT_MIN) && (value <= SHIFT_MAX)) {
_global.conf->shiftY = value;
}
at = strstr(data, "rotate = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_ROTATE) && (sscanf(at, "rotate = %d", &value) == 1) && ((value % ROTATE_STEP) == 0)) {
_global.conf->rotate = value;
}
free(data);
}
videoSetAudioCalibration(delay);
}
static void _logicalRect(SDL_FRect *rect) {
rect->x = 0.0f;
rect->y = 0.0f;
rect->w = (float)_global.canvasWidth;
rect->h = (float)_global.canvasHeight;
}
// Hands a hooked call to the original function kept as upvalue 1, returning all it returns.
static int32_t _luaCallOriginal(lua_State *L) {
lua_pushvalue(L, lua_upvalueindex(1));
lua_insert(L, 1);
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
return lua_gettop(L);
}
// Reports a script level error with the calling Lua line and exits.
static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) {
va_list args;
char *message = NULL;
va_start(args, fmt);
message = _luaFormat(L, method, fmt, args);
va_end(args);
if (_global.conf->scriptTracing) {
utilTrace("%s", message);
}
utilDie("%s", message);
}
// dofile(name) through the vfs; without a name the original reads stdin.
static int32_t _luaDofile(lua_State *L) {
const char *name = luaL_optstring(L, 1, NULL);
if (name == NULL) {
return _luaCallOriginal(L);
}
lua_settop(L, 1);
if (_luaLoadFile(L, name, NULL, _global.conf->reload) != LUA_OK) {
return lua_error(L);
}
lua_call(L, 0, LUA_MULTRET);
return lua_gettop(L) - 1;
}
static int32_t _luaFileClose(lua_State *L) {
_luaFileShut((LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE));
lua_pushboolean(L, 1);
return 1;
}
static int32_t _luaFileGc(lua_State *L) {
_luaFileShut((LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE));
return 0;
}
// One byte, or -1 at the end. The buffer is what keeps a line at a time from costing a database
// read a character.
static int32_t _luaFileGetc(LuaFileT *file) {
if (file->at >= file->have) {
file->have = vfsStreamRead(file->stream, file->buffer, LUA_VFS_BUFFER);
file->at = 0;
if (file->have <= 0) {
file->have = 0;
return -1;
}
}
file->pos++;
return file->buffer[file->at++];
}
// The iterator file:lines() and io.lines() hand back. Upvalues: the file, whether to close it at
// the end, and the formats.
static int32_t _luaFileIterate(lua_State *L) {
LuaFileT *file = (LuaFileT *)lua_touserdata(L, lua_upvalueindex(1));
bool closing = lua_toboolean(L, lua_upvalueindex(2));
int32_t formats = (int32_t)lua_tointeger(L, lua_upvalueindex(3));
int32_t i = 0;
if (file->closed) {
return luaL_error(L, "file is already closed");
}
// Whatever the generic for passed in is not wanted; the results build up from an empty stack.
lua_settop(L, 0);
if (formats == 0) {
if (_luaFileReadLine(L, file, false)) {
return 1;
}
} else {
// Each format is pushed, read from where it sits, and then taken out from under its value,
// so the values are left in order and nothing read earlier is lost to a later format.
for (i = 0; i < formats; i++) {
lua_pushvalue(L, lua_upvalueindex(4 + i));
if (!_luaFileReadFormat(L, file, lua_gettop(L))) {
lua_pop(L, 1);
break;
}
lua_remove(L, -2);
}
// As Lua does: only the first format coming up empty ends the loop. A later one hands
// back what was read before it, with nil in its place.
if (i == formats) {
return formats;
}
if (i > 0) {
lua_pushnil(L);
return i + 1;
}
}
if (closing) {
_luaFileShut(file);
}
lua_pushnil(L);
return 1;
}
static int32_t _luaFileLines(lua_State *L) {
_luaFileOpen(L, 1);
return _luaFileLinesFrom(L, 1, false);
}
// Builds that iterator over the file at index, with the formats that follow it.
static int32_t _luaFileLinesFrom(lua_State *L, int32_t index, bool closing) {
int32_t formats = lua_gettop(L) - index;
int32_t i = 0;
lua_pushvalue(L, index);
lua_pushboolean(L, closing);
lua_pushinteger(L, formats);
for (i = 0; i < formats; i++) {
lua_pushvalue(L, index + 1 + i);
}
lua_pushcclosure(L, _luaFileIterate, 3 + formats);
return 1;
}
// Buffering is ours to do and there is nothing to flush; both are here so the object answers
// everything Lua's does.
static int32_t _luaFileNoop(lua_State *L) {
_luaFileOpen(L, 1);
lua_pushvalue(L, 1);
return 1;
}
static LuaFileT *_luaFileOpen(lua_State *L, int32_t index) {
LuaFileT *file = (LuaFileT *)luaL_checkudata(L, index, LUA_VFS_FILE);
if (file->closed) {
luaL_error(L, "attempt to use a closed file");
}
return file;
}
// Opens name through the vfs and pushes the handle, or nil and a message the Lua way.
static bool _luaFilePush(lua_State *L, const char *name) {
VfsStreamT *stream = vfsStreamOpen(name);
LuaFileT *file = NULL;
if (stream == NULL) {
lua_pushnil(L);
lua_pushfstring(L, "%s: No such file or directory", name);
lua_pushinteger(L, ENOENT);
return false;
}
file = (LuaFileT *)lua_newuserdatauv(L, sizeof(LuaFileT), 0);
memset(file, 0, sizeof(*file));
file->stream = stream;
luaL_setmetatable(L, LUA_VFS_FILE);
return true;
}
// file:read(...) -- every format in turn, stopping at the first that finds nothing.
static int32_t _luaFileRead(lua_State *L) {
LuaFileT *file = _luaFileOpen(L, 1);
int32_t formats = lua_gettop(L) - 1;
int32_t i = 0;
if (formats == 0) {
if (!_luaFileReadLine(L, file, false)) {
lua_pushnil(L);
}
return 1;
}
for (i = 0; i < formats; i++) {
if (!_luaFileReadFormat(L, file, i + 2)) {
lua_pushnil(L);
return i + 1;
}
}
return formats;
}
// Reads until the end and pushes what there was. Always a string, empty at the end of the file,
// which is what Lua's "a" does. What the buffer already holds goes first, and the rest is pulled
// in blocks: a level file or a save is read whole, and a byte at a time is how that gets slow.
static void _luaFileReadAll(lua_State *L, LuaFileT *file) {
luaL_Buffer out;
int64_t got = 0;
luaL_buffinit(L, &out);
luaL_addlstring(&out, (const char *)file->buffer + file->at, (size_t)(file->have - file->at));
file->pos += file->have - file->at;
file->at = file->have;
while ((got = vfsStreamRead(file->stream, file->buffer, LUA_VFS_BUFFER)) > 0) {
luaL_addlstring(&out, (const char *)file->buffer, (size_t)got);
file->pos += got;
}
file->have = 0;
file->at = 0;
luaL_pushresult(&out);
}
// At most count bytes. False at the end of the file unless none were asked for, which Lua uses to
// test for it.
static bool _luaFileReadCount(lua_State *L, LuaFileT *file, int64_t count) {
luaL_Buffer out;
int64_t read = 0;
int32_t c = 0;
// read(0) is Lua's test for the end: an empty string while there is more, nil at the end.
if (count == 0) {
if (_luaFileGetc(file) < 0) {
return false;
}
_luaFileUngetc(file);
lua_pushliteral(L, "");
return true;
}
luaL_buffinit(L, &out);
while ((read < count) && ((c = _luaFileGetc(file)) >= 0)) {
luaL_addchar(&out, (char)c);
read++;
}
luaL_pushresult(&out);
if (read == 0) {
lua_pop(L, 1);
return false;
}
return true;
}
// One of read's formats. Answers false when there was nothing left to read it from.
static bool _luaFileReadFormat(lua_State *L, LuaFileT *file, int32_t index) {
const char *format = NULL;
if (lua_type(L, index) == LUA_TNUMBER) {
return _luaFileReadCount(L, file, (int64_t)luaL_checkinteger(L, index));
}
format = luaL_checkstring(L, index);
if (*format == '*') { // 5.2 wrote the formats with a star; 5.4 allows both.
format++;
}
switch (*format) {
case 'l':
return _luaFileReadLine(L, file, false);
case 'L':
return _luaFileReadLine(L, file, true);
case 'a':
_luaFileReadAll(L, file);
return true;
case 'n':
return _luaFileReadNumber(L, file);
default:
luaL_argerror(L, index, "invalid format");
}
return false;
}
// A line, with or without its ending. False at the end of the file, where Lua answers nil.
static bool _luaFileReadLine(lua_State *L, LuaFileT *file, bool keepEnding) {
luaL_Buffer out;
int32_t c = 0;
bool found = false;
luaL_buffinit(L, &out);
while ((c = _luaFileGetc(file)) >= 0) {
found = true;
if (c == '\n') {
if (keepEnding) {
luaL_addchar(&out, (char)c);
}
break;
}
luaL_addchar(&out, (char)c);
}
luaL_pushresult(&out);
if (!found) {
lua_pop(L, 1);
}
return found;
}
// A number, the way Lua reads one: whitespace, then as much as still looks like a numeral. What
// is accepted is whatever lua_stringtonumber accepts, so hexadecimal and exponents come along.
static bool _luaFileReadNumber(lua_State *L, LuaFileT *file) {
char text[64];
size_t length = 0;
int32_t c = 0;
do {
c = _luaFileGetc(file);
} while ((c >= 0) && isspace(c));
while ((c > 0) && (length < sizeof(text) - 1) && (isalnum(c) || (strchr("+-.", c) != NULL))) {
text[length++] = (char)c;
c = _luaFileGetc(file);
}
if (c >= 0) {
_luaFileUngetc(file);
}
text[length] = 0;
if ((length == 0) || (lua_stringtonumber(L, text) == 0)) {
return false;
}
return true;
}
// The metatable, once.
static void _luaFileRegister(lua_State *L) {
static const luaL_Reg methods[] = {
{ "close", _luaFileClose },
{ "flush", _luaFileNoop },
{ "lines", _luaFileLines },
{ "read", _luaFileRead },
{ "seek", _luaFileSeek },
{ "setvbuf", _luaFileNoop },
{ "write", _luaFileWrite },
{ NULL, NULL }
};
luaL_newmetatable(L, LUA_VFS_FILE);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, methods, 0);
lua_pushcfunction(L, _luaFileGc);
lua_setfield(L, -2, "__gc");
lua_pushcfunction(L, _luaFileClose);
lua_setfield(L, -2, "__close");
lua_pushcfunction(L, _luaFileToString);
lua_setfield(L, -2, "__tostring");
lua_pushliteral(L, "FILE*");
lua_setfield(L, -2, "__name");
lua_pop(L, 1);
}
// require() of a game's own module: name.lua, name/init.lua, or name.singe under the script's
// directory, then relative to the game root.
static int32_t _luaFileSearcher(lua_State *L) {
static const char *const patterns[] = { "%s%s.lua", "%s%s/init.lua", "%s%s.singe", NULL };
char *module = strdup(lua_tostring(L, 1));
char *scriptDir = utilGetUpToLastPathComponent(_global.conf->scriptFile);
const char *prefixes[2];
char *name = NULL;
char *p = NULL;
int32_t x = 0;
int32_t y = 0;
prefixes[0] = scriptDir;
prefixes[1] = "";
for (p = module; *p != 0; p++) {
if (*p == '.') {
*p = '/';
}
}
for (y = 0; y < 2; y++) {
for (x = 0; patterns[x] != NULL; x++) {
name = utilCreateString(patterns[x], prefixes[y], module);
if (vfsExists(name)) {
if (_luaLoadFile(L, name, NULL, _global.conf->reload) != LUA_OK) {
lua_pushfstring(L, "error loading module '%s' from file '%s':\n\t%s", lua_tostring(L, 1), name, lua_tostring(L, -1));
free(name);
free(module);
free(scriptDir);
return lua_error(L);
}
lua_pushstring(L, name);
free(name);
free(module);
free(scriptDir);
return 2;
}
free(name);
}
}
lua_pushfstring(L, "\n\tno file '%s%s.lua' in the game", scriptDir, module);
free(module);
free(scriptDir);
return 1;
}
// file:seek([whence [, offset]]) -- "set", "cur" or "end", as Lua's does.
static int32_t _luaFileSeek(lua_State *L) {
static const char *const names[] = { "set", "cur", "end", NULL };
LuaFileT *file = _luaFileOpen(L, 1);
int32_t whence = luaL_checkoption(L, 2, "cur", names);
int64_t offset = (int64_t)luaL_optinteger(L, 3, 0);
int64_t target = 0;
switch (whence) {
case 0:
target = offset;
break;
case 1:
target = file->pos + offset;
break;
default:
target = vfsStreamSize(file->stream) + offset;
break;
}
if (target < 0) {
lua_pushnil(L);
lua_pushliteral(L, "Invalid argument");
return 2;
}
if (vfsStreamSeek(file->stream, target, SEEK_SET) < 0) {
lua_pushnil(L);
lua_pushliteral(L, "seek failed");
return 2;
}
file->pos = target;
file->have = 0;
file->at = 0;
lua_pushinteger(L, (lua_Integer)target);
return 1;
}
// Lets the stream go. Safe to call twice: close, collection and the end of io.lines all reach it.
static void _luaFileShut(LuaFileT *file) {
if (!file->closed) {
vfsStreamClose(file->stream);
file->stream = NULL;
file->closed = true;
}
}
// The handle, if that is what is at index; NULL for anything else.
static LuaFileT *_luaFileTest(lua_State *L, int32_t index) {
return (LuaFileT *)luaL_testudata(L, index, LUA_VFS_FILE);
}
static int32_t _luaFileToString(lua_State *L) {
LuaFileT *file = (LuaFileT *)luaL_checkudata(L, 1, LUA_VFS_FILE);
lua_pushfstring(L, file->closed ? "file (closed)" : "file (%p)", (void *)file);
return 1;
}
static void _luaFileUngetc(LuaFileT *file) {
if (file->at > 0) {
file->at--;
file->pos--;
}
}
// Writing is not a thing these do: they are handed out for reading a packed asset, and a packed
// asset is read only. A game writing to its own directory gets a real file, as it always did.
static int32_t _luaFileWrite(lua_State *L) {
_luaFileOpen(L, 1);
return luaL_error(L, "file is read only: it lives inside a .game");
}
// Formats "line:method: message" for tracing and errors. Caller frees.
static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args) {
lua_Debug ar;
int32_t line = 0;
char *body = NULL;
char *message = NULL;
if (lua_getstack(L, 1, &ar) && lua_getinfo(L, "Sl", &ar)) {
line = ar.currentline;
}
body = utilCreateStringVArgs(fmt, args);
if (!body) {
utilDie("Unable to allocate trace string.");
}
message = utilCreateString("%d:%s: %s", line, method, body);
if (!message) {
utilDie("Unable to allocate trace string.");
}
free(body);
return message;
}
// io.close([file]) -- ours if that is what it was given, or what io.input was last set to.
static int32_t _luaIoClose(lua_State *L) {
if (lua_gettop(L) == 0) {
if (_luaIoInput(L) == NULL) {
return _luaCallOriginal(L);
}
lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
_luaIoForgetInput(L);
} else if (_luaFileTest(L, 1) == NULL) {
return _luaCallOriginal(L);
}
return _luaFileClose(L);
}
// Whatever io.input was last given, if it was one of ours. The stack is left as it was found.
static void _luaIoForgetInput(lua_State *L) {
lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
}
// io.open and friends with a name resolved through the vfs. Upvalues: the original, and how the name is used.
static int32_t _luaIoHook(lua_State *L) {
IoHookModeE mode = (IoHookModeE)lua_tointeger(L, lua_upvalueindex(2));
const char *modeString = NULL;
const char *name = NULL;
char *path = NULL;
bool writing = (mode == IO_HOOK_WRITE);
if (lua_type(L, 1) == LUA_TSTRING) {
name = lua_tostring(L, 1);
if (mode == IO_HOOK_OPEN) {
modeString = luaL_optstring(L, 2, "r");
writing = (strpbrk(modeString, "wa+") != NULL);
}
// A name that lives only inside the .game is read where it lies. Everything else -- a
// loose file, anything being written -- is a real file and goes to the original.
if (!writing && vfsIsPacked(name)) {
if (!_luaFilePush(L, name)) {
if (mode != IO_HOOK_OPEN) {
return luaL_error(L, "%s: No such file or directory", name);
}
return 3;
}
lua_replace(L, 1);
if (mode == IO_HOOK_LINES) {
return _luaFileLinesFrom(L, 1, true);
}
if (mode == IO_HOOK_INPUT) {
return _luaIoSetInput(L);
}
lua_settop(L, 1);
return 1;
}
if (mode == IO_HOOK_INPUT) {
// A real file takes over as the default input, so ours stops being it.
_luaIoForgetInput(L);
}
path = vfsFilePath(lua_tostring(L, 1), writing);
if (path == NULL) {
// A name inside a packed game that resolves nowhere: io.open reports it the Lua way, the others raise.
if (mode != IO_HOOK_OPEN) {
return luaL_error(L, "%s reaches outside the game", lua_tostring(L, 1));
}
lua_pushnil(L);
lua_pushfstring(L, "%s reaches outside the game", lua_tostring(L, 1));
return 2;
}
lua_pushstring(L, path);
lua_replace(L, 1);
free(path);
} else if (mode == IO_HOOK_LINES) {
// io.lines() with no name reads the default input, which may be one of ours.
if (_luaIoInput(L) != NULL) {
lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
lua_insert(L, 1);
return _luaFileLinesFrom(L, 1, false);
}
} else if (mode == IO_HOOK_INPUT) {
if (_luaFileTest(L, 1) != NULL) {
return _luaIoSetInput(L);
}
if (lua_isnoneornil(L, 1) && (_luaIoInput(L) != NULL)) {
lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
return 1;
}
_luaIoForgetInput(L);
}
return _luaCallOriginal(L);
}
static LuaFileT *_luaIoInput(lua_State *L) {
LuaFileT *file = NULL;
if (lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT) != LUA_TNIL) {
file = _luaFileTest(L, -1);
}
lua_pop(L, 1);
return file;
}
// io.read(...) -- from ours when io.input was given one, otherwise from Lua's own default input.
static int32_t _luaIoRead(lua_State *L) {
if (_luaIoInput(L) == NULL) {
return _luaCallOriginal(L);
}
lua_getfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
lua_insert(L, 1);
return _luaFileRead(L);
}
// The handle at 1 becomes the default input, and answers as io.input's result. Lua keeps its own
// default in the registry and its own functions would take ours for a FILE *, so ours is kept
// beside it and the three functions that read the default look here first.
static int32_t _luaIoSetInput(lua_State *L) {
lua_settop(L, 1);
lua_pushvalue(L, 1);
lua_setfield(L, LUA_REGISTRYINDEX, LUA_VFS_INPUT);
return 1;
}
// io.type(x) -- ours answer as files, because to a script that is what they are.
static int32_t _luaIoType(lua_State *L) {
LuaFileT *file = _luaFileTest(L, 1);
if (file == NULL) {
return _luaCallOriginal(L);
}
lua_pushstring(L, file->closed ? "closed file" : "file");
return 1;
}
// loadfile(name, mode, env) through the vfs; without a name the original reads stdin.
static int32_t _luaLoadfile(lua_State *L) {
const char *name = luaL_optstring(L, 1, NULL);
const char *mode = luaL_optstring(L, 2, NULL);
if (name == NULL) {
return _luaCallOriginal(L);
}
if (_luaLoadFile(L, name, mode, _global.conf->reload) != LUA_OK) {
lua_pushnil(L);
lua_insert(L, -2);
return 2;
}
if (!lua_isnone(L, 3)) {
lua_pushvalue(L, 3);
if (lua_setupvalue(L, -2, 1) == NULL) {
lua_pop(L, 1);
}
}
return 1;
}
// Loads a chunk from the vfs, leaving the function or an error message on the stack. Returns the
// Lua status. With watch, a loose file joins the list --reload checks.
static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bool watch) {
char *data = NULL;
char *chunkName = NULL;
size_t bytes = 0;
int32_t status = LUA_ERRFILE;
data = vfsRead(name, &bytes);
if (data == NULL) {
lua_pushfstring(L, "cannot open %s", name);
return status;
}
// Watched from here so a syntax error can be fixed and picked up.
if (watch) {
_watchFile(name);
}
chunkName = utilCreateString("@%s", name);
status = luaL_loadbufferx(L, data, bytes, chunkName, mode);
free(chunkName);
free(data);
return status;
}
// require("lfs") with dir and attributes routed through the vfs, so a packed game can list itself.
static int32_t _luaopenLfs(lua_State *L) {
luaopen_lfs(L);
lua_getfield(L, -1, "dir");
lua_pushcclosure(L, _lfsDir, 1);
lua_setfield(L, -2, "dir");
lua_getfield(L, -1, "attributes");
lua_pushcclosure(L, _lfsAttributes, 1);
lua_setfield(L, -2, "attributes");
lua_getfield(L, -1, "symlinkattributes");
lua_pushcclosure(L, _lfsAttributes, 1);
lua_setfield(L, -2, "symlinkattributes");
lua_getfield(L, -1, "mkdir");
lua_pushcclosure(L, _lfsMkdir, 1);
lua_setfield(L, -2, "mkdir");
lua_getfield(L, -1, "rmdir");
lua_pushcclosure(L, _lfsRmdir, 1);
lua_setfield(L, -2, "rmdir");
return 1;
}
// require("sqlite3") with open resolving names the way io.open does, so a database packed in the game
// opens (see _luaSqliteOpen).
static int32_t _luaopenSqlite3(lua_State *L) {
luaopen_lsqlite3(L);
lua_getfield(L, -1, "open");
lua_pushcclosure(L, _luaSqliteOpen, 1);
lua_setfield(L, -2, "open");
return 1;
}
// Lua panic handler: something went wrong outside a protected call.
static int32_t _luaPanic(lua_State *L) {
lua_Debug ar;
int32_t level = 0;
utilSay("Singe has panicked! Very bad!");
utilSay("Error: %s", lua_tostring(L, -1));
utilSay("Stack trace:");
while (lua_getstack(L, level, &ar) != 0) {
lua_getinfo(L, "nSl", &ar);
utilSay(" %d: function `%s' at line %d %s", level, ar.name ? ar.name : "?", ar.currentline, ar.short_src);
level++;
}
utilSay("Trace complete.");
return 0;
}
// package.searchers entry serving the embedded Lua modules.
// https://leiradel.github.io/2020/03/01/Embedding-Lua-Modules.html
static int32_t _luaSearcher(lua_State *L) {
const char *modname = lua_tostring(L, 1);
size_t i = 0;
for (i = 0; i < sizeof(_luaModules) / sizeof(_luaModules[0]); i++) {
if (strcmp(modname, _luaModules[i].name) == 0) {
if (_luaModules[i].length != 0) {
// It's a Lua module, return the chunk that defines the module.
if (luaL_loadbufferx(L, _luaModules[i].source, _luaModules[i].length, modname, "t") != LUA_OK) {
return lua_error(L);
}
} else {
// It's a native module, return the native function that defines the module.
lua_pushcfunction(L, _luaModules[i].openf);
}
return 1;
}
}
// A searcher explains itself with a string when it has nothing.
lua_pushfstring(L, "\n\tno embedded module '%s'", modname);
return 1;
}
// sqlite3.open with the name resolved through the vfs. A database packed inside the game is opened
// where it lies, read only, when that is what the script asked for; asked to write, it is copied
// once to the game's data directory -- where io.open sends a write to a packed name -- and the copy
// is opened, then and from then on. Any other name resolves as io.open's would. SQLite's own
// special names (":memory:", "", a "file:" URI) pass through untouched. Upvalue: the original.
static int32_t _luaSqliteOpen(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
lua_Integer flags = luaL_optinteger(L, 2, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
bool writing = ((flags & (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)) != 0);
char *path = NULL;
int32_t base = 0;
if ((name[0] != 0) && (strcmp(name, ":memory:") != 0) && (strncmp(name, "file:", 5) != 0)) {
if (vfsIsPacked(name) && !writing) {
path = vfsSqliteUri(name);
flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_URI;
} else {
path = vfsFilePath(name, writing);
if (path == NULL) {
lua_pushnil(L);
lua_pushinteger(L, SQLITE_CANTOPEN);
lua_pushfstring(L, "%s reaches outside the game", name);
return 3;
}
if (writing && vfsIsPacked(name) && !vfsExtract(name, path)) {
lua_pushnil(L);
lua_pushinteger(L, SQLITE_CANTOPEN);
lua_pushfstring(L, "%s could not be copied out of the game to %s", name, path);
free(path);
return 3;
}
}
}
// The arguments stay on the stack below the call, so name stays alive until it is done with.
base = lua_gettop(L);
lua_pushvalue(L, lua_upvalueindex(1));
lua_pushstring(L, (path != NULL) ? path : name);
lua_pushinteger(L, flags);
free(path);
lua_call(L, 2, LUA_MULTRET);
return lua_gettop(L) - base;
}
static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) {
va_list args;
char *message = NULL;
if (_global.conf->scriptTracing) {
va_start(args, fmt);
message = _luaFormat(L, method, fmt, args);
va_end(args);
utilTrace("%s", message);
free(message);
}
}
// Message handler for lua_pcall: appends a traceback to the error.
static int32_t _luaTraceback(lua_State *L) {
const char *message = lua_tostring(L, 1);
if (message == NULL) {
message = "(error object is not a string)";
}
luaL_traceback(L, L, message, 1);
return 1;
}
// A pointer position in presentation coordinates to the overlay coordinates the game works in:
// the rotation comes off first, then the position maps into whatever the shift, the scale factor
// and the Sinden border left of the picture.
// The machine file lives beside the data directories, since what it holds belongs to the cabinet
// rather than to any game.
static char *_machineFilePath(void) {
return utilCreateString("%s%s", _global.conf->dataDirBase, MACHINE_FILE);
}
static void _mapPointer(float px, float py, int32_t *x, int32_t *y) {
float vx = 0;
float vy = 0;
_unrotatePoint(px, py, &vx, &vy);
*x = (int32_t)((vx - _global.videoRect.x) * _global.drawScaleX * _global.overlayScaleX);
*y = (int32_t)((vy - _global.videoRect.y) * _global.drawScaleY * _global.overlayScaleY);
}
// The same for a relative movement, which turns with the picture but does not translate.
static void _mapPointerDelta(float dx, float dy, int32_t *xr, int32_t *yr) {
float vx = 0;
float vy = 0;
_unrotateDelta(dx, dy, &vx, &vy);
*xr = (int32_t)(vx * _global.drawScaleX * _global.overlayScaleX);
*yr = (int32_t)(vy * _global.drawScaleY * _global.overlayScaleY);
}
// Every joystick SDL has no gamepad mapping for gets one written for it, so an arcade encoder, a
// spinner, a wheel or a flight stick reaches a game through GAMEPAD_0 to GAMEPAD_3 like anything
// else, and nothing else in the engine, in controls.cfg or in a script has to know the difference.
// The order of precedence is SDL's own database first, then whatever a gamecontrollerdb.txt
// supplied, and only then this: the synthetic mapping is the last resort, and it is how a device
// nobody has ever written a mapping for still works.
static void _mapUnknownJoysticks(void) {
char mapping[GAMEPAD_MAPPING_MAX];
int32_t count = 0;
int32_t x = 0;
SDL_JoystickID *ids = SDL_GetJoysticks(&count);
for (x = 0; (ids != NULL) && (x < count); x++) {
if (SDL_IsGamepad(ids[x]) || !_gamepadMappingFor(ids[x], mapping, sizeof(mapping))) {
continue;
}
if (SDL_AddGamepadMapping(mapping) < 0) {
utilSay("Warning: no mapping could be made for %s: %s", SDL_GetJoystickNameForID(ids[x]), SDL_GetError());
} else {
_progTrace("No gamepad mapping for \"%s\"; using %s", SDL_GetJoystickNameForID(ids[x]), mapping);
}
}
SDL_free(ids);
}
// materialSetXxxMap(material[, image[, strength]]): a loaded sprite's surface or a KTX2 file as one
// of the material's maps; nil (or nothing) clears it. Strength is only for the normal and
// occlusion maps.
static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength) {
int32_t material = 0;
SDL_Surface *surface = NULL;
Ktx2ImageT ktx2;
bool ok = false;
float strength = 1.0f;
_argCheck(L, method, 1, hasStrength ? 3 : 2);
material = _argMaterial(L, method, 1);
if (hasStrength && (lua_gettop(L) >= 3)) {
strength = (float)_argNumber(L, method, 3);
}
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2) && _argMapImage(L, method, 2, &surface, &ktx2)) {
ok = materialSetMap(material, map, &ktx2, strength);
ktx2Free(&ktx2);
if (!ok) {
_luaDie(L, method, "Unable to upload the texture.");
}
return 0;
}
if (!materialSetMapSurface(material, map, surface, strength)) {
_luaDie(L, method, "%s", SDL_GetError());
}
return 0;
}
// A MIDI channel argument, which a person counts from 1 and the wire counts from 0.
static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index) {
int32_t channel = _argInteger(L, method, index) - 1;
if ((channel < 0) || (channel > MIDI_CHANNEL_MAX)) {
_luaDie(L, method, "A MIDI channel is 1 to %d, not %d.", MIDI_CHANNEL_MAX + 1, channel + 1);
}
return (uint8_t)channel;
}
// A seven bit MIDI data argument: a key, a velocity, a controller or its value.
static uint8_t _midiData(lua_State *L, const char *method, int32_t index) {
int32_t value = _argInteger(L, method, index);
if ((value < 0) || (value > MIDI_DATA_MAX)) {
_luaDie(L, method, "A MIDI value is 0 to %d, not %d.", MIDI_DATA_MAX, value);
}
return (uint8_t)value;
}
// onMidiMessage(status, data1, data2, bytes) for one message that arrived on the input port. The
// three numbers are the usual message; the string is the whole thing, which is what a system
// exclusive message needs.
static void _midiReceived(void *context, const uint8_t *bytes, size_t size) {
(void)context;
_callLua("onMidiMessage", "iiiS", (int32_t)bytes[0], (size > 1) ? (int32_t)bytes[1] : 0, (size > 2) ? (int32_t)bytes[2] : 0, (const char *)bytes, size);
}
// Converts a script visible volume to the mixer's 0 to 1 gain. The two families keep different
// scales: sound effects run to AUDIO_MAX_VOLUME and music to MUSIC_MAX_VOLUME, which is Hypseus's.
static float _mixerGain(int32_t volume, int32_t maximum) {
return (float)volume / (float)maximum;
}
// Input code for a mouse button (0 = left, 1 = right, 2 = middle, ...) or wheel offset.
static int32_t _mouseCode(int32_t device, int32_t button) {
return CODE_MOUSE_BASE + device * CODE_MOUSE_STRIDE + button;
}
// Stops a music track, frees it and forgets it.
static void _musicDestroy(MusicT *music) {
HASH_DEL(_global.musicList, music);
MIX_StopTrack(music->track, 0);
MIX_SetTrackAudio(music->track, NULL);
MIX_DestroyTrack(music->track);
MIX_DestroyAudio(music->audio);
free(music);
}
// onNavArrived(agent) for every agent that reached its target this frame.
static void _navCallbacks(void) {
int32_t agents[NAV_ARRIVAL_QUEUE];
int32_t count = navPollArrived(agents, (int32_t)SDL_arraysize(agents));
int32_t x = 0;
for (x = 0; x < count; x++) {
_callLua("onNavArrived", "i", agents[x]);
}
}
// Any input of any kind: it restarts --idleexit's clock and lifts a --startsilent mute.
static void _noteInput(void) {
_global.idleClock = utilTicks();
// Only --startsilent leaves the mixer muted with sound wanted, so nothing else can be lifted here.
if (_global.audioMuted && !_global.conf->noSound) {
_setAudioMuted(false);
}
}
// Applies setOverlayOpacity's level to the overlay texture. Called again whenever the texture is made.
static void _overlayApplyOpacity(void) {
int32_t x = 0;
for (x = 0; x < OVERLAY_TEXTURES; x++) {
SDL_SetTextureAlphaMod(_global.overlayTextures[x], _global.overlayOpacity);
}
}
// Applies setOverlayLinearScale's choice to the overlay texture. Called again whenever the texture is made.
static void _overlayApplyScaleMode(void) {
int32_t x = 0;
for (x = 0; x < OVERLAY_TEXTURES; x++) {
SDL_SetTextureScaleMode(_global.overlayTextures[x], _global.overlayLinearScale ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST);
}
}
// The rectangle spanning two corners, either way round, grown by margin on every side: where a
// shape between them can have drawn.
static SDL_Rect _overlayArea(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t margin) {
SDL_Rect area;
area.x = SDL_min(x1, x2) - margin;
area.y = SDL_min(y1, y2) - margin;
area.w = abs(x2 - x1) + 1 + margin * 2;
area.h = abs(y2 - y1) + 1 + margin * 2;
return area;
}
// A colour as the overlay surface stores it.
static uint32_t _overlayColor(const SDL_Color *color) {
return SDL_MapRGBA(SDL_GetPixelFormatDetails(_global.overlay->format), NULL, color->r, color->g, color->b, color->a);
}
// The overlay in luma only (BT.601 weights, alpha as it is) in a scratch surface its size, for
// overlaySetMonochrome. The overlay's own pixels stay in colour.
static SDL_Surface *_overlayGrey(void) {
const uint8_t *in = NULL;
uint8_t *out = NULL;
int32_t x = 0;
int32_t y = 0;
uint8_t grey = 0;
if (_global.overlayGrey == NULL) {
_global.overlayGrey = SDL_CreateSurface(_global.overlayBase->w, _global.overlayBase->h, _global.overlayBase->format);
if (_global.overlayGrey == NULL) {
utilDie("%s", SDL_GetError());
}
}
for (y = 0; y < _global.overlayBase->h; y++) {
in = (const uint8_t *)_global.overlayBase->pixels + (size_t)y * (size_t)_global.overlayBase->pitch;
out = (uint8_t *)_global.overlayGrey->pixels + (size_t)y * (size_t)_global.overlayGrey->pitch;
for (x = 0; x < _global.overlayBase->w; x++) {
grey = (uint8_t)((LUMA_WEIGHT_RED * in[BGRA_RED] + LUMA_WEIGHT_GREEN * in[BGRA_GREEN] + LUMA_WEIGHT_BLUE * in[BGRA_BLUE]) >> LUMA_WEIGHT_SHIFT);
out[BGRA_BLUE] = grey;
out[BGRA_GREEN] = grey;
out[BGRA_RED] = grey;
out[BGRA_ALPHA] = in[BGRA_ALPHA];
in += COLOR_COMPONENTS;
out += COLOR_COMPONENTS;
}
}
return _global.overlayGrey;
}
// Draws one layer over its GUI, uploading it first when something was drawn on it since.
static void _overlayLayerDraw(int32_t index, const SDL_FRect *target) {
OverlayLayerT *layer = &_global.overlayLayers[index];
if (layer->dirty) {
SDL_UpdateTexture(layer->texture, NULL, layer->surface->pixels, layer->surface->pitch);
layer->dirty = false;
}
SDL_RenderTexture(_global.renderer, layer->texture, NULL, target);
}
// Opens the next layer: clear glass the size of the overlay, made the first time it is needed,
// which overlay drawing lands on from now until the script's next turn. Answers its index, or
// OVERLAY_NO_LAYER when every layer is in use, in which case drawing stays where it was.
static int32_t _overlayLayerOpen(void) {
OverlayLayerT *layer = NULL;
int32_t index = _global.overlayLayerCount;
if (index >= GUI_DRAW_MAX) {
return OVERLAY_NO_LAYER;
}
layer = &_global.overlayLayers[index];
if (layer->surface == NULL) {
layer->surface = SDL_CreateSurface(_global.overlayBase->w, _global.overlayBase->h, SDL_PIXELFORMAT_BGRA32);
layer->texture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, _global.overlayBase->w, _global.overlayBase->h);
if ((layer->surface == NULL) || (layer->texture == NULL)) {
utilDie("%s", SDL_GetError());
}
SDL_SetSurfaceBlendMode(layer->surface, SDL_BLENDMODE_BLEND);
SDL_SetTextureBlendMode(layer->texture, SDL_BLENDMODE_BLEND);
}
SDL_FillSurfaceRect(layer->surface, NULL, 0);
layer->dirty = true;
_global.overlay = layer->surface;
_global.overlayLayerCount = index + 1;
return index;
}
// Drops every layer: the overlay changed size, or the engine is closing.
static void _overlayLayersFree(void) {
int32_t x = 0;
for (x = 0; x < GUI_DRAW_MAX; x++) {
SDL_DestroySurface(_global.overlayLayers[x].surface);
SDL_DestroyTexture(_global.overlayLayers[x].texture);
_global.overlayLayers[x].surface = NULL;
_global.overlayLayers[x].texture = NULL;
}
_global.overlayLayerCount = 0;
}
// Replaces the overlay surface and texture (and the scene's targets) at a new size; its contents are lost.
static void _overlayResize(int32_t width, int32_t height) {
int32_t x = 0;
SDL_DestroySurface(_global.overlayGrey);
_global.overlayGrey = NULL;
_overlayLayersFree();
SDL_DestroySurface(_global.overlayBase);
SDL_DestroySurface(_global.blitScratch);
_global.overlayBase = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_BGRA32);
_global.blitScratch = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_BGRA32);
if ((_global.overlayBase == NULL) || (_global.blitScratch == NULL)) {
utilDie("%s", SDL_GetError());
}
_global.overlay = _global.overlayBase;
_global.overlayVideoCount = 0;
SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND);
_overlayTexturesDestroy();
for (x = 0; x < OVERLAY_TEXTURES; x++) {
_global.overlayTextures[x] = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, width, height);
if (_global.overlayTextures[x] == NULL) {
utilDie("%s", SDL_GetError());
}
SDL_SetTextureBlendMode(_global.overlayTextures[x], SDL_BLENDMODE_BLEND);
}
_global.overlayTextureIndex = 0;
_global.overlayTexture = _global.overlayTextures[0];
sceneResize(width, height);
_overlayApplyScaleMode();
_overlayApplyOpacity();
_global.overlayScaleX = (double)width / (double)_global.canvasWidth;
_global.overlayScaleY = (double)height / (double)_global.canvasHeight;
_overlayTouched(NULL);
}
// The overlay's textures, all of them, with the current one.
static void _overlayTexturesDestroy(void) {
int32_t x = 0;
for (x = 0; x < OVERLAY_TEXTURES; x++) {
SDL_DestroyTexture(_global.overlayTextures[x]);
_global.overlayTextures[x] = NULL;
}
_global.overlayTexture = NULL;
}
// Every overlay drawing call ends here so a texture is only re-uploaded when needed: the base's,
// or the open layer's. area is where it drew, so overlayClear refills and the upload sends only
// that: NULL for anywhere, and an empty one for a change that drew nothing but is to be shown.
static void _overlayTouched(const SDL_Rect *area) {
SDL_Rect bounds = { 0, 0, _global.overlayBase->w, _global.overlayBase->h };
SDL_Rect clipped;
if (_global.overlay != _global.overlayBase) {
_global.overlayLayers[_global.overlayLayerCount - 1].dirty = true;
return;
}
_global.overlayDirty = true;
if (area == NULL) {
_regionAdd(&_global.overlayPainted, NULL);
_regionAdd(&_global.overlayChanged, NULL);
return;
}
if (SDL_GetRectIntersection(area, &bounds, &clipped)) {
_regionAdd(&_global.overlayPainted, &clipped);
_regionAdd(&_global.overlayChanged, &clipped);
}
}
// Sends a region of the overlay to one of its textures.
static void _overlayUpload(SDL_Texture *texture, SDL_Surface *surface, const RegionT *region) {
const SDL_Rect *area = NULL;
int32_t x = 0;
if (region->all) {
SDL_UpdateTexture(texture, NULL, surface->pixels, surface->pitch);
return;
}
for (x = 0; x < region->count; x++) {
area = &region->rect[x];
SDL_UpdateTexture(texture, area, (const uint8_t *)surface->pixels + (size_t)area->y * (size_t)surface->pitch + (size_t)area->x * sizeof(uint32_t), surface->pitch);
}
}
// The videos under the base overlay, in the order they were drawn, scaled and faded as the overlay is.
static void _overlayVideosDraw(const SDL_FRect *target) {
float scaleX = target->w / (float)_global.overlayBase->w;
float scaleY = target->h / (float)_global.overlayBase->h;
OverlayVideoT *video = NULL;
SDL_FRect dest;
int32_t x = 0;
for (x = 0; x < _global.overlayVideoCount; x++) {
video = &_global.overlayVideos[x];
dest.x = target->x + (float)video->dest.x * scaleX;
dest.y = target->y + (float)video->dest.y * scaleY;
dest.w = (float)video->dest.w * scaleX;
dest.h = (float)video->dest.h * scaleY;
SDL_SetTextureScaleMode(video->texture, _global.overlayLinearScale ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST);
SDL_SetTextureAlphaMod(video->texture, _global.overlayOpacity);
SDL_RenderTexture(_global.renderer, video->texture, NULL, &dest);
// The scene may show the same texture as a material, at full strength.
SDL_SetTextureAlphaMod(video->texture, COLOR_BYTE_MAX);
}
}
// Takes a videoDraw for the GPU when that looks the same as the CPU copying the frame in: onto the
// base overlay, where nothing has been drawn since a clear to a see-through colour, in full colour.
// Anything drawn afterwards goes over it, as it would over the copied frame. False leaves the
// draw to the CPU.
static bool _overlayVideoUnder(SDL_Texture *texture, const SDL_Rect *dest) {
SDL_Rect bounds = { 0, 0, _global.overlayBase->w, _global.overlayBase->h };
SDL_Rect area;
int32_t x = 0;
if ((texture == NULL) || (_global.overlay != _global.overlayBase) || _global.overlayPainted.all || (_global.overlayClearAlpha != 0) || _global.overlayMonochrome || (_global.overlayVideoCount >= OVERLAY_VIDEOS_MAX)) {
return false;
}
if (!SDL_GetRectIntersection(dest, &bounds, &area)) {
return false;
}
for (x = 0; x < _global.overlayPainted.count; x++) {
if (SDL_HasRectIntersection(&area, &_global.overlayPainted.rect[x])) {
return false;
}
}
_global.overlayVideos[_global.overlayVideoCount].texture = texture;
_global.overlayVideos[_global.overlayVideoCount].dest = *dest;
_global.overlayVideoCount++;
_global.refreshDisplay = true;
return true;
}
// Frees the renderer textures kept for an emitter's 2D drawing.
static void _particleTexturesDestroy(ParticleTexturesT *cache) {
int32_t f = 0;
HASH_DEL(_global.particleTextures, cache);
for (f = 0; f < cache->count; f++) {
SDL_DestroyTexture(cache->textures[f]);
}
SDL_free(cache->textures);
SDL_free(cache);
}
static void _particleTexturesFree(int32_t emitter) {
ParticleTexturesT *cache = NULL;
HASH_FIND_INT(_global.particleTextures, &emitter, cache);
if (cache != NULL) {
_particleTexturesDestroy(cache);
}
}
// Every emitter's textures; the emitters themselves go with particlesQuit.
static void _particleTexturesFreeAll(void) {
ParticleTexturesT *cache = NULL;
ParticleTexturesT *temp = NULL;
HASH_ITER(hh, _global.particleTextures, cache, temp) {
_particleTexturesDestroy(cache);
}
}
// The textures for an emitter's frames, made on first use and remade when the frames change.
static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view) {
ParticleTexturesT *cache = NULL;
int32_t f = 0;
HASH_FIND_INT(_global.particleTextures, &view->id, cache);
if ((cache != NULL) && (cache->version != view->textureVersion)) {
_particleTexturesDestroy(cache);
cache = NULL;
}
if (cache != NULL) {
return cache;
}
cache = SDL_calloc(1, sizeof(ParticleTexturesT));
if (cache == NULL) {
utilDie("Out of memory for particle textures.");
}
cache->id = view->id;
cache->version = view->textureVersion;
cache->count = view->frameCount;
cache->blend = view->blend;
cache->textures = SDL_calloc((size_t)view->frameCount, sizeof(SDL_Texture *));
if (cache->textures == NULL) {
utilDie("Out of memory for particle textures.");
}
for (f = 0; f < view->frameCount; f++) {
cache->textures[f] = SDL_CreateTextureFromSurface(_global.renderer, view->frames[f]);
if (cache->textures[f] == NULL) {
utilDie("%s", SDL_GetError());
}
SDL_SetTextureScaleMode(cache->textures[f], SDL_SCALEMODE_LINEAR);
SDL_SetTextureBlendMode(cache->textures[f], (view->blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND);
}
HASH_ADD_INT(_global.particleTextures, id, cache);
return cache;
}
static void _pauseAllVideos(bool pause) {
VideoT *video = NULL;
VideoT *temp = NULL;
HASH_ITER(hh, _global.videoList, video, temp) {
if (pause) {
if (videoIsPlaying(video->handle)) {
video->wasPlayingBeforePause = true;
videoPause(video->handle);
}
} else {
if (video->wasPlayingBeforePause) {
video->wasPlayingBeforePause = false;
videoPlay(video->handle);
}
}
}
}
// Stands in for an engine call while -p traces, timing the real one. Its upvalues are the call and
// its slot in _perfCalls. A call that raises a Lua error leaves without being counted, which only
// ever loses the call that failed.
static int _perfCall(lua_State *L) {
lua_CFunction real = lua_tocfunction(L, lua_upvalueindex(1));
int32_t slot = (int32_t)lua_tointeger(L, lua_upvalueindex(2));
uint64_t startNS = SDL_GetTicksNS();
int results = real(L);
uint64_t spentNS = SDL_GetTicksNS() - startNS;
_perfCalls.call[slot].ns += spentNS;
_perfCalls.call[slot].calls++;
_perfCalls.ns += spentNS;
if (_perfCalls.inScript) {
_perfCalls.scriptNS += spentNS;
}
return results;
}
// Costliest first.
static int _perfCompare(const void *a, const void *b) {
const PerfCallT *left = (const PerfCallT *)a;
const PerfCallT *right = (const PerfCallT *)b;
return (left->ns < right->ns) - (left->ns > right->ns);
}
// --perfsync: the GPU finishes what this stage gave it before the stage's time is taken.
static void _perfSync(void) {
if (_global.conf->perfSync) {
renderFinish(_global.device);
}
}
// One loop's worth into -p's account, and the account traced and restarted once PERF_TRACE_NS has
// passed. The stage timings were added in as the loop went.
static void _perfTrace(uint64_t loopNS) {
uint64_t now = SDL_GetTicksNS();
double perLoop = 0.0;
_perf.loops++;
_perf.totalNS += loopNS;
if (loopNS > _perf.worstNS) {
_perf.worstNS = loopNS;
}
if ((now - _perf.windowStartNS) < PERF_TRACE_NS) {
return;
}
perLoop = (double)SDL_NS_PER_MS * (double)_perf.loops;
_progTrace("Perf: %.1f frames/s shown, %" PRIu64 " loops; per loop %.1f ms (worst %.1f): disc %.1f, script %.1f, scene %.1f, gui %.1f, overlay %.1f, compose %.1f, present %.1f, other %.1f%s", (double)_perf.presents * (double)SDL_NS_PER_SECOND / (double)(now - _perf.windowStartNS), _perf.loops, (double)_perf.totalNS / perLoop, (double)_perf.worstNS / (double)SDL_NS_PER_MS, (double)_perf.discNS / perLoop, (double)_perf.scriptNS / perLoop, (double)_perf.sceneNS / perLoop, (double)_perf.guiNS / perLoop, (double)_perf.overlayNS / perLoop, (double)_perf.composeNS / perLoop, (double)_perf.presentNS / perLoop, (double)(_perf.totalNS - _perf.discNS - _perf.scriptNS - _perf.sceneNS - _perf.guiNS - _perf.overlayNS - _perf.composeNS - _perf.presentNS) / perLoop, _global.conf->perfSync ? " (GPU time included)" : "");
if (_perfCalls.count > 0) {
PerfCallT *sorted = malloc(sizeof(PerfCallT) * (size_t)_perfCalls.count);
char line[PERF_LINE_BYTES];
size_t used = 0;
int32_t x = 0;
if (sorted != NULL) {
memcpy(sorted, _perfCalls.call, sizeof(PerfCallT) * (size_t)_perfCalls.count);
qsort(sorted, (size_t)_perfCalls.count, sizeof(PerfCallT), _perfCompare);
used = (size_t)snprintf(line, sizeof(line), "Perf script: per loop %.1f ms of Lua and %.1f in engine calls it made; %.1f in engine calls altogether:", (double)(_perf.scriptNS - SDL_min(_perfCalls.scriptNS, _perf.scriptNS)) / perLoop, (double)_perfCalls.scriptNS / perLoop, (double)_perfCalls.ns / perLoop);
for (x = 0; (x < PERF_TOP_CALLS) && (x < _perfCalls.count) && (sorted[x].calls > 0) && (used < sizeof(line)); x++) {
used += (size_t)snprintf(line + used, sizeof(line) - used, "%s %s %.2f (%.1f calls)", (x == 0) ? "" : ",", sorted[x].name, (double)sorted[x].ns / perLoop, (double)sorted[x].calls / (double)_perf.loops);
}
_progTrace("%s", line);
free(sorted);
}
for (x = 0; x < _perfCalls.count; x++) {
_perfCalls.call[x].ns = 0;
_perfCalls.call[x].calls = 0;
}
_perfCalls.ns = 0;
_perfCalls.scriptNS = 0;
}
_perf.windowStartNS = now;
_perf.loops = 0;
_perf.presents = 0;
_perf.discNS = 0;
_perf.scriptNS = 0;
_perf.sceneNS = 0;
_perf.guiNS = 0;
_perf.overlayNS = 0;
_perf.composeNS = 0;
_perf.presentNS = 0;
_perf.totalNS = 0;
_perf.worstNS = 0;
}
// Wraps every engine call _registerApi just added -- the globals that are C functions without
// upvalues and are not in the snapshot of the globals from before, which sits on the top of the
// stack -- in _perfCall. Lua's own libraries were there before, so pcall and its kind are left
// alone: timing them would bill all the Lua they run to the engine.
static void _perfWrapApi(lua_State *L) {
int32_t snapshot = lua_gettop(L);
PerfCallT *grown = NULL;
int32_t x = 0;
bool bare = false;
for (x = 0; x < _perfCalls.count; x++) {
free(_perfCalls.call[x].name);
}
free(_perfCalls.call);
memset(&_perfCalls, 0, sizeof(_perfCalls));
lua_pushglobaltable(L);
lua_pushnil(L);
while (lua_next(L, -2)) {
// lua_getupvalue pushes the upvalue when there is one, so a closure's is popped straight away.
bare = false;
if (lua_iscfunction(L, -1)) {
if (lua_getupvalue(L, -1, 1) == NULL) {
bare = true;
} else {
lua_pop(L, 1);
}
}
if (bare && (lua_type(L, -2) == LUA_TSTRING)) {
lua_pushvalue(L, -2);
lua_rawget(L, snapshot);
if (lua_isnil(L, -1)) {
grown = realloc(_perfCalls.call, sizeof(PerfCallT) * (size_t)(_perfCalls.count + 1));
if (grown != NULL) {
_perfCalls.call = grown;
memset(&_perfCalls.call[_perfCalls.count], 0, sizeof(PerfCallT));
_perfCalls.call[_perfCalls.count].name = strdup(lua_tostring(L, -3));
// Replacing a field that exists is allowed while lua_next walks the table.
lua_pushvalue(L, -3);
lua_pushvalue(L, -3);
lua_pushinteger(L, _perfCalls.count);
lua_pushcclosure(L, _perfCall, 2);
lua_rawset(L, -6);
_perfCalls.count++;
}
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
// Whether a key or button is physically down right now.
static bool _physicallyHeld(int32_t scancode) {
int32_t i = 0;
for (i = 0; i < _global.physicalKeyCount; i++) {
if (_global.physicalKeys[i].scancode == scancode) {
return true;
}
}
return false;
}
// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z,
// speed) and onTrigger(trigger, other, entered), when the script defines them. Physics keeps what
// one batch cannot hold, so the queue is drained until empty.
static void _physicsCallbacks(void) {
PhysicsEventT events[PHYSICS_MAX_EVENTS];
PhysicsEventT *event = NULL;
int32_t count = 0;
int32_t x = 0;
while ((count = physicsGetEvents(events, (int32_t)SDL_arraysize(events))) > 0) {
for (x = 0; x < count; x++) {
event = &events[x];
if (event->type == PHYSICS_EVENT_COLLISION) {
_callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed);
} else {
_callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0);
}
}
}
}
// The logical size the renderer presents: the video size, with the axes swapped by a quarter turn.
static void _presentSize(int32_t *width, int32_t *height) {
if ((_global.videoRotate == 90) || (_global.videoRotate == 270)) {
*width = _global.canvasHeight;
*height = _global.canvasWidth;
} else {
*width = _global.canvasWidth;
*height = _global.canvasHeight;
}
}
// Routes a key, button, or axis direction code: recorded as held, perhaps held back a moment for the
// quit chord, then acted on by _actOnKey -- engine switches first, then the script.
static void _processKey(bool down, int32_t keysym, int32_t scancode) {
bool keyboard = (scancode < CODE_GAMEPAD_BASE); // Every defined scancode is below the gamepad range
// Physical state is tracked even while the game is frozen.
if (down) {
_global.keyboardLastDown = scancode;
} else {
_global.keyboardLastUp = scancode;
}
if (keyboard && (scancode >= 0) && (scancode < SDL_SCANCODE_COUNT)) {
_global.keyboardState[scancode] = down;
}
_heldListUpdate(_global.physicalKeys, &_global.physicalKeyCount, down, keysym, scancode);
if (down && _quitChordHold(scancode)) {
return;
}
if (!down) {
_quitChordLetGo(scancode);
}
_actOnKey(down, keysym, scancode);
}
static void _progTrace(const char *fmt, ...) {
va_list args;
if (_global.conf->programTracing) {
va_start(args, fmt);
utilTraceVArgs(fmt, args);
va_end(args);
}
}
// Constants every script (and controls.cfg) can rely on. These are the single source of truth.
static void _pushConstants(lua_State *L) {
int32_t x = 0;
for (x = 0; x < INPUT_COUNT; x++) {
lua_pushinteger(L, x);
lua_setglobal(L, _inputNames[x].switchName);
}
// The easings tweenValue takes.
lua_pushinteger(L, SCHEDULER_EASE_LINEAR);
lua_setglobal(L, "EASE_LINEAR");
lua_pushinteger(L, SCHEDULER_EASE_QUAD_IN);
lua_setglobal(L, "EASE_QUAD_IN");
lua_pushinteger(L, SCHEDULER_EASE_QUAD_OUT);
lua_setglobal(L, "EASE_QUAD_OUT");
lua_pushinteger(L, SCHEDULER_EASE_QUAD_IN_OUT);
lua_setglobal(L, "EASE_QUAD_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_CUBIC_IN);
lua_setglobal(L, "EASE_CUBIC_IN");
lua_pushinteger(L, SCHEDULER_EASE_CUBIC_OUT);
lua_setglobal(L, "EASE_CUBIC_OUT");
lua_pushinteger(L, SCHEDULER_EASE_CUBIC_IN_OUT);
lua_setglobal(L, "EASE_CUBIC_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_SINE_IN);
lua_setglobal(L, "EASE_SINE_IN");
lua_pushinteger(L, SCHEDULER_EASE_SINE_OUT);
lua_setglobal(L, "EASE_SINE_OUT");
lua_pushinteger(L, SCHEDULER_EASE_SINE_IN_OUT);
lua_setglobal(L, "EASE_SINE_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_EXPO_IN);
lua_setglobal(L, "EASE_EXPO_IN");
lua_pushinteger(L, SCHEDULER_EASE_EXPO_OUT);
lua_setglobal(L, "EASE_EXPO_OUT");
lua_pushinteger(L, SCHEDULER_EASE_EXPO_IN_OUT);
lua_setglobal(L, "EASE_EXPO_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BACK_IN);
lua_setglobal(L, "EASE_BACK_IN");
lua_pushinteger(L, SCHEDULER_EASE_BACK_OUT);
lua_setglobal(L, "EASE_BACK_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BACK_IN_OUT);
lua_setglobal(L, "EASE_BACK_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_ELASTIC_IN);
lua_setglobal(L, "EASE_ELASTIC_IN");
lua_pushinteger(L, SCHEDULER_EASE_ELASTIC_OUT);
lua_setglobal(L, "EASE_ELASTIC_OUT");
lua_pushinteger(L, SCHEDULER_EASE_ELASTIC_IN_OUT);
lua_setglobal(L, "EASE_ELASTIC_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BOUNCE_IN);
lua_setglobal(L, "EASE_BOUNCE_IN");
lua_pushinteger(L, SCHEDULER_EASE_BOUNCE_OUT);
lua_setglobal(L, "EASE_BOUNCE_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BOUNCE_IN_OUT);
lua_setglobal(L, "EASE_BOUNCE_IN_OUT");
lua_pushinteger(L, FONT_QUALITY_SOLID);
lua_setglobal(L, "FONT_QUALITY_SOLID");
lua_pushinteger(L, FONT_QUALITY_SHADED);
lua_setglobal(L, "FONT_QUALITY_SHADED");
lua_pushinteger(L, FONT_QUALITY_BLENDED);
lua_setglobal(L, "FONT_QUALITY_BLENDED");
lua_pushinteger(L, KEYBOARD_NORMAL);
lua_setglobal(L, "MODE_NORMAL");
lua_pushinteger(L, KEYBOARD_FULL);
lua_setglobal(L, "MODE_FULL");
lua_pushinteger(L, MOUSE_SINGLE);
lua_setglobal(L, "MOUSE_SINGLE");
lua_pushinteger(L, MOUSE_MANY);
lua_setglobal(L, "MOUSE_MANY");
lua_pushinteger(L, MOUSE_SINGLE);
lua_setglobal(L, "SINGLE_MOUSE");
lua_pushinteger(L, MOUSE_MANY);
lua_setglobal(L, "MANY_MOUSE");
lua_pushinteger(L, OVERLAY_NOT_UPDATED);
lua_setglobal(L, "OVERLAY_NOT_UPDATED");
lua_pushinteger(L, OVERLAY_UPDATED);
lua_setglobal(L, "OVERLAY_UPDATED");
lua_pushinteger(L, RENDER_PIXELATED);
lua_setglobal(L, "RENDER_PIXELATED");
lua_pushinteger(L, RENDER_SMOOTH);
lua_setglobal(L, "RENDER_SMOOTH");
lua_pushinteger(L, SPRITE_ALPHA);
lua_setglobal(L, "SPRITE_ALPHA");
lua_pushinteger(L, SPRITE_ADD);
lua_setglobal(L, "SPRITE_ADD");
lua_pushinteger(L, SPRITE_MULTIPLY);
lua_setglobal(L, "SPRITE_MULTIPLY");
lua_pushinteger(L, DISC_STOPPED);
lua_setglobal(L, "DISC_STOPPED");
lua_pushinteger(L, DISC_PLAYING);
lua_setglobal(L, "DISC_PLAYING");
lua_pushinteger(L, DISC_PAUSED);
lua_setglobal(L, "DISC_PAUSED");
lua_pushinteger(L, DISC_EJECTED);
lua_setglobal(L, "DISC_EJECTED");
// 3D light types
lua_pushinteger(L, LIGHT_DIRECTIONAL);
lua_setglobal(L, "LIGHT_DIRECTIONAL");
lua_pushinteger(L, LIGHT_POINT);
lua_setglobal(L, "LIGHT_POINT");
lua_pushinteger(L, LIGHT_SPOT);
lua_setglobal(L, "LIGHT_SPOT");
// Physics bodies and shapes
lua_pushinteger(L, BODY_STATIC);
lua_setglobal(L, "BODY_STATIC");
lua_pushinteger(L, BODY_DYNAMIC);
lua_setglobal(L, "BODY_DYNAMIC");
lua_pushinteger(L, BODY_KINEMATIC);
lua_setglobal(L, "BODY_KINEMATIC");
lua_pushinteger(L, SHAPE_BOX);
lua_setglobal(L, "SHAPE_BOX");
lua_pushinteger(L, SHAPE_SPHERE);
lua_setglobal(L, "SHAPE_SPHERE");
lua_pushinteger(L, SHAPE_CAPSULE);
lua_setglobal(L, "SHAPE_CAPSULE");
lua_pushinteger(L, SHAPE_CYLINDER);
lua_setglobal(L, "SHAPE_CYLINDER");
lua_pushinteger(L, SHAPE_HULL);
lua_setglobal(L, "SHAPE_HULL");
lua_pushinteger(L, SHAPE_MESH);
lua_setglobal(L, "SHAPE_MESH");
lua_pushinteger(L, JOINT_HINGE);
lua_setglobal(L, "JOINT_HINGE");
lua_pushinteger(L, JOINT_BALL);
lua_setglobal(L, "JOINT_BALL");
lua_pushinteger(L, JOINT_SLIDER);
lua_setglobal(L, "JOINT_SLIDER");
lua_pushinteger(L, DEBUG_NONE);
lua_setglobal(L, "DEBUG_NONE");
lua_pushinteger(L, DEBUG_SHAPES);
lua_setglobal(L, "DEBUG_SHAPES");
lua_pushinteger(L, DEBUG_CONSTRAINTS);
lua_setglobal(L, "DEBUG_CONSTRAINTS");
lua_pushinteger(L, DEBUG_CONTACTS);
lua_setglobal(L, "DEBUG_CONTACTS");
lua_pushinteger(L, DEBUG_VELOCITIES);
lua_setglobal(L, "DEBUG_VELOCITIES");
lua_pushinteger(L, DEBUG_STATIC);
lua_setglobal(L, "DEBUG_STATIC");
lua_pushinteger(L, DEBUG_ALL);
lua_setglobal(L, "DEBUG_ALL");
// Particles
lua_pushinteger(L, PARTICLE_ALPHA);
lua_setglobal(L, "PARTICLE_ALPHA");
lua_pushinteger(L, PARTICLE_ADD);
lua_setglobal(L, "PARTICLE_ADD");
lua_pushinteger(L, PARTICLE_OVER);
lua_setglobal(L, "PARTICLE_OVER");
lua_pushinteger(L, PARTICLE_UNDER);
lua_setglobal(L, "PARTICLE_UNDER");
// Billboards
lua_pushinteger(L, BILLBOARD_NONE);
lua_setglobal(L, "BILLBOARD_NONE");
lua_pushinteger(L, BILLBOARD_ALL);
lua_setglobal(L, "BILLBOARD_ALL");
lua_pushinteger(L, BILLBOARD_Y);
lua_setglobal(L, "BILLBOARD_Y");
// Scene layers
lua_pushinteger(L, LAYER_SCENE);
lua_setglobal(L, "LAYER_SCENE");
lua_pushinteger(L, LAYER_FRONT);
lua_setglobal(L, "LAYER_FRONT");
// Particle collisions
lua_pushinteger(L, COLLIDE_NONE);
lua_setglobal(L, "COLLIDE_NONE");
lua_pushinteger(L, COLLIDE_FLOOR);
lua_setglobal(L, "COLLIDE_FLOOR");
lua_pushinteger(L, COLLIDE_SCENE);
lua_setglobal(L, "COLLIDE_SCENE");
// Texture filtering
lua_pushinteger(L, FILTER_LINEAR);
lua_setglobal(L, "FILTER_LINEAR");
lua_pushinteger(L, FILTER_NEAREST);
lua_setglobal(L, "FILTER_NEAREST");
// Tone curves
lua_pushinteger(L, TONEMAP_NONE);
lua_setglobal(L, "TONEMAP_NONE");
lua_pushinteger(L, TONEMAP_NEUTRAL);
lua_setglobal(L, "TONEMAP_NEUTRAL");
lua_pushinteger(L, TONEMAP_ACES);
lua_setglobal(L, "TONEMAP_ACES");
// Vehicles
lua_pushinteger(L, VEHICLE_CAR);
lua_setglobal(L, "VEHICLE_CAR");
lua_pushinteger(L, VEHICLE_MOTORCYCLE);
lua_setglobal(L, "VEHICLE_MOTORCYCLE");
lua_pushinteger(L, VEHICLE_TANK);
lua_setglobal(L, "VEHICLE_TANK");
lua_pushinteger(L, VEHICLE_BOAT);
lua_setglobal(L, "VEHICLE_BOAT");
// Soft bodies
lua_pushinteger(L, SOFT_CLOTH);
lua_setglobal(L, "SOFT_CLOTH");
lua_pushinteger(L, SOFT_BODY);
lua_setglobal(L, "SOFT_BODY");
lua_pushinteger(L, SOFT_ROPE);
lua_setglobal(L, "SOFT_ROPE");
lua_pushinteger(L, SOUND_CHANNEL_NONE);
lua_setglobal(L, "SOUND_ERROR_INVALID");
lua_pushinteger(L, SOUND_CHANNEL_NONE);
lua_setglobal(L, "SOUND_REMOVE_HANDLE");
// Input code layout so Framework.singe can build the GAMEPAD_N and MOUSE_N tables.
lua_pushinteger(L, CODE_GAMEPAD_BASE);
lua_setglobal(L, "SINGE_GAMEPAD_BASE");
lua_pushinteger(L, CODE_GAMEPAD_STRIDE);
lua_setglobal(L, "SINGE_GAMEPAD_STRIDE");
lua_pushinteger(L, CODE_AXIS_STRIDE);
lua_setglobal(L, "SINGE_AXIS_STRIDE");
lua_pushinteger(L, CODE_GAMEPAD_BUTTON_OFFSET);
lua_setglobal(L, "SINGE_GAMEPAD_BUTTON_OFFSET");
lua_pushinteger(L, CODE_MOUSE_BASE);
lua_setglobal(L, "SINGE_MOUSE_BASE");
lua_pushinteger(L, CODE_MOUSE_STRIDE);
lua_setglobal(L, "SINGE_MOUSE_STRIDE");
lua_pushinteger(L, MAX_CONTROLLERS);
lua_setglobal(L, "SINGE_MAX_CONTROLLERS");
lua_pushinteger(L, CONTROLLER_AXIS_COUNT);
lua_setglobal(L, "SINGE_CONTROLLER_AXES");
lua_pushinteger(L, CONTROLLER_BUTTON_COUNT);
lua_setglobal(L, "SINGE_CONTROLLER_BUTTONS");
// What vldpSetScale, vldpSetShift and vldpSetRotate will take, so a service screen clamps to
// the engine's limits rather than to its own copy of them: a shift out of range ends the
// script, which makes a stale copy fatal rather than merely wrong.
lua_pushinteger(L, SCALE_FACTOR_MIN);
lua_setglobal(L, "SINGE_SCALE_MIN");
lua_pushinteger(L, SCALE_FACTOR_MAX);
lua_setglobal(L, "SINGE_SCALE_MAX");
lua_pushinteger(L, SHIFT_MIN);
lua_setglobal(L, "SINGE_SHIFT_MIN");
lua_pushinteger(L, SHIFT_MAX);
lua_setglobal(L, "SINGE_SHIFT_MAX");
lua_pushinteger(L, ROTATE_STEP);
lua_setglobal(L, "SINGE_ROTATE_STEP");
lua_pushinteger(L, MAX_MICE);
lua_setglobal(L, "SINGE_MAX_MICE");
lua_pushinteger(L, SINGE_VERSION_MAJOR);
lua_setglobal(L, "SINGE_VERSION_MAJOR");
lua_pushinteger(L, SINGE_VERSION_MINOR);
lua_setglobal(L, "SINGE_VERSION_MINOR");
lua_pushstring(L, VERSION_STRING);
lua_setglobal(L, "SINGE_VERSION_STRING");
lua_pushnumber(L, SINGE_VERSION);
lua_setglobal(L, "SINGE_FRAMEWORK_VERSION");
lua_pushinteger(L, _global.controllerDeadZone);
lua_setglobal(L, "SINGE_DEAD_ZONE");
lua_pushinteger(L, _global.triggerThreshold);
lua_setglobal(L, "SINGE_TRIGGER_THRESHOLD");
lua_pushboolean(L, _global.conf->legacySpriteArgs);
lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS");
lua_pushboolean(L, _global.conf->disc);
lua_setglobal(L, "SINGE_DISC");
lua_pushboolean(L, !_global.conf->noUpdateCheck);
lua_setglobal(L, "SINGE_UPDATE_CHECK");
}
// Pushes a vector as three results.
static int32_t _pushVec3(lua_State *L, Vec3T v) {
lua_pushnumber(L, v.x);
lua_pushnumber(L, v.y);
lua_pushnumber(L, v.z);
return 3;
}
// Writes one overlay pixel. The overlay is always BGRA32, one uint32_t per pixel (_overlayResize
// makes it so), and must be locked by the caller.
static void _putPixel(int32_t x, int32_t y, uint32_t pixel) {
SDL_Surface *surface = _global.overlay;
uint32_t *row = NULL;
if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) {
return;
}
row = (uint32_t *)((uint8_t *)surface->pixels + (size_t)y * (size_t)surface->pitch);
row[x] = pixel;
}
// Writes the two triangles of quad number quad (corners quad * 4 onwards) at index slot.
static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad) {
int32_t *out = indices + slot * QUAD_INDICES;
int32_t first = quad * QUAD_VERTICES;
out[0] = first + 0;
out[1] = first + 1;
out[2] = first + 2;
out[3] = first + 0;
out[4] = first + 2;
out[5] = first + 3;
}
// Vertex and index room for this many textured quads, and start slots for this many frames,
// kept between frames and grown only when a bigger emitter comes along.
static void _quadScratch(int32_t quads, int32_t frames) {
if (quads > _global.quadCapacity) {
_global.quadVertices = SDL_realloc(_global.quadVertices, (size_t)quads * QUAD_VERTICES * sizeof(SDL_Vertex));
_global.quadIndices = SDL_realloc(_global.quadIndices, (size_t)quads * QUAD_INDICES * sizeof(int32_t));
if ((_global.quadVertices == NULL) || (_global.quadIndices == NULL)) {
utilDie("Out of memory drawing particles.");
}
_global.quadCapacity = quads;
}
if (frames > _global.frameStartCapacity) {
_global.frameStarts = SDL_realloc(_global.frameStarts, (size_t)frames * sizeof(int32_t));
if (_global.frameStarts == NULL) {
utilDie("Out of memory drawing particles.");
}
_global.frameStartCapacity = frames;
}
}
// A press of one of the quit chord's buttons, before the rest of the chord is down, waits: the
// button's own meaning -- Start launches a game from the menu -- would otherwise happen on the way
// to quitting, and in the menu instead of it. True if the press is held back. It goes on when
// the chord completes, dropped, or when the button is let go or QUIT_CHORD_WAIT_MS passes, late.
static bool _quitChordHold(int32_t scancode) {
bool member = false;
bool complete = true;
int32_t c = 0;
if ((_global.quitChord.count == 0) || _global.quitChord.active || (_global.quitChordHeldCount >= CHORD_CODES_MAX)) {
return false;
}
for (c = 0; c < _global.quitChord.count; c++) {
member = member || (_global.quitChord.code[c] == scancode);
complete = complete && _physicallyHeld(_global.quitChord.code[c]);
}
if (!member || complete) {
return false;
}
if (_global.quitChordHeldCount == 0) {
_global.quitChordHeldMS = utilTicks();
}
_global.quitChordHeld[_global.quitChordHeldCount++] = scancode;
return true;
}
// Lets the held back presses through, ahead of the release of one of them, or of all of them for -1:
// the wait ran out.
static void _quitChordLetGo(int32_t scancode) {
int32_t held[CHORD_CODES_MAX];
int32_t count = _global.quitChordHeldCount;
bool found = (scancode < 0);
int32_t x = 0;
for (x = 0; x < count; x++) {
found = found || (_global.quitChordHeld[x] == scancode);
}
if (!found) {
return;
}
// Emptied first: the presses go through _actOnKey, which must not see them as held.
memcpy(held, _global.quitChordHeld, sizeof(int32_t) * (size_t)count);
_global.quitChordHeldCount = 0;
for (x = 0; x < count; x++) {
_actOnKey(true, 0, held[x]);
}
}
// colorXxx(r, g, b[, a]) with components clamped to 0..255.
static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha) {
int32_t n = lua_gettop(L);
uint8_t value[COLOR_COMPONENTS] = { 0, 0, 0, defaultAlpha };
int32_t x = 0;
_argCheck(L, method, 3, COLOR_COMPONENTS);
for (x = 0; x < n; x++) {
value[x] = _argColorByte(L, method, x + 1);
}
color->r = value[0];
color->g = value[1];
color->b = value[2];
color->a = value[3];
_luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a);
}
// The whole window as it is about to be shown, letterbox included: with a logical size set, a NULL
// rect would only cover the scaled viewport, so it is switched off around the read.
static SDL_Surface *_readWindow(void) {
int32_t logicalW = 0;
int32_t logicalH = 0;
SDL_Surface *surface = NULL;
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED;
SDL_GetRenderLogicalPresentation(_global.renderer, &logicalW, &logicalH, &mode);
SDL_SetRenderLogicalPresentation(_global.renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
surface = SDL_RenderReadPixels(_global.renderer, NULL);
SDL_SetRenderLogicalPresentation(_global.renderer, logicalW, logicalH, mode);
if (surface == NULL) {
utilDie("%s", SDL_GetError());
}
return surface;
}
// At each frame drawn: a recording started or stopped as asked, and the frame handed to one that is
// running. --record starts on the first frame and runs to the end; the record switch and
// singeRecordStart write the next singeNNN.mp4, or the named file, in the data directory.
static void _recordUpdate(void) {
char *filename = NULL;
int32_t x = 0;
SDL_Surface *surface = NULL;
if (_global.recordRequest == RECORD_REQUEST_STOP) {
_global.recordRequest = RECORD_REQUEST_NONE;
_progTrace("Stopping the recording of %s", recordPath());
recordStop();
}
if (_global.recordRequest == RECORD_REQUEST_START) {
_global.recordRequest = RECORD_REQUEST_NONE;
if (!recordIsRecording()) {
if (_global.recordName != NULL) {
filename = utilCreateString("%s%s", _global.conf->dataDir, _global.recordName);
} else if ((_global.conf->recordFile != NULL) && (_global.nextRecording == 0)) {
filename = utilCreateString("%s", _global.conf->recordFile);
} else {
for (x = _global.nextRecording; x < SCREENSHOT_MAX; x++) {
free(filename);
filename = utilCreateString("%ssinge%03d.mp4", _global.conf->dataDir, x);
if (!utilFileExists(filename)) {
break;
}
}
}
_global.nextRecording = SDL_max(_global.nextRecording, x + 1);
free(_global.recordName);
_global.recordName = NULL;
// The recording is the size of the frames the window gives back, whatever the renderer
// says its output is.
surface = _readWindow();
if (!recordStart(filename, surface->w, surface->h, utilIsDeterministic() ? _global.conf->deterministicStep : 0, videoGetMixer(), _global.generatedAudio ? 0 : videoGetAudioLatency(), utilTicks())) {
utilTrace("Unable to record %s: %s", filename, SDL_GetError());
} else {
_progTrace("Recording %s with %s", filename, recordVideoEncoder());
}
free(filename);
}
}
if (recordIsRecording()) {
if (surface == NULL) {
surface = _readWindow();
}
recordFrame(surface, utilTicks());
}
SDL_DestroySurface(surface);
}
// Adds a rectangle, or everything for NULL. A full region merges it into the rectangle it grows least.
static void _regionAdd(RegionT *region, const SDL_Rect *area) {
SDL_Rect merged;
int64_t growth = 0;
int64_t least = INT64_MAX;
int32_t closest = 0;
int32_t x = 0;
if (region->all) {
return;
}
if (area == NULL) {
region->all = true;
region->count = 0;
return;
}
if (region->count < REGION_RECTS_MAX) {
region->rect[region->count++] = *area;
return;
}
for (x = 0; x < REGION_RECTS_MAX; x++) {
SDL_GetRectUnion(&region->rect[x], area, &merged);
growth = (int64_t)merged.w * merged.h - (int64_t)region->rect[x].w * region->rect[x].h;
if (growth < least) {
least = growth;
closest = x;
}
}
SDL_GetRectUnion(&region->rect[closest], area, &region->rect[closest]);
}
static void _regionAddRegion(RegionT *region, const RegionT *other) {
int32_t x = 0;
if (other->all) {
_regionAdd(region, NULL);
return;
}
for (x = 0; x < other->count; x++) {
_regionAdd(region, &other->rect[x]);
}
}
static void _regionClear(RegionT *region) {
region->count = 0;
region->all = false;
}
// Every Singe call a script may make. Comments give the version each call appeared in.
static void _registerApi(lua_State *L) {
lua_register(L, "allowSocketCall", apiAllowSocketCall); // Hypseus
lua_register(L, "animationGetTime", apiAnimationGetTime); // 3.00
lua_register(L, "animationIsPlaying", apiAnimationIsPlaying); // 3.00
lua_register(L, "animationPause", apiAnimationPause); // 3.00
lua_register(L, "animationPlay", apiAnimationPlay); // 3.00
lua_register(L, "animationPlayLayer", apiAnimationPlayLayer); // 3.00
lua_register(L, "animationResume", apiAnimationResume); // 3.00
lua_register(L, "animationSetLayerMask", apiAnimationSetLayerMask); // 3.00
lua_register(L, "animationSetLayerWeight", apiAnimationSetLayerWeight); // 3.00
lua_register(L, "animationSetTime", apiAnimationSetTime); // 3.00
lua_register(L, "animationStop", apiAnimationStop); // 3.00
lua_register(L, "bodyApplyForce", apiBodyApplyForce); // 3.00
lua_register(L, "bodyApplyImpulse", apiBodyApplyImpulse); // 3.00
lua_register(L, "bodyDelete", apiBodyDelete); // 3.00
lua_register(L, "bodyGetAngularVelocity", apiBodyGetAngularVelocity); // 3.00
lua_register(L, "bodyGetVelocity", apiBodyGetVelocity); // 3.00
lua_register(L, "bodyIsResting", apiBodyIsResting); // 3.00
lua_register(L, "bodyNew", apiBodyNew); // 3.00
lua_register(L, "bodySetAngularVelocity", apiBodySetAngularVelocity); // 3.00
lua_register(L, "bodySetBounce", apiBodySetBounce); // 3.00
lua_register(L, "bodySetBuoyancy", apiBodySetBuoyancy); // 3.00
lua_register(L, "bodySetCurrent", apiBodySetCurrent); // 3.00
lua_register(L, "bodySetEnabled", apiBodySetEnabled); // 3.00
lua_register(L, "bodySetFriction", apiBodySetFriction); // 3.00
lua_register(L, "bodySetMass", apiBodySetMass); // 3.00
lua_register(L, "bodySetTrigger", apiBodySetTrigger); // 3.00
lua_register(L, "bodySetVelocity", apiBodySetVelocity); // 3.00
lua_register(L, "bodySetWater", apiBodySetWater); // 3.00
lua_register(L, "cameraSet", apiCameraSet); // 3.00
lua_register(L, "cameraSetOrthographic", apiCameraSetOrthographic); // 3.00
lua_register(L, "cameraSetPerspective", apiCameraSetPerspective); // 3.00
lua_register(L, "colorBackground", apiColorBackground); // 1.xx
lua_register(L, "collideCircles", apiCollideCircles); // 3.00
lua_register(L, "collidePointCircle", apiCollidePointCircle); // 3.00
lua_register(L, "collidePointPolygon", apiCollidePointPolygon); // 3.00
lua_register(L, "collidePointRect", apiCollidePointRect); // 3.00
lua_register(L, "collideRectCircle", apiCollideRectCircle); // 3.00
lua_register(L, "collideRects", apiCollideRects); // 3.00
lua_register(L, "collideSegments", apiCollideSegments); // 3.00
lua_register(L, "colorForeground", apiColorForeground); // 1.xx
lua_register(L, "controllerDoRumble", apiControllerDoRumble); // Hypseus
lua_register(L, "controllerGetAxis", apiControllerGetAxis); // 2.00
lua_register(L, "controllerGetButton", apiControllerGetButton); // 2.10
lua_register(L, "controllerGetName", apiControllerGetName); // 3.00
lua_register(L, "controllerGetPadding", apiControllerGetPadding); // Hypseus
lua_register(L, "controllerHowMany", apiControllerHowMany); // Hypseus
lua_register(L, "controllerIsValid", apiControllerIsValid); // Hypseus
lua_register(L, "controllerSetPadding", apiControllerSetPadding); // Hypseus
lua_register(L, "debugPrint", apiDebugPrint); // 1.xx
lua_register(L, "discAudio", apiDiscAudio); // 1.xx
lua_register(L, "discAudioSuffix", apiDiscAudioSuffix); // Hypseus
lua_register(L, "discChangeSpeed", apiDiscChangeSpeed); // 1.xx
lua_register(L, "discGetAudioTrack", apiDiscGetAudioTrack); // 2.10
lua_register(L, "discGetAudioTracks", apiDiscGetAudioTracks); // 2.10
lua_register(L, "discGetSubtitleLanguage", apiDiscGetSubtitleLanguage); // 3.00
lua_register(L, "discGetSubtitleTracks", apiDiscGetSubtitleTracks); // 3.00
lua_register(L, "discGetFrame", apiDiscGetFrame); // 1.xx
lua_register(L, "discGetFrameCount", apiDiscGetFrameCount); // 3.00
lua_register(L, "discGetHeight", apiDiscGetHeight); // 2.00
lua_register(L, "discGetLanguage", apiDiscGetLanguage); // 2.10
lua_register(L, "discGetState", apiDiscGetState); // 1.xx RDG
lua_register(L, "discGetWidth", apiDiscGetWidth); // 2.00
lua_register(L, "discPause", apiDiscPause); // 1.xx
lua_register(L, "discPauseAtFrame", apiDiscSearch); // 1.18 Same as discSearch.
lua_register(L, "discPlay", apiDiscPlay); // 1.xx
lua_register(L, "discSearch", apiDiscSearch); // 1.xx
lua_register(L, "discSearchBlanking", apiDiscSearchBlanking); // 1.xx
lua_register(L, "discSetAudioTrack", apiDiscSetAudioTrack); // 2.10
lua_register(L, "discSetFPS", apiDiscSetFPS); // 1.xx
lua_register(L, "discSkipBackward", apiDiscSkipBackward); // 1.xx
lua_register(L, "discSkipBlanking", apiDiscSkipBlanking); // 1.xx
lua_register(L, "discSkipForward", apiDiscSkipForward); // 1.xx
lua_register(L, "discSkipToFrame", apiDiscSkipToFrame); // 1.xx
lua_register(L, "discStepBackward", apiDiscStepBackward); // 1.xx
lua_register(L, "discStepForward", apiDiscStepForward); // 1.xx
lua_register(L, "discStop", apiDiscStop); // 1.xx
lua_register(L, "drawTransparent", apiDrawTransparent); // Hypseus
lua_register(L, "emitterBurst", apiEmitterBurst); // 3.00
lua_register(L, "emitterClear", apiEmitterClear); // 3.00
lua_register(L, "emitterDelete", apiEmitterDelete); // 3.00
lua_register(L, "emitterDraw", apiEmitterDraw); // 3.00
lua_register(L, "emitterGetCount", apiEmitterGetCount); // 3.00
lua_register(L, "emitterIsActive", apiEmitterIsActive); // 3.00
lua_register(L, "emitterNew", apiEmitterNew); // 3.00
lua_register(L, "emitterSetBlend", apiEmitterSetBlend); // 3.00
lua_register(L, "emitterSetCollide", apiEmitterSetCollide); // 3.00
lua_register(L, "emitterSetColor", apiEmitterSetColor); // 3.00
lua_register(L, "emitterSetDirection", apiEmitterSetDirection); // 3.00
lua_register(L, "emitterSetDrag", apiEmitterSetDrag); // 3.00
lua_register(L, "emitterSetFrames", apiEmitterSetFrames); // 3.00
lua_register(L, "emitterSetGravity", apiEmitterSetGravity); // 3.00
lua_register(L, "emitterSetLayer", apiEmitterSetLayer); // 3.00
lua_register(L, "emitterSetLife", apiEmitterSetLife); // 3.00
lua_register(L, "emitterSetLit", apiEmitterSetLit); // 3.00
lua_register(L, "emitterSetLocal", apiEmitterSetLocal); // 3.00
lua_register(L, "emitterSetMax", apiEmitterSetMax); // 3.00
lua_register(L, "emitterSetPosition", apiEmitterSetPosition); // 3.00
lua_register(L, "emitterSetRadius", apiEmitterSetRadius); // 3.00
lua_register(L, "emitterSetRate", apiEmitterSetRate); // 3.00
lua_register(L, "emitterSetSize", apiEmitterSetSize); // 3.00
lua_register(L, "emitterSetSoftness", apiEmitterSetSoftness); // 3.00
lua_register(L, "emitterSetSpeed", apiEmitterSetSpeed); // 3.00
lua_register(L, "emitterSetSpin", apiEmitterSetSpin); // 3.00
lua_register(L, "emitterSetSpread", apiEmitterSetSpread); // 3.00
lua_register(L, "emitterSetTexture", apiEmitterSetTexture); // 3.00
lua_register(L, "emitterSetTrail", apiEmitterSetTrail); // 3.00
lua_register(L, "emitterStart", apiEmitterStart); // 3.00
lua_register(L, "emitterStop", apiEmitterStop); // 3.00
lua_register(L, "fontLoad", apiFontLoad); // 1.xx
lua_register(L, "fontPrint", apiFontPrint); // 1.xx
lua_register(L, "fontQuality", apiFontQuality); // 1.xx
lua_register(L, "fontSelect", apiFontSelect); // 1.xx
lua_register(L, "fontToSprite", apiFontToSprite); // 1.xx
lua_register(L, "fontUnload", apiFontUnload); // 2.00
lua_register(L, "getFValue", apiGetFValue); // Hypseus
lua_register(L, "getUserString", apiGetUserString); // Hypseus
lua_register(L, "guiClose", apiGuiClose); // 3.00
lua_register(L, "guiDelete", apiGuiDelete); // 3.00
lua_register(L, "guiDraw", apiGuiDraw); // 3.00
lua_register(L, "guiDrawScreen", apiGuiDrawScreen); // 3.00
lua_register(L, "guiGetHeight", apiGuiGetHeight); // 3.00
lua_register(L, "guiGetStats", apiGuiGetStats); // 3.00
lua_register(L, "guiGetValue", apiGuiGetValue); // 3.00
lua_register(L, "guiGetWidth", apiGuiGetWidth); // 3.00
lua_register(L, "guiHide", apiGuiHide); // 3.00
lua_register(L, "guiLoad", apiGuiLoad); // 3.00
lua_register(L, "guiLoadFont", apiGuiLoadFont); // 3.00
lua_register(L, "guiNew", apiGuiNew); // 3.00
lua_register(L, "guiRefresh", apiGuiRefresh); // 3.00
lua_register(L, "guiSetHandler", apiGuiSetHandler); // 3.00
lua_register(L, "guiSetInput", apiGuiSetInput); // 3.00
lua_register(L, "guiSetValue", apiGuiSetValue); // 3.00
lua_register(L, "guiShow", apiGuiShow); // 3.00
lua_register(L, "jitterGet", apiJitterGet); // 3.00
lua_register(L, "jitterNew", apiJitterNew); // 3.00
lua_register(L, "jitterPut", apiJitterPut); // 3.00
lua_register(L, "jointBall", apiJointBall); // 3.00
lua_register(L, "jointDelete", apiJointDelete); // 3.00
lua_register(L, "jointHinge", apiJointHinge); // 3.00
lua_register(L, "jointSetLimits", apiJointSetLimits); // 3.00
lua_register(L, "jointSlider", apiJointSlider); // 3.00
lua_register(L, "joyMouseEnable", apiJoyMouseEnable); // Hypseus
lua_register(L, "joyMouseIsEnabled", apiJoyMouseIsEnabled); // 3.00
lua_register(L, "keyboardGetLastDown", apiKeyboardGetLastDown); // 2.10
lua_register(L, "keyboardGetLastUp", apiKeyboardGetLastUp); // 2.10
lua_register(L, "keyboardGetMode", apiKeyboardGetMode); // 1.xx RDG
lua_register(L, "keyboardGetModifiers", apiKeyboardGetModifiers); // 2.10
lua_register(L, "keyboardIsDown", apiKeyboardIsDown); // 2.10
lua_register(L, "keyboardSetMode", apiKeyboardSetMode); // 1.xx RDG
lua_register(L, "lightNew", apiLightNew); // 3.00
lua_register(L, "lightSetColor", apiLightSetColor); // 3.00
lua_register(L, "lightSetCone", apiLightSetCone); // 3.00
lua_register(L, "lightSetIntensity", apiLightSetIntensity); // 3.00
lua_register(L, "lightSetRange", apiLightSetRange); // 3.00
lua_register(L, "lightSetShadow", apiLightSetShadow); // 3.00
lua_register(L, "lineDraw", apiLineDraw); // 3.00
lua_register(L, "mainBezelLoaded", apiMainBezelLoaded); // Hypseus
lua_register(L, "materialDelete", apiMaterialDelete); // 3.00
lua_register(L, "materialNew", apiMaterialNew); // 3.00
lua_register(L, "materialSetBlend", apiMaterialSetBlend); // 3.00
lua_register(L, "materialSetColor", apiMaterialSetColor); // 3.00
lua_register(L, "materialSetCutoff", apiMaterialSetCutoff); // 3.00
lua_register(L, "materialSetDoubleSided", apiMaterialSetDoubleSided); // 3.00
lua_register(L, "materialSetEmissive", apiMaterialSetEmissive); // 3.00
lua_register(L, "materialSetEmissiveMap", apiMaterialSetEmissiveMap); // 3.00
lua_register(L, "materialSetFilter", apiMaterialSetFilter); // 3.00
lua_register(L, "materialSetGui", apiMaterialSetGui); // 3.00
lua_register(L, "materialSetMetallic", apiMaterialSetMetallic); // 3.00
lua_register(L, "materialSetMetallicRoughnessMap", apiMaterialSetMetallicRoughnessMap); // 3.00
lua_register(L, "materialSetNormalMap", apiMaterialSetNormalMap); // 3.00
lua_register(L, "materialSetOccluder", apiMaterialSetOccluder); // 3.00
lua_register(L, "materialSetOcclusionMap", apiMaterialSetOcclusionMap); // 3.00
lua_register(L, "materialSetRoughness", apiMaterialSetRoughness); // 3.00
lua_register(L, "materialSetTexture", apiMaterialSetTexture); // 3.00
lua_register(L, "materialSetTiling", apiMaterialSetTiling); // 3.00
lua_register(L, "materialSetUnlit", apiMaterialSetUnlit); // 3.00
lua_register(L, "materialSetVideo", apiMaterialSetVideo); // 3.00
lua_register(L, "materialSetView", apiMaterialSetView); // 3.00
lua_register(L, "meshBox", apiMeshBox); // 3.00
lua_register(L, "meshCone", apiMeshCone); // 3.00
lua_register(L, "meshCylinder", apiMeshCylinder); // 3.00
lua_register(L, "meshDelete", apiMeshDelete); // 3.00
lua_register(L, "meshHeightmap", apiMeshHeightmap); // 3.00
lua_register(L, "meshNew", apiMeshNew); // 3.00
lua_register(L, "meshPlane", apiMeshPlane); // 3.00
lua_register(L, "meshSphere", apiMeshSphere); // 3.00
lua_register(L, "meshTorus", apiMeshTorus); // 3.00
lua_register(L, "micCalibrate", apiMicCalibrate); // 3.00
lua_register(L, "micCalibration", apiMicCalibration); // 3.00
lua_register(L, "micClose", apiMicClose); // 3.00
lua_register(L, "micIsOpen", apiMicIsOpen); // 3.00
lua_register(L, "micLatency", apiMicLatency); // 3.00
lua_register(L, "micLevel", apiMicLevel); // 3.00
lua_register(L, "micList", apiMicList); // 3.00
lua_register(L, "micMonitor", apiMicMonitor); // 3.00
lua_register(L, "micOpen", apiMicOpen); // 3.00
lua_register(L, "micPitch", apiMicPitch); // 3.00
lua_register(L, "micRead", apiMicRead); // 3.00
lua_register(L, "micRecordStart", apiMicRecordStart); // 3.00
lua_register(L, "micRecordStop", apiMicRecordStop); // 3.00
lua_register(L, "micSetLatency", apiMicSetLatency); // 3.00
lua_register(L, "midiCloseInput", apiMidiCloseInput); // 3.00
lua_register(L, "midiCloseOutput", apiMidiCloseOutput); // 3.00
lua_register(L, "midiControlChange", apiMidiControlChange); // 3.00
lua_register(L, "midiInputCount", apiMidiInputCount); // 3.00
lua_register(L, "midiInputName", apiMidiInputName); // 3.00
lua_register(L, "midiIsInputOpen", apiMidiIsInputOpen); // 3.00
lua_register(L, "midiIsOutputOpen", apiMidiIsOutputOpen); // 3.00
lua_register(L, "midiNoteOff", apiMidiNoteOff); // 3.00
lua_register(L, "midiNoteOn", apiMidiNoteOn); // 3.00
lua_register(L, "midiOpenInput", apiMidiOpenInput); // 3.00
lua_register(L, "midiOpenOutput", apiMidiOpenOutput); // 3.00
lua_register(L, "midiOutputCount", apiMidiOutputCount); // 3.00
lua_register(L, "midiOutputName", apiMidiOutputName); // 3.00
lua_register(L, "midiPitchBend", apiMidiPitchBend); // 3.00
lua_register(L, "midiProgramChange", apiMidiProgramChange); // 3.00
lua_register(L, "midiRescan", apiMidiRescan); // 3.00
lua_register(L, "midiSend", apiMidiSend); // 3.00
lua_register(L, "modelDelete", apiModelDelete); // 3.00
lua_register(L, "modelGetAnimations", apiModelGetAnimations); // 3.00
lua_register(L, "modelInstance", apiModelInstance); // 3.00
lua_register(L, "modelLoad", apiModelLoad); // 3.00
lua_register(L, "mouseGetName", apiMouseGetName); // 3.00
lua_register(L, "mouseGetPadding", apiMouseGetPadding); // Hypseus
lua_register(L, "mouseGetPosition", apiMouseGetPosition); // 2.00
lua_register(L, "mouseHowMany", apiMouseHowMany); // 1.18 RDG
lua_register(L, "mouseSetCaptured", apiMouseSetCaptured); // 2.00
lua_register(L, "mouseSetEnabled", apiMouseSetEnabled); // 3.00 mouseEnable/mouseDisable are framework aliases.
lua_register(L, "mouseSetMode", apiMouseSetMode); // 1.18 RDG
lua_register(L, "musicGetPosition", apiMusicGetPosition); // 3.00
lua_register(L, "musicIsPlaying", apiMusicIsPlaying); // Hypseus
lua_register(L, "musicLoad", apiMusicLoad); // Hypseus
lua_register(L, "musicPause", apiMusicPause); // Hypseus
lua_register(L, "musicPlay", apiMusicPlay); // Hypseus
lua_register(L, "musicResume", apiMusicResume); // Hypseus
lua_register(L, "musicSetVolume", apiMusicSetVolume); // Hypseus
lua_register(L, "musicStop", apiMusicStop); // Hypseus
lua_register(L, "musicUnload", apiMusicUnload); // Hypseus
lua_register(L, "navAddNode", apiNavAddNode); // 3.00
lua_register(L, "navAgentDelete", apiNavAgentDelete); // 3.00
lua_register(L, "navAgentGetVelocity", apiNavAgentGetVelocity); // 3.00
lua_register(L, "navAgentIsArrived", apiNavAgentIsArrived); // 3.00
lua_register(L, "navAgentMoveTo", apiNavAgentMoveTo); // 3.00
lua_register(L, "navAgentNew", apiNavAgentNew); // 3.00
lua_register(L, "navAgentSetPlayer", apiNavAgentSetPlayer); // 3.00
lua_register(L, "navAgentStop", apiNavAgentStop); // 3.00
lua_register(L, "navBuild", apiNavBuild); // 3.00
lua_register(L, "navDelete", apiNavDelete); // 3.00
lua_register(L, "navDraw", apiNavDraw); // 3.00
lua_register(L, "navLoad", apiNavLoad); // 3.00
lua_register(L, "navNearest", apiNavNearest); // 3.00
lua_register(L, "navNew", apiNavNew); // 3.00
lua_register(L, "navPath", apiNavPath); // 3.00
lua_register(L, "navRandomPoint", apiNavRandomPoint); // 3.00
lua_register(L, "navRaycast", apiNavRaycast); // 3.00
lua_register(L, "navSave", apiNavSave); // 3.00
lua_register(L, "netTrustSystem", apiNetTrustSystem); // 3.00
lua_register(L, "nodeDelete", apiNodeDelete); // 3.00
lua_register(L, "nodeFind", apiNodeFind); // 3.00
lua_register(L, "nodeGetChildren", apiNodeGetChildren); // 3.00
lua_register(L, "nodeGetMorph", apiNodeGetMorph); // 3.00
lua_register(L, "nodeGetMorphs", apiNodeGetMorphs); // 3.00
lua_register(L, "nodeGetName", apiNodeGetName); // 3.00
lua_register(L, "nodeGetParent", apiNodeGetParent); // 3.00
lua_register(L, "nodeGetPosition", apiNodeGetPosition); // 3.00
lua_register(L, "nodeGetQuaternion", apiNodeGetQuaternion); // 3.00
lua_register(L, "nodeGetRotation", apiNodeGetRotation); // 3.00
lua_register(L, "nodeGetScale", apiNodeGetScale); // 3.00
lua_register(L, "nodeGetWorldPosition", apiNodeGetWorldPosition); // 3.00
lua_register(L, "nodeLookAt", apiNodeLookAt); // 3.00
lua_register(L, "nodeMove", apiNodeMove); // 3.00
lua_register(L, "nodeNew", apiNodeNew); // 3.00
lua_register(L, "nodeRotate", apiNodeRotate); // 3.00
lua_register(L, "nodeSetBillboard", apiNodeSetBillboard); // 3.00
lua_register(L, "nodeSetLayer", apiNodeSetLayer); // 3.00
lua_register(L, "nodeSetMaterial", apiNodeSetMaterial); // 3.00
lua_register(L, "nodeSetMesh", apiNodeSetMesh); // 3.00
lua_register(L, "nodeSetMorph", apiNodeSetMorph); // 3.00
lua_register(L, "nodeSetName", apiNodeSetName); // 3.00
lua_register(L, "nodeSetParent", apiNodeSetParent); // 3.00
lua_register(L, "nodeSetPosition", apiNodeSetPosition); // 3.00
lua_register(L, "nodeSetQuaternion", apiNodeSetQuaternion); // 3.00
lua_register(L, "nodeSetRotation", apiNodeSetRotation); // 3.00
lua_register(L, "nodeSetScale", apiNodeSetScale); // 3.00
lua_register(L, "nodeSetShadow", apiNodeSetShadow); // 3.00
lua_register(L, "nodeSetSprite", apiNodeSetSprite); // 3.00
lua_register(L, "nodeSetSpriteFrame", apiNodeSetSpriteFrame); // 3.00
lua_register(L, "nodeSetText", apiNodeSetText); // 3.00
lua_register(L, "nodeSetVisible", apiNodeSetVisible); // 3.00
lua_register(L, "opusDecode", apiOpusDecode); // 3.00
lua_register(L, "opusDecoderNew", apiOpusDecoderNew); // 3.00
lua_register(L, "opusEncode", apiOpusEncode); // 3.00
lua_register(L, "opusEncoderNew", apiOpusEncoderNew); // 3.00
lua_register(L, "opusEncoderSet", apiOpusEncoderSet); // 3.00
lua_register(L, "overlayBanner", apiOverlayBanner); // Hypseus
lua_register(L, "overlayBox", apiOverlayBox); // 2.00
lua_register(L, "overlayCircle", apiOverlayCircle); // 2.00
lua_register(L, "overlayClear", apiOverlayClear); // 1.xx
lua_register(L, "overlayEllipse", apiOverlayEllipse); // 2.00
lua_register(L, "overlayGetFontHeight", apiOverlayGetFontHeight); // 3.00
lua_register(L, "overlayGetFontWidth", apiOverlayGetFontWidth); // 3.00
lua_register(L, "overlayGetHeight", apiOverlayGetHeight); // 1.xx
lua_register(L, "overlayGetWidth", apiOverlayGetWidth); // 1.xx
lua_register(L, "overlayLine", apiOverlayLine); // 2.00
lua_register(L, "overlayPlot", apiOverlayPlot); // 2.00
lua_register(L, "overlayPrint", apiOverlayPrint); // 1.xx
lua_register(L, "overlaySetMonochrome", apiOverlaySetMonochrome); // Hypseus
lua_register(L, "overlaySetResolution", apiOverlaySetResolution); // 2.00
lua_register(L, "physicsRaycast", apiPhysicsRaycast); // 3.00
lua_register(L, "physicsSet2D", apiPhysicsSet2D); // 3.00
lua_register(L, "physicsSetDebug", apiPhysicsSetDebug); // 3.00
lua_register(L, "physicsSetEnabled", apiPhysicsSetEnabled); // 3.00
lua_register(L, "physicsSetGravity", apiPhysicsSetGravity); // 3.00
lua_register(L, "playerDelete", apiPlayerDelete); // 3.00
lua_register(L, "playerGetGround", apiPlayerGetGround); // 3.00
lua_register(L, "playerGetVelocity", apiPlayerGetVelocity); // 3.00
lua_register(L, "playerIsOnGround", apiPlayerIsOnGround); // 3.00
lua_register(L, "playerIsSwimming", apiPlayerIsSwimming); // 3.00
lua_register(L, "playerJump", apiPlayerJump); // 3.00
lua_register(L, "playerMove", apiPlayerMove); // 3.00
lua_register(L, "playerNew", apiPlayerNew); // 3.00
lua_register(L, "playerSetEnabled", apiPlayerSetEnabled); // 3.00
lua_register(L, "playerSetGravityScale", apiPlayerSetGravityScale); // 3.00
lua_register(L, "playerSetMass", apiPlayerSetMass); // 3.00
lua_register(L, "playerSetPosition", apiPlayerSetPosition); // 3.00
lua_register(L, "playerSetPush", apiPlayerSetPush); // 3.00
lua_register(L, "playerSetSlope", apiPlayerSetSlope); // 3.00
lua_register(L, "playerSetStep", apiPlayerSetStep); // 3.00
lua_register(L, "playerSetSwim", apiPlayerSetSwim); // 3.00
lua_register(L, "playerSetVelocity", apiPlayerSetVelocity); // 3.00
lua_register(L, "ragdollActivate", apiRagdollActivate); // 3.00
lua_register(L, "ragdollApplyImpulse", apiRagdollApplyImpulse); // 3.00
lua_register(L, "ragdollDeactivate", apiRagdollDeactivate); // 3.00
lua_register(L, "ragdollDelete", apiRagdollDelete); // 3.00
lua_register(L, "ragdollIsActive", apiRagdollIsActive); // 3.00
lua_register(L, "ragdollIsResting", apiRagdollIsResting); // 3.00
lua_register(L, "ragdollNew", apiRagdollNew); // 3.00
lua_register(L, "ragdollSetJoint", apiRagdollSetJoint); // 3.00
lua_register(L, "ragdollSetStrength", apiRagdollSetStrength); // 3.00
lua_register(L, "ratioGetX", apiRatioGetX); // Hypseus
lua_register(L, "ratioGetY", apiRatioGetY); // Hypseus
lua_register(L, "saveClear", apiSaveClear); // 3.00
lua_register(L, "saveDelete", apiSaveDelete); // 3.00
lua_register(L, "saveFlush", apiSaveFlush); // 3.00
lua_register(L, "saveGet", apiSaveGet); // 3.00
lua_register(L, "saveGetAll", apiSaveGetAll); // 3.00
lua_register(L, "saveSet", apiSaveSet); // 3.00
lua_register(L, "saveSetAll", apiSaveSetAll); // 3.00
lua_register(L, "sceneEnable", apiSceneEnable); // 3.00
lua_register(L, "sceneGetSize", apiSceneGetSize); // 3.00
lua_register(L, "sceneGetStats", apiSceneGetStats); // 3.00
lua_register(L, "sceneMeasure", apiSceneMeasure); // 3.00
lua_register(L, "sceneProbeGui", apiSceneProbeGui); // 3.00
lua_register(L, "sceneProject", apiSceneProject); // 3.00
lua_register(L, "sceneSetAmbient", apiSceneSetAmbient); // 3.00
lua_register(L, "sceneSetAntialias", apiSceneSetAntialias); // 3.00
lua_register(L, "sceneSetBackground", apiSceneSetBackground); // 3.00
lua_register(L, "sceneSetBloom", apiSceneSetBloom); // 3.00
lua_register(L, "sceneSetEnvironment", apiSceneSetEnvironment); // 3.00
lua_register(L, "sceneSetExposure", apiSceneSetExposure); // 3.00
lua_register(L, "sceneSetFog", apiSceneSetFog); // 3.00
lua_register(L, "sceneSetShadowCascades", apiSceneSetShadowCascades); // 3.00
lua_register(L, "sceneSetShadowDistance", apiSceneSetShadowDistance); // 3.00
lua_register(L, "sceneSetShadowSize", apiSceneSetShadowSize); // 3.00
lua_register(L, "sceneSetSky", apiSceneSetSky); // 3.00
lua_register(L, "sceneSetSkyIntensity", apiSceneSetSkyIntensity); // 3.00
lua_register(L, "sceneSetTonemap", apiSceneSetTonemap); // 3.00
lua_register(L, "sceneUnproject", apiSceneUnproject); // 3.00
lua_register(L, "scoreBezelClear", apiScoreBezelClear); // Hypseus
lua_register(L, "scoreBezelCredits", apiScoreBezelCredits); // Hypseus
lua_register(L, "scoreBezelEnable", apiScoreBezelEnable); // Hypseus
lua_register(L, "scoreBezelGetState", apiScoreBezelGetState); // Hypseus
lua_register(L, "scoreBezelLives", apiScoreBezelLives); // Hypseus
lua_register(L, "scoreBezelScore", apiScoreBezelScore); // Hypseus
lua_register(L, "scoreBezelTwinScoreOn", apiScoreBezelTwinScoreOn); // Hypseus
lua_register(L, "scriptExecute", apiScriptExecute); // 2.00
lua_register(L, "scriptPush", apiScriptPush); // 2.00
lua_register(L, "setOverlayFullAlpha", apiSetOverlayFullAlpha); // Hypseus
lua_register(L, "setOverlayLinearScale", apiSetOverlayLinearScale); // Hypseus
lua_register(L, "setOverlayOnTop", apiSetOverlayOnTop); // Hypseus
lua_register(L, "setOverlayOpacity", apiSetOverlayOpacity); // Hypseus
lua_register(L, "singeGetAudioCalibration", apiSingeGetAudioCalibration); // 3.00
lua_register(L, "singeGetAudioDelay", apiSingeGetAudioDelay); // 3.00
lua_register(L, "singeGetAudioLatency", apiSingeGetAudioLatency); // 3.00
lua_register(L, "singeCapabilities", apiSingeCapabilities); // 3.00
lua_register(L, "singeGetDataPath", apiSingeGetDataPath); // 2.00
lua_register(L, "singeGetGameId", apiSingeGetGameId); // 3.00
lua_register(L, "singeGetHeight", apiSingeGetHeight); // 1.xx
lua_register(L, "singeGetSystemInfo", apiSingeGetSystemInfo); // 3.00
lua_register(L, "singeGetPauseFlag", apiSingeGetPauseFlag); // 1.xx RDG
lua_register(L, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG
lua_register(L, "singeGetTicks", apiSingeGetTicks); // 3.00
lua_register(L, "singeGetWidth", apiSingeGetWidth); // 1.xx
lua_register(L, "singeHasGpu", apiSingeHasGpu); // 3.00
lua_register(L, "singeIsRecording", apiSingeIsRecording); // 3.00
lua_register(L, "singePack", apiSingePack); // 3.00
lua_register(L, "singeQuit", apiSingeQuit); // 1.xx RDG
lua_register(L, "singeRecordStart", apiSingeRecordStart); // 3.00
lua_register(L, "singeRecordStop", apiSingeRecordStop); // 3.00
lua_register(L, "singeRelaunch", apiSingeRelaunch); // 3.00
lua_register(L, "singeReload", apiSingeReload); // 3.00
lua_register(L, "singeSaveGeometry", apiSingeSaveGeometry); // 3.00
lua_register(L, "singeSaveTier", apiSingeSaveTier); // 3.00
lua_register(L, "singeScreenshot", apiSingeScreenshot); // 1.xx
lua_register(L, "singeSetAudioCalibration", apiSingeSetAudioCalibration); // 3.00
lua_register(L, "singeSetAudioDelay", apiSingeSetAudioDelay); // 3.00
lua_register(L, "singeSetGameName", apiSingeSetGameName); // 1.15 RDG
lua_register(L, "singeSetPauseFlag", apiSingeSetPauseFlag); // 1.xx RDG
lua_register(L, "singeSetPauseKeyEnabled", apiSingeSetPauseKeyEnabled); // 3.00 singeEnablePauseKey/singeDisablePauseKey are framework aliases.
lua_register(L, "singeSetQuitKeyEnabled", apiSingeSetQuitKeyEnabled); // Hypseus keyboardCatchQuit is a framework alias with the opposite sense.
lua_register(L, "singeVersion", apiSingeVersion); // 1.xx RDG
lua_register(L, "singeWantsCrosshairs", apiSingeWantsCrosshairs); // 2.00
lua_register(L, "utilSha256", apiUtilSha256); // 3.00
lua_register(L, "softDelete", apiSoftDelete); // 3.00
lua_register(L, "softNew", apiSoftNew); // 3.00
lua_register(L, "softPin", apiSoftPin); // 3.00
lua_register(L, "softSetDamping", apiSoftSetDamping); // 3.00
lua_register(L, "softSetMass", apiSoftSetMass); // 3.00
lua_register(L, "softSetPressure", apiSoftSetPressure); // 3.00
lua_register(L, "softSetStiffness", apiSoftSetStiffness); // 3.00
lua_register(L, "softUnpin", apiSoftUnpin); // 3.00
lua_register(L, "soundFullStop", apiSoundFullStop); // 1.16
lua_register(L, "soundGetPosition", apiSoundGetPosition); // 3.00
lua_register(L, "soundGetVolume", apiSoundGetVolume); // 1.16
lua_register(L, "soundIsPlaying", apiSoundIsPlaying); // 1.16 RDG
lua_register(L, "soundLoad", apiSoundLoad); // 1.xx
lua_register(L, "soundLoadData", apiSoundLoadData); // Hypseus
lua_register(L, "soundPause", apiSoundPause); // 1.16 RDG
lua_register(L, "soundPlay", apiSoundPlay); // 1.xx
lua_register(L, "soundResume", apiSoundResume); // 1.16 RDG
lua_register(L, "soundSetChannelVolume", apiSoundSetChannelVolume); // 3.00
lua_register(L, "soundSetListener", apiSoundSetListener); // 3.00
lua_register(L, "soundSetNode", apiSoundSetNode); // 3.00
lua_register(L, "soundSetPan", apiSoundSetPan); // 3.00
lua_register(L, "soundSetPosition", apiSoundSetPosition); // 3.00
lua_register(L, "soundSetRange", apiSoundSetRange); // 3.00
lua_register(L, "soundSetVolume", apiSoundSetVolume); // 1.16
lua_register(L, "soundStop", apiSoundStop); // 1.xx RDG
lua_register(L, "soundUnload", apiSoundUnload); // 2.00
lua_register(L, "spriteBlend", apiSpriteBlend); // Hypseus
lua_register(L, "spriteDraw", apiSpriteDraw); // 1.xx Handle first since 3.00.
lua_register(L, "spriteDrawFrame", apiSpriteDrawFrame); // Hypseus
lua_register(L, "spriteDrawGrid", apiSpriteDrawGrid); // Hypseus
lua_register(L, "spriteDrawRotatedFrame", apiSpriteDrawRotatedFrame); // Hypseus Handle first since 3.00.
lua_register(L, "spriteFlip", apiSpriteFlip); // 3.00
lua_register(L, "spriteFrameHeight", apiSpriteFrameHeight); // Hypseus
lua_register(L, "spriteFrameWidth", apiSpriteFrameWidth); // Hypseus
lua_register(L, "spriteGetFrame", apiSpriteGetFrame); // 2.10
lua_register(L, "spriteGetFrames", apiSpriteGetFrames); // Hypseus
lua_register(L, "spriteGetHeight", apiSpriteGetHeight); // 2.00
lua_register(L, "spriteGetWidth", apiSpriteGetWidth); // 2.00
lua_register(L, "spriteIsPlaying", apiSpriteIsPlaying); // 2.10
lua_register(L, "spriteLoad", apiSpriteLoad); // 1.xx
lua_register(L, "spriteLoadData", apiSpriteLoadData); // Hypseus
lua_register(L, "spriteLoadFrames", apiSpriteLoadFrames); // Hypseus
lua_register(L, "spriteLoop", apiSpriteLoop); // 2.10 Handle first since 3.00.
lua_register(L, "spriteNew", apiSpriteNew); // 3.00
lua_register(L, "spritePause", apiSpritePause); // 2.10
lua_register(L, "spritePlay", apiSpritePlay); // 2.10
lua_register(L, "spriteQuality", apiSpriteQuality); // 2.10 Handle first since 3.00.
lua_register(L, "spriteResetColorKey", apiSpriteResetColorKey); // Hypseus
lua_register(L, "spriteRotate", apiSpriteRotate); // 2.10 Handle first since 3.00.
lua_register(L, "spriteRotateAndScale", apiSpriteRotateAndScale); // 2.10 Handle first since 3.00.
lua_register(L, "spriteRotateFrame", apiSpriteRotateFrame); // Hypseus Handle first since 3.00.
lua_register(L, "spriteScale", apiSpriteScale); // 2.10 Handle first since 3.00.
lua_register(L, "spriteSetAlpha", apiSpriteSetAlpha); // 3.00
lua_register(L, "spriteSetBlend", apiSpriteSetBlend); // 3.00
lua_register(L, "spriteSetFrame", apiSpriteSetFrame); // 2.10 Handle first since 3.00.
lua_register(L, "spriteSetPixels", apiSpriteSetPixels); // 3.00
lua_register(L, "spriteSetTint", apiSpriteSetTint); // 3.00
lua_register(L, "spriteUnload", apiSpriteUnload); // 2.00
lua_register(L, "srtClear", apiSrtClear); // Hypseus
lua_register(L, "srtEnable", apiSrtEnable); // Hypseus
lua_register(L, "srtLoad", apiSrtLoad); // Hypseus
lua_register(L, "srtLoadTrack", apiSrtLoadTrack); // 3.00
lua_register(L, "srtPosition", apiSrtPosition); // Hypseus
lua_register(L, "statsEnable", apiStatsEnable); // 3.00
lua_register(L, "statsIsEnabled", apiStatsIsEnabled); // 3.00
lua_register(L, "terrainGetHeight", apiTerrainGetHeight); // 3.00
lua_register(L, "timerAfter", apiTimerAfter); // 3.00
lua_register(L, "timerCancel", apiTimerCancel); // 3.00
lua_register(L, "timerEvery", apiTimerEvery); // 3.00
lua_register(L, "timerIsActive", apiTimerIsActive); // 3.00
lua_register(L, "tweenCancel", apiTweenCancel); // 3.00
lua_register(L, "tweenIsActive", apiTweenIsActive); // 3.00
lua_register(L, "tweenValue", apiTweenValue); // 3.00
lua_register(L, "vehicleAddWheel", apiVehicleAddWheel); // 3.00
lua_register(L, "vehicleDelete", apiVehicleDelete); // 3.00
lua_register(L, "vehicleDrive", apiVehicleDrive); // 3.00
lua_register(L, "vehicleGetGear", apiVehicleGetGear); // 3.00
lua_register(L, "vehicleGetRpm", apiVehicleGetRpm); // 3.00
lua_register(L, "vehicleGetSpeed", apiVehicleGetSpeed); // 3.00
lua_register(L, "vehicleGetWheelSlip", apiVehicleGetWheelSlip); // 3.00
lua_register(L, "vehicleIsWheelOnGround", apiVehicleIsWheelOnGround); // 3.00
lua_register(L, "vehicleNew", apiVehicleNew); // 3.00
lua_register(L, "vehicleSetAntiRoll", apiVehicleSetAntiRoll); // 3.00
lua_register(L, "vehicleSetBrakes", apiVehicleSetBrakes); // 3.00
lua_register(L, "vehicleSetEngine", apiVehicleSetEngine); // 3.00
lua_register(L, "vehicleSetGears", apiVehicleSetGears); // 3.00
lua_register(L, "vehicleSetSteering", apiVehicleSetSteering); // 3.00
lua_register(L, "vehicleSetRudder", apiVehicleSetRudder); // 3.00
lua_register(L, "vehicleSetSuspension", apiVehicleSetSuspension); // 3.00
lua_register(L, "vehicleSetThrust", apiVehicleSetThrust); // 3.00
lua_register(L, "vehicleSetWheel", apiVehicleSetWheel); // 3.00
lua_register(L, "videoDraw", apiVideoDraw); // 2.00
lua_register(L, "videoGetAudioTrack", apiVideoGetAudioTrack); // 2.10
lua_register(L, "videoGetAudioTracks", apiVideoGetAudioTracks); // 2.10
lua_register(L, "videoGetFrame", apiVideoGetFrame); // 2.00
lua_register(L, "videoGetFrameCount", apiVideoGetFrameCount); // 2.00
lua_register(L, "videoGetHeight", apiVideoGetHeight); // 2.00
lua_register(L, "videoGetLanguage", apiVideoGetLanguage); // 2.10
lua_register(L, "videoGetLanguageDescription", apiVideoGetLanguageDescription); // 2.10
lua_register(L, "videoGetVolume", apiVideoGetVolume); // 2.00
lua_register(L, "videoGetWidth", apiVideoGetWidth); // 2.00
lua_register(L, "videoIsPlaying", apiVideoIsPlaying); // 2.00
lua_register(L, "videoLoad", apiVideoLoad); // 2.00
lua_register(L, "videoPause", apiVideoPause); // 2.00
lua_register(L, "videoPlay", apiVideoPlay); // 2.00
lua_register(L, "videoQuality", apiVideoQuality); // 2.10
lua_register(L, "videoRotate", apiVideoRotate); // 2.10
lua_register(L, "videoRotateAndScale", apiVideoRotateAndScale); // 2.10
lua_register(L, "videoScale", apiVideoScale); // 2.10
lua_register(L, "videoSeek", apiVideoSeek); // 2.00
lua_register(L, "videoSetAudioTrack", apiVideoSetAudioTrack); // 2.10
lua_register(L, "videoSetVolume", apiVideoSetVolume); // 2.00
lua_register(L, "videoUnload", apiVideoUnload); // 2.00
lua_register(L, "viewDelete", apiViewDelete); // 3.00
lua_register(L, "viewNew", apiViewNew); // 3.00
lua_register(L, "viewSetCamera", apiViewSetCamera); // 3.00
lua_register(L, "vldpFlash", apiVldpFlash); // Hypseus
lua_register(L, "vldpFocusArea", apiVldpFocusArea); // Hypseus
lua_register(L, "vldpGetHeight", apiDiscGetHeight); // 1.xx Same as discGetHeight.
lua_register(L, "vldpGetPixel", apiVldpGetPixel); // 1.xx
lua_register(L, "vldpGetRotate", apiVldpGetRotate); // Hypseus
lua_register(L, "vldpGetScale", apiVldpGetScale); // Hypseus
lua_register(L, "vldpGetShift", apiVldpGetShift); // 3.00
lua_register(L, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth.
lua_register(L, "vldpGetYUVPixel", apiVldpGetYUVPixel); // Hypseus
lua_register(L, "vldpResetFocus", apiVldpResetFocus); // Hypseus
lua_register(L, "vldpSetBlend", apiVldpSetBlend); // Hypseus
lua_register(L, "vldpSetLuma", apiVldpSetLuma); // Hypseus
lua_register(L, "vldpSetMonochrome", apiVldpSetMonochrome); // Hypseus
lua_register(L, "vldpSetRotate", apiVldpSetRotate); // Hypseus
lua_register(L, "vldpSetScale", apiVldpSetScale); // Hypseus
lua_register(L, "vldpSetShift", apiVldpSetShift); // 3.00
lua_register(L, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx
lua_register(L, "voiceFree", apiVoiceFree); // 3.00
lua_register(L, "voiceProcess", apiVoiceProcess); // 3.00
lua_register(L, "voiceProcessorNew", apiVoiceProcessorNew); // 3.00
}
// Releases whatever direction code an axis was holding.
static void _releaseAxis(int32_t axisIndex) {
if (_global.axisCode[axisIndex] != 0) {
_processKey(false, 0, _global.axisCode[axisIndex]);
_global.axisCode[axisIndex] = 0;
}
}
// Runs the game again from its script without leaving: every script-owned thing goes (sounds,
// the Lua state, fonts, sprites, videos, the scene, physics, navigation, particles, the overlay
// back to its default size), the engine (window, GPU device, disc, controllers) stays, and the
// script runs afresh. An error in it is traced and the game sits empty until the next reload.
static void _reloadScript(void) {
_progTrace("Reloading %s", _global.conf->scriptFile);
_global.reloadRequested = false;
// The save table lives in the Lua state that is about to be closed, so anything set since the
// last end of frame is written now rather than lost.
_saveWrite();
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
MIX_StopTag(videoGetMixer(), MUSIC_TAG, 0);
guiDetachLua();
lua_close(_global.luaContext);
_global.luaContext = NULL;
_unloadScriptResources();
_subsystemsQuit();
_subsystemsInit();
_overlayResize((int32_t)(_global.canvasWidth * OVERLAY_SCALE_DEFAULT), (int32_t)(_global.canvasHeight * OVERLAY_SCALE_DEFAULT));
_resetScriptState();
_createScriptContext();
guiAttachLua(_global.luaContext);
_runScript(false);
_global.refreshDisplay = true;
}
// Renders text with the current font, quality, and colors.
static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message) {
SDL_Surface *surface = NULL;
if (_global.fontCurrent == NULL) {
_luaDie(L, method, "No font selected.");
}
switch (_global.fontQuality) {
case FONT_QUALITY_SOLID:
surface = TTF_RenderText_Solid(_global.fontCurrent->font, message, 0, _global.colorForeground);
break;
case FONT_QUALITY_SHADED:
surface = TTF_RenderText_Shaded(_global.fontCurrent->font, message, 0, _global.colorForeground, _global.colorBackground);
break;
case FONT_QUALITY_BLENDED:
surface = TTF_RenderText_Blended(_global.fontCurrent->font, message, 0, _global.colorForeground);
break;
default:
_luaDie(L, method, "Unknown font quality!");
}
if (!surface) {
_luaDie(L, method, "%s", SDL_GetError());
}
_surfaceUnpack(&surface);
_surfaceKey(surface);
return surface;
}
// _renderText through the cache, for fontPrint. The surface stays the cache's: blit it, never free it.
static SDL_Surface *_renderTextCached(lua_State *L, const char *method, const char *message) {
TextCacheT *entry = NULL;
TextCacheT *entryTemp = NULL;
SDL_Surface *surface = NULL;
char *grown = NULL;
size_t bytes = strlen(message) + TEXT_CACHE_KEY_HEADER;
uint64_t now = utilTicks();
SDL_Color foreground = _global.colorForeground;
SDL_Color background = { 0, 0, 0, 0 };
if (_global.fontCurrent == NULL) {
return _renderText(L, method, message);
}
if (now - _global.textCacheSweepMS >= TEXT_CACHE_KEEP_MS) {
HASH_ITER(hh, _global.textCache, entry, entryTemp) {
if (entry->usedMS < _global.textCacheSweepMS) {
_textCacheRemove(entry);
}
}
_global.textCacheSweepMS = now;
}
if (bytes > _global.textCacheKeyBytes) {
grown = realloc(_global.textCacheKey, bytes);
if (grown == NULL) {
utilDie("Out of memory in %s.", method);
}
_global.textCacheKey = grown;
_global.textCacheKeyBytes = bytes;
}
// Only shaded text has a background.
if (_global.fontQuality == FONT_QUALITY_SHADED) {
background = _global.colorBackground;
}
snprintf(_global.textCacheKey, _global.textCacheKeyBytes, "%d %d %d %02x%02x%02x%02x %02x%02x%02x%02x %s", _global.fontCurrent->id, (int32_t)_global.fontQuality, (int32_t)_global.spriteKeyed, foreground.r, foreground.g, foreground.b, foreground.a, background.r, background.g, background.b, background.a, message);
HASH_FIND_STR(_global.textCache, _global.textCacheKey, entry);
if (entry == NULL) {
// Rendered first, so a font that fails raises its Lua error with nothing half added.
surface = _renderText(L, method, message);
entry = calloc(1, sizeof(TextCacheT));
if (entry == NULL) {
utilDie("Out of memory in %s.", method);
}
entry->key = strdup(_global.textCacheKey);
entry->surface = surface;
entry->font = _global.fontCurrent->id;
// Run length encoded, since it will be blitted with its colour key again and again.
SDL_SetSurfaceRLE(entry->surface, true);
HASH_ADD_KEYPTR(hh, _global.textCache, entry->key, strlen(entry->key), entry);
}
entry->usedMS = now;
return entry->surface;
}
// Everything a script sets about itself, back to what a script may expect at its start: colours,
// font quality, keyboard mode, listener, pause, the effect channels, their volume and their
// completion queue, the mouse (mode, enabled, captured), the disc parked on frame 1, what the
// previous script believed was held, and --reload's file list. Keys physically down now were held
// over from before this script, so they are ignored until released. Nothing here calls into Lua,
// so it is safe between one state closing and the next opening.
static void _resetScriptState(void) {
int32_t finished[SOUND_QUEUE_SIZE];
const char *suffix = NULL;
int32_t x = 0;
_global.guiDrawCount = 0;
_global.guiShownCount = 0;
_global.guiHover = GUI_NO_HANDLE;
_scorePanelReset();
_subtitleReset();
_scheduleReset(_global.luaContext);
if (_global.textInputOn) {
SDL_StopTextInput(_global.window);
_global.textInputOn = false;
_global.guiTextActive = false;
}
for (x = 0; x < EFFECT_TRACKS; x++) {
_effectReset(x);
}
_soundQueueDrain(finished);
_global.effectsVolume = _defaultVolume(AUDIO_MAX_VOLUME);
for (x = 0; x < EFFECT_TRACKS; x++) {
_effectApplyGain(x);
}
// Music starts as loud as the sound effects, on its own scale and with its own gain, so
// musicSetVolume and soundSetVolume never move each other.
_global.musicVolume = _defaultVolume(MUSIC_MAX_VOLUME);
MIX_SetTagGain(videoGetMixer(), MUSIC_TAG, _mixerGain(_global.musicVolume, MUSIC_MAX_VOLUME));
_global.mouseMode = (_global.conf->manyMouse) ? MOUSE_MANY : MOUSE_SINGLE;
_global.mouseEnabled = !_global.conf->noMouse && (_global.mouseCount > 0);
// --joymouse and the cursor it drives are the command line's again, as the scale and the
// rotation are, so a reload starts where the user put them.
_global.joyMouseEnabled = _global.conf->joyMouse;
_global.joyMouseX = _global.canvasWidth / 2;
_global.joyMouseY = _global.canvasHeight / 2;
_global.joyMouseClock = 0;
_setMouseCaptured(true);
if (_global.videoHandle >= 0) {
_discPark();
}
_global.colorForeground.r = SDL_ALPHA_OPAQUE;
_global.colorForeground.g = SDL_ALPHA_OPAQUE;
_global.colorForeground.b = SDL_ALPHA_OPAQUE;
_global.colorForeground.a = SDL_ALPHA_OPAQUE;
memset(&_global.colorBackground, 0, sizeof(_global.colorBackground));
_global.fontQuality = FONT_QUALITY_SOLID;
_global.spriteKeyed = true;
_global.keyboardMode = KEYBOARD_NORMAL;
_global.listenerNode = LISTENER_CAMERA;
_global.pauseEnabled = true;
_global.quitEnabled = true;
_global.frozen = false;
_global.focused = false;
_global.overlayMonochrome = false;
_global.overlayOnTop = false;
_global.overlayLinearScale = _global.conf->linearScale;
_global.overlayOpacity = COLOR_BYTE_MAX;
_overlayApplyScaleMode();
_overlayApplyOpacity();
// vldpSetScale and vldpSetRotate are the script's, so the next script starts from the command line again.
_global.videoScale = _global.conf->scaleFactor;
_global.videoShiftX = _global.conf->shiftX;
_global.videoShiftY = _global.conf->shiftY;
_global.videoScaleClock = 0;
if (_global.canvasWidth > 0) {
_setVideoRotate(_global.conf->rotate);
_computeVideoRect();
}
// The disc picture controls are the script's too, so --monochrome is where a reload starts.
_global.discMonochrome = _global.conf->monochrome;
_global.discBlend = false;
_global.discLumaOn = false;
_global.discLumaLevel = LUMA_LEVEL_NEUTRAL;
_discApplyPicture();
if (_global.videoHandle >= 0) {
// The suffix goes back to --altaudio's, not to nothing, so a reload starts where the
// command line put it just as the scale and the rotation do.
suffix = (_global.discAudioSuffix != NULL) ? _global.discAudioSuffix : "";
if (strcmp(suffix, _startAudioSuffix()) != 0) {
if (_discSetAudioSuffix(_startAudioSuffix())) {
_progTrace("Disc audio suffix is \"%s\"", _startAudioSuffix());
} else {
utilSay("Warning: there is no disc audio named for --altaudio %s; the game's own audio is playing.", _startAudioSuffix());
}
}
}
if (_global.pauseState) {
_global.pauseState = false;
_updatePauseState();
}
memset(_global.switchHeld, 0, sizeof(_global.switchHeld));
memset(_global.axisCode, 0, sizeof(_global.axisCode));
_global.heldKeyCount = 0;
_global.physicalKeyCount = 0;
_global.keyboardLastDown = SDL_SCANCODE_UNKNOWN;
_global.keyboardLastUp = SDL_SCANCODE_UNKNOWN;
_suppressHeldInput();
for (x = 0; x < _global.watchedCount; x++) {
free(_global.watched[x].name);
}
free(_global.watched);
_global.watched = NULL;
_global.watchedCount = 0;
}
// Text into the document as text and not as markup: the three characters RML would take for markup
// are escaped, and a line break becomes one. Truncates rather than overrun the caller's buffer.
static void _rmlEscape(char *out, size_t size, const char *text) {
size_t room = 0;
size_t used = 0;
size_t x = 0;
for (x = 0; (text != NULL) && (text[x] != '\0'); x++) {
const char *piece = NULL;
char one[2];
switch (text[x]) {
case '&':
piece = "&amp;";
break;
case '<':
piece = "&lt;";
break;
case '>':
piece = "&gt;";
break;
case '\n':
piece = "<br/>";
break;
default:
one[0] = text[x];
one[1] = '\0';
piece = one;
break;
}
room = strlen(piece);
if ((used + room + 1) > size) {
break;
}
memcpy(out + used, piece, room);
used += room;
}
out[used] = '\0';
}
// Loads and runs the game script under the traceback handler. A failure is fatal when the game
// starts; on a reload it is reported and the game sits empty until the next one.
static void _runScript(bool fatal) {
_progTrace("Running %s", _global.conf->scriptFile);
lua_pushcfunction(_global.luaContext, _luaTraceback);
if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL, _global.conf->reload) || scriptWatchCall(_global.luaContext, 0, 0, -2)) {
if (fatal) {
utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1));
}
utilSay("Error running script: %s", lua_tostring(_global.luaContext, -1));
}
lua_settop(_global.luaContext, 0);
}
// True when two loose file names name the same file: separators unified, leading "./" ignored.
static bool _sameLoosePath(const char *a, const char *b) {
char *left = strdup(a);
char *right = strdup(b);
bool same = false;
utilFixPathSeparators(&left, false);
utilFixPathSeparators(&right, false);
same = (utilStricmp(_skipDotSlash(left), _skipDotSlash(right)) == 0);
free(left);
free(right);
return same;
}
// A value a script wants kept. Tables are allowed because a save is usually a table of them; a
// function or a handle to something the engine owns is not, because neither means anything on the
// next run.
static void _saveCheckValue(lua_State *L, const char *method, int32_t index) {
int32_t type = lua_type(L, index);
if ((type != LUA_TNUMBER) && (type != LUA_TSTRING) && (type != LUA_TBOOLEAN) && (type != LUA_TTABLE)) {
_luaDie(L, method, "A saved value is a number, a string, a boolean or a table of them, not a %s.", lua_typename(L, type));
}
}
// Pushes one of cjson's functions. The save file is JSON because a person should be able to read
// and fix one, and cjson is already bundled and already knows every corner of the format.
static bool _saveCjson(lua_State *L, const char *name) {
lua_getglobal(L, "require");
lua_pushstring(L, "cjson");
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
lua_pop(L, 1);
return false;
}
lua_getfield(L, -1, name);
lua_remove(L, -2);
if (!lua_isfunction(L, -1)) {
lua_pop(L, 1);
return false;
}
return true;
}
// A deep copy of the value at "index", pushed on the stack. Everything going into the save and
// everything coming out of it is copied, so the table the engine keeps is its own: what a script
// sets is what is saved, and what it gets back is its own to change. Sharing the table instead
// made an edit through saveGet or saveGetAll persist or not depending on whether something unrelated
// saved later in the same run, which is not a rule anyone could hold in their head.
static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth) {
int32_t source = lua_absindex(L, index);
if (!lua_istable(L, source)) {
lua_pushvalue(L, source);
return;
}
if (depth >= SAVE_DEPTH_MAX) {
_luaDie(L, method, "A saved table is nested more than %d deep, or refers to itself.", SAVE_DEPTH_MAX);
}
lua_newtable(L);
lua_pushnil(L);
while (lua_next(L, source) != 0) {
// The table, its key and its value are on the stack; the copy of the value goes beside
// them, then the key is put back on top so the pair can be written into the new table.
_saveCopy(L, -1, method, depth + 1);
lua_pushvalue(L, -3);
lua_insert(L, -2);
lua_rawset(L, -5);
lua_pop(L, 1);
}
}
static void _saveMachineSettings(int32_t milliseconds) {
char *path = _machineFilePath();
FILE *out = fopen(path, "w");
// Everything the service tools can set for this machine goes in one file, and it is written
// whole: the geometry comes from what is live now, which is what the operator just adjusted.
if (out) {
fprintf(out, "delay = %d\n", milliseconds);
fprintf(out, "scale = %d\n", _global.videoScale);
fprintf(out, "shiftx = %d\n", _global.videoShiftX);
fprintf(out, "shifty = %d\n", _global.videoShiftY);
fprintf(out, "rotate = %d\n", _global.videoRotate);
if (_global.machineTier != TIER_UNMEASURED) {
fprintf(out, "tier = %d\n", _global.machineTier);
}
if (micLatency() != MIC_LATENCY_UNKNOWN) {
fprintf(out, "mic = %d\n", micLatency());
}
fclose(out);
} else {
utilSay("Unable to write %s", path);
}
free(path);
}
// JSON has one kind of number and Lua 5.4 has two, so everything comes back from a decode as a
// float: a score saved as 100 would return 100.0 and print with a decimal point. Whole numbers are
// put back to integers as they are read, which is what the game that saved them had. Recursive,
// because a save is usually a table of tables, with a depth limit for a file somebody hand edited
// into a spiral.
static void _saveNormalise(lua_State *L, int32_t depth) {
lua_Number number = 0;
lua_Integer whole = 0;
if (depth > SAVE_DEPTH_MAX) {
return;
}
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_istable(L, -1)) {
_saveNormalise(L, depth + 1);
} else if (lua_isnumber(L, -1) && !lua_isinteger(L, -1)) {
number = lua_tonumber(L, -1);
whole = (lua_Integer)number;
if ((lua_Number)whole == number) {
// The key is still below the value, so the value is replaced in place rather than
// set again, which would disturb the traversal.
lua_pop(L, 1);
lua_pushinteger(L, whole);
lua_pushvalue(L, -2);
lua_pushvalue(L, -2);
lua_rawset(L, -5);
}
}
lua_pop(L, 1);
}
}
// The save table, read from the file the first time anything asks for it. Always leaves a table on
// the stack: a save that will not parse is reported and then treated as an empty one, because a
// game that cannot start is worse than a game that lost its high scores.
static void _saveTable(lua_State *L) {
char *text = NULL;
lua_getfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
if (lua_istable(L, -1)) {
return;
}
lua_pop(L, 1);
lua_newtable(L);
text = persistRead();
if (text != NULL) {
if (_saveCjson(L, "decode")) {
lua_pushstring(L, text);
if ((lua_pcall(L, 1, 1, 0) == LUA_OK) && lua_istable(L, -1)) {
lua_remove(L, -2);
_saveNormalise(L, 0);
} else {
utilSay("Warning: %s could not be read; starting with an empty save.", persistPath());
lua_pop(L, 1);
}
}
free(text);
}
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
}
// Writes the save table out. Called at the end of a frame that changed it, and at shutdown.
static void _saveWrite(void) {
lua_State *L = _global.luaContext;
const char *text = NULL;
size_t size = 0;
if ((L == NULL) || !_global.saveDirty) {
return;
}
_global.saveDirty = false;
if (!_saveCjson(L, "encode")) {
utilSay("Warning: the save file needs the cjson module, which did not load.");
return;
}
_saveTable(L);
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
utilSay("Warning: the save could not be turned into JSON: %s", lua_tostring(L, -1));
lua_pop(L, 1);
return;
}
text = lua_tolstring(L, -1, &size);
if ((text != NULL) && !persistWrite(text, size)) {
utilSay("Warning: %s could not be written.", persistPath());
}
lua_pop(L, 1);
}
// A player's current frame for the 3D scene's video materials: the disc's texture is already
// updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless).
static SDL_Texture *_sceneVideoSource(int32_t player) {
VideoT *video;
if (player == PLAYER_DISC) {
return (_global.videoHandle >= 0) ? _global.videoTexture : NULL;
}
for (video = _global.videoList; video != NULL; video = video->hh.next) {
if (video->handle == player) {
videoUpdate(video->handle, &video->texture);
return video->texture;
}
}
return NULL;
}
// A timer or a tween has fired. Called from the frame loop by way of the scheduler, never from
// another thread, so this is an ordinary Lua call.
static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished) {
lua_State *L = _global.luaContext;
int32_t handler;
int32_t slot = (finished && (kind == SCHEDULER_TWEEN)) ? SCHEDULE_DONE : SCHEDULE_UPDATE;
(void)context;
if (L == NULL) {
return;
}
if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS)) {
lua_pop(L, 1);
return;
}
lua_rawgeti(L, -1, handle);
lua_remove(L, -2);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return;
}
// A finished tween calls its update one last time at the end value, and then the optional
// second function; a game that only wants the end result gives just that one.
if ((slot == SCHEDULE_DONE) && (kind == SCHEDULER_TWEEN)) {
lua_rawgeti(L, -1, SCHEDULE_UPDATE);
if (lua_isfunction(L, -1)) {
lua_pushcfunction(L, _luaTraceback);
lua_insert(L, -2);
handler = lua_gettop(L) - 1;
lua_pushnumber(L, value);
lua_pushnumber(L, progress);
if (scriptWatchCall(L, 2, 0, handler) != LUA_OK) {
utilDie("Error in a tween: %s", lua_tostring(L, -1));
}
lua_remove(L, handler);
} else {
lua_pop(L, 1);
}
}
lua_rawgeti(L, -1, slot);
if (!lua_isfunction(L, -1)) {
lua_pop(L, 2);
return;
}
lua_pushcfunction(L, _luaTraceback);
lua_insert(L, -2);
handler = lua_gettop(L) - 1;
if (kind == SCHEDULER_TWEEN) {
lua_pushnumber(L, value);
lua_pushnumber(L, progress);
if (scriptWatchCall(L, 2, 0, handler) != LUA_OK) {
utilDie("Error in a tween: %s", lua_tostring(L, -1));
}
} else {
lua_pushinteger(L, handle);
if (scriptWatchCall(L, 1, 0, handler) != LUA_OK) {
utilDie("Error in a timer: %s", lua_tostring(L, -1));
}
}
lua_remove(L, handler);
lua_pop(L, 1);
if (finished) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_pushnil(L);
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
}
}
// Remembers the functions a timer or tween will call, under its handle.
static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_newtable(L);
lua_pushvalue(L, updateIndex);
lua_rawseti(L, -2, SCHEDULE_UPDATE);
if ((doneIndex > 0) && lua_isfunction(L, doneIndex)) {
lua_pushvalue(L, doneIndex);
lua_rawseti(L, -2, SCHEDULE_DONE);
}
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
}
// Everything a timer or tween left behind, for a script reload.
static void _scheduleReset(lua_State *L) {
schedulerReset();
if (L == NULL) {
return;
}
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
}
// One readout's digits: the value right justified in the places that readout has, with leading
// zeros the way an arcade board shows them, the symbol Hypseus puts on every digit for each of its
// negative codes, or nothing at all while the panel is blanked.
static void _scorePanelDigits(char *text, size_t size, int32_t value, int32_t digits, bool blank) {
int32_t limit = 1;
int32_t i = 0;
char symbol = 0;
text[0] = 0;
if (blank) {
return;
}
if (value < 0) {
switch (value) {
case SCORE_CODE_DASH:
symbol = '-';
break;
case SCORE_CODE_H:
symbol = 'H';
break;
case SCORE_CODE_A:
symbol = 'A';
break;
// SCORE_CODE_OFF and SCORE_CODE_BLANK light no segments at all, and so does any other
// negative number, which Hypseus shows as a zero it never lights either.
default:
return;
}
for (i = 0; (i < digits) && ((size_t)i < size - 1); i++) {
text[i] = symbol;
}
text[i] = 0;
return;
}
for (i = 0; i < digits; i++) {
limit *= 10;
}
snprintf(text, size, "%0*d", digits, value % limit);
}
// scoreBezelEnable: builds the panel from the shipped document the first time a game asks for it,
// and puts it away when a game asks for none. A machine with no GPU has no GUIs at all, so the
// panel stays off and scoreBezelGetState answers false, which sends the game down the branch it
// already has for a scoreboard it did not get.
static bool _scorePanelEnable(bool enabled) {
char markup[GUI_VALUE_MAX];
_global.scoreEnabled = false;
if (!enabled) {
_global.refreshDisplay = true;
return false;
}
if (!guiValid(_global.scoreGui)) {
_global.scoreGui = guiNew(SCORE_PANEL_WIDTH, SCORE_PANEL_HEIGHT);
if (_global.scoreGui == GUI_NO_HANDLE) {
utilSay("Warning: No score panel: %s", guiLastError());
return false;
}
_global.scoreDocument = guiLoad(_global.scoreGui, SCORE_PANEL_RML);
if (_global.scoreDocument == GUI_NO_HANDLE) {
utilSay("Warning: Unable to load %s: %s", SCORE_PANEL_RML, guiLastError());
guiDelete(_global.scoreGui);
_global.scoreGui = GUI_NO_HANDLE;
return false;
}
// The document ships showing the second player, so its markup is kept here and put back
// exactly as it was written when a game asks for a second score. The panel's whole
// appearance stays in the document and its style sheet.
if (guiGetValue(_global.scoreGui, _global.scoreDocument, SCORE_PANEL_PLAYER2, markup, sizeof(markup))) {
free(_global.scoreTwinMarkup);
_global.scoreTwinMarkup = strdup(markup);
}
_global.scoreTwinShown = true;
}
_global.scoreEnabled = true;
_scorePanelUpdate();
return true;
}
// The readout a player number addresses, or -1 for one the panel does not have. Hypseus ignores
// every number but 1 and 2, and ignores 2 as well until a game asks for the second score.
static int32_t _scorePanelPlayer(int32_t player) {
if ((player == 1) || ((player == 2) && _global.scoreTwin)) {
return player - 1;
}
return -1;
}
// Queues the score panel for this frame as a screen space GUI, so it draws over the whole window
// and can sit outside the picture.
static void _scorePanelQueue(void) {
GuiDrawT *draw = NULL;
if (!_global.scoreEnabled || !guiValid(_global.scoreGui) || (_global.guiDrawCount >= GUI_DRAW_MAX)) {
return;
}
draw = &_global.guiDraws[_global.guiDrawCount];
draw->gui = _global.scoreGui;
draw->whole = false;
draw->screen = true;
_scorePanelRect(&draw->dest);
_global.guiDrawCount++;
}
// Where the score panel goes, in the overlay coordinates a screen space GUI is drawn in. It takes
// the deeper of the bands the picture leaves above and below it -- the room a bezel's cutout
// usually leaves -- centred on the picture, and lies along the bottom of the picture instead when
// neither band is deep enough to hold it.
static void _scorePanelRect(SDL_FRect *dest) {
float above = _global.videoRect.y;
float below = (float)_global.canvasHeight - (_global.videoRect.y + _global.videoRect.h);
float least = (float)_global.canvasHeight / (float)SCORE_PANEL_BAND_MIN;
float most = (float)_global.canvasHeight / (float)SCORE_PANEL_BAND_MAX;
float band = 0.0f;
float top = 0.0f;
float width = 0.0f;
float height = 0.0f;
if (below >= above) {
band = below;
top = _global.videoRect.y + _global.videoRect.h;
} else {
band = above;
top = 0.0f;
}
if (band < least) {
band = SDL_min(most, _global.videoRect.h);
top = _global.videoRect.y + _global.videoRect.h - band;
}
height = SDL_min(band, most);
width = height * (float)SCORE_PANEL_WIDTH / (float)SCORE_PANEL_HEIGHT;
if (width > (float)_global.canvasWidth) {
width = (float)_global.canvasWidth;
height = width * (float)SCORE_PANEL_HEIGHT / (float)SCORE_PANEL_WIDTH;
}
dest->x = _global.videoRect.x + (_global.videoRect.w - width) / 2.0f;
dest->y = top + (band - height) / 2.0f;
dest->x = SDL_clamp(dest->x, 0.0f, (float)_global.canvasWidth - width);
dest->w = width;
dest->h = height;
// Into overlay coordinates, which is what a draw record carries.
dest->x *= (float)_global.overlay->w / (float)_global.canvasWidth;
dest->w *= (float)_global.overlay->w / (float)_global.canvasWidth;
dest->y *= (float)_global.overlay->h / (float)_global.canvasHeight;
dest->h *= (float)_global.overlay->h / (float)_global.canvasHeight;
}
// A reload takes every GUI with it, so the panel goes back to off with nothing on it and the next
// script asks for it again, exactly as it would on a fresh start.
static void _scorePanelReset(void) {
free(_global.scoreTwinMarkup);
_global.scoreTwinMarkup = NULL;
_global.scoreGui = GUI_NO_HANDLE;
_global.scoreDocument = GUI_NO_HANDLE;
_global.scoreEnabled = false;
_global.scoreTwin = false;
_global.scoreTwinShown = false;
_global.scoreBlank = false;
_global.scoreCredits = 0;
memset(_global.scoreValue, 0, sizeof(_global.scoreValue));
memset(_global.scoreLives, 0, sizeof(_global.scoreLives));
}
// The panel's numbers into its document, each padded to the digits its readout has so the dim
// "every segment lit" digits behind it line up. The second player's cell is put in or taken out
// only when that changes, since it costs a reparse of its markup.
static void _scorePanelUpdate(void) {
char text[SCORE_PANEL_TEXT_MAX];
if (!guiValid(_global.scoreGui)) {
return;
}
if (_global.scoreTwin != _global.scoreTwinShown) {
guiSetValue(_global.scoreGui, _global.scoreDocument, SCORE_PANEL_PLAYER2, (_global.scoreTwin && (_global.scoreTwinMarkup != NULL)) ? _global.scoreTwinMarkup : "");
_global.scoreTwinShown = _global.scoreTwin;
}
_scorePanelDigits(text, sizeof(text), _global.scoreCredits, SCORE_PANEL_CREDIT_DIGITS, _global.scoreBlank);
guiSetValue(_global.scoreGui, _global.scoreDocument, "credits", text);
_scorePanelDigits(text, sizeof(text), _global.scoreValue[0], SCORE_PANEL_SCORE_DIGITS, _global.scoreBlank);
guiSetValue(_global.scoreGui, _global.scoreDocument, "score1", text);
_scorePanelDigits(text, sizeof(text), _global.scoreLives[0], SCORE_PANEL_LIVES_DIGITS, _global.scoreBlank);
guiSetValue(_global.scoreGui, _global.scoreDocument, "lives1", text);
if (_global.scoreTwin) {
_scorePanelDigits(text, sizeof(text), _global.scoreValue[1], SCORE_PANEL_SCORE_DIGITS, _global.scoreBlank);
guiSetValue(_global.scoreGui, _global.scoreDocument, "score2", text);
_scorePanelDigits(text, sizeof(text), _global.scoreLives[1], SCORE_PANEL_LIVES_DIGITS, _global.scoreBlank);
guiSetValue(_global.scoreGui, _global.scoreDocument, "lives2", text);
}
_global.refreshDisplay = true;
}
// scriptExecute and scriptPush: the games.dat style table at argument 1 as a config, with the data
// directory the launched game will write to. Caller destroys it.
static ConfigT *_scriptConfFromTable(lua_State *L, const char *method) {
ConfigT *conf = NULL;
_argCheck(L, method, 1, 1);
if (!lua_istable(L, 1)) {
_luaDie(L, method, "Argument 1 must be a table.");
}
conf = _buildConfFromTable(L, _global.conf, method);
conf->dataDir = resolveDataDir(conf);
if (conf->dataDir == NULL) {
_luaDie(L, method, "Unable to create the data directory for %s.", conf->scriptFile);
}
return conf;
}
// Applies the command line audio track to a freshly loaded video, when it has one.
static void _selectDefaultAudioTrack(int32_t handle) {
if ((_global.conf->audioOutputTrack >= 0) && (_global.conf->audioOutputTrack < videoGetAudioTracks(handle))) {
videoSetAudioTrack(handle, _global.conf->audioOutputTrack);
}
}
// Silence, and back again, without touching a single configured volume: the mixer's own gain
// carries every disc, effect and video track. --nosound holds it down for the whole run and
// --startsilent until the first input.
static void _setAudioMuted(bool muted) {
_global.audioMuted = muted;
MIX_SetMixerGain(videoGetMixer(), muted ? 0.0f : 1.0f);
}
static void _setMouseCaptured(bool captured) {
_global.mouseGrabbed = captured;
SDL_SetWindowMouseGrab(_global.window, captured);
if (captured) {
SDL_HideCursor();
} else {
SDL_ShowCursor();
}
}
// Changes the pause flag. Only the pause key freezes the script; a script that sets the flag
// itself keeps running so it can clear it again.
static void _setPause(bool paused, bool fromKey) {
_global.pauseState = paused;
_updatePauseState();
if (paused && fromKey && !_global.frozen) {
_freezeGame(true);
}
if (!paused && _global.frozen) {
_freezeGame(false);
}
}
// Records one settings file key, replacing whatever an earlier file in the search order set.
static void _settingsAdd(SettingT **list, int32_t *count, const char *key, const char *value, const char *source) {
SettingT *grown = NULL;
int32_t x = 0;
for (x = 0; x < *count; x++) {
if (strcmp((*list)[x].key, key) == 0) {
free((*list)[x].value);
free((*list)[x].source);
(*list)[x].value = strdup(value);
(*list)[x].source = strdup(source);
return;
}
}
grown = (SettingT *)realloc(*list, (size_t)(*count + 1) * sizeof(SettingT));
if (grown == NULL) {
utilDie("Out of memory reading %s.", source);
}
*list = grown;
grown[*count].key = strdup(key);
grown[*count].value = strdup(value);
grown[*count].source = strdup(source);
(*count)++;
}
// Settings come back in a fixed order whatever order Lua walked the files in, so a trace header
// reads the same twice running.
static int32_t _settingsCompare(const void *a, const void *b) {
return strcmp(((const SettingT *)a)->key, ((const SettingT *)b)->key);
}
// Runs one settings file and collects the globals it left. It runs in a state with no libraries,
// because a settings file is data and not a program, and every value is handed on as a string:
// what the option would have been given on the command line.
static void _settingsReadFile(const char *path, SettingT **list, int32_t *count) {
lua_State *L = NULL;
char *source = NULL;
const char *key = NULL;
const char *value = NULL;
size_t bytes = 0;
int32_t status = 0;
if (!vfsExists(path)) {
return;
}
source = vfsRead(path, &bytes);
if (source == NULL) {
utilDie("Unable to read the settings file %s.", path);
}
L = luaL_newstate();
if (L == NULL) {
utilDie("Out of memory reading the settings file %s.", path);
}
status = luaL_loadbuffer(L, source, bytes, path);
free(source);
if (status || lua_pcall(L, 0, 0, 0)) {
utilDie("%s", lua_tostring(L, -1));
}
lua_pushglobaltable(L);
lua_pushnil(L);
while (lua_next(L, -2)) {
// Keys must be strings; converting another kind in place would confuse lua_next. A value
// may be converted, since lua_next only needs the key back.
if (lua_type(L, -2) == LUA_TSTRING) {
key = lua_tostring(L, -2);
value = (lua_type(L, -1) == LUA_TBOOLEAN) ? (lua_toboolean(L, -1) ? "true" : "false") : lua_tostring(L, -1);
if (value == NULL) {
utilDie("%s: %s must be a number, a string, true or false.", path, key);
}
_settingsAdd(list, count, key, value, path);
}
lua_pop(L, 1);
}
lua_pop(L, 1);
lua_close(L);
}
// Turns the whole presentation clockwise by 0, 90, 180 or 270 degrees, and returns whether it took.
// The frame is drawn unrotated into a render target and blitted turned, so the overlay, the GUIs,
// the 3D scene and the particles all turn with the video; a quarter turn swaps the logical size so
// a portrait window is filled. Rotation costs one screen sized texture and one extra blit a frame,
// and none of it exists while the rotation is zero.
static bool _setVideoRotate(int32_t degrees) {
int32_t width = 0;
int32_t height = 0;
if ((degrees < ROTATE_MIN) || (degrees > ROTATE_MAX) || ((degrees % ROTATE_STEP) != 0)) {
return false;
}
_global.videoRotate = degrees;
if (degrees == ROTATE_MIN) {
SDL_DestroyTexture(_global.rotateTexture);
_global.rotateTexture = NULL;
} else if (_global.rotateTexture == NULL) {
_global.rotateTexture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, _global.canvasWidth, _global.canvasHeight);
if (_global.rotateTexture == NULL) {
utilDie("%s", SDL_GetError());
}
SDL_SetTextureScaleMode(_global.rotateTexture, SDL_SCALEMODE_LINEAR);
}
_presentSize(&width, &height);
SDL_SetRenderLogicalPresentation(_global.renderer, width, height, _global.conf->stretchVideo ? SDL_LOGICAL_PRESENTATION_STRETCH : SDL_LOGICAL_PRESENTATION_LETTERBOX);
_global.refreshDisplay = true;
return true;
}
static const char *_skipDotSlash(const char *path) {
while ((path[0] == '.') && ((path[1] == '/') || (path[1] == '\\'))) {
path += 2;
}
return path;
}
static void _soundDestroy(SoundT *sound) {
HASH_DEL(_global.soundList, sound);
MIX_DestroyAudio(sound->audio);
free(sound);
}
// The sound both soundLoad and soundLoadData end up making, from a stream either of them opened.
// The stream is closed whatever happens.
static SoundT *_soundLoadFromIO(lua_State *L, const char *method, SDL_IOStream *io) {
SoundT *sound = (SoundT *)calloc(1, sizeof(SoundT));
if (!sound) {
SDL_CloseIO(io);
_luaDie(L, method, "Unable to allocate new sound.");
}
sound->audio = decodeAudioIO(videoGetMixer(), io, true);
if (!sound->audio) {
free(sound);
_luaDie(L, method, "%s", SDL_GetError());
}
sound->id = _global.nextSoundId++;
HASH_ADD_INT(_global.soundList, id, sound);
return sound;
}
// Takes the channels the mixer thread reported finished since the last call, under its lock.
static int32_t _soundQueueDrain(int32_t *finished) {
int32_t count = 0;
videoLockAudio();
count = _global.soundQueueCount;
memcpy(finished, _global.soundQueue, sizeof(int32_t) * (size_t)count);
_global.soundQueueCount = 0;
videoUnlockAudio();
return count;
}
// Prepares an animation's frames for drawing (unpacked and colour keyed), sums its loop time, and
// parks the sprite on the first frame.
static void _spriteAdoptAnimation(SpriteT *sprite) {
int32_t x = 0;
for (x = 0; x < sprite->animation->count; x++) {
_surfaceUnpack(&sprite->animation->frames[x]);
_surfaceKey(sprite->animation->frames[x]);
sprite->loopMs += (uint64_t)SDL_max(sprite->animation->delays[x], ANIMATION_MIN_DELAY_MS);
}
sprite->originalSurface = sprite->animation->frames[0];
}
// Every sprite draw ends here: the sprite's blend, alpha, and tint are set on the surface being
// drawn (a frame, a turned copy, or the image itself, so they follow every draw of the sprite) and
// it is blitted, stretched into dest or at its own size. SDL only rebuilds its blitter when one of
// them changes, so setting them on every draw costs nothing.
static void _spriteBlit(SpriteT *sprite, SDL_Surface *surface, const SDL_Rect *source, SDL_Rect *dest, bool stretched) {
SDL_BlendMode mode = SDL_BLENDMODE_BLEND;
bool modified = (sprite->alpha != SDL_ALPHA_OPAQUE) || (sprite->tintR != SDL_ALPHA_OPAQUE) || (sprite->tintG != SDL_ALPHA_OPAQUE) || (sprite->tintB != SDL_ALPHA_OPAQUE);
if (sprite->blend == SPRITE_ADD) {
mode = SDL_BLENDMODE_ADD;
} else if (sprite->blend == SPRITE_MULTIPLY) {
mode = SDL_BLENDMODE_MUL;
}
if ((stretched || modified) && _spriteBlitFast(sprite, surface, source, dest, mode)) {
return;
}
SDL_SetSurfaceBlendMode(surface, mode);
SDL_SetSurfaceAlphaMod(surface, sprite->alpha);
SDL_SetSurfaceColorMod(surface, sprite->tintR, sprite->tintG, sprite->tintB);
if (stretched) {
SDL_BlitSurfaceScaled(surface, source, _global.overlay, dest, SDL_SCALEMODE_NEAREST);
} else {
SDL_BlitSurface(surface, source, _global.overlay, dest);
}
}
// A stretched, tinted, or faded sprite the quick way. SDL blends a sprite onto the overlay quickly
// only unstretched and unmodified: stretching it, or changing its alpha or colour, sends every pixel
// down a general path ten to twenty times slower, about 8 ms for a whole 1920 by 1080 screen. So
// the picture is made in the scratch surface first -- sampled nearest at each pixel's centre in
// 16.16 fixed point as SDL's own stretch does, the colour key turned to transparency, tint and alpha
// multiplied in -- and that is blended on unmodified, which SDL does fast. Only the part of dest on
// the overlay is made. Pictures this does not read (3 byte pixels) answer false and go SDL's way.
static bool _spriteBlitFast(SpriteT *sprite, SDL_Surface *surface, const SDL_Rect *source, const SDL_Rect *dest, SDL_BlendMode mode) {
const SDL_PixelFormatDetails *in = SDL_GetPixelFormatDetails(surface->format);
const SDL_PixelFormatDetails *out = SDL_GetPixelFormatDetails(_global.blitScratch->format);
SDL_Palette *palette = SDL_GetSurfacePalette(surface);
SDL_Rect src = { 0, 0, surface->w, surface->h };
SDL_Rect clip;
SDL_Rect shown;
SDL_Rect scratch;
uint32_t table[256];
uint32_t red[256];
uint32_t green[256];
uint32_t blue[256];
uint32_t alpha[256];
uint32_t key = 0;
uint32_t rgbMask = 0;
uint32_t aMask = 0;
uint32_t rShift = 0;
uint32_t gShift = 0;
uint32_t bShift = 0;
uint32_t aShift = 0;
bool keyed = SDL_SurfaceHasColorKey(surface);
bool indexed = SDL_ISPIXELFORMAT_INDEXED(surface->format);
bool plain = false;
int64_t stepX = 0;
int64_t stepY = 0;
int64_t posY = 0;
int32_t x = 0;
int32_t y = 0;
int32_t c = 0;
if ((in == NULL) || (out == NULL) || (indexed ? (in->bytes_per_pixel != 1) || (palette == NULL) : (in->bytes_per_pixel != 4))) {
return false;
}
if (source != NULL) {
src = *source;
}
if ((src.w <= 0) || (src.h <= 0) || (dest->w <= 0) || (dest->h <= 0)) {
return true;
}
SDL_GetSurfaceClipRect(_global.overlay, &clip);
if (!SDL_GetRectIntersection(dest, &clip, &shown)) {
return true;
}
if (keyed) {
SDL_GetSurfaceColorKey(surface, &key);
}
// Each channel's tint (and the alpha) as a table, already shifted to where the scratch keeps it.
for (c = 0; c < 256; c++) {
red[c] = ((uint32_t)c * sprite->tintR / 255) << out->Rshift;
green[c] = ((uint32_t)c * sprite->tintG / 255) << out->Gshift;
blue[c] = ((uint32_t)c * sprite->tintB / 255) << out->Bshift;
alpha[c] = ((uint32_t)c * sprite->alpha / 255) << out->Ashift;
}
// Indexed: the palette as finished pixels, the keyed index clear.
if (indexed) {
for (c = 0; c < 256; c++) {
SDL_Color colour = (c < palette->ncolors) ? palette->colors[c] : (SDL_Color){ 0, 0, 0, SDL_ALPHA_OPAQUE };
table[c] = red[colour.r] | green[colour.g] | blue[colour.b] | ((keyed && ((uint32_t)c == key)) ? 0 : alpha[colour.a]);
}
} else {
rgbMask = in->Rmask | in->Gmask | in->Bmask;
aMask = in->Amask;
rShift = in->Rshift;
gShift = in->Gshift;
bShift = in->Bshift;
aShift = in->Ashift;
key &= rgbMask;
// Nothing to change in a pixel: copied as it is.
plain = !keyed && (aMask != 0) && (sprite->alpha == SDL_ALPHA_OPAQUE) && (sprite->tintR == SDL_ALPHA_OPAQUE) && (sprite->tintG == SDL_ALPHA_OPAQUE) && (sprite->tintB == SDL_ALPHA_OPAQUE) && (in->Rmask == out->Rmask) && (in->Gmask == out->Gmask) && (in->Bmask == out->Bmask) && (aMask == out->Amask);
}
if (SDL_MUSTLOCK(surface) && !SDL_LockSurface(surface)) {
return false;
}
stepX = ((int64_t)src.w << 16) / dest->w;
stepY = ((int64_t)src.h << 16) / dest->h;
posY = stepY / 2 + stepY * (shown.y - dest->y);
for (y = 0; y < shown.h; y++, posY += stepY) {
const uint8_t *row = (const uint8_t *)surface->pixels + (size_t)(src.y + (int32_t)(posY >> 16)) * (size_t)surface->pitch + src.x * (indexed ? 1 : 4);
uint32_t *to = (uint32_t *)((uint8_t *)_global.blitScratch->pixels + (size_t)y * (size_t)_global.blitScratch->pitch);
int64_t posX = stepX / 2 + stepX * (shown.x - dest->x);
// A row stretched over several of the screen's is made once and copied.
if ((y > 0) && ((posY >> 16) == ((posY - stepY) >> 16))) {
memcpy(to, (uint8_t *)to - _global.blitScratch->pitch, (size_t)shown.w * sizeof(uint32_t));
continue;
}
if (plain) {
const uint32_t *pixels = (const uint32_t *)row;
for (x = 0; x < shown.w; x++, posX += stepX) {
to[x] = pixels[posX >> 16];
}
} else if (indexed) {
for (x = 0; x < shown.w; x++, posX += stepX) {
to[x] = table[row[posX >> 16]];
}
} else {
const uint32_t *pixels = (const uint32_t *)row;
for (x = 0; x < shown.w; x++, posX += stepX) {
uint32_t p = pixels[posX >> 16];
uint32_t a = aMask ? alpha[(p & aMask) >> aShift] : alpha[SDL_ALPHA_OPAQUE];
to[x] = red[(p >> rShift) & 0xFF] | green[(p >> gShift) & 0xFF] | blue[(p >> bShift) & 0xFF] | ((keyed && ((p & rgbMask) == key)) ? 0 : a);
}
}
}
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
}
scratch.x = 0;
scratch.y = 0;
scratch.w = shown.w;
scratch.h = shown.h;
SDL_SetSurfaceBlendMode(_global.blitScratch, mode);
SDL_BlitSurface(_global.blitScratch, &scratch, _global.overlay, &shown);
return true;
}
static void _spriteDestroy(SpriteT *sprite) {
HASH_DEL(_global.spriteList, sprite);
_spriteFreeSurface(sprite);
SDL_DestroySurface(sprite->rotatedFrame);
SDL_DestroySurface(sprite->flippedFrame);
if (sprite->animation != NULL) {
// Frames belong to the animation.
IMG_FreeAnimation(sprite->animation);
} else {
SDL_DestroySurface(sprite->originalSurface);
}
free(sprite);
}
// The mirrored copy of one sheet frame that spriteDrawFrame draws while the sprite is flipped, kept
// from one draw to the next so a sprite standing on a frame is not mirrored again every frame.
static SDL_Surface *_spriteFlippedFrame(SpriteT *sprite, SDL_Surface *source, int32_t frame) {
if ((sprite->flippedFrame == NULL) || (sprite->flippedIndex != frame)) {
SDL_DestroySurface(sprite->flippedFrame);
sprite->flippedFrame = SDL_DuplicateSurface(source);
if (sprite->flippedFrame == NULL) {
utilDie("Unable to mirror frame %d of sprite %d.", frame, sprite->id);
}
SDL_FlipSurface(sprite->flippedFrame, sprite->flip);
sprite->flippedIndex = frame;
}
return sprite->flippedFrame;
}
// Releases the drawn surface if it is a transformed copy. Animation frames are never freed here.
static void _spriteFreeSurface(SpriteT *sprite) {
if (sprite->surfaceOwned) {
SDL_DestroySurface(sprite->surface);
}
sprite->surface = NULL;
sprite->surfaceOwned = false;
}
// The sprite both spriteLoad and spriteLoadData end up making, from a stream either of them opened.
// An animated GIF or WEBP becomes an animation; anything else becomes a still. The stream is closed
// whatever happens.
static SpriteT *_spriteLoadFromIO(lua_State *L, const char *method, SDL_IOStream *io, int32_t width, int32_t height) {
SpriteT *sprite = _spriteNew(L, method);
// A wanted size only means anything to a vector picture, and only spriteLoad offers one.
if ((width > 0) && (height > 0)) {
sprite->originalSurface = IMG_LoadSizedSVG_IO(io, width, height);
if (sprite->originalSurface == NULL) {
SDL_SeekIO(io, 0, SDL_IO_SEEK_SET);
} else {
_surfaceUnpack(&sprite->originalSurface);
SDL_CloseIO(io);
_surfaceKey(sprite->originalSurface);
_spriteRegister(sprite);
return sprite;
}
}
sprite->animation = IMG_LoadAnimation_IO(io, false);
if ((sprite->animation != NULL) && (sprite->animation->count < 2)) {
// Only one frame: take it over as the still image (no copy, so packed 1-bit PNGs survive).
sprite->originalSurface = sprite->animation->frames[0];
sprite->animation->frames[0] = NULL;
_surfaceUnpack(&sprite->originalSurface);
IMG_FreeAnimation(sprite->animation);
sprite->animation = NULL;
} else {
if (sprite->animation != NULL) {
_spriteAdoptAnimation(sprite);
} else {
SDL_SeekIO(io, 0, SDL_IO_SEEK_SET);
sprite->originalSurface = decodeImageIO(io, false);
_surfaceUnpack(&sprite->originalSurface);
}
}
SDL_CloseIO(io);
if (!sprite->originalSurface) {
_luaDie(L, method, "%s", SDL_GetError());
}
if (sprite->animation == NULL) {
// An animation's frames were keyed above, this one included.
_surfaceKey(sprite->originalSurface);
}
_spriteRegister(sprite);
return sprite;
}
// An empty sprite record for a loader to fill.
static SpriteT *_spriteNew(lua_State *L, const char *method) {
SpriteT *sprite = (SpriteT *)calloc(1, sizeof(SpriteT));
if (!sprite) {
_luaDie(L, method, "Unable to allocate new sprite.");
}
return sprite;
}
// Rebuilds the drawn surface after a frame, angle, scale, or quality change.
static void _spriteRebuildSurface(SpriteT *sprite) {
_spriteFreeSurface(sprite);
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0) && (sprite->flip == SDL_FLIP_NONE)) {
// Untransformed sprites draw straight from the original.
sprite->surface = sprite->originalSurface;
} else {
sprite->surface = _spriteTransform(sprite, sprite->originalSurface);
if (sprite->surface == NULL) {
utilDie("Unable to transform sprite %d.", sprite->id);
}
sprite->surfaceOwned = true;
}
}
// Finishes a loaded sprite: drawn untransformed from its original image, whose size is the frame
// size, under a new handle.
static void _spriteRegister(SpriteT *sprite) {
sprite->surface = sprite->originalSurface;
sprite->scaleX = 1.0;
sprite->scaleY = 1.0;
sprite->blend = SPRITE_ALPHA;
sprite->alpha = 255;
sprite->tintR = 255;
sprite->tintG = 255;
sprite->tintB = 255;
sprite->frameWidth = sprite->originalSurface->w;
sprite->frameHeight = sprite->originalSurface->h;
sprite->id = _global.nextSpriteId++;
HASH_ADD_INT(_global.spriteList, id, sprite);
}
// A fresh copy of source turned by the sprite's angle, scaled by its scale, and then mirrored as
// its flip says, or NULL when the copy could not be made. Mirroring after turning is what lets
// spriteFlip change the mirror on a turned frame without turning it again.
static SDL_Surface *_spriteTransform(SpriteT *sprite, SDL_Surface *source) {
SDL_Surface *copy = NULL;
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0)) {
copy = SDL_DuplicateSurface(source);
} else {
// A scale that would leave no pixels (a bullet shrinking to nothing) draws as one pixel
// rather than failing the transform; the scale the script set is kept as it reads back.
double scaleX = fmax(fabs(sprite->scaleX), 1.0 / (double)source->w);
double scaleY = fmax(fabs(sprite->scaleY), 1.0 / (double)source->h);
copy = rotoZoomSurface(source, -sprite->angle, scaleX, scaleY, sprite->smooth != 0);
}
if ((copy != NULL) && (sprite->flip != SDL_FLIP_NONE)) {
SDL_FlipSurface(copy, sprite->flip);
}
return copy;
}
// The disc audio suffix a run and a reload start from: --altaudio's, or the game's own audio.
static const char *_startAudioSuffix(void) {
return (_global.conf->audioSuffix != NULL) ? _global.conf->audioSuffix : "";
}
static void _startControllers(void) {
int32_t order[MAX_CONTROLLERS];
int32_t slot = 0;
int32_t count = 0;
SDL_JoystickID *ids = NULL;
char *mapping = NULL;
_stopControllers();
// --nogamepad ignores every pad, the counterpart of --nomouse: none is opened and no gamepad
// event is delivered, so a stuck arcade encoder cannot press anything.
if (_global.conf->noGamepad) {
SDL_SetGamepadEventsEnabled(false);
return;
}
// A device SDL does not recognise is given a mapping of its own first, so that it is a gamepad
// by the time the pads are enumerated and nothing downstream has to know it was ever anything else.
if (_global.conf->mapJoysticks) {
_mapUnknownJoysticks();
}
ids = SDL_GetGamepads(&count);
// Which physical pad lands in which slot. Without --gamepad_reorder that is SDL's own order,
// and the first few pads found fill the slots as they always have.
_gamepadOrder(order, count);
for (slot = 0; (ids != NULL) && (slot < MAX_CONTROLLERS); slot++) {
if ((order[slot] < 0) || (order[slot] >= count)) {
continue;
}
_global.controllers[slot] = SDL_OpenGamepad(ids[order[slot]]);
if (_global.controllers[slot]) {
mapping = SDL_GetGamepadMapping(_global.controllers[slot]);
_progTrace("Found %d - %s: %s", slot, SDL_GetGamepadName(_global.controllers[slot]), (mapping != NULL) ? mapping : "no mapping");
SDL_free(mapping);
} else {
_progTrace("Controller %d not opened", slot);
}
}
SDL_free(ids);
SDL_SetGamepadEventsEnabled(true);
}
// Prepares a Lua state: standard libraries, our constants, and the embedded module searcher.
static void _startLuaContext(lua_State *L) {
size_t length = 0;
size_t i = 0;
// What to do when bad things happen
lua_atpanic(L, _luaPanic);
// Register the standard libraries
luaL_openlibs(L);
// Games use os.clock() as a wall clock for debounces and timers, and it is processor time: with
// the GPU decoding video the engine mostly sleeps, so those timers crawl. Give them what they meant.
lua_getglobal(L, "os");
lua_pushcfunction(L, apiOsClock);
lua_setfield(L, -2, "clock");
// A deterministic run reads the same wall clock every time it plays: games seed their
// generator from os.time() when a game starts, and the run must repeat.
if (_global.conf->deterministic) {
lua_getfield(L, -1, "time");
lua_pushcclosure(L, apiOsTime, 1);
lua_setfield(L, -2, "time");
}
lua_pop(L, 1);
// Games written for 2.x seed the generator with fractional numbers; Lua 5.4 refuses them.
lua_getglobal(L, "math");
lua_getfield(L, -1, "randomseed");
lua_pushcclosure(L, apiMathRandomseed, 1);
lua_setfield(L, -2, "randomseed");
// A deterministic run starts from a known seed, and does so again for every rebuilt context, so
// a reload lands on the same numbers the first context saw.
if (_global.conf->deterministic) {
lua_getfield(L, -1, "randomseed");
lua_pushinteger(L, (lua_Integer)_global.conf->deterministicStep);
lua_call(L, 1, 0);
}
lua_pop(L, 1);
// Every file a script names goes through the vfs.
_installFileHooks(L);
_pushConstants(L);
// Put our searchers at the front of package.searchers: embedded modules, then game files.
lua_getglobal(L, "package");
lua_getfield(L, -1, "searchers");
length = lua_rawlen(L, -1);
for (i = length + 2; i > 2; i--) {
lua_rawgeti(L, -1, (lua_Integer)(i - 2));
lua_rawseti(L, -2, (lua_Integer)i);
}
lua_pushcfunction(L, _luaSearcher);
lua_rawseti(L, -2, 1);
lua_pushcfunction(L, _luaFileSearcher);
lua_rawseti(L, -2, 2);
lua_pop(L, 2);
}
// Everything the developer's overlay shows, gathered from wherever it lives. Drawn after the
// screenshot is taken, so a reference shot never has the overlay in it.
static void _statsDraw(void) {
StatsT stats;
int64_t textureBytes = 0;
SpriteT *sprite = NULL;
SpriteT *spriteNext = NULL;
SoundT *sound = NULL;
SoundT *soundNext = NULL;
if (!statsIsEnabled()) {
return;
}
memset(&stats, 0, sizeof(stats));
stats.frameMilliseconds = _global.statsFrameMs;
stats.worstMilliseconds = statsWorstMilliseconds();
stats.framesPerSecond = statsFramesPerSecond();
stats.luaKilobytes = (_global.luaContext != NULL) ? (double)lua_gc(_global.luaContext, LUA_GCCOUNT) : 0;
stats.timers = schedulerCount();
HASH_ITER(hh, _global.spriteList, sprite, spriteNext) {
stats.sprites++;
}
HASH_ITER(hh, _global.soundList, sound, soundNext) {
stats.sounds++;
}
sceneGetStats(&stats.nodesTotal, &stats.nodesDrawn, &stats.batches, &textureBytes);
stats.textureKilobytes = textureBytes / BYTES_PER_KIB;
if (_global.videoHandle >= 0) {
stats.discFrame = videoGetFrame(_global.videoHandle);
stats.discState = _global.discStopped ? "stopped" : (videoIsPlaying(_global.videoHandle) ? "playing" : "paused");
}
statsDraw(_global.renderer, &stats);
}
static void _stopControllers(void) {
int32_t x = 0;
for (x = 0; x < MAX_CONTROLLERS; x++) {
if (_global.controllers[x] != NULL) {
SDL_CloseGamepad(_global.controllers[x]);
_global.controllers[x] = NULL;
}
}
// Anything held on a controller axis is gone with the controller.
for (x = 0; x < AXIS_CONTROLLER_COUNT; x++) {
_releaseAxis(x);
_global.axisCache[x] = 0;
}
}
// The 3D side a script builds on: scene, physics, particles and navigation, in dependency order.
static void _subsystemsInit(void) {
sceneInit(_global.device, _global.renderer);
sceneSetLowQuality(_global.conf->sceneLow);
guiInit(_global.device, _global.renderer, _global.window);
guiSetEventCallback(_guiEvent);
physicsInit();
particlesInit();
navInit();
}
static void _subsystemsQuit(void) {
guiQuit();
_particleTexturesFreeAll();
modelQuit();
navQuit();
particlesQuit();
physicsQuit();
sceneQuit();
}
// Forgets the cues srtLoad parsed.
static void _subtitleClearCues(void) {
int32_t x = 0;
for (x = 0; x < _global.subtitleCueCount; x++) {
free(_global.subtitleCues[x].text);
}
free(_global.subtitleCues);
_global.subtitleCues = NULL;
_global.subtitleCueCount = 0;
_global.subtitleCue = SUBTITLE_NONE;
}
// The cue covering the disc's current frame, or SUBTITLE_NONE. The cues are in file order, so a
// binary search finds the right one wherever the disc has been sent, which is what makes a subtitle
// survive a seek.
static int32_t _subtitleCueNow(void) {
int64_t frame = 0;
int32_t found = SUBTITLE_NONE;
int32_t low = 0;
int32_t high = _global.subtitleCueCount - 1;
int32_t mid = 0;
if ((_global.subtitleCueCount == 0) || (_global.videoHandle < 0)) {
return SUBTITLE_NONE;
}
frame = _discGetFrame();
while (low <= high) {
mid = (low + high) / 2;
if (_global.subtitleCues[mid].startFrame <= frame) {
found = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
if ((found != SUBTITLE_NONE) && (frame > _global.subtitleCues[found].endFrame)) {
found = SUBTITLE_NONE;
}
return found;
}
// Builds the subtitle document the first time a cue or a banner has something to say. A machine
// with no GPU has no GUIs at all, so the text is simply not shown and everything else carries on.
static bool _subtitleEnsureGui(void) {
if (guiValid(_global.subtitleGui)) {
return true;
}
_global.subtitleGui = guiNew(SUBTITLE_WIDTH, SUBTITLE_HEIGHT);
if (_global.subtitleGui == GUI_NO_HANDLE) {
utilSay("Warning: No subtitles: %s", guiLastError());
return false;
}
_global.subtitleDocument = guiLoad(_global.subtitleGui, SUBTITLE_RML);
if (_global.subtitleDocument == GUI_NO_HANDLE) {
utilSay("Warning: Unable to load %s: %s", SUBTITLE_RML, guiLastError());
guiDelete(_global.subtitleGui);
_global.subtitleGui = GUI_NO_HANDLE;
return false;
}
return true;
}
// The next line of a writable block of text, NUL terminated in place and trimmed of the spaces,
// tabs and carriage returns a Windows written file leaves at either end. Unlike utilReadLine it
// hands back the blank lines, because a blank line is what separates one SubRip cue from the next.
static char *_subtitleNextLine(char **cursor) {
char *line = *cursor;
char *tail = NULL;
if ((line == NULL) || (*line == '\0')) {
*cursor = NULL;
return NULL;
}
tail = line;
while ((*tail != '\0') && (*tail != '\n')) {
tail++;
}
*cursor = (*tail == '\0') ? tail : (tail + 1);
*tail = '\0';
while ((*line == ' ') || (*line == '\t')) {
line++;
}
while ((tail > line) && ((tail[-1] == '\r') || (tail[-1] == ' ') || (tail[-1] == '\t'))) {
tail--;
*tail = '\0';
}
return line;
}
// Parses SubRip: a cue number, a line of "HH:MM:SS,mmm --> HH:MM:SS,mmm", one or more lines of
// text, and a blank line. Timestamps become disc frames at the disc's own frame rate, so a cue is
// found again wherever the disc is sent. Anything that does not parse is skipped, as Hypseus skips
// it, and a file that yields no cue at all is a failure. Works on a writable copy of the text.
static bool _subtitleParse(const char *text) {
SubtitleCueT *cues = NULL;
char *copy = strdup(text);
char *cursor = NULL;
char *line = NULL;
char *arrow = NULL;
char body[SUBTITLE_MARKUP_MAX];
size_t used = 0;
int32_t count = 0;
int32_t room = 0;
int64_t start = 0;
int64_t end = 0;
if (copy == NULL) {
utilDie("Unable to copy the subtitle file.");
}
cursor = copy;
while ((line = _subtitleNextLine(&cursor)) != NULL) {
if (*line == '\0') {
continue;
}
// That was the cue number, which nothing needs. The times come next.
line = _subtitleNextLine(&cursor);
if (line == NULL) {
break;
}
arrow = strstr(line, SUBTITLE_ARROW);
if (arrow == NULL) {
continue;
}
*arrow = '\0';
if (!_subtitleParseTime(line, &start) || !_subtitleParseTime(arrow + strlen(SUBTITLE_ARROW), &end)) {
continue;
}
// Every line up to the next blank one is this cue's text.
body[0] = '\0';
used = 0;
while ((line = _subtitleNextLine(&cursor)) != NULL) {
if (*line == '\0') {
break;
}
used += (size_t)snprintf(body + used, sizeof(body) - used, "%s%s", (used > 0) ? "\n" : "", line);
if (used >= sizeof(body)) {
used = sizeof(body) - 1;
}
}
if (body[0] == '\0') {
continue;
}
if (count >= SUBTITLE_CUES_MAX) {
break;
}
if (count == room) {
room = (room == 0) ? SUBTITLE_CUES_FIRST : (room * 2);
cues = (SubtitleCueT *)realloc(cues, sizeof(SubtitleCueT) * (size_t)room);
if (cues == NULL) {
utilDie("Unable to allocate the subtitle cues.");
}
}
cues[count].startFrame = start;
cues[count].endFrame = end;
cues[count].text = strdup(body);
count++;
}
free(copy);
_subtitleClearCues();
if (count == 0) {
free(cues);
return false;
}
_global.subtitleCues = cues;
_global.subtitleCueCount = count;
return true;
}
// One SubRip timestamp, "HH:MM:SS,mmm", into a disc frame at the disc's frame rate.
static bool _subtitleParseTime(const char *text, int64_t *frame) {
double fps = 0.0;
double seconds = 0.0;
int32_t hours = 0;
int32_t minutes = 0;
int32_t second = 0;
int32_t millis = 0;
if (_global.videoHandle < 0) {
return false;
}
if (sscanf(text, "%d:%d:%d,%d", &hours, &minutes, &second, &millis) != SUBTITLE_TIME_FIELDS) {
return false;
}
fps = videoGetFps(_global.videoHandle);
seconds = (double)hours * SECONDS_PER_HOUR + (double)minutes * SECONDS_PER_MINUTE + (double)second + (double)millis / MS_PER_SECOND_NUMBER;
*frame = (int64_t)(seconds * fps + 0.5);
return true;
}
// Queues the subtitle document over the picture for this frame, when it has anything on it.
static void _subtitleQueue(void) {
GuiDrawT *draw = NULL;
if ((_global.subtitleCue == SUBTITLE_NONE) && (_global.bannerFrames == 0)) {
return;
}
if (!guiValid(_global.subtitleGui) || (_global.guiDrawCount >= GUI_DRAW_MAX)) {
return;
}
draw = &_global.guiDraws[_global.guiDrawCount];
draw->gui = _global.subtitleGui;
draw->whole = true;
draw->screen = false;
_global.guiDrawCount++;
}
// A reload takes every GUI with it, so the subtitles go back to none loaded and none shown and the
// next script loads its own, exactly as it would on a fresh start.
static void _subtitleReset(void) {
_subtitleClearCues();
free(_global.bannerText);
_global.bannerText = NULL;
_global.bannerFrames = 0;
_global.bannerPosition = BANNER_POSITION_DEFAULT;
_global.subtitleGui = GUI_NO_HANDLE;
_global.subtitleDocument = GUI_NO_HANDLE;
_global.subtitleEnabled = false;
_global.subtitlePosition = SUBTITLE_POSITION_DEFAULT;
}
// Puts one line of text into the document at a height given as a per cent down the picture, or
// takes whatever was there away when the text is NULL. The text is escaped, since the document
// element takes markup, and the height rides in as an inline style so the look stays in the RCSS.
static void _subtitleShow(const char *text, int32_t position) {
char markup[SUBTITLE_MARKUP_MAX + SUBTITLE_WRAPPER_MAX];
char escaped[SUBTITLE_MARKUP_MAX];
if ((text != NULL) && !_subtitleEnsureGui()) {
return;
}
if (!guiValid(_global.subtitleGui)) {
return;
}
if (text == NULL) {
guiSetValue(_global.subtitleGui, _global.subtitleDocument, SUBTITLE_SLOT, "");
} else {
_rmlEscape(escaped, sizeof(escaped), text);
snprintf(markup, sizeof(markup), "<div class=\"cue\" style=\"top: %d%%;\"><span class=\"text\">%s</span></div>", position, escaped);
guiSetValue(_global.subtitleGui, _global.subtitleDocument, SUBTITLE_SLOT, markup);
}
_global.refreshDisplay = true;
}
// Once a drawn frame: a banner counts down and then makes way, and otherwise the cue covering the
// disc's current frame goes up. Nothing is written into the document unless the cue changed.
static void _subtitleUpdate(void) {
int32_t found = SUBTITLE_NONE;
if (_global.bannerFrames > 0) {
_global.bannerFrames--;
_global.refreshDisplay = true;
if (_global.bannerFrames == 0) {
free(_global.bannerText);
_global.bannerText = NULL;
_global.subtitleCue = SUBTITLE_NONE;
_subtitleShow(NULL, 0);
}
return;
}
if (_global.subtitleEnabled) {
found = _subtitleCueNow();
}
if (found != _global.subtitleCue) {
_global.subtitleCue = found;
_subtitleShow((found == SUBTITLE_NONE) ? NULL : _global.subtitleCues[found].text, _global.subtitlePosition);
}
}
// Keys and buttons that are already down when a script starts, or when the window gains focus, are
// not presses meant for this script: SDL reports them as fresh key downs, and the button that
// confirmed "exit" in a game would otherwise relaunch it from the menu. They stay ignored until released.
static void _suppressHeldInput(void) {
int32_t count = 0;
int32_t x = 0;
int32_t b = 0;
const bool *state = SDL_GetKeyboardState(&count);
// SDL may not know about a held key or button yet (gamepads are polled on their own thread, and
// X11 re-reports keys after the focus event), so presses that arrive soon after count as held too.
_global.inputGraceUntil = utilTicks() + INPUT_GRACE_MS;
for (x = 0; x < SDL_SCANCODE_COUNT; x++) {
_global.keySuppressed[x] = (x < count) && state[x];
}
for (x = 0; x < MAX_CONTROLLERS; x++) {
for (b = 0; b < CONTROLLER_BUTTON_COUNT; b++) {
_global.buttonSuppressed[x][b] = (_global.controllers[x] != NULL) && SDL_GetGamepadButton(_global.controllers[x], (SDL_GamepadButton)b);
}
}
}
// The colour key that matches a surface's top left pixel, in that surface's own format.
static uint32_t _surfaceCornerKey(SDL_Surface *surface) {
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
uint8_t a = 0;
if (!SDL_ReadSurfacePixel(surface, 0, 0, &r, &g, &b, &a)) {
utilDie("%s", SDL_GetError());
}
return SDL_MapSurfaceRGBA(surface, r, g, b, a);
}
// Colour 0 drawn transparent on a new sprite or text -- palette index 0, or black whatever its alpha,
// since SDL matches a key against the colour bits alone -- unless drawTransparent(true) said to go
// by alpha alone. Hypseus keys the same way, and its games are drawn for it.
static void _surfaceKey(SDL_Surface *surface) {
if (_global.spriteKeyed) {
SDL_SetSurfaceColorKey(surface, true, COLOR_KEY_VALUE);
}
}
// Every surface the engine keeps is 8 bits per channel: SDL3_image keeps 1, 2, and 4 bit indexed
// PNGs packed, and blits from those are unreliable, so they expand to one byte per pixel as SDL2
// always produced; anything else that is not an 8 bit indexed, packed 8888 or byte array format
// (16 bit PNGs as RGB48 or RGBA64, float formats, 565 and its kind) becomes RGBA32, since colour
// keying and blitting those is undefined or aborts inside SDL. Replaces *surface.
static void _surfaceUnpack(SDL_Surface **surface) {
SDL_Surface *unpacked = NULL;
SDL_PixelFormat format = SDL_PIXELFORMAT_UNKNOWN;
SDL_PixelFormat wanted = SDL_PIXELFORMAT_UNKNOWN;
if (*surface == NULL) {
return;
}
format = (*surface)->format;
if (SDL_ISPIXELFORMAT_INDEXED(format)) {
if (SDL_BITSPERPIXEL(format) < 8) {
wanted = SDL_PIXELFORMAT_INDEX8;
}
} else if (SDL_ISPIXELFORMAT_PACKED(format)) {
if (SDL_PIXELLAYOUT(format) != SDL_PACKEDLAYOUT_8888) {
wanted = SDL_PIXELFORMAT_RGBA32;
}
} else if (!SDL_ISPIXELFORMAT_ARRAY(format) || (SDL_PIXELTYPE(format) != SDL_PIXELTYPE_ARRAYU8)) {
wanted = SDL_PIXELFORMAT_RGBA32;
}
if (wanted == SDL_PIXELFORMAT_UNKNOWN) {
return;
}
unpacked = SDL_ConvertSurface(*surface, wanted);
if (unpacked == NULL) {
utilDie("%s", SDL_GetError());
}
SDL_DestroySurface(*surface);
*surface = unpacked;
}
// Saves the current frame buffer to the next free singeNNN.png in the data directory.
// Must be called before SDL_RenderPresent for the frame being captured.
static void _takeScreenshot(void) {
int32_t x = 0;
char *filename = NULL;
SDL_Surface *surface = NULL;
SDL_Surface *rgb = NULL;
if (_global.screenshotName != NULL) {
// A shot the script named goes where it said, over whatever was there.
filename = utilCreateString("%s%s", _global.conf->dataDir, _global.screenshotName);
free(_global.screenshotName);
_global.screenshotName = NULL;
} else {
// Each script starts scanning at zero; later shots resume past the last one saved.
for (x = _global.nextScreenshot; x < SCREENSHOT_MAX; x++) {
free(filename);
filename = utilCreateString("%ssinge%03d.png", _global.conf->dataDir, x);
if (!utilFileExists(filename)) {
break;
}
}
if (x >= SCREENSHOT_MAX) {
utilDie("Seriously? You have %d screenshots in this folder? Remove some.", SCREENSHOT_MAX);
}
_global.nextScreenshot = x + 1;
}
surface = _readWindow();
rgb = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGB24);
if (rgb == NULL) {
utilDie("%s", SDL_GetError());
}
if (!IMG_SavePNG(rgb, filename)) {
utilDie("%s", SDL_GetError());
}
_progTrace("Saved %s", filename);
SDL_DestroySurface(rgb);
SDL_DestroySurface(surface);
free(filename);
}
// Forgets a font's rendered strings, or every font's for -1.
static void _textCacheDrop(int32_t font) {
TextCacheT *entry = NULL;
TextCacheT *entryTemp = NULL;
HASH_ITER(hh, _global.textCache, entry, entryTemp) {
if ((font < 0) || (entry->font == font)) {
_textCacheRemove(entry);
}
}
}
// Takes one string out of the cache and frees it.
static void _textCacheRemove(TextCacheT *entry) {
HASH_DEL(_global.textCache, entry);
SDL_DestroySurface(entry->surface);
free(entry->key);
free(entry);
}
// Alt-Enter. Only the window changes: the renderer presents a logical size in video coordinates
// and scales it itself, _computeVideoRect() works in those same coordinates, the rotation target is
// the canvas's size and not the window's, and the GUIs are drawn into rectangles measured in the
// logical rectangle, so none of them has to be rebuilt -- the frame is only redrawn. A window that
// started in exclusive full screen keeps the display mode it was given; one that started windowed
// gets a borderless desktop window, which is what a player asking for full screen means.
static void _toggleFullScreen(void) {
bool wanted = !_global.fullScreen;
if (!SDL_SetWindowFullscreen(_global.window, wanted)) {
utilSay("Warning: full screen could not be changed: %s", SDL_GetError());
return;
}
SDL_SyncWindow(_global.window);
// What Singe last asked for, not what SDL_GetWindowFlags reports: a desktop with no window
// manager never confirms the change, and reading the flag back would leave the key stuck on.
_global.fullScreen = wanted;
_global.refreshDisplay = true;
_progTrace("Full screen is now %s", wanted ? "on" : "off");
}
// Everything a script loads: fonts, sounds, sprites and videos, whatever it forgot to unload.
static void _unloadScriptResources(void) {
FontT *font;
FontT *fontTemp;
MusicT *music;
MusicT *musicTemp;
SoundT *sound;
SoundT *soundTemp;
SpriteT *sprite;
SpriteT *spriteTemp;
VideoT *video;
VideoT *videoTemp;
HASH_ITER(hh, _global.fontList, font, fontTemp) {
_progTrace("Unloading font handle %d", font->id);
_fontDestroy(font);
}
// Every font's strings went with it; only the scratch for keys is left.
free(_global.textCacheKey);
_global.textCacheKey = NULL;
_global.textCacheKeyBytes = 0;
HASH_ITER(hh, _global.musicList, music, musicTemp) {
_progTrace("Unloading music handle %d", music->id);
_musicDestroy(music);
}
HASH_ITER(hh, _global.soundList, sound, soundTemp) {
_progTrace("Unloading sound handle %d", sound->id);
_soundDestroy(sound);
}
HASH_ITER(hh, _global.spriteList, sprite, spriteTemp) {
_progTrace("Unloading sprite handle %d", sprite->id);
_spriteDestroy(sprite);
}
HASH_ITER(hh, _global.videoList, video, videoTemp) {
_progTrace("Unloading video handle %d", video->id);
_videoDestroy(video);
}
// The microphone and every voice object the script made.
micClose();
voiceFreeAll();
}
// Turns a movement in presentation coordinates back into the video's own axes, the inverse of the
// clockwise rotation the finished frame is shown with.
static void _unrotateDelta(float dx, float dy, float *vx, float *vy) {
switch (_global.videoRotate) {
case 90:
*vx = dy;
*vy = -dx;
break;
case 180:
*vx = -dx;
*vy = -dy;
break;
case 270:
*vx = -dy;
*vy = dx;
break;
default:
*vx = dx;
*vy = dy;
break;
}
}
// The same for a position: a quarter turn also swaps which side of the picture the axis runs from.
static void _unrotatePoint(float px, float py, float *vx, float *vy) {
float width = (float)_global.canvasWidth;
float height = (float)_global.canvasHeight;
switch (_global.videoRotate) {
case 90:
*vx = py;
*vy = height - px;
break;
case 180:
*vx = width - px;
*vy = height - py;
break;
case 270:
*vx = width - py;
*vy = px;
break;
default:
*vx = px;
*vy = py;
break;
}
}
static void _updatePauseState(void) {
if (_global.pauseState) {
// Pause laserdisc
if ((_global.videoHandle >= 0) && !_global.discStopped && videoIsPlaying(_global.videoHandle)) {
_global.wasPlayingBeforePause = true;
videoPause(_global.videoHandle);
}
_pauseAllVideos(true);
MIX_PauseTag(videoGetMixer(), EFFECT_TAG);
MIX_PauseTag(videoGetMixer(), MUSIC_TAG);
} else {
// Resume laserdisc
if ((_global.videoHandle >= 0) && !_global.discStopped && _global.wasPlayingBeforePause) {
_global.wasPlayingBeforePause = false;
videoPlay(_global.videoHandle);
}
_pauseAllVideos(false);
MIX_ResumeTag(videoGetMixer(), EFFECT_TAG);
MIX_ResumeTag(videoGetMixer(), MUSIC_TAG);
}
}
// Positioned sounds: every playing channel that sits in the scene is handed to the mixer in the
// listener's frame (the camera's view, or a chosen node's) as a unit direction, since the mixer's
// own distance curve is flat inside one unit, and its gain is the range curve here. A channel
// following a node that has gone falls back to plain playback.
static void _updateSounds(void) {
Mat4T view = sceneGetView();
Vec3T listener = vec3(0.0f, 0.0f, 0.0f);
QuatT inverse = quatIdentity();
Vec3T scale;
Vec3T world;
Vec3T relative;
MIX_Point3D point;
EffectT *effect = NULL;
float distance = 0.0f;
float gain = 0.0f;
bool useNode = false;
int32_t x = 0;
if ((_global.listenerNode != LISTENER_CAMERA) && nodeValid(_global.listenerNode) && nodeGetWorldTransform(_global.listenerNode, &listener, &inverse, &scale)) {
inverse.x = -inverse.x;
inverse.y = -inverse.y;
inverse.z = -inverse.z;
useNode = true;
}
for (x = 0; x < EFFECT_TRACKS; x++) {
effect = &_effects[x];
if (!effect->positioned) {
continue;
}
if (effect->node != LISTENER_CAMERA) {
if (!nodeValid(effect->node)) {
effect->positioned = false;
MIX_SetTrack3DPosition(_effectTracks[x], NULL);
_effectApplyGain(x);
continue;
}
world = nodeGetWorldPosition(effect->node);
} else {
world = effect->position;
}
relative = useNode ? quatRotate(inverse, vec3Subtract(world, listener)) : mat4TransformPoint(view, world);
distance = vec3Length(relative);
if (distance > SOUND_DIRECTION_EPSILON) {
relative = vec3Scale(relative, 1.0f / distance);
}
gain = _effectGain(effect, distance);
// The mixer only hears about changes.
if ((gain == effect->gain) && (relative.x == effect->relative[0]) && (relative.y == effect->relative[1]) && (relative.z == effect->relative[2])) {
continue;
}
effect->gain = gain;
effect->relative[0] = relative.x;
effect->relative[1] = relative.y;
effect->relative[2] = relative.z;
point.x = relative.x;
point.y = relative.y;
point.z = relative.z;
MIX_SetTrack3DPosition(_effectTracks[x], &point);
_effectApplyGain(x);
}
}
static void _videoDestroy(VideoT *video) {
HASH_DEL(_global.videoList, video);
videoUnload(video->handle);
SDL_DestroySurface(video->transformedSurface);
free(video);
}
// The handle checks _argHandle takes, one for each kind of voice object.
static bool _voiceIsAny(int32_t handle) {
return voiceKind(handle, VOICE_KIND_ENCODER) || voiceKind(handle, VOICE_KIND_DECODER) || voiceKind(handle, VOICE_KIND_PROCESSOR) || voiceKind(handle, VOICE_KIND_JITTER);
}
static bool _voiceIsDecoder(int32_t handle) {
return voiceKind(handle, VOICE_KIND_DECODER);
}
static bool _voiceIsEncoder(int32_t handle) {
return voiceKind(handle, VOICE_KIND_ENCODER);
}
static bool _voiceIsJitter(int32_t handle) {
return voiceKind(handle, VOICE_KIND_JITTER);
}
static bool _voiceIsProcessor(int32_t handle) {
return voiceKind(handle, VOICE_KIND_PROCESSOR);
}
// Once a second: whether any watched file's modification time moved.
static bool _watchedChanged(void) {
uint64_t now = utilTicks();
int32_t x;
if (now < _global.watchTick + WATCH_INTERVAL_MS) {
return false;
}
_global.watchTick = now;
for (x = 0; x < _global.watchedCount; x++) {
int64_t size = 0;
int64_t modified = 0;
if (vfsStat(_global.watched[x].name, &size, &modified) && (modified != _global.watched[x].modified)) {
_progTrace("%s changed", _global.watched[x].name);
return true;
}
}
return false;
}
// --reload keeps the modification time of every loose script file the game loads.
static void _watchFile(const char *name) {
int64_t size = 0;
int64_t modified = 0;
int32_t x;
if (!vfsIsFilesystem(name) || !vfsStat(name, &size, &modified)) {
_progTrace("Not watching %s (packed or unreadable)", name);
return;
}
for (x = 0; x < _global.watchedCount; x++) {
if (strcmp(_global.watched[x].name, name) == 0) {
_global.watched[x].modified = modified;
return;
}
}
_progTrace("Watching %s", name);
_global.watched = realloc(_global.watched, sizeof(WatchedFileT) * (size_t)(_global.watchedCount + 1));
if (_global.watched == NULL) {
utilDie("Out of memory watching %s", name);
}
_global.watched[_global.watchedCount].name = strdup(name);
_global.watched[_global.watchedCount].modified = modified;
_global.watchedCount++;
}
// ===== Lua API =====
// seconds = animationGetTime(node)
// allowSocketCall()
// Hypseus answers whether the user let it send data to a stats server, which -noserversend turns
// off; the sending itself is commented out there. Singe sends nothing anywhere, so the answer is
// false and stays false. It exists so a game that asks does not die on a nil call.
static int32_t apiAllowSocketCall(lua_State *L) {
_argCheck(L, "allowSocketCall", 0, 0);
lua_pushboolean(L, false);
_luaTrace(L, "allowSocketCall", "false");
return 1;
}
static int32_t apiAnimationGetTime(lua_State *L) {
int32_t root;
_argCheck(L, "animationGetTime", 1, 1);
root = _argNode(L, "animationGetTime", 1);
if (modelRootOf(root) < 0) {
_luaDie(L, "animationGetTime", "Node %d is not a model instance.", root);
}
lua_pushnumber(L, animationGetTime(root));
return 1;
}
// playing = animationIsPlaying(node)
static int32_t apiAnimationIsPlaying(lua_State *L) {
_argCheck(L, "animationIsPlaying", 1, 1);
lua_pushboolean(L, animationIsPlaying(_argNode(L, "animationIsPlaying", 1)));
return 1;
}
// animationPause(node)
static int32_t apiAnimationPause(lua_State *L) {
int32_t root;
_argCheck(L, "animationPause", 1, 1);
root = _argNode(L, "animationPause", 1);
if (!animationPause(root)) {
_luaDie(L, "animationPause", "Node %d is not a model instance.", root);
}
return 0;
}
// animationPlay(node, nameOrIndex [, loop [, speed [, fade]]]): on a model instance's root node
static int32_t apiAnimationPlay(lua_State *L) {
int32_t root;
int32_t model;
int32_t index;
bool loop = true;
float speed = 1.0f;
float fade = 0.0f;
_argCheck(L, "animationPlay", 2, 5);
root = _argNode(L, "animationPlay", 1);
model = modelRootOf(root);
if (model < 0) {
_luaDie(L, "animationPlay", "Node %d is not a model instance.", root);
}
index = _argAnimation(L, "animationPlay", model, 2);
if (lua_gettop(L) >= 3) {
loop = _argBoolean(L, "animationPlay", 3);
}
if (lua_gettop(L) >= 4) {
speed = (float)_argNumber(L, "animationPlay", 4);
}
if (lua_gettop(L) >= 5) {
fade = (float)_argNumber(L, "animationPlay", 5);
}
if (!animationPlay(root, index, loop, speed, fade)) {
_luaDie(L, "animationPlay", "Model %d has no animation %d.", model, index + 1);
}
_luaTrace(L, "animationPlay", "node %d animation %d%s x%.2f fade %.2f", root, index + 1, loop ? " looping" : "", speed, fade);
return 0;
}
// animationPlayLayer(node, layer, nameOrIndex [, loop [, speed [, fade]]]): a clip blended over the base
static int32_t apiAnimationPlayLayer(lua_State *L) {
int32_t root;
int32_t model;
int32_t layer;
int32_t index;
bool loop = true;
float speed = 1.0f;
float fade = 0.0f;
_argCheck(L, "animationPlayLayer", 3, 6);
root = _argNode(L, "animationPlayLayer", 1);
model = modelRootOf(root);
if (model < 0) {
_luaDie(L, "animationPlayLayer", "Node %d is not a model instance.", root);
}
layer = _argAnimationLayer(L, "animationPlayLayer", 2);
index = _argAnimation(L, "animationPlayLayer", model, 3);
if (lua_gettop(L) >= 4) {
loop = _argBoolean(L, "animationPlayLayer", 4);
}
if (lua_gettop(L) >= 5) {
speed = (float)_argNumber(L, "animationPlayLayer", 5);
}
if (lua_gettop(L) >= 6) {
fade = (float)_argNumber(L, "animationPlayLayer", 6);
}
if (!animationPlayLayer(root, layer, index, loop, speed, fade)) {
_luaDie(L, "animationPlayLayer", "Model %d has no animation %d.", model, index + 1);
}
return 0;
}
// animationResume(node)
static int32_t apiAnimationResume(lua_State *L) {
int32_t root;
_argCheck(L, "animationResume", 1, 1);
root = _argNode(L, "animationResume", 1);
if (!animationResume(root)) {
_luaDie(L, "animationResume", "Node %d has no animation to resume.", root);
}
return 0;
}
// animationSetLayerMask(node, layer [, maskNode]): limits the layer to the subtree under maskNode; none lifts it
static int32_t apiAnimationSetLayerMask(lua_State *L) {
int32_t root;
int32_t layer;
int32_t mask = ANIMATION_NO_MASK;
_argCheck(L, "animationSetLayerMask", 2, 3);
root = _argNode(L, "animationSetLayerMask", 1);
layer = _argAnimationLayer(L, "animationSetLayerMask", 2);
if ((lua_gettop(L) >= 3) && !lua_isnil(L, 3)) {
mask = _argNode(L, "animationSetLayerMask", 3);
}
if (!animationSetLayerMask(root, layer, mask)) {
_luaDie(L, "animationSetLayerMask", "Node %d is not a model instance, or node %d is not part of it.", root, mask);
}
return 0;
}
// animationSetLayerWeight(node, layer, weight): 0 to 1
static int32_t apiAnimationSetLayerWeight(lua_State *L) {
int32_t root;
int32_t layer;
_argCheck(L, "animationSetLayerWeight", 3, 3);
root = _argNode(L, "animationSetLayerWeight", 1);
layer = _argAnimationLayer(L, "animationSetLayerWeight", 2);
if (!animationSetLayerWeight(root, layer, (float)_argNumber(L, "animationSetLayerWeight", 3))) {
_luaDie(L, "animationSetLayerWeight", "Node %d is not a model instance.", root);
}
return 0;
}
// animationSetTime(node, seconds)
static int32_t apiAnimationSetTime(lua_State *L) {
int32_t root;
_argCheck(L, "animationSetTime", 2, 2);
root = _argNode(L, "animationSetTime", 1);
if (!animationSetTime(root, _argNumber(L, "animationSetTime", 2))) {
_luaDie(L, "animationSetTime", "Node %d has no animation.", root);
}
return 0;
}
// animationStop(node [, layer]): every layer, or one of the layers above the base
static int32_t apiAnimationStop(lua_State *L) {
int32_t root;
int32_t layer = ANIMATION_ALL_LAYERS;
_argCheck(L, "animationStop", 1, 2);
root = _argNode(L, "animationStop", 1);
if (lua_gettop(L) >= 2) {
layer = _argAnimationLayer(L, "animationStop", 2);
}
if (!animationStop(root, layer)) {
_luaDie(L, "animationStop", "Node %d is not a model instance.", root);
}
return 0;
}
// bodyApplyForce(node, fx, fy, fz [, px, py, pz]): this step, at the centre or a world point
static int32_t apiBodyApplyForce(lua_State *L) {
int32_t node;
Vec3T force;
Vec3T at;
_argCheck(L, "bodyApplyForce", 4, 7);
node = _argBody(L, "bodyApplyForce", 1);
force = _argVec3(L, "bodyApplyForce", 2);
if (lua_gettop(L) >= 7) {
at = _argVec3(L, "bodyApplyForce", 5);
bodyApplyForce(node, force, &at);
} else {
bodyApplyForce(node, force, NULL);
}
return 0;
}
// bodyApplyImpulse(node, ix, iy, iz [, px, py, pz]): an instant change of momentum
static int32_t apiBodyApplyImpulse(lua_State *L) {
int32_t node;
Vec3T impulse;
Vec3T at;
_argCheck(L, "bodyApplyImpulse", 4, 7);
node = _argBody(L, "bodyApplyImpulse", 1);
impulse = _argVec3(L, "bodyApplyImpulse", 2);
if (lua_gettop(L) >= 7) {
at = _argVec3(L, "bodyApplyImpulse", 5);
bodyApplyImpulse(node, impulse, &at);
} else {
bodyApplyImpulse(node, impulse, NULL);
}
return 0;
}
// bodyDelete(node)
static int32_t apiBodyDelete(lua_State *L) {
_argCheck(L, "bodyDelete", 1, 1);
bodyDelete(_argBody(L, "bodyDelete", 1));
return 0;
}
// x, y, z = bodyGetAngularVelocity(node): radians per second about each axis
static int32_t apiBodyGetAngularVelocity(lua_State *L) {
_argCheck(L, "bodyGetAngularVelocity", 1, 1);
return _pushVec3(L, bodyGetAngularVelocity(_argBody(L, "bodyGetAngularVelocity", 1)));
}
// x, y, z = bodyGetVelocity(node): units per second
static int32_t apiBodyGetVelocity(lua_State *L) {
_argCheck(L, "bodyGetVelocity", 1, 1);
return _pushVec3(L, bodyGetVelocity(_argBody(L, "bodyGetVelocity", 1)));
}
// resting = bodyIsResting(node): asleep
static int32_t apiBodyIsResting(lua_State *L) {
_argCheck(L, "bodyIsResting", 1, 1);
lua_pushboolean(L, bodyIsResting(_argBody(L, "bodyIsResting", 1)));
return 1;
}
// bodyNew(node, type, shape [, a [, b [, c]]]): BODY_* and SHAPE_*; a, b, c size the primitive shapes
static int32_t apiBodyNew(lua_State *L) {
int32_t node = 0;
int32_t type = 0;
int32_t shape = 0;
float dims[SHAPE_SIZES] = { 0.0f, 0.0f, 0.0f };
int32_t x = 0;
_argCheck(L, "bodyNew", 3, 6);
node = _argNode(L, "bodyNew", 1);
type = _argInteger(L, "bodyNew", 2);
shape = _argInteger(L, "bodyNew", 3);
if ((type < BODY_STATIC) || (type > BODY_KINEMATIC)) {
_luaDie(L, "bodyNew", "Unknown body type %d.", type);
}
if ((shape < SHAPE_BOX) || (shape > SHAPE_MESH)) {
_luaDie(L, "bodyNew", "Unknown shape %d.", shape);
}
for (x = 0; x < SHAPE_SIZES; x++) {
if (lua_gettop(L) >= 4 + x) {
dims[x] = (float)_argNumber(L, "bodyNew", 4 + x);
}
}
if (!physicsAvailable()) {
_luaDie(L, "bodyNew", "Physics is not available on this machine.");
}
if (!bodyNew(node, (BodyTypeE)type, (ShapeTypeE)shape, dims[0], dims[1], dims[2])) {
_luaDie(L, "bodyNew", "Unable to create the body.");
}
_luaTrace(L, "bodyNew", "node %d type %d shape %d", node, type, shape);
return 0;
}
// bodySetAngularVelocity(node, x, y, z)
static int32_t apiBodySetAngularVelocity(lua_State *L) {
_argCheck(L, "bodySetAngularVelocity", 4, 4);
bodySetAngularVelocity(_argBody(L, "bodySetAngularVelocity", 1), _argVec3(L, "bodySetAngularVelocity", 2));
return 0;
}
// bodySetBounce(node, 0..1)
static int32_t apiBodySetBounce(lua_State *L) {
_argCheck(L, "bodySetBounce", 2, 2);
bodySetBounce(_argBody(L, "bodySetBounce", 1), (float)_argNumber(L, "bodySetBounce", 2));
return 0;
}
// bodySetBuoyancy(node, factor): 1 floats neutrally in water, more floats, less sinks
static int32_t apiBodySetBuoyancy(lua_State *L) {
_argCheck(L, "bodySetBuoyancy", 2, 2);
bodySetBuoyancy(_argBody(L, "bodySetBuoyancy", 1), (float)_argNumber(L, "bodySetBuoyancy", 2));
return 0;
}
// bodySetCurrent(node, x, y, z): the flow inside a water volume
static int32_t apiBodySetCurrent(lua_State *L) {
_argCheck(L, "bodySetCurrent", 4, 4);
bodySetCurrent(_argBody(L, "bodySetCurrent", 1), _argVec3(L, "bodySetCurrent", 2));
return 0;
}
// bodySetEnabled(node, bool): out of the world and back
static int32_t apiBodySetEnabled(lua_State *L) {
_argCheck(L, "bodySetEnabled", 2, 2);
bodySetEnabled(_argBody(L, "bodySetEnabled", 1), _argBoolean(L, "bodySetEnabled", 2));
return 0;
}
// bodySetFriction(node, friction)
static int32_t apiBodySetFriction(lua_State *L) {
_argCheck(L, "bodySetFriction", 2, 2);
bodySetFriction(_argBody(L, "bodySetFriction", 1), (float)_argNumber(L, "bodySetFriction", 2));
return 0;
}
// bodySetMass(node, kilograms): dynamic bodies
static int32_t apiBodySetMass(lua_State *L) {
int32_t node;
_argCheck(L, "bodySetMass", 2, 2);
node = _argBody(L, "bodySetMass", 1);
if (!bodySetMass(node, (float)_argNumber(L, "bodySetMass", 2))) {
_luaDie(L, "bodySetMass", "Node %d is not a dynamic body, or the mass is not positive.", node);
}
return 0;
}
// bodySetTrigger(node, bool): a trigger reports what enters and leaves it (onTrigger) and pushes nothing
static int32_t apiBodySetTrigger(lua_State *L) {
_argCheck(L, "bodySetTrigger", 2, 2);
bodySetTrigger(_argBody(L, "bodySetTrigger", 1), _argBoolean(L, "bodySetTrigger", 2));
return 0;
}
// bodySetVelocity(node, x, y, z)
static int32_t apiBodySetVelocity(lua_State *L) {
_argCheck(L, "bodySetVelocity", 4, 4);
bodySetVelocity(_argBody(L, "bodySetVelocity", 1), _argVec3(L, "bodySetVelocity", 2));
return 0;
}
// bodySetWater(node, density, linearDrag, angularDrag): a static body becomes a water volume
static int32_t apiBodySetWater(lua_State *L) {
_argCheck(L, "bodySetWater", 2, 4);
if (!bodySetWater(_argBody(L, "bodySetWater", 1), (float)_argNumber(L, "bodySetWater", 2), (lua_gettop(L) >= 3) ? (float)_argNumber(L, "bodySetWater", 3) : WATER_DEFAULT_LINEAR_DRAG, (lua_gettop(L) >= 4) ? (float)_argNumber(L, "bodySetWater", 4) : WATER_DEFAULT_ANGULAR_DRAG)) {
_luaDie(L, "bodySetWater", "Water needs a static body.");
}
return 0;
}
// cameraSet(node): any node can be the camera (it looks down its own -Z); -1 restores the default view.
static int32_t apiCameraSet(lua_State *L) {
int32_t node;
_argCheck(L, "cameraSet", 1, 1);
node = _argInteger(L, "cameraSet", 1);
if (!cameraSet(node)) {
_luaDie(L, "cameraSet", "No node %d.", node);
}
_luaTrace(L, "cameraSet", "%d", node);
return 0;
}
// cameraSetOrthographic(height, near, far)
static int32_t apiCameraSetOrthographic(lua_State *L) {
_argCheck(L, "cameraSetOrthographic", 3, 3);
cameraSetOrthographic((float)_argNumber(L, "cameraSetOrthographic", 1), (float)_argNumber(L, "cameraSetOrthographic", 2), (float)_argNumber(L, "cameraSetOrthographic", 3));
return 0;
}
// cameraSetPerspective(fovDegrees, near, far)
static int32_t apiCameraSetPerspective(lua_State *L) {
_argCheck(L, "cameraSetPerspective", 3, 3);
cameraSetPerspective((float)_argNumber(L, "cameraSetPerspective", 1), (float)_argNumber(L, "cameraSetPerspective", 2), (float)_argNumber(L, "cameraSetPerspective", 3));
return 0;
}
// colorBackground(r, g, b[, a]) Default alpha is transparent so overlayPrint shows the video through.
// touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
static int32_t apiCollideCircles(lua_State *L) {
_argCheck(L, "collideCircles", 6, 6);
lua_pushboolean(L, collideCircles(_argNumber(L, "collideCircles", 1), _argNumber(L, "collideCircles", 2), _argNumber(L, "collideCircles", 3),
_argNumber(L, "collideCircles", 4), _argNumber(L, "collideCircles", 5), _argNumber(L, "collideCircles", 6)));
return 1;
}
// inside = collidePointCircle(pointX, pointY, x, y, radius)
static int32_t apiCollidePointCircle(lua_State *L) {
_argCheck(L, "collidePointCircle", 5, 5);
lua_pushboolean(L, collidePointCircle(_argNumber(L, "collidePointCircle", 1), _argNumber(L, "collidePointCircle", 2),
_argNumber(L, "collidePointCircle", 3), _argNumber(L, "collidePointCircle", 4), _argNumber(L, "collidePointCircle", 5)));
return 1;
}
// inside = collidePointPolygon(pointX, pointY, points)
// The points are a flat list, x then y, which is the shape a hitbox table in the game library
// already has.
static int32_t apiCollidePointPolygon(lua_State *L) {
double points[SAVE_POLYGON_MAX * 2];
int32_t corners = 0;
int32_t count = 0;
int32_t x = 0;
_argCheck(L, "collidePointPolygon", 3, 3);
if (!lua_istable(L, 3)) {
_luaDie(L, "collidePointPolygon", "The third argument is a table of x, y pairs.");
}
count = (int32_t)lua_rawlen(L, 3);
if ((count % 2) != 0) {
_luaDie(L, "collidePointPolygon", "A polygon is x, y pairs, so the table has an even number of entries, not %d.", count);
}
corners = count / 2;
if (corners > SAVE_POLYGON_MAX) {
_luaDie(L, "collidePointPolygon", "A polygon may have %d corners, not %d.", SAVE_POLYGON_MAX, corners);
}
for (x = 0; x < count; x++) {
lua_rawgeti(L, 3, x + 1);
if (!lua_isnumber(L, -1)) {
_luaDie(L, "collidePointPolygon", "Entry %d of the polygon is not a number.", x + 1);
}
points[x] = lua_tonumber(L, -1);
lua_pop(L, 1);
}
lua_pushboolean(L, collidePointPolygon(_argNumber(L, "collidePointPolygon", 1), _argNumber(L, "collidePointPolygon", 2), points, corners));
return 1;
}
// inside = collidePointRect(pointX, pointY, x, y, width, height)
static int32_t apiCollidePointRect(lua_State *L) {
_argCheck(L, "collidePointRect", 6, 6);
lua_pushboolean(L, collidePointRect(_argNumber(L, "collidePointRect", 1), _argNumber(L, "collidePointRect", 2), _argNumber(L, "collidePointRect", 3),
_argNumber(L, "collidePointRect", 4), _argNumber(L, "collidePointRect", 5), _argNumber(L, "collidePointRect", 6)));
return 1;
}
// touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
static int32_t apiCollideRectCircle(lua_State *L) {
_argCheck(L, "collideRectCircle", 7, 7);
lua_pushboolean(L, collideRectCircle(_argNumber(L, "collideRectCircle", 1), _argNumber(L, "collideRectCircle", 2), _argNumber(L, "collideRectCircle", 3),
_argNumber(L, "collideRectCircle", 4), _argNumber(L, "collideRectCircle", 5), _argNumber(L, "collideRectCircle", 6),
_argNumber(L, "collideRectCircle", 7)));
return 1;
}
// touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
static int32_t apiCollideRects(lua_State *L) {
_argCheck(L, "collideRects", 8, 8);
lua_pushboolean(L, collideRects(_argNumber(L, "collideRects", 1), _argNumber(L, "collideRects", 2), _argNumber(L, "collideRects", 3), _argNumber(L, "collideRects", 4),
_argNumber(L, "collideRects", 5), _argNumber(L, "collideRects", 6), _argNumber(L, "collideRects", 7), _argNumber(L, "collideRects", 8)));
return 1;
}
// crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
static int32_t apiCollideSegments(lua_State *L) {
_argCheck(L, "collideSegments", 8, 8);
lua_pushboolean(L, collideSegments(_argNumber(L, "collideSegments", 1), _argNumber(L, "collideSegments", 2), _argNumber(L, "collideSegments", 3), _argNumber(L, "collideSegments", 4),
_argNumber(L, "collideSegments", 5), _argNumber(L, "collideSegments", 6), _argNumber(L, "collideSegments", 7), _argNumber(L, "collideSegments", 8)));
return 1;
}
static int32_t apiColorBackground(lua_State *L) {
_readColor(L, "colorBackground", &_global.colorBackground, SDL_ALPHA_TRANSPARENT);
return 0;
}
// colorForeground(r, g, b[, a])
static int32_t apiColorForeground(lua_State *L) {
_readColor(L, "colorForeground", &_global.colorForeground, SDL_ALPHA_OPAQUE);
return 0;
}
// controllerDoRumble([controller,] strength, duration) Hypseus extension. Strength and duration are steps 1 to 4; an empty slot does nothing.
static int32_t apiControllerDoRumble(lua_State *L) {
int32_t n = lua_gettop(L);
int32_t c = 0;
int32_t strength = 0;
int32_t duration = 0;
uint16_t level = 0;
uint32_t ms = 0;
bool rumbled = false;
_argCheck(L, "controllerDoRumble", 2, 3);
if (n == 3) {
c = _argInteger(L, "controllerDoRumble", 1);
}
strength = _argInteger(L, "controllerDoRumble", n - 1);
duration = _argInteger(L, "controllerDoRumble", n);
if ((c < 0) || (c >= MAX_CONTROLLERS)) {
_luaDie(L, "controllerDoRumble", "Invalid controller index: %d", c);
}
if ((strength < RUMBLE_LEVEL_MIN) || (strength > RUMBLE_LEVEL_MAX)) {
_luaDie(L, "controllerDoRumble", "Invalid rumble strength: %d", strength);
}
if ((duration < RUMBLE_LEVEL_MIN) || (duration > RUMBLE_LEVEL_MAX)) {
_luaDie(L, "controllerDoRumble", "Invalid rumble duration: %d", duration);
}
// --haptic is the strongest step the user will have: 0 stops rumble altogether and 1 to 3
// cap what a script may ask for, which is what Hypseus's own strength preset does.
if (strength > _global.conf->haptic) {
strength = _global.conf->haptic;
}
level = (uint16_t)(((uint32_t)SDL_MAX_UINT16 * (uint32_t)strength) / RUMBLE_LEVEL_MAX);
ms = (uint32_t)RUMBLE_BASE_MS << (duration - RUMBLE_LEVEL_MIN);
if ((strength > 0) && (_global.controllers[c] != NULL)) {
rumbled = SDL_RumbleGamepad(_global.controllers[c], level, level, ms);
}
_luaTrace(L, "controllerDoRumble", "%d %d %d %d", c, strength, duration, rumbled);
return 0;
}
// value = controllerGetAxis(controller, axis)
static int32_t apiControllerGetAxis(lua_State *L) {
int32_t c = 0;
int32_t a = 0;
int32_t v = 0;
_argCheck(L, "controllerGetAxis", 2, 2);
c = _argInteger(L, "controllerGetAxis", 1);
a = _argInteger(L, "controllerGetAxis", 2);
if ((c < 0) || (c >= MAX_CONTROLLERS)) {
_luaDie(L, "controllerGetAxis", "Invalid controller index: %d", c);
}
if ((a < 0) || (a >= CONTROLLER_AXIS_COUNT)) {
_luaDie(L, "controllerGetAxis", "Invalid controller axis: %d", a);
}
v = _global.axisCache[AXIS_INDEX_CONTROLLER(c, a)];
_luaTrace(L, "controllerGetAxis", "%d %d %d", c, a, v);
lua_pushinteger(L, v);
return 1;
}
// pressed = controllerGetButton(controller, GAMEPAD_N.BUTTON_X.value)
static int32_t apiControllerGetButton(lua_State *L) {
int32_t c = 0;
int32_t code = 0;
int32_t button = 0;
bool v = false;
_argCheck(L, "controllerGetButton", 2, 2);
c = _argInteger(L, "controllerGetButton", 1);
code = _argInteger(L, "controllerGetButton", 2);
if ((c < 0) || (c >= MAX_CONTROLLERS)) {
_luaDie(L, "controllerGetButton", "Invalid controller index: %d", c);
}
// Convert the framework code back to SDL's button enumeration.
button = code - CODE_GAMEPAD_BASE - (c * CODE_GAMEPAD_STRIDE) - CODE_GAMEPAD_BUTTON_OFFSET;
if ((button < 0) || (button >= CONTROLLER_BUTTON_COUNT)) {
_luaDie(L, "controllerGetButton", "Invalid controller button: %d", code);
}
if (_global.controllers[c] != NULL) {
v = SDL_GetGamepadButton(_global.controllers[c], (SDL_GamepadButton)button);
}
_luaTrace(L, "controllerGetButton", "%d %d %d", c, code, v);
lua_pushboolean(L, v);
return 1;
}
// name = controllerGetName(controller) The device in the slot, or nil when the slot is empty. A
// device SDL did not recognise reports its own name through the mapping Singe wrote for it.
static int32_t apiControllerGetName(lua_State *L) {
int32_t c = 0;
const char *name = NULL;
_argCheck(L, "controllerGetName", 1, 1);
c = _argInteger(L, "controllerGetName", 1);
if ((c < 0) || (c >= MAX_CONTROLLERS)) {
_luaDie(L, "controllerGetName", "Invalid controller index: %d", c);
}
if (_global.controllers[c] != NULL) {
name = SDL_GetGamepadName(_global.controllers[c]);
}
_luaTrace(L, "controllerGetName", "%d %s", c, (name != NULL) ? name : "nil");
if (name == NULL) {
lua_pushnil(L);
} else {
lua_pushstring(L, name);
}
return 1;
}
// count = controllerHowMany() Hypseus extension. Gamepads open in the four slots.
// controllerGetPadding()
// Hypseus adds this offset to a switch code that came from a gamepad, so a game can tell which
// device pressed what and still recover the switch by subtracting it. Singe delivers gamepad input
// as ordinary mapped switches with no such mark, so the honest answer is zero: a game then takes
// its "not a gamepad" branch and works, rather than subtracting a number that means nothing here.
static int32_t apiControllerGetPadding(lua_State *L) {
_argCheck(L, "controllerGetPadding", 0, 0);
lua_pushinteger(L, 0);
_luaTrace(L, "controllerGetPadding", "0");
return 1;
}
static int32_t apiControllerHowMany(lua_State *L) {
int32_t count = 0;
int32_t x = 0;
_argCheck(L, "controllerHowMany", 0, 0);
for (x = 0; x < MAX_CONTROLLERS; x++) {
if (_global.controllers[x] != NULL) {
count++;
}
}
_luaTrace(L, "controllerHowMany", "%d", count);
lua_pushinteger(L, count);
return 1;
}
// valid = controllerIsValid(controller) Hypseus extension. Whether a controller is open in the slot.
static int32_t apiControllerIsValid(lua_State *L) {
int32_t c = 0;
bool v = false;
_argCheck(L, "controllerIsValid", 1, 1);
c = _argInteger(L, "controllerIsValid", 1);
if ((c < 0) || (c >= MAX_CONTROLLERS)) {
_luaDie(L, "controllerIsValid", "Invalid controller index: %d", c);
}
v = (_global.controllers[c] != NULL);
_luaTrace(L, "controllerIsValid", "%d %d", c, v);
lua_pushboolean(L, v);
return 1;
}
// controllerSetPadding(boolean)
// Accepted and ignored, as controllerGetPadding explains. Turning the offset on without marking
// the events would be worse than leaving it off: the game would subtract it from switch codes that
// never carried it.
static int32_t apiControllerSetPadding(lua_State *L) {
_argCheck(L, "controllerSetPadding", 1, 1);
_argBoolean(L, "controllerSetPadding", 1);
return 0;
}
// debugPrint(message)
static int32_t apiDebugPrint(lua_State *L) {
const char *message = NULL;
_argCheck(L, "debugPrint", 1, 1);
message = _argString(L, "debugPrint", 1);
_luaTrace(L, "debugPrint", "%s", message);
utilSay("%s", message);
return 0;
}
// discAudio(channel, enabled) Channel 1 is left, 2 is right.
// drawTransparent(boolean)
// As Hypseus: true draws the sprites and text made after it by their alpha alone, and false (the
// start) keys colour 0 on them as well, which drops opaque black from a picture with alpha too.
static int32_t apiDrawTransparent(lua_State *L) {
_argCheck(L, "drawTransparent", 1, 1);
_global.spriteKeyed = !_argBoolean(L, "drawTransparent", 1);
_luaTrace(L, "drawTransparent", "%d", !_global.spriteKeyed);
return 0;
}
static int32_t apiDiscAudio(lua_State *L) {
int32_t channel = 0;
int32_t left = 0;
int32_t right = 0;
bool onOff = false;
_argCheck(L, "discAudio", 2, 2);
channel = _argInteger(L, "discAudio", 1);
onOff = _argBoolean(L, "discAudio", 2);
if ((channel < 1) || (channel > 2)) {
_luaDie(L, "discAudio", "Invalid audio channel: %d", channel);
}
if (_global.videoHandle >= 0) {
videoGetVolume(_global.videoHandle, &left, &right);
if (channel == 1) {
left = onOff ? _global.conf->volumeVldp : 0;
} else {
right = onOff ? _global.conf->volumeVldp : 0;
}
videoSetVolume(_global.videoHandle, left, right);
}
_luaTrace(L, "discAudio", "%d %d", left, right);
return 0;
}
// opened = discAudioSuffix(suffix) Hypseus extension. Plays "<base><suffix>.ogg" beside the disc's video from now on; "" restores the default audio.
static int32_t apiDiscAudioSuffix(lua_State *L) {
const char *suffix = NULL;
bool opened = false;
_argCheck(L, "discAudioSuffix", 1, 1);
suffix = _argString(L, "discAudioSuffix", 1);
if (_global.videoHandle >= 0) {
opened = _discSetAudioSuffix(suffix);
}
_luaTrace(L, "discAudioSuffix", "%s %d", suffix, opened);
lua_pushboolean(L, opened);
return 1;
}
static int32_t apiDiscChangeSpeed(lua_State *L) {
return _apiUnimplemented(L, "discChangeSpeed");
}
// track = discGetAudioTrack()
static int32_t apiDiscGetAudioTrack(lua_State *L) {
int32_t track = 0;
if (_global.videoHandle >= 0) {
track = videoGetAudioTrack(_global.videoHandle);
}
_luaTrace(L, "discGetAudioTrack", "%d", track);
lua_pushinteger(L, track);
return 1;
}
// count = discGetAudioTracks()
static int32_t apiDiscGetAudioTracks(lua_State *L) {
int32_t count = 0;
if (_global.videoHandle >= 0) {
count = videoGetAudioTracks(_global.videoHandle);
}
_luaTrace(L, "discGetAudioTracks", "%d", count);
lua_pushinteger(L, count);
return 1;
}
// language = discGetSubtitleLanguage(track) The language a subtitle track inside the disc's own
// container is labelled with, or "" when it is labelled with nothing.
static int32_t apiDiscGetSubtitleLanguage(lua_State *L) {
const char *language = "";
_argCheck(L, "discGetSubtitleLanguage", 1, 1);
if (_global.videoHandle >= 0) {
language = videoGetSubtitleLanguage(_global.videoHandle, _argInteger(L, "discGetSubtitleLanguage", 1));
}
lua_pushstring(L, language);
return 1;
}
// count = discGetSubtitleTracks() How many subtitle tracks the disc's own container holds.
static int32_t apiDiscGetSubtitleTracks(lua_State *L) {
int32_t count = 0;
_argCheck(L, "discGetSubtitleTracks", 0, 0);
if (_global.videoHandle >= 0) {
count = videoGetSubtitleTracks(_global.videoHandle);
}
_luaTrace(L, "discGetSubtitleTracks", "%d", count);
lua_pushinteger(L, count);
return 1;
}
// count = discGetFrameCount() How many frames the disc holds, which is the last frame plus one.
// With a frame file it is the whole disc rather than the segment playing.
static int32_t apiDiscGetFrameCount(lua_State *L) {
int64_t count = 0;
_argCheck(L, "discGetFrameCount", 0, 0);
if (_global.videoHandle >= 0) {
count = _global.conf->isFrameFile ? frameFileGetFrameCount(_global.frameFileHandle) : videoGetFrameCount(_global.videoHandle);
}
lua_pushinteger(L, (lua_Integer)count);
return 1;
}
// frame = discGetFrame()
static int32_t apiDiscGetFrame(lua_State *L) {
int64_t frame = 0;
if (!_global.discStopped && (_global.videoHandle >= 0)) {
frame = _discGetFrame();
}
_luaTrace(L, "discGetFrame", "%" PRId64, frame);
lua_pushinteger(L, frame);
return 1;
}
// height = discGetHeight() Also registered as vldpGetHeight.
static int32_t apiDiscGetHeight(lua_State *L) {
int32_t height = _global.canvasHeight;
_luaTrace(L, "discGetHeight", "%d", height);
lua_pushinteger(L, height);
return 1;
}
// code = discGetLanguage(track)
static int32_t apiDiscGetLanguage(lua_State *L) {
int32_t track = 0;
const char *language = "unk";
_argCheck(L, "discGetLanguage", 1, 1);
track = _argInteger(L, "discGetLanguage", 1);
if (_global.videoHandle >= 0) {
if ((track < 0) || (track >= videoGetAudioTracks(_global.videoHandle))) {
_luaDie(L, "discGetLanguage", "Invalid audio track: %d", track);
}
language = videoGetLanguage(_global.videoHandle, track);
}
_luaTrace(L, "discGetLanguage", "%d %s", track, language);
lua_pushstring(L, language);
return 1;
}
// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED, or DISC_EJECTED (no disc).
static int32_t apiDiscGetState(lua_State *L) {
DiscStateE state = DISC_PAUSED;
if (_global.videoHandle < 0) {
state = DISC_EJECTED;
} else if (_global.discStopped) {
state = DISC_STOPPED;
} else if (videoIsPlaying(_global.videoHandle)) {
state = DISC_PLAYING;
}
_luaTrace(L, "discGetState", "%d", state);
lua_pushinteger(L, state);
return 1;
}
// width = discGetWidth() Also registered as vldpGetWidth.
static int32_t apiDiscGetWidth(lua_State *L) {
int32_t width = _global.canvasWidth;
_luaTrace(L, "discGetWidth", "%d", width);
lua_pushinteger(L, width);
return 1;
}
// discPause()
static int32_t apiDiscPause(lua_State *L) {
if (_global.discStopped) {
_luaTrace(L, "discPause", "Ignored. Disc is stopped.");
return 0;
}
if (_global.videoHandle >= 0) {
videoPause(_global.videoHandle);
}
_luaTrace(L, "discPause", "Paused.");
return 0;
}
// discPlay()
static int32_t apiDiscPlay(lua_State *L) {
if (_global.videoHandle >= 0) {
videoPlay(_global.videoHandle);
}
_global.discStopped = false;
_luaTrace(L, "discPlay", "Playing.");
return 0;
}
// discSearch(frame) Seeks, shows the frame, and pauses. Also registered as discPauseAtFrame.
static int32_t apiDiscSearch(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSearch", 1, 1);
frame = _argInteger64(L, "discSearch", 1);
_discSeek(frame);
if (_global.videoHandle >= 0) {
videoPause(_global.videoHandle);
}
_global.discStopped = false;
_luaTrace(L, "discSearch", "%" PRId64, frame);
return 0;
}
static int32_t apiDiscSearchBlanking(lua_State *L) {
return _apiUnimplemented(L, "discSearchBlanking");
}
// discSetAudioTrack(track)
static int32_t apiDiscSetAudioTrack(lua_State *L) {
int32_t track = 0;
_argCheck(L, "discSetAudioTrack", 1, 1);
track = _argInteger(L, "discSetAudioTrack", 1);
if (_global.videoHandle >= 0) {
if ((track < 0) || (track >= videoGetAudioTracks(_global.videoHandle))) {
_luaDie(L, "discSetAudioTrack", "Invalid audio track: %d", track);
}
videoSetAudioTrack(_global.videoHandle, track);
}
_luaTrace(L, "discSetAudioTrack", "%d", track);
return 0;
}
static int32_t apiDiscSetFPS(lua_State *L) {
return _apiUnimplemented(L, "discSetFPS");
}
// discSkipBackward(frames) Play/pause state is unchanged.
static int32_t apiDiscSkipBackward(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSkipBackward", 1, 1);
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discSkipBackward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() - _argInteger64(L, "discSkipBackward", 1);
_discSeek(frame);
_luaTrace(L, "discSkipBackward", "%" PRId64, frame);
return 0;
}
static int32_t apiDiscSkipBlanking(lua_State *L) {
return _apiUnimplemented(L, "discSkipBlanking");
}
// discSkipForward(frames) Play/pause state is unchanged.
static int32_t apiDiscSkipForward(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSkipForward", 1, 1);
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discSkipForward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() + _argInteger64(L, "discSkipForward", 1);
_discSeek(frame);
_luaTrace(L, "discSkipForward", "%" PRId64, frame);
return 0;
}
// discSkipToFrame(frame) Seeks and plays no matter the disc state.
static int32_t apiDiscSkipToFrame(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSkipToFrame", 1, 1);
frame = _argInteger64(L, "discSkipToFrame", 1);
_discSeek(frame);
if (_global.videoHandle >= 0) {
videoPlay(_global.videoHandle);
}
_global.discStopped = false;
_luaTrace(L, "discSkipToFrame", "%" PRId64, frame);
return 0;
}
// discStepBackward() Go back a frame and pause. Ignored while stopped, like the skips.
static int32_t apiDiscStepBackward(lua_State *L) {
int64_t frame = 0;
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discStepBackward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() - 1;
_discSeek(frame);
videoPause(_global.videoHandle);
_luaTrace(L, "discStepBackward", "%" PRId64, frame);
return 0;
}
// discStepForward() Go forward a frame and pause. Ignored while stopped, like the skips.
static int32_t apiDiscStepForward(lua_State *L) {
int64_t frame = 0;
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discStepForward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() + 1;
_discSeek(frame);
videoPause(_global.videoHandle);
_luaTrace(L, "discStepForward", "%" PRId64, frame);
return 0;
}
// discStop() Pauses and shows the classic blue screen until the next play or search.
static int32_t apiDiscStop(lua_State *L) {
if (_global.discStopped) {
_luaTrace(L, "discStop", "Ignored. Disc is stopped.");
return 0;
}
if (_global.videoHandle >= 0) {
videoPause(_global.videoHandle);
}
_global.discStopped = true;
_global.refreshDisplay = true;
_luaTrace(L, "discStop", "Stopped.");
return 0;
}
static int32_t apiEmitterBurst(lua_State *L) {
_argCheck(L, "emitterBurst", 2, 2);
emitterBurst(_argEmitter(L, "emitterBurst", 1), _argInteger(L, "emitterBurst", 2));
return 0;
}
static int32_t apiEmitterClear(lua_State *L) {
_argCheck(L, "emitterClear", 1, 1);
emitterClear(_argEmitter(L, "emitterClear", 1));
return 0;
}
static int32_t apiEmitterDelete(lua_State *L) {
int32_t emitter = 0;
_argCheck(L, "emitterDelete", 1, 1);
emitter = _argEmitter(L, "emitterDelete", 1);
_particleTexturesFree(emitter);
emitterDelete(emitter);
return 0;
}
static int32_t apiEmitterDraw(lua_State *L) {
int32_t emitter = 0;
_argCheck(L, "emitterDraw", 1, 1);
emitter = _argEmitter(L, "emitterDraw", 1);
if (emitterIs3D(emitter)) {
_luaDie(L, "emitterDraw", "Emitter %d is a 3D emitter; it draws itself in the scene.", emitter);
}
particlesQueue2D(emitter);
return 0;
}
static int32_t apiEmitterGetCount(lua_State *L) {
_argCheck(L, "emitterGetCount", 1, 1);
lua_pushinteger(L, emitterGetCount(_argEmitter(L, "emitterGetCount", 1)));
return 1;
}
static int32_t apiEmitterIsActive(lua_State *L) {
_argCheck(L, "emitterIsActive", 1, 1);
lua_pushboolean(L, emitterIsActive(_argEmitter(L, "emitterIsActive", 1)));
return 1;
}
static int32_t apiEmitterNew(lua_State *L) {
int32_t node = -1;
_argCheck(L, "emitterNew", 0, 1);
if (lua_gettop(L) == 1) {
node = _argNode(L, "emitterNew", 1);
if (!sceneAvailable()) {
_luaDie(L, "emitterNew", "3D particles need the 3D scene, which this machine cannot provide.");
}
}
lua_pushinteger(L, emitterNew(node));
return 1;
}
static int32_t apiEmitterSetBlend(lua_State *L) {
int32_t emitter = 0;
int32_t blend = 0;
_argCheck(L, "emitterSetBlend", 2, 2);
emitter = _argEmitter(L, "emitterSetBlend", 1);
blend = _argInteger(L, "emitterSetBlend", 2);
if ((blend != PARTICLE_ALPHA) && (blend != PARTICLE_ADD)) {
_luaDie(L, "emitterSetBlend", "Blend must be PARTICLE_ALPHA or PARTICLE_ADD.");
}
emitterSetBlend(emitter, (ParticleBlendE)blend);
return 0;
}
// emitterSetCollide(emitter, COLLIDE_* [, bounce [, friction [, floor]]])
static int32_t apiEmitterSetCollide(lua_State *L) {
int32_t emitter = 0;
int32_t mode = 0;
float bounce = EMITTER_DEFAULT_BOUNCE;
float friction = EMITTER_DEFAULT_FRICTION;
float floor = EMITTER_DEFAULT_FLOOR;
_argCheck(L, "emitterSetCollide", 2, 5);
emitter = _argEmitter(L, "emitterSetCollide", 1);
mode = _argInteger(L, "emitterSetCollide", 2);
if ((mode != COLLIDE_NONE) && (mode != COLLIDE_FLOOR) && (mode != COLLIDE_SCENE)) {
_luaDie(L, "emitterSetCollide", "Mode must be COLLIDE_NONE, COLLIDE_FLOOR or COLLIDE_SCENE.");
}
if (lua_gettop(L) >= 3) {
bounce = (float)_argNumber(L, "emitterSetCollide", 3);
}
if (lua_gettop(L) >= 4) {
friction = (float)_argNumber(L, "emitterSetCollide", 4);
}
if (lua_gettop(L) >= 5) {
floor = (float)_argNumber(L, "emitterSetCollide", 5);
}
emitterSetCollide(emitter, (ParticleCollideE)mode, bounce, friction, floor);
return 0;
}
static int32_t apiEmitterSetColor(lua_State *L) {
float start[COLOR_COMPONENTS];
float finish[COLOR_COMPONENTS];
float value = 0.0f;
int32_t c = 0;
_argCheck(L, "emitterSetColor", 9, 9);
for (c = 0; c < COLOR_COMPONENTS; c++) {
value = (float)_argNumber(L, "emitterSetColor", 2 + c) / (float)COLOR_BYTE_MAX;
start[c] = SDL_clamp(value, 0.0f, 1.0f);
value = (float)_argNumber(L, "emitterSetColor", 6 + c) / (float)COLOR_BYTE_MAX;
finish[c] = SDL_clamp(value, 0.0f, 1.0f);
}
emitterSetColor(_argEmitter(L, "emitterSetColor", 1), start, finish);
return 0;
}
static int32_t apiEmitterSetDirection(lua_State *L) {
int32_t emitter = 0;
Vec3T direction;
_argCheck(L, "emitterSetDirection", 3, 4);
emitter = _argEmitter(L, "emitterSetDirection", 1);
direction = vec3((float)_argNumber(L, "emitterSetDirection", 2), (float)_argNumber(L, "emitterSetDirection", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "emitterSetDirection", 4) : 0.0f);
emitterSetDirection(emitter, direction);
return 0;
}
static int32_t apiEmitterSetDrag(lua_State *L) {
_argCheck(L, "emitterSetDrag", 2, 2);
emitterSetDrag(_argEmitter(L, "emitterSetDrag", 1), (float)_argNumber(L, "emitterSetDrag", 2));
return 0;
}
static int32_t apiEmitterSetFrames(lua_State *L) {
_argCheck(L, "emitterSetFrames", 3, 3);
emitterSetFrames(_argEmitter(L, "emitterSetFrames", 1), _argInteger(L, "emitterSetFrames", 2), _argInteger(L, "emitterSetFrames", 3));
return 0;
}
static int32_t apiEmitterSetGravity(lua_State *L) {
int32_t emitter = 0;
Vec3T gravity;
_argCheck(L, "emitterSetGravity", 3, 4);
emitter = _argEmitter(L, "emitterSetGravity", 1);
gravity = vec3((float)_argNumber(L, "emitterSetGravity", 2), (float)_argNumber(L, "emitterSetGravity", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "emitterSetGravity", 4) : 0.0f);
emitterSetGravity(emitter, gravity);
return 0;
}
static int32_t apiEmitterSetLayer(lua_State *L) {
int32_t emitter = 0;
int32_t layer = 0;
_argCheck(L, "emitterSetLayer", 2, 2);
emitter = _argEmitter(L, "emitterSetLayer", 1);
layer = _argInteger(L, "emitterSetLayer", 2);
if ((layer != PARTICLE_OVER) && (layer != PARTICLE_UNDER)) {
_luaDie(L, "emitterSetLayer", "Layer must be PARTICLE_OVER or PARTICLE_UNDER.");
}
emitterSetLayer(emitter, (ParticleLayerE)layer);
return 0;
}
static int32_t apiEmitterSetLife(lua_State *L) {
_argCheck(L, "emitterSetLife", 3, 3);
emitterSetLife(_argEmitter(L, "emitterSetLife", 1), (float)_argNumber(L, "emitterSetLife", 2), (float)_argNumber(L, "emitterSetLife", 3));
return 0;
}
// emitterSetLit(emitter, lit): 3D particles shaded by the scene's lights
static int32_t apiEmitterSetLit(lua_State *L) {
_argCheck(L, "emitterSetLit", 2, 2);
emitterSetLit(_argEmitter(L, "emitterSetLit", 1), _argBoolean(L, "emitterSetLit", 2));
return 0;
}
static int32_t apiEmitterSetLocal(lua_State *L) {
_argCheck(L, "emitterSetLocal", 2, 2);
emitterSetLocal(_argEmitter(L, "emitterSetLocal", 1), _argBoolean(L, "emitterSetLocal", 2));
return 0;
}
static int32_t apiEmitterSetMax(lua_State *L) {
int32_t emitter = 0;
int32_t count = 0;
_argCheck(L, "emitterSetMax", 2, 2);
emitter = _argEmitter(L, "emitterSetMax", 1);
count = _argInteger(L, "emitterSetMax", 2);
if (count < 1) {
_luaDie(L, "emitterSetMax", "An emitter needs room for at least one particle.");
}
emitterSetMax(emitter, count);
return 0;
}
static int32_t apiEmitterSetPosition(lua_State *L) {
int32_t emitter = 0;
_argCheck(L, "emitterSetPosition", 3, 3);
emitter = _argEmitter(L, "emitterSetPosition", 1);
if (emitterIs3D(emitter)) {
_luaDie(L, "emitterSetPosition", "Emitter %d follows its node; move the node instead.", emitter);
}
emitterSetPosition(emitter, vec3((float)_argNumber(L, "emitterSetPosition", 2), (float)_argNumber(L, "emitterSetPosition", 3), 0.0f));
return 0;
}
static int32_t apiEmitterSetRadius(lua_State *L) {
_argCheck(L, "emitterSetRadius", 2, 2);
emitterSetRadius(_argEmitter(L, "emitterSetRadius", 1), (float)_argNumber(L, "emitterSetRadius", 2));
return 0;
}
static int32_t apiEmitterSetRate(lua_State *L) {
_argCheck(L, "emitterSetRate", 2, 2);
emitterSetRate(_argEmitter(L, "emitterSetRate", 1), (float)_argNumber(L, "emitterSetRate", 2));
return 0;
}
static int32_t apiEmitterSetSize(lua_State *L) {
_argCheck(L, "emitterSetSize", 3, 4);
emitterSetSize(_argEmitter(L, "emitterSetSize", 1), (float)_argNumber(L, "emitterSetSize", 2), (float)_argNumber(L, "emitterSetSize", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "emitterSetSize", 4) : 0.0f);
return 0;
}
// emitterSetSoftness(emitter, distance): 3D particles fade over this distance where they meet geometry
static int32_t apiEmitterSetSoftness(lua_State *L) {
_argCheck(L, "emitterSetSoftness", 2, 2);
emitterSetSoftness(_argEmitter(L, "emitterSetSoftness", 1), (float)_argNumber(L, "emitterSetSoftness", 2));
return 0;
}
static int32_t apiEmitterSetSpeed(lua_State *L) {
_argCheck(L, "emitterSetSpeed", 3, 3);
emitterSetSpeed(_argEmitter(L, "emitterSetSpeed", 1), (float)_argNumber(L, "emitterSetSpeed", 2), (float)_argNumber(L, "emitterSetSpeed", 3));
return 0;
}
static int32_t apiEmitterSetSpin(lua_State *L) {
_argCheck(L, "emitterSetSpin", 3, 3);
emitterSetSpin(_argEmitter(L, "emitterSetSpin", 1), (float)_argNumber(L, "emitterSetSpin", 2), (float)_argNumber(L, "emitterSetSpin", 3));
return 0;
}
static int32_t apiEmitterSetSpread(lua_State *L) {
_argCheck(L, "emitterSetSpread", 2, 2);
emitterSetSpread(_argEmitter(L, "emitterSetSpread", 1), (float)_argNumber(L, "emitterSetSpread", 2));
return 0;
}
static int32_t apiEmitterSetTexture(lua_State *L) {
int32_t emitter = 0;
SpriteT *sprite = NULL;
_argCheck(L, "emitterSetTexture", 1, 2);
emitter = _argEmitter(L, "emitterSetTexture", 1);
if ((lua_gettop(L) == 2) && !lua_isnil(L, 2)) {
sprite = _argSprite(L, "emitterSetTexture", 2);
if (sprite->animation != NULL) {
emitterSetTexture(emitter, sprite->animation->frames, sprite->animation->count);
} else {
emitterSetTexture(emitter, &sprite->originalSurface, 1);
}
} else {
emitterSetTexture(emitter, NULL, 0);
}
return 0;
}
// emitterSetTrail(emitter, length, width): a ribbon of the last length positions behind each particle
static int32_t apiEmitterSetTrail(lua_State *L) {
_argCheck(L, "emitterSetTrail", 3, 3);
emitterSetTrail(_argEmitter(L, "emitterSetTrail", 1), _argInteger(L, "emitterSetTrail", 2), (float)_argNumber(L, "emitterSetTrail", 3));
return 0;
}
static int32_t apiEmitterStart(lua_State *L) {
_argCheck(L, "emitterStart", 1, 1);
emitterStart(_argEmitter(L, "emitterStart", 1));
return 0;
}
static int32_t apiEmitterStop(lua_State *L) {
_argCheck(L, "emitterStop", 1, 1);
emitterStop(_argEmitter(L, "emitterStop", 1));
return 0;
}
// id = fontLoad(filename, points) The new font becomes current.
static int32_t apiFontLoad(lua_State *L) {
const char *name = NULL;
int32_t points = 0;
FontT *font = NULL;
SDL_IOStream *io = NULL;
_argCheck(L, "fontLoad", 2, 2);
name = _argString(L, "fontLoad", 1);
points = _argInteger(L, "fontLoad", 2);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "fontLoad", "Unable to open %s", name);
}
font = (FontT *)calloc(1, sizeof(FontT));
if (!font) {
_luaDie(L, "fontLoad", "Unable to allocate new font.");
}
font->font = TTF_OpenFontIO(io, true, (float)points);
if (!font->font) {
_luaDie(L, "fontLoad", "%s", SDL_GetError());
}
font->id = _global.nextFontId++;
_global.fontCurrent = font;
HASH_ADD_INT(_global.fontList, id, font);
_luaTrace(L, "fontLoad", "%s %d", name, font->id);
lua_pushinteger(L, font->id);
return 1;
}
// fontPrint(x, y, text) Uses the current font.
static int32_t apiFontPrint(lua_State *L) {
const char *message = NULL;
SDL_Surface *text = NULL;
SDL_Rect dest;
_argCheck(L, "fontPrint", 3, 3);
dest.x = _argInteger(L, "fontPrint", 1);
dest.y = _argInteger(L, "fontPrint", 2);
message = _argString(L, "fontPrint", 3);
text = _renderTextCached(L, "fontPrint", message);
dest.w = text->w;
dest.h = text->h;
SDL_BlitSurface(text, NULL, _global.overlay, &dest);
_overlayTouched(&dest);
_luaTrace(L, "fontPrint", "%s", message);
return 0;
}
// fontQuality(FONT_QUALITY_SOLID | FONT_QUALITY_SHADED | FONT_QUALITY_BLENDED)
static int32_t apiFontQuality(lua_State *L) {
int32_t quality = 0;
_argCheck(L, "fontQuality", 1, 1);
quality = _argInteger(L, "fontQuality", 1);
if ((quality < FONT_QUALITY_SOLID) || (quality > FONT_QUALITY_BLENDED)) {
_luaDie(L, "fontQuality", "Unknown font quality: %d", quality);
}
_global.fontQuality = (FontQualityE)quality;
_luaTrace(L, "fontQuality", "%d", _global.fontQuality);
return 0;
}
// fontSelect(id) A font that was never loaded (a script naming one it misspelled) leaves the
// current font as it is with a warning, as Hypseus lets it pass, instead of ending the script.
static int32_t apiFontSelect(lua_State *L) {
FontT *font = NULL;
int32_t id = 0;
_argCheck(L, "fontSelect", 1, 1);
if (lua_isnumber(L, 1)) {
id = (int32_t)lua_tointeger(L, 1);
HASH_FIND_INT(_global.fontList, &id, font);
}
if (!font) {
utilSay("Warning: fontSelect names no font (%s); the current font stays.", lua_isnumber(L, 1) ? "a bad index" : "not a number");
return 0;
}
_global.fontCurrent = font;
_luaTrace(L, "fontSelect", "%d", _global.fontCurrent->id);
return 0;
}
// id = fontToSprite(text) Renders text with the current font into a new sprite.
static int32_t apiFontToSprite(lua_State *L) {
const char *message = NULL;
SpriteT *sprite = NULL;
_argCheck(L, "fontToSprite", 1, 1);
message = _argString(L, "fontToSprite", 1);
sprite = _spriteNew(L, "fontToSprite");
sprite->originalSurface = _renderText(L, "fontToSprite", message);
_spriteRegister(sprite);
_luaTrace(L, "fontToSprite", "%d %s", sprite->id, message);
lua_pushinteger(L, sprite->id);
return 1;
}
// fontUnload(id)
static int32_t apiFontUnload(lua_State *L) {
FontT *font = NULL;
_argCheck(L, "fontUnload", 1, 1);
font = _argFont(L, "fontUnload", 1);
_luaTrace(L, "fontUnload", "%d", font->id);
_fontDestroy(font);
return 0;
}
// joint = jointBall(nodeA, nodeB, ax, ay, az): nodeB -1 fixes to the world; anchor in world space
// value = getFValue() Hypseus extension. The number --fvalue carried from the launcher, 0 with none.
static int32_t apiGetFValue(lua_State *L) {
_argCheck(L, "getFValue", 0, 0);
lua_pushnumber(L, _global.conf->fValue);
_luaTrace(L, "getFValue", "%f", _global.conf->fValue);
return 1;
}
// guiClose(gui, document): unloads a document
static int32_t apiGuiClose(lua_State *L) {
int32_t gui;
_argCheck(L, "guiClose", 2, 2);
gui = _argGui(L, "guiClose", 1);
if (!guiClose(gui, _argInteger(L, "guiClose", 2))) {
_luaDie(L, "guiClose", "No document %d in GUI %d.", (int32_t)lua_tointeger(L, 2), gui);
}
return 0;
}
// guiDelete(gui): drops the GUI, its documents and its texture; materials showing it go back to theirs
static int32_t apiGuiDelete(lua_State *L) {
int32_t gui;
_argCheck(L, "guiDelete", 1, 1);
gui = _argGui(L, "guiDelete", 1);
materialForgetGui(gui);
guiDelete(gui);
return 0;
}
// guiDraw(gui [, x, y [, width, height]]): composites the GUI over the overlay this frame, in overlay
// coordinates (its own size when width and height are left out), or covering the overlay
static int32_t apiGuiDraw(lua_State *L) {
return _guiQueueDraw(L, "guiDraw", false);
}
// guiDrawScreen(gui [, x, y [, width, height]]): the same, in overlay coordinates stretched over the
// whole window, so the GUI may sit on the bezel artwork outside the picture
static int32_t apiGuiDrawScreen(lua_State *L) {
return _guiQueueDraw(L, "guiDrawScreen", true);
}
// getUserString()
// Always empty. Despite the name it is not a user string: Hypseus answers the machine install ID
// it sends with network statistics, from the Windows MachineGuid or the Linux machine-id. Singe
// sends nothing anywhere and will not hand a game a fingerprint of the machine it runs on.
static int32_t apiGetUserString(lua_State *L) {
_argCheck(L, "getUserString", 0, 0);
lua_pushstring(L, "");
_luaTrace(L, "getUserString", "empty");
return 1;
}
static int32_t apiGuiGetHeight(lua_State *L) {
_argCheck(L, "guiGetHeight", 1, 1);
lua_pushinteger(L, guiGetHeight(_argGui(L, "guiGetHeight", 1)));
return 1;
}
// value = guiGetValue(gui, document, id): a form control's value, or an element's inner RML; nil when missing
// stats = guiGetStats(): what the GUIs cost to render in the last frame, as a table
static int32_t apiGuiGetStats(lua_State *L) {
GuiStatsT stats;
_argCheck(L, "guiGetStats", 0, 0);
guiGetStats(&stats);
lua_newtable(L);
lua_pushinteger(L, stats.drawCalls);
lua_setfield(L, -2, "drawCalls");
lua_pushinteger(L, stats.layerPushes);
lua_setfield(L, -2, "layerPushes");
lua_pushinteger(L, stats.filterPasses);
lua_setfield(L, -2, "filterPasses");
lua_pushinteger(L, stats.maskWrites);
lua_setfield(L, -2, "maskWrites");
lua_pushinteger(L, stats.layerBytes);
lua_setfield(L, -2, "layerBytes");
lua_pushinteger(L, stats.passes);
lua_setfield(L, -2, "passes");
return 1;
}
static int32_t apiGuiGetValue(lua_State *L) {
char buffer[GUI_VALUE_MAX];
int32_t gui;
_argCheck(L, "guiGetValue", 3, 3);
gui = _argGui(L, "guiGetValue", 1);
if (!guiGetValue(gui, _argInteger(L, "guiGetValue", 2), _argString(L, "guiGetValue", 3), buffer, sizeof(buffer))) {
lua_pushnil(L);
return 1;
}
lua_pushstring(L, buffer);
return 1;
}
static int32_t apiGuiGetWidth(lua_State *L) {
_argCheck(L, "guiGetWidth", 1, 1);
lua_pushinteger(L, guiGetWidth(_argGui(L, "guiGetWidth", 1)));
return 1;
}
static int32_t apiGuiHide(lua_State *L) {
int32_t gui;
_argCheck(L, "guiHide", 2, 2);
gui = _argGui(L, "guiHide", 1);
if (!guiHide(gui, _argInteger(L, "guiHide", 2))) {
_luaDie(L, "guiHide", "No document %d in GUI %d.", (int32_t)lua_tointeger(L, 2), gui);
}
return 0;
}
// document = guiLoad(gui, filename): loads an RML document into the GUI and shows it
static int32_t apiGuiLoad(lua_State *L) {
int32_t gui;
int32_t document;
const char *name;
_argCheck(L, "guiLoad", 2, 2);
gui = _argGui(L, "guiLoad", 1);
name = _argString(L, "guiLoad", 2);
document = guiLoad(gui, name);
if (document < 0) {
_luaDie(L, "guiLoad", "%s: %s", name, guiLastError());
}
_luaTrace(L, "guiLoad", "%d %d %s", gui, document, name);
lua_pushinteger(L, document);
return 1;
}
// guiLoadFont(filename [, fallback]): a font face for every GUI; a fallback face serves glyphs no other has
static int32_t apiGuiLoadFont(lua_State *L) {
const char *name;
bool fallback = false;
_argCheck(L, "guiLoadFont", 1, 2);
name = _argString(L, "guiLoadFont", 1);
if (lua_gettop(L) >= 2) {
fallback = _argBoolean(L, "guiLoadFont", 2);
}
if (!guiLoadFont(name, fallback)) {
_luaDie(L, "guiLoadFont", "Unable to load %s.", name);
}
return 0;
}
// gui = guiNew(width, height): a GUI of that size in pixels, rendered to its own texture
static int32_t apiGuiNew(lua_State *L) {
int32_t gui;
int32_t width;
int32_t height;
_argCheck(L, "guiNew", 2, 2);
width = _argInteger(L, "guiNew", 1);
height = _argInteger(L, "guiNew", 2);
if ((width <= 0) || (height <= 0)) {
_luaDie(L, "guiNew", "The size must be positive.");
}
_capabilityUsed(CAPABILITY_GUI);
gui = guiNew(width, height);
if (gui < 0) {
_luaDie(L, "guiNew", "No GUI available (is a GPU device present, and are fewer than %d in use?).", GUI_MAX_CONTEXTS);
}
_luaTrace(L, "guiNew", "%d %dx%d", gui, width, height);
lua_pushinteger(L, gui);
return 1;
}
// guiRefresh(gui) Draws the GUI again next frame even if nothing seems to have changed. A GUI
// that looks the same as last frame is not redrawn; this is the way out should that ever be wrong.
static int32_t apiGuiRefresh(lua_State *L) {
int32_t gui;
_argCheck(L, "guiRefresh", 1, 1);
gui = _argGui(L, "guiRefresh", 1);
guiInvalidate(gui);
_luaTrace(L, "guiRefresh", "%d", gui);
return 0;
}
// guiSetHandler(gui, document, id, event, function): calls function(gui, document, id, event, value)
// when the element with that id fires the event ("click", "change", "submit", ...); nil removes it
static int32_t apiGuiSetHandler(lua_State *L) {
char key[GUI_ID_MAX * 2];
int32_t gui;
int32_t document;
const char *id;
const char *event;
_argCheck(L, "guiSetHandler", 5, 5);
gui = _argGui(L, "guiSetHandler", 1);
document = _argInteger(L, "guiSetHandler", 2);
id = _argString(L, "guiSetHandler", 3);
event = _argString(L, "guiSetHandler", 4);
if (!lua_isfunction(L, 5) && !lua_isnil(L, 5)) {
_luaDie(L, "guiSetHandler", "Argument 5 must be a function or nil.");
}
if (strlen(id) >= GUI_ID_MAX) {
_luaDie(L, "guiSetHandler", "Element ids are at most %d characters.", GUI_ID_MAX - 1);
}
if (lua_isfunction(L, 5) && !guiListen(gui, document, id, event)) {
_luaDie(L, "guiSetHandler", "No element \"%s\" in document %d of GUI %d.", id, document, gui);
}
_guiHandlerKey(key, sizeof(key), gui, document, id, event);
luaL_getsubtable(L, LUA_REGISTRYINDEX, GUI_HANDLERS);
lua_pushvalue(L, 5);
lua_setfield(L, -2, key);
lua_pop(L, 1);
return 0;
}
// guiSetInput(gui, enabled): whether the mouse, keys, text and the framework switches reach the GUI
static int32_t apiGuiSetInput(lua_State *L) {
_argCheck(L, "guiSetInput", 2, 2);
guiSetInput(_argGui(L, "guiSetInput", 1), _argBoolean(L, "guiSetInput", 2));
return 0;
}
// guiSetValue(gui, document, id, value): a form control's value, or an element's inner RML
static int32_t apiGuiSetValue(lua_State *L) {
int32_t gui;
_argCheck(L, "guiSetValue", 4, 4);
gui = _argGui(L, "guiSetValue", 1);
if (!guiSetValue(gui, _argInteger(L, "guiSetValue", 2), _argString(L, "guiSetValue", 3), _argString(L, "guiSetValue", 4))) {
_luaDie(L, "guiSetValue", "No element \"%s\" in document %d of GUI %d.", lua_tostring(L, 3), (int32_t)lua_tointeger(L, 2), gui);
}
return 0;
}
static int32_t apiGuiShow(lua_State *L) {
int32_t gui;
_argCheck(L, "guiShow", 2, 2);
gui = _argGui(L, "guiShow", 1);
if (!guiShow(gui, _argInteger(L, "guiShow", 2))) {
_luaDie(L, "guiShow", "No document %d in GUI %d.", (int32_t)lua_tointeger(L, 2), gui);
}
return 0;
}
// packet, status = jitterGet(jitter [, span]) The packet due for the next span samples (the step
// unless given), or nil, and the buffer moved on; status is "ok", "missing" (conceal the gap), or
// "insertion" (play a little silence while it grows).
static int32_t apiJitterGet(lua_State *L) {
static const char *names[] = { "ok", "missing", "insertion" };
uint8_t packet[VOICE_PACKET_MAX];
int32_t handle = 0;
int32_t span = 0;
int32_t bytes = 0;
VoiceJitterE status = VOICE_JITTER_MISSING;
_argCheck(L, "jitterGet", 1, 2);
handle = _argHandle(L, "jitterGet", 1, _voiceIsJitter, "jitter buffer");
span = (lua_gettop(L) >= 2) ? _argInteger(L, "jitterGet", 2) : voiceFrame(handle);
bytes = voiceJitterGet(handle, packet, (int32_t)sizeof(packet), span, &status);
if (status == VOICE_JITTER_OK) {
lua_pushlstring(L, (const char *)packet, (size_t)bytes);
} else {
lua_pushnil(L);
}
lua_pushstring(L, names[status]);
return 2;
}
// jitter = jitterNew([step]) A jitter buffer: packets put in by timestamp, taken out in order a
// step at a time (960 samples, 20 ms at 48 kHz, unless given).
static int32_t apiJitterNew(lua_State *L) {
int32_t step = VOICE_DEFAULT_FRAME;
int32_t handle = 0;
_argCheck(L, "jitterNew", 0, 1);
if (lua_gettop(L) >= 1) {
step = _argInteger(L, "jitterNew", 1);
}
handle = voiceJitterNew(step);
if (handle < 0) {
_luaDie(L, "jitterNew", "%s", voiceError(handle));
}
_luaTrace(L, "jitterNew", "%d step %d", handle, step);
lua_pushinteger(L, handle);
return 1;
}
// jitterPut(jitter, packet, timestamp [, span]) A packet that arrived, by its timestamp in samples; its
// span is the step unless given.
static int32_t apiJitterPut(lua_State *L) {
const char *packet = NULL;
size_t bytes = 0;
int32_t handle = 0;
int32_t span = 0;
_argCheck(L, "jitterPut", 3, 4);
handle = _argHandle(L, "jitterPut", 1, _voiceIsJitter, "jitter buffer");
packet = _argData(L, "jitterPut", 2, &bytes);
span = (lua_gettop(L) >= 4) ? _argInteger(L, "jitterPut", 4) : voiceFrame(handle);
if ((bytes == 0) || (bytes > VOICE_PACKET_MAX)) {
_luaDie(L, "jitterPut", "A packet is 1 to %d bytes: %d", VOICE_PACKET_MAX, (int32_t)bytes);
}
voiceJitterPut(handle, (const uint8_t *)packet, (int32_t)bytes, _argInteger(L, "jitterPut", 3), span);
return 0;
}
static int32_t apiJointBall(lua_State *L) {
int32_t nodeA;
int32_t nodeB;
int32_t joint;
_argCheck(L, "jointBall", 5, 5);
nodeA = _argBody(L, "jointBall", 1);
nodeB = _argInteger(L, "jointBall", 2);
if ((nodeB != -1) && !bodyExists(nodeB)) {
_luaDie(L, "jointBall", "Node %d has no body.", nodeB);
}
joint = jointNew(JOINT_BALL, nodeA, nodeB, _argVec3(L, "jointBall", 3), vec3(0.0f, 1.0f, 0.0f));
if (joint < 0) {
_luaDie(L, "jointBall", "Unable to create the joint.");
}
lua_pushinteger(L, joint);
return 1;
}
// jointDelete(joint)
static int32_t apiJointDelete(lua_State *L) {
int32_t joint;
_argCheck(L, "jointDelete", 1, 1);
joint = _argInteger(L, "jointDelete", 1);
if (!jointDelete(joint)) {
_luaDie(L, "jointDelete", "No joint %d.", joint);
}
return 0;
}
// joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz): anchor and axis in world space; nodeB -1 is the world
static int32_t apiJointHinge(lua_State *L) {
int32_t nodeA;
int32_t nodeB;
int32_t joint;
_argCheck(L, "jointHinge", 8, 8);
nodeA = _argBody(L, "jointHinge", 1);
nodeB = _argInteger(L, "jointHinge", 2);
if ((nodeB != -1) && !bodyExists(nodeB)) {
_luaDie(L, "jointHinge", "Node %d has no body.", nodeB);
}
joint = jointNew(JOINT_HINGE, nodeA, nodeB, _argVec3(L, "jointHinge", 3), _argVec3(L, "jointHinge", 6));
if (joint < 0) {
_luaDie(L, "jointHinge", "Unable to create the joint.");
}
lua_pushinteger(L, joint);
return 1;
}
// jointSetLimits(joint, low, high): degrees for a hinge, distance for a slider
static int32_t apiJointSetLimits(lua_State *L) {
int32_t joint;
_argCheck(L, "jointSetLimits", 3, 3);
joint = _argInteger(L, "jointSetLimits", 1);
if (!jointSetLimits(joint, (float)_argNumber(L, "jointSetLimits", 2), (float)_argNumber(L, "jointSetLimits", 3))) {
_luaDie(L, "jointSetLimits", "Joint %d has no limits to set.", joint);
}
return 0;
}
// joint = jointSlider(nodeA, nodeB, ax, ay, az, dx, dy, dz): slides along the axis through the anchor; nodeB -1 is the world
static int32_t apiJointSlider(lua_State *L) {
int32_t nodeA;
int32_t nodeB;
int32_t joint;
_argCheck(L, "jointSlider", 8, 8);
nodeA = _argBody(L, "jointSlider", 1);
nodeB = _argInteger(L, "jointSlider", 2);
if ((nodeB != -1) && !bodyExists(nodeB)) {
_luaDie(L, "jointSlider", "Node %d has no body.", nodeB);
}
joint = jointNew(JOINT_SLIDER, nodeA, nodeB, _argVec3(L, "jointSlider", 3), _argVec3(L, "jointSlider", 6));
if (joint < 0) {
_luaDie(L, "jointSlider", "Unable to create the joint.");
}
lua_pushinteger(L, joint);
return 1;
}
// joyMouseEnable(enabled) Hypseus extension. Whether the left stick of the first gamepad drives
// the mouse cursor. --joymouse sets where it starts, and a reload puts it back there.
static int32_t apiJoyMouseEnable(lua_State *L) {
_argCheck(L, "joyMouseEnable", 1, 1);
_global.joyMouseEnabled = _argBoolean(L, "joyMouseEnable", 1);
_luaTrace(L, "joyMouseEnable", "%d", _global.joyMouseEnabled);
return 0;
}
// enabled = joyMouseIsEnabled()
static int32_t apiJoyMouseIsEnabled(lua_State *L) {
_luaTrace(L, "joyMouseIsEnabled", "%d", _global.joyMouseEnabled);
lua_pushboolean(L, _global.joyMouseEnabled);
return 1;
}
// scancode = keyboardGetLastDown() Cleared every frame.
static int32_t apiKeyboardGetLastDown(lua_State *L) {
_luaTrace(L, "keyboardGetLastDown", "%d", _global.keyboardLastDown);
lua_pushinteger(L, _global.keyboardLastDown);
return 1;
}
// scancode = keyboardGetLastUp() Cleared every frame.
static int32_t apiKeyboardGetLastUp(lua_State *L) {
_luaTrace(L, "keyboardGetLastUp", "%d", _global.keyboardLastUp);
lua_pushinteger(L, _global.keyboardLastUp);
return 1;
}
static int32_t apiKeyboardGetMode(lua_State *L) {
_luaTrace(L, "keyboardGetMode", "%d", _global.keyboardMode);
lua_pushinteger(L, _global.keyboardMode);
return 1;
}
// modifiers = keyboardGetModifiers() SDL KMOD_* bits; compare with the MODIFIER table.
static int32_t apiKeyboardGetModifiers(lua_State *L) {
SDL_Keymod m = SDL_GetModState();
_luaTrace(L, "keyboardGetModifiers", "%d", (int32_t)m);
lua_pushinteger(L, (int32_t)m);
return 1;
}
// down = keyboardIsDown(scancode)
static int32_t apiKeyboardIsDown(lua_State *L) {
int32_t scancode = 0;
bool down = false;
_argCheck(L, "keyboardIsDown", 1, 1);
scancode = _argInteger(L, "keyboardIsDown", 1);
if ((scancode >= 0) && (scancode < SDL_SCANCODE_COUNT)) {
down = _global.keyboardState[scancode];
}
_luaTrace(L, "keyboardIsDown", "%d %d", scancode, down);
lua_pushboolean(L, down);
return 1;
}
// keyboardSetMode(MODE_NORMAL | MODE_FULL)
// MODE_NORMAL only reports inputs mapped in controls.cfg; MODE_FULL reports every key.
static int32_t apiKeyboardSetMode(lua_State *L) {
int32_t mode = 0;
_argCheck(L, "keyboardSetMode", 1, 1);
mode = _argInteger(L, "keyboardSetMode", 1);
if ((mode != KEYBOARD_NORMAL) && (mode != KEYBOARD_FULL)) {
_luaDie(L, "keyboardSetMode", "Unknown keyboard mode: %d", mode);
}
_global.keyboardMode = (KeyboardModeE)mode;
_guiTextInput();
_luaTrace(L, "keyboardSetMode", "%d", _global.keyboardMode);
return 0;
}
// node = lightNew(type [, parent]): a node carrying a light
static int32_t apiLightNew(lua_State *L) {
int32_t type;
int32_t parent = SCENE_ROOT_NODE;
int32_t node;
_argCheck(L, "lightNew", 1, 2);
type = _argInteger(L, "lightNew", 1);
if ((type < LIGHT_DIRECTIONAL) || (type > LIGHT_SPOT)) {
_luaDie(L, "lightNew", "Unknown light type %d.", type);
}
if (lua_gettop(L) >= 2) {
parent = _argNode(L, "lightNew", 2);
}
node = lightNew((LightTypeE)type, parent);
if (node < 0) {
_luaDie(L, "lightNew", "Unable to create the node.");
}
_luaTrace(L, "lightNew", "%d type %d under %d", node, type, parent);
lua_pushinteger(L, node);
return 1;
}
// lightSetColor(node, r, g, b)
static int32_t apiLightSetColor(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetColor", 4, 4);
node = _argNode(L, "lightSetColor", 1);
if (!lightSetColor(node, _argColorByte(L, "lightSetColor", 2), _argColorByte(L, "lightSetColor", 3), _argColorByte(L, "lightSetColor", 4))) {
_luaDie(L, "lightSetColor", "Node %d is not a light.", node);
}
return 0;
}
// lightSetCone(node, innerDegrees, outerDegrees) for spot lights
static int32_t apiLightSetCone(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetCone", 3, 3);
node = _argNode(L, "lightSetCone", 1);
if (!lightSetCone(node, (float)_argNumber(L, "lightSetCone", 2), (float)_argNumber(L, "lightSetCone", 3))) {
_luaDie(L, "lightSetCone", "Node %d is not a light.", node);
}
return 0;
}
// lightSetIntensity(node, intensity)
static int32_t apiLightSetIntensity(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetIntensity", 2, 2);
node = _argNode(L, "lightSetIntensity", 1);
if (!lightSetIntensity(node, (float)_argNumber(L, "lightSetIntensity", 2))) {
_luaDie(L, "lightSetIntensity", "Node %d is not a light.", node);
}
return 0;
}
// lightSetRange(node, range): 0 for unlimited
static int32_t apiLightSetRange(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetRange", 2, 2);
node = _argNode(L, "lightSetRange", 1);
if (!lightSetRange(node, (float)_argNumber(L, "lightSetRange", 2))) {
_luaDie(L, "lightSetRange", "Node %d is not a light.", node);
}
return 0;
}
// lightSetShadow(node, bool): this light casts shadows (a point light's cost six passes)
static int32_t apiLightSetShadow(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetShadow", 2, 2);
node = _argNode(L, "lightSetShadow", 1);
if (_argBoolean(L, "lightSetShadow", 2)) {
_capabilityUsed(CAPABILITY_SHADOWS);
}
if (!lightSetShadow(node, _argBoolean(L, "lightSetShadow", 2))) {
_luaDie(L, "lightSetShadow", "Node %d is not a light.", node);
}
return 0;
}
// lineDraw(x0, y0, z0, x1, y1, z1 [, r, g, b]): a world-space line over the scene for this frame, white by default
static int32_t apiLineDraw(lua_State *L) {
uint8_t r = COLOR_BYTE_MAX;
uint8_t g = COLOR_BYTE_MAX;
uint8_t b = COLOR_BYTE_MAX;
_argCheck(L, "lineDraw", 6, 9);
_argOptionalColor(L, "lineDraw", 7, &r, &g, &b);
sceneDrawLine(_argVec3(L, "lineDraw", 1), _argVec3(L, "lineDraw", 4), r, g, b);
return 0;
}
// loaded, id = mainBezelLoaded() Hypseus extension. Whether --bezel loaded artwork, and the
// identifier of the file it loaded, which is "0" when none is.
static int32_t apiMainBezelLoaded(lua_State *L) {
const char *id = (_global.bezelHash != NULL) ? _global.bezelHash : BEZEL_HASH_NONE;
_argCheck(L, "mainBezelLoaded", 0, 0);
_luaTrace(L, "mainBezelLoaded", "%d %s", _global.bezelTexture != NULL, id);
lua_pushboolean(L, _global.bezelTexture != NULL);
lua_pushstring(L, id);
return 2;
}
// materialDelete(material): a sprite node's own material (from nodeGetMaterial) stays with its node
static int32_t apiMaterialDelete(lua_State *L) {
int32_t material = 0;
_argCheck(L, "materialDelete", 1, 1);
material = _argMaterial(L, "materialDelete", 1);
if (!materialDelete(material)) {
_luaDie(L, "materialDelete", "Material %d belongs to a sprite node; clear the sprite instead.", material);
}
return 0;
}
// material = materialNew(): white, half rough
static int32_t apiMaterialNew(lua_State *L) {
int32_t material;
material = materialNew();
if (material < 0) {
_luaDie(L, "materialNew", "3D is not available on this machine.");
}
_luaTrace(L, "materialNew", "%d", material);
lua_pushinteger(L, material);
return 1;
}
// materialSetBlend(material, bool): alpha blended instead of opaque
static int32_t apiMaterialSetBlend(lua_State *L) {
_argCheck(L, "materialSetBlend", 2, 2);
materialSetBlend(_argMaterial(L, "materialSetBlend", 1), _argBoolean(L, "materialSetBlend", 2));
return 0;
}
// materialSetColor(material, r, g, b [, a])
static int32_t apiMaterialSetColor(lua_State *L) {
int32_t material = 0;
uint8_t a = SDL_ALPHA_OPAQUE;
_argCheck(L, "materialSetColor", 4, 5);
material = _argMaterial(L, "materialSetColor", 1);
if (lua_gettop(L) >= 5) {
a = _argColorByte(L, "materialSetColor", 5);
}
materialSetColor(material, _argColorByte(L, "materialSetColor", 2), _argColorByte(L, "materialSetColor", 3), _argColorByte(L, "materialSetColor", 4), a);
return 0;
}
// materialSetCutoff(material, cutoff)
static int32_t apiMaterialSetCutoff(lua_State *L) {
_argCheck(L, "materialSetCutoff", 2, 2);
materialSetCutoff(_argMaterial(L, "materialSetCutoff", 1), (float)_argNumber(L, "materialSetCutoff", 2));
return 0;
}
// materialSetDoubleSided(material, bool)
static int32_t apiMaterialSetDoubleSided(lua_State *L) {
_argCheck(L, "materialSetDoubleSided", 2, 2);
materialSetDoubleSided(_argMaterial(L, "materialSetDoubleSided", 1), _argBoolean(L, "materialSetDoubleSided", 2));
return 0;
}
// materialSetEmissive(material, r, g, b)
static int32_t apiMaterialSetEmissive(lua_State *L) {
_argCheck(L, "materialSetEmissive", 4, 4);
materialSetEmissive(_argMaterial(L, "materialSetEmissive", 1), _argColorByte(L, "materialSetEmissive", 2), _argColorByte(L, "materialSetEmissive", 3), _argColorByte(L, "materialSetEmissive", 4));
return 0;
}
// materialSetEmissiveMap(material[, image]) nil clears.
static int32_t apiMaterialSetEmissiveMap(lua_State *L) {
return _materialSetMap(L, "materialSetEmissiveMap", MAP_EMISSIVE, false);
}
static int32_t apiMaterialSetFilter(lua_State *L) {
int32_t material = 0;
int32_t filter = 0;
_argCheck(L, "materialSetFilter", 2, 2);
material = _argMaterial(L, "materialSetFilter", 1);
filter = _argInteger(L, "materialSetFilter", 2);
if ((filter != FILTER_LINEAR) && (filter != FILTER_NEAREST)) {
_luaDie(L, "materialSetFilter", "Filter must be FILTER_LINEAR or FILTER_NEAREST.");
}
materialSetFilter(material, (MaterialFilterE)filter);
return 0;
}
// materialSetMetallic(material, 0..1)
// materialSetGui(material [, gui]): a GUI's texture as the base colour texture; none restores the texture
static int32_t apiMaterialSetGui(lua_State *L) {
int32_t material;
int32_t gui = -1;
_argCheck(L, "materialSetGui", 1, 2);
material = _argMaterial(L, "materialSetGui", 1);
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
gui = _argGui(L, "materialSetGui", 2);
}
materialSetGui(material, gui);
return 0;
}
static int32_t apiMaterialSetMetallic(lua_State *L) {
_argCheck(L, "materialSetMetallic", 2, 2);
materialSetMetallic(_argMaterial(L, "materialSetMetallic", 1), (float)_argNumber(L, "materialSetMetallic", 2));
return 0;
}
// materialSetMetallicRoughnessMap(material[, image]) roughness in G, metallic in B; nil clears.
static int32_t apiMaterialSetMetallicRoughnessMap(lua_State *L) {
return _materialSetMap(L, "materialSetMetallicRoughnessMap", MAP_METALLIC_ROUGHNESS, false);
}
// materialSetNormalMap(material[, image[, strength]]) nil clears; strength defaults to 1.
static int32_t apiMaterialSetNormalMap(lua_State *L) {
return _materialSetMap(L, "materialSetNormalMap", MAP_NORMAL, true);
}
// materialSetOccluder(material, bool)
static int32_t apiMaterialSetOccluder(lua_State *L) {
_argCheck(L, "materialSetOccluder", 2, 2);
materialSetOccluder(_argMaterial(L, "materialSetOccluder", 1), _argBoolean(L, "materialSetOccluder", 2));
return 0;
}
// materialSetOcclusionMap(material[, image[, strength]]) occlusion in R; nil clears; strength defaults to 1.
static int32_t apiMaterialSetOcclusionMap(lua_State *L) {
return _materialSetMap(L, "materialSetOcclusionMap", MAP_OCCLUSION, true);
}
// materialSetRoughness(material, 0..1)
static int32_t apiMaterialSetRoughness(lua_State *L) {
_argCheck(L, "materialSetRoughness", 2, 2);
materialSetRoughness(_argMaterial(L, "materialSetRoughness", 1), (float)_argNumber(L, "materialSetRoughness", 2));
return 0;
}
// materialSetTexture(material [, image]): a sprite's image (or a KTX2 file) as the base colour; none clears it
static int32_t apiMaterialSetTexture(lua_State *L) {
return _materialSetMap(L, "materialSetTexture", MAP_BASE, false);
}
// materialSetTiling(material, u, v): how many times its textures repeat across a surface
static int32_t apiMaterialSetTiling(lua_State *L) {
_argCheck(L, "materialSetTiling", 3, 3);
materialSetTiling(_argMaterial(L, "materialSetTiling", 1), (float)_argNumber(L, "materialSetTiling", 2), (float)_argNumber(L, "materialSetTiling", 3));
return 0;
}
// materialSetUnlit(material, bool): shows the base colour as is
static int32_t apiMaterialSetUnlit(lua_State *L) {
_argCheck(L, "materialSetUnlit", 2, 2);
materialSetUnlit(_argMaterial(L, "materialSetUnlit", 1), _argBoolean(L, "materialSetUnlit", 2));
return 0;
}
// materialSetVideo(material [, video]): the disc (or a loaded video) as the base colour texture
static int32_t apiMaterialSetVideo(lua_State *L) {
int32_t material;
int32_t player;
VideoT *video;
_argCheck(L, "materialSetVideo", 1, 2);
material = _argMaterial(L, "materialSetVideo", 1);
if (lua_gettop(L) >= 2) {
video = _argVideo(L, "materialSetVideo", 2);
player = video->handle;
} else {
if (_global.videoHandle < 0) {
_luaDie(L, "materialSetVideo", "This game has no disc.");
}
player = PLAYER_DISC;
}
materialSetVideo(material, player);
return 0;
}
// materialSetView(material [, view]): a rendered view as the base colour texture; none restores the texture
static int32_t apiMaterialSetView(lua_State *L) {
int32_t material;
int32_t view = -1;
_argCheck(L, "materialSetView", 1, 2);
material = _argMaterial(L, "materialSetView", 1);
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
view = _argView(L, "materialSetView", 2);
}
materialSetView(material, view);
return 0;
}
// mesh = meshBox(width, height, depth)
// math.randomseed(x[, y]) Lua 5.4 wants integer seeds; games written for Singe 2.x pass products of
// os.clock() and the like, so fractional seeds are floored before Lua's own function sees them.
static int32_t apiMathRandomseed(lua_State *L) {
int32_t top = lua_gettop(L);
int32_t x = 0;
lua_Number n = 0.0;
// Nothing is unseeded in a deterministic run: a script asking Lua to pick a seed for itself
// gets the one the option named instead, so the run still repeats.
if ((top == 0) && _global.conf->deterministic) {
lua_pushinteger(L, (lua_Integer)_global.conf->deterministicStep);
top = 1;
}
for (x = 1; (x <= top) && (x <= 2); x++) {
if (lua_isnumber(L, x) && !lua_isinteger(L, x)) {
n = floor(lua_tonumber(L, x));
if (!isfinite(n)) {
n = 0.0;
}
lua_pushinteger(L, (lua_Integer)n);
lua_replace(L, x);
}
}
lua_pushvalue(L, lua_upvalueindex(1));
lua_insert(L, 1);
lua_call(L, top, LUA_MULTRET);
return lua_gettop(L);
}
static int32_t apiMeshBox(lua_State *L) {
int32_t mesh;
_argCheck(L, "meshBox", 3, 3);
mesh = meshBox((float)_argNumber(L, "meshBox", 1), (float)_argNumber(L, "meshBox", 2), (float)_argNumber(L, "meshBox", 3));
if (mesh < 0) {
_luaDie(L, "meshBox", "Unable to create the mesh.");
}
_luaTrace(L, "meshBox", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshCone(radius, height [, segments])
static int32_t apiMeshCone(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SEGMENTS_DEFAULT;
_argCheck(L, "meshCone", 2, 3);
if (lua_gettop(L) >= 3) {
segments = _argInteger(L, "meshCone", 3);
}
mesh = meshCone((float)_argNumber(L, "meshCone", 1), (float)_argNumber(L, "meshCone", 2), segments);
if (mesh < 0) {
_luaDie(L, "meshCone", "Unable to create the mesh.");
}
_luaTrace(L, "meshCone", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshCylinder(radius, height [, segments])
static int32_t apiMeshCylinder(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SEGMENTS_DEFAULT;
_argCheck(L, "meshCylinder", 2, 3);
if (lua_gettop(L) >= 3) {
segments = _argInteger(L, "meshCylinder", 3);
}
mesh = meshCylinder((float)_argNumber(L, "meshCylinder", 1), (float)_argNumber(L, "meshCylinder", 2), segments);
if (mesh < 0) {
_luaDie(L, "meshCylinder", "Unable to create the mesh.");
}
_luaTrace(L, "meshCylinder", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// meshDelete(mesh)
static int32_t apiMeshDelete(lua_State *L) {
_argCheck(L, "meshDelete", 1, 1);
meshDelete(_argMesh(L, "meshDelete", 1));
return 0;
}
// mesh = meshHeightmap(image, sizeX, sizeY, sizeZ): a terrain from a greyscale image (its red channel,
// black low and white sizeY high), sizeX by sizeZ across, one vertex per pixel
static int32_t apiMeshHeightmap(lua_State *L) {
const char *name = NULL;
SDL_IOStream *io = NULL;
SDL_Surface *image = NULL;
SDL_Surface *rgba = NULL;
const uint8_t *row = NULL;
float *heights = NULL;
int32_t bytesPerPixel = 0;
int32_t columns = 0;
int32_t rows = 0;
int32_t x = 0;
int32_t y = 0;
int32_t mesh = 0;
_argCheck(L, "meshHeightmap", 4, 4);
name = _argString(L, "meshHeightmap", 1);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "meshHeightmap", "Unable to open %s", name);
}
image = decodeImageIO(io, true);
if (image == NULL) {
_luaDie(L, "meshHeightmap", "%s: %s", name, SDL_GetError());
}
rgba = SDL_ConvertSurface(image, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(image);
if (rgba == NULL) {
_luaDie(L, "meshHeightmap", "%s", SDL_GetError());
}
if ((rgba->w < 2) || (rgba->h < 2) || (rgba->w > HEIGHTMAP_MAX) || (rgba->h > HEIGHTMAP_MAX)) {
SDL_DestroySurface(rgba);
_luaDie(L, "meshHeightmap", "%s must be 2 to %d pixels each way.", name, HEIGHTMAP_MAX);
}
columns = rgba->w - 1;
rows = rgba->h - 1;
heights = SDL_malloc(sizeof(float) * (size_t)rgba->w * (size_t)rgba->h);
if (heights == NULL) {
utilDie("Out of memory reading a heightmap.");
}
bytesPerPixel = (int32_t)SDL_BYTESPERPIXEL(rgba->format);
for (y = 0; y < rgba->h; y++) {
row = (const uint8_t *)rgba->pixels + (size_t)y * (size_t)rgba->pitch;
for (x = 0; x < rgba->w; x++) {
heights[y * rgba->w + x] = row[x * bytesPerPixel] / (float)COLOR_BYTE_MAX;
}
}
SDL_DestroySurface(rgba);
mesh = meshHeightmap(heights, columns, rows, (float)_argNumber(L, "meshHeightmap", 2), (float)_argNumber(L, "meshHeightmap", 3), (float)_argNumber(L, "meshHeightmap", 4));
SDL_free(heights);
if (mesh < 0) {
_luaDie(L, "meshHeightmap", "Unable to make a mesh from %s (is 3D available?).", name);
}
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshNew(positions, normals, uvs, indices): tables of numbers; normals and uvs may be nil
static int32_t apiMeshNew(lua_State *L) {
float *positions;
float *normals;
float *uvs;
float *indexValues;
uint32_t *indices;
int32_t positionCount;
int32_t normalCount;
int32_t uvCount;
int32_t indexCount;
int32_t vertexCount;
int32_t x;
int32_t mesh;
_argCheck(L, "meshNew", 4, 4);
positions = _argFloatTable(L, "meshNew", 1, &positionCount);
normals = _argFloatTable(L, "meshNew", 2, &normalCount);
uvs = _argFloatTable(L, "meshNew", 3, &uvCount);
indexValues = _argFloatTable(L, "meshNew", 4, &indexCount);
vertexCount = positionCount / 3;
if ((positionCount == 0) || (positionCount % 3 != 0)) {
_luaDie(L, "meshNew", "Positions must hold three numbers per vertex.");
}
if ((normals != NULL) && (normalCount != positionCount)) {
_luaDie(L, "meshNew", "Normals must hold three numbers per vertex, or be nil.");
}
if ((uvs != NULL) && (uvCount != vertexCount * 2)) {
_luaDie(L, "meshNew", "Texture coordinates must hold two numbers per vertex, or be nil.");
}
if ((indexCount < 3) || (indexCount % 3 != 0)) {
_luaDie(L, "meshNew", "Indices must hold three vertex numbers per triangle.");
}
indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t));
if (indices == NULL) {
_luaDie(L, "meshNew", "Out of memory.");
}
// Scripts number vertices from 1, as they appear in the positions table.
for (x = 0; x < indexCount; x++) {
if ((indexValues[x] < 1.0f) || (indexValues[x] > (float)vertexCount)) {
_luaDie(L, "meshNew", "Index %d refers to vertex %d; there are %d.", x + 1, (int32_t)indexValues[x], vertexCount);
}
indices[x] = (uint32_t)indexValues[x] - 1;
}
mesh = meshNew(positions, normals, uvs, vertexCount, indices, indexCount);
SDL_free(positions);
SDL_free(normals);
SDL_free(uvs);
SDL_free(indexValues);
SDL_free(indices);
if (mesh < 0) {
_luaDie(L, "meshNew", "Unable to create the mesh.");
}
_luaTrace(L, "meshNew", "%d (%d vertices, %d triangles)", mesh, vertexCount, indexCount / 3);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshPlane(width, depth): flat in XZ, facing up
static int32_t apiMeshPlane(lua_State *L) {
int32_t mesh;
_argCheck(L, "meshPlane", 2, 4);
if (lua_gettop(L) >= 3) {
// meshPlane(width, depth, columns[, rows]): subdivided, for cloth.
int32_t columns = _argInteger(L, "meshPlane", 3);
int32_t rows = (lua_gettop(L) == 4) ? _argInteger(L, "meshPlane", 4) : columns;
mesh = meshGrid((float)_argNumber(L, "meshPlane", 1), (float)_argNumber(L, "meshPlane", 2), columns, rows);
} else {
mesh = meshPlane((float)_argNumber(L, "meshPlane", 1), (float)_argNumber(L, "meshPlane", 2));
}
if (mesh < 0) {
_luaDie(L, "meshPlane", "Unable to create the mesh.");
}
_luaTrace(L, "meshPlane", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshSphere(radius [, segments])
static int32_t apiMeshSphere(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SPHERE_SEGMENTS_DEFAULT;
_argCheck(L, "meshSphere", 1, 2);
if (lua_gettop(L) >= 2) {
segments = _argInteger(L, "meshSphere", 2);
}
mesh = meshSphere((float)_argNumber(L, "meshSphere", 1), segments);
if (mesh < 0) {
_luaDie(L, "meshSphere", "Unable to create the mesh.");
}
_luaTrace(L, "meshSphere", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshTorus(radius, tubeRadius [, segments])
static int32_t apiMeshTorus(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SPHERE_SEGMENTS_DEFAULT;
_argCheck(L, "meshTorus", 2, 3);
if (lua_gettop(L) >= 3) {
segments = _argInteger(L, "meshTorus", 3);
}
mesh = meshTorus((float)_argNumber(L, "meshTorus", 1), (float)_argNumber(L, "meshTorus", 2), segments);
if (mesh < 0) {
_luaDie(L, "meshTorus", "Unable to create the mesh.");
}
_luaTrace(L, "meshTorus", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// started = micCalibrate() Plays clicks and times them coming back through the microphone, over a few
// seconds; micCalibration says when it is done. The answer is kept for the machine.
static int32_t apiMicCalibrate(lua_State *L) {
bool started = false;
_argCheck(L, "micCalibrate", 0, 0);
started = micCalibrate();
_luaTrace(L, "micCalibrate", "%s", started ? "started" : "no microphone or no speaker");
lua_pushboolean(L, started);
return 1;
}
// state, latency = micCalibration() "idle", "running", "done", or "failed", and the latency in
// milliseconds once one is known.
static int32_t apiMicCalibration(lua_State *L) {
static const char *names[] = { "idle", "running", "done", "failed" };
_argCheck(L, "micCalibration", 0, 0);
lua_pushstring(L, names[micCalibrationState()]);
if (micLatency() == MIC_LATENCY_UNKNOWN) {
lua_pushnil(L);
} else {
lua_pushinteger(L, micLatency());
}
return 2;
}
// micClose() Stops listening. The microphone also closes when the game ends.
static int32_t apiMicClose(lua_State *L) {
_argCheck(L, "micClose", 0, 0);
micClose();
_luaTrace(L, "micClose", "Closed.");
return 0;
}
// open = micIsOpen()
static int32_t apiMicIsOpen(lua_State *L) {
_argCheck(L, "micIsOpen", 0, 0);
lua_pushboolean(L, micIsOpen());
return 1;
}
// milliseconds = micLatency() Between a sound being made and the game hearing it, or nil until a
// calibration (on this machine, at any time) has measured it.
static int32_t apiMicLatency(lua_State *L) {
_argCheck(L, "micLatency", 0, 0);
if (micLatency() == MIC_LATENCY_UNKNOWN) {
lua_pushnil(L);
} else {
lua_pushinteger(L, micLatency());
}
return 1;
}
// rms, peak = micLevel() How loud the last moment heard was, each 0 to 1.
static int32_t apiMicLevel(lua_State *L) {
float rms = 0.0f;
float peak = 0.0f;
_argCheck(L, "micLevel", 0, 0);
micLevel(&rms, &peak);
lua_pushnumber(L, rms);
lua_pushnumber(L, peak);
return 2;
}
// names = micList() The microphones this machine has, by name, for micOpen.
static int32_t apiMicList(lua_State *L) {
int32_t count = 0;
int32_t i = 0;
_argCheck(L, "micList", 0, 0);
count = micDeviceCount();
lua_createtable(L, count, 0);
for (i = 0; i < count; i++) {
lua_pushstring(L, (micDeviceName(i) != NULL) ? micDeviceName(i) : "");
lua_rawseti(L, -2, i + 1);
}
return 1;
}
// ok = micMonitor(on [, volume]) Plays what the microphone hears through the speakers, volume 0 to 1.
// Most hardware adds a delay a singer can hear, so it is off until asked for.
static int32_t apiMicMonitor(lua_State *L) {
bool on = false;
float volume = 1.0f;
bool ok = false;
_argCheck(L, "micMonitor", 1, 2);
on = _argBoolean(L, "micMonitor", 1);
if (lua_gettop(L) >= 2) {
volume = (float)_argNumber(L, "micMonitor", 2);
}
ok = micMonitor(on, volume);
_luaTrace(L, "micMonitor", "%s at %.2f%s", on ? "on" : "off", volume, ok ? "" : ", refused");
lua_pushboolean(L, ok);
return 1;
}
// opened = micOpen([name]) Starts listening to a microphone by name (micList), or the machine's default.
static int32_t apiMicOpen(lua_State *L) {
const char *name = NULL;
bool opened = false;
_argCheck(L, "micOpen", 0, 1);
if ((lua_gettop(L) >= 1) && !lua_isnil(L, 1)) {
name = _argString(L, "micOpen", 1);
}
opened = micOpen(name);
_luaTrace(L, "micOpen", "%s %s", (name != NULL) ? name : "default", opened ? "opened" : "failed");
lua_pushboolean(L, opened);
return 1;
}
// hz, clarity = micPitch() The note being sung, as its fundamental in Hz, and how clearly it is one
// note, 0 to 1; 0 Hz when nothing voiced is heard.
static int32_t apiMicPitch(lua_State *L) {
float hz = 0.0f;
float clarity = 0.0f;
_argCheck(L, "micPitch", 0, 0);
micPitch(&hz, &clarity);
lua_pushnumber(L, hz);
lua_pushnumber(L, clarity);
return 2;
}
// samples = micRead([max]) What was heard since the last read, as 16-bit mono at 48 kHz in a string
// (two bytes a sample, little endian), at most max samples.
static int32_t apiMicRead(lua_State *L) {
int16_t *samples = NULL;
int32_t max = MIC_RATE * MIC_RING_SECONDS;
int32_t count = 0;
_argCheck(L, "micRead", 0, 1);
if (lua_gettop(L) >= 1) {
max = _argInteger(L, "micRead", 1);
if ((max < 0) || (max > MIC_RATE * MIC_RING_SECONDS)) {
_luaDie(L, "micRead", "max must be 0 to %d samples: %d", MIC_RATE * MIC_RING_SECONDS, max);
}
}
samples = (int16_t *)malloc((size_t)(max > 0 ? max : 1) * sizeof(int16_t));
if (samples == NULL) {
_luaDie(L, "micRead", "Out of memory.");
}
count = micRead(samples, max);
lua_pushlstring(L, (const char *)samples, (size_t)count * sizeof(int16_t));
free(samples);
return 1;
}
// started = micRecordStart(name) Writes everything heard from now on to a WAV file, named like any
// file the game writes, until micRecordStop.
static int32_t apiMicRecordStart(lua_State *L) {
char *path = NULL;
bool started = false;
_argCheck(L, "micRecordStart", 1, 1);
path = vfsFilePath(_argString(L, "micRecordStart", 1), true);
if (path == NULL) {
_luaDie(L, "micRecordStart", "%s reaches outside the game", lua_tostring(L, 1));
}
started = micRecordStart(path);
_luaTrace(L, "micRecordStart", "%s %s", path, started ? "recording" : "failed");
free(path);
lua_pushboolean(L, started);
return 1;
}
// micRecordStop() Finishes the recording.
static int32_t apiMicRecordStop(lua_State *L) {
_argCheck(L, "micRecordStop", 0, 0);
micRecordStop();
_luaTrace(L, "micRecordStop", "Stopped.");
return 0;
}
// micSetLatency(milliseconds | nil) Sets the microphone's latency for this machine, as a service menu
// does from a measurement taken elsewhere; nil forgets it.
static int32_t apiMicSetLatency(lua_State *L) {
int32_t value = MIC_LATENCY_UNKNOWN;
_argCheck(L, "micSetLatency", 1, 1);
if (!lua_isnil(L, 1)) {
value = _argInteger(L, "micSetLatency", 1);
if ((value < 0) || (value > MIC_LATENCY_MAX)) {
_luaDie(L, "micSetLatency", "Microphone latency must be 0 to %d milliseconds: %d", MIC_LATENCY_MAX, value);
}
}
micSetLatency(value);
_saveMachineSettings(videoGetAudioCalibration());
_luaTrace(L, "micSetLatency", "%d", value);
return 0;
}
// modelDelete(model): frees its meshes and materials; instances keep their nodes, bare
// midiCloseInput() Stops listening to the MIDI input port.
static int32_t apiMidiCloseInput(lua_State *L) {
_argCheck(L, "midiCloseInput", 0, 0);
midiIoCloseInput();
_luaTrace(L, "midiCloseInput", "Closed.");
return 0;
}
// midiCloseOutput() Releases the MIDI output port.
static int32_t apiMidiCloseOutput(lua_State *L) {
_argCheck(L, "midiCloseOutput", 0, 0);
midiIoCloseOutput();
_luaTrace(L, "midiCloseOutput", "Closed.");
return 0;
}
// sent = midiControlChange(channel, control, value) Moves a controller on the open output port.
static int32_t apiMidiControlChange(lua_State *L) {
uint8_t message[3];
_argCheck(L, "midiControlChange", 3, 3);
message[0] = (uint8_t)(MIDI_CONTROL_CHANGE | _midiChannel(L, "midiControlChange", 1));
message[1] = _midiData(L, "midiControlChange", 2);
message[2] = _midiData(L, "midiControlChange", 3);
lua_pushboolean(L, midiIoSend(message, sizeof(message)));
return 1;
}
// count = midiInputCount() How many MIDI input ports this machine has.
static int32_t apiMidiInputCount(lua_State *L) {
_argCheck(L, "midiInputCount", 0, 0);
lua_pushinteger(L, midiIoInputCount());
return 1;
}
// name = midiInputName(index) The name of an input port, or nil when the index names none.
static int32_t apiMidiInputName(lua_State *L) {
const char *name = NULL;
_argCheck(L, "midiInputName", 1, 1);
name = midiIoInputName(_argInteger(L, "midiInputName", 1));
if (name == NULL) {
lua_pushnil(L);
} else {
lua_pushstring(L, name);
}
return 1;
}
// open = midiIsInputOpen() Whether a MIDI input port is open.
static int32_t apiMidiIsInputOpen(lua_State *L) {
_argCheck(L, "midiIsInputOpen", 0, 0);
lua_pushboolean(L, midiIoInputOpen());
return 1;
}
// open = midiIsOutputOpen() Whether a MIDI output port is open.
static int32_t apiMidiIsOutputOpen(lua_State *L) {
_argCheck(L, "midiIsOutputOpen", 0, 0);
lua_pushboolean(L, midiIoOutputOpen());
return 1;
}
// sent = midiNoteOff(channel, key) Releases a note on the open output port.
static int32_t apiMidiNoteOff(lua_State *L) {
uint8_t message[3];
_argCheck(L, "midiNoteOff", 2, 2);
message[0] = (uint8_t)(MIDI_NOTE_OFF | _midiChannel(L, "midiNoteOff", 1));
message[1] = _midiData(L, "midiNoteOff", 2);
message[2] = 0;
lua_pushboolean(L, midiIoSend(message, sizeof(message)));
return 1;
}
// sent = midiNoteOn(channel, key, velocity) Sounds a note on the open output port.
static int32_t apiMidiNoteOn(lua_State *L) {
uint8_t message[3];
_argCheck(L, "midiNoteOn", 3, 3);
message[0] = (uint8_t)(MIDI_NOTE_ON | _midiChannel(L, "midiNoteOn", 1));
message[1] = _midiData(L, "midiNoteOn", 2);
message[2] = _midiData(L, "midiNoteOn", 3);
lua_pushboolean(L, midiIoSend(message, sizeof(message)));
return 1;
}
// opened = midiOpenInput(index) Listens to one input port; onMidiMessage then fires for what arrives.
static int32_t apiMidiOpenInput(lua_State *L) {
bool opened = false;
_argCheck(L, "midiOpenInput", 1, 1);
opened = midiIoOpenInput(_argInteger(L, "midiOpenInput", 1));
_luaTrace(L, "midiOpenInput", "%s", opened ? "opened" : "failed");
lua_pushboolean(L, opened);
return 1;
}
// opened = midiOpenOutput(index) Takes one output port for the midi* sending calls.
static int32_t apiMidiOpenOutput(lua_State *L) {
bool opened = false;
_argCheck(L, "midiOpenOutput", 1, 1);
opened = midiIoOpenOutput(_argInteger(L, "midiOpenOutput", 1));
_luaTrace(L, "midiOpenOutput", "%s", opened ? "opened" : "failed");
lua_pushboolean(L, opened);
return 1;
}
// count = midiOutputCount() How many MIDI output ports this machine has.
static int32_t apiMidiOutputCount(lua_State *L) {
_argCheck(L, "midiOutputCount", 0, 0);
lua_pushinteger(L, midiIoOutputCount());
return 1;
}
// name = midiOutputName(index) The name of an output port, or nil when the index names none.
static int32_t apiMidiOutputName(lua_State *L) {
const char *name = NULL;
_argCheck(L, "midiOutputName", 1, 1);
name = midiIoOutputName(_argInteger(L, "midiOutputName", 1));
if (name == NULL) {
lua_pushnil(L);
} else {
lua_pushstring(L, name);
}
return 1;
}
// sent = midiPitchBend(channel, value) Bends a channel, 0 to 16383 with 8192 in the middle.
static int32_t apiMidiPitchBend(lua_State *L) {
uint8_t message[3];
int32_t value = 0;
_argCheck(L, "midiPitchBend", 2, 2);
value = _argInteger(L, "midiPitchBend", 2);
if ((value < 0) || (value > MIDI_BEND_MAX)) {
_luaDie(L, "midiPitchBend", "A pitch bend is 0 to %d, not %d.", MIDI_BEND_MAX, value);
}
message[0] = (uint8_t)(MIDI_PITCH_BEND | _midiChannel(L, "midiPitchBend", 1));
message[1] = (uint8_t)(value & MIDI_DATA_MAX);
message[2] = (uint8_t)((value >> MIDI_DATA_BITS) & MIDI_DATA_MAX);
lua_pushboolean(L, midiIoSend(message, sizeof(message)));
return 1;
}
// sent = midiProgramChange(channel, program) Chooses the sound a channel plays.
static int32_t apiMidiProgramChange(lua_State *L) {
uint8_t message[2];
_argCheck(L, "midiProgramChange", 2, 2);
message[0] = (uint8_t)(MIDI_PROGRAM_CHANGE | _midiChannel(L, "midiProgramChange", 1));
message[1] = _midiData(L, "midiProgramChange", 2);
lua_pushboolean(L, midiIoSend(message, sizeof(message)));
return 1;
}
// midiRescan() Looks for ports again, for a device plugged in while the game is running.
static int32_t apiMidiRescan(lua_State *L) {
_argCheck(L, "midiRescan", 0, 0);
midiIoRescan();
_luaTrace(L, "midiRescan", "%d in, %d out", midiIoInputCount(), midiIoOutputCount());
return 0;
}
// sent = midiSend(byte [, byte [, byte]]) or midiSend(string)
// Any message at all, for the ones the named calls do not cover: a string carries a whole system
// exclusive message, and the numbers carry an ordinary one.
static int32_t apiMidiSend(lua_State *L) {
uint8_t message[MIDI_MESSAGE_BYTES];
const char *bytes = NULL;
size_t size = 0;
int32_t count = 0;
int32_t x = 0;
_argCheck(L, "midiSend", 1, MIDI_MESSAGE_BYTES);
if (lua_type(L, 1) == LUA_TSTRING) {
bytes = lua_tolstring(L, 1, &size);
lua_pushboolean(L, midiIoSend((const uint8_t *)bytes, size));
return 1;
}
count = lua_gettop(L);
for (x = 0; x < count; x++) {
message[x] = (uint8_t)(_argInteger(L, "midiSend", x + 1) & MIDI_BYTE_MAX);
}
lua_pushboolean(L, midiIoSend(message, (size_t)count));
return 1;
}
static int32_t apiModelDelete(lua_State *L) {
int32_t model;
_argCheck(L, "modelDelete", 1, 1);
model = _argInteger(L, "modelDelete", 1);
if (!modelDelete(model)) {
_luaDie(L, "modelDelete", "No model %d.", model);
}
return 0;
}
// names = modelGetAnimations(model): a table of animation names, in file order
static int32_t apiModelGetAnimations(lua_State *L) {
int32_t model;
int32_t count;
int32_t x;
_argCheck(L, "modelGetAnimations", 1, 1);
model = _argInteger(L, "modelGetAnimations", 1);
if (!modelValid(model)) {
_luaDie(L, "modelGetAnimations", "No model %d.", model);
}
count = modelGetAnimationCount(model);
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_pushstring(L, modelGetAnimationName(model, x));
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// node = modelInstance(model [, parent]): the model's node tree under parent; returns its root
static int32_t apiModelInstance(lua_State *L) {
int32_t model;
int32_t parent = SCENE_ROOT_NODE;
int32_t root;
_argCheck(L, "modelInstance", 1, 2);
model = _argInteger(L, "modelInstance", 1);
if (!modelValid(model)) {
_luaDie(L, "modelInstance", "No model %d.", model);
}
if (lua_gettop(L) >= 2) {
parent = _argNode(L, "modelInstance", 2);
}
if (modelIsSkinned(model)) {
_capabilityUsed(CAPABILITY_SKIN);
}
root = modelInstance(model, parent);
if (root < 0) {
_luaDie(L, "modelInstance", "Unable to instance model %d.", model);
}
_luaTrace(L, "modelInstance", "%d -> node %d under %d", model, root, parent);
lua_pushinteger(L, root);
return 1;
}
// model = modelLoad(name): a self-contained .glb through the vfs
static int32_t apiModelLoad(lua_State *L) {
const char *name;
int32_t model;
_argCheck(L, "modelLoad", 1, 1);
name = _argString(L, "modelLoad", 1);
model = modelLoad(name);
if (model < 0) {
_luaDie(L, "modelLoad", "%s", modelLastError());
}
_luaTrace(L, "modelLoad", "%d %s", model, name);
lua_pushinteger(L, model);
return 1;
}
// seconds = musicGetPosition(id) How far into the track the listener is: the mixer's position less
// what is still on its way to the speakers (the device queue, the machine's calibration, and the
// game's delay), so a picture timed to it lands with the sound. The mixer moves its position a
// buffer at a time, so between moves the answer runs on with the clock, no further than
// MUSIC_GLIDE_MAX_MS, and a paused track holds still. A track that is not playing answers -1.
static int32_t apiMusicGetPosition(lua_State *L) {
MusicT *music = NULL;
Sint64 frames = 0;
uint64_t now = 0;
double seconds = -1.0;
_argCheck(L, "musicGetPosition", 1, 1);
music = _argMusic(L, "musicGetPosition", 1);
if (MIX_TrackPlaying(music->track) || MIX_TrackPaused(music->track)) {
frames = MIX_GetTrackPlaybackPosition(music->track);
now = utilTicks();
if (frames != music->heardFrames) {
music->heardFrames = frames;
music->heardTicks = now;
}
seconds = (double)MIX_TrackFramesToMS(music->track, frames);
if (!MIX_TrackPaused(music->track)) {
seconds += (double)SDL_min(now - music->heardTicks, (uint64_t)MUSIC_GLIDE_MAX_MS);
}
seconds -= (double)(videoGetAudioLatency() + videoGetAudioCalibration() + videoGetAudioDelay());
seconds = SDL_max(seconds, 0.0) / 1000.0;
}
_luaTrace(L, "musicGetPosition", "%d %f", music->id, seconds);
lua_pushnumber(L, seconds);
return 1;
}
// playing = musicIsPlaying([id]) Hypseus extension. With no handle, whether any music is playing.
static int32_t apiMusicIsPlaying(lua_State *L) {
MusicT *music = NULL;
MusicT *next = NULL;
bool playing = false;
_argCheck(L, "musicIsPlaying", 0, 1);
if (lua_gettop(L) == 1) {
playing = MIX_TrackPlaying(_argMusic(L, "musicIsPlaying", 1)->track);
} else {
HASH_ITER(hh, _global.musicList, music, next) {
playing |= MIX_TrackPlaying(music->track);
}
}
_luaTrace(L, "musicIsPlaying", "%d", playing);
lua_pushboolean(L, playing);
return 1;
}
// id = musicLoad(filename) Hypseus extension. Music is loaded and played apart from the sound
// effects and carries its own volume, which is what Hypseus's second mixer gives it.
static int32_t apiMusicLoad(lua_State *L) {
const char *name = NULL;
MusicT *music = NULL;
SDL_IOStream *io = NULL;
_argCheck(L, "musicLoad", 1, 1);
name = _argString(L, "musicLoad", 1);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "musicLoad", "Unable to open %s", name);
}
music = (MusicT *)calloc(1, sizeof(MusicT));
if (!music) {
SDL_CloseIO(io);
_luaDie(L, "musicLoad", "Unable to allocate new music.");
}
music->audio = decodeAudioIO(videoGetMixer(), io, true);
if (!music->audio) {
free(music);
_luaDie(L, "musicLoad", "%s", SDL_GetError());
}
music->track = MIX_CreateTrack(videoGetMixer());
if (!music->track) {
MIX_DestroyAudio(music->audio);
free(music);
_luaDie(L, "musicLoad", "%s", SDL_GetError());
}
MIX_TagTrack(music->track, MUSIC_TAG);
MIX_SetTrackGain(music->track, _mixerGain(_global.musicVolume, MUSIC_MAX_VOLUME));
music->id = _global.nextMusicId++;
HASH_ADD_INT(_global.musicList, id, music);
_luaTrace(L, "musicLoad", "%d %s", music->id, name);
lua_pushinteger(L, music->id);
return 1;
}
// musicPause([id]) Hypseus extension. With no handle every piece of music pauses.
static int32_t apiMusicPause(lua_State *L) {
MusicT *music = NULL;
MusicT *next = NULL;
_argCheck(L, "musicPause", 0, 1);
if (lua_gettop(L) == 1) {
music = _argMusic(L, "musicPause", 1);
MIX_PauseTrack(music->track);
} else {
HASH_ITER(hh, _global.musicList, music, next) {
MIX_PauseTrack(music->track);
}
}
_luaTrace(L, "musicPause", "%d", (lua_gettop(L) == 1) ? music->id : -1);
return 0;
}
// id = musicPlay(id [, loops]) Hypseus extension. loops means what soundPlay means: 0 (the
// default) plays once, -1 forever, N repeats N more times. Hypseus clamps the count to -1 to 64 and
// so does this, so a game asking for more gets what it would have got there. Answers the handle it
// was given, as Hypseus does: the games keep it (sndHandle = musicPlay(music)) to stop the piece.
static int32_t apiMusicPlay(lua_State *L) {
MusicT *music = NULL;
int32_t loops = 0;
SDL_PropertiesID options;
_argCheck(L, "musicPlay", 1, 2);
music = _argMusic(L, "musicPlay", 1);
if (lua_gettop(L) >= 2) {
loops = SDL_clamp(_argInteger(L, "musicPlay", 2), SOUND_LOOP_FOREVER, MUSIC_LOOP_MAX);
}
MIX_SetTrackAudio(music->track, music->audio);
MIX_SetTrackGain(music->track, _mixerGain(_global.musicVolume, MUSIC_MAX_VOLUME));
options = SDL_CreateProperties();
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loops);
if (!MIX_PlayTrack(music->track, options)) {
SDL_DestroyProperties(options);
_luaDie(L, "musicPlay", "%s", SDL_GetError());
}
SDL_DestroyProperties(options);
_luaTrace(L, "musicPlay", "%d loops %d", music->id, loops);
lua_pushinteger(L, music->id);
return 1;
}
// musicResume([id]) Hypseus extension. With no handle every paused piece of music resumes, where
// Hypseus's own zero argument form does nothing at all.
static int32_t apiMusicResume(lua_State *L) {
MusicT *music = NULL;
MusicT *next = NULL;
_argCheck(L, "musicResume", 0, 1);
if (lua_gettop(L) == 1) {
music = _argMusic(L, "musicResume", 1);
MIX_ResumeTrack(music->track);
} else {
HASH_ITER(hh, _global.musicList, music, next) {
MIX_ResumeTrack(music->track);
}
}
_luaTrace(L, "musicResume", "%d", (lua_gettop(L) == 1) ? music->id : -1);
return 0;
}
// volume = musicSetVolume(volume) Hypseus extension. 0 to MUSIC_MAX_VOLUME, Hypseus's own scale,
// over every piece of music at once and nothing else: soundSetVolume does not move music and this
// does not move a sound effect. Returns the level it took.
static int32_t apiMusicSetVolume(lua_State *L) {
int32_t volume = 0;
_argCheck(L, "musicSetVolume", 1, 1);
volume = _argInteger(L, "musicSetVolume", 1);
if ((volume < 0) || (volume > MUSIC_MAX_VOLUME)) {
_luaDie(L, "musicSetVolume", "Invalid music volume value: %d", volume);
}
_global.musicVolume = volume;
MIX_SetTagGain(videoGetMixer(), MUSIC_TAG, _mixerGain(_global.musicVolume, MUSIC_MAX_VOLUME));
_luaTrace(L, "musicSetVolume", "%d", _global.musicVolume);
lua_pushinteger(L, _global.musicVolume);
return 1;
}
// musicStop([id [, fadeMs]]) Hypseus extension. With no handle (or a nil one) every piece of
// music stops, as Hypseus stops whatever its music mixer is playing. A fade is in milliseconds.
static int32_t apiMusicStop(lua_State *L) {
MusicT *music = NULL;
MusicT *next = NULL;
int32_t fade = 0;
bool one = false;
_argCheck(L, "musicStop", 0, 2);
if (lua_gettop(L) >= 2) {
fade = _argInteger(L, "musicStop", 2);
if (fade < 0) {
_luaDie(L, "musicStop", "A fade cannot be negative: %d", fade);
}
}
one = !lua_isnoneornil(L, 1);
if (one) {
music = _argMusic(L, "musicStop", 1);
MIX_StopTrack(music->track, MIX_TrackMSToFrames(music->track, fade));
} else {
HASH_ITER(hh, _global.musicList, music, next) {
MIX_StopTrack(music->track, MIX_TrackMSToFrames(music->track, fade));
}
}
_luaTrace(L, "musicStop", "%d fade %d", one ? music->id : -1, fade);
return 0;
}
// musicUnload(id) Hypseus extension. Stops the music first, as Hypseus frees a playing chunk.
static int32_t apiMusicUnload(lua_State *L) {
MusicT *music = NULL;
_argCheck(L, "musicUnload", 1, 1);
music = _argMusic(L, "musicUnload", 1);
_luaTrace(L, "musicUnload", "%d", music->id);
_musicDestroy(music);
return 0;
}
// name = mouseGetName(mouse) What the driver calls the device, or nil for a device that is not there.
static int32_t apiMouseGetName(lua_State *L) {
int32_t m = 0;
_argCheck(L, "mouseGetName", 1, 1);
m = _argInteger(L, "mouseGetName", 1);
if ((m < 0) || (m >= MAX_MICE)) {
_luaDie(L, "mouseGetName", "Invalid mouse index: %d", m);
}
_luaTrace(L, "mouseGetName", "%d %s", m, (m < _global.mouseCount) ? _global.mice[m].name : "nil");
if (m < _global.mouseCount) {
lua_pushstring(L, _global.mice[m].name);
} else {
lua_pushnil(L);
}
return 1;
}
// x, y = mouseGetPosition(mouse)
// mouseGetPadding()
// Always 0, for the reason controllerGetPadding gives: Hypseus adds this offset to the input codes
// its mice report in relative mode, and Singe's codes carry no such offset to subtract.
static int32_t apiMouseGetPadding(lua_State *L) {
_argCheck(L, "mouseGetPadding", 0, 0);
lua_pushinteger(L, 0);
_luaTrace(L, "mouseGetPadding", "0");
return 1;
}
static int32_t apiMouseGetPosition(lua_State *L) {
int32_t m = 0;
int32_t x = 0;
int32_t y = 0;
_argCheck(L, "mouseGetPosition", 1, 1);
m = _argInteger(L, "mouseGetPosition", 1);
if ((m < 0) || (m >= MAX_MICE)) {
_luaDie(L, "mouseGetPosition", "Invalid mouse index: %d", m);
}
x = _global.axisCache[AXIS_INDEX_MOUSE(m, 0)];
y = _global.axisCache[AXIS_INDEX_MOUSE(m, 1)];
_luaTrace(L, "mouseGetPosition", "%d %d %d", m, x, y);
lua_pushinteger(L, x);
lua_pushinteger(L, y);
return 2;
}
// count = mouseHowMany()
static int32_t apiMouseHowMany(lua_State *L) {
_luaTrace(L, "mouseHowMany", "%d", _global.mouseCount);
lua_pushinteger(L, _global.mouseCount);
return 1;
}
// mouseSetCaptured(captured) Grabs and hides the cursor.
static int32_t apiMouseSetCaptured(lua_State *L) {
_argCheck(L, "mouseSetCaptured", 1, 1);
_setMouseCaptured(_argBoolean(L, "mouseSetCaptured", 1));
_luaTrace(L, "mouseSetCaptured", "%d", _global.mouseGrabbed);
return 0;
}
// mouseSetEnabled(enabled) Framework.singe aliases mouseEnable() and mouseDisable() to this.
static int32_t apiMouseSetEnabled(lua_State *L) {
_argCheck(L, "mouseSetEnabled", 1, 1);
_global.mouseEnabled = _argBoolean(L, "mouseSetEnabled", 1) && !_global.conf->noMouse;
_luaTrace(L, "mouseSetEnabled", "%d", _global.mouseEnabled);
return 0;
}
// mouseSetMode(MOUSE_SINGLE | MOUSE_MANY)
static int32_t apiMouseSetMode(lua_State *L) {
int32_t mode = 0;
_argCheck(L, "mouseSetMode", 1, 1);
mode = _argInteger(L, "mouseSetMode", 1);
if ((mode != MOUSE_SINGLE) && (mode != MOUSE_MANY)) {
_luaDie(L, "mouseSetMode", "Unknown mouse mode: %d", mode);
}
// --manymouse is the user's decision and outranks the game's: a two gun cabinet whose game
// never asks for the many mouse mode can still be made to work from outside.
_global.mouseMode = (_global.conf->manyMouse) ? MOUSE_MANY : (MouseModeE)mode;
_luaTrace(L, "mouseSetMode", "%d", _global.mouseMode);
return 0;
}
// navAddNode(nav, node): the node's mesh and its children's become walkable geometry for navBuild
static int32_t apiNavAddNode(lua_State *L) {
_argCheck(L, "navAddNode", 2, 2);
if (!navAddNode(_argNav(L, "navAddNode", 1), _argNode(L, "navAddNode", 2))) {
_luaDie(L, "navAddNode", "The mesh is already built.");
}
return 0;
}
static int32_t apiNavAgentDelete(lua_State *L) {
_argCheck(L, "navAgentDelete", 1, 1);
navAgentDelete(_argNavAgent(L, "navAgentDelete", 1));
return 0;
}
// x, y, z = navAgentGetVelocity(agent)
static int32_t apiNavAgentGetVelocity(lua_State *L) {
Vec3T velocity = vec3(0.0f, 0.0f, 0.0f);
_argCheck(L, "navAgentGetVelocity", 1, 1);
navAgentGetVelocity(_argNavAgent(L, "navAgentGetVelocity", 1), &velocity);
return _pushVec3(L, velocity);
}
static int32_t apiNavAgentIsArrived(lua_State *L) {
_argCheck(L, "navAgentIsArrived", 1, 1);
lua_pushboolean(L, navAgentIsArrived(_argNavAgent(L, "navAgentIsArrived", 1)));
return 1;
}
// navAgentMoveTo(agent, x, y, z): true when a path exists
static int32_t apiNavAgentMoveTo(lua_State *L) {
int32_t agent;
_argCheck(L, "navAgentMoveTo", 4, 4);
agent = _argNavAgent(L, "navAgentMoveTo", 1);
lua_pushboolean(L, navAgentMoveTo(agent, vec3((float)_argNumber(L, "navAgentMoveTo", 2), (float)_argNumber(L, "navAgentMoveTo", 3), (float)_argNumber(L, "navAgentMoveTo", 4))));
return 1;
}
// agent = navAgentNew(nav, node, radius, height, speed): the node walks the mesh from where it stands
static int32_t apiNavAgentNew(lua_State *L) {
int32_t nav;
int32_t node;
int32_t agent;
_argCheck(L, "navAgentNew", 5, 5);
nav = _argNav(L, "navAgentNew", 1);
node = _argNode(L, "navAgentNew", 2);
agent = navAgentNew(nav, node, (float)_argNumber(L, "navAgentNew", 3), (float)_argNumber(L, "navAgentNew", 4), (float)_argNumber(L, "navAgentNew", 5));
if (agent < 0) {
_luaDie(L, "navAgentNew", "No agent available (is the mesh built, and are fewer than %d agents on it?).", NAV_MAX_CROWD_AGENTS);
}
lua_pushinteger(L, agent);
return 1;
}
// navAgentSetPlayer(agent, player): steer the player controller on the node instead of placing the node
static int32_t apiNavAgentSetPlayer(lua_State *L) {
_argCheck(L, "navAgentSetPlayer", 2, 2);
navAgentSetPlayer(_argNavAgent(L, "navAgentSetPlayer", 1), _argBoolean(L, "navAgentSetPlayer", 2));
return 0;
}
static int32_t apiNavAgentStop(lua_State *L) {
_argCheck(L, "navAgentStop", 1, 1);
navAgentStop(_argNavAgent(L, "navAgentStop", 1));
return 0;
}
// navBuild(nav): bakes the walkable mesh from everything added
static int32_t apiNavBuild(lua_State *L) {
int32_t nav;
_argCheck(L, "navBuild", 1, 1);
nav = _argNav(L, "navBuild", 1);
if (!navBuild(nav)) {
_luaDie(L, "navBuild", "Unable to bake navigation mesh %d (nothing walkable added, or already built).", nav);
}
return 0;
}
static int32_t apiNavDelete(lua_State *L) {
_argCheck(L, "navDelete", 1, 1);
navDelete(_argNav(L, "navDelete", 1));
return 0;
}
// navDraw(nav [, r, g, b]): the baked mesh's triangles as lines over the scene for this frame, cyan by default
static int32_t apiNavDraw(lua_State *L) {
int32_t nav = 0;
int32_t capacity = 0;
int32_t count = 0;
int32_t x = 0;
Vec3T *grown = NULL;
Vec3T *vertices = NULL;
uint8_t r = 0;
uint8_t g = COLOR_BYTE_MAX;
uint8_t b = COLOR_BYTE_MAX;
_argCheck(L, "navDraw", 1, 4);
nav = _argNav(L, "navDraw", 1);
_argOptionalColor(L, "navDraw", 2, &r, &g, &b);
// The triangle list is kept between frames (this is drawn every frame) and doubled until the mesh fits.
capacity = SDL_max(_global.navDrawCapacity, NAV_DRAW_VERTICES);
for (;;) {
if (capacity > _global.navDrawCapacity) {
grown = SDL_realloc(_global.navDrawVertices, (size_t)capacity * sizeof(Vec3T));
if (grown == NULL) {
_luaDie(L, "navDraw", "Out of memory.");
}
_global.navDrawVertices = grown;
_global.navDrawCapacity = capacity;
}
vertices = _global.navDrawVertices;
count = navGetPolygons(nav, vertices, capacity);
if (count + 3 <= capacity) {
break;
}
capacity *= 2;
}
for (x = 0; x + 2 < count; x += 3) {
sceneDrawLine(vertices[x], vertices[x + 1], r, g, b);
sceneDrawLine(vertices[x + 1], vertices[x + 2], r, g, b);
sceneDrawLine(vertices[x + 2], vertices[x], r, g, b);
}
return 0;
}
// nav = navLoad(name, agentRadius, agentHeight): a mesh navSave wrote, from the game or its data folder
static int32_t apiNavLoad(lua_State *L) {
const char *name = NULL;
char *data = NULL;
char *path = NULL;
size_t size = 0;
int32_t nav = 0;
_argCheck(L, "navLoad", 3, 3);
name = _argString(L, "navLoad", 1);
data = vfsRead(name, &size);
if (data == NULL) {
// Not in the game: maybe in its data folder, where navSave writes.
path = utilCreateString("%s%s", _global.conf->dataDir, name);
data = utilReadFile(path, &size);
free(path);
}
if (data == NULL) {
_luaDie(L, "navLoad", "Unable to read %s", name);
}
nav = navLoad(data, size, (float)_argNumber(L, "navLoad", 2), (float)_argNumber(L, "navLoad", 3));
free(data);
if (nav < 0) {
_luaDie(L, "navLoad", "%s is not a navigation mesh, or every mesh slot is in use.", name);
}
lua_pushinteger(L, nav);
return 1;
}
// x, y, z = navNearest(nav, x, y, z): the closest walkable point, or nil
static int32_t apiNavNearest(lua_State *L) {
Vec3T out;
_argCheck(L, "navNearest", 4, 4);
if (!navNearest(_argNav(L, "navNearest", 1), vec3((float)_argNumber(L, "navNearest", 2), (float)_argNumber(L, "navNearest", 3), (float)_argNumber(L, "navNearest", 4)), &out)) {
lua_pushnil(L);
return 1;
}
return _pushVec3(L, out);
}
// nav = navNew(agentRadius, agentHeight, maxSlope, maxStep)
static int32_t apiNavNew(lua_State *L) {
int32_t nav;
_argCheck(L, "navNew", 4, 4);
nav = navNew((float)_argNumber(L, "navNew", 1), (float)_argNumber(L, "navNew", 2), (float)_argNumber(L, "navNew", 3), (float)_argNumber(L, "navNew", 4));
if (nav < 0) {
_luaDie(L, "navNew", "No navigation mesh slot available.");
}
lua_pushinteger(L, nav);
return 1;
}
// points = navPath(nav, x0, y0, z0, x1, y1, z1): a table of {x, y, z} corners, or nil for no path
static int32_t apiNavPath(lua_State *L) {
Vec3T points[NAV_MAX_PATH];
int32_t count = 0;
int32_t x = 0;
_argCheck(L, "navPath", 7, 7);
count = navPath(_argNav(L, "navPath", 1), vec3((float)_argNumber(L, "navPath", 2), (float)_argNumber(L, "navPath", 3), (float)_argNumber(L, "navPath", 4)), vec3((float)_argNumber(L, "navPath", 5), (float)_argNumber(L, "navPath", 6), (float)_argNumber(L, "navPath", 7)), points, (int32_t)SDL_arraysize(points));
if (count < 0) {
lua_pushnil(L);
return 1;
}
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_createtable(L, 3, 0);
lua_pushnumber(L, points[x].x);
lua_rawseti(L, -2, 1);
lua_pushnumber(L, points[x].y);
lua_rawseti(L, -2, 2);
lua_pushnumber(L, points[x].z);
lua_rawseti(L, -2, 3);
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// x, y, z = navRandomPoint(nav)
static int32_t apiNavRandomPoint(lua_State *L) {
Vec3T out;
_argCheck(L, "navRandomPoint", 1, 1);
if (!navRandomPoint(_argNav(L, "navRandomPoint", 1), &out)) {
lua_pushnil(L);
return 1;
}
return _pushVec3(L, out);
}
// blocked, x, y, z = navRaycast(nav, x0, y0, z0, x1, y1, z1): whether a straight walk leaves the mesh, and where
static int32_t apiNavRaycast(lua_State *L) {
Vec3T hit = vec3(0.0f, 0.0f, 0.0f);
bool blocked;
_argCheck(L, "navRaycast", 7, 7);
blocked = navRaycast(_argNav(L, "navRaycast", 1), vec3((float)_argNumber(L, "navRaycast", 2), (float)_argNumber(L, "navRaycast", 3), (float)_argNumber(L, "navRaycast", 4)), vec3((float)_argNumber(L, "navRaycast", 5), (float)_argNumber(L, "navRaycast", 6), (float)_argNumber(L, "navRaycast", 7)), &hit);
lua_pushboolean(L, blocked);
if (!blocked) {
return 1;
}
return 1 + _pushVec3(L, hit);
}
// navSave(nav, name): the baked mesh into the game's data folder, for navLoad
static int32_t apiNavSave(lua_State *L) {
int32_t nav;
char *path;
bool ok;
_argCheck(L, "navSave", 2, 2);
nav = _argNav(L, "navSave", 1);
path = utilCreateString("%s%s", _global.conf->dataDir, _argString(L, "navSave", 2));
ok = navSave(nav, path);
if (!ok) {
_luaDie(L, "navSave", "Unable to write %s", path);
}
free(path);
return 0;
}
// lists, report = netTrustSystem(context) Adds the machine's own trusted roots, and the data
// directory's cacerts.local.pem when there is one, to a LuaSec client context that already holds
// the shipped bundle, so a connection verifies when either knows the server's root. lists is how
// many were added; report names what was consulted, one "what path" per line (PLAN.md section 61).
static int32_t apiNetTrustSystem(lua_State *L) {
SSL_CTX *context = NULL;
int32_t lists = 0;
char report[NET_TRUST_REPORT_SIZE];
_argCheck(L, "netTrustSystem", 1, 1);
context = lsec_checkcontext(L, 1);
lists = netTrustSystem(context, _global.conf->dataDir, report, sizeof(report));
_luaTrace(L, "netTrustSystem", "%d", lists);
lua_pushinteger(L, lists);
lua_pushstring(L, report);
return 2;
}
// nodeDelete(node): the node and everything under it
static int32_t apiNodeDelete(lua_State *L) {
int32_t node;
_argCheck(L, "nodeDelete", 1, 1);
node = _argNode(L, "nodeDelete", 1);
if (!nodeDelete(node)) {
_luaDie(L, "nodeDelete", "The root node cannot be deleted.");
}
_luaTrace(L, "nodeDelete", "%d", node);
return 0;
}
// node = nodeFind(name [, root]): by name, below root; nil when absent
static int32_t apiNodeFind(lua_State *L) {
const char *name = NULL;
int32_t root = SCENE_ROOT_NODE;
int32_t found = 0;
_argCheck(L, "nodeFind", 1, 2);
name = _argString(L, "nodeFind", 1);
if (lua_gettop(L) >= 2) {
root = _argNode(L, "nodeFind", 2);
}
found = nodeFind(root, name);
if (found < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, found);
}
return 1;
}
// children = nodeGetChildren(node): a table of handles
static int32_t apiNodeGetChildren(lua_State *L) {
int32_t node;
int32_t count;
int32_t x;
_argCheck(L, "nodeGetChildren", 1, 1);
node = _argNode(L, "nodeGetChildren", 1);
count = nodeGetChildCount(node);
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_pushinteger(L, nodeGetChild(node, x));
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// weight = nodeGetMorph(node, nameOrIndex)
static int32_t apiNodeGetMorph(lua_State *L) {
int32_t node;
int32_t target;
_argCheck(L, "nodeGetMorph", 2, 2);
node = _argNode(L, "nodeGetMorph", 1);
target = _argMorph(L, "nodeGetMorph", node, 2);
lua_pushnumber(L, nodeGetMorphWeight(node, target));
return 1;
}
// names = nodeGetMorphs(node): the mesh's morph target names, in order (empty strings when unnamed)
static int32_t apiNodeGetMorphs(lua_State *L) {
int32_t node;
int32_t count;
int32_t x;
_argCheck(L, "nodeGetMorphs", 1, 1);
node = _argNode(L, "nodeGetMorphs", 1);
count = nodeGetMorphCount(node);
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_pushstring(L, meshGetMorphName(nodeGetMesh(node), x));
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// name = nodeGetName(node)
static int32_t apiNodeGetName(lua_State *L) {
_argCheck(L, "nodeGetName", 1, 1);
lua_pushstring(L, nodeGetName(_argNode(L, "nodeGetName", 1)));
return 1;
}
// parent = nodeGetParent(node): nil for the root
static int32_t apiNodeGetParent(lua_State *L) {
int32_t parent;
_argCheck(L, "nodeGetParent", 1, 1);
parent = nodeGetParent(_argNode(L, "nodeGetParent", 1));
if (parent < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, parent);
}
return 1;
}
// x, y, z = nodeGetPosition(node)
static int32_t apiNodeGetPosition(lua_State *L) {
_argCheck(L, "nodeGetPosition", 1, 1);
return _pushVec3(L, nodeGetPosition(_argNode(L, "nodeGetPosition", 1)));
}
// x, y, z, w = nodeGetQuaternion(node)
static int32_t apiNodeGetQuaternion(lua_State *L) {
QuatT q;
_argCheck(L, "nodeGetQuaternion", 1, 1);
q = nodeGetRotation(_argNode(L, "nodeGetQuaternion", 1));
lua_pushnumber(L, q.x);
lua_pushnumber(L, q.y);
lua_pushnumber(L, q.z);
lua_pushnumber(L, q.w);
return 4;
}
// x, y, z = nodeGetRotation(node): Euler degrees
static int32_t apiNodeGetRotation(lua_State *L) {
float x;
float y;
float z;
_argCheck(L, "nodeGetRotation", 1, 1);
quatToEuler(nodeGetRotation(_argNode(L, "nodeGetRotation", 1)), &x, &y, &z);
lua_pushnumber(L, x);
lua_pushnumber(L, y);
lua_pushnumber(L, z);
return 3;
}
// x, y, z = nodeGetScale(node)
static int32_t apiNodeGetScale(lua_State *L) {
_argCheck(L, "nodeGetScale", 1, 1);
return _pushVec3(L, nodeGetScale(_argNode(L, "nodeGetScale", 1)));
}
// x, y, z = nodeGetWorldPosition(node): as of the last rendered frame
static int32_t apiNodeGetWorldPosition(lua_State *L) {
_argCheck(L, "nodeGetWorldPosition", 1, 1);
return _pushVec3(L, nodeGetWorldPosition(_argNode(L, "nodeGetWorldPosition", 1)));
}
// nodeLookAt(node, x, y, z): points the node's -Z at a world point
static int32_t apiNodeLookAt(lua_State *L) {
_argCheck(L, "nodeLookAt", 4, 4);
nodeLookAt(_argNode(L, "nodeLookAt", 1), _argVec3(L, "nodeLookAt", 2));
return 0;
}
// nodeMove(node, dx, dy, dz): along the node's own axes
static int32_t apiNodeMove(lua_State *L) {
_argCheck(L, "nodeMove", 4, 4);
nodeMove(_argNode(L, "nodeMove", 1), _argVec3(L, "nodeMove", 2));
return 0;
}
// node = nodeNew([parent]): a new node at its parent's origin
static int32_t apiNodeNew(lua_State *L) {
int32_t parent = SCENE_ROOT_NODE;
int32_t node;
_argCheck(L, "nodeNew", 0, 1);
if (lua_gettop(L) >= 1) {
parent = _argNode(L, "nodeNew", 1);
}
node = nodeNew(parent);
if (node < 0) {
_luaDie(L, "nodeNew", "Unable to create the node.");
}
_luaTrace(L, "nodeNew", "%d under %d", node, parent);
lua_pushinteger(L, node);
return 1;
}
// nodeRotate(node, dx, dy, dz): degrees, about the node's own axes
static int32_t apiNodeRotate(lua_State *L) {
_argCheck(L, "nodeRotate", 4, 4);
nodeRotate(_argNode(L, "nodeRotate", 1), _argEuler(L, "nodeRotate", 2));
return 0;
}
// nodeSetBillboard(node, BILLBOARD_*): the node turns to face the camera
static int32_t apiNodeSetBillboard(lua_State *L) {
int32_t node = 0;
int32_t mode = 0;
_argCheck(L, "nodeSetBillboard", 2, 2);
node = _argNode(L, "nodeSetBillboard", 1);
mode = _argInteger(L, "nodeSetBillboard", 2);
if ((mode != BILLBOARD_NONE) && (mode != BILLBOARD_ALL) && (mode != BILLBOARD_Y)) {
_luaDie(L, "nodeSetBillboard", "Mode must be BILLBOARD_NONE, BILLBOARD_ALL or BILLBOARD_Y.");
}
nodeSetBillboard(node, (BillboardE)mode);
return 0;
}
// nodeSetLayer(node, LAYER_*): the node and its children drawn in the scene, or in front of the overlay
static int32_t apiNodeSetLayer(lua_State *L) {
int32_t node = 0;
int32_t layer = 0;
_argCheck(L, "nodeSetLayer", 2, 2);
node = _argNode(L, "nodeSetLayer", 1);
layer = _argInteger(L, "nodeSetLayer", 2);
if ((layer != LAYER_SCENE) && (layer != LAYER_FRONT)) {
_luaDie(L, "nodeSetLayer", "Layer must be LAYER_SCENE or LAYER_FRONT.");
}
nodeSetLayer(node, (SceneLayerE)layer);
return 0;
}
// nodeSetMaterial(node, material): a new material on whatever mesh the node has (nil for none)
static int32_t apiNodeSetMaterial(lua_State *L) {
int32_t material = -1;
_argCheck(L, "nodeSetMaterial", 2, 2);
if (!lua_isnil(L, 2)) {
material = _argMaterial(L, "nodeSetMaterial", 2);
}
nodeSetMaterial(_argNode(L, "nodeSetMaterial", 1), material);
return 0;
}
// nodeSetMesh(node, mesh [, material])
static int32_t apiNodeSetMesh(lua_State *L) {
int32_t node;
int32_t mesh;
int32_t material = -1;
_argCheck(L, "nodeSetMesh", 2, 3);
node = _argNode(L, "nodeSetMesh", 1);
mesh = _argMesh(L, "nodeSetMesh", 2);
if (lua_gettop(L) >= 3) {
material = _argMaterial(L, "nodeSetMesh", 3);
}
nodeSetMesh(node, mesh, material);
return 0;
}
// nodeSetMorph(node, nameOrIndex, weight): how much of a morph target shows, usually 0 to 1
static int32_t apiNodeSetMorph(lua_State *L) {
int32_t node;
int32_t target;
_argCheck(L, "nodeSetMorph", 3, 3);
node = _argNode(L, "nodeSetMorph", 1);
target = _argMorph(L, "nodeSetMorph", node, 2);
_capabilityUsed(CAPABILITY_MORPH);
nodeSetMorphWeight(node, target, (float)_argNumber(L, "nodeSetMorph", 3));
return 0;
}
// nodeSetName(node, name)
static int32_t apiNodeSetName(lua_State *L) {
_argCheck(L, "nodeSetName", 2, 2);
nodeSetName(_argNode(L, "nodeSetName", 1), _argString(L, "nodeSetName", 2));
return 0;
}
// nodeSetParent(node, parent): keeps the local transform
static int32_t apiNodeSetParent(lua_State *L) {
int32_t node;
int32_t parent;
_argCheck(L, "nodeSetParent", 2, 2);
node = _argNode(L, "nodeSetParent", 1);
parent = _argNode(L, "nodeSetParent", 2);
if (!nodeSetParent(node, parent)) {
_luaDie(L, "nodeSetParent", "Node %d cannot go under node %d.", node, parent);
}
return 0;
}
// nodeSetPosition(node, x, y, z)
static int32_t apiNodeSetPosition(lua_State *L) {
_argCheck(L, "nodeSetPosition", 4, 4);
nodeSetPosition(_argNode(L, "nodeSetPosition", 1), _argVec3(L, "nodeSetPosition", 2));
return 0;
}
// nodeSetQuaternion(node, x, y, z, w)
static int32_t apiNodeSetQuaternion(lua_State *L) {
QuatT q;
_argCheck(L, "nodeSetQuaternion", 5, 5);
q.x = (float)_argNumber(L, "nodeSetQuaternion", 2);
q.y = (float)_argNumber(L, "nodeSetQuaternion", 3);
q.z = (float)_argNumber(L, "nodeSetQuaternion", 4);
q.w = (float)_argNumber(L, "nodeSetQuaternion", 5);
nodeSetRotation(_argNode(L, "nodeSetQuaternion", 1), q);
return 0;
}
// nodeSetRotation(node, x, y, z): Euler degrees
static int32_t apiNodeSetRotation(lua_State *L) {
_argCheck(L, "nodeSetRotation", 4, 4);
nodeSetRotation(_argNode(L, "nodeSetRotation", 1), _argEuler(L, "nodeSetRotation", 2));
return 0;
}
// nodeSetScale(node, s) or nodeSetScale(node, x, y, z)
static int32_t apiNodeSetScale(lua_State *L) {
int32_t node;
Vec3T scale;
_argCheck(L, "nodeSetScale", 2, 4);
node = _argNode(L, "nodeSetScale", 1);
if (lua_gettop(L) == 2) {
scale.x = (float)_argNumber(L, "nodeSetScale", 2);
scale.y = scale.x;
scale.z = scale.x;
} else {
scale = _argVec3(L, "nodeSetScale", 2);
}
nodeSetScale(node, scale);
return 0;
}
// nodeSetShadow(node, bool): whether the node's mesh casts shadows
static int32_t apiNodeSetShadow(lua_State *L) {
_argCheck(L, "nodeSetShadow", 2, 2);
nodeSetShadow(_argNode(L, "nodeSetShadow", 1), _argBoolean(L, "nodeSetShadow", 2));
return 0;
}
// nodeSetSprite(node, sprite [, height [, lit]]): the sprite's picture (every frame of an animated one)
// on a quad height world units tall, wide by its aspect; nil clears it
static int32_t apiNodeSetSprite(lua_State *L) {
int32_t node;
SpriteT *sprite = NULL;
SDL_Surface **frames;
int32_t count;
float height = 1.0f;
bool lit = true;
float width;
_argCheck(L, "nodeSetSprite", 1, 4);
node = _argNode(L, "nodeSetSprite", 1);
if ((lua_gettop(L) < 2) || lua_isnil(L, 2)) {
nodeSetSprite(node, NULL, 0, 0.0f, 0.0f, true);
return 0;
}
sprite = _argSprite(L, "nodeSetSprite", 2);
if (lua_gettop(L) >= 3) {
height = (float)_argNumber(L, "nodeSetSprite", 3);
}
if (lua_gettop(L) >= 4) {
lit = _argBoolean(L, "nodeSetSprite", 4);
}
if (sprite->animation != NULL) {
frames = sprite->animation->frames;
count = sprite->animation->count;
} else {
frames = &sprite->originalSurface;
count = 1;
}
width = (frames[0]->h > 0) ? height * (float)frames[0]->w / (float)frames[0]->h : height;
if (!nodeSetSprite(node, frames, count, width, height, lit)) {
_luaDie(L, "nodeSetSprite", "Unable to put sprite %d on node %d (is 3D available?).", sprite->id, node);
}
return 0;
}
// nodeSetSpriteFrame(node, frame): which frame of the node's animated sprite shows, from 0
static int32_t apiNodeSetSpriteFrame(lua_State *L) {
int32_t node;
int32_t frame;
_argCheck(L, "nodeSetSpriteFrame", 2, 2);
node = _argNode(L, "nodeSetSpriteFrame", 1);
frame = _argInteger(L, "nodeSetSpriteFrame", 2);
if (!nodeSetSpriteFrame(node, frame)) {
_luaDie(L, "nodeSetSpriteFrame", "Node %d has no sprite frame %d.", node, frame);
}
return 0;
}
// nodeSetText(node, text [, height]): the text in the selected font and colour on a quad height world
// units tall (per line of the font), unlit; nil clears it
static int32_t apiNodeSetText(lua_State *L) {
int32_t node;
SDL_Surface *surface;
float height = 1.0f;
float width;
bool ok;
_argCheck(L, "nodeSetText", 1, 3);
node = _argNode(L, "nodeSetText", 1);
if ((lua_gettop(L) < 2) || lua_isnil(L, 2)) {
nodeSetSprite(node, NULL, 0, 0.0f, 0.0f, false);
return 0;
}
if (lua_gettop(L) >= 3) {
height = (float)_argNumber(L, "nodeSetText", 3);
}
surface = _renderText(L, "nodeSetText", _argString(L, "nodeSetText", 2));
width = (surface->h > 0) ? height * (float)surface->w / (float)surface->h : height;
ok = nodeSetSprite(node, &surface, 1, width, height, false);
SDL_DestroySurface(surface);
if (!ok) {
_luaDie(L, "nodeSetText", "Unable to put text on node %d (is 3D available?).", node);
}
return 0;
}
// nodeSetVisible(node, bool): hides the node and its children
static int32_t apiNodeSetVisible(lua_State *L) {
_argCheck(L, "nodeSetVisible", 2, 2);
nodeSetVisible(_argNode(L, "nodeSetVisible", 1), _argBoolean(L, "nodeSetVisible", 2));
return 0;
}
// samples = opusDecode(decoder, packet | nil) A packet back to 16-bit samples (interleaved when there
// are two channels); nil for a packet that never arrived asks Opus to conceal the gap.
static int32_t apiOpusDecode(lua_State *L) {
int16_t samples[VOICE_FRAME_MAX * 2];
const char *packet = NULL;
size_t bytes = 0;
int32_t handle = 0;
int32_t channels = 0;
int32_t decoded = 0;
_argCheck(L, "opusDecode", 2, 2);
handle = _argHandle(L, "opusDecode", 1, _voiceIsDecoder, "Opus decoder");
channels = voiceChannels(handle);
if (!lua_isnil(L, 2)) {
packet = _argData(L, "opusDecode", 2, &bytes);
}
decoded = voiceDecode(handle, (const uint8_t *)packet, (int32_t)bytes, samples, VOICE_FRAME_MAX);
if (decoded < 0) {
_luaDie(L, "opusDecode", "%s", voiceError(decoded));
}
lua_pushlstring(L, (const char *)samples, (size_t)decoded * (size_t)channels * sizeof(int16_t));
return 1;
}
// decoder = opusDecoderNew([rate [, channels]]) 48000 and 1 unless given.
static int32_t apiOpusDecoderNew(lua_State *L) {
int32_t rate = MIC_RATE;
int32_t channels = 1;
int32_t handle = 0;
_argCheck(L, "opusDecoderNew", 0, 2);
if (lua_gettop(L) >= 1) {
rate = _argInteger(L, "opusDecoderNew", 1);
}
if (lua_gettop(L) >= 2) {
channels = _argInteger(L, "opusDecoderNew", 2);
}
handle = voiceDecoderNew(rate, channels);
if (handle < 0) {
_luaDie(L, "opusDecoderNew", "%s (rate %d, channels %d)", voiceError(handle), rate, channels);
}
_luaTrace(L, "opusDecoderNew", "%d at %d, %d channel(s)", handle, rate, channels);
lua_pushinteger(L, handle);
return 1;
}
// packet = opusEncode(encoder, samples) One frame of 16-bit samples (interleaved when there are two
// channels) -- 2.5, 5, 10, 20, 40, or 60 ms of them -- as one Opus packet.
static int32_t apiOpusEncode(lua_State *L) {
int16_t samples[VOICE_FRAME_MAX * 2];
uint8_t packet[VOICE_PACKET_MAX];
const char *data = NULL;
size_t bytes = 0;
int32_t handle = 0;
int32_t channels = 0;
int32_t encoded = 0;
_argCheck(L, "opusEncode", 2, 2);
handle = _argHandle(L, "opusEncode", 1, _voiceIsEncoder, "Opus encoder");
channels = voiceChannels(handle);
data = _argData(L, "opusEncode", 2, &bytes);
if ((bytes == 0) || (bytes > sizeof(samples)) || ((bytes % ((size_t)channels * sizeof(int16_t))) != 0)) {
_luaDie(L, "opusEncode", "A frame is whole 16-bit samples for %d channel(s), up to %d: %d bytes", channels, VOICE_FRAME_MAX, (int32_t)bytes);
}
// Copied out of the Lua string, which makes no promise about alignment.
memcpy(samples, data, bytes);
encoded = voiceEncode(handle, samples, (int32_t)(bytes / ((size_t)channels * sizeof(int16_t))), packet, (int32_t)sizeof(packet));
if (encoded < 0) {
_luaDie(L, "opusEncode", "%s", voiceError(encoded));
}
lua_pushlstring(L, (const char *)packet, (size_t)encoded);
return 1;
}
// encoder = opusEncoderNew([rate [, channels [, application]]]) 48000, 1, and "voip" unless given;
// application is "voip" (speech), "audio" (music), or "lowdelay".
static int32_t apiOpusEncoderNew(lua_State *L) {
static const char *names[] = { "voip", "audio", "lowdelay" };
int32_t rate = MIC_RATE;
int32_t channels = 1;
int32_t application = VOICE_APPLICATION_VOIP;
int32_t handle = 0;
int32_t i = 0;
const char *name = NULL;
_argCheck(L, "opusEncoderNew", 0, 3);
if (lua_gettop(L) >= 1) {
rate = _argInteger(L, "opusEncoderNew", 1);
}
if (lua_gettop(L) >= 2) {
channels = _argInteger(L, "opusEncoderNew", 2);
}
if (lua_gettop(L) >= 3) {
name = _argString(L, "opusEncoderNew", 3);
application = -1;
for (i = 0; i < (int32_t)(sizeof(names) / sizeof(names[0])); i++) {
if (strcmp(name, names[i]) == 0) {
application = i;
}
}
if (application < 0) {
_luaDie(L, "opusEncoderNew", "The application is voip, audio, or lowdelay: %s", name);
}
}
handle = voiceEncoderNew(rate, channels, (VoiceApplicationE)application);
if (handle < 0) {
_luaDie(L, "opusEncoderNew", "%s (rate %d, channels %d)", voiceError(handle), rate, channels);
}
_luaTrace(L, "opusEncoderNew", "%d at %d, %d channel(s), %s", handle, rate, channels, names[application]);
lua_pushinteger(L, handle);
return 1;
}
// opusEncoderSet(encoder, bitrate [, complexity]) Bits a second (0 leaves it as it is), and how much
// work to spend, 0 to 10.
static int32_t apiOpusEncoderSet(lua_State *L) {
int32_t handle = 0;
int32_t bitrate = 0;
int32_t complexity = -1;
_argCheck(L, "opusEncoderSet", 2, 3);
handle = _argHandle(L, "opusEncoderSet", 1, _voiceIsEncoder, "Opus encoder");
bitrate = _argInteger(L, "opusEncoderSet", 2);
if (lua_gettop(L) >= 3) {
complexity = _argInteger(L, "opusEncoderSet", 3);
if ((complexity < 0) || (complexity > 10)) {
_luaDie(L, "opusEncoderSet", "Complexity is 0 to 10: %d", complexity);
}
}
if (!voiceEncoderSet(handle, bitrate, complexity)) {
_luaDie(L, "opusEncoderSet", "Opus refused bitrate %d", bitrate);
}
return 0;
}
// seconds = os.clock() Replaces Lua's processor-time clock with wall time since the engine started.
static int32_t apiOsClock(lua_State *L) {
lua_pushnumber(L, (lua_Number)utilTicks() / MS_PER_SECOND_NUMBER);
return 1;
}
// os.time([table]) In a deterministic run the bare call answers with the virtual clock's seconds
// from the option's seed, so a game seeding from the wall clock lands on the same numbers every
// run; a call with a table is a date conversion, and Lua's own answers it.
static int32_t apiOsTime(lua_State *L) {
if (lua_gettop(L) == 0) {
lua_pushinteger(L, (lua_Integer)_global.conf->deterministicStep + (lua_Integer)(utilTicks() / MS_PER_SECOND));
return 1;
}
lua_pushvalue(L, lua_upvalueindex(1));
lua_insert(L, 1);
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
return lua_gettop(L);
}
// overlayBox(x1, y1, x2, y2) Outline only.
static int32_t apiOverlayBox(lua_State *L) {
int32_t x1 = 0;
int32_t y1 = 0;
int32_t x2 = 0;
int32_t y2 = 0;
uint32_t pixel = 0;
SDL_Rect area;
_argCheck(L, "overlayBox", 4, 4);
x1 = _argInteger(L, "overlayBox", 1);
y1 = _argInteger(L, "overlayBox", 2);
x2 = _argInteger(L, "overlayBox", 3);
y2 = _argInteger(L, "overlayBox", 4);
pixel = _overlayColor(&_global.colorForeground);
area = _overlayArea(x1, y1, x2, y2, 0);
SDL_LockSurface(_global.overlay);
_drawLine(x1, y1, x2, y1, pixel);
_drawLine(x2, y1, x2, y2, pixel);
_drawLine(x2, y2, x1, y2, pixel);
_drawLine(x1, y2, x1, y1, pixel);
SDL_UnlockSurface(_global.overlay);
_overlayTouched(&area);
_luaTrace(L, "overlayBox", "%d %d %d %d", x1, y1, x2, y2);
return 0;
}
// overlayCircle(x, y, radius) Midpoint circle.
static int32_t apiOverlayCircle(lua_State *L) {
int32_t x0 = 0;
int32_t y0 = 0;
int32_t r = 0;
int32_t x = 0;
int32_t y = 0;
int32_t dx = 1;
int32_t dy = 1;
int32_t err = 0;
uint32_t pixel = 0;
SDL_Rect area;
_argCheck(L, "overlayCircle", 3, 3);
x0 = _argInteger(L, "overlayCircle", 1);
y0 = _argInteger(L, "overlayCircle", 2);
r = _argInteger(L, "overlayCircle", 3);
x = r - 1;
err = dx - (r << 1);
pixel = _overlayColor(&_global.colorForeground);
area = _overlayArea(x0 - r, y0 - r, x0 + r, y0 + r, 0);
SDL_LockSurface(_global.overlay);
while (x >= y) {
_putPixel(x0 + x, y0 + y, pixel);
_putPixel(x0 + y, y0 + x, pixel);
_putPixel(x0 - y, y0 + x, pixel);
_putPixel(x0 - x, y0 + y, pixel);
_putPixel(x0 - x, y0 - y, pixel);
_putPixel(x0 - y, y0 - x, pixel);
_putPixel(x0 + y, y0 - x, pixel);
_putPixel(x0 + x, y0 - y, pixel);
if (err <= 0) {
y++;
err += dy;
dy += 2;
}
if (err > 0) {
x--;
dx += 2;
err += dx - (r << 1);
}
}
SDL_UnlockSurface(_global.overlay);
_overlayTouched(&area);
_luaTrace(L, "overlayCircle", "%d %d %d", x0, y0, r);
return 0;
}
// overlayClear() Fills with the background color.
static int32_t apiOverlayClear(lua_State *L) {
uint32_t color = _overlayColor(&_global.colorBackground);
const SDL_Rect *area = NULL;
uint8_t *row = NULL;
int32_t x = 0;
int32_t y = 0;
if (_global.overlay != _global.overlayBase) {
SDL_FillSurfaceRect(_global.overlay, NULL, color);
_overlayTouched(NULL);
_luaTrace(L, "overlayClear", "Cleared the layer.");
return 0;
}
// Clearing to the colour already there only has to refill what was drawn since: a HUD of a few
// strings over a picture that is otherwise all background. Nothing drawn at all, as in a menu
// whose picture is all GUI and 3D, changes nothing, and the texture is not even uploaded again.
// The videos under it are erased too.
if (_global.overlayVideoCount > 0) {
_global.overlayVideoCount = 0;
_global.overlayDirty = true;
}
if (!_global.overlayPainted.all && (_global.overlayClearColor == color)) {
if (_global.overlayPainted.count == 0) {
_luaTrace(L, "overlayClear", "Already clear.");
return 0;
}
// Row by row with ordinary stores: SDL's x86 fill streams past the cache, which suits a whole
// surface and makes a few small rectangles slower than clearing everything.
for (x = 0; x < _global.overlayPainted.count; x++) {
area = &_global.overlayPainted.rect[x];
row = (uint8_t *)_global.overlay->pixels + (size_t)area->y * (size_t)_global.overlay->pitch + (size_t)area->x * sizeof(uint32_t);
for (y = 0; y < area->h; y++) {
SDL_memset4(row, color, (size_t)area->w);
row += _global.overlay->pitch;
}
}
_regionAddRegion(&_global.overlayChanged, &_global.overlayPainted);
} else {
SDL_FillSurfaceRect(_global.overlay, NULL, color);
_regionAdd(&_global.overlayChanged, NULL);
}
_regionClear(&_global.overlayPainted);
_global.overlayClearColor = color;
_global.overlayClearAlpha = _global.colorBackground.a;
_global.overlayDirty = true;
_luaTrace(L, "overlayClear", "Cleared.");
return 0;
}
// overlayEllipse(x1, y1, x2, y2) Bresenham ellipse inside the given rectangle.
static int32_t apiOverlayEllipse(lua_State *L) {
int32_t x0 = 0;
int32_t y0 = 0;
int32_t x1 = 0;
int32_t y1 = 0;
int32_t a = 0;
int32_t b = 0;
int32_t b1 = 0;
int32_t dx = 0;
int32_t dy = 0;
int32_t err = 0;
int32_t e2 = 0;
uint32_t pixel = 0;
SDL_Rect area;
_argCheck(L, "overlayEllipse", 4, 4);
x0 = _argInteger(L, "overlayEllipse", 1);
y0 = _argInteger(L, "overlayEllipse", 2);
x1 = _argInteger(L, "overlayEllipse", 3);
y1 = _argInteger(L, "overlayEllipse", 4);
pixel = _overlayColor(&_global.colorForeground);
area = _overlayArea(x0, y0, x1, y1, 1);
_luaTrace(L, "overlayEllipse", "%d %d %d %d", x0, y0, x1, y1);
a = abs(x1 - x0);
b = abs(y1 - y0);
b1 = b & 1; // values of diameter
dx = 4 * (1 - a) * b * b;
dy = 4 * (b1 + 1) * a * a; // error increment
err = dx + dy + b1 * a * a;
if (x0 > x1) { // if called with swapped points
x0 = x1;
x1 += a;
}
if (y0 > y1) { // exchange them
y0 = y1;
}
y0 += (b + 1) / 2; // starting pixel
y1 = y0 - b1;
a *= 8 * a;
b1 = 8 * b * b;
SDL_LockSurface(_global.overlay);
do {
_putPixel(x1, y0, pixel); // I. Quadrant
_putPixel(x0, y0, pixel); // II. Quadrant
_putPixel(x0, y1, pixel); // III. Quadrant
_putPixel(x1, y1, pixel); // IV. Quadrant
e2 = 2 * err;
if (e2 <= dy) { // y step
y0++;
y1--;
dy += a;
err += dy;
}
if ((e2 >= dx) || (2 * err > dy)) { // x step
x0++;
x1--;
dx += b1;
err += dx;
}
} while (x0 <= x1);
while (y0 - y1 < b) { // too early stop of flat ellipses a = 1
_putPixel(x0 - 1, y0, pixel); // finish tip of ellipse
_putPixel(x1 + 1, y0, pixel);
y0++;
_putPixel(x0 - 1, y1, pixel);
_putPixel(x1 + 1, y1, pixel);
y1--;
}
SDL_UnlockSurface(_global.overlay);
_overlayTouched(&area);
return 0;
}
// overlayBanner(text [, height]) Hypseus extension. Shows up to 60 characters over the picture for
// about thirty drawn frames, at a height given as a per cent down it, through the subtitle document.
// A longer message is refused, as Hypseus refuses it.
static int32_t apiOverlayBanner(lua_State *L) {
const char *text = NULL;
int32_t position = BANNER_POSITION_DEFAULT;
_argCheck(L, "overlayBanner", 1, 2);
text = _argString(L, "overlayBanner", 1);
if (lua_gettop(L) == 2) {
position = _argInteger(L, "overlayBanner", 2);
if ((position < SUBTITLE_POSITION_MIN) || (position > SUBTITLE_POSITION_MAX)) {
position = BANNER_POSITION_DEFAULT;
}
}
if (strlen(text) <= BANNER_TEXT_MAX) {
free(_global.bannerText);
_global.bannerText = strdup(text);
_global.bannerFrames = BANNER_FRAMES;
_global.bannerPosition = position;
_subtitleShow(_global.bannerText, _global.bannerPosition);
}
_luaTrace(L, "overlayBanner", "%s %d %d", text, position, _global.bannerFrames);
return 0;
}
// height = overlayGetFontHeight()
//
// The console font is one embedded picture of 256 glyphs, loaded once before any script runs, so
// both of these answer the same thing for the life of the program. They exist because
// overlayPrint counts in character cells while everything else counts in pixels, and without them
// a script converting between the two has to guess the cell size.
static int32_t apiOverlayGetFontHeight(lua_State *L) {
_luaTrace(L, "overlayGetFontHeight", "%d", _global.consoleFontHeight);
lua_pushinteger(L, _global.consoleFontHeight);
return 1;
}
// width = overlayGetFontWidth()
static int32_t apiOverlayGetFontWidth(lua_State *L) {
_luaTrace(L, "overlayGetFontWidth", "%d", _global.consoleFontWidth);
lua_pushinteger(L, _global.consoleFontWidth);
return 1;
}
// height = overlayGetHeight()
static int32_t apiOverlayGetHeight(lua_State *L) {
_luaTrace(L, "overlayGetHeight", "%d", _global.overlay->h);
lua_pushinteger(L, _global.overlay->h);
return 1;
}
// width = overlayGetWidth()
static int32_t apiOverlayGetWidth(lua_State *L) {
_luaTrace(L, "overlayGetWidth", "%d", _global.overlay->w);
lua_pushinteger(L, _global.overlay->w);
return 1;
}
// overlayLine(x1, y1, x2, y2)
static int32_t apiOverlayLine(lua_State *L) {
int32_t x1 = 0;
int32_t y1 = 0;
int32_t x2 = 0;
int32_t y2 = 0;
uint32_t pixel = 0;
SDL_Rect area;
_argCheck(L, "overlayLine", 4, 4);
x1 = _argInteger(L, "overlayLine", 1);
y1 = _argInteger(L, "overlayLine", 2);
x2 = _argInteger(L, "overlayLine", 3);
y2 = _argInteger(L, "overlayLine", 4);
pixel = _overlayColor(&_global.colorForeground);
area = _overlayArea(x1, y1, x2, y2, 0);
SDL_LockSurface(_global.overlay);
_drawLine(x1, y1, x2, y2, pixel);
SDL_UnlockSurface(_global.overlay);
_overlayTouched(&area);
_luaTrace(L, "overlayLine", "%d %d %d %d", x1, y1, x2, y2);
return 0;
}
// overlayPlot(x, y)
static int32_t apiOverlayPlot(lua_State *L) {
int32_t x = 0;
int32_t y = 0;
uint32_t pixel = 0;
SDL_Rect area;
_argCheck(L, "overlayPlot", 2, 2);
x = _argInteger(L, "overlayPlot", 1);
y = _argInteger(L, "overlayPlot", 2);
pixel = _overlayColor(&_global.colorForeground);
area = _overlayArea(x, y, x, y, 0);
SDL_LockSurface(_global.overlay);
_putPixel(x, y, pixel);
SDL_UnlockSurface(_global.overlay);
_overlayTouched(&area);
_luaTrace(L, "overlayPlot", "%d %d", x, y);
return 0;
}
// overlayPrint(column, row, text) Built in console font; coordinates are character cells.
static int32_t apiOverlayPrint(lua_State *L) {
const uint8_t *text = NULL;
int32_t i = 0;
int32_t length = 0;
int32_t fit = 0;
SDL_Rect src;
SDL_Rect dst;
SDL_Rect area;
_argCheck(L, "overlayPrint", 3, 3);
dst.x = _argInteger(L, "overlayPrint", 1) * _global.consoleFontWidth;
dst.y = _argInteger(L, "overlayPrint", 2) * _global.consoleFontHeight;
dst.w = _global.consoleFontWidth;
dst.h = _global.consoleFontHeight;
src.y = 0;
src.w = _global.consoleFontWidth;
src.h = _global.consoleFontHeight;
text = (const uint8_t *)_argString(L, "overlayPrint", 3);
_luaTrace(L, "overlayPrint", "%s", (const char *)text);
// Clip to the right edge of the overlay.
length = (int32_t)strlen((const char *)text);
fit = (_global.overlay->w - dst.x) / _global.consoleFontWidth;
if (fit < 0) {
fit = 0;
}
if (length > fit) {
length = fit;
}
area = dst;
area.w = length * _global.consoleFontWidth;
for (i = 0; i < length; i++) {
src.x = text[i] * _global.consoleFontWidth;
SDL_BlitSurface(_global.consoleFontSurface, &src, _global.overlay, &dst);
dst.x += _global.consoleFontWidth;
}
_overlayTouched(&area);
return 0;
}
// overlaySetMonochrome(enabled) Hypseus extension. Shows the overlay in grey; its pixels stay in colour.
static int32_t apiOverlaySetMonochrome(lua_State *L) {
_argCheck(L, "overlaySetMonochrome", 1, 1);
_global.overlayMonochrome = _argBoolean(L, "overlaySetMonochrome", 1);
// Every pixel shown changes, though none is drawn.
_regionAdd(&_global.overlayChanged, NULL);
_global.overlayDirty = true;
_luaTrace(L, "overlaySetMonochrome", "%d", _global.overlayMonochrome);
return 0;
}
// overlaySetResolution(width, height) Replaces the overlay; its contents are lost.
static int32_t apiOverlaySetResolution(lua_State *L) {
int32_t width = 0;
int32_t height = 0;
_argCheck(L, "overlaySetResolution", 2, 2);
width = _argInteger(L, "overlaySetResolution", 1);
height = _argInteger(L, "overlaySetResolution", 2);
if ((width <= 0) || (height <= 0)) {
_luaDie(L, "overlaySetResolution", "Invalid overlay size: %dx%d", width, height);
}
_overlayResize(width, height);
_luaTrace(L, "overlaySetResolution", "%d %d", width, height);
return 0;
}
// node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz [, maxDistance]): nil when nothing is hit
static int32_t apiPhysicsRaycast(lua_State *L) {
Vec3T origin;
Vec3T direction;
float maxDistance = 0.0f;
int32_t node;
Vec3T point;
Vec3T normal;
_argCheck(L, "physicsRaycast", 6, 7);
origin = _argVec3(L, "physicsRaycast", 1);
direction = _argVec3(L, "physicsRaycast", 4);
if (lua_gettop(L) >= 7) {
maxDistance = (float)_argNumber(L, "physicsRaycast", 7);
}
if (!physicsRaycast(origin, direction, maxDistance, &node, &point, &normal)) {
lua_pushnil(L);
return 1;
}
lua_pushinteger(L, node);
_pushVec3(L, point);
_pushVec3(L, normal);
return 7;
}
// physicsSet2D(bool): bodies made from now on stay in the XY plane (2D games)
static int32_t apiPhysicsSet2D(lua_State *L) {
_argCheck(L, "physicsSet2D", 1, 1);
physicsSet2D(_argBoolean(L, "physicsSet2D", 1));
return 0;
}
// physicsSetDebug(mask): DEBUG_* flags added together, drawn as lines over the scene each frame; DEBUG_NONE stops
static int32_t apiPhysicsSetDebug(lua_State *L) {
_argCheck(L, "physicsSetDebug", 1, 1);
physicsSetDebug((uint32_t)_argInteger(L, "physicsSetDebug", 1));
return 0;
}
// physicsSetEnabled(bool): pauses the simulation without losing it
static int32_t apiPhysicsSetEnabled(lua_State *L) {
_argCheck(L, "physicsSetEnabled", 1, 1);
physicsSetEnabled(_argBoolean(L, "physicsSetEnabled", 1));
return 0;
}
// physicsSetGravity(x, y, z): default 0, -9.81, 0
static int32_t apiPhysicsSetGravity(lua_State *L) {
_argCheck(L, "physicsSetGravity", 3, 3);
physicsSetGravity(_argVec3(L, "physicsSetGravity", 1));
return 0;
}
static int32_t apiPlayerDelete(lua_State *L) {
_argCheck(L, "playerDelete", 1, 1);
playerDelete(_argPlayer(L, "playerDelete", 1));
return 0;
}
// ground, nx, ny, nz = playerGetGround(node): the node stood on (nil in the air) and the normal.
static int32_t apiPlayerGetGround(lua_State *L) {
Vec3T normal;
int32_t ground;
_argCheck(L, "playerGetGround", 1, 1);
ground = playerGetGround(_argPlayer(L, "playerGetGround", 1), &normal);
if (ground < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, ground);
}
_pushVec3(L, normal);
return 4;
}
static int32_t apiPlayerGetVelocity(lua_State *L) {
_argCheck(L, "playerGetVelocity", 1, 1);
_pushVec3(L, playerGetVelocity(_argPlayer(L, "playerGetVelocity", 1)));
return 3;
}
static int32_t apiPlayerIsOnGround(lua_State *L) {
_argCheck(L, "playerIsOnGround", 1, 1);
lua_pushboolean(L, playerIsOnGround(_argPlayer(L, "playerIsOnGround", 1)));
return 1;
}
static int32_t apiPlayerIsSwimming(lua_State *L) {
_argCheck(L, "playerIsSwimming", 1, 1);
lua_pushboolean(L, playerIsSwimming(_argPlayer(L, "playerIsSwimming", 1)));
return 1;
}
static int32_t apiPlayerJump(lua_State *L) {
_argCheck(L, "playerJump", 2, 2);
lua_pushboolean(L, playerJump(_argPlayer(L, "playerJump", 1), (float)_argNumber(L, "playerJump", 2)));
return 1;
}
// playerMove(node, vx, vz), playerMove(node, vx, vy, vz) when swimming, or in a 2D world playerMove(node, vx)
static int32_t apiPlayerMove(lua_State *L) {
int32_t node = 0;
float vx = 0.0f;
float vy = 0.0f;
float vz = 0.0f;
_argCheck(L, "playerMove", 2, 4);
node = _argPlayer(L, "playerMove", 1);
vx = (float)_argNumber(L, "playerMove", 2);
if (lua_gettop(L) == 3) {
vz = (float)_argNumber(L, "playerMove", 3);
} else if (lua_gettop(L) == 4) {
vy = (float)_argNumber(L, "playerMove", 3);
vz = (float)_argNumber(L, "playerMove", 4);
}
playerMove(node, vec3(vx, vy, vz));
return 0;
}
// playerNew(node, radius, height) for a capsule, or playerNew(node, SHAPE_*, a, b, c). Three arguments
// always mean a capsule, so a sphere or cylinder spells out every size (SHAPE_HULL needs none).
static int32_t apiPlayerNew(lua_State *L) {
int32_t node = 0;
int32_t shape = SHAPE_CAPSULE;
float dims[SHAPE_SIZES] = { 0.0f, 0.0f, 0.0f };
int32_t first = 2;
int32_t x = 0;
_argCheck(L, "playerNew", 2, 5);
node = _argNode(L, "playerNew", 1);
if (lua_isinteger(L, 2) && (lua_tointeger(L, 2) >= SHAPE_BOX) && (lua_tointeger(L, 2) <= SHAPE_MESH) && (lua_gettop(L) != 3)) {
shape = (int32_t)lua_tointeger(L, 2);
first = 3;
}
for (x = 0; x < SHAPE_SIZES; x++) {
if (lua_gettop(L) >= first + x) {
dims[x] = (float)_argNumber(L, "playerNew", first + x);
}
}
if (shape == SHAPE_MESH) {
_luaDie(L, "playerNew", "A player needs a convex shape; use SHAPE_HULL.");
}
if (!physicsAvailable()) {
_luaDie(L, "playerNew", "Physics is not available on this machine.");
}
if (!playerNew(node, (ShapeTypeE)shape, dims[0], dims[1], dims[2])) {
_luaDie(L, "playerNew", "Unable to create the player.");
}
_luaTrace(L, "playerNew", "node %d shape %d", node, shape);
return 0;
}
static int32_t apiPlayerSetEnabled(lua_State *L) {
_argCheck(L, "playerSetEnabled", 2, 2);
playerSetEnabled(_argPlayer(L, "playerSetEnabled", 1), _argBoolean(L, "playerSetEnabled", 2));
return 0;
}
static int32_t apiPlayerSetGravityScale(lua_State *L) {
_argCheck(L, "playerSetGravityScale", 2, 2);
playerSetGravityScale(_argPlayer(L, "playerSetGravityScale", 1), (float)_argNumber(L, "playerSetGravityScale", 2));
return 0;
}
static int32_t apiPlayerSetMass(lua_State *L) {
_argCheck(L, "playerSetMass", 2, 2);
playerSetMass(_argPlayer(L, "playerSetMass", 1), (float)_argNumber(L, "playerSetMass", 2));
return 0;
}
static int32_t apiPlayerSetPosition(lua_State *L) {
_argCheck(L, "playerSetPosition", 4, 4);
playerSetPosition(_argPlayer(L, "playerSetPosition", 1), _argVec3(L, "playerSetPosition", 2));
return 0;
}
static int32_t apiPlayerSetPush(lua_State *L) {
_argCheck(L, "playerSetPush", 2, 2);
playerSetPush(_argPlayer(L, "playerSetPush", 1), (float)_argNumber(L, "playerSetPush", 2));
return 0;
}
static int32_t apiPlayerSetSlope(lua_State *L) {
_argCheck(L, "playerSetSlope", 2, 2);
playerSetSlope(_argPlayer(L, "playerSetSlope", 1), (float)_argNumber(L, "playerSetSlope", 2));
return 0;
}
static int32_t apiPlayerSetStep(lua_State *L) {
_argCheck(L, "playerSetStep", 2, 2);
playerSetStep(_argPlayer(L, "playerSetStep", 1), (float)_argNumber(L, "playerSetStep", 2));
return 0;
}
// playerSetSwim(node, sinkSpeed, drag): how the player behaves in water
static int32_t apiPlayerSetSwim(lua_State *L) {
_argCheck(L, "playerSetSwim", 3, 3);
playerSetSwim(_argPlayer(L, "playerSetSwim", 1), (float)_argNumber(L, "playerSetSwim", 2), (float)_argNumber(L, "playerSetSwim", 3));
return 0;
}
static int32_t apiPlayerSetVelocity(lua_State *L) {
_argCheck(L, "playerSetVelocity", 4, 4);
playerSetVelocity(_argPlayer(L, "playerSetVelocity", 1), _argVec3(L, "playerSetVelocity", 2));
return 0;
}
static int32_t apiRagdollActivate(lua_State *L) {
_argCheck(L, "ragdollActivate", 1, 1);
if (!ragdollActivate(_argRagdoll(L, "ragdollActivate", 1))) {
_luaDie(L, "ragdollActivate", "Unable to build the ragdoll's bodies.");
}
return 0;
}
// ragdollApplyImpulse(node, jointName, x, y, z)
static int32_t apiRagdollApplyImpulse(lua_State *L) {
_argCheck(L, "ragdollApplyImpulse", 5, 5);
lua_pushboolean(L, ragdollApplyImpulse(_argRagdoll(L, "ragdollApplyImpulse", 1), _argString(L, "ragdollApplyImpulse", 2), _argVec3(L, "ragdollApplyImpulse", 3)));
return 1;
}
static int32_t apiRagdollDeactivate(lua_State *L) {
_argCheck(L, "ragdollDeactivate", 1, 1);
ragdollDeactivate(_argRagdoll(L, "ragdollDeactivate", 1));
return 0;
}
static int32_t apiRagdollDelete(lua_State *L) {
_argCheck(L, "ragdollDelete", 1, 1);
ragdollDelete(_argRagdoll(L, "ragdollDelete", 1));
return 0;
}
static int32_t apiRagdollIsActive(lua_State *L) {
_argCheck(L, "ragdollIsActive", 1, 1);
lua_pushboolean(L, ragdollIsActive(_argRagdoll(L, "ragdollIsActive", 1)));
return 1;
}
static int32_t apiRagdollIsResting(lua_State *L) {
_argCheck(L, "ragdollIsResting", 1, 1);
lua_pushboolean(L, ragdollIsResting(_argRagdoll(L, "ragdollIsResting", 1)));
return 1;
}
// ragdollNew(node): node is a model instance with a skin
static int32_t apiRagdollNew(lua_State *L) {
int32_t node = 0;
_argCheck(L, "ragdollNew", 1, 1);
node = _argNode(L, "ragdollNew", 1);
if (!physicsAvailable()) {
_luaDie(L, "ragdollNew", "Physics is not available on this machine.");
}
if (!ragdollNew(node)) {
_luaDie(L, "ragdollNew", "Node %d has no skinned model with bones under it.", node);
}
return 0;
}
// ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
static int32_t apiRagdollSetJoint(lua_State *L) {
_argCheck(L, "ragdollSetJoint", 5, 5);
if (!ragdollSetJoint(_argRagdoll(L, "ragdollSetJoint", 1), _argString(L, "ragdollSetJoint", 2), (float)_argNumber(L, "ragdollSetJoint", 3), (float)_argNumber(L, "ragdollSetJoint", 4), (float)_argNumber(L, "ragdollSetJoint", 5))) {
_luaDie(L, "ragdollSetJoint", "No bone by that name.");
}
return 0;
}
static int32_t apiRagdollSetStrength(lua_State *L) {
_argCheck(L, "ragdollSetStrength", 2, 2);
ragdollSetStrength(_argRagdoll(L, "ragdollSetStrength", 1), (float)_argNumber(L, "ragdollSetStrength", 2));
return 0;
}
// ratio = ratioGetX() Hypseus extension. --xratio's gun coordinate scale, 0 when it was not given.
static int32_t apiRatioGetX(lua_State *L) {
_argCheck(L, "ratioGetX", 0, 0);
lua_pushnumber(L, _global.conf->ratioX);
_luaTrace(L, "ratioGetX", "%f", _global.conf->ratioX);
return 1;
}
// ratio = ratioGetY() Hypseus extension. --yratio's gun coordinate scale, 0 when it was not given.
static int32_t apiRatioGetY(lua_State *L) {
_argCheck(L, "ratioGetY", 0, 0);
lua_pushnumber(L, _global.conf->ratioY);
_luaTrace(L, "ratioGetY", "%f", _global.conf->ratioY);
return 1;
}
// saveClear() Forgets everything and writes the empty save out.
static int32_t apiSaveClear(lua_State *L) {
_argCheck(L, "saveClear", 0, 0);
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
_global.saveDirty = true;
_luaTrace(L, "saveClear", "Cleared.");
return 0;
}
// saveDelete(key) Removes one key.
static int32_t apiSaveDelete(lua_State *L) {
_argCheck(L, "saveDelete", 1, 1);
_saveTable(L);
lua_pushvalue(L, 1);
lua_pushnil(L);
lua_rawset(L, -3);
lua_pop(L, 1);
_global.saveDirty = true;
return 0;
}
// saveFlush() Writes the save out now rather than at the end of the frame, for a cabinet that may
// lose power at any moment.
static int32_t apiSaveFlush(lua_State *L) {
_argCheck(L, "saveFlush", 0, 0);
_global.saveDirty = true;
_saveWrite();
return 0;
}
// value = saveGet(key [, default]) What was saved under that key, or the default when nothing was.
static int32_t apiSaveGet(lua_State *L) {
_argCheck(L, "saveGet", 1, 2);
_saveTable(L);
lua_pushvalue(L, 1);
lua_rawget(L, -2);
if (lua_isnil(L, -1) && (lua_gettop(L) >= 4)) {
lua_pop(L, 1);
lua_pushvalue(L, 2);
} else {
_saveCopy(L, -1, "saveGet", 0);
}
return 1;
}
// everything = saveGetAll() A copy of the whole save table, for reading: dumping it while
// debugging, or walking keys the game does not know the names of, which is what moving an old save
// to a new layout looks like. Changing what comes back changes nothing; saveSetAll writes it.
static int32_t apiSaveGetAll(lua_State *L) {
_argCheck(L, "saveGetAll", 0, 0);
_saveTable(L);
_saveCopy(L, -1, "saveGetAll", 0);
return 1;
}
// saveSet(key, value) Keeps a value until the game is uninstalled. The write happens once at the
// end of the frame however many keys were set in it.
static int32_t apiSaveSet(lua_State *L) {
_argCheck(L, "saveSet", 2, 2);
_saveCheckValue(L, "saveSet", 2);
_saveTable(L);
lua_pushvalue(L, 1);
_saveCopy(L, 2, "saveSet", 0);
lua_rawset(L, -3);
lua_pop(L, 1);
_global.saveDirty = true;
return 0;
}
// saveSetAll(everything) Replaces the whole save with this table, in one step. Keys that were
// there and are not in the table given are gone. Moving a save to a new layout is the reason it
// exists: read it with saveGetAll, change it, put it back, with no moment in between where the
// save is empty and a crash would lose it.
static int32_t apiSaveSetAll(lua_State *L) {
_argCheck(L, "saveSetAll", 1, 1);
if (!lua_istable(L, 1)) {
_luaDie(L, "saveSetAll", "saveSetAll takes a table of everything to keep, not a %s.", lua_typename(L, lua_type(L, 1)));
}
_saveCopy(L, 1, "saveSetAll", 0);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
_global.saveDirty = true;
_luaTrace(L, "saveSetAll", "Replaced.");
return 0;
}
static int32_t apiSceneEnable(lua_State *L) {
bool enabled;
_argCheck(L, "sceneEnable", 1, 1);
enabled = _argBoolean(L, "sceneEnable", 1);
if (!sceneEnable(enabled)) {
_luaDie(L, "sceneEnable", "3D is not available on this machine.");
}
if (enabled) {
_capabilityUsed(CAPABILITY_SCENE);
}
_global.refreshDisplay = true;
_luaTrace(L, "sceneEnable", "%s", enabled ? "on" : "off");
return 0;
}
// width, height = sceneGetSize(): the layer's size in overlay coordinates
static int32_t apiSceneGetSize(lua_State *L) {
int32_t width;
int32_t height;
sceneGetSize(&width, &height);
lua_pushinteger(L, width);
lua_pushinteger(L, height);
return 2;
}
// sceneGetStats(): last frame's draws collected, inside the view, and draw calls made
static int32_t apiSceneGetStats(lua_State *L) {
int32_t total = 0;
int32_t drawn = 0;
int32_t batches = 0;
int64_t textureBytes = 0;
sceneGetStats(&total, &drawn, &batches, &textureBytes);
lua_pushinteger(L, total);
lua_pushinteger(L, drawn);
lua_pushinteger(L, batches);
lua_pushinteger(L, textureBytes / BYTES_PER_KIB);
return 4;
}
// sceneMeasure(true) starts timing the scene: every frame after it waits for the GPU to finish the
// scene and adds that time up, so what is measured is the work and not the display's pacing.
// sceneMeasure(false) stops and returns the milliseconds a frame averaged, or nil when no frame was
// drawn. The menu times its own backdrop this way to find the machine's tier (PLAN.md section 57);
// the wait costs frame rate, so it is for a few seconds, not for a whole game.
static int32_t apiSceneMeasure(lua_State *L) {
bool on = false;
_argCheck(L, "sceneMeasure", 1, 1);
on = _argBoolean(L, "sceneMeasure", 1);
if (on) {
_global.sceneMeasuring = true;
_global.sceneMeasureNS = 0;
_global.sceneMeasureFrames = 0;
_luaTrace(L, "sceneMeasure", "on");
return 0;
}
_global.sceneMeasuring = false;
if (_global.sceneMeasureFrames == 0) {
lua_pushnil(L);
} else {
lua_pushnumber(L, (lua_Number)_global.sceneMeasureNS / (lua_Number)_global.sceneMeasureFrames / (lua_Number)SDL_NS_PER_MS);
}
_luaTrace(L, "sceneMeasure", "off after %" PRIu64 " frames", _global.sceneMeasureFrames);
return 1;
}
// x, y, depth, inFront = sceneProject(wx, wy, wz): a world point in overlay coordinates
// gui, x, y = sceneProbeGui(sx, sy): the GUI on the 3D surface under an overlay point and the pixel of it
// there (a light gun aimed at a screen in the scene); nil when the point is off every such surface
static int32_t apiSceneProbeGui(lua_State *L) {
int32_t gui;
float u;
float v;
_argCheck(L, "sceneProbeGui", 2, 2);
if (!sceneProbeGui((float)_argNumber(L, "sceneProbeGui", 1), (float)_argNumber(L, "sceneProbeGui", 2), &gui, &u, &v)) {
lua_pushnil(L);
return 1;
}
lua_pushinteger(L, gui);
lua_pushinteger(L, (int32_t)(u * (float)guiGetWidth(gui)));
lua_pushinteger(L, (int32_t)(v * (float)guiGetHeight(gui)));
return 3;
}
static int32_t apiSceneProject(lua_State *L) {
float x;
float y;
float depth;
bool inFront;
_argCheck(L, "sceneProject", 3, 3);
inFront = sceneProject(_argVec3(L, "sceneProject", 1), &x, &y, &depth);
lua_pushnumber(L, x);
lua_pushnumber(L, y);
lua_pushnumber(L, depth);
lua_pushboolean(L, inFront);
return 4;
}
// sceneSetAmbient(r, g, b): light from everywhere
static int32_t apiSceneSetAmbient(lua_State *L) {
_argCheck(L, "sceneSetAmbient", 3, 3);
sceneSetAmbient((uint8_t)_argInteger(L, "sceneSetAmbient", 1), (uint8_t)_argInteger(L, "sceneSetAmbient", 2), (uint8_t)_argInteger(L, "sceneSetAmbient", 3));
return 0;
}
// sceneSetAntialias(bool): 4x multisampling, on by default where the GPU offers it
static int32_t apiSceneSetAntialias(lua_State *L) {
_argCheck(L, "sceneSetAntialias", 1, 1);
sceneSetAntialias(_argBoolean(L, "sceneSetAntialias", 1));
return 0;
}
// The colour the scene clears to; alpha below 255 lets the video show through.
static int32_t apiSceneSetBackground(lua_State *L) {
int32_t r;
int32_t g;
int32_t b;
int32_t a;
_argCheck(L, "sceneSetBackground", 3, 4);
r = _argInteger(L, "sceneSetBackground", 1);
g = _argInteger(L, "sceneSetBackground", 2);
b = _argInteger(L, "sceneSetBackground", 3);
a = (lua_gettop(L) >= 4) ? _argInteger(L, "sceneSetBackground", 4) : SDL_ALPHA_OPAQUE;
sceneSetBackground((uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)a);
_global.refreshDisplay = true;
return 0;
}
// sceneSetBloom(threshold, strength): the glow of everything brighter than the threshold; strength 0 for none
static int32_t apiSceneSetBloom(lua_State *L) {
_argCheck(L, "sceneSetBloom", 2, 2);
if (_argNumber(L, "sceneSetBloom", 2) > 0.0) {
_capabilityUsed(CAPABILITY_POST);
}
sceneSetBloom((float)_argNumber(L, "sceneSetBloom", 1), (float)_argNumber(L, "sceneSetBloom", 2));
return 0;
}
// sceneSetEnvironment(lit): whether the sky lights the scene
static int32_t apiSceneSetEnvironment(lua_State *L) {
_argCheck(L, "sceneSetEnvironment", 1, 1);
sceneSetEnvironment(_argBoolean(L, "sceneSetEnvironment", 1));
return 0;
}
static int32_t apiSceneSetExposure(lua_State *L) {
_argCheck(L, "sceneSetExposure", 1, 1);
sceneSetExposure((float)_argNumber(L, "sceneSetExposure", 1));
return 0;
}
// sceneSetFog(r, g, b, near, far) or sceneSetFog() for none
static int32_t apiSceneSetFog(lua_State *L) {
_argCheck(L, "sceneSetFog", 0, 5);
if (lua_gettop(L) == 0) {
sceneSetFog(0, 0, 0, 0.0f, 0.0f);
return 0;
}
if (lua_gettop(L) != 5) {
_luaDie(L, "sceneSetFog", "Expected r, g, b, near, far or nothing.");
}
sceneSetFog((uint8_t)_argInteger(L, "sceneSetFog", 1), (uint8_t)_argInteger(L, "sceneSetFog", 2), (uint8_t)_argInteger(L, "sceneSetFog", 3), (float)_argNumber(L, "sceneSetFog", 4), (float)_argNumber(L, "sceneSetFog", 5));
return 0;
}
// sceneSetShadowCascades(count): 1 to 4 maps along the view for a directional light's shadow
static int32_t apiSceneSetShadowCascades(lua_State *L) {
_argCheck(L, "sceneSetShadowCascades", 1, 1);
sceneSetShadowCascades(_argInteger(L, "sceneSetShadowCascades", 1));
return 0;
}
// sceneSetShadowDistance(distance): how far from the camera cascaded shadows reach
static int32_t apiSceneSetShadowDistance(lua_State *L) {
_argCheck(L, "sceneSetShadowDistance", 1, 1);
sceneSetShadowDistance((float)_argNumber(L, "sceneSetShadowDistance", 1));
return 0;
}
// sceneSetShadowSize(size): shadow map texels per side, 256 to 4096 (default 1024)
static int32_t apiSceneSetShadowSize(lua_State *L) {
_argCheck(L, "sceneSetShadowSize", 1, 1);
sceneSetShadowSize(_argInteger(L, "sceneSetShadowSize", 1));
return 0;
}
// sceneSetSky(file) from an equirectangular image (Radiance .hdr keeps its range; PNG or JPEG is
// decoded from sRGB), or sceneSetSky() / sceneSetSky(nil) for none
static int32_t apiSceneSetSky(lua_State *L) {
float *rgb = NULL;
int32_t width = 0;
int32_t height = 0;
_argCheck(L, "sceneSetSky", 0, 1);
if ((lua_gettop(L) == 0) || lua_isnil(L, 1)) {
sceneSetSky(NULL, 0, 0);
return 0;
}
rgb = hdrLoad(_argString(L, "sceneSetSky", 1), &width, &height);
if (rgb == NULL) {
_luaDie(L, "sceneSetSky", "%s", SDL_GetError());
}
if (!sceneSetSky(rgb, width, height)) {
SDL_free(rgb);
_luaDie(L, "sceneSetSky", "Unable to build the sky (is 3D available?).");
}
SDL_free(rgb);
_luaTrace(L, "sceneSetSky", "%dx%d", width, height);
return 0;
}
// sceneSetSkyIntensity(scale): brightness of the sky and its light, 1 as loaded
static int32_t apiSceneSetSkyIntensity(lua_State *L) {
_argCheck(L, "sceneSetSkyIntensity", 1, 1);
sceneSetSkyIntensity((float)_argNumber(L, "sceneSetSkyIntensity", 1));
return 0;
}
static int32_t apiSceneSetTonemap(lua_State *L) {
int32_t tonemap = 0;
_argCheck(L, "sceneSetTonemap", 1, 1);
tonemap = _argInteger(L, "sceneSetTonemap", 1);
if ((tonemap != TONEMAP_NONE) && (tonemap != TONEMAP_NEUTRAL) && (tonemap != TONEMAP_ACES)) {
_luaDie(L, "sceneSetTonemap", "Tonemap must be TONEMAP_NONE, TONEMAP_NEUTRAL or TONEMAP_ACES.");
}
sceneSetTonemap((SceneTonemapE)tonemap);
return 0;
}
// x, y, z = sceneUnproject(sx, sy, distance): the world point that far along the ray through an overlay point
static int32_t apiSceneUnproject(lua_State *L) {
_argCheck(L, "sceneUnproject", 3, 3);
return _pushVec3(L, sceneUnproject((float)_argNumber(L, "sceneUnproject", 1), (float)_argNumber(L, "sceneUnproject", 2), (float)_argNumber(L, "sceneUnproject", 3)));
}
// scoreBezelClear() Hypseus extension. Blanks every readout until a value arrives.
static int32_t apiScoreBezelClear(lua_State *L) {
_argCheck(L, "scoreBezelClear", 0, 0);
_global.scoreBlank = true;
_scorePanelUpdate();
_luaTrace(L, "scoreBezelClear", "%d", _global.scoreEnabled);
return 0;
}
// scoreBezelCredits(credits) Hypseus extension. The credit readout.
static int32_t apiScoreBezelCredits(lua_State *L) {
_argCheck(L, "scoreBezelCredits", 1, 1);
_global.scoreCredits = _argInteger(L, "scoreBezelCredits", 1);
_global.scoreBlank = false;
_scorePanelUpdate();
_luaTrace(L, "scoreBezelCredits", "%d", _global.scoreCredits);
return 0;
}
// scoreBezelEnable(enabled [, type]) Hypseus extension. Shows or hides the score panel. Hypseus
// chooses one of five scoreboards with type; Singe has the one document, so the argument is checked
// and ignored.
static int32_t apiScoreBezelEnable(lua_State *L) {
bool enabled = false;
_argCheck(L, "scoreBezelEnable", 1, 2);
enabled = _argBoolean(L, "scoreBezelEnable", 1);
if (lua_gettop(L) == 2) {
_argInteger(L, "scoreBezelEnable", 2);
}
_scorePanelEnable(enabled);
_luaTrace(L, "scoreBezelEnable", "%d %d", enabled, _global.scoreEnabled);
return 0;
}
// state = scoreBezelGetState() Hypseus extension. Whether the score panel is showing. Every
// Hypseus game asks before using it and draws its own sprite scoreboard when it is not.
static int32_t apiScoreBezelGetState(lua_State *L) {
_argCheck(L, "scoreBezelGetState", 0, 0);
_luaTrace(L, "scoreBezelGetState", "%d", _global.scoreEnabled);
lua_pushboolean(L, _global.scoreEnabled);
return 1;
}
// scoreBezelLives(player, lives) Hypseus extension. A player's life readout.
static int32_t apiScoreBezelLives(lua_State *L) {
int32_t player = 0;
_argCheck(L, "scoreBezelLives", 2, 2);
player = _scorePanelPlayer(_argInteger(L, "scoreBezelLives", 1));
if (player >= 0) {
_global.scoreLives[player] = _argInteger(L, "scoreBezelLives", 2);
_global.scoreBlank = false;
_scorePanelUpdate();
}
_luaTrace(L, "scoreBezelLives", "%d %d", player, (player >= 0) ? _global.scoreLives[player] : 0);
return 0;
}
// scoreBezelScore(player, score) Hypseus extension. A player's score readout.
static int32_t apiScoreBezelScore(lua_State *L) {
int32_t player = 0;
_argCheck(L, "scoreBezelScore", 2, 2);
player = _scorePanelPlayer(_argInteger(L, "scoreBezelScore", 1));
if (player >= 0) {
_global.scoreValue[player] = _argInteger(L, "scoreBezelScore", 2);
_global.scoreBlank = false;
_scorePanelUpdate();
}
_luaTrace(L, "scoreBezelScore", "%d %d", player, (player >= 0) ? _global.scoreValue[player] : 0);
return 0;
}
// scoreBezelTwinScoreOn(enabled) Hypseus extension. Shows the second player's readouts; switching
// them off blanks the panel, as it does in Hypseus.
static int32_t apiScoreBezelTwinScoreOn(lua_State *L) {
_argCheck(L, "scoreBezelTwinScoreOn", 1, 1);
_global.scoreTwin = _argBoolean(L, "scoreBezelTwinScoreOn", 1);
if (!_global.scoreTwin) {
_global.scoreBlank = true;
}
_scorePanelUpdate();
_luaTrace(L, "scoreBezelTwinScoreOn", "%d", _global.scoreTwin);
return 0;
}
// scriptExecute(config) Runs another script after this one ends.
static int32_t apiScriptExecute(lua_State *L) {
ConfigT *conf = _scriptConfFromTable(L, "scriptExecute");
queueScript(conf);
destroyConf(&conf);
_global.running = false;
_luaTrace(L, "scriptExecute", "Queued.");
return 0;
}
// scriptPush(config) Runs another script, then returns to this one.
static int32_t apiScriptPush(lua_State *L) {
ConfigT *conf = _scriptConfFromTable(L, "scriptPush");
queueScript(conf);
destroyConf(&conf);
queueScript(_global.conf);
_global.running = false;
_luaTrace(L, "scriptPush", "Queued.");
return 0;
}
static int32_t apiSetOverlayFullAlpha(lua_State *L) {
_argCheck(L, "setOverlayFullAlpha", 0, 0);
return 0;
}
static int32_t apiSetOverlayLinearScale(lua_State *L) {
bool enabled = false;
_argCheck(L, "setOverlayLinearScale", 1, 1);
enabled = _argBoolean(L, "setOverlayLinearScale", 1);
if (enabled != _global.overlayLinearScale) {
_global.overlayLinearScale = enabled;
_overlayApplyScaleMode();
}
_luaTrace(L, "setOverlayLinearScale", "%d", _global.overlayLinearScale);
return 0;
}
// loaded, id = setOverlayOnTop(enabled) Hypseus extension. Whether the overlay and the GUIs draw
// above the bezel artwork or below it. Only bezel artwork drawn in front of the picture
// (--bezelflip) is ever above them, so this only changes anything there.
static int32_t apiSetOverlayOnTop(lua_State *L) {
const char *id = BEZEL_HASH_NONE;
bool loaded = (_global.bezelTexture != NULL);
bool enabled = false;
_argCheck(L, "setOverlayOnTop", 1, 1);
enabled = _argBoolean(L, "setOverlayOnTop", 1);
if (loaded) {
_global.overlayOnTop = enabled;
_global.refreshDisplay = true;
id = _global.bezelHash;
}
_luaTrace(L, "setOverlayOnTop", "%d %d %s", enabled, loaded, id);
lua_pushboolean(L, loaded);
lua_pushstring(L, id);
return 2;
}
// setOverlayOpacity(alpha) Hypseus extension. How solid the whole overlay is, 0 for invisible to
// 255 for the alpha the game drew. It multiplies whatever alpha the overlay's own pixels carry.
static int32_t apiSetOverlayOpacity(lua_State *L) {
int32_t alpha = 0;
_argCheck(L, "setOverlayOpacity", 1, 1);
alpha = _argInteger(L, "setOverlayOpacity", 1);
_global.overlayOpacity = (uint8_t)SDL_clamp(alpha, 0, COLOR_BYTE_MAX);
_overlayApplyOpacity();
_global.refreshDisplay = true;
_luaTrace(L, "setOverlayOpacity", "%d", _global.overlayOpacity);
return 0;
}
// capabilities = singeCapabilities() What this machine can run: a table of the capability names
// (SCENE, GUI, SHADOWS, POST, SKIN, MORPH) each true or false, and tier, the measured performance
// class from 0 to 3, or nil before the menu has measured it. A script asks so it can degrade itself
// rather than be refused (PLAN.md section 57).
static int32_t apiSingeCapabilities(lua_State *L) {
bool scene = sceneAvailable();
_argCheck(L, "singeCapabilities", 0, 0);
lua_newtable(L);
lua_pushboolean(L, scene);
lua_setfield(L, -2, "SCENE");
lua_pushboolean(L, guiAvailable());
lua_setfield(L, -2, "GUI");
lua_pushboolean(L, sceneSupportsShadows());
lua_setfield(L, -2, "SHADOWS");
lua_pushboolean(L, sceneSupportsPost());
lua_setfield(L, -2, "POST");
// Skinning and morph targets run in the scene's own shaders: where there is a scene, there are both.
lua_pushboolean(L, scene);
lua_setfield(L, -2, "SKIN");
lua_pushboolean(L, scene);
lua_setfield(L, -2, "MORPH");
if (_global.machineTier != TIER_UNMEASURED) {
lua_pushinteger(L, _global.machineTier);
lua_setfield(L, -2, "tier");
}
_luaTrace(L, "singeCapabilities", "tier %d", _global.machineTier);
return 1;
}
// milliseconds = singeGetAudioCalibration() The per-machine value saved by the menu's calibration screen.
static int32_t apiSingeGetAudioCalibration(lua_State *L) {
int32_t value = videoGetAudioCalibration();
_luaTrace(L, "singeGetAudioCalibration", "%d", value);
lua_pushinteger(L, value);
return 1;
}
// milliseconds = singeGetAudioDelay()
static int32_t apiSingeGetAudioDelay(lua_State *L) {
int32_t delay = videoGetAudioDelay();
_luaTrace(L, "singeGetAudioDelay", "%d", delay);
lua_pushinteger(L, delay);
return 1;
}
// milliseconds = singeGetAudioLatency() The audio device queue measured at startup.
static int32_t apiSingeGetAudioLatency(lua_State *L) {
int32_t value = videoGetAudioLatency();
_luaTrace(L, "singeGetAudioLatency", "%d", value);
lua_pushinteger(L, value);
return 1;
}
// info = singeGetSystemInfo()
// What the trace header says about this machine, as a table, so a service screen can put it on
// screen and a photograph of it can answer most of a bug report. The same strings, from the same
// place, rather than a second description that could drift from the first.
static int32_t apiSingeGetSystemInfo(lua_State *L) {
char *os = mainDescribeOs();
char *cpu = mainDescribeCpu();
char *audio = mainDescribeAudioDecoders();
int32_t width = 0;
int32_t height = 0;
_argCheck(L, "singeGetSystemInfo", 0, 0);
lua_newtable(L);
lua_pushstring(L, VERSION_STRING); lua_setfield(L, -2, "version");
lua_pushstring(L, (os != NULL) ? os : ""); lua_setfield(L, -2, "os");
lua_pushstring(L, SINGE_PLATFORM); lua_setfield(L, -2, "platform");
lua_pushstring(L, (mainExecutablePath() != NULL) ? mainExecutablePath() : ""); lua_setfield(L, -2, "executable");
lua_pushstring(L, (cpu != NULL) ? cpu : ""); lua_setfield(L, -2, "cpu");
lua_pushstring(L, SDL_GetRendererName(_global.renderer)); lua_setfield(L, -2, "renderer");
lua_pushstring(L, (_global.device != NULL) ? rgpuGetDeviceDriver(_global.device) : "none (3D unavailable)"); lua_setfield(L, -2, "gpu");
lua_pushstring(L, videoGetDecoderDescription()); lua_setfield(L, -2, "decoder");
lua_pushstring(L, (audio != NULL) ? audio : ""); lua_setfield(L, -2, "audio");
lua_pushstring(L, midiSoundfont()); lua_setfield(L, -2, "soundFont");
lua_pushstring(L, midiIoDescription()); lua_setfield(L, -2, "midi");
lua_pushstring(L, _global.conf->dataDir); lua_setfield(L, -2, "dataPath");
lua_pushstring(L, _global.conf->dataDirBase); lua_setfield(L, -2, "dataRoot");
lua_pushinteger(L, _global.canvasWidth); lua_setfield(L, -2, "canvasWidth");
lua_pushinteger(L, _global.canvasHeight); lua_setfield(L, -2, "canvasHeight");
SDL_GetWindowSize(_global.window, &width, &height);
lua_pushinteger(L, width); lua_setfield(L, -2, "windowWidth");
lua_pushinteger(L, height); lua_setfield(L, -2, "windowHeight");
free(os);
free(cpu);
free(audio);
return 1;
}
// singeSaveGeometry() Keeps the picture's scale, shift and rotation for this machine, beside the
// audio delay, so an operator sets a monitor up once rather than once per game. The live values
// are what is written, which is what the service menu has just been adjusting.
static int32_t apiSingeSaveGeometry(lua_State *L) {
_argCheck(L, "singeSaveGeometry", 0, 0);
_saveMachineSettings(videoGetAudioCalibration());
_luaTrace(L, "singeSaveGeometry", "%d %d %d %d", _global.videoScale, _global.videoShiftX, _global.videoShiftY, _global.videoRotate);
return 0;
}
// singeSaveTier(tier) Keeps this machine's measured performance class, 0 to 3, beside the audio
// delay and the geometry. The menu measures it from its own backdrop; nothing asks the player.
static int32_t apiSingeSaveTier(lua_State *L) {
int32_t tier = 0;
_argCheck(L, "singeSaveTier", 1, 1);
tier = (int32_t)_argInteger(L, "singeSaveTier", 1);
if ((tier < 0) || (tier > TIER_MAX)) {
_luaDie(L, "singeSaveTier", "The tier must be 0 to %d.", TIER_MAX);
}
_global.machineTier = tier;
_saveMachineSettings(videoGetAudioCalibration());
_luaTrace(L, "singeSaveTier", "%d", tier);
return 0;
}
// path = singeGetDataPath()
static int32_t apiSingeGetDataPath(lua_State *L) {
_luaTrace(L, "singeGetDataPath", "%s", _global.conf->dataDir);
lua_pushstring(L, _global.conf->dataDir);
return 1;
}
// height = singeGetHeight() Window height in pixels.
// The GAME_ID from this game's games.dat entry, or nil when it has none. A game without one can be
// played but not ranked: there would be nothing stable to key a leaderboard on, and a made-up id
// would make a different board every time the files moved.
static int32_t apiSingeGetGameId(lua_State *L) {
_argCheck(L, "singeGetGameId", 0, 0);
if (_global.conf->gameId != NULL) {
lua_pushstring(L, _global.conf->gameId);
} else {
lua_pushnil(L);
}
return 1;
}
static int32_t apiSingeGetHeight(lua_State *L) {
int32_t y = 0;
SDL_GetWindowSize(_global.window, NULL, &y);
_luaTrace(L, "singeGetHeight", "%d", y);
lua_pushinteger(L, y);
return 1;
}
// paused = singeGetPauseFlag()
static int32_t apiSingeGetPauseFlag(lua_State *L) {
_luaTrace(L, "singeGetPauseFlag", "%d", _global.pauseState);
lua_pushboolean(L, _global.pauseState);
return 1;
}
// path = singeGetScriptPath()
static int32_t apiSingeGetScriptPath(lua_State *L) {
_luaTrace(L, "singeGetScriptPath", "%s", _global.conf->scriptFile);
lua_pushstring(L, _global.conf->scriptFile);
return 1;
}
// milliseconds = singeGetTicks() Wall clock since the engine started.
static int32_t apiSingeGetTicks(lua_State *L) {
uint64_t ticks = utilTicks();
_luaTrace(L, "singeGetTicks", "%" PRIu64, ticks);
lua_pushinteger(L, (lua_Integer)ticks);
return 1;
}
// width = singeGetWidth() Window width in pixels.
static int32_t apiSingeGetWidth(lua_State *L) {
int32_t x = 0;
SDL_GetWindowSize(_global.window, &x, NULL);
_luaTrace(L, "singeGetWidth", "%d", x);
lua_pushinteger(L, x);
return 1;
}
// singeQuit()
// Whether the GPU device came up. Everything drawn through it -- the 3D scene, the particles and
// every GUI document -- is unavailable without one, and guiNew ends the script rather than answering
// false, so a script that can do without has to ask before it asks for a GUI.
static int32_t apiSingeHasGpu(lua_State *L) {
bool has = (_global.device != NULL);
_argCheck(L, "singeHasGpu", 0, 0);
_luaTrace(L, "singeHasGpu", "%s", has ? "true" : "false");
lua_pushboolean(L, has);
return 1;
}
// recording = singeIsRecording()
static int32_t apiSingeIsRecording(lua_State *L) {
_argCheck(L, "singeIsRecording", 0, 0);
lua_pushboolean(L, recordIsRecording() || (_global.recordRequest == RECORD_REQUEST_START));
return 1;
}
// result = singePack(directory, database)
//
// The packer that --pack runs, from a script: what lets an authoring tool release a game without
// leaving the engine. The reasons for a refusal are printed as they are for the command line.
static int32_t apiSingePack(lua_State *L) {
const char *directory = NULL;
const char *database = NULL;
bool ok = false;
_argCheck(L, "singePack", 2, 2);
directory = _argString(L, "singePack", 1);
database = _argString(L, "singePack", 2);
ok = packGame(directory, database, _global.conf->dataDirBase);
_luaTrace(L, "singePack", "%s %s %s", directory, database, ok ? "ok" : "failed");
lua_pushboolean(L, ok);
return 1;
}
static int32_t apiSingeQuit(lua_State *L) {
_luaTrace(L, "singeQuit", "Quit requested.");
_global.running = false;
return 0;
}
// singeRecordStart([name]) Starts writing the picture and the sound to the next singeNNN.mp4, or to
// a file of the given name (.mp4 added when it has no extension), in the data directory, from the
// next frame drawn. Ignored while a recording is running.
static int32_t apiSingeRecordStart(lua_State *L) {
const char *name = NULL;
_argCheck(L, "singeRecordStart", 0, 1);
free(_global.recordName);
_global.recordName = NULL;
if (lua_gettop(L) >= 1) {
name = _argString(L, "singeRecordStart", 1);
if ((strchr(name, '/') != NULL) || (strchr(name, '\\') != NULL) || (name[0] == '\0')) {
_luaDie(L, "singeRecordStart", "A recording name is a file name, not a path: %s", name);
}
_global.recordName = (strrchr(name, '.') != NULL) ? utilCreateString("%s", name) : utilCreateString("%s.mp4", name);
}
_luaTrace(L, "singeRecordStart", "Recording requested: %s", (_global.recordName != NULL) ? _global.recordName : "next");
_global.recordRequest = RECORD_REQUEST_START;
_global.refreshDisplay = true;
return 0;
}
// singeRecordStop() Finishes the recording at the next frame drawn.
static int32_t apiSingeRecordStop(lua_State *L) {
_argCheck(L, "singeRecordStop", 0, 0);
_luaTrace(L, "singeRecordStop", "Stop requested.");
if (recordIsRecording()) {
_global.recordRequest = RECORD_REQUEST_STOP;
_global.refreshDisplay = true;
}
return 0;
}
// SDL ranks gamepad mappings: its own database lowest, then SDL_AddGamepadMapping, where
// _loadGamepadDatabase puts a gamecontrollerdb.txt, and SDL_GAMECONTROLLERCONFIG highest. A system
// that sets that variable -- ArkOS does, with a mapping for its own pad that leaves Select and Start
// out -- would beat any gamecontrollerdb.txt. So before SDL starts, the files the first script would
// read go on the end of that hint, where a later line for a pad replaces an earlier one. SDL reads
// the hint once, so a later game's own file still only gets _loadGamepadDatabase.
void singeGamepadHint(const ConfigT *conf) {
const char *existing = SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG);
char *places[SETTINGS_PLACES];
char *hint = strdup((existing != NULL) ? existing : "");
char *joined = NULL;
char *data = NULL;
char *line = NULL;
char *save = NULL;
size_t bytes = 0;
int32_t c = 0;
_configPlaces(GAMEPAD_DATABASE_FILE, conf->dataDir, conf->scriptFile, places);
for (c = 0; c < SETTINGS_PLACES; c++) {
data = ((places[c] != NULL) && vfsExists(places[c])) ? vfsRead(places[c], &bytes) : NULL;
// SDL skips comments in a file, but not in the hint.
for (line = (data != NULL) ? SDL_strtok_r(data, "\r\n", &save) : NULL; line != NULL; line = SDL_strtok_r(NULL, "\r\n", &save)) {
if (line[0] == '#') {
continue;
}
joined = (hint[0] == 0) ? strdup(line) : utilCreateString("%s\n%s", hint, line);
free(hint);
hint = joined;
}
free(data);
free(places[c]);
}
// The variable it extends would otherwise beat a hint set at normal priority.
SDL_SetHintWithPriority(SDL_HINT_GAMECONTROLLERCONFIG, hint, SDL_HINT_OVERRIDE);
free(hint);
}
// A quit chord as --quitchord and settings.cfg give it: SDL's names for the first gamepad's buttons
// joined by "+", such as "back+start". SDL's names rather than controls.cfg's, so nothing here keeps
// a second list of them. False for a name SDL does not know, or for fewer than two buttons or more
// than CHORD_CODES_MAX; codes and count may be NULL to check the text alone.
bool singeParseQuitChord(const char *text, int32_t *codes, int32_t *count) {
char *copy = SDL_strdup(text);
char *save = NULL;
char *name = NULL;
int32_t found = 0;
SDL_GamepadButton button = SDL_GAMEPAD_BUTTON_INVALID;
bool ok = copy != NULL;
for (name = ok ? SDL_strtok_r(copy, "+", &save) : NULL; ok && (name != NULL); name = SDL_strtok_r(NULL, "+", &save)) {
button = SDL_GetGamepadButtonFromString(name);
ok = (button != SDL_GAMEPAD_BUTTON_INVALID) && (found < CHORD_CODES_MAX);
if (ok && (codes != NULL)) {
codes[found] = CODE_GAMEPAD_BASE + CODE_GAMEPAD_BUTTON_OFFSET + (int32_t)button;
}
found++;
}
SDL_free(copy);
ok = ok && (found >= 2);
if (count != NULL) {
*count = ok ? found : 0;
}
return ok;
}
// result = singeRelaunch(path [, extra])
//
// Ends the engine as singeQuit does and, once everything has been torn down, starts path in its
// place with the same arguments, plus extra when one is given. For the menu's Engine Update,
// which has put a newer Singe beside this one and asks it for a fresh support folder. The file
// has to exist; it is made executable when the moment comes.
const char *singeRelaunchExtra(void) {
return _global.relaunchExtra;
}
const char *singeRelaunchPath(void) {
return _global.relaunchPath;
}
static int32_t apiSingeRelaunch(lua_State *L) {
const char *path = NULL;
_argCheck(L, "singeRelaunch", 1, 2);
path = _argString(L, "singeRelaunch", 1);
free(_global.relaunchExtra);
_global.relaunchExtra = (lua_gettop(L) >= 2) ? strdup(_argString(L, "singeRelaunch", 2)) : NULL;
if (!utilFileExists(path)) {
_luaTrace(L, "singeRelaunch", "%s does not exist", path);
lua_pushboolean(L, false);
return 1;
}
free(_global.relaunchPath);
_global.relaunchPath = strdup(path);
_global.running = false;
_luaTrace(L, "singeRelaunch", "%s", path);
lua_pushboolean(L, true);
return 1;
}
// singeReload(): runs the game again from its script at the end of this frame
static int32_t apiSingeReload(lua_State *L) {
_luaTrace(L, "singeReload", "Reload requested.");
_global.reloadRequested = true;
return 0;
}
// singeScreenshot() Saved after the next frame is drawn.
// singeScreenshot([name]) The next singeNNN.png, or a file of the given name (.png added when it
// has no extension), in the data directory.
static int32_t apiSingeScreenshot(lua_State *L) {
const char *name = NULL;
_argCheck(L, "singeScreenshot", 0, 1);
free(_global.screenshotName);
_global.screenshotName = NULL;
if (lua_gettop(L) >= 1) {
name = _argString(L, "singeScreenshot", 1);
if ((strchr(name, '/') != NULL) || (strchr(name, '\\') != NULL) || (name[0] == '\0')) {
_luaDie(L, "singeScreenshot", "A screenshot name is a file name, not a path: %s", name);
}
_global.screenshotName = (strrchr(name, '.') != NULL) ? utilCreateString("%s", name) : utilCreateString("%s.png", name);
}
_luaTrace(L, "singeScreenshot", "Screenshot requested: %s", (_global.screenshotName != NULL) ? _global.screenshotName : "next");
_global.requestScreenShot = true;
_global.refreshDisplay = true;
return 0;
}
// singeSetAudioCalibration(milliseconds) Applies now and is remembered for every game on this machine.
static int32_t apiSingeSetAudioCalibration(lua_State *L) {
int32_t value = 0;
_argCheck(L, "singeSetAudioCalibration", 1, 1);
value = _argInteger(L, "singeSetAudioCalibration", 1);
if ((value < -VIDEO_AUDIO_DELAY_MAX) || (value > VIDEO_AUDIO_DELAY_MAX)) {
_luaDie(L, "singeSetAudioCalibration", "Audio calibration must be between %d and %d milliseconds: %d", -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, value);
}
videoSetAudioCalibration(value);
_saveMachineSettings(value);
_luaTrace(L, "singeSetAudioCalibration", "%d", value);
return 0;
}
// singeSetAudioDelay(milliseconds) Positive when the audio device is heard later than it reports.
static int32_t apiSingeSetAudioDelay(lua_State *L) {
int32_t delay = 0;
_argCheck(L, "singeSetAudioDelay", 1, 1);
delay = _argInteger(L, "singeSetAudioDelay", 1);
if ((delay < -VIDEO_AUDIO_DELAY_MAX) || (delay > VIDEO_AUDIO_DELAY_MAX)) {
_luaDie(L, "singeSetAudioDelay", "Audio delay must be between %d and %d milliseconds: %d", -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, delay);
}
videoSetAudioDelay(delay);
_luaTrace(L, "singeSetAudioDelay", "%d", delay);
return 0;
}
// singeSetGameName(title)
static int32_t apiSingeSetGameName(lua_State *L) {
const char *title = NULL;
_argCheck(L, "singeSetGameName", 1, 1);
title = _argString(L, "singeSetGameName", 1);
SDL_SetWindowTitle(_global.window, title);
_luaTrace(L, "singeSetGameName", "%s", title);
return 0;
}
// singeSetPauseFlag(paused)
static int32_t apiSingeSetPauseFlag(lua_State *L) {
_argCheck(L, "singeSetPauseFlag", 1, 1);
_setPause(_argBoolean(L, "singeSetPauseFlag", 1), false);
_luaTrace(L, "singeSetPauseFlag", "%d", _global.pauseState);
return 0;
}
// singeSetPauseKeyEnabled(enabled) Framework.singe aliases singeEnablePauseKey()/singeDisablePauseKey().
static int32_t apiSingeSetPauseKeyEnabled(lua_State *L) {
_argCheck(L, "singeSetPauseKeyEnabled", 1, 1);
_global.pauseEnabled = _argBoolean(L, "singeSetPauseKeyEnabled", 1);
_luaTrace(L, "singeSetPauseKeyEnabled", "%d", _global.pauseEnabled);
return 0;
}
// singeSetQuitKeyEnabled(enabled) Hypseus extension. Framework.singe aliases keyboardCatchQuit(catch) with the opposite sense.
static int32_t apiSingeSetQuitKeyEnabled(lua_State *L) {
_argCheck(L, "singeSetQuitKeyEnabled", 1, 1);
_global.quitEnabled = _argBoolean(L, "singeSetQuitKeyEnabled", 1);
_luaTrace(L, "singeSetQuitKeyEnabled", "%d", _global.quitEnabled);
return 0;
}
// version = singeVersion()
// SHA-256 of a string, as lowercase hex. Needed wherever a script has to know that the bytes it
// has are the bytes it was promised: a downloaded game against the digest its catalogue entry
// carries, or a server's public key against a pin. md5 is bundled as a module, but a digest used
// to decide whether to trust something has to be one that is still worth trusting.
static int32_t apiUtilSha256(lua_State *L) {
const char *data = NULL;
size_t length = 0;
unsigned char digest[SHA256_DIGEST_LENGTH];
char hex[SHA256_DIGEST_LENGTH * 2 + 1];
int32_t i = 0;
_argCheck(L, "utilSha256", 1, 1);
data = _argData(L, "utilSha256", 1, &length);
SHA256((const unsigned char *)data, length, digest);
for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
snprintf(&hex[i * 2], 3, "%02x", digest[i]);
}
lua_pushlstring(L, hex, sizeof(hex) - 1);
return 1;
}
static int32_t apiSingeVersion(lua_State *L) {
_luaTrace(L, "singeVersion", "%s", VERSION_STRING);
lua_pushnumber(L, SINGE_VERSION);
return 1;
}
// wanted = singeWantsCrosshairs() False when --nocrosshair was given.
static int32_t apiSingeWantsCrosshairs(lua_State *L) {
bool wanted = !_global.conf->noCrosshair;
_luaTrace(L, "singeWantsCrosshairs", "%d", wanted);
lua_pushboolean(L, wanted);
return 1;
}
static int32_t apiSoftDelete(lua_State *L) {
_argCheck(L, "softDelete", 1, 1);
softDelete(_argSoft(L, "softDelete", 1));
return 0;
}
// softNew(node, SOFT_CLOTH | SOFT_BODY) from the node's mesh, or softNew(node, SOFT_ROPE, x, y, z, segments, radius)
static int32_t apiSoftNew(lua_State *L) {
int32_t node = 0;
int32_t kind = 0;
bool ok = false;
_argCheck(L, "softNew", 2, 7);
node = _argNode(L, "softNew", 1);
kind = _argInteger(L, "softNew", 2);
if (!physicsAvailable()) {
_luaDie(L, "softNew", "Physics is not available on this machine.");
}
if (kind == SOFT_ROPE) {
_argCheck(L, "softNew", 7, 7);
ok = softNewRope(node, _argVec3(L, "softNew", 3), _argInteger(L, "softNew", 6), (float)_argNumber(L, "softNew", 7));
} else if ((kind == SOFT_CLOTH) || (kind == SOFT_BODY)) {
_argCheck(L, "softNew", 2, 2);
ok = softNew(node, (SoftKindE)kind);
} else {
_luaDie(L, "softNew", "Unknown soft body kind %d.", kind);
}
if (!ok) {
_luaDie(L, "softNew", "Unable to make the soft body: the node needs a mesh (or a rope needs a length).");
}
_luaTrace(L, "softNew", "node %d kind %d", node, kind);
return 0;
}
// softPin(node, x, y, z[, otherNode]): hold the nearest particle there, or to that node
static int32_t apiSoftPin(lua_State *L) {
int32_t follow = -1;
_argCheck(L, "softPin", 4, 5);
if (lua_gettop(L) == 5) {
follow = _argNode(L, "softPin", 5);
}
softPin(_argSoft(L, "softPin", 1), _argVec3(L, "softPin", 2), follow);
return 0;
}
static int32_t apiSoftSetDamping(lua_State *L) {
_argCheck(L, "softSetDamping", 2, 2);
softSetDamping(_argSoft(L, "softSetDamping", 1), (float)_argNumber(L, "softSetDamping", 2));
return 0;
}
static int32_t apiSoftSetMass(lua_State *L) {
_argCheck(L, "softSetMass", 2, 2);
softSetMass(_argSoft(L, "softSetMass", 1), (float)_argNumber(L, "softSetMass", 2));
return 0;
}
static int32_t apiSoftSetPressure(lua_State *L) {
_argCheck(L, "softSetPressure", 2, 2);
softSetPressure(_argSoft(L, "softSetPressure", 1), (float)_argNumber(L, "softSetPressure", 2));
return 0;
}
static int32_t apiSoftSetStiffness(lua_State *L) {
_argCheck(L, "softSetStiffness", 3, 3);
softSetStiffness(_argSoft(L, "softSetStiffness", 1), (float)_argNumber(L, "softSetStiffness", 2), (float)_argNumber(L, "softSetStiffness", 3));
return 0;
}
static int32_t apiSoftUnpin(lua_State *L) {
_argCheck(L, "softUnpin", 4, 4);
softUnpin(_argSoft(L, "softUnpin", 1), _argVec3(L, "softUnpin", 2));
return 0;
}
// soundFullStop() Halts every sound effect channel.
static int32_t apiSoundFullStop(lua_State *L) {
_luaTrace(L, "soundFullStop", "Halting all channels.");
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
return 0;
}
// soundGetPosition(channel): where a positioned channel sits relative to the listener (x right, y up,
// z back, unit length) and its distance gain; a channel with no position returns 0, 0, 0, 1.
static int32_t apiSoundGetPosition(lua_State *L) {
int32_t channel;
EffectT *effect;
_argCheck(L, "soundGetPosition", 1, 1);
channel = _argChannel(L, "soundGetPosition", 1);
effect = &_effects[channel];
lua_pushnumber(L, effect->relative[0]);
lua_pushnumber(L, effect->relative[1]);
lua_pushnumber(L, effect->relative[2]);
lua_pushnumber(L, effect->gain);
return 4;
}
// volume = soundGetVolume() 0 to AUDIO_MAX_VOLUME.
static int32_t apiSoundGetVolume(lua_State *L) {
_luaTrace(L, "soundGetVolume", "%d", _global.effectsVolume);
lua_pushinteger(L, _global.effectsVolume);
return 1;
}
// playing = soundIsPlaying(channel)
static int32_t apiSoundIsPlaying(lua_State *L) {
int32_t channel = 0;
bool playing = false;
_argCheck(L, "soundIsPlaying", 1, 1);
channel = _argChannel(L, "soundIsPlaying", 1);
playing = MIX_TrackPlaying(_effectTracks[channel]);
_luaTrace(L, "soundIsPlaying", "%d %d", channel, playing);
lua_pushboolean(L, playing);
return 1;
}
// id = soundLoad(filename)
static int32_t apiSoundLoad(lua_State *L) {
const char *name = NULL;
SoundT *sound = NULL;
SDL_IOStream *io = NULL;
_argCheck(L, "soundLoad", 1, 1);
name = _argString(L, "soundLoad", 1);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "soundLoad", "Unable to open %s", name);
}
sound = _soundLoadFromIO(L, "soundLoad", io);
_luaTrace(L, "soundLoad", "%d %s", sound->id, name);
lua_pushinteger(L, sound->id);
return 1;
}
// id = soundLoadData(data) Hypseus extension. A sound built from bytes held in a Lua string
// instead of a file, for a game that keeps its audio somewhere Singe cannot open by name. Every
// format soundLoad reads is read here.
static int32_t apiSoundLoadData(lua_State *L) {
const char *data = NULL;
SoundT *sound = NULL;
size_t length = 0;
_argCheck(L, "soundLoadData", 1, 1);
data = _argData(L, "soundLoadData", 1, &length);
sound = _soundLoadFromIO(L, "soundLoadData", SDL_IOFromConstMem(data, length));
_luaTrace(L, "soundLoadData", "%d %zu", sound->id, length);
lua_pushinteger(L, sound->id);
return 1;
}
// wasPlaying = soundPause(channel)
static int32_t apiSoundPause(lua_State *L) {
int32_t channel = 0;
bool playing = false;
_argCheck(L, "soundPause", 1, 1);
channel = _argChannel(L, "soundPause", 1);
playing = MIX_TrackPlaying(_effectTracks[channel]);
MIX_PauseTrack(_effectTracks[channel]);
_luaTrace(L, "soundPause", "%d %d", channel, playing);
lua_pushboolean(L, playing);
return 1;
}
// channel = soundPlay(id) Returns -1 (SOUND_ERROR_INVALID) when every channel is busy.
// soundPlay(id [, loops]): loops 0 (the default) plays once, N repeats N more times, -1 forever
static int32_t apiSoundPlay(lua_State *L) {
SoundT *sound = NULL;
int32_t channel = SOUND_CHANNEL_NONE;
int32_t loops = 0;
int32_t volume = AUDIO_MAX_VOLUME;
SDL_PropertiesID options;
_argCheck(L, "soundPlay", 1, 3);
sound = _argSound(L, "soundPlay", 1);
if (lua_gettop(L) >= 2) {
loops = _argInteger(L, "soundPlay", 2);
if (loops < SOUND_LOOP_FOREVER) {
_luaDie(L, "soundPlay", "Loops must be -1 (forever), 0 (once) or a repeat count: %d", loops);
}
}
if (lua_gettop(L) >= 3) {
volume = _argInteger(L, "soundPlay", 3);
if ((volume < 0) || (volume > AUDIO_MAX_VOLUME)) {
_luaDie(L, "soundPlay", "Invalid sound volume value: %d", volume);
}
}
channel = _effectTrackFree();
if (channel >= 0) {
_effectReset(channel);
_effects[channel].volume = _mixerGain(volume, AUDIO_MAX_VOLUME);
_effectApplyGain(channel);
MIX_SetTrackAudio(_effectTracks[channel], sound->audio);
options = SDL_CreateProperties();
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loops);
if (!MIX_PlayTrack(_effectTracks[channel], options)) {
SDL_DestroyProperties(options);
_luaDie(L, "soundPlay", "%s", SDL_GetError());
}
SDL_DestroyProperties(options);
}
_luaTrace(L, "soundPlay", "%d %d loops %d volume %d", sound->id, channel, loops, volume);
lua_pushinteger(L, channel);
return 1;
}
// wasPaused = soundResume(channel)
static int32_t apiSoundResume(lua_State *L) {
int32_t channel = 0;
bool paused = false;
_argCheck(L, "soundResume", 1, 1);
channel = _argChannel(L, "soundResume", 1);
paused = MIX_TrackPaused(_effectTracks[channel]);
MIX_ResumeTrack(_effectTracks[channel]);
_luaTrace(L, "soundResume", "%d %d", channel, paused);
lua_pushboolean(L, paused);
return 1;
}
// soundSetChannelVolume(channel, volume): one channel's own volume, 0 to AUDIO_MAX_VOLUME, under
// the master. soundPlay starts every channel at full.
static int32_t apiSoundSetChannelVolume(lua_State *L) {
int32_t channel = 0;
int32_t volume = 0;
_argCheck(L, "soundSetChannelVolume", 2, 2);
channel = _argChannel(L, "soundSetChannelVolume", 1);
volume = _argInteger(L, "soundSetChannelVolume", 2);
if ((volume < 0) || (volume > AUDIO_MAX_VOLUME)) {
_luaDie(L, "soundSetChannelVolume", "Invalid sound volume value: %d", volume);
}
_effects[channel].volume = _mixerGain(volume, AUDIO_MAX_VOLUME);
_effectApplyGain(channel);
_luaTrace(L, "soundSetChannelVolume", "%d %d", channel, volume);
return 0;
}
// soundSetListener([node]): positioned sounds are heard from this node; none means the scene camera
static int32_t apiSoundSetListener(lua_State *L) {
_argCheck(L, "soundSetListener", 0, 1);
_global.listenerNode = ((lua_gettop(L) >= 1) && !lua_isnil(L, 1)) ? _argNode(L, "soundSetListener", 1) : LISTENER_CAMERA;
_luaTrace(L, "soundSetListener", "%d", _global.listenerNode);
return 0;
}
// soundSetNode(channel [, node]): the channel follows the node through the scene; none stops following
static int32_t apiSoundSetNode(lua_State *L) {
int32_t channel;
EffectT *effect;
_argCheck(L, "soundSetNode", 1, 2);
channel = _argChannel(L, "soundSetNode", 1);
effect = &_effects[channel];
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
effect->node = _argNode(L, "soundSetNode", 2);
effect->positioned = true;
} else {
effect->node = LISTENER_CAMERA;
effect->positioned = false;
MIX_SetTrack3DPosition(_effectTracks[channel], NULL);
_effectApplyGain(channel);
}
return 0;
}
// soundSetPan(channel, pan): -1 left to 1 right for sounds that are not in the scene
static int32_t apiSoundSetPan(lua_State *L) {
int32_t channel;
float pan;
MIX_StereoGains gains;
_argCheck(L, "soundSetPan", 2, 2);
channel = _argChannel(L, "soundSetPan", 1);
pan = (float)_argNumber(L, "soundSetPan", 2);
pan = SDL_clamp(pan, -1.0f, 1.0f);
_effects[channel].positioned = false;
_effects[channel].node = LISTENER_CAMERA;
gains.left = SDL_min(1.0f, 1.0f - pan);
gains.right = SDL_min(1.0f, 1.0f + pan);
MIX_SetTrackStereo(_effectTracks[channel], &gains);
_effectApplyGain(channel);
return 0;
}
// soundSetPosition(channel, x, y, z): places the channel in the scene, in world units
static int32_t apiSoundSetPosition(lua_State *L) {
int32_t channel;
EffectT *effect;
_argCheck(L, "soundSetPosition", 4, 4);
channel = _argChannel(L, "soundSetPosition", 1);
effect = &_effects[channel];
effect->node = LISTENER_CAMERA;
effect->position = vec3((float)_argNumber(L, "soundSetPosition", 2), (float)_argNumber(L, "soundSetPosition", 3), (float)_argNumber(L, "soundSetPosition", 4));
effect->positioned = true;
return 0;
}
// soundSetRange(channel, near, far): full volume within near, silent beyond far
static int32_t apiSoundSetRange(lua_State *L) {
int32_t channel;
EffectT *effect;
float nearBy;
float farOff;
_argCheck(L, "soundSetRange", 3, 3);
channel = _argChannel(L, "soundSetRange", 1);
nearBy = (float)_argNumber(L, "soundSetRange", 2);
farOff = (float)_argNumber(L, "soundSetRange", 3);
if ((nearBy <= 0.0f) || (farOff <= nearBy)) {
_luaDie(L, "soundSetRange", "Range needs 0 < near < far: %.2f %.2f", nearBy, farOff);
}
effect = &_effects[channel];
effect->nearBy = nearBy;
effect->farOff = farOff;
return 0;
}
// soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel.
static int32_t apiSoundSetVolume(lua_State *L) {
int32_t volume = 0;
int32_t x = 0;
_argCheck(L, "soundSetVolume", 1, 1);
volume = _argInteger(L, "soundSetVolume", 1);
if ((volume < 0) || (volume > AUDIO_MAX_VOLUME)) {
_luaDie(L, "soundSetVolume", "Invalid sound volume value: %d", volume);
}
_global.effectsVolume = volume;
for (x = 0; x < EFFECT_TRACKS; x++) {
_effectApplyGain(x);
}
_luaTrace(L, "soundSetVolume", "%d", _global.effectsVolume);
return 0;
}
// wasPlaying = soundStop(channel)
static int32_t apiSoundStop(lua_State *L) {
int32_t channel = 0;
bool playing = false;
_argCheck(L, "soundStop", 1, 1);
channel = _argChannel(L, "soundStop", 1);
playing = MIX_TrackPlaying(_effectTracks[channel]);
MIX_StopTrack(_effectTracks[channel], 0);
_luaTrace(L, "soundStop", "%d %d", channel, playing);
lua_pushboolean(L, playing);
return 1;
}
// soundUnload(id)
static int32_t apiSoundUnload(lua_State *L) {
SoundT *sound = NULL;
_argCheck(L, "soundUnload", 1, 1);
sound = _argSound(L, "soundUnload", 1);
_luaTrace(L, "soundUnload", "%d", sound->id);
_soundDestroy(sound);
return 0;
}
// spriteBlend(boolean, id)
// Accepted and ignored. It is a Hypseus workaround: that engine loads sprites with SDL's blend
// mode off, and this turns it back on per sprite. Singe draws every sprite with its alpha already.
static int32_t apiSpriteBlend(lua_State *L) {
_argCheck(L, "spriteBlend", 2, 2);
_argBoolean(L, "spriteBlend", 1);
_argSprite(L, "spriteBlend", 2);
return 0;
}
// spriteDraw(id, x, y[, centered]) - Draw at natural size
// spriteDraw(id, x, y, x2, y2[, centered]) - Stretch into the rectangle
static int32_t apiSpriteDraw(lua_State *L) {
int32_t n = lua_gettop(L);
bool center = false;
bool stretched = false;
bool newFrame = false;
uint64_t now = 0;
int32_t delay = 0;
SpriteT *sprite = NULL;
SDL_Rect dest;
_argCheck(L, "spriteDraw", 3, 6);
sprite = _argSprite(L, "spriteDraw", 1);
dest.x = _argInteger(L, "spriteDraw", 2);
dest.y = _argInteger(L, "spriteDraw", 3);
dest.w = 0;
dest.h = 0;
if (n >= 5) {
stretched = true;
dest.w = _argInteger(L, "spriteDraw", 4) - dest.x + 1;
dest.h = _argInteger(L, "spriteDraw", 5) - dest.y + 1;
}
if ((n == 4) || (n == 6)) {
center = _argBoolean(L, "spriteDraw", n);
}
// Advance animation, if any.
if ((sprite->animation != NULL) && sprite->animating) {
now = utilTicks();
sprite->ticks += now - sprite->lastTick;
sprite->lastTick = now;
// Whole loops (after a long pause, say) land on the same frame, so they need not be stepped.
if (sprite->loop && (sprite->ticks >= sprite->loopMs)) {
sprite->ticks %= sprite->loopMs;
}
while (sprite->animating) {
delay = sprite->animation->delays[sprite->currentFrame];
if (delay < ANIMATION_MIN_DELAY_MS) {
delay = ANIMATION_MIN_DELAY_MS;
}
if (sprite->ticks < (uint64_t)delay) {
break;
}
sprite->ticks -= (uint64_t)delay;
sprite->currentFrame++;
newFrame = true;
if (sprite->currentFrame >= sprite->animation->count) {
if (sprite->loop) {
sprite->currentFrame = 0;
} else {
sprite->currentFrame = sprite->animation->count - 1;
sprite->animating = false;
}
}
}
if (newFrame) {
sprite->originalSurface = sprite->animation->frames[sprite->currentFrame];
_spriteRebuildSurface(sprite);
}
}
if (!stretched) {
dest.w = sprite->surface->w;
dest.h = sprite->surface->h;
}
if (center) {
// Move sprite so the drawing coordinate is the center of the sprite
dest.x -= dest.w / 2;
dest.y -= dest.h / 2;
}
_spriteBlit(sprite, sprite->surface, NULL, &dest, stretched);
_overlayTouched(&dest);
_luaTrace(L, "spriteDraw", "%d %d %d %d %d %d", sprite->id, dest.x, dest.y, dest.w, dest.h, center);
return 0;
}
// spriteDrawFrame(id, x, y, frame[, scale[, scaleY]]) Hypseus extension. Frames are 1-based and an out of range frame draws frame 1; the sprite's rotation, scale and playback state are ignored.
static int32_t apiSpriteDrawFrame(lua_State *L) {
int32_t n = lua_gettop(L);
SpriteT *sprite = NULL;
SDL_Surface *source = NULL;
int32_t frame = 0;
double scaleX = 1.0;
double scaleY = 1.0;
SDL_Rect dest;
_argCheck(L, "spriteDrawFrame", 4, 6);
sprite = _argSprite(L, "spriteDrawFrame", 1);
dest.x = _argInteger(L, "spriteDrawFrame", 2);
dest.y = _argInteger(L, "spriteDrawFrame", 3);
frame = _argInteger(L, "spriteDrawFrame", 4);
source = sprite->originalSurface;
if (sprite->animation != NULL) {
if ((frame < 1) || (frame > sprite->animation->count)) {
frame = 1;
}
source = sprite->animation->frames[frame - 1];
}
if (sprite->flip != SDL_FLIP_NONE) {
source = _spriteFlippedFrame(sprite, source, (sprite->animation != NULL) ? frame : 1);
}
dest.w = source->w;
dest.h = source->h;
if (n == 4) {
_spriteBlit(sprite, source, NULL, &dest, false);
} else {
scaleX = _argNumber(L, "spriteDrawFrame", 5);
scaleY = (n == 6) ? _argNumber(L, "spriteDrawFrame", 6) : scaleX;
dest.w = (int32_t)(source->w * scaleX);
dest.h = (int32_t)(source->h * scaleY);
if ((dest.w > 0) && (dest.h > 0)) {
_spriteBlit(sprite, source, NULL, &dest, true);
}
}
_overlayTouched(&dest);
_luaTrace(L, "spriteDrawFrame", "%d %d %d %d %d %d", sprite->id, dest.x, dest.y, frame, dest.w, dest.h);
return 0;
}
// spriteDrawGrid(id, x, y, sourceX, sourceY, width, height[, x2, y2]) Hypseus extension. Draws
// one rectangle out of the sprite with its top left corner at x, y: at its own size, or stretched
// to x2, y2 as spriteDraw stretches, which is the Singe addition. The rectangle is in the sprite's
// own pixels and must lie inside it. A stretch with no width or height draws nothing.
static int32_t apiSpriteDrawGrid(lua_State *L) {
int32_t n = lua_gettop(L);
SpriteT *sprite = NULL;
SDL_Rect source;
SDL_Rect dest;
_argCheck(L, "spriteDrawGrid", 7, 9);
if (n == 8) {
_luaDie(L, "spriteDrawGrid", "A stretched rectangle needs both x2 and y2.");
}
sprite = _argSprite(L, "spriteDrawGrid", 1);
dest.x = _argInteger(L, "spriteDrawGrid", 2);
dest.y = _argInteger(L, "spriteDrawGrid", 3);
source.x = _argInteger(L, "spriteDrawGrid", 4);
source.y = _argInteger(L, "spriteDrawGrid", 5);
source.w = _argInteger(L, "spriteDrawGrid", 6);
source.h = _argInteger(L, "spriteDrawGrid", 7);
if ((source.x < 0) || (source.y < 0) || (source.w < 1) || (source.h < 1) || ((source.x + source.w) > sprite->surface->w) || ((source.y + source.h) > sprite->surface->h)) {
_luaDie(L, "spriteDrawGrid", "Rectangle %d,%d %dx%d is not inside sprite %d, which is %dx%d.", source.x, source.y, source.w, source.h, sprite->id, sprite->surface->w, sprite->surface->h);
}
if (n == 9) {
dest.w = _argInteger(L, "spriteDrawGrid", 8) - dest.x + 1;
dest.h = _argInteger(L, "spriteDrawGrid", 9) - dest.y + 1;
if ((dest.w > 0) && (dest.h > 0)) {
_spriteBlit(sprite, sprite->surface, &source, &dest, true);
}
} else {
dest.w = source.w;
dest.h = source.h;
_spriteBlit(sprite, sprite->surface, &source, &dest, false);
}
_overlayTouched(&dest);
_luaTrace(L, "spriteDrawGrid", "%d %d %d %d %d %d %d %d %d", sprite->id, dest.x, dest.y, source.x, source.y, source.w, source.h, dest.w, dest.h);
return 0;
}
// spriteDrawRotatedFrame(id, x, y[, scale[, scaleY]]) Hypseus extension. Draws the frame
// spriteRotateFrame turned, centred on x, y, at its own size or scaled again. Unlike spriteDrawFrame
// this one honours a rotation, which is the reason the pair exists.
static int32_t apiSpriteDrawRotatedFrame(lua_State *L) {
int32_t n = lua_gettop(L);
SpriteT *sprite = NULL;
double scaleX = 1.0;
double scaleY = 1.0;
SDL_Rect dest;
_argCheck(L, "spriteDrawRotatedFrame", 3, 5);
sprite = _argSprite(L, "spriteDrawRotatedFrame", 1);
dest.x = _argInteger(L, "spriteDrawRotatedFrame", 2);
dest.y = _argInteger(L, "spriteDrawRotatedFrame", 3);
if (sprite->rotatedFrame == NULL) {
_luaDie(L, "spriteDrawRotatedFrame", "Sprite %d has no rotated frame; call spriteRotateFrame first.", sprite->id);
}
if (n >= 4) {
scaleX = _argNumber(L, "spriteDrawRotatedFrame", 4);
scaleY = (n == 5) ? _argNumber(L, "spriteDrawRotatedFrame", 5) : scaleX;
}
dest.w = (int32_t)(sprite->rotatedFrame->w * scaleX);
dest.h = (int32_t)(sprite->rotatedFrame->h * scaleY);
dest.x -= dest.w / 2;
dest.y -= dest.h / 2;
if ((dest.w > 0) && (dest.h > 0)) {
_spriteBlit(sprite, sprite->rotatedFrame, NULL, &dest, n != 3);
}
_overlayTouched(&dest);
_luaTrace(L, "spriteDrawRotatedFrame", "%d %d %d %d %d", sprite->id, dest.x, dest.y, dest.w, dest.h);
return 0;
}
// spriteFlip(id, horizontal, vertical) Mirrors the sprite: every draw of it, of its frames, and of
// its turned frame, until it is called again with false, false. The mirror is applied after the
// turn, so a turned frame is just mirrored the other way rather than turned again.
static int32_t apiSpriteFlip(lua_State *L) {
SpriteT *sprite = NULL;
SDL_FlipMode flip = SDL_FLIP_NONE;
_argCheck(L, "spriteFlip", 3, 3);
sprite = _argSprite(L, "spriteFlip", 1);
if (_argBoolean(L, "spriteFlip", 2)) {
flip |= SDL_FLIP_HORIZONTAL;
}
if (_argBoolean(L, "spriteFlip", 3)) {
flip |= SDL_FLIP_VERTICAL;
}
if (flip != sprite->flip) {
if (sprite->rotatedFrame != NULL) {
SDL_FlipSurface(sprite->rotatedFrame, sprite->flip ^ flip);
}
SDL_DestroySurface(sprite->flippedFrame);
sprite->flippedFrame = NULL;
sprite->flip = flip;
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteFlip", "%d %d", sprite->id, flip);
return 0;
}
// height = spriteFrameHeight(id) Hypseus extension. One frame's height before scaling and rotation; the image's, for a still.
static int32_t apiSpriteFrameHeight(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteFrameHeight", 1, 1);
sprite = _argSprite(L, "spriteFrameHeight", 1);
_luaTrace(L, "spriteFrameHeight", "%d %d", sprite->id, sprite->frameHeight);
lua_pushinteger(L, sprite->frameHeight);
return 1;
}
// width = spriteFrameWidth(id) Hypseus extension. One frame's width before scaling and rotation; the image's, for a still.
static int32_t apiSpriteFrameWidth(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteFrameWidth", 1, 1);
sprite = _argSprite(L, "spriteFrameWidth", 1);
_luaTrace(L, "spriteFrameWidth", "%d %d", sprite->id, sprite->frameWidth);
lua_pushinteger(L, sprite->frameWidth);
return 1;
}
// frame = spriteGetFrame(id)
static int32_t apiSpriteGetFrame(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteGetFrame", 1, 1);
sprite = _argSprite(L, "spriteGetFrame", 1);
_luaTrace(L, "spriteGetFrame", "%d %d", sprite->id, sprite->currentFrame);
lua_pushinteger(L, sprite->currentFrame);
return 1;
}
// count = spriteGetFrames(id) Hypseus extension. Frames in a sheet or an animation; 0 for a still.
static int32_t apiSpriteGetFrames(lua_State *L) {
SpriteT *sprite = NULL;
int32_t frames = 0;
_argCheck(L, "spriteGetFrames", 1, 1);
sprite = _argSprite(L, "spriteGetFrames", 1);
frames = (sprite->animation != NULL) ? sprite->animation->count : 0;
_luaTrace(L, "spriteGetFrames", "%d %d", sprite->id, frames);
lua_pushinteger(L, frames);
return 1;
}
// height = spriteGetHeight(id) Height as drawn, after scaling and rotation.
static int32_t apiSpriteGetHeight(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteGetHeight", 1, 1);
sprite = _argSprite(L, "spriteGetHeight", 1);
_luaTrace(L, "spriteGetHeight", "%d %d", sprite->id, sprite->surface->h);
lua_pushinteger(L, sprite->surface->h);
return 1;
}
// width = spriteGetWidth(id) Width as drawn, after scaling and rotation.
static int32_t apiSpriteGetWidth(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteGetWidth", 1, 1);
sprite = _argSprite(L, "spriteGetWidth", 1);
_luaTrace(L, "spriteGetWidth", "%d %d", sprite->id, sprite->surface->w);
lua_pushinteger(L, sprite->surface->w);
return 1;
}
// playing = spriteIsPlaying(id)
static int32_t apiSpriteIsPlaying(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteIsPlaying", 1, 1);
sprite = _argSprite(L, "spriteIsPlaying", 1);
_luaTrace(L, "spriteIsPlaying", "%d %d", sprite->id, sprite->animating);
lua_pushboolean(L, sprite->animating);
return 1;
}
// spriteLoad(name [, width, height])
// The size is for a vector picture, an SVG, which has no size of its own worth having: it is
// rasterised to fit that box, keeping its own proportions, rather than drawn small and scaled up.
// Ask spriteGetWidth and spriteGetHeight what it came out as. Other formats ignore the size.
static int32_t apiSpriteLoad(lua_State *L) {
const char *name = NULL;
SpriteT *sprite = NULL;
SDL_IOStream *io = NULL;
int32_t width = 0;
int32_t height = 0;
_argCheck(L, "spriteLoad", 1, 3);
name = _argString(L, "spriteLoad", 1);
if (lua_gettop(L) > 1) {
_argCheck(L, "spriteLoad", 3, 3);
width = _argInteger(L, "spriteLoad", 2);
height = _argInteger(L, "spriteLoad", 3);
if ((width < 1) || (height < 1) || (width > SPRITE_VECTOR_MAX) || (height > SPRITE_VECTOR_MAX)) {
_luaDie(L, "spriteLoad", "A vector size is 1 to %d a side, not %dx%d.", SPRITE_VECTOR_MAX, width, height);
}
}
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "spriteLoad", "%s", SDL_GetError());
}
sprite = _spriteLoadFromIO(L, "spriteLoad", io, width, height);
_luaTrace(L, "spriteLoad", "%d %s", sprite->id, name);
lua_pushinteger(L, sprite->id);
return 1;
}
// id = spriteLoadData(data) Hypseus extension. A sprite built from an image held in a Lua string
// instead of a file, for a game that keeps its artwork somewhere Singe cannot open by name. Every
// format spriteLoad reads is read here, animations included.
static int32_t apiSpriteLoadData(lua_State *L) {
const char *data = NULL;
SpriteT *sprite = NULL;
size_t length = 0;
_argCheck(L, "spriteLoadData", 1, 1);
data = _argData(L, "spriteLoadData", 1, &length);
sprite = _spriteLoadFromIO(L, "spriteLoadData", SDL_IOFromConstMem(data, length), 0, 0);
_luaTrace(L, "spriteLoadData", "%d %zu", sprite->id, length);
lua_pushinteger(L, sprite->id);
return 1;
}
// id = spriteLoadFrames(count, filename) Hypseus extension. A single row strip of count equal frames loads as an animation that is not playing.
static int32_t apiSpriteLoadFrames(lua_State *L) {
const char *name = NULL;
SpriteT *sprite = NULL;
SDL_IOStream *io = NULL;
SDL_Surface *sheet = NULL;
SDL_Surface *frame = NULL;
int32_t count = 0;
int32_t x = 0;
SDL_Rect source;
_argCheck(L, "spriteLoadFrames", 2, 2);
count = _argInteger(L, "spriteLoadFrames", 1);
name = _argString(L, "spriteLoadFrames", 2);
if ((count < SPRITE_SHEET_FRAMES_MIN) || (count > SPRITE_SHEET_FRAMES_MAX)) {
_luaDie(L, "spriteLoadFrames", "Frame count %d is outside %d to %d.", count, SPRITE_SHEET_FRAMES_MIN, SPRITE_SHEET_FRAMES_MAX);
}
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "spriteLoadFrames", "%s", SDL_GetError());
}
sheet = decodeImageIO(io, true);
if (sheet == NULL) {
_luaDie(L, "spriteLoadFrames", "%s", SDL_GetError());
}
_surfaceUnpack(&sheet);
source.x = 0;
source.y = 0;
source.w = sheet->w / count;
source.h = sheet->h;
if (source.w < 1) {
_luaDie(L, "spriteLoadFrames", "%s is narrower than %d frames.", name, count);
}
sprite = _spriteNew(L, "spriteLoadFrames");
sprite->animation = (IMG_Animation *)SDL_calloc(1, sizeof(IMG_Animation));
if (sprite->animation != NULL) {
// IMG_FreeAnimation releases these, so they come from SDL's allocator.
sprite->animation->frames = (SDL_Surface **)SDL_calloc((size_t)count, sizeof(SDL_Surface *));
sprite->animation->delays = (int *)SDL_calloc((size_t)count, sizeof(int));
}
if ((sprite->animation == NULL) || (sprite->animation->frames == NULL) || (sprite->animation->delays == NULL)) {
_luaDie(L, "spriteLoadFrames", "Unable to allocate the sprite sheet.");
}
sprite->animation->w = source.w;
sprite->animation->h = source.h;
sprite->animation->count = count;
// Copy the pixels as they are; blending would darken translucent ones over the empty frame.
SDL_SetSurfaceBlendMode(sheet, SDL_BLENDMODE_NONE);
for (x = 0; x < count; x++) {
frame = SDL_CreateSurface(source.w, source.h, sheet->format);
if (frame == NULL) {
_luaDie(L, "spriteLoadFrames", "%s", SDL_GetError());
}
if (SDL_ISPIXELFORMAT_INDEXED(sheet->format)) {
SDL_SetSurfacePalette(frame, SDL_GetSurfacePalette(sheet));
}
SDL_BlitSurface(sheet, &source, frame, NULL);
sprite->animation->frames[x] = frame;
source.x += source.w;
}
SDL_DestroySurface(sheet);
_spriteAdoptAnimation(sprite);
_spriteRegister(sprite);
_luaTrace(L, "spriteLoadFrames", "%d %d %s", sprite->id, count, name);
lua_pushinteger(L, sprite->id);
return 1;
}
// spriteLoop(id, loop)
static int32_t apiSpriteLoop(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteLoop", 2, 2);
sprite = _argSprite(L, "spriteLoop", 1);
sprite->loop = _argBoolean(L, "spriteLoop", 2);
_luaTrace(L, "spriteLoop", "%d %d", sprite->id, sprite->loop);
return 0;
}
// id = spriteNew(width, height) A blank sprite, every pixel transparent, for spriteSetPixels to
// fill: a picture a script makes itself, a pixel at a time.
static int32_t apiSpriteNew(lua_State *L) {
int32_t width = 0;
int32_t height = 0;
SpriteT *sprite = NULL;
_argCheck(L, "spriteNew", 2, 2);
width = _argInteger(L, "spriteNew", 1);
height = _argInteger(L, "spriteNew", 2);
if ((width < 1) || (height < 1) || (width > SPRITE_NEW_MAX) || (height > SPRITE_NEW_MAX)) {
_luaDie(L, "spriteNew", "A new sprite is 1 to %d pixels each way, not %dx%d.", SPRITE_NEW_MAX, width, height);
}
sprite = _spriteNew(L, "spriteNew");
sprite->originalSurface = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32);
if (sprite->originalSurface == NULL) {
free(sprite);
_luaDie(L, "spriteNew", "%s", SDL_GetError());
}
SDL_FillSurfaceRect(sprite->originalSurface, NULL, 0);
_spriteRegister(sprite);
_luaTrace(L, "spriteNew", "%d %dx%d", sprite->id, width, height);
lua_pushinteger(L, sprite->id);
return 1;
}
// spritePause(id)
static int32_t apiSpritePause(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spritePause", 1, 1);
sprite = _argSprite(L, "spritePause", 1);
sprite->animating = false;
_luaTrace(L, "spritePause", "%d", sprite->id);
return 0;
}
// spritePlay(id)
static int32_t apiSpritePlay(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spritePlay", 1, 1);
sprite = _argSprite(L, "spritePlay", 1);
if (!sprite->animating) {
sprite->lastTick = utilTicks();
sprite->animating = true;
}
_luaTrace(L, "spritePlay", "%d", sprite->id);
return 0;
}
// spriteQuality(id, RENDER_PIXELATED | RENDER_SMOOTH)
static int32_t apiSpriteQuality(lua_State *L) {
SpriteT *sprite = NULL;
int32_t smooth = 0;
_argCheck(L, "spriteQuality", 2, 2);
sprite = _argSprite(L, "spriteQuality", 1);
smooth = _argInteger(L, "spriteQuality", 2) ? RENDER_SMOOTH : RENDER_PIXELATED;
if (smooth != sprite->smooth) {
sprite->smooth = smooth;
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteQuality", "%d %d", sprite->id, sprite->smooth);
return 0;
}
// spriteResetColorKey(id, enabled) Hypseus extension. Accepted for compatibility: Singe's rotated and scaled copies keep their transparency, so there is nothing to re-key.
static int32_t apiSpriteResetColorKey(lua_State *L) {
SpriteT *sprite = NULL;
bool enabled = false;
_argCheck(L, "spriteResetColorKey", 2, 2);
sprite = _argSprite(L, "spriteResetColorKey", 1);
enabled = _argBoolean(L, "spriteResetColorKey", 2);
_luaTrace(L, "spriteResetColorKey", "%d %d", sprite->id, enabled);
return 0;
}
// spriteRotate(id, degrees) Clockwise.
static int32_t apiSpriteRotate(lua_State *L) {
SpriteT *sprite = NULL;
double angle = 0.0;
_argCheck(L, "spriteRotate", 2, 2);
sprite = _argSprite(L, "spriteRotate", 1);
angle = fmod(_argNumber(L, "spriteRotate", 2), DEGREES_PER_CIRCLE);
if (angle != sprite->angle) {
sprite->angle = angle;
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteRotate", "%d %f", sprite->id, sprite->angle);
return 0;
}
// spriteRotateAndScale(id, degrees, scale) or spriteRotateAndScale(id, degrees, scaleX, scaleY)
static int32_t apiSpriteRotateAndScale(lua_State *L) {
int32_t n = lua_gettop(L);
SpriteT *sprite = NULL;
double angle = 0.0;
double scaleX = 1.0;
double scaleY = 1.0;
_argCheck(L, "spriteRotateAndScale", 3, 4);
sprite = _argSprite(L, "spriteRotateAndScale", 1);
angle = fmod(_argNumber(L, "spriteRotateAndScale", 2), DEGREES_PER_CIRCLE);
scaleX = _argNumber(L, "spriteRotateAndScale", 3);
scaleY = (n == 4) ? _argNumber(L, "spriteRotateAndScale", 4) : scaleX;
if ((angle != sprite->angle) || (scaleX != sprite->scaleX) || (scaleY != sprite->scaleY)) {
sprite->angle = angle;
sprite->scaleX = scaleX;
sprite->scaleY = scaleY;
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteRotateAndScale", "%d %f %f %f", sprite->id, sprite->angle, sprite->scaleX, sprite->scaleY);
return 0;
}
// spriteRotateFrame(id, degrees, frame) Hypseus extension. Takes one frame out of a sheet, turns
// it clockwise and scales it by the sprite's own scale, and keeps it for spriteDrawRotatedFrame.
// Frames are 1 based and an out of range frame is frame 1, as spriteDrawFrame has them; a still
// image turns its whole picture. The sprite's own drawn surface is left alone.
static int32_t apiSpriteRotateFrame(lua_State *L) {
SpriteT *sprite = NULL;
SDL_Surface *source = NULL;
int32_t frame = 0;
_argCheck(L, "spriteRotateFrame", 3, 3);
sprite = _argSprite(L, "spriteRotateFrame", 1);
sprite->angle = fmod(_argNumber(L, "spriteRotateFrame", 2), DEGREES_PER_CIRCLE);
frame = _argInteger(L, "spriteRotateFrame", 3);
source = sprite->originalSurface;
if (sprite->animation != NULL) {
if ((frame < 1) || (frame > sprite->animation->count)) {
frame = 1;
}
source = sprite->animation->frames[frame - 1];
}
SDL_DestroySurface(sprite->rotatedFrame);
sprite->rotatedFrame = _spriteTransform(sprite, source);
if (sprite->rotatedFrame == NULL) {
_luaDie(L, "spriteRotateFrame", "Unable to turn frame %d of sprite %d.", frame, sprite->id);
}
_luaTrace(L, "spriteRotateFrame", "%d %f %d %dx%d", sprite->id, sprite->angle, frame, sprite->rotatedFrame->w, sprite->rotatedFrame->h);
return 0;
}
// spriteScale(id, scale) or spriteScale(id, scaleX, scaleY)
static int32_t apiSpriteScale(lua_State *L) {
int32_t n = lua_gettop(L);
SpriteT *sprite = NULL;
double scaleX = 1.0;
double scaleY = 1.0;
_argCheck(L, "spriteScale", 2, 3);
sprite = _argSprite(L, "spriteScale", 1);
scaleX = _argNumber(L, "spriteScale", 2);
scaleY = (n == 3) ? _argNumber(L, "spriteScale", 3) : scaleX;
if ((scaleX != sprite->scaleX) || (scaleY != sprite->scaleY)) {
sprite->scaleX = scaleX;
sprite->scaleY = scaleY;
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteScale", "%d %f %f", sprite->id, sprite->scaleX, sprite->scaleY);
return 0;
}
// spriteSetAlpha(id, alpha) Multiplies every pixel's alpha by alpha / 255 on every draw of the
// sprite, so it fades with no new image: 255, the default, draws it as it is, 0 not at all.
// Out-of-range values are clamped.
static int32_t apiSpriteSetAlpha(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteSetAlpha", 2, 2);
sprite = _argSprite(L, "spriteSetAlpha", 1);
sprite->alpha = (uint8_t)SDL_clamp(_argInteger(L, "spriteSetAlpha", 2), 0, 255);
_luaTrace(L, "spriteSetAlpha", "%d %d", sprite->id, sprite->alpha);
return 0;
}
// spriteSetBlend(id, SPRITE_ALPHA | SPRITE_ADD | SPRITE_MULTIPLY) How every draw of the sprite
// lays it over the overlay: over what is there by its alpha (the default), added to what is there,
// or darkening what is there by its colour. Additive and multiplied pixels keep the overlay's own
// alpha, so they show where the overlay already has something drawn.
static int32_t apiSpriteSetBlend(lua_State *L) {
SpriteT *sprite = NULL;
int32_t blend = 0;
_argCheck(L, "spriteSetBlend", 2, 2);
sprite = _argSprite(L, "spriteSetBlend", 1);
blend = _argInteger(L, "spriteSetBlend", 2);
if ((blend != SPRITE_ALPHA) && (blend != SPRITE_ADD) && (blend != SPRITE_MULTIPLY)) {
_luaDie(L, "spriteSetBlend", "Blend must be SPRITE_ALPHA, SPRITE_ADD, or SPRITE_MULTIPLY.");
}
sprite->blend = (SpriteBlendE)blend;
_luaTrace(L, "spriteSetBlend", "%d %d", sprite->id, blend);
return 0;
}
// spriteSetFrame(id, frame) Ignored for still images and out of range frames.
static int32_t apiSpriteSetFrame(lua_State *L) {
SpriteT *sprite = NULL;
int32_t frame = 0;
_argCheck(L, "spriteSetFrame", 2, 2);
sprite = _argSprite(L, "spriteSetFrame", 1);
frame = _argInteger(L, "spriteSetFrame", 2);
if ((sprite->animation != NULL) && (frame >= 0) && (frame < sprite->animation->count) && (frame != sprite->currentFrame)) {
sprite->currentFrame = frame;
sprite->ticks = 0;
sprite->originalSurface = sprite->animation->frames[frame];
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteSetFrame", "%d %d", sprite->id, sprite->currentFrame);
return 0;
}
// spriteSetPixels(id, pixels[, palette]) Replaces every pixel of a still sprite from a string, a
// row at a time from the top: four bytes a pixel, red, green, blue, and alpha, or with a palette one
// byte a pixel, each the number of a colour in it. The palette is 256 colours of three bytes (red,
// green, blue, all opaque) or four (with alpha). Changing only the palette and setting the same
// pixels again is palette cycling.
static int32_t apiSpriteSetPixels(lua_State *L) {
SpriteT *sprite = NULL;
SDL_Surface *surface = NULL;
const uint8_t *pixels = NULL;
const uint8_t *palette = NULL;
size_t length = 0;
size_t colours = 0;
size_t count = 0;
int32_t stride = 0;
int32_t x = 0;
int32_t y = 0;
uint8_t *row = NULL;
const uint8_t *colour = NULL;
_argCheck(L, "spriteSetPixels", 2, 3);
sprite = _argSprite(L, "spriteSetPixels", 1);
pixels = (const uint8_t *)_argData(L, "spriteSetPixels", 2, &length);
surface = sprite->originalSurface;
if (sprite->animation != NULL) {
_luaDie(L, "spriteSetPixels", "Sprite %d is an animation; only a still sprite takes new pixels.", sprite->id);
}
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
// A loaded picture is whatever format it decoded to; it becomes plain RGBA the first time.
surface = SDL_ConvertSurface(sprite->originalSurface, SDL_PIXELFORMAT_RGBA32);
if (surface == NULL) {
_luaDie(L, "spriteSetPixels", "%s", SDL_GetError());
}
SDL_DestroySurface(sprite->originalSurface);
if (sprite->surface == sprite->originalSurface) {
sprite->surface = surface;
}
sprite->originalSurface = surface;
}
count = (size_t)surface->w * surface->h;
if (lua_gettop(L) >= 3) {
palette = (const uint8_t *)_argData(L, "spriteSetPixels", 3, &colours);
stride = (int32_t)(colours / PALETTE_COLOURS);
if (((stride != 3) && (stride != 4)) || (colours % PALETTE_COLOURS != 0)) {
_luaDie(L, "spriteSetPixels", "A palette is %d colours of 3 or 4 bytes, not %zu bytes.", PALETTE_COLOURS, colours);
}
if (length != count) {
_luaDie(L, "spriteSetPixels", "Sprite %d is %dx%d, so it takes %zu bytes with a palette, not %zu.", sprite->id, surface->w, surface->h, count, length);
}
} else if (length != count * 4) {
_luaDie(L, "spriteSetPixels", "Sprite %d is %dx%d, so it takes %zu bytes, not %zu.", sprite->id, surface->w, surface->h, count * 4, length);
}
SDL_LockSurface(surface);
for (y = 0; y < surface->h; y++) {
row = (uint8_t *)surface->pixels + (size_t)y * surface->pitch;
if (palette == NULL) {
memcpy(row, pixels + (size_t)y * surface->w * 4, (size_t)surface->w * 4);
continue;
}
for (x = 0; x < surface->w; x++) {
colour = palette + (size_t)pixels[(size_t)y * surface->w + x] * stride;
row[x * 4] = colour[0];
row[x * 4 + 1] = colour[1];
row[x * 4 + 2] = colour[2];
row[x * 4 + 3] = (stride == 4) ? colour[3] : 255;
}
}
SDL_UnlockSurface(surface);
if (sprite->surface != sprite->originalSurface) {
_spriteRebuildSurface(sprite);
}
_luaTrace(L, "spriteSetPixels", "%d %zu bytes%s", sprite->id, length, (palette != NULL) ? " with a palette" : "");
return 0;
}
// spriteSetTint(id, r, g, b) Multiplies every pixel's colour by r, g, b / 255 on every draw of the
// sprite: a white sprite drawn in any colour, or any sprite darkened or shifted, with no new image.
// 255, 255, 255, the default, draws it as it is. Out-of-range values are clamped.
static int32_t apiSpriteSetTint(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteSetTint", 4, 4);
sprite = _argSprite(L, "spriteSetTint", 1);
sprite->tintR = (uint8_t)SDL_clamp(_argInteger(L, "spriteSetTint", 2), 0, 255);
sprite->tintG = (uint8_t)SDL_clamp(_argInteger(L, "spriteSetTint", 3), 0, 255);
sprite->tintB = (uint8_t)SDL_clamp(_argInteger(L, "spriteSetTint", 4), 0, 255);
_luaTrace(L, "spriteSetTint", "%d %d %d %d", sprite->id, sprite->tintR, sprite->tintG, sprite->tintB);
return 0;
}
// spriteUnload(id)
static int32_t apiSpriteUnload(lua_State *L) {
SpriteT *sprite = NULL;
_argCheck(L, "spriteUnload", 1, 1);
sprite = _argSprite(L, "spriteUnload", 1);
_luaTrace(L, "spriteUnload", "%d", sprite->id);
_spriteDestroy(sprite);
return 0;
}
// srtClear() Hypseus extension. Takes whatever is on screen off at once, subtitle or banner, and
// leaves the loaded cues and the enabled state alone: the next cue still shows.
static int32_t apiSrtClear(lua_State *L) {
_argCheck(L, "srtClear", 0, 0);
free(_global.bannerText);
_global.bannerText = NULL;
_global.bannerFrames = 0;
_subtitleShow(NULL, 0);
// Remembering the cue that was showing as the one already dealt with is what keeps it off:
// the next frame finds the same cue and leaves it alone, and the one after it comes up as usual.
_global.subtitleCue = _subtitleCueNow();
_luaTrace(L, "srtClear", "%d %d", _global.subtitleCue, _global.subtitleCueCount);
return 0;
}
// srtEnable(enabled) Hypseus extension. Whether the loaded cues are shown as the disc plays.
static int32_t apiSrtEnable(lua_State *L) {
_argCheck(L, "srtEnable", 1, 1);
_global.subtitleEnabled = _argBoolean(L, "srtEnable", 1);
if (!_global.subtitleEnabled) {
_global.subtitleCue = SUBTITLE_NONE;
_subtitleShow(NULL, 0);
}
_luaTrace(L, "srtEnable", "%d", _global.subtitleEnabled);
return 0;
}
// loaded = srtLoad(filename) Hypseus extension. Reads a SubRip file and turns its timestamps into
// disc frames at the disc's frame rate. Answers false, having loaded nothing, when the file is
// missing, holds no usable cue, or there is no disc to time it against. Loading replaces whatever
// was loaded before; srtEnable then shows it.
static int32_t apiSrtLoad(lua_State *L) {
const char *name = NULL;
char *text = NULL;
size_t bytes = 0;
bool loaded = false;
_argCheck(L, "srtLoad", 1, 1);
name = _argString(L, "srtLoad", 1);
text = vfsRead(name, &bytes);
if (text != NULL) {
loaded = _subtitleParse(text);
free(text);
}
if (!loaded) {
_subtitleClearCues();
_subtitleShow(NULL, 0);
utilSay("Warning: No subtitles were loaded from %s.", name);
}
_luaTrace(L, "srtLoad", "%s %d %d", name, loaded, _global.subtitleCueCount);
lua_pushboolean(L, loaded);
return 1;
}
// loaded = srtLoadTrack(track) The same as srtLoad, but the subtitles come from inside the disc's
// own container instead of from a .srt beside it.
static int32_t apiSrtLoadTrack(lua_State *L) {
char *text = NULL;
int32_t track = 0;
bool loaded = false;
_argCheck(L, "srtLoadTrack", 1, 1);
track = _argInteger(L, "srtLoadTrack", 1);
if (_global.videoHandle >= 0) {
text = videoReadSubtitles(_global.videoHandle, track);
if (text != NULL) {
loaded = _subtitleParse(text);
free(text);
}
}
if (!loaded) {
_subtitleClearCues();
_subtitleShow(NULL, 0);
utilSay("Warning: No subtitles were loaded from track %d of the disc.", track);
}
_luaTrace(L, "srtLoadTrack", "%d %d %d", track, loaded, _global.subtitleCueCount);
lua_pushboolean(L, loaded);
return 1;
}
// srtPosition(percent) Hypseus extension. Where a subtitle sits, as a per cent down the picture,
// 1 to 95. Anything outside that is ignored, as Hypseus ignores it.
static int32_t apiSrtPosition(lua_State *L) {
int32_t position = 0;
_argCheck(L, "srtPosition", 1, 1);
position = _argInteger(L, "srtPosition", 1);
if ((position >= SUBTITLE_POSITION_MIN) && (position <= SUBTITLE_POSITION_MAX)) {
_global.subtitlePosition = position;
_global.subtitleCue = SUBTITLE_NONE;
}
_luaTrace(L, "srtPosition", "%d %d", position, _global.subtitlePosition);
return 0;
}
// height = terrainGetHeight(node, x, z): the terrain's height at a world x, z, or nil off it
// statsEnable(enabled) The developer's overlay: frame time, what is alive, what the disc is doing.
static int32_t apiStatsEnable(lua_State *L) {
_argCheck(L, "statsEnable", 1, 1);
statsSetEnabled(_argBoolean(L, "statsEnable", 1));
_global.refreshDisplay = true;
_luaTrace(L, "statsEnable", "%d", statsIsEnabled());
return 0;
}
// shown = statsIsEnabled()
static int32_t apiStatsIsEnabled(lua_State *L) {
_argCheck(L, "statsIsEnabled", 0, 0);
lua_pushboolean(L, statsIsEnabled());
return 1;
}
static int32_t apiTerrainGetHeight(lua_State *L) {
float height = 0.0f;
_argCheck(L, "terrainGetHeight", 3, 3);
if (!terrainGetHeight(_argNode(L, "terrainGetHeight", 1), (float)_argNumber(L, "terrainGetHeight", 2), (float)_argNumber(L, "terrainGetHeight", 3), &height)) {
lua_pushnil(L);
return 1;
}
lua_pushnumber(L, height);
return 1;
}
// index = vehicleAddWheel(vehicle, wheelNode, radius, width, suspensionLength)
// id = timerAfter(milliseconds, function) Calls the function once, later. The handle comes back so
// it can be cancelled; the function is given it too, so one function can serve several timers.
static int32_t apiTimerAfter(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "timerAfter", 2, 2);
if (!lua_isfunction(L, 2)) {
_luaDie(L, "timerAfter", "The second argument is the function to call.");
}
handle = schedulerTimer(_argInteger64(L, "timerAfter", 1), false, utilTicks());
_scheduleRemember(L, handle, 2, 0);
lua_pushinteger(L, handle);
return 1;
}
// timerCancel(id) Stops one. A timer that has already fired is not an error to cancel.
static int32_t apiTimerCancel(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "timerCancel", 1, 1);
handle = _argInteger(L, "timerCancel", 1);
schedulerCancel(handle);
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_pushnil(L);
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
return 0;
}
// id = timerEvery(milliseconds, function) Calls the function over and over until it is cancelled.
static int32_t apiTimerEvery(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "timerEvery", 2, 2);
if (!lua_isfunction(L, 2)) {
_luaDie(L, "timerEvery", "The second argument is the function to call.");
}
handle = schedulerTimer(_argInteger64(L, "timerEvery", 1), true, utilTicks());
_scheduleRemember(L, handle, 2, 0);
lua_pushinteger(L, handle);
return 1;
}
// running = timerIsActive(id) Whether it will fire again. True for a repeating timer until it is
// cancelled, false for a one shot that has been.
static int32_t apiTimerIsActive(lua_State *L) {
_argCheck(L, "timerIsActive", 1, 1);
lua_pushboolean(L, schedulerIsActive(_argInteger(L, "timerIsActive", 1)));
return 1;
}
// tweenCancel(id) Stops a tween where it is. Neither function is called again.
static int32_t apiTweenCancel(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "tweenCancel", 1, 1);
handle = _argInteger(L, "tweenCancel", 1);
schedulerCancel(handle);
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_pushnil(L);
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
return 0;
}
// running = tweenIsActive(id)
static int32_t apiTweenIsActive(lua_State *L) {
_argCheck(L, "tweenIsActive", 1, 1);
lua_pushboolean(L, schedulerIsActive(_argInteger(L, "tweenIsActive", 1)));
return 1;
}
// id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
// Moves a number from one value to another over time, calling onUpdate(value, progress) every frame
// and onDone() at the end. What the number means is the game's business: a position, an opacity,
// a volume, a camera angle. That is why this hands over a number rather than moving a node itself.
static int32_t apiTweenValue(lua_State *L) {
int32_t handle = 0;
int32_t easing = 0;
_argCheck(L, "tweenValue", 5, 6);
easing = _argInteger(L, "tweenValue", 4);
if ((easing < 0) || (easing >= SCHEDULER_EASE_COUNT)) {
_luaDie(L, "tweenValue", "Easing is one of the EASE_ values, 0 to %d, not %d.", SCHEDULER_EASE_COUNT - 1, easing);
}
if (!lua_isfunction(L, 5)) {
_luaDie(L, "tweenValue", "The fifth argument is the function called with each value.");
}
if ((lua_gettop(L) >= 6) && !lua_isfunction(L, 6)) {
_luaDie(L, "tweenValue", "The sixth argument, when given, is the function called at the end.");
}
handle = schedulerTween(_argNumber(L, "tweenValue", 1), _argNumber(L, "tweenValue", 2), _argInteger64(L, "tweenValue", 3), (SchedulerEaseE)easing, utilTicks());
_scheduleRemember(L, handle, 5, (lua_gettop(L) >= 6) ? 6 : 0);
lua_pushinteger(L, handle);
return 1;
}
static int32_t apiVehicleAddWheel(lua_State *L) {
int32_t index = 0;
_argCheck(L, "vehicleAddWheel", 5, 5);
index = vehicleAddWheel(_argVehicle(L, "vehicleAddWheel", 1), _argNode(L, "vehicleAddWheel", 2), (float)_argNumber(L, "vehicleAddWheel", 3), (float)_argNumber(L, "vehicleAddWheel", 4), (float)_argNumber(L, "vehicleAddWheel", 5));
if (index < 0) {
_luaDie(L, "vehicleAddWheel", "Unable to add the wheel (too many?).");
}
lua_pushinteger(L, index);
return 1;
}
static int32_t apiVehicleDelete(lua_State *L) {
_argCheck(L, "vehicleDelete", 1, 1);
vehicleDelete(_argVehicle(L, "vehicleDelete", 1));
return 0;
}
// vehicleDrive(vehicle, forward, right, brake, handBrake)
static int32_t apiVehicleDrive(lua_State *L) {
int32_t node = 0;
float in[VEHICLE_DRIVE_INPUTS] = { 0.0f, 0.0f, 0.0f, 0.0f };
int32_t x = 0;
_argCheck(L, "vehicleDrive", 3, 5);
node = _argVehicle(L, "vehicleDrive", 1);
for (x = 0; x < VEHICLE_DRIVE_INPUTS; x++) {
if (lua_gettop(L) >= 2 + x) {
in[x] = (float)_argNumber(L, "vehicleDrive", 2 + x);
}
}
vehicleDrive(node, in[0], in[1], in[2], in[3]);
return 0;
}
static int32_t apiVehicleGetGear(lua_State *L) {
_argCheck(L, "vehicleGetGear", 1, 1);
lua_pushinteger(L, vehicleGetGear(_argVehicle(L, "vehicleGetGear", 1)));
return 1;
}
static int32_t apiVehicleGetRpm(lua_State *L) {
_argCheck(L, "vehicleGetRpm", 1, 1);
lua_pushnumber(L, (lua_Number)vehicleGetRpm(_argVehicle(L, "vehicleGetRpm", 1)));
return 1;
}
static int32_t apiVehicleGetSpeed(lua_State *L) {
_argCheck(L, "vehicleGetSpeed", 1, 1);
lua_pushnumber(L, (lua_Number)vehicleGetSpeed(_argVehicle(L, "vehicleGetSpeed", 1)));
return 1;
}
static int32_t apiVehicleGetWheelSlip(lua_State *L) {
_argCheck(L, "vehicleGetWheelSlip", 2, 2);
lua_pushnumber(L, (lua_Number)vehicleGetWheelSlip(_argVehicle(L, "vehicleGetWheelSlip", 1), _argInteger(L, "vehicleGetWheelSlip", 2)));
return 1;
}
static int32_t apiVehicleIsWheelOnGround(lua_State *L) {
_argCheck(L, "vehicleIsWheelOnGround", 2, 2);
lua_pushboolean(L, vehicleIsWheelOnGround(_argVehicle(L, "vehicleIsWheelOnGround", 1), _argInteger(L, "vehicleIsWheelOnGround", 2)));
return 1;
}
// vehicleNew(node, VEHICLE_CAR | VEHICLE_MOTORCYCLE | VEHICLE_TANK | VEHICLE_BOAT): the node must carry a dynamic body
static int32_t apiVehicleNew(lua_State *L) {
int32_t node = 0;
int32_t kind = 0;
_argCheck(L, "vehicleNew", 2, 2);
node = _argBody(L, "vehicleNew", 1);
kind = _argInteger(L, "vehicleNew", 2);
if ((kind < VEHICLE_CAR) || (kind > VEHICLE_BOAT)) {
_luaDie(L, "vehicleNew", "Unknown vehicle kind %d.", kind);
}
if (!vehicleNew(node, (VehicleKindE)kind)) {
_luaDie(L, "vehicleNew", "Unable to create the vehicle: the node needs a dynamic body, in a 3D world.");
}
_luaTrace(L, "vehicleNew", "node %d kind %d", node, kind);
return 0;
}
static int32_t apiVehicleSetAntiRoll(lua_State *L) {
_argCheck(L, "vehicleSetAntiRoll", 2, 2);
vehicleSetAntiRoll(_argVehicle(L, "vehicleSetAntiRoll", 1), (float)_argNumber(L, "vehicleSetAntiRoll", 2));
return 0;
}
static int32_t apiVehicleSetBrakes(lua_State *L) {
_argCheck(L, "vehicleSetBrakes", 3, 3);
vehicleSetBrakes(_argVehicle(L, "vehicleSetBrakes", 1), (float)_argNumber(L, "vehicleSetBrakes", 2), (float)_argNumber(L, "vehicleSetBrakes", 3));
return 0;
}
// vehicleSetEngine(vehicle, maxTorque, maxRpm[, minRpm])
static int32_t apiVehicleSetEngine(lua_State *L) {
_argCheck(L, "vehicleSetEngine", 3, 4);
vehicleSetEngine(_argVehicle(L, "vehicleSetEngine", 1), (float)_argNumber(L, "vehicleSetEngine", 2), (float)_argNumber(L, "vehicleSetEngine", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "vehicleSetEngine", 4) : VEHICLE_DEFAULT_MIN_RPM);
return 0;
}
// vehicleSetGears(vehicle, {ratio, ...}[, reverseRatio][, automatic])
static int32_t apiVehicleSetGears(lua_State *L) {
int32_t node = 0;
int32_t count = 0;
float *ratios = NULL;
float reverse = VEHICLE_DEFAULT_REVERSE_GEAR;
bool automatic = true;
_argCheck(L, "vehicleSetGears", 2, 4);
node = _argVehicle(L, "vehicleSetGears", 1);
ratios = _argFloatTable(L, "vehicleSetGears", 2, &count);
if (lua_gettop(L) >= 3) {
reverse = (float)_argNumber(L, "vehicleSetGears", 3);
}
if (lua_gettop(L) == 4) {
automatic = _argBoolean(L, "vehicleSetGears", 4);
}
if ((count < 1) || (count > VEHICLE_MAX_GEARS) || !vehicleSetGears(node, ratios, count, reverse, automatic)) {
SDL_free(ratios);
_luaDie(L, "vehicleSetGears", "Give one to %d forward gear ratios.", VEHICLE_MAX_GEARS);
}
SDL_free(ratios);
return 0;
}
// vehicleSetRudder(boat, maxTorque)
static int32_t apiVehicleSetRudder(lua_State *L) {
_argCheck(L, "vehicleSetRudder", 2, 2);
vehicleSetRudder(_argVehicle(L, "vehicleSetRudder", 1), (float)_argNumber(L, "vehicleSetRudder", 2));
return 0;
}
static int32_t apiVehicleSetSteering(lua_State *L) {
_argCheck(L, "vehicleSetSteering", 2, 2);
vehicleSetSteering(_argVehicle(L, "vehicleSetSteering", 1), (float)_argNumber(L, "vehicleSetSteering", 2));
return 0;
}
static int32_t apiVehicleSetSuspension(lua_State *L) {
_argCheck(L, "vehicleSetSuspension", 3, 3);
vehicleSetSuspension(_argVehicle(L, "vehicleSetSuspension", 1), (float)_argNumber(L, "vehicleSetSuspension", 2), (float)_argNumber(L, "vehicleSetSuspension", 3));
return 0;
}
// vehicleSetThrust(boat, maxForce, x, y, z): the propeller's push and where it pushes, in the hull
static int32_t apiVehicleSetThrust(lua_State *L) {
_argCheck(L, "vehicleSetThrust", 5, 5);
vehicleSetThrust(_argVehicle(L, "vehicleSetThrust", 1), (float)_argNumber(L, "vehicleSetThrust", 2), _argVec3(L, "vehicleSetThrust", 3));
return 0;
}
// vehicleSetWheel(vehicle, index, steered, driven)
static int32_t apiVehicleSetWheel(lua_State *L) {
_argCheck(L, "vehicleSetWheel", 4, 4);
if (!vehicleSetWheel(_argVehicle(L, "vehicleSetWheel", 1), _argInteger(L, "vehicleSetWheel", 2), _argBoolean(L, "vehicleSetWheel", 3), _argBoolean(L, "vehicleSetWheel", 4))) {
_luaDie(L, "vehicleSetWheel", "No such wheel.");
}
return 0;
}
// videoDraw(id, x, y, x2, y2) - Stretch the frame into the rectangle
// videoDraw(id, x, y, centered) - Draw with the video's rotation and scale
static int32_t apiVideoDraw(lua_State *L) {
int32_t n = lua_gettop(L);
VideoT *video = NULL;
bool center = false;
bool newFrame = false;
int64_t frame = 0;
const uint8_t *pixels = NULL;
int32_t pitch = 0;
SDL_Surface *source = NULL;
SDL_Surface *rgba = NULL;
SDL_Rect dest;
SDL_Rect gpu;
_argCheck(L, "videoDraw", 4, 5);
video = _argVideo(L, "videoDraw", 1);
dest.x = _argInteger(L, "videoDraw", 2);
dest.y = _argInteger(L, "videoDraw", 3);
dest.w = 0;
dest.h = 0;
if (n == 5) {
dest.w = _argInteger(L, "videoDraw", 4) - dest.x + 1;
dest.h = _argInteger(L, "videoDraw", 5) - dest.y + 1;
} else {
center = _argBoolean(L, "videoDraw", 4);
}
// Advance the video. Its picture is on its texture; the CPU only needs the pixels to copy them.
frame = videoUpdate(video->handle, &video->texture);
if (frame < 0) {
_luaTrace(L, "videoDraw", "%d no frame yet", video->id);
return 0;
}
newFrame = (frame != video->lastFrame);
video->lastFrame = frame;
// Stretched, or at its own size, the GPU can draw it instead, when that looks the same. Tried
// before asking for the pixels: asking makes a zero-copy video copy its frames from then on.
if ((n == 5) || ((video->angle == 0.0) && (video->scaleX == 1.0) && (video->scaleY == 1.0))) {
gpu = dest;
if (n != 5) {
gpu.w = videoGetWidth(video->handle);
gpu.h = videoGetHeight(video->handle);
if (center) {
gpu.x -= gpu.w / 2;
gpu.y -= gpu.h / 2;
}
}
if ((gpu.w > 0) && (gpu.h > 0) && _overlayVideoUnder(video->texture, &gpu)) {
_luaTrace(L, "videoDraw", "%d %d %d %d %d %" PRId64 " by the GPU", video->id, gpu.x, gpu.y, gpu.w, gpu.h, frame);
return 0;
}
}
if (!videoGetPixels(video->handle, &pixels, &pitch)) {
_luaTrace(L, "videoDraw", "%d no pixels yet", video->id);
return 0;
}
source = SDL_CreateSurfaceFrom(videoGetWidth(video->handle), videoGetHeight(video->handle), VIDEO_SURFACE_FORMAT, (void *)pixels, pitch);
if (source == NULL) {
utilDie("%s", SDL_GetError());
}
if (n == 5) {
// Simple/Stretched draw
SDL_BlitSurfaceScaled(source, NULL, _global.overlay, &dest, SDL_SCALEMODE_NEAREST);
} else {
if ((video->angle == 0.0) && (video->scaleX == 1.0) && (video->scaleY == 1.0)) {
// Untransformed: draw the frame directly.
dest.w = source->w;
dest.h = source->h;
if (center) {
dest.x -= dest.w / 2;
dest.y -= dest.h / 2;
}
SDL_BlitSurface(source, NULL, _global.overlay, &dest);
} else {
// Rebuild the transformed frame only when something changed.
if (newFrame || video->transformChanged || (video->transformedSurface == NULL)) {
SDL_DestroySurface(video->transformedSurface);
// Give the frame an alpha channel so rotated corners come out transparent.
rgba = SDL_ConvertSurface(source, SDL_PIXELFORMAT_RGBA32);
if (rgba == NULL) {
utilDie("%s", SDL_GetError());
}
video->transformedSurface = rotoZoomSurface(rgba, -video->angle, video->scaleX, video->scaleY, video->smooth != 0);
SDL_DestroySurface(rgba);
if (video->transformedSurface == NULL) {
utilDie("Unable to transform video %d.", video->id);
}
video->transformChanged = false;
}
dest.w = video->transformedSurface->w;
dest.h = video->transformedSurface->h;
if (center) {
dest.x -= dest.w / 2;
dest.y -= dest.h / 2;
}
SDL_BlitSurface(video->transformedSurface, NULL, _global.overlay, &dest);
}
}
SDL_DestroySurface(source);
_overlayTouched(&dest);
_luaTrace(L, "videoDraw", "%d %d %d %d %d %" PRId64, video->id, dest.x, dest.y, dest.w, dest.h, frame);
return 0;
}
// track = videoGetAudioTrack(id)
static int32_t apiVideoGetAudioTrack(lua_State *L) {
VideoT *video = NULL;
int32_t track = 0;
_argCheck(L, "videoGetAudioTrack", 1, 1);
video = _argVideo(L, "videoGetAudioTrack", 1);
track = videoGetAudioTrack(video->handle);
_luaTrace(L, "videoGetAudioTrack", "%d %d", video->id, track);
lua_pushinteger(L, track);
return 1;
}
// count = videoGetAudioTracks(id)
static int32_t apiVideoGetAudioTracks(lua_State *L) {
VideoT *video = NULL;
int32_t count = 0;
_argCheck(L, "videoGetAudioTracks", 1, 1);
video = _argVideo(L, "videoGetAudioTracks", 1);
count = videoGetAudioTracks(video->handle);
_luaTrace(L, "videoGetAudioTracks", "%d %d", video->id, count);
lua_pushinteger(L, count);
return 1;
}
// frame = videoGetFrame(id)
static int32_t apiVideoGetFrame(lua_State *L) {
VideoT *video = NULL;
int64_t frame = 0;
_argCheck(L, "videoGetFrame", 1, 1);
video = _argVideo(L, "videoGetFrame", 1);
frame = videoGetFrame(video->handle);
_luaTrace(L, "videoGetFrame", "%d %" PRId64, video->id, frame);
lua_pushinteger(L, frame);
return 1;
}
// count = videoGetFrameCount(id)
static int32_t apiVideoGetFrameCount(lua_State *L) {
VideoT *video = NULL;
int64_t count = 0;
_argCheck(L, "videoGetFrameCount", 1, 1);
video = _argVideo(L, "videoGetFrameCount", 1);
count = videoGetFrameCount(video->handle);
_luaTrace(L, "videoGetFrameCount", "%d %" PRId64, video->id, count);
lua_pushinteger(L, count);
return 1;
}
// height = videoGetHeight(id)
static int32_t apiVideoGetHeight(lua_State *L) {
VideoT *video = NULL;
int32_t height = 0;
_argCheck(L, "videoGetHeight", 1, 1);
video = _argVideo(L, "videoGetHeight", 1);
height = videoGetHeight(video->handle);
_luaTrace(L, "videoGetHeight", "%d %d", video->id, height);
lua_pushinteger(L, height);
return 1;
}
// code = videoGetLanguage(id, track)
static int32_t apiVideoGetLanguage(lua_State *L) {
VideoT *video = NULL;
int32_t track = 0;
const char *language = NULL;
_argCheck(L, "videoGetLanguage", 2, 2);
video = _argVideo(L, "videoGetLanguage", 1);
track = _argInteger(L, "videoGetLanguage", 2);
if ((track < 0) || (track >= videoGetAudioTracks(video->handle))) {
_luaDie(L, "videoGetLanguage", "Invalid audio track %d in video %d.", track, video->id);
}
language = videoGetLanguage(video->handle, track);
_luaTrace(L, "videoGetLanguage", "%d %d %s", video->id, track, language);
lua_pushstring(L, language);
return 1;
}
// name = videoGetLanguageDescription(code) English name for an ISO 639 code.
static int32_t apiVideoGetLanguageDescription(lua_State *L) {
const char *code = NULL;
const char *description = NULL;
_argCheck(L, "videoGetLanguageDescription", 1, 1);
code = _argString(L, "videoGetLanguageDescription", 1);
description = videoGetLanguageDescription(code);
_luaTrace(L, "videoGetLanguageDescription", "%s %s", code, description);
lua_pushstring(L, description);
return 1;
}
// left, right = videoGetVolume(id), or volume = videoGetVolume() for the laserdisc
static int32_t apiVideoGetVolume(lua_State *L) {
VideoT *video = NULL;
int32_t left = 0;
int32_t right = 0;
_argCheck(L, "videoGetVolume", 0, 1);
// Hypseus extension: no argument answers the laserdisc's own volume, on the 0 to 64 scale its
// games expect -- they double it for musicSetVolume, whose range is 0 to 128, and a level
// outside that ends the script. Singe keeps the disc volume as a percentage, so it is scaled
// here rather than kept twice.
if (lua_gettop(L) == 0) {
int32_t level = (_global.conf->volumeVldp * HYPSEUS_VOLUME_MAX) / VOLUME_MAX;
_luaTrace(L, "videoGetVolume", "disc %d", level);
lua_pushinteger(L, level);
return 1;
}
video = _argVideo(L, "videoGetVolume", 1);
videoGetVolume(video->handle, &left, &right);
_luaTrace(L, "videoGetVolume", "%d %d %d", video->id, left, right);
lua_pushinteger(L, left);
lua_pushinteger(L, right);
return 2;
}
// width = videoGetWidth(id)
static int32_t apiVideoGetWidth(lua_State *L) {
VideoT *video = NULL;
int32_t width = 0;
_argCheck(L, "videoGetWidth", 1, 1);
video = _argVideo(L, "videoGetWidth", 1);
width = videoGetWidth(video->handle);
_luaTrace(L, "videoGetWidth", "%d %d", video->id, width);
lua_pushinteger(L, width);
return 1;
}
// playing = videoIsPlaying(id)
static int32_t apiVideoIsPlaying(lua_State *L) {
VideoT *video = NULL;
bool playing = false;
_argCheck(L, "videoIsPlaying", 1, 1);
video = _argVideo(L, "videoIsPlaying", 1);
playing = videoIsPlaying(video->handle);
_luaTrace(L, "videoIsPlaying", "%d %d", video->id, playing);
lua_pushboolean(L, playing);
return 1;
}
// id = videoLoad(filename)
static int32_t apiVideoLoad(lua_State *L) {
const char *name = NULL;
char *dataDir = NULL;
VideoT *video = NULL;
_argCheck(L, "videoLoad", 1, 1);
name = _argString(L, "videoLoad", 1);
// The index file lives in a data directory named for the video's directory.
dataDir = createDataDir(_global.conf->dataDirBase, name);
if (dataDir == NULL) {
_luaDie(L, "videoLoad", "Unable to create data directory for %s.", name);
}
video = (VideoT *)calloc(1, sizeof(VideoT));
if (!video) {
_luaDie(L, "videoLoad", "Unable to allocate new video.");
}
// Indexed in the background, so a long clip does not stop the frame loop; not on the virtual
// clock, where a run has to repeat exactly and waiting is what makes it do so.
video->handle = videoLoad(name, NULL, dataDir, _global.renderer, !utilIsDeterministic());
video->id = _global.nextVideoId++;
video->lastFrame = -1;
video->scaleX = 1.0;
video->scaleY = 1.0;
HASH_ADD_INT(_global.videoList, id, video);
_selectDefaultAudioTrack(video->handle);
videoSetVolume(video->handle, _global.conf->volumeNonVldp, _global.conf->volumeNonVldp);
_luaTrace(L, "videoLoad", "%s %s %d", name, dataDir, video->id);
free(dataDir);
lua_pushinteger(L, video->id);
return 1;
}
// videoPause(id)
static int32_t apiVideoPause(lua_State *L) {
VideoT *video = NULL;
_argCheck(L, "videoPause", 1, 1);
video = _argVideo(L, "videoPause", 1);
videoPause(video->handle);
_luaTrace(L, "videoPause", "%d", video->id);
return 0;
}
// videoPlay(id)
static int32_t apiVideoPlay(lua_State *L) {
VideoT *video = NULL;
_argCheck(L, "videoPlay", 1, 1);
video = _argVideo(L, "videoPlay", 1);
videoPlay(video->handle);
_luaTrace(L, "videoPlay", "%d", video->id);
return 0;
}
// videoQuality(id, RENDER_PIXELATED | RENDER_SMOOTH)
static int32_t apiVideoQuality(lua_State *L) {
VideoT *video = NULL;
int32_t smooth = 0;
_argCheck(L, "videoQuality", 2, 2);
video = _argVideo(L, "videoQuality", 1);
smooth = _argInteger(L, "videoQuality", 2) ? RENDER_SMOOTH : RENDER_PIXELATED;
if (smooth != video->smooth) {
video->smooth = smooth;
video->transformChanged = true;
}
_luaTrace(L, "videoQuality", "%d %d", video->id, video->smooth);
return 0;
}
// videoRotate(id, degrees)
static int32_t apiVideoRotate(lua_State *L) {
VideoT *video = NULL;
double angle = 0.0;
_argCheck(L, "videoRotate", 2, 2);
video = _argVideo(L, "videoRotate", 1);
angle = fmod(_argNumber(L, "videoRotate", 2), DEGREES_PER_CIRCLE);
if (angle != video->angle) {
video->angle = angle;
video->transformChanged = true;
}
_luaTrace(L, "videoRotate", "%d %f", video->id, video->angle);
return 0;
}
// videoRotateAndScale(id, degrees, scale) or videoRotateAndScale(id, degrees, scaleX, scaleY)
static int32_t apiVideoRotateAndScale(lua_State *L) {
int32_t n = lua_gettop(L);
VideoT *video = NULL;
double angle = 0.0;
double scaleX = 1.0;
double scaleY = 1.0;
_argCheck(L, "videoRotateAndScale", 3, 4);
video = _argVideo(L, "videoRotateAndScale", 1);
angle = fmod(_argNumber(L, "videoRotateAndScale", 2), DEGREES_PER_CIRCLE);
scaleX = _argNumber(L, "videoRotateAndScale", 3);
scaleY = (n == 4) ? _argNumber(L, "videoRotateAndScale", 4) : scaleX;
if ((angle != video->angle) || (scaleX != video->scaleX) || (scaleY != video->scaleY)) {
video->angle = angle;
video->scaleX = scaleX;
video->scaleY = scaleY;
video->transformChanged = true;
}
_luaTrace(L, "videoRotateAndScale", "%d %f %f %f", video->id, video->angle, video->scaleX, video->scaleY);
return 0;
}
// videoScale(id, scale) or videoScale(id, scaleX, scaleY)
static int32_t apiVideoScale(lua_State *L) {
int32_t n = lua_gettop(L);
VideoT *video = NULL;
double scaleX = 1.0;
double scaleY = 1.0;
_argCheck(L, "videoScale", 2, 3);
video = _argVideo(L, "videoScale", 1);
scaleX = _argNumber(L, "videoScale", 2);
scaleY = (n == 3) ? _argNumber(L, "videoScale", 3) : scaleX;
if ((scaleX != video->scaleX) || (scaleY != video->scaleY)) {
video->scaleX = scaleX;
video->scaleY = scaleY;
video->transformChanged = true;
}
_luaTrace(L, "videoScale", "%d %f %f", video->id, video->scaleX, video->scaleY);
return 0;
}
// videoSeek(id, frame)
static int32_t apiVideoSeek(lua_State *L) {
VideoT *video = NULL;
int64_t frame = 0;
_argCheck(L, "videoSeek", 2, 2);
video = _argVideo(L, "videoSeek", 1);
frame = _argInteger64(L, "videoSeek", 2);
videoSeek(video->handle, frame);
_luaTrace(L, "videoSeek", "%d %" PRId64, video->id, frame);
return 0;
}
// videoSetAudioTrack(id, track)
static int32_t apiVideoSetAudioTrack(lua_State *L) {
VideoT *video = NULL;
int32_t track = 0;
_argCheck(L, "videoSetAudioTrack", 2, 2);
video = _argVideo(L, "videoSetAudioTrack", 1);
track = _argInteger(L, "videoSetAudioTrack", 2);
if ((track < 0) || (track >= videoGetAudioTracks(video->handle))) {
_luaDie(L, "videoSetAudioTrack", "Invalid audio track %d in video %d.", track, video->id);
}
videoSetAudioTrack(video->handle, track);
_luaTrace(L, "videoSetAudioTrack", "%d %d", video->id, track);
return 0;
}
// videoSetVolume(id, left, right) Percent, clamped to 0..100.
static int32_t apiVideoSetVolume(lua_State *L) {
VideoT *video = NULL;
int32_t left = 0;
int32_t right = 0;
_argCheck(L, "videoSetVolume", 1, 3);
// Hypseus extension: one argument sets the laserdisc's own volume on its 0 to 64 scale, the
// twin of the bare videoGetVolume(). Hypseus ignores a level outside that range with a warning
// rather than stopping the script, and so does this: a game that oversteps keeps playing.
if (lua_gettop(L) == 1) {
int32_t level = _argInteger(L, "videoSetVolume", 1);
if ((level < 0) || (level > HYPSEUS_VOLUME_MAX)) {
_luaTrace(L, "videoSetVolume", "disc %d out of range, ignored", level);
return 0;
}
_global.conf->volumeVldp = (level * VOLUME_MAX) / HYPSEUS_VOLUME_MAX;
if (_global.videoHandle >= 0) {
videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp);
}
_luaTrace(L, "videoSetVolume", "disc %d (%d percent)", level, _global.conf->volumeVldp);
return 0;
}
if (lua_gettop(L) != 3) {
_luaDie(L, "videoSetVolume", "Expected 1 or 3 arguments, got %d.", lua_gettop(L));
}
video = _argVideo(L, "videoSetVolume", 1);
left = _argInteger(L, "videoSetVolume", 2);
right = _argInteger(L, "videoSetVolume", 3);
left = SDL_clamp(left, 0, VIDEO_VOLUME_MAX);
right = SDL_clamp(right, 0, VIDEO_VOLUME_MAX);
videoSetVolume(video->handle, left, right);
_luaTrace(L, "videoSetVolume", "%d %d %d", video->id, left, right);
return 0;
}
// videoUnload(id)
static int32_t apiVideoUnload(lua_State *L) {
VideoT *video = NULL;
_argCheck(L, "videoUnload", 1, 1);
video = _argVideo(L, "videoUnload", 1);
_luaTrace(L, "videoUnload", "%d", video->id);
_videoDestroy(video);
return 0;
}
// viewDelete(view)
static int32_t apiViewDelete(lua_State *L) {
_argCheck(L, "viewDelete", 1, 1);
viewDelete(_argView(L, "viewDelete", 1));
return 0;
}
// view = viewNew(width, height): a camera rendered to a texture every frame, for materialSetView
static int32_t apiViewNew(lua_State *L) {
int32_t view;
_argCheck(L, "viewNew", 2, 2);
view = viewNew(_argInteger(L, "viewNew", 1), _argInteger(L, "viewNew", 2));
if (view < 0) {
_luaDie(L, "viewNew", "No view available (is 3D available, and are fewer than %d in use?).", MAX_VIEWS);
}
lua_pushinteger(L, view);
return 1;
}
// viewSetCamera(view [, node]): the node the view looks from; none for the default view
static int32_t apiViewSetCamera(lua_State *L) {
int32_t view;
int32_t camera = -1;
_argCheck(L, "viewSetCamera", 1, 2);
view = _argView(L, "viewSetCamera", 1);
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
camera = _argNode(L, "viewSetCamera", 2);
}
viewSetCamera(view, camera);
return 0;
}
// vldpFlash() Hypseus extension. Puts one white picture on the disc, the way a real player flashes
// when it is searched. The next disc update takes it off again, so it lasts one drawn frame; a
// stopped or paused disc keeps it that long and no longer.
static int32_t apiVldpFlash(lua_State *L) {
_argCheck(L, "vldpFlash", 0, 0);
if (_global.videoHandle >= 0) {
videoFlash(_global.videoHandle);
_global.refreshDisplay = true;
}
_luaTrace(L, "vldpFlash", "%d", _global.videoHandle);
return 0;
}
// vldpFocusArea(x, y, width, height) Hypseus extension. Shows only that part of the disc frame, in disc pixels, stretched into the usual place; with no arguments the whole frame again.
static int32_t apiVldpFocusArea(lua_State *L) {
int32_t n = lua_gettop(L);
double width = 0.0;
double height = 0.0;
double x = 0.0;
double y = 0.0;
double w = 0.0;
double h = 0.0;
_argCheck(L, "vldpFocusArea", 0, 4);
if (n == 0) {
_global.focused = false;
_global.refreshDisplay = true;
_luaTrace(L, "vldpFocusArea", "reset");
return 0;
}
if (n != 4) {
_luaDie(L, "vldpFocusArea", "Expected 0 or 4 arguments, got %d.", n);
}
x = _argNumber(L, "vldpFocusArea", 1);
y = _argNumber(L, "vldpFocusArea", 2);
w = _argNumber(L, "vldpFocusArea", 3);
h = _argNumber(L, "vldpFocusArea", 4);
if (_global.videoHandle >= 0) {
// Held inside the frame.
width = videoGetWidth(_global.videoHandle);
height = videoGetHeight(_global.videoHandle);
x = SDL_clamp(x, 0.0, width);
y = SDL_clamp(y, 0.0, height);
w = SDL_clamp(w, 0.0, width - x);
h = SDL_clamp(h, 0.0, height - y);
if ((w <= 0.0) || (h <= 0.0)) {
_luaDie(L, "vldpFocusArea", "The focus area lies outside the disc frame.");
}
_global.focusArea.x = (float)x;
_global.focusArea.y = (float)y;
_global.focusArea.w = (float)w;
_global.focusArea.h = (float)h;
_global.focused = true;
_global.refreshDisplay = true;
}
_luaTrace(L, "vldpFocusArea", "%f %f %f %f", x, y, w, h);
return 0;
}
// r, g, b = vldpGetPixel(x, y) Overlay coordinates; reads the current laserdisc frame.
static int32_t apiVldpGetPixel(lua_State *L) {
int32_t x = 0;
int32_t y = 0;
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
_argCheck(L, "vldpGetPixel", 2, 2);
x = (int32_t)(_argNumber(L, "vldpGetPixel", 1) / _global.overlayScaleX);
y = (int32_t)(_argNumber(L, "vldpGetPixel", 2) / _global.overlayScaleY);
if (_global.videoHandle >= 0) {
videoGetPixel(_global.videoHandle, x, y, &r, &g, &b);
}
_luaTrace(L, "vldpGetPixel", "%d %d %d %d %d", x, y, r, g, b);
lua_pushinteger(L, r);
lua_pushinteger(L, g);
lua_pushinteger(L, b);
return 3;
}
// degrees = vldpGetRotate() Hypseus extension. The presentation rotation in degrees clockwise (--rotate, 0 when unset).
static int32_t apiVldpGetRotate(lua_State *L) {
_argCheck(L, "vldpGetRotate", 0, 0);
_luaTrace(L, "vldpGetRotate", "%d", _global.videoRotate);
lua_pushinteger(L, _global.videoRotate);
return 1;
}
// scale = vldpGetScale() Hypseus extension. The display scale factor as a percentage (--scalefactor, 100 when unset).
static int32_t apiVldpGetScale(lua_State *L) {
_argCheck(L, "vldpGetScale", 0, 0);
_luaTrace(L, "vldpGetScale", "%d", _global.videoScale);
lua_pushinteger(L, _global.videoScale);
return 1;
}
// y, u, v = vldpGetYUVPixel(x, y) Hypseus extension. Raw 4:2:0 samples of the current disc frame at overlay coordinates; -1, -1, -1 without one.
static int32_t apiVldpGetYUVPixel(lua_State *L) {
int32_t x = 0;
int32_t y = 0;
int32_t luma = -1;
int32_t cb = -1;
int32_t cr = -1;
uint8_t l = 0;
uint8_t u = 0;
uint8_t v = 0;
_argCheck(L, "vldpGetYUVPixel", 2, 2);
x = (int32_t)(_argNumber(L, "vldpGetYUVPixel", 1) / _global.overlayScaleX);
y = (int32_t)(_argNumber(L, "vldpGetYUVPixel", 2) / _global.overlayScaleY);
if ((_global.videoHandle >= 0) && videoGetYUVPixel(_global.videoHandle, x, y, &l, &u, &v)) {
luma = l;
cb = u;
cr = v;
}
_luaTrace(L, "vldpGetYUVPixel", "%d %d %d %d %d", x, y, luma, cb, cr);
lua_pushinteger(L, luma);
lua_pushinteger(L, cb);
lua_pushinteger(L, cr);
return 3;
}
// vldpResetFocus() Hypseus extension. The whole disc frame again, as vldpFocusArea() with no arguments.
static int32_t apiVldpResetFocus(lua_State *L) {
_argCheck(L, "vldpResetFocus", 0, 0);
_global.focused = false;
_global.refreshDisplay = true;
_luaTrace(L, "vldpResetFocus", "reset");
return 0;
}
// vldpSetBlend(enabled) Hypseus extension. Smooths the disc picture down its rows, each luma
// sample becoming the average of itself and the samples above and below it, which is Hypseus's own
// three tap filter and takes the hard edges off an interlaced transfer.
static int32_t apiVldpSetBlend(lua_State *L) {
_argCheck(L, "vldpSetBlend", 1, 1);
_global.discBlend = _argBoolean(L, "vldpSetBlend", 1);
_discApplyPicture();
_global.refreshDisplay = true;
_luaTrace(L, "vldpSetBlend", "%d", _global.discBlend);
return 0;
}
// vldpSetLuma(enabled [, level]) Hypseus extension. Brightens or darkens the disc picture. The
// level runs 0 to 8 with 4, the default, changing nothing: the distance from 4 is added to every
// luma sample in eighths, so 0 halves the brightness and 8 raises it by half.
static int32_t apiVldpSetLuma(lua_State *L) {
_argCheck(L, "vldpSetLuma", 1, 2);
_global.discLumaOn = _argBoolean(L, "vldpSetLuma", 1);
_global.discLumaLevel = (lua_gettop(L) == 2) ? _argInteger(L, "vldpSetLuma", 2) : LUMA_LEVEL_NEUTRAL;
if ((_global.discLumaLevel < 0) || (_global.discLumaLevel > LUMA_LEVEL_MAX)) {
_luaDie(L, "vldpSetLuma", "Luma level must be 0 to %d: %d", LUMA_LEVEL_MAX, _global.discLumaLevel);
}
_discApplyPicture();
_global.refreshDisplay = true;
_luaTrace(L, "vldpSetLuma", "%d %d", _global.discLumaOn, _global.discLumaLevel);
return 0;
}
// vldpSetMonochrome(enabled) Hypseus extension. Shows the disc in luma only; vldpGetPixel keeps returning colour.
static int32_t apiVldpSetMonochrome(lua_State *L) {
bool enabled = false;
_argCheck(L, "vldpSetMonochrome", 1, 1);
enabled = _argBoolean(L, "vldpSetMonochrome", 1);
_global.discMonochrome = enabled;
_discApplyPicture();
_global.refreshDisplay = true;
_luaTrace(L, "vldpSetMonochrome", "%d", enabled);
return 0;
}
// applied = vldpSetRotate(degrees) Hypseus extension. Turns the whole presentation clockwise; only
// 0, 90, 180 and 270 are taken, and anything else answers false rather than dying.
static int32_t apiVldpSetRotate(lua_State *L) {
int32_t degrees = 0;
bool applied = false;
_argCheck(L, "vldpSetRotate", 1, 1);
degrees = (int32_t)_argNumber(L, "vldpSetRotate", 1);
applied = _setVideoRotate(degrees);
if (applied) {
_computeVideoRect();
}
_luaTrace(L, "vldpSetRotate", "%d %d", degrees, applied);
lua_pushboolean(L, applied);
return 1;
}
// applied = vldpSetScale(percent) Hypseus extension. Shrinks the picture about its centre, 25 to
// 100 per cent; anything else answers false, as does a second change within 15 milliseconds, which
// is the rate Hypseus limits its own held-key zoom to.
// x, y = vldpGetShift() Where the picture sits inside the room the scale factor leaves, as
// --shiftx and --shifty set it, -100 to 100.
static int32_t apiVldpGetShift(lua_State *L) {
_argCheck(L, "vldpGetShift", 0, 0);
lua_pushinteger(L, _global.videoShiftX);
lua_pushinteger(L, _global.videoShiftY);
return 2;
}
// vldpSetShift(x, y) Moves the picture inside that room, live. vldpGetScale and vldpSetScale
// decide how much room there is.
static int32_t apiVldpSetShift(lua_State *L) {
int32_t x = 0;
int32_t y = 0;
_argCheck(L, "vldpSetShift", 2, 2);
x = _argInteger(L, "vldpSetShift", 1);
y = _argInteger(L, "vldpSetShift", 2);
if ((x < SHIFT_MIN) || (x > SHIFT_MAX) || (y < SHIFT_MIN) || (y > SHIFT_MAX)) {
_luaDie(L, "vldpSetShift", "A shift is %d to %d per cent: %d, %d", SHIFT_MIN, SHIFT_MAX, x, y);
}
_global.videoShiftX = x;
_global.videoShiftY = y;
_computeVideoRect();
_global.refreshDisplay = true;
_luaTrace(L, "vldpSetShift", "%d %d", x, y);
return 0;
}
static int32_t apiVldpSetScale(lua_State *L) {
int32_t percent = 0;
uint64_t now = utilTicks();
bool applied = false;
_argCheck(L, "vldpSetScale", 1, 1);
percent = (int32_t)_argNumber(L, "vldpSetScale", 1);
if ((percent >= SCALE_FACTOR_MIN) && (percent <= SCALE_FACTOR_MAX) && (now >= _global.videoScaleClock)) {
_global.videoScale = percent;
_global.videoScaleClock = now + VIDEO_SCALE_THROTTLE_MS;
_computeVideoRect();
applied = true;
}
_luaTrace(L, "vldpSetScale", "%d %d", percent, applied);
lua_pushboolean(L, applied);
return 1;
}
static int32_t apiVldpSetVerbose(lua_State *L) {
return _apiUnimplemented(L, "vldpSetVerbose");
}
// voiceFree(handle) Releases an Opus encoder or decoder, a voice processor, or a jitter buffer. All of
// them are released when the game ends.
static int32_t apiVoiceFree(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "voiceFree", 1, 1);
handle = _argHandle(L, "voiceFree", 1, _voiceIsAny, "voice handle");
voiceFree(handle);
_luaTrace(L, "voiceFree", "%d", handle);
return 0;
}
// cleaned, voiced = voiceProcess(processor, mic [, speaker]) One frame of the microphone with the echo
// of what the speakers played taken out, noise suppressed, and the level evened, and whether it is a
// voice. mic and speaker are the processor's frame of 16-bit samples each.
static int32_t apiVoiceProcess(lua_State *L) {
int16_t mic[VOICE_FRAME_MAX];
int16_t speaker[VOICE_FRAME_MAX];
const char *data = NULL;
size_t bytes = 0;
int32_t handle = 0;
size_t frame = 0;
bool heard = false;
bool voiced = false;
_argCheck(L, "voiceProcess", 2, 3);
handle = _argHandle(L, "voiceProcess", 1, _voiceIsProcessor, "voice processor");
frame = (size_t)voiceFrame(handle) * sizeof(int16_t);
data = _argData(L, "voiceProcess", 2, &bytes);
if (bytes != frame) {
_luaDie(L, "voiceProcess", "The microphone frame must be %d bytes: %d", (int32_t)frame, (int32_t)bytes);
}
memcpy(mic, data, frame);
if ((lua_gettop(L) >= 3) && !lua_isnil(L, 3)) {
data = _argData(L, "voiceProcess", 3, &bytes);
if (bytes != frame) {
_luaDie(L, "voiceProcess", "The speaker frame must be %d bytes: %d", (int32_t)frame, (int32_t)bytes);
}
memcpy(speaker, data, frame);
heard = true;
}
voiceProcess(handle, mic, heard ? speaker : NULL, &voiced);
lua_pushlstring(L, (const char *)mic, frame);
lua_pushboolean(L, voiced);
return 2;
}
// processor = voiceProcessorNew([frame [, rate [, tailMs]]]) Echo cancellation, noise suppression,
// gain, and voice detection for frames of 960 samples at 48000, against echoes up to 200 ms, unless
// given.
static int32_t apiVoiceProcessorNew(lua_State *L) {
int32_t frame = VOICE_DEFAULT_FRAME;
int32_t rate = MIC_RATE;
int32_t tailMs = VOICE_DEFAULT_TAIL_MS;
int32_t handle = 0;
_argCheck(L, "voiceProcessorNew", 0, 3);
if (lua_gettop(L) >= 1) {
frame = _argInteger(L, "voiceProcessorNew", 1);
}
if (lua_gettop(L) >= 2) {
rate = _argInteger(L, "voiceProcessorNew", 2);
}
if (lua_gettop(L) >= 3) {
tailMs = _argInteger(L, "voiceProcessorNew", 3);
}
handle = voiceProcessorNew(frame, rate, tailMs);
if (handle < 0) {
_luaDie(L, "voiceProcessorNew", "%s (frame %d, rate %d, tail %d ms)", voiceError(handle), frame, rate, tailMs);
}
_luaTrace(L, "voiceProcessorNew", "%d frame %d at %d, tail %d ms", handle, frame, rate, tailMs);
lua_pushinteger(L, handle);
return 1;
}
// ===== Engine entry point =====
// A game database named on the command line runs one entry of its games.dat (--entry, default the first).
ConfigT *confFromDatabase(const ConfigT *conf) {
lua_State *L = luaL_newstate();
ConfigT *base = cloneConf(conf);
ConfigT *result = NULL;
base->container = strdup(conf->scriptFile);
free(base->scriptFile);
base->scriptFile = NULL;
vfsInit(base->container, base->dataDirBase, NULL);
luaL_openlibs(L);
if ((_luaLoadFile(L, VFS_GAMES_DAT, NULL, false) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
utilDie("%s: %s", base->container, lua_tostring(L, -1));
}
lua_getglobal(L, "GAMES");
if (!lua_istable(L, -1) || (lua_rawgeti(L, -1, conf->entry) != LUA_TTABLE)) {
utilDie("%s: games.dat has no entry %d.", base->container, conf->entry);
}
lua_replace(L, 1);
lua_settop(L, 1);
result = _buildConfFromTable(L, base, NULL);
if (result->scriptFile == NULL) {
utilDie("%s: games.dat entry %d has no SCRIPT.", base->container, conf->entry);
}
free(result->container);
result->container = strdup(base->container);
lua_close(L);
destroyConf(&base);
return result;
}
// A loose script launched by name runs with the settings of its games.dat entry, as the menu would
// run it: the nearest games.dat in the script's directory or above, whose entry names the script
// (entry paths count from the directory above the games.dat). Options typed on the command line
// keep their values. NULL when there is no such entry.
ConfigT *confFromGamesDat(const ConfigT *conf) {
lua_State *L = NULL;
ConfigT *result = NULL;
char *dir = utilGetUpToLastPathComponent(conf->scriptFile);
char *file = NULL;
char *base = NULL;
char *trimmed = NULL;
char *parent = NULL;
char *data = NULL;
char *resolved = NULL;
const char *script = NULL;
size_t bytes = 0;
size_t length = 0;
int32_t level = 0;
int32_t entry = 0;
bool found = false;
// Find the games.dat.
for (level = 0; (level < GAMES_DAT_LEVELS) && (base == NULL); level++) {
file = utilCreateString("%s%s", dir, VFS_GAMES_DAT);
if (utilFileExists(file)) {
// Entry paths are relative to the directory above the one holding games.dat.
trimmed = strdup(dir);
length = strlen(trimmed);
while ((length > 0) && ((trimmed[length - 1] == '/') || (trimmed[length - 1] == '\\'))) {
trimmed[--length] = 0;
}
base = utilGetUpToLastPathComponent(trimmed);
free(trimmed);
break;
}
free(file);
file = NULL;
trimmed = strdup(dir);
length = strlen(trimmed);
while ((length > 0) && ((trimmed[length - 1] == '/') || (trimmed[length - 1] == '\\'))) {
trimmed[--length] = 0;
}
if (length == 0) {
free(trimmed);
break;
}
parent = utilGetUpToLastPathComponent(trimmed);
free(trimmed);
if (strcmp(parent, dir) == 0) {
free(parent);
break;
}
free(dir);
dir = parent;
}
if (base == NULL) {
free(dir);
return NULL;
}
// Run it and look for the entry naming this script.
data = utilReadFile(file, &bytes);
L = luaL_newstate();
luaL_openlibs(L);
if ((data == NULL) || (luaL_loadbuffer(L, data, bytes, file) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
utilSay("Note: %s ignored: %s", file, (data == NULL) ? "cannot read it" : lua_tostring(L, -1));
} else {
lua_getglobal(L, "GAMES");
if (lua_istable(L, -1)) {
for (entry = 1; !found; entry++) {
if (lua_rawgeti(L, -1, entry) != LUA_TTABLE) {
lua_pop(L, 1);
break;
}
if (lua_getfield(L, -1, "SCRIPT") == LUA_TSTRING) {
script = lua_tostring(L, -1);
resolved = utilCreateString("%s%s", (strcmp(base, "./") == 0) ? "" : base, script);
found = _sameLoosePath(resolved, conf->scriptFile);
free(resolved);
}
lua_pop(L, 1);
if (found) {
lua_replace(L, 1);
lua_settop(L, 1);
result = _buildConfFromTable(L, conf, NULL);
result->entry = entry;
} else {
lua_pop(L, 1);
}
}
}
}
lua_close(L);
free(data);
if (result != NULL) {
// The launched file is the entry's script, however either was spelled; the video keeps
// the entry's spelling, made relative to where the launch happened.
free(result->scriptFile);
result->scriptFile = strdup(conf->scriptFile);
if (conf->given & GIVEN_VIDEO) {
free(result->videoFile);
result->videoFile = (conf->videoFile != NULL) ? strdup(conf->videoFile) : NULL;
result->disc = conf->disc;
result->isFrameFile = conf->isFrameFile;
} else if ((result->videoFile != NULL) && (strcmp(base, "./") != 0)) {
resolved = utilCreateString("%s%s", base, result->videoFile);
free(result->videoFile);
result->videoFile = resolved;
}
if (conf->given & GIVEN_STRETCH) {
result->stretchVideo = conf->stretchVideo;
}
if (conf->given & GIVEN_NO_MOUSE) {
result->noMouse = conf->noMouse;
}
if (conf->given & GIVEN_RESOLUTION) {
result->xResolution = conf->xResolution;
result->yResolution = conf->yResolution;
result->resolutionWasCalculated = conf->resolutionWasCalculated;
}
if (conf->given & GIVEN_SINDEN) {
result->sindenArgc = conf->sindenArgc;
memcpy(result->sindenArgv, conf->sindenArgv, sizeof(result->sindenArgv));
}
if (conf->given & GIVEN_AUDIO_TRACK) {
result->audioOutputTrack = conf->audioOutputTrack;
}
if (conf->given & GIVEN_AUDIO_DELAY) {
result->audioDelayMs = conf->audioDelayMs;
}
if (conf->given & GIVEN_AUDIO_SUFFIX) {
free(result->audioSuffix);
result->audioSuffix = (conf->audioSuffix != NULL) ? strdup(conf->audioSuffix) : NULL;
}
if (conf->given & GIVEN_CANVAS) {
result->canvasWidth = conf->canvasWidth;
result->canvasHeight = conf->canvasHeight;
}
}
free(file);
free(base);
free(dir);
return result;
}
// Releases what settingsLoad returned.
void settingsFree(SettingT *settings, int32_t count) {
int32_t x = 0;
for (x = 0; x < count; x++) {
free(settings[x].key);
free(settings[x].value);
free(settings[x].source);
}
free(settings);
}
// Reads settings.cfg from the four places controls.cfg comes from, in the same order and each
// place only once, and hands back every key the files set with the file that set it. A later file
// replaces an earlier one key by key, exactly as the control mappings merge. Values come back as
// the strings the command line would have carried, so the caller needs no second parser.
SettingT *settingsLoad(const ConfigT *conf, int32_t *count) {
SettingT *list = NULL;
char *places[SETTINGS_PLACES];
int32_t c = 0;
*count = 0;
_configPlaces(SETTINGS_FILE, conf->dataDir, conf->scriptFile, places);
for (c = 0; c < SETTINGS_PLACES; c++) {
if (places[c] != NULL) {
_settingsReadFile(places[c], &list, count);
}
free(places[c]);
}
if (*count > 1) {
qsort(list, (size_t)*count, sizeof(SettingT), _settingsCompare);
}
return list;
}
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf) {
int32_t x = 0;
int32_t y = 0;
int32_t xr = 0;
int32_t yr = 0;
int32_t slot = 0;
int32_t axisIndex = 0;
int32_t code = 0;
int32_t intReturn = 0;
int32_t videoWidth = 0;
int32_t videoHeight = 0;
int64_t thisFrame = -1;
int64_t lastFrame = -1;
uint64_t frameClock = 0;
SDL_FRect rotateTarget;
SDL_FRect screenRect;
SDL_Texture *sceneTexture = NULL;
SDL_Texture *frontTexture = NULL;
SDL_Surface *overlayShown = NULL;
int32_t presentWidth = 0;
int32_t presentHeight = 0;
float deltaX = 0;
float deltaY = 0;
double mouseFraction = 0;
SDL_Event event;
ManyMouseEvent mouseEvent;
MouseT *mouse = NULL;
char *audioFile = NULL;
uint64_t frameStartNS = 0;
uint64_t stageStartNS = 0;
uint64_t loopNS = 0;
int32_t finished[SOUND_QUEUE_SIZE];
int32_t finishedCount = 0;
bool denied = false;
// Set up globals
memset(&_global, 0, sizeof(GlobalT));
_global.frameFileHandle = -1;
_global.videoHandle = -1;
_global.mouseMode = MOUSE_SINGLE;
_global.controllerDeadZone = CONTROLLER_DEAD_ZONE_DEFAULT;
_global.fullScreen = conf->fullScreen || conf->fullScreenWindow;
_global.running = true;
_global.discStopped = true;
_global.mouseEnabled = true;
_global.window = window;
_global.renderer = renderer;
_global.device = device;
// Local copy of config, ahead of the subsystems, which read it
_global.conf = cloneConf(conf);
_subsystemsInit();
_global.quitChordRelease = -1;
_global.quitChordHeldCount = 0;
if (_global.conf->quitChord != NULL) {
singeParseQuitChord(_global.conf->quitChord, _global.quitChord.code, &_global.quitChord.count);
}
// --deterministic: the virtual clock and the fixed random stream, both settled before anything
// that reads either. The step doubles as the seed, so one number describes the whole run.
utilDeterministic(_global.conf->deterministic, (uint32_t)_global.conf->deterministicStep);
if (_global.conf->recordFile != NULL) {
_global.recordRequest = RECORD_REQUEST_START;
_global.generatedAudio = _global.conf->deterministic;
}
if (_global.conf->deterministic) {
SDL_srand((uint64_t)_global.conf->deterministicStep);
_progTrace("Deterministic mode: %d ms a frame, generators seeded with %d", _global.conf->deterministicStep, _global.conf->deterministicStep);
}
vfsInit(_global.conf->container, _global.conf->dataDirBase, _global.conf->dataDir);
// The save file lives in the game's own data directory, so every game keeps its own and a
// packed game carries none of it inside the .game.
persistOpen(_global.conf->dataDir);
videoSetAudioDelay(_global.conf->audioDelayMs);
// What the service tools left this machine set to, over whatever the command line asked for.
_loadMachineSettings();
utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay());
_loadControlMappings();
// Show splash screens
if (!_global.conf->noLogos) {
_progTrace("Showing splash screens");
_doLogos();
}
// Start Lua for game
_createScriptContext();
guiAttachLua(_global.luaContext);
// Open main video file, if this is a laserdisc game. Otherwise the canvas is the world.
if (_global.conf->disc) {
_progTrace("Opening main video file");
if (_global.conf->isFrameFile) {
_global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated);
frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle
} else {
// A lone .m2v is a Daphne era elementary stream with no audio of its own; its sound is
// the .ogg beside it, which is the same sidecar a framefile's segments look for. Any
// other container keeps the audio inside it, and frameFileAudioName answers NULL.
audioFile = frameFileAudioName(_global.conf->videoFile, _startAudioSuffix());
_global.videoHandle = videoLoad(_global.conf->videoFile, audioFile, _global.conf->dataDir, _global.renderer, false);
free(audioFile);
}
videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp);
_global.canvasWidth = videoGetWidth(_global.videoHandle);
_global.canvasHeight = videoGetHeight(_global.videoHandle);
} else {
_progTrace("No disc; canvas is %dx%d", _global.conf->canvasWidth, _global.conf->canvasHeight);
_global.canvasWidth = _global.conf->canvasWidth;
_global.canvasHeight = _global.conf->canvasHeight;
}
videoWidth = _global.canvasWidth;
videoHeight = _global.canvasHeight;
// Should we resize the window to the video's shape?
if (_global.conf->resolutionWasCalculated && !_global.conf->fullScreen && !_global.conf->fullScreenWindow) {
if (videoWidth * _global.conf->yResolution > videoHeight * _global.conf->xResolution) {
// Video is wider than the window: keep the width, shrink the height.
_global.conf->yResolution = _global.conf->xResolution * videoHeight / videoWidth;
} else {
// Video is taller: keep the height, shrink the width.
_global.conf->xResolution = _global.conf->yResolution * videoWidth / videoHeight;
}
_progTrace("Resizing window to %dx%d based on main video file", _global.conf->xResolution, _global.conf->yResolution);
SDL_SetWindowSize(_global.window, _global.conf->xResolution, _global.conf->yResolution);
SDL_SyncWindow(_global.window);
SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
}
// The picture's position, size and rotation start from the command line; a script may move
// them later through vldpSetScale and vldpSetRotate.
_global.videoScale = _global.conf->scaleFactor;
_global.videoShiftX = _global.conf->shiftX;
_global.videoShiftY = _global.conf->shiftY;
// Sinden Light Gun Border Setup: the black border (if any) is the outermost ring, the white
// border sits inside it, and the game is drawn inside both. Widths are in video pixels;
// _computeVideoRect() places the rings, so a runtime change moves them too.
if (_global.conf->sindenArgc > 0) {
_global.sindenBorder = true;
_global.sindenWhiteColor.r = COLOR_BYTE_MAX;
_global.sindenWhiteColor.g = COLOR_BYTE_MAX;
_global.sindenWhiteColor.b = COLOR_BYTE_MAX;
_global.sindenWhiteColor.a = SDL_ALPHA_OPAQUE;
_global.sindenBlackColor.a = SDL_ALPHA_OPAQUE;
switch (_global.conf->sindenArgc) {
// WW - Just the width of the white border
case SINDEN_WHITE:
_global.sindenWhiteWidth = _global.conf->sindenArgv[0];
break;
// WW WB - Width of white border and then black border
case SINDEN_WHITE_BLACK:
_global.sindenWhiteWidth = _global.conf->sindenArgv[0];
_global.sindenBlackWidth = _global.conf->sindenArgv[1];
break;
// RW GW BW WW - Custom color "white" border and width
case SINDEN_CUSTOM_WHITE:
_global.sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
_global.sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
_global.sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
_global.sindenWhiteWidth = _global.conf->sindenArgv[3];
break;
// RW GW BW WW WB - Custom color "white" border and width then width of black border
case SINDEN_CUSTOM_WHITE_BLACK:
_global.sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
_global.sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
_global.sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
_global.sindenWhiteWidth = _global.conf->sindenArgv[3];
_global.sindenBlackWidth = _global.conf->sindenArgv[4];
break;
// RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width
case SINDEN_CUSTOM_WHITE_CUSTOM_BLACK:
_global.sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
_global.sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
_global.sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
_global.sindenWhiteWidth = _global.conf->sindenArgv[3];
_global.sindenBlackColor.r = (uint8_t)_global.conf->sindenArgv[4];
_global.sindenBlackColor.g = (uint8_t)_global.conf->sindenArgv[5];
_global.sindenBlackColor.b = (uint8_t)_global.conf->sindenArgv[6];
_global.sindenBlackWidth = _global.conf->sindenArgv[7];
break;
default:
utilDie("Bad Sinden argument count: %d", _global.conf->sindenArgc);
}
if ((_global.sindenWhiteWidth < 0) || (_global.sindenBlackWidth < 0) || (2 * (_global.sindenWhiteWidth + _global.sindenBlackWidth) >= SDL_min(videoWidth, videoHeight))) {
utilDie("Sinden border of %d + %d pixels does not fit a %dx%d video", _global.sindenWhiteWidth, _global.sindenBlackWidth, videoWidth, videoHeight);
}
}
// The bezel artwork is an input to the rectangle (its cutout is where the picture goes) and to
// where the Sinden border sits, so it is loaded before either is worked out.
_bezelLoad();
// Everything renders in video coordinates, letterboxed unless the user wants it stretched, and
// either way mouse positions convert back into those coordinates. A quarter turn swaps the
// logical size, so _setVideoRotate sets the presentation for every rotation including none.
_setVideoRotate(_global.conf->rotate);
_computeVideoRect();
// Create overlay surface and its texture
x = (int32_t)(videoWidth * OVERLAY_SCALE_DEFAULT);
y = (int32_t)(videoHeight * OVERLAY_SCALE_DEFAULT);
_progTrace("Creating overlay of %dx%d", x, y);
_overlayResize(x, y);
// Mouse setup
_global.mouseEnabled = !_global.conf->noMouse;
_progTrace("Initializing ManyMouse");
_global.mouseCount = ManyMouse_Init();
_progTrace("Mouse Driver: %s", ManyMouse_DriverName());
_progTrace("Mice Found: %d", _global.mouseCount);
if (_global.mouseCount < 0) {
_global.mouseCount = 0;
}
if (_global.mouseCount > MAX_MICE) {
_global.mouseCount = MAX_MICE;
}
// ManyMouse enumerates /dev/input itself, which needs membership of the "input" group; SDL's
// own pointer needs nothing. Only many-mouse mode reads ManyMouse's events -- the poll in the
// event loop discards them in single-mouse mode -- so enumerating nothing must not cost a
// machine its pointer, or Singe has no mouse at all on a stock Debian or Ubuntu install, in a
// container, or under any account that was never added to that group. The group is only
// mentioned when it is the reason: a machine with no mouse, a handheld say, is not warned.
if ((_global.mouseCount < 1) && _global.mouseEnabled) {
denied = _inputDevicesDenied();
if (_global.conf->manyMouse) {
utilSay("Warning: No mice detected. Mouse input disabled.%s", denied ? " This user cannot read the input devices; add it to the \"input\" group." : "");
_global.mouseEnabled = false;
} else if (denied) {
utilSay("Warning: This user cannot read the input devices, so light guns and --manymouse will not work; add it to the \"input\" group. The system pointer still works.");
} else {
_progTrace("No mice found; using the system pointer");
}
}
for (x = 0; x < _global.mouseCount; x++) {
strncpy(_global.mice[x].name, ManyMouse_DeviceName((unsigned)x), sizeof(_global.mice[x].name) - 1);
_global.mice[x].x = videoWidth / 2;
_global.mice[x].y = videoHeight / 2;
_progTrace("Mouse %d: %s", x, _global.mice[x].name);
}
if (_global.conf->manyMouse) {
_progTrace("--manymouse: the mice are told apart whatever the game asks for");
}
if (_global.conf->absolutesOnly) {
_progTrace("--absolutes_only: only a device that has reported an absolute position is heard");
}
// Extra controller mappings, before any pad is opened: what a user supplies beats SDL's own
// database, and both beat the mapping Singe writes for a device nobody has ever mapped.
_loadGamepadDatabase();
// Controllers are started by the event loop only for the first script in
// the queue - so kick 'em here to be sure they're going.
_startControllers();
// Sound effect tracks: a fixed pool so scripts keep getting small channel numbers.
_global.effectsVolume = _defaultVolume(AUDIO_MAX_VOLUME);
_progTrace("Setting up sound effects mixer");
for (x = 0; x < EFFECT_TRACKS; x++) {
_effectTracks[x] = MIX_CreateTrack(videoGetMixer());
if (_effectTracks[x] == NULL) {
utilDie("%s", SDL_GetError());
}
MIX_TagTrack(_effectTracks[x], EFFECT_TAG);
MIX_SetTrackGain(_effectTracks[x], _mixerGain(_global.effectsVolume, AUDIO_MAX_VOLUME));
// Let us know when sounds end
MIX_SetTrackStoppedCallback(_effectTracks[x], _effectStopped, (void *)(intptr_t)x);
}
// Nothing is heard while --nosound is given, and nothing until the first input while
// --startsilent is: one gain over the whole mixer, so no configured volume is disturbed.
_setAudioMuted(_global.conf->noSound || _global.conf->startSilent);
_global.idleClock = utilTicks();
// The script's own defaults (the disc parked on frame 1 among them), now that everything they touch exists.
_resetScriptState();
// Load overlay font
_progTrace("Loading console font");
_global.consoleFontSurface = _loadEmbeddedPng(font_png, font_png_len);
_global.consoleFontWidth = _global.consoleFontSurface->w / CONSOLE_FONT_GLYPHS;
_global.consoleFontHeight = _global.consoleFontSurface->h;
_progTrace("Console font is %dx%d, %s", _global.consoleFontSurface->w, _global.consoleFontSurface->h, SDL_GetPixelFormatName(_global.consoleFontSurface->format));
// The glyph background is whatever colour the top left pixel has (magenta in the shipped font).
SDL_SetSurfaceColorKey(_global.consoleFontSurface, true, _surfaceCornerKey(_global.consoleFontSurface));
// Start script
memset(&_perf, 0, sizeof(_perf));
_perf.scriptStartNS = SDL_GetTicksNS();
scriptWatchStart();
_runScript(true);
_perf.windowStartNS = SDL_GetTicksNS();
// Game Loop
_progTrace("Script is running");
while (_global.running) {
// The overlay's clock is the real one even in deterministic mode: a developer asking how
// long a frame took wants the answer in the seconds they are sitting through.
frameStartNS = SDL_GetTicksNS();
// One frame of simulated time, the only place the virtual clock moves. Off it, nothing happens.
utilTickAdvance();
if (_global.generatedAudio) {
_generateAudio();
}
// A reload asked for by the script, F5 or a changed file happens here, between frames.
if (_global.conf->reload && _watchedChanged()) {
_global.reloadRequested = true;
}
if (_global.reloadRequested) {
_reloadScript();
}
// SDL Event Loop
while (SDL_PollEvent(&event)) {
if (_isInputEvent(event.type)) {
_noteInput();
}
switch (event.type) {
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
slot = _controllerSlot(event.gaxis.which);
if ((slot < 0) || (event.gaxis.axis >= CONTROLLER_AXIS_COUNT)) {
break;
}
axisIndex = AXIS_INDEX_CONTROLLER(slot, event.gaxis.axis);
// Each axis direction is a "key" so it can be mapped in controls.cfg.
code = CODE_GAMEPAD_BASE + slot * CODE_GAMEPAD_STRIDE + event.gaxis.axis * CODE_AXIS_STRIDE;
code += (event.gaxis.value < 0) ? CODE_AXIS_NEGATIVE : CODE_AXIS_POSITIVE;
if (abs(event.gaxis.value) > _global.axisDeadZone[axisIndex]) {
if (_global.axisCode[axisIndex] != code) {
_releaseAxis(axisIndex);
_processKey(true, 0, code);
_global.axisCode[axisIndex] = code;
}
} else {
_releaseAxis(axisIndex);
}
_global.axisCache[axisIndex] = event.gaxis.value;
if (!_global.frozen) {
_callLua("onControllerMoved", "iii", event.gaxis.axis, event.gaxis.value, slot);
}
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
slot = _controllerSlot(event.gbutton.which);
if (slot < 0) {
break;
}
if (event.gbutton.button < CONTROLLER_BUTTON_COUNT) {
if ((event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) && (utilTicks() < _global.inputGraceUntil)) {
_global.buttonSuppressed[slot][event.gbutton.button] = true;
}
if (_global.buttonSuppressed[slot][event.gbutton.button]) {
// Held since before this script: swallow it, and its release.
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_UP) {
_global.buttonSuppressed[slot][event.gbutton.button] = false;
}
break;
}
}
code = CODE_GAMEPAD_BASE + slot * CODE_GAMEPAD_STRIDE + CODE_GAMEPAD_BUTTON_OFFSET + event.gbutton.button;
// Which button SDL says it is, by the name controls.cfg and --quitchord use for it.
_progTrace("Pad %d: %s %s", slot, SDL_GetGamepadStringForButton((SDL_GamepadButton)event.gbutton.button), (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) ? "down" : "up");
_processKey(event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN, 0, code);
break;
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
case SDL_EVENT_JOYSTICK_BUTTON_UP:
_joystickButtonTrace(&event.jbutton);
break;
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
case SDL_EVENT_JOYSTICK_ADDED:
case SDL_EVENT_JOYSTICK_REMOVED:
// A joystick arriving matters as much as a gamepad arriving: a device SDL does
// not recognise is only ever reported as a joystick, and it is here that it is
// given a mapping of its own and becomes a pad.
_startControllers();
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
// Mapped switches want one press per key; full mode keeps repeats for text entry.
if (event.key.repeat && (_global.keyboardMode == KEYBOARD_NORMAL)) {
break;
}
if (event.key.scancode < SDL_SCANCODE_COUNT) {
if ((event.type == SDL_EVENT_KEY_DOWN) && (utilTicks() < _global.inputGraceUntil)) {
_global.keySuppressed[event.key.scancode] = true;
}
if (_global.keySuppressed[event.key.scancode]) {
// Held since before this script: swallow it, and its release.
if (event.type == SDL_EVENT_KEY_UP) {
_global.keySuppressed[event.key.scancode] = false;
}
break;
}
}
// Alt-Enter is the engine's, as it is in Hypseus: the first hotkey a user reaches
// for, and the game never sees it.
if ((event.type == SDL_EVENT_KEY_DOWN) && ((event.key.mod & SDL_KMOD_ALT) != 0) && ((event.key.scancode == SDL_SCANCODE_RETURN) || (event.key.scancode == SDL_SCANCODE_KP_ENTER))) {
// Swallow the release too, through the same list that swallows a key held
// over from before the script, so the game never sees half a keystroke.
_global.keySuppressed[event.key.scancode] = true;
_toggleFullScreen();
break;
}
// A GUI with a focused element takes the key before the game does.
if (!_guiKey(event.key.key, event.key.mod, event.type == SDL_EVENT_KEY_DOWN)) {
_processKey(event.type == SDL_EVENT_KEY_DOWN, event.key.key, event.key.scancode);
}
break;
case SDL_EVENT_TEXT_INPUT:
// A focused text field takes it; otherwise a script with the keyboard whole does.
if (!_guiText(event.text.text) && (_global.keyboardMode == KEYBOARD_FULL) && !_global.frozen) {
_callLua("onTextInput", "s", event.text.text);
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
// SDL re-reports every held key as a press when a window gains focus.
_suppressHeldInput();
break;
case SDL_EVENT_MOUSE_MOTION:
if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE)) {
// Positions arrive in window pixels; the game works in the overlay's coordinates,
// drawn into whatever the rotation, shift, scale and Sinden border left of it.
SDL_ConvertEventToRenderCoordinates(_global.renderer, &event);
_mapPointer(event.motion.x, event.motion.y, &x, &y);
_mapPointerDelta(event.motion.xrel, event.motion.yrel, &xr, &yr);
_guiMouseMove(x, y);
_fireMouseMoved(0, x, y, xr, yr);
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE) && (event.button.button >= SDL_BUTTON_LEFT) && (event.button.button <= SDL_BUTTON_X2)) {
// A GUI under the pointer takes the click before the game does.
if (!_guiMouseButton(event.button.button, event.type == SDL_EVENT_MOUSE_BUTTON_DOWN)) {
_processKey(event.type == SDL_EVENT_MOUSE_BUTTON_DOWN, 0, _mouseCode(0, _sdlMouseButtonToCode[event.button.button]));
}
}
break;
case SDL_EVENT_MOUSE_WHEEL:
if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE) && (event.wheel.y != 0.0f) && !_guiMouseWheel(event.wheel.y)) {
code = _mouseCode(0, (event.wheel.y > 0.0f) ? CODE_MOUSE_WHEEL_UP : CODE_MOUSE_WHEEL_DOWN);
_processKey(true, 0, code);
_processKey(false, 0, code);
}
break;
case SDL_EVENT_MOUSE_REMOVED:
// A gun unplugged. Hypseus's SWITCH_MOUSE_DISCONNECT, so a gun game can say so
// on screen instead of appearing to have died.
_progTrace("A mouse was disconnected");
_deliverSwitch(INPUT_MOUSE_DISCONNECT, true);
_deliverSwitch(INPUT_MOUSE_DISCONNECT, false);
break;
case SDL_EVENT_QUIT:
_progTrace("Quit requested");
_global.running = false;
break;
default:
break;
}
}
if ((_global.quitChordHeldCount > 0) && (utilTicks() - _global.quitChordHeldMS >= QUIT_CHORD_WAIT_MS)) {
_quitChordLetGo(-1);
}
if (mainTerminated() && _global.running) {
_progTrace("Terminated by a signal");
_global.running = false;
}
// Mouse Event Loop - drained even when unused so the queue never fills.
while (ManyMouse_PollEvent(&mouseEvent)) {
_noteInput();
if (!_global.mouseEnabled || (_global.mouseMode != MOUSE_MANY) || (mouseEvent.device >= (unsigned)_global.mouseCount)) {
continue;
}
mouse = &_global.mice[mouseEvent.device];
if (mouseEvent.type == MANYMOUSE_EVENT_ABSMOTION) {
_global.mouseAbsoluteSeen[mouseEvent.device] = true;
}
// --absolutes_only keeps the light guns and drops the ordinary mice. ManyMouse has no
// way to ask a device what it is, so a device counts as absolute once it has reported an
// absolute position and not before; a real gun does that with its first movement.
if (_global.conf->absolutesOnly && !_global.mouseAbsoluteSeen[mouseEvent.device]) {
continue;
}
switch (mouseEvent.type) {
case MANYMOUSE_EVENT_RELMOTION:
// Integrate the motion into an absolute position clamped to the video. A rotated
// presentation turns the axes, so the cursor follows the hand on the screen shown.
deltaX = (mouseEvent.item == 0) ? (float)mouseEvent.value : 0.0f;
deltaY = (mouseEvent.item == 0) ? 0.0f : (float)mouseEvent.value;
_unrotateDelta(deltaX, deltaY, &deltaX, &deltaY);
xr = (int32_t)deltaX;
yr = (int32_t)deltaY;
mouse->x += xr;
mouse->y += yr;
if (mouse->x < 0) {
mouse->x = 0;
}
if (mouse->x >= videoWidth) {
mouse->x = videoWidth - 1;
}
if (mouse->y < 0) {
mouse->y = 0;
}
if (mouse->y >= videoHeight) {
mouse->y = videoHeight - 1;
}
x = (int32_t)(mouse->x * _global.overlayScaleX);
y = (int32_t)(mouse->y * _global.overlayScaleY);
xr = (int32_t)(xr * _global.overlayScaleX);
yr = (int32_t)(yr * _global.overlayScaleY);
_fireMouseMoved((int32_t)mouseEvent.device, x, y, xr, yr);
break;
case MANYMOUSE_EVENT_ABSMOTION:
// Absolute devices (tablets, some guns) report a position within a range that spans
// the screen, one axis at a time; the two are remembered so a rotated presentation
// can be turned back as a pair, and the game then sits in whatever the shift, the
// scale factor and the Sinden border left of the picture.
if (mouseEvent.maxval > mouseEvent.minval) {
_presentSize(&presentWidth, &presentHeight);
mouseFraction = (double)(mouseEvent.value - mouseEvent.minval) / (double)(mouseEvent.maxval - mouseEvent.minval);
if (mouseEvent.item == 0) {
mouse->absX = (float)(mouseFraction * presentWidth);
} else {
mouse->absY = (float)(mouseFraction * presentHeight);
}
_mapPointer(mouse->absX, mouse->absY, &x, &y);
mouse->x = (int32_t)(x / _global.overlayScaleX);
mouse->y = (int32_t)(y / _global.overlayScaleY);
_fireMouseMoved((int32_t)mouseEvent.device, x, y, 0, 0);
}
break;
case MANYMOUSE_EVENT_BUTTON:
// Limited to the same five buttons as single-mouse mode.
if (mouseEvent.item < CODE_MOUSE_BUTTON_COUNT) {
_processKey(mouseEvent.value == 1, 0, _mouseCode((int32_t)mouseEvent.device, (int32_t)mouseEvent.item));
}
break;
case MANYMOUSE_EVENT_SCROLL:
// Vertical wheel only.
if ((mouseEvent.item == 0) && (mouseEvent.value != 0)) {
code = _mouseCode((int32_t)mouseEvent.device, (mouseEvent.value > 0) ? CODE_MOUSE_WHEEL_UP : CODE_MOUSE_WHEEL_DOWN);
_processKey(true, 0, code);
_processKey(false, 0, code);
}
break;
case MANYMOUSE_EVENT_DISCONNECT:
// A gun unplugged, told apart by device here, which the single mouse path cannot do.
_progTrace("Mouse %u was disconnected", mouseEvent.device);
_deliverSwitch(INPUT_MOUSE_DISCONNECT, true);
_deliverSwitch(INPUT_MOUSE_DISCONNECT, false);
break;
default:
break;
}
}
// --joymouse: the stick has had a frame to move the cursor in.
_joyMouseUpdate();
// Whatever arrived on the MIDI input port. This belongs with the other input, not with the
// drawing: a game that never redraws still has to be given its messages.
midiIoPoll(_midiReceived, NULL);
// The microphone's recording written and its calibration stepped; a calibration that has
// just finished is kept for the machine, beside the audio calibration.
if (micUpdate()) {
_saveMachineSettings(videoGetAudioCalibration());
}
// Timers and tweens, for the same reason.
schedulerUpdate(utilTicks(), _scheduleFired, NULL);
// One write for however many keys the frame changed.
_saveWrite();
// The overlay redraws every frame while it is up, so its figures stay live.
if (statsIsEnabled()) {
_global.refreshDisplay = true;
}
// --idleexit: nothing has been touched for that long, so an attract cabinet lets go.
if ((_global.conf->idleExitSeconds > 0) && ((utilTicks() - _global.idleClock) >= ((uint64_t)_global.conf->idleExitSeconds * MS_PER_SECOND))) {
_progTrace("Idle for %d seconds; quitting", _global.conf->idleExitSeconds);
_global.running = false;
}
// Deliver sound completions on this thread. They wait out an engine pause.
if (!_global.frozen) {
finishedCount = _soundQueueDrain(finished);
for (x = 0; x < finishedCount; x++) {
_callLua("onSoundCompleted", "i", finished[x]);
}
}
// Update the disc video
stageStartNS = SDL_GetTicksNS();
if (_global.videoHandle >= 0) {
thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture);
if (_global.conf->isFrameFile) {
frameFileUpdate(_global.frameFileHandle, &_global.videoHandle);
// A frame file crosses into another player as it plays, and the new one knows
// nothing of vldpSetMonochrome, vldpSetBlend or vldpSetLuma until it is told.
_discApplyPicture();
}
// Did we get a new video frame?
if (thisFrame != lastFrame) {
lastFrame = thisFrame;
frameClock = 0;
_global.refreshDisplay = true;
}
}
// Call game code, unless the engine has it paused.
//
// The queues guiDraw and emitterDraw fill are emptied here, at the start of the script's
// turn, rather than after the frame is rendered. Both are documented as lasting "one
// frame", and a frame means one onOverlayUpdate: that call is throttled to FRAME_TICK_MS
// while the display refreshes as often as it can, and once a 3D scene is enabled it
// refreshes continuously. Emptying them per rendered frame therefore drew the GUIs and
// the 2D particles only on the frames the script happened to run on, which is a menu that
// blinks over a grid that does not.
//
// The throttle is inclusive: on the virtual clock every loop iteration is exactly one
// step, and a strict comparison ran the script every other iteration, a disc frame apart.
_perfSync();
_perf.discNS += SDL_GetTicksNS() - stageStartNS;
stageStartNS = SDL_GetTicksNS();
_perfCalls.inScript = true;
if (!_global.frozen && (utilTicks() >= frameClock)) {
// The next call is due a tick after this one began, so a script's own work is inside the
// tick rather than added to it; after a stall it is due a tick from now, not at once.
frameClock = (utilTicks() >= frameClock + FRAME_TICK_MS) ? utilTicks() + FRAME_TICK_MS : frameClock + FRAME_TICK_MS;
intReturn = OVERLAY_NOT_UPDATED;
_global.guiDrawCount = 0;
_global.overlayLayerCount = 0;
_global.overlay = _global.overlayBase;
particlesClearQueue2D();
_callLua("onOverlayUpdate", ">i", &intReturn);
if (intReturn == OVERLAY_UPDATED) {
_global.refreshDisplay = true;
}
// Clear per-frame values.
_global.keyboardLastDown = SDL_SCANCODE_UNKNOWN;
_global.keyboardLastUp = SDL_SCANCODE_UNKNOWN;
}
_perf.scriptNS += SDL_GetTicksNS() - stageStartNS;
_perfCalls.inScript = false;
// Update display
if (_global.refreshDisplay || _global.overlayDirty || sceneIsEnabled()) {
// A rotated presentation draws the whole frame into a texture first and turns it at the end.
if (_global.rotateTexture != NULL) {
SDL_SetRenderTarget(_global.renderer, _global.rotateTexture);
}
// Clear entire display to black
SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
// Bezel artwork behind everything, unless --bezelflip put it in front
if ((_global.bezelTexture != NULL) && !_global.bezelInFront) {
_bezelDraw();
}
// Sinden Gun Border, outer ring first; the game then covers the middle. One at the
// window's edge is drawn at the end of the frame instead, over the bezel artwork.
if (_global.sindenBorder && !_global.sindenAtWindow) {
_drawSindenBorder();
}
// Laserdisc Video. Games without a disc draw on black.
if (_global.videoHandle >= 0) {
if (_global.discStopped) {
// Stopped discs display blue like the good old days
SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(_global.renderer, &_global.videoRect);
} else {
SDL_RenderTexture(_global.renderer, _global.videoTexture, _global.focused ? &_global.focusArea : NULL, &_global.videoRect);
}
}
// 3D scene
modelUpdate(!_global.frozen);
physicsUpdate(!_global.frozen);
_physicsCallbacks();
navUpdate(!_global.frozen);
_navCallbacks();
particlesUpdate(!_global.frozen);
// The subtitle or the banner is settled before the GUIs render, so a change lands this frame.
_subtitleUpdate();
stageStartNS = SDL_GetTicksNS();
guiUpdate((double)utilTicks() / MS_PER_SECOND_NUMBER);
_guiTextInput();
_perfSync();
_perf.guiNS += SDL_GetTicksNS() - stageStartNS;
stageStartNS = SDL_GetTicksNS();
sceneUpdateVideo(_sceneVideoSource);
sceneTexture = sceneRender();
_perfSync();
_perf.sceneNS += SDL_GetTicksNS() - stageStartNS;
if (_global.sceneMeasuring && (sceneTexture != NULL)) {
// The GPU's share as well: a frame paced by the display says nothing about the machine.
renderFinish(_global.device);
_global.sceneMeasureNS += SDL_GetTicksNS() - stageStartNS;
_global.sceneMeasureFrames++;
}
_updateSounds();
if (sceneTexture != NULL) {
SDL_RenderTexture(_global.renderer, sceneTexture, NULL, &_global.videoRect);
}
// 2D particles beneath the overlay, then the overlay, then the ones above it
_drawParticles2D(PARTICLE_UNDER, &_global.videoRect);
if (_global.overlayDirty) {
stageStartNS = SDL_GetTicksNS();
overlayShown = _global.overlayMonochrome ? _overlayGrey() : _global.overlayBase;
// Into the next of the overlay's textures: rewriting the one the last frame drew from makes a
// tiling GPU wait for that frame or copy the texture first, and ARM's drivers copy. Each
// texture is sent only what changed since it was last brought up to date, a few frames ago.
for (x = 0; x < OVERLAY_TEXTURES; x++) {
_regionAddRegion(&_global.overlayStale[x], &_global.overlayChanged);
}
_regionClear(&_global.overlayChanged);
_global.overlayTextureIndex = (_global.overlayTextureIndex + 1) % OVERLAY_TEXTURES;
_global.overlayTexture = _global.overlayTextures[_global.overlayTextureIndex];
_overlayUpload(_global.overlayTexture, overlayShown, &_global.overlayStale[_global.overlayTextureIndex]);
_regionClear(&_global.overlayStale[_global.overlayTextureIndex]);
_global.overlayDirty = false;
_perfSync();
_perf.overlayNS += SDL_GetTicksNS() - stageStartNS;
}
// Artwork in front of the picture goes over it here when the overlay is to stay on
// top of the artwork, and after the overlay and the GUIs when it is not.
if ((_global.bezelTexture != NULL) && _global.bezelInFront && _global.overlayOnTop) {
_bezelDraw();
}
_overlayVideosDraw(&_global.videoRect);
SDL_RenderTexture(_global.renderer, _global.overlayTexture, NULL, &_global.videoRect);
// The scene's front layer stands on the overlay, beneath the GUIs and the particles over it.
frontTexture = sceneFront();
if (frontTexture != NULL) {
SDL_RenderTexture(_global.renderer, frontTexture, NULL, &_global.videoRect);
}
_subtitleQueue();
stageStartNS = SDL_GetTicksNS();
_drawGuis(&_global.videoRect, false);
_perf.guiNS += SDL_GetTicksNS() - stageStartNS;
_drawParticles2D(PARTICLE_OVER, &_global.videoRect);
if ((_global.bezelTexture != NULL) && _global.bezelInFront && !_global.overlayOnTop) {
_bezelDraw();
}
// Screen space GUIs and the score panel are on the artwork, so they come after it.
_logicalRect(&screenRect);
_scorePanelQueue();
stageStartNS = SDL_GetTicksNS();
_drawGuis(&screenRect, true);
_perf.guiNS += SDL_GetTicksNS() - stageStartNS;
_guisShown();
if (_global.sindenBorder && _global.sindenAtWindow) {
_drawSindenBorder();
}
if (_global.frozen) {
_drawPauseIndicator(&_global.videoRect);
}
// The finished frame, turned into the window. The destination is the unrotated
// rectangle centred in the presentation; a quarter turn about its centre lands it
// exactly on the swapped logical rectangle.
if (_global.rotateTexture != NULL) {
_presentSize(&presentWidth, &presentHeight);
rotateTarget.x = ((float)presentWidth - (float)videoWidth) / 2.0f;
rotateTarget.y = ((float)presentHeight - (float)videoHeight) / 2.0f;
rotateTarget.w = (float)videoWidth;
rotateTarget.h = (float)videoHeight;
SDL_SetRenderTarget(_global.renderer, NULL);
SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
SDL_RenderTextureRotated(_global.renderer, _global.rotateTexture, NULL, &rotateTarget, (double)_global.videoRotate, NULL, SDL_FLIP_NONE);
}
// Save it?
if (_global.requestScreenShot) {
_global.requestScreenShot = false;
_progTrace("Taking screenshot");
_takeScreenshot();
}
// Record it?
_recordUpdate();
// The developer's overlay goes on after the screenshot, so a reference shot is of the
// game rather than of the overlay.
_statsDraw();
// With --perfsync the GPU finishes SDL_Renderer's own drawing -- the picture, the overlay,
// the GUIs -- here, so present is left with only the swap.
stageStartNS = SDL_GetTicksNS();
_perfSync();
_perf.composeNS += SDL_GetTicksNS() - stageStartNS;
// Show it
stageStartNS = SDL_GetTicksNS();
SDL_RenderPresent(_global.renderer);
_perf.presentNS += SDL_GetTicksNS() - stageStartNS;
_perf.presents++;
if (!_perf.firstPresented) {
// Everything a first frame waits on -- shader links, texture uploads -- lands here, and
// a disc or a video that started with the script has been playing all along.
_perf.firstPresented = true;
_progTrace("First frame presented %" PRIu64 " ms after the script started", (SDL_GetTicksNS() - _perf.scriptStartNS) / SDL_NS_PER_MS);
}
_global.refreshDisplay = false;
}
// How long the whole loop took, drawing and presenting included, for the overlay and for -p.
// Sampled here at the end: taken after input, as it once was, it left out everything that
// is actually slow. Keeping it while the overlay is off means turning it on shows the last
// two seconds rather than an empty graph. The overlay drawn this frame shows the last one.
loopNS = SDL_GetTicksNS() - frameStartNS;
_global.statsFrameMs = (double)loopNS / (double)SDL_NS_PER_MS;
statsSample(_global.statsFrameMs);
_perfTrace(loopNS);
// Real time is meaningless on the virtual clock, so a reproducible run does not wait.
if (!utilIsDeterministic()) {
SDL_Delay(IDLE_SLEEP_MS);
}
}
// End game
_progTrace("Script is shutting down");
_callLua("onShutdown", "");
recordStop();
free(_global.recordName);
_global.recordName = NULL;
free(_global.generatedBuffer);
_global.generatedBuffer = NULL;
// Anything the script saved on its way out, including in onShutdown itself.
_saveWrite();
persistClose();
// Stop all sounds
_progTrace("Stopping all audio");
for (x = 0; x < EFFECT_TRACKS; x++) {
MIX_SetTrackStoppedCallback(_effectTracks[x], NULL, NULL);
MIX_DestroyTrack(_effectTracks[x]);
_effectTracks[x] = NULL;
}
// Stop controllers and release the mouse while the script can still hear the releases
_progTrace("Stopping controllers");
_stopControllers();
_progTrace("Releasing mouse");
_setMouseCaptured(false);
// Stop Lua
_progTrace("Stopping Lua");
scriptWatchStop();
guiDetachLua();
lua_close(_global.luaContext);
_global.luaContext = NULL;
// Free overlay & overlay font
_progTrace("Destroying overlay");
SDL_DestroyTexture(_global.rotateTexture);
_global.rotateTexture = NULL;
SDL_DestroyTexture(_global.bezelTexture);
_global.bezelTexture = NULL;
free(_global.bezelHash);
free(_global.scoreTwinMarkup);
SDL_DestroyTexture(_global.pauseTexture);
_subsystemsQuit();
_overlayTexturesDestroy();
_overlayLayersFree();
SDL_DestroySurface(_global.overlayBase);
SDL_DestroySurface(_global.blitScratch);
SDL_DestroySurface(_global.overlayGrey);
_progTrace("Destroying console font");
SDL_DestroySurface(_global.consoleFontSurface);
// Unload resources the script left behind
_unloadScriptResources();
// Unload background video
if (_global.conf->isFrameFile) {
_progTrace("Unloading framefile");
frameFileUnload(_global.frameFileHandle);
} else {
if (_global.videoHandle >= 0) {
_progTrace("Unloading main video file");
videoUnload(_global.videoHandle);
}
}
// Stop mice
_progTrace("Stopping ManyMouse");
ManyMouse_Quit();
// Release global conf memory
destroyConf(&_global.conf);
// Release control mappings
for (x = 0; x < INPUT_COUNT; x++) {
free(_global.controlMappings[x].input);
free(_global.controlMappings[x].chord);
}
for (x = 0; x < _perfCalls.count; x++) {
free(_perfCalls.call[x].name);
}
free(_perfCalls.call);
memset(&_perfCalls, 0, sizeof(_perfCalls));
for (x = 0; x < _global.watchedCount; x++) {
free(_global.watched[x].name);
}
free(_global.watched);
free(_global.discAudioSuffix);
SDL_free(_global.navDrawVertices);
SDL_free(_global.quadVertices);
SDL_free(_global.quadIndices);
SDL_free(_global.frameStarts);
}