/* * * Singe 3 * Copyright (C) 2006-2026 Scott Duensing * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. * */ #include #include #include #include #ifdef __linux__ #include #include #endif // SHA-256 for utilSha256, from the OpenSSL already linked in for LuaSec. #include #include #include #include #include #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//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 ".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(¬hing); 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 = ®ion->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(®ion->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(®ion->rect[closest], area, ®ion->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 = "&"; break; case '<': piece = "<"; break; case '>': piece = ">"; break; case '\n': piece = "
"; 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), "
%s
", 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 ".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); }