/* * * 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 #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); // The module table below needs this before the prototypes. static int32_t _luaopenLfs(lua_State *L); // There is no header for ssl.config binding. Make our own. LSEC_API int luaopen_ssl_config(lua_State *L); #include "main.h" #include "util.h" #include "frameFile.h" #include "vfs.h" #include "scene.h" #include "model.h" #include "physics.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 AUDIO_CALIBRATION_FILE "audio.cfg" // Per-machine audio delay, in the data root #define EMBED_HERE #include "embedded.h" // soundSetVolume/soundGetVolume use this legacy scale; the mixer uses MIX_MAX_VOLUME. #define AUDIO_MAX_VOLUME 63 #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 AXIS_COUNT (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + MAX_MICE * MOUSE_AXIS_COUNT) #define AXIS_INDEX_CONTROLLER(c, a) ((c) * CONTROLLER_AXIS_COUNT + (a)) #define AXIS_INDEX_MOUSE(m, a) (MAX_CONTROLLERS * CONTROLLER_AXIS_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 FRAME_TICK_MS 15 // Minimum time between onOverlayUpdate calls #define IDLE_SLEEP_MS 1 #define OVERLAY_SCALE_DEFAULT 0.5 #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 SOUND_QUEUE_SIZE 64 #define MS_PER_SECOND_NUMBER 1000.0 #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 EFFECT_TAG "effects" #define HELD_KEYS_MAX 64 // Keys physically down at once #define PAUSE_TEXT "PAUSED" #define PAUSE_TEXT_SCALE 3 // Console font is small; scale the indicator up #define COLOR_KEY_VALUE 0 #define BLUE_SCREEN_BLUE 255 #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; 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, INPUT_COUNT } InputE; typedef struct LuaModuleS { const char *name; union { const char *source; lua_CFunction openf; }; size_t length; } LuaModuleT; 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; char name[64]; } MouseT; 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 bool surfaceOwned; // True when surface is a transformed copy to free double angle; double scaleX; double scaleY; int32_t smooth; int32_t currentFrame; bool loop; bool animating; uint64_t lastTick; uint64_t ticks; UT_hash_handle hh; } SpriteT; typedef struct SoundS { int32_t id; MIX_Audio *audio; UT_hash_handle hh; } SoundT; typedef struct FontS { int32_t id; TTF_Font *font; UT_hash_handle hh; } FontT; 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; typedef struct MappingS { int32_t inputCount; int32_t *input; } MappingT; typedef struct GlobalS { MouseT mice[MAX_MICE]; lua_State *luaContext; SDL_Color colorForeground; SDL_Color colorBackground; SDL_Surface *overlay; SDL_Texture *overlayTexture; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *videoTexture; SDL_Surface *consoleFontSurface; SDL_Gamepad *controllers[MAX_CONTROLLERS]; int32_t controllerDeadZone; int32_t consoleFontWidth; int32_t consoleFontHeight; int32_t nextSpriteId; int32_t nextSoundId; int32_t nextFontId; int32_t nextVideoId; int32_t nextScreenshot; // First index worth checking; lower ones are taken int32_t effectsVolume; 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; 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; bool overlayDirty; bool pauseState; // by RDG2010 bool pauseEnabled; // by RDG2010 bool refreshDisplay; bool running; bool discStopped; bool mouseEnabled; bool mouseGrabbed; bool requestScreenShot; bool wasPlayingBeforePause; VideoT *videoList; SpriteT *spriteList; SoundT *soundList; FontT *fontList; FontT *fontCurrent; MappingT controlMappings[INPUT_COUNT]; ConfigT *conf; // Local copy of command line options } GlobalT; static GlobalT _global; static MIX_Track *_effectTracks[EFFECT_TRACKS]; #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), // SQLite for script data MODC("sqlite3", luaopen_lsqlite3), // 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), // Copas MODL("copas", copas_lua), MODL("copas.ftp", copas_ftp_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), }; // 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" } }; // 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 int32_t _apiUnimplemented(lua_State *L, const char *method); static bool _argBoolean(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 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 _argInteger(lua_State *L, const char *method, int32_t index); static int32_t _argBody(lua_State *L, const char *method, int32_t index); 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 _argNode(lua_State *L, const char *method, int32_t index); static int64_t _argInteger64(lua_State *L, const char *method, int32_t index); static double _argNumber(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 VideoT *_argVideo(lua_State *L, const char *method, int32_t index); static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base); static void _callLua(const char *func, const char *sig, ...); static int32_t _effectTrackFree(void); static void _effectStopped(void *userdata, MIX_Track *track); static int32_t _controllerSlot(SDL_JoystickID which); static bool _delayAndPump(uint32_t ms); static void _deliverKey(bool down, int32_t keysym, int32_t scancode); static void _discSeek(int64_t frame); static void _doLogos(void); static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel); static void _drawPauseIndicator(const SDL_FRect *target); 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 void _heldListUpdate(HeldKeyT *list, int32_t *count, bool down, int32_t keysym, int32_t scancode); static void _installFileHooks(lua_State *L); static int32_t _lfsAttributes(lua_State *L); static int32_t _lfsDir(lua_State *L); static int32_t _lfsDirIterator(lua_State *L); static int32_t _lfsMkdir(lua_State *L); static int32_t _lfsRmdir(lua_State *L); static int32_t _loadAudioCalibration(void); static void _loadControlsFile(const char *path); static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int length); static SDL_Texture *_loadEmbeddedTexture(const unsigned char *data, unsigned int length, SDL_Surface **surface); 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 _luaFileSearcher(lua_State *L); static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args); static int32_t _luaIoHook(lua_State *L); static int32_t _luaLoadfile(lua_State *L); static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode); static int32_t _luaopenLfs(lua_State *L); static int32_t _luaPanic(lua_State *L); static int32_t _luaSearcher(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 float _mixerGain(int32_t effectsVolume); static int32_t _mouseCode(int32_t device, int32_t button); static uint32_t _overlayColor(const SDL_Color *color); static void _overlayTouched(void); static void _pauseAllVideos(bool pause); static void _processKey(bool down, int32_t keysym, int32_t scancode); static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static int32_t _pushVec3(lua_State *L, Vec3T v); static void _physicsCallbacks(void); static SDL_Texture *_sceneVideoSource(int32_t player); static void _pushConstants(lua_State *L); static void _putPixel(int32_t x, int32_t y, uint32_t pixel); static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha); static void _releaseAxis(int32_t axisIndex); static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message); static void _saveAudioCalibration(int32_t milliseconds); static void _selectDefaultAudioTrack(int32_t handle); static void _setMouseCaptured(bool captured); static void _setPause(bool paused, bool fromKey); static void _soundDestroy(SoundT *sound); static void _spriteDestroy(SpriteT *sprite); static void _spriteFreeSurface(SpriteT *sprite); static void _spriteRebuildSurface(SpriteT *sprite); static void _startControllers(void); static void _startLuaContext(lua_State *L); static void _stopControllers(void); static void _suppressHeldInput(void); static SDL_Surface *_surfaceCopy(SDL_Surface *source); static uint32_t _surfaceCornerKey(SDL_Surface *surface); static void _surfaceUnpack(SDL_Surface **surface); static void _takeScreenshot(void); static void _updatePauseState(void); static void _videoDestroy(VideoT *video); 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 apiAnimationResume(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 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 apiCameraSet(lua_State *L); static int32_t apiCameraSetOrthographic(lua_State *L); static int32_t apiCameraSetPerspective(lua_State *L); static int32_t apiColorBackground(lua_State *L); static int32_t apiColorForeground(lua_State *L); static int32_t apiControllerGetAxis(lua_State *L); static int32_t apiControllerGetButton(lua_State *L); static int32_t apiDebugPrint(lua_State *L); static int32_t apiDiscAudio(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 apiDiscGetFrame(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 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 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 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 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 apiMaterialSetDoubleSided(lua_State *L); static int32_t apiMaterialSetEmissive(lua_State *L); static int32_t apiMaterialSetMetallic(lua_State *L); static int32_t apiMaterialSetRoughness(lua_State *L); static int32_t apiMaterialSetTexture(lua_State *L); static int32_t apiMaterialSetUnlit(lua_State *L); static int32_t apiMaterialSetVideo(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 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 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 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 apiNodeDelete(lua_State *L); static int32_t apiNodeFind(lua_State *L); static int32_t apiNodeGetChildren(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 apiNodeSetMesh(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 apiNodeSetVisible(lua_State *L); static int32_t apiOsClock(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 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 apiOverlaySetResolution(lua_State *L); static int32_t apiPhysicsRaycast(lua_State *L); static int32_t apiPhysicsSetEnabled(lua_State *L); static int32_t apiPhysicsSetGravity(lua_State *L); static int32_t apiSceneEnable(lua_State *L); static int32_t apiSceneGetSize(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 apiSceneSetShadowSize(lua_State *L); static int32_t apiSceneUnproject(lua_State *L); static int32_t apiScriptExecute(lua_State *L); static int32_t apiScriptPush(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 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 apiSingeQuit(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 apiSingeVersion(lua_State *L); static int32_t apiSingeWantsCrosshairs(lua_State *L); static int32_t apiSoundFullStop(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 apiSoundPause(lua_State *L); static int32_t apiSoundPlay(lua_State *L); static int32_t apiSoundResume(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 apiSpriteDraw(lua_State *L); static int32_t apiSpriteGetFrame(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 apiSpriteLoop(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 apiSpriteRotate(lua_State *L); static int32_t apiSpriteRotateAndScale(lua_State *L); static int32_t apiSpriteScale(lua_State *L); static int32_t apiSpriteSetFrame(lua_State *L); static int32_t apiSpriteUnload(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 apiVldpGetPixel(lua_State *L); static int32_t apiVldpSetVerbose(lua_State *L); // ===== Internal helpers ===== // 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; } 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; } // 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); } } 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; } // 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 int32_t _argInteger(lua_State *L, const char *method, int32_t index) { return (int32_t)_argNumber(L, method, index); } // A node that carries a physics body, checked. static int32_t _argBody(lua_State *L, const char *method, int32_t index) { int32_t node = _argNode(L, method, index); if (!bodyExists(node)) { _luaDie(L, method, "Node %d has no body.", node); } return node; } // A material handle argument, checked. static int32_t _argMaterial(lua_State *L, const char *method, int32_t index) { int32_t material = _argInteger(L, method, index); if (!materialValid(material)) { _luaDie(L, method, "No material %d.", material); } return material; } // A mesh handle argument, checked. static int32_t _argMesh(lua_State *L, const char *method, int32_t index) { int32_t mesh = _argInteger(L, method, index); if (!meshValid(mesh)) { _luaDie(L, method, "No mesh %d.", mesh); } return mesh; } // A scene node handle argument, checked. static int32_t _argNode(lua_State *L, const char *method, int32_t index) { int32_t node = _argInteger(L, method, index); if (!nodeValid(node)) { _luaDie(L, method, "No node %d.", node); } return node; } static int64_t _argInteger64(lua_State *L, const char *method, int32_t index) { return (int64_t)_argNumber(L, method, index); } 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); } 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)); } 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; } // 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 *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 and container. c = cloneConf(base); free(c->container); c->container = NULL; c->disc = false; c->isFrameFile = false; free(c->videoFile); c->videoFile = NULL; // 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) { utilDie("SCRIPT must be a string."); } free(c->scriptFile); c->scriptFile = strdup(valueString); utilFixPathSeparators(&c->scriptFile, false); } else if (strcmp(confKey, "CONTAINER") == 0) { if (valueString == NULL) { utilDie("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) { utilDie("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; } else if (strcmp(confKey, "RESOLUTION_Y") == 0) { c->yResolution = (int32_t)valueNumber; } 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, "LEGACY_SPRITE_ARGS") == 0) { c->legacySpriteArgs = valueBoolean; } 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)) { utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile); } // Create new data dir location based on script location. free(c->dataDir); c->dataDir = createDataDirFor(c); if (!c->dataDir) { utilDie("Unable to create data directory for %s.", 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; double d = 0; const int32_t 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 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 '>': done = true; break; default: utilDie("Invalid argument option (%c)", *(sig - 1)); } if (!done) { narg++; luaL_checkstack(_global.luaContext, 1, "Too many arguments"); } } // Do the call. Script errors are fatal, like every other error in Singe. nres = (int32_t)strlen(sig); if (lua_pcall(_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); } // First effect track that is neither playing nor paused, or -1 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 -1; } // 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; } } // Maps an SDL joystick instance ID to our controller slot, or -1. 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; } // Sleeps while keeping the window responsive. Returns false if the user asked to quit. 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; } } SDL_Delay(IDLE_SLEEP_MS); } while (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; if (_global.keyboardMode == KEYBOARD_FULL) { _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; _callLua(down ? "onInputPressed" : "onInputReleased", "i", move); } } } } // Seeks the laserdisc, whichever kind it is. static void _discSeek(int64_t frame) { int64_t actualFrame = 0; if (_global.conf->isFrameFile) { frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &actualFrame); } else { if (_global.videoHandle >= 0) { videoSeek(_global.videoHandle, frame); } } } // 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. The overlay must be locked by the caller. static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel) { int32_t x = x1; int32_t y = y1; int32_t dx = abs(x2 - x1); int32_t dy = abs(y2 - y1); int32_t incX = (x2 >= x1) ? 1 : -1; int32_t incY = (y2 >= y1) ? 1 : -1; int32_t balance = 0; 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 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); } // Which engine owned switch (pause, quit, screenshot, grab) 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 }; 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; } 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. 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]; } } } // 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) { static const struct { const char *name; int32_t mode; // 0 read, 1 io.open (mode string decides), 2 write } ioHooks[] = { { "input", 0 }, { "lines", 0 }, { "open", 1 }, { "output", 2 } }; 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"); 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); } lua_pop(L, 1); } // lfs.attributes through the vfs: a packed entry answers with mode, size and modification; a // plain filesystem path goes to the original (upvalue 1). static int32_t _lfsAttributes(lua_State *L) { const char *name = luaL_checkstring(L, 1); const char *attribute = luaL_optstring(L, 2, NULL); int64_t size = 0; int64_t modified = 0; bool directory = false; if (vfsIsFilesystem(name)) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(L); } directory = vfsIsDirectory(name); if (!directory && !vfsStat(name, &size, &modified)) { lua_pushnil(L); lua_pushfstring(L, "cannot obtain information from file '%s'", name); return 2; } lua_newtable(L); lua_pushstring(L, directory ? "directory" : "file"); lua_setfield(L, -2, "mode"); lua_pushinteger(L, (lua_Integer)size); lua_setfield(L, -2, "size"); 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)modified); lua_setfield(L, -2, "access"); if (attribute != NULL) { lua_getfield(L, -1, attribute); } 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)) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(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; } // 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)) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(L); } path = vfsFilePath(name, true); ok = utilMkDirP(path, 0755); 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)) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(L); } path = vfsFilePath(name, true); 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 per-machine audio delay lives beside the data directories, since it is not a property of any game. static int32_t _loadAudioCalibration(void) { char *path = utilCreateString("%s%s", _global.conf->dataDirBase, AUDIO_CALIBRATION_FILE); size_t bytes = 0; char *data = utilReadFile(path, &bytes); int32_t value = 0; if (data) { if (sscanf(data, "delay = %d", &value) != 1) { value = 0; } free(data); } free(path); if ((value < -VIDEO_AUDIO_DELAY_MAX) || (value > VIDEO_AUDIO_DELAY_MAX)) { value = 0; } return value; } // 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) || lua_pcall(_global.luaContext, 0, 0, 0)) { utilDie("%s", lua_tostring(_global.luaContext, -1)); } } } static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int 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 unsigned char *data, unsigned int 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; } // 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); } // Formats "line:method: message" for tracing and errors. Caller frees. // 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) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(L); } lua_settop(L, 1); if (_luaLoadFile(L, name, NULL) != LUA_OK) { return lua_error(L); } lua_call(L, 0, LUA_MULTRET); return lua_gettop(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) != 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; } 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; } // Lua panic handler: something went wrong outside a protected call. // 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) { int32_t mode = (int32_t)lua_tointeger(L, lua_upvalueindex(2)); const char *modeString = NULL; char *path = NULL; bool writing = (mode == 2); int32_t top = lua_gettop(L); if (lua_type(L, 1) == LUA_TSTRING) { if (mode == 1) { modeString = luaL_optstring(L, 2, "r"); writing = (strpbrk(modeString, "wa+") != NULL); } path = vfsFilePath(lua_tostring(L, 1), writing); lua_pushstring(L, path); lua_replace(L, 1); free(path); } lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, top, LUA_MULTRET); return lua_gettop(L); } // 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) { lua_pushvalue(L, lua_upvalueindex(1)); lua_insert(L, 1); lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); return lua_gettop(L); } if (_luaLoadFile(L, name, mode) != 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. static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode) { 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; } 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; } 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; } 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; } // Converts the script visible 0..AUDIO_MAX_VOLUME scale to the mixer's scale. static float _mixerGain(int32_t effectsVolume) { return (float)effectsVolume / (float)AUDIO_MAX_VOLUME; } // 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); } // 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; } // Every overlay drawing call ends here so the texture is only re-uploaded when needed. static void _overlayTouched(void) { _global.overlayDirty = true; } 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); } } } } // Routes a key, button, or axis direction code: engine switches first, then the script. static void _processKey(bool down, int32_t keysym, int32_t scancode) { InputE engine = INPUT_COUNT; bool keyboard = (scancode < CODE_GAMEPAD_BASE); // Real scancodes never reach the gamepad range // Physical state is tracked even while the game is frozen. if (down) { _global.keyboardLastDown = scancode; } else { _global.keyboardLastUp = scancode; } if ((scancode >= 0) && (scancode < SDL_SCANCODE_COUNT)) { _global.keyboardState[scancode] = down; } _heldListUpdate(_global.physicalKeys, &_global.physicalKeyCount, down, keysym, scancode); // 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) { switch (engine) { case INPUT_PAUSE: if (_global.pauseEnabled) { _setPause(!_global.pauseState, true); } break; case INPUT_GRAB: _setMouseCaptured(!_global.mouseGrabbed); break; case INPUT_QUIT: _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; default: break; } } // 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); } static void _progTrace(const char *fmt, ...) { va_list args; if (_global.conf->programTracing) { va_start(args, fmt); utilTraceVArgs(fmt, args); va_end(args); } } // 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; } // 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. static void _physicsCallbacks(void) { PhysicsEventT events[64]; int32_t count; int32_t x; count = physicsGetEvents(events, (int32_t)SDL_arraysize(events)); for (x = 0; x < count; x++) { PhysicsEventT *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); } } } // 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 == _global.videoHandle) && (player >= 0)) { return _global.videoTexture; } for (video = _global.videoList; video != NULL; video = video->hh.next) { if (video->handle == player) { videoUpdate(video->handle, &video->texture); return video->texture; } } return NULL; } // 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); } 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, 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, -1); lua_setglobal(L, "SOUND_ERROR_INVALID"); lua_pushinteger(L, -1); 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, 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_pushboolean(L, _global.conf->legacySpriteArgs); lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS"); lua_pushboolean(L, _global.conf->disc); lua_setglobal(L, "SINGE_DISC"); } // Writes one overlay pixel. The overlay is always 32 bit and must be locked by the caller. static void _putPixel(int32_t x, int32_t y, uint32_t pixel) { SDL_Surface *surface = _global.overlay; uint8_t *p = NULL; if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) { return; } p = (uint8_t *)surface->pixels + y * surface->pitch + x * SDL_BYTESPERPIXEL(surface->format); memcpy(p, &pixel, sizeof(pixel)); } // 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); int32_t value[4] = { 0, 0, 0, defaultAlpha }; int32_t x = 0; _argCheck(L, method, 3, 4); for (x = 0; x < n; x++) { value[x] = _argInteger(L, method, x + 1); if (value[x] < 0) { value[x] = 0; } if (value[x] > SDL_ALPHA_OPAQUE) { value[x] = SDL_ALPHA_OPAQUE; } } color->r = (uint8_t)value[0]; color->g = (uint8_t)value[1]; color->b = (uint8_t)value[2]; color->a = (uint8_t)value[3]; _luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a); } // 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; } } // 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()); } SDL_SetSurfaceColorKey(surface, true, COLOR_KEY_VALUE); return surface; } static void _saveAudioCalibration(int32_t milliseconds) { char *path = utilCreateString("%s%s", _global.conf->dataDirBase, AUDIO_CALIBRATION_FILE); FILE *out = fopen(path, "w"); if (out) { fprintf(out, "delay = %d\n", milliseconds); fclose(out); } else { utilSay("Unable to write %s", path); } free(path); } // 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 < videoGetAudioTracks(handle)) { videoSetAudioTrack(handle, _global.conf->audioOutputTrack); } } 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); } } static void _soundDestroy(SoundT *sound) { HASH_DEL(_global.soundList, sound); MIX_DestroyAudio(sound->audio); free(sound); } static void _spriteDestroy(SpriteT *sprite) { HASH_DEL(_global.spriteList, sprite); _spriteFreeSurface(sprite); if (sprite->animation != NULL) { // Frames belong to the animation. IMG_FreeAnimation(sprite->animation); } else { SDL_DestroySurface(sprite->originalSurface); } free(sprite); } // 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; } // 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)) { // Untransformed sprites draw straight from the original. sprite->surface = sprite->originalSurface; } else { sprite->surface = rotoZoomSurface(sprite->originalSurface, -sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth != 0); if (sprite->surface == NULL) { utilDie("Unable to transform sprite %d.", sprite->id); } sprite->surfaceOwned = true; } } static void _startControllers(void) { int32_t x = 0; int32_t count = 0; SDL_JoystickID *ids = SDL_GetGamepads(&count); _stopControllers(); // Clamp to the first few controllers found. if (count > MAX_CONTROLLERS) { count = MAX_CONTROLLERS; } for (x = 0; (ids != NULL) && (x < count); x++) { _global.controllers[x] = SDL_OpenGamepad(ids[x]); if (_global.controllers[x]) { _progTrace("Found %d - %s", x, SDL_GetGamepadName(_global.controllers[x])); } else { _progTrace("Controller %d not opened", x); } } 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"); 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, -2, (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); } 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 an axis is gone with the controller. for (x = 0; x < AXIS_COUNT; x++) { _releaseAxis(x); _global.axisCache[x] = 0; } } // 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 = SDL_GetTicks() + 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); } // SDL3_image keeps 1, 2, and 4 bit indexed PNGs packed, and blits from those are unreliable. // Expand them to one byte per pixel, which is what SDL2 always produced. Replaces *surface. static void _surfaceUnpack(SDL_Surface **surface) { SDL_Surface *unpacked = NULL; if ((*surface != NULL) && SDL_ISPIXELFORMAT_INDEXED((*surface)->format) && (SDL_BITSPERPIXEL((*surface)->format) < 8)) { unpacked = SDL_ConvertSurface(*surface, SDL_PIXELFORMAT_INDEX8); if (unpacked == NULL) { utilDie("%s", SDL_GetError()); } SDL_DestroySurface(*surface); *surface = unpacked; } } static SDL_Surface *_surfaceCopy(SDL_Surface *source) { SDL_Surface *destination = SDL_CreateSurface(source->w, source->h, source->format); if (destination == NULL) { utilDie("%s", SDL_GetError()); } SDL_BlitSurface(source, NULL, destination, NULL); return destination; } // 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; int32_t logicalW = 0; int32_t logicalH = 0; char *filename = NULL; SDL_Surface *surface = NULL; SDL_Surface *rgb = NULL; SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED; // 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; // Read the whole window, letterbox included: with a logical size set, a NULL rect would // only cover the scaled viewport, so switch it off around the read. 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()); } 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); } 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); } else { // Resume laserdisc if ((_global.videoHandle >= 0) && !_global.discStopped && _global.wasPlayingBeforePause) { _global.wasPlayingBeforePause = false; videoPlay(_global.videoHandle); } _pauseAllVideos(false); MIX_ResumeTag(videoGetMixer(), EFFECT_TAG); } } static void _videoDestroy(VideoT *video) { HASH_DEL(_global.videoList, video); videoUnload(video->handle); SDL_DestroySurface(video->transformedSurface); free(video); } // ===== Lua API ===== // seconds = animationGetTime(node) 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]]): 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; _argCheck(L, "animationPlay", 2, 4); root = _argNode(L, "animationPlay", 1); model = modelRootOf(root); if (model < 0) { _luaDie(L, "animationPlay", "Node %d is not a model instance.", root); } if (lua_type(L, 2) == LUA_TSTRING) { index = modelAnimationIndex(model, lua_tostring(L, 2)); if (index < 0) { _luaDie(L, "animationPlay", "Model %d has no animation named %s.", model, lua_tostring(L, 2)); } } else { // Scripts number animations from 1, as modelGetAnimations lists them. index = _argInteger(L, "animationPlay", 2) - 1; } if (lua_gettop(L) >= 3) { loop = _argBoolean(L, "animationPlay", 3); } if (lua_gettop(L) >= 4) { speed = (float)_argNumber(L, "animationPlay", 4); } if (!animationPlay(root, index, loop, speed)) { _luaDie(L, "animationPlay", "Model %d has no animation %d.", model, index + 1); } _luaTrace(L, "animationPlay", "node %d animation %d%s x%.2f", root, index + 1, loop ? " looping" : "", speed); 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; } // 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) static int32_t apiAnimationStop(lua_State *L) { int32_t root; _argCheck(L, "animationStop", 1, 1); root = _argNode(L, "animationStop", 1); if (!animationStop(root)) { _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; int32_t type; int32_t shape; float dims[3] = { 0.0f, 0.0f, 0.0f }; int32_t x; _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 < 3; 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; } // 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; } // 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. 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; } // 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; } // 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. 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; } 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; } // frame = discGetFrame() static int32_t apiDiscGetFrame(lua_State *L) { int64_t frame = 0; if (!_global.discStopped && (_global.videoHandle >= 0)) { if (_global.conf->isFrameFile) { frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle); } else { frame = videoGetFrame(_global.videoHandle); } } _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 = 0; 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 ((_global.videoHandle >= 0) && 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 = 0; 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 = videoGetFrame(_global.videoHandle) - _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 = videoGetFrame(_global.videoHandle) + _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. static int32_t apiDiscStepBackward(lua_State *L) { int64_t frame = 0; if (_global.videoHandle >= 0) { if (_global.conf->isFrameFile) { frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) - 1; } else { frame = videoGetFrame(_global.videoHandle) - 1; } if (frame < 0) { frame = 0; } _discSeek(frame); videoPause(_global.videoHandle); } _luaTrace(L, "discStepBackward", "%" PRId64, frame); return 0; } // discStepForward() Go forward a frame and pause. static int32_t apiDiscStepForward(lua_State *L) { int64_t frame = 0; if (_global.videoHandle >= 0) { if (_global.conf->isFrameFile) { frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) + 1; } else { frame = videoGetFrame(_global.videoHandle) + 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; } // 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; _argCheck(L, "fontLoad", 2, 2); name = _argString(L, "fontLoad", 1); points = _argInteger(L, "fontLoad", 2); font = (FontT *)calloc(1, sizeof(FontT)); if (!font) { _luaDie(L, "fontLoad", "Unable to allocate new font."); } font->font = TTF_OpenFontIO(vfsOpenIO(name), 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 = _renderText(L, "fontPrint", message); dest.w = text->w; dest.h = text->h; SDL_BlitSurface(text, NULL, _global.overlay, &dest); SDL_DestroySurface(text); _overlayTouched(); _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) static int32_t apiFontSelect(lua_State *L) { _argCheck(L, "fontSelect", 1, 1); _global.fontCurrent = _argFont(L, "fontSelect", 1); _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 = (SpriteT *)calloc(1, sizeof(SpriteT)); if (!sprite) { _luaDie(L, "fontToSprite", "Unable to allocate new text sprite."); } sprite->originalSurface = _renderText(L, "fontToSprite", message); sprite->surface = sprite->originalSurface; sprite->scaleX = 1.0; sprite->scaleY = 1.0; sprite->id = _global.nextSpriteId++; HASH_ADD_INT(_global.spriteList, id, 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 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; } // 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; } // mode = keyboardGetMode() 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; _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", "3D is not available on this machine."); } _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, (uint8_t)_argInteger(L, "lightSetColor", 2), (uint8_t)_argInteger(L, "lightSetColor", 3), (uint8_t)_argInteger(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 (!lightSetShadow(node, _argBoolean(L, "lightSetShadow", 2))) { _luaDie(L, "lightSetShadow", "Node %d is not a light.", node); } return 0; } // materialDelete(material) static int32_t apiMaterialDelete(lua_State *L) { _argCheck(L, "materialDelete", 1, 1); materialDelete(_argMaterial(L, "materialDelete", 1)); return 0; } // material = materialNew(): white, half rough static int32_t apiMaterialNew(lua_State *L) { int32_t material; _argCheck(L, "materialNew", 0, 0); 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; int32_t a = SDL_ALPHA_OPAQUE; _argCheck(L, "materialSetColor", 4, 5); material = _argMaterial(L, "materialSetColor", 1); if (lua_gettop(L) >= 5) { a = _argInteger(L, "materialSetColor", 5); } materialSetColor(material, (uint8_t)_argInteger(L, "materialSetColor", 2), (uint8_t)_argInteger(L, "materialSetColor", 3), (uint8_t)_argInteger(L, "materialSetColor", 4), (uint8_t)a); 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), (uint8_t)_argInteger(L, "materialSetEmissive", 2), (uint8_t)_argInteger(L, "materialSetEmissive", 3), (uint8_t)_argInteger(L, "materialSetEmissive", 4)); return 0; } // materialSetMetallic(material, 0..1) static int32_t apiMaterialSetMetallic(lua_State *L) { _argCheck(L, "materialSetMetallic", 2, 2); materialSetMetallic(_argMaterial(L, "materialSetMetallic", 1), (float)_argNumber(L, "materialSetMetallic", 2)); return 0; } // 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 [, sprite]): a sprite's image as the base colour; none clears it static int32_t apiMaterialSetTexture(lua_State *L) { int32_t material; SpriteT *sprite = NULL; _argCheck(L, "materialSetTexture", 1, 2); material = _argMaterial(L, "materialSetTexture", 1); if (lua_gettop(L) >= 2) { sprite = _argSprite(L, "materialSetTexture", 2); } if (!materialSetTexture(material, sprite ? sprite->originalSurface : NULL)) { _luaDie(L, "materialSetTexture", "%s", SDL_GetError()); } 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 = _global.videoHandle; } materialSetVideo(material, player); return 0; } // mesh = meshBox(width, height, depth) 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; int32_t segments = 24; _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; int32_t segments = 24; _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 = 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, 2); 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; int32_t segments = 32; _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; int32_t segments = 32; _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; } // modelDelete(model): frees its meshes and materials; instances keep their nodes, bare 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); } 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; } // x, y = mouseGetPosition(mouse) 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); } _global.mouseMode = (MouseModeE)mode; _luaTrace(L, "mouseSetMode", "%d", _global.mouseMode); return 0; } // 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) { int32_t root = SCENE_ROOT_NODE; int32_t found; _argCheck(L, "nodeFind", 1, 2); if (lua_gettop(L) >= 2) { root = _argNode(L, "nodeFind", 2); } found = nodeFind(root, _argString(L, "nodeFind", 1)); 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; } // 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", "3D is not available on this machine."); } _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; } // 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; } // 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; } // 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; } // 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)SDL_GetTicks() / MS_PER_SECOND_NUMBER); return 1; } // 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; _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); 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(); _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; _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); 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(); _luaTrace(L, "overlayCircle", "%d %d %d", x0, y0, r); return 0; } // overlayClear() Fills with the background color. static int32_t apiOverlayClear(lua_State *L) { SDL_FillSurfaceRect(_global.overlay, NULL, _overlayColor(&_global.colorBackground)); _overlayTouched(); _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; _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); _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(); return 0; } // 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; _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); SDL_LockSurface(_global.overlay); _drawLine(x1, y1, x2, y2, pixel); SDL_UnlockSurface(_global.overlay); _overlayTouched(); _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; _argCheck(L, "overlayPlot", 2, 2); x = _argInteger(L, "overlayPlot", 1); y = _argInteger(L, "overlayPlot", 2); pixel = _overlayColor(&_global.colorForeground); SDL_LockSurface(_global.overlay); _putPixel(x, y, pixel); SDL_UnlockSurface(_global.overlay); _overlayTouched(); _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; _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; } 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(); 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); } SDL_DestroySurface(_global.overlay); _global.overlay = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_BGRA32); if (_global.overlay == NULL) { utilDie("%s", SDL_GetError()); } SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND); SDL_DestroyTexture(_global.overlayTexture); _global.overlayTexture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, width, height); if (_global.overlayTexture == NULL) { utilDie("%s", SDL_GetError()); } sceneResize(width, height); SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); _global.overlayScaleX = (double)width / (double)_global.canvasWidth; _global.overlayScaleY = (double)height / (double)_global.canvasHeight; _overlayTouched(); _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; } // 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; } // scriptExecute(config) Runs another script after this one ends. // Turns the 3D layer on or off. 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."); } _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; _argCheck(L, "sceneGetSize", 0, 0); sceneGetSize(&width, &height); lua_pushinteger(L, width); lua_pushinteger(L, height); return 2; } // x, y, depth, inFront = sceneProject(wx, wy, wz): a world point in overlay coordinates 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; } // 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; } // 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))); } static int32_t apiScriptExecute(lua_State *L) { ConfigT *conf = NULL; _argCheck(L, "scriptExecute", 1, 1); if (!lua_istable(L, 1)) { _luaDie(L, "scriptExecute", "Argument 1 must be a table."); } conf = _buildConfFromTable(L, _global.conf); 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 = NULL; _argCheck(L, "scriptPush", 1, 1); if (!lua_istable(L, 1)) { _luaDie(L, "scriptPush", "Argument 1 must be a table."); } conf = _buildConfFromTable(L, _global.conf); queueScript(conf); destroyConf(&conf); queueScript(_global.conf); _global.running = false; _luaTrace(L, "scriptPush", "Queued."); return 0; } // 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; } // 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. 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 = SDL_GetTicks(); _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() static int32_t apiSingeQuit(lua_State *L) { _luaTrace(L, "singeQuit", "Quit requested."); _global.running = false; return 0; } // singeScreenshot() Saved after the next frame is drawn. static int32_t apiSingeScreenshot(lua_State *L) { _luaTrace(L, "singeScreenshot", "Screenshot requested."); _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); _saveAudioCalibration(value); _luaTrace(L, "singeSetAudioCalibration", "%d", value); 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; } // 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; } // 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; } // version = singeVersion() 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; } // 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; } // 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 = _argInteger(L, "soundIsPlaying", 1); if ((channel < 0) || (channel >= EFFECT_TRACKS)) { _luaDie(L, "soundIsPlaying", "Invalid channel: %d", channel); } 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; _argCheck(L, "soundLoad", 1, 1); name = _argString(L, "soundLoad", 1); sound = (SoundT *)calloc(1, sizeof(SoundT)); if (!sound) { _luaDie(L, "soundLoad", "Unable to allocate new sound."); } sound->audio = MIX_LoadAudio_IO(videoGetMixer(), vfsOpenIO(name), true, true); if (!sound->audio) { _luaDie(L, "soundLoad", "%s", SDL_GetError()); } sound->id = _global.nextSoundId++; HASH_ADD_INT(_global.soundList, id, sound); _luaTrace(L, "soundLoad", "%d %s", sound->id, name); 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 = _argInteger(L, "soundPause", 1); if ((channel < 0) || (channel >= EFFECT_TRACKS)) { _luaDie(L, "soundPause", "Invalid channel: %d", channel); } 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. static int32_t apiSoundPlay(lua_State *L) { SoundT *sound = NULL; int32_t channel = -1; _argCheck(L, "soundPlay", 1, 1); sound = _argSound(L, "soundPlay", 1); channel = _effectTrackFree(); if (channel >= 0) { MIX_SetTrackAudio(_effectTracks[channel], sound->audio); MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume)); if (!MIX_PlayTrack(_effectTracks[channel], 0)) { _luaDie(L, "soundPlay", "%s", SDL_GetError()); } } _luaTrace(L, "soundPlay", "%d %d", sound->id, channel); 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 = _argInteger(L, "soundResume", 1); if ((channel < 0) || (channel >= EFFECT_TRACKS)) { _luaDie(L, "soundResume", "Invalid channel: %d", channel); } paused = MIX_TrackPaused(_effectTracks[channel]); MIX_ResumeTrack(_effectTracks[channel]); _luaTrace(L, "soundResume", "%d %d", channel, paused); lua_pushboolean(L, paused); return 1; } // soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel. static int32_t apiSoundSetVolume(lua_State *L) { int32_t volume = 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; MIX_SetTagGain(videoGetMixer(), EFFECT_TAG, _mixerGain(_global.effectsVolume)); _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 = _argInteger(L, "soundStop", 1); if ((channel < 0) || (channel >= EFFECT_TRACKS)) { _luaDie(L, "soundStop", "Invalid channel: %d", channel); } 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; } // 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 = SDL_GetTicks(); sprite->ticks += now - sprite->lastTick; sprite->lastTick = now; 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; } if (stretched) { SDL_BlitSurfaceScaled(sprite->surface, NULL, _global.overlay, &dest, SDL_SCALEMODE_NEAREST); } else { SDL_BlitSurface(sprite->surface, NULL, _global.overlay, &dest); } _overlayTouched(); _luaTrace(L, "spriteDraw", "%d %d %d %d %d %d", sprite->id, dest.x, dest.y, dest.w, dest.h, center); return 0; } // 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; } // 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; } // id = spriteLoad(filename) Animated GIF and WEBP files load as animations. static int32_t apiSpriteLoad(lua_State *L) { const char *name = NULL; SpriteT *sprite = NULL; SDL_IOStream *io = NULL; int32_t x = 0; _argCheck(L, "spriteLoad", 1, 1); name = _argString(L, "spriteLoad", 1); sprite = (SpriteT *)calloc(1, sizeof(SpriteT)); if (!sprite) { _luaDie(L, "spriteLoad", "Unable to allocate new sprite."); } // Try to load requested file as an animation first io = vfsOpenIO(name); if (io == NULL) { _luaDie(L, "spriteLoad", "%s", SDL_GetError()); } sprite->animation = IMG_LoadAnimation_IO(io, false); if ((sprite->animation != NULL) && (sprite->animation->count < 2)) { // Only one frame - keep it as a still image. sprite->originalSurface = _surfaceCopy(sprite->animation->frames[0]); _surfaceUnpack(&sprite->originalSurface); IMG_FreeAnimation(sprite->animation); sprite->animation = NULL; } else { if (sprite->animation != NULL) { for (x = 0; x < sprite->animation->count; x++) { _surfaceUnpack(&sprite->animation->frames[x]); SDL_SetSurfaceColorKey(sprite->animation->frames[x], true, COLOR_KEY_VALUE); } sprite->originalSurface = sprite->animation->frames[0]; } else { SDL_SeekIO(io, 0, SDL_IO_SEEK_SET); sprite->originalSurface = IMG_Load_IO(io, false); _surfaceUnpack(&sprite->originalSurface); } } SDL_CloseIO(io); if (!sprite->originalSurface) { _luaDie(L, "spriteLoad", "%s", SDL_GetError()); } SDL_SetSurfaceColorKey(sprite->originalSurface, true, COLOR_KEY_VALUE); sprite->surface = sprite->originalSurface; sprite->scaleX = 1.0; sprite->scaleY = 1.0; sprite->id = _global.nextSpriteId++; HASH_ADD_INT(_global.spriteList, id, sprite); _luaTrace(L, "spriteLoad", "%d %s", sprite->id, 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; } // 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 = SDL_GetTicks(); 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; } // 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; } // 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; } // 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; } // 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; } // 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; _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 and wrap its decoded frame without copying it. frame = videoUpdate(video->handle, &video->texture); if (!videoGetPixels(video->handle, &pixels, &pitch)) { _luaTrace(L, "videoDraw", "%d no frame yet", video->id); return 0; } newFrame = (frame != video->lastFrame); video->lastFrame = frame; 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(); _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) static int32_t apiVideoGetVolume(lua_State *L) { VideoT *video = NULL; int32_t left = 0; int32_t right = 0; _argCheck(L, "videoGetVolume", 1, 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."); } video->handle = videoLoad(name, NULL, dataDir, _global.renderer, true); 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", 3, 3); video = _argVideo(L, "videoSetVolume", 1); left = _argInteger(L, "videoSetVolume", 2); right = _argInteger(L, "videoSetVolume", 3); if (left < 0) { left = 0; } if (left > VIDEO_VOLUME_MAX) { left = VIDEO_VOLUME_MAX; } if (right < 0) { right = 0; } if (right > VIDEO_VOLUME_MAX) { right = 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; } // 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; } static int32_t apiVldpSetVerbose(lua_State *L) { return _apiUnimplemented(L, "vldpSetVerbose"); } // ===== 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, "games.dat", NULL) != 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); 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; } 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; char *temp = NULL; char *temp2 = NULL; SDL_FRect windowTarget; SDL_FRect sindenWhite; SDL_FRect sindenBlack; SDL_Texture *sceneTexture = NULL; SDL_Color sindenWhiteColor = { 255, 255, 255, SDL_ALPHA_OPAQUE }; SDL_Color sindenBlackColor = { 0, 0, 0, SDL_ALPHA_OPAQUE }; SpriteT *sprite = NULL; SpriteT *spriteTemp = NULL; SoundT *sound = NULL; SoundT *soundTemp = NULL; FontT *font = NULL; FontT *fontTemp = NULL; VideoT *video = NULL; VideoT *videoTemp = NULL; SDL_Event event; ManyMouseEvent mouseEvent; MouseT *mouse = NULL; int32_t finished[SOUND_QUEUE_SIZE]; int32_t finishedCount = 0; // Set up globals memset(&_global, 0, sizeof(GlobalT)); _global.colorForeground.r = SDL_ALPHA_OPAQUE; _global.colorForeground.g = SDL_ALPHA_OPAQUE; _global.colorForeground.b = SDL_ALPHA_OPAQUE; _global.colorForeground.a = SDL_ALPHA_OPAQUE; _global.effectsVolume = AUDIO_MAX_VOLUME; _global.keyboardMode = KEYBOARD_NORMAL; _global.frameFileHandle = -1; _global.videoHandle = -1; _global.fontQuality = FONT_QUALITY_SOLID; _global.mouseMode = MOUSE_SINGLE; _global.overlayScaleX = OVERLAY_SCALE_DEFAULT; _global.overlayScaleY = OVERLAY_SCALE_DEFAULT; _global.controllerDeadZone = CONTROLLER_DEAD_ZONE_DEFAULT; _global.pauseEnabled = true; _global.running = true; _global.discStopped = true; _global.mouseEnabled = true; _global.window = window; _global.renderer = renderer; sceneInit(device, renderer); physicsInit(); // Local copy of config _global.conf = cloneConf(conf); vfsInit(_global.conf->container, _global.conf->dataDirBase, _global.conf->dataDir); videoSetAudioDelay(_global.conf->audioDelayMs); videoSetAudioCalibration(_loadAudioCalibration()); utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay()); // Load controller mappings in a throwaway Lua context. _progTrace("Creating Lua context for Singe setup"); _global.luaContext = luaL_newstate(); _startLuaContext(_global.luaContext); // 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(_global.luaContext, (const char *)Framework_singe, Framework_singe_len, "Framework.singe") || lua_pcall(_global.luaContext, 0, 0, 0)) { utilDie("%s", lua_tostring(_global.luaContext, -1)); } // Load default mappings, then each override in turn. _progTrace("Loading default control mappings"); if (luaL_loadbuffer(_global.luaContext, (const char *)controls_cfg, controls_cfg_len, "controls.cfg") || lua_pcall(_global.luaContext, 0, 0, 0)) { utilDie("%s", lua_tostring(_global.luaContext, -1)); } _loadControlsFile("controls.cfg"); temp = utilCreateString("%s..%ccontrols.cfg", _global.conf->dataDir, utilGetPathSeparator()); _loadControlsFile(temp); free(temp); temp = utilCreateString("%scontrols.cfg", _global.conf->dataDir); _loadControlsFile(temp); free(temp); temp2 = utilGetUpToLastPathComponent(_global.conf->scriptFile); temp = utilCreateString("%scontrols.cfg", temp2); _loadControlsFile(temp); free(temp); free(temp2); // Parse results lua_getglobal(_global.luaContext, "DEAD_ZONE"); if (lua_isnumber(_global.luaContext, -1)) { _global.controllerDeadZone = (int32_t)lua_tonumber(_global.luaContext, -1); } lua_pop(_global.luaContext, 1); _progTrace("Controller dead zone is %d", _global.controllerDeadZone); for (x = 0; x < INPUT_COUNT; x++) { // Each INPUT_* table holds { name = ..., value = ... } entries; collect the values. lua_getglobal(_global.luaContext, _inputNames[x].configName); if (!lua_istable(_global.luaContext, -1)) { utilSay("Configuration option %s missing!", _inputNames[x].configName); lua_pop(_global.luaContext, 1); continue; } y = (int32_t)lua_rawlen(_global.luaContext, -1); _global.controlMappings[x].input = (int32_t *)calloc((size_t)(y + 1), sizeof(int32_t)); if (!_global.controlMappings[x].input) { utilDie("Unable to allocate memory for control mappings."); } _global.controlMappings[x].inputCount = 0; lua_pushnil(_global.luaContext); while (lua_next(_global.luaContext, -2)) { if (lua_istable(_global.luaContext, -1)) { lua_getfield(_global.luaContext, -1, "value"); if (lua_isnumber(_global.luaContext, -1) && (_global.controlMappings[x].inputCount < y)) { _global.controlMappings[x].input[_global.controlMappings[x].inputCount++] = (int32_t)lua_tonumber(_global.luaContext, -1); } lua_pop(_global.luaContext, 1); } lua_pop(_global.luaContext, 1); } lua_pop(_global.luaContext, 1); } lua_close(_global.luaContext); // Show splash screens if (!_global.conf->noLogos) { _progTrace("Showing splash screens"); _doLogos(); } // Start Lua for game _progTrace("Creating Lua context for script"); _global.luaContext = luaL_newstate(); _startLuaContext(_global.luaContext); // Lua API for Singe. Comments give the version each call appeared in. lua_register(_global.luaContext, "animationGetTime", apiAnimationGetTime); // 3.00 lua_register(_global.luaContext, "animationIsPlaying", apiAnimationIsPlaying); // 3.00 lua_register(_global.luaContext, "animationPause", apiAnimationPause); // 3.00 lua_register(_global.luaContext, "animationPlay", apiAnimationPlay); // 3.00 lua_register(_global.luaContext, "animationResume", apiAnimationResume); // 3.00 lua_register(_global.luaContext, "animationSetTime", apiAnimationSetTime); // 3.00 lua_register(_global.luaContext, "animationStop", apiAnimationStop); // 3.00 lua_register(_global.luaContext, "bodyApplyForce", apiBodyApplyForce); // 3.00 lua_register(_global.luaContext, "bodyApplyImpulse", apiBodyApplyImpulse); // 3.00 lua_register(_global.luaContext, "bodyDelete", apiBodyDelete); // 3.00 lua_register(_global.luaContext, "bodyGetAngularVelocity", apiBodyGetAngularVelocity); // 3.00 lua_register(_global.luaContext, "bodyGetVelocity", apiBodyGetVelocity); // 3.00 lua_register(_global.luaContext, "bodyIsResting", apiBodyIsResting); // 3.00 lua_register(_global.luaContext, "bodyNew", apiBodyNew); // 3.00 lua_register(_global.luaContext, "bodySetAngularVelocity", apiBodySetAngularVelocity); // 3.00 lua_register(_global.luaContext, "bodySetBounce", apiBodySetBounce); // 3.00 lua_register(_global.luaContext, "bodySetEnabled", apiBodySetEnabled); // 3.00 lua_register(_global.luaContext, "bodySetFriction", apiBodySetFriction); // 3.00 lua_register(_global.luaContext, "bodySetMass", apiBodySetMass); // 3.00 lua_register(_global.luaContext, "bodySetTrigger", apiBodySetTrigger); // 3.00 lua_register(_global.luaContext, "bodySetVelocity", apiBodySetVelocity); // 3.00 lua_register(_global.luaContext, "colorBackground", apiColorBackground); // 1.xx lua_register(_global.luaContext, "colorForeground", apiColorForeground); // 1.xx lua_register(_global.luaContext, "cameraSet", apiCameraSet); // 3.00 lua_register(_global.luaContext, "cameraSetOrthographic", apiCameraSetOrthographic); // 3.00 lua_register(_global.luaContext, "cameraSetPerspective", apiCameraSetPerspective); // 3.00 lua_register(_global.luaContext, "controllerGetAxis", apiControllerGetAxis); // 2.00 lua_register(_global.luaContext, "controllerGetButton", apiControllerGetButton); // 2.10 lua_register(_global.luaContext, "debugPrint", apiDebugPrint); // 1.xx lua_register(_global.luaContext, "discAudio", apiDiscAudio); // 1.xx lua_register(_global.luaContext, "discChangeSpeed", apiDiscChangeSpeed); // 1.xx lua_register(_global.luaContext, "discGetAudioTrack", apiDiscGetAudioTrack); // 2.10 lua_register(_global.luaContext, "discGetAudioTracks", apiDiscGetAudioTracks); // 2.10 lua_register(_global.luaContext, "discGetFrame", apiDiscGetFrame); // 1.xx lua_register(_global.luaContext, "discGetHeight", apiDiscGetHeight); // 2.00 lua_register(_global.luaContext, "discGetLanguage", apiDiscGetLanguage); // 2.10 lua_register(_global.luaContext, "discGetState", apiDiscGetState); // 1.xx RDG lua_register(_global.luaContext, "discGetWidth", apiDiscGetWidth); // 2.00 lua_register(_global.luaContext, "discPause", apiDiscPause); // 1.xx lua_register(_global.luaContext, "discPauseAtFrame", apiDiscSearch); // 1.18 Same as discSearch. lua_register(_global.luaContext, "discPlay", apiDiscPlay); // 1.xx lua_register(_global.luaContext, "discSearch", apiDiscSearch); // 1.xx lua_register(_global.luaContext, "discSearchBlanking", apiDiscSearchBlanking); // 1.xx lua_register(_global.luaContext, "discSetAudioTrack", apiDiscSetAudioTrack); // 2.10 lua_register(_global.luaContext, "discSetFPS", apiDiscSetFPS); // 1.xx lua_register(_global.luaContext, "discSkipBackward", apiDiscSkipBackward); // 1.xx lua_register(_global.luaContext, "discSkipBlanking", apiDiscSkipBlanking); // 1.xx lua_register(_global.luaContext, "discSkipForward", apiDiscSkipForward); // 1.xx lua_register(_global.luaContext, "discSkipToFrame", apiDiscSkipToFrame); // 1.xx lua_register(_global.luaContext, "discStepBackward", apiDiscStepBackward); // 1.xx lua_register(_global.luaContext, "discStepForward", apiDiscStepForward); // 1.xx lua_register(_global.luaContext, "discStop", apiDiscStop); // 1.xx lua_register(_global.luaContext, "fontLoad", apiFontLoad); // 1.xx lua_register(_global.luaContext, "fontPrint", apiFontPrint); // 1.xx lua_register(_global.luaContext, "fontQuality", apiFontQuality); // 1.xx lua_register(_global.luaContext, "fontSelect", apiFontSelect); // 1.xx lua_register(_global.luaContext, "fontToSprite", apiFontToSprite); // 1.xx lua_register(_global.luaContext, "fontUnload", apiFontUnload); // 2.00 lua_register(_global.luaContext, "jointBall", apiJointBall); // 3.00 lua_register(_global.luaContext, "jointDelete", apiJointDelete); // 3.00 lua_register(_global.luaContext, "jointHinge", apiJointHinge); // 3.00 lua_register(_global.luaContext, "jointSetLimits", apiJointSetLimits); // 3.00 lua_register(_global.luaContext, "jointSlider", apiJointSlider); // 3.00 lua_register(_global.luaContext, "keyboardGetLastDown", apiKeyboardGetLastDown); // 2.10 lua_register(_global.luaContext, "keyboardGetLastUp", apiKeyboardGetLastUp); // 2.10 lua_register(_global.luaContext, "keyboardGetMode", apiKeyboardGetMode); // 1.xx RDG lua_register(_global.luaContext, "keyboardGetModifiers", apiKeyboardGetModifiers); // 2.10 lua_register(_global.luaContext, "keyboardIsDown", apiKeyboardIsDown); // 2.10 lua_register(_global.luaContext, "keyboardSetMode", apiKeyboardSetMode); // 1.xx RDG lua_register(_global.luaContext, "lightNew", apiLightNew); // 3.00 lua_register(_global.luaContext, "lightSetColor", apiLightSetColor); // 3.00 lua_register(_global.luaContext, "lightSetCone", apiLightSetCone); // 3.00 lua_register(_global.luaContext, "lightSetIntensity", apiLightSetIntensity); // 3.00 lua_register(_global.luaContext, "lightSetRange", apiLightSetRange); // 3.00 lua_register(_global.luaContext, "lightSetShadow", apiLightSetShadow); // 3.00 lua_register(_global.luaContext, "materialDelete", apiMaterialDelete); // 3.00 lua_register(_global.luaContext, "materialNew", apiMaterialNew); // 3.00 lua_register(_global.luaContext, "materialSetBlend", apiMaterialSetBlend); // 3.00 lua_register(_global.luaContext, "materialSetColor", apiMaterialSetColor); // 3.00 lua_register(_global.luaContext, "materialSetDoubleSided", apiMaterialSetDoubleSided); // 3.00 lua_register(_global.luaContext, "materialSetEmissive", apiMaterialSetEmissive); // 3.00 lua_register(_global.luaContext, "materialSetMetallic", apiMaterialSetMetallic); // 3.00 lua_register(_global.luaContext, "materialSetRoughness", apiMaterialSetRoughness); // 3.00 lua_register(_global.luaContext, "materialSetTexture", apiMaterialSetTexture); // 3.00 lua_register(_global.luaContext, "materialSetUnlit", apiMaterialSetUnlit); // 3.00 lua_register(_global.luaContext, "materialSetVideo", apiMaterialSetVideo); // 3.00 lua_register(_global.luaContext, "meshBox", apiMeshBox); // 3.00 lua_register(_global.luaContext, "meshCone", apiMeshCone); // 3.00 lua_register(_global.luaContext, "meshCylinder", apiMeshCylinder); // 3.00 lua_register(_global.luaContext, "meshDelete", apiMeshDelete); // 3.00 lua_register(_global.luaContext, "meshNew", apiMeshNew); // 3.00 lua_register(_global.luaContext, "meshPlane", apiMeshPlane); // 3.00 lua_register(_global.luaContext, "meshSphere", apiMeshSphere); // 3.00 lua_register(_global.luaContext, "meshTorus", apiMeshTorus); // 3.00 lua_register(_global.luaContext, "modelDelete", apiModelDelete); // 3.00 lua_register(_global.luaContext, "modelGetAnimations", apiModelGetAnimations); // 3.00 lua_register(_global.luaContext, "modelInstance", apiModelInstance); // 3.00 lua_register(_global.luaContext, "modelLoad", apiModelLoad); // 3.00 lua_register(_global.luaContext, "mouseGetPosition", apiMouseGetPosition); // 2.00 lua_register(_global.luaContext, "mouseHowMany", apiMouseHowMany); // 1.18 RDG lua_register(_global.luaContext, "mouseSetCaptured", apiMouseSetCaptured); // 2.00 lua_register(_global.luaContext, "mouseSetEnabled", apiMouseSetEnabled); // 3.00 mouseEnable/mouseDisable are framework aliases. lua_register(_global.luaContext, "mouseSetMode", apiMouseSetMode); // 1.18 RDG lua_register(_global.luaContext, "nodeDelete", apiNodeDelete); // 3.00 lua_register(_global.luaContext, "nodeFind", apiNodeFind); // 3.00 lua_register(_global.luaContext, "nodeGetChildren", apiNodeGetChildren); // 3.00 lua_register(_global.luaContext, "nodeGetName", apiNodeGetName); // 3.00 lua_register(_global.luaContext, "nodeGetParent", apiNodeGetParent); // 3.00 lua_register(_global.luaContext, "nodeGetPosition", apiNodeGetPosition); // 3.00 lua_register(_global.luaContext, "nodeGetQuaternion", apiNodeGetQuaternion); // 3.00 lua_register(_global.luaContext, "nodeGetRotation", apiNodeGetRotation); // 3.00 lua_register(_global.luaContext, "nodeGetScale", apiNodeGetScale); // 3.00 lua_register(_global.luaContext, "nodeGetWorldPosition", apiNodeGetWorldPosition); // 3.00 lua_register(_global.luaContext, "nodeLookAt", apiNodeLookAt); // 3.00 lua_register(_global.luaContext, "nodeMove", apiNodeMove); // 3.00 lua_register(_global.luaContext, "nodeNew", apiNodeNew); // 3.00 lua_register(_global.luaContext, "nodeRotate", apiNodeRotate); // 3.00 lua_register(_global.luaContext, "nodeSetMesh", apiNodeSetMesh); // 3.00 lua_register(_global.luaContext, "nodeSetName", apiNodeSetName); // 3.00 lua_register(_global.luaContext, "nodeSetParent", apiNodeSetParent); // 3.00 lua_register(_global.luaContext, "nodeSetPosition", apiNodeSetPosition); // 3.00 lua_register(_global.luaContext, "nodeSetQuaternion", apiNodeSetQuaternion); // 3.00 lua_register(_global.luaContext, "nodeSetRotation", apiNodeSetRotation); // 3.00 lua_register(_global.luaContext, "nodeSetScale", apiNodeSetScale); // 3.00 lua_register(_global.luaContext, "nodeSetVisible", apiNodeSetVisible); // 3.00 lua_register(_global.luaContext, "overlayBox", apiOverlayBox); // 2.00 lua_register(_global.luaContext, "overlayCircle", apiOverlayCircle); // 2.00 lua_register(_global.luaContext, "overlayClear", apiOverlayClear); // 1.xx lua_register(_global.luaContext, "overlayEllipse", apiOverlayEllipse); // 2.00 lua_register(_global.luaContext, "overlayGetHeight", apiOverlayGetHeight); // 1.xx lua_register(_global.luaContext, "overlayGetWidth", apiOverlayGetWidth); // 1.xx lua_register(_global.luaContext, "overlayLine", apiOverlayLine); // 2.00 lua_register(_global.luaContext, "overlayPlot", apiOverlayPlot); // 2.00 lua_register(_global.luaContext, "overlayPrint", apiOverlayPrint); // 1.xx lua_register(_global.luaContext, "overlaySetResolution", apiOverlaySetResolution); // 2.00 lua_register(_global.luaContext, "physicsRaycast", apiPhysicsRaycast); // 3.00 lua_register(_global.luaContext, "physicsSetEnabled", apiPhysicsSetEnabled); // 3.00 lua_register(_global.luaContext, "physicsSetGravity", apiPhysicsSetGravity); // 3.00 lua_register(_global.luaContext, "sceneEnable", apiSceneEnable); // 3.00 lua_register(_global.luaContext, "sceneGetSize", apiSceneGetSize); // 3.00 lua_register(_global.luaContext, "sceneProject", apiSceneProject); // 3.00 lua_register(_global.luaContext, "sceneSetAmbient", apiSceneSetAmbient); // 3.00 lua_register(_global.luaContext, "sceneSetAntialias", apiSceneSetAntialias); // 3.00 lua_register(_global.luaContext, "sceneSetBackground", apiSceneSetBackground); // 3.00 lua_register(_global.luaContext, "sceneSetShadowSize", apiSceneSetShadowSize); // 3.00 lua_register(_global.luaContext, "sceneUnproject", apiSceneUnproject); // 3.00 lua_register(_global.luaContext, "scriptExecute", apiScriptExecute); // 2.00 lua_register(_global.luaContext, "scriptPush", apiScriptPush); // 2.00 lua_register(_global.luaContext, "singeGetAudioCalibration", apiSingeGetAudioCalibration); // 3.00 lua_register(_global.luaContext, "singeGetAudioDelay", apiSingeGetAudioDelay); // 3.00 lua_register(_global.luaContext, "singeGetAudioLatency", apiSingeGetAudioLatency); // 3.00 lua_register(_global.luaContext, "singeGetDataPath", apiSingeGetDataPath); // 2.00 lua_register(_global.luaContext, "singeGetHeight", apiSingeGetHeight); // 1.xx lua_register(_global.luaContext, "singeGetPauseFlag", apiSingeGetPauseFlag); // 1.xx RDG lua_register(_global.luaContext, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG lua_register(_global.luaContext, "singeGetTicks", apiSingeGetTicks); // 3.00 lua_register(_global.luaContext, "singeGetWidth", apiSingeGetWidth); // 1.xx lua_register(_global.luaContext, "singeQuit", apiSingeQuit); // 1.xx RDG lua_register(_global.luaContext, "singeScreenshot", apiSingeScreenshot); // 1.xx lua_register(_global.luaContext, "singeSetAudioCalibration", apiSingeSetAudioCalibration); // 3.00 lua_register(_global.luaContext, "singeSetAudioDelay", apiSingeSetAudioDelay); // 3.00 lua_register(_global.luaContext, "singeSetGameName", apiSingeSetGameName); // 1.15 RDG lua_register(_global.luaContext, "singeSetPauseFlag", apiSingeSetPauseFlag); // 1.xx RDG lua_register(_global.luaContext, "singeSetPauseKeyEnabled", apiSingeSetPauseKeyEnabled); // 3.00 singeEnablePauseKey/singeDisablePauseKey are framework aliases. lua_register(_global.luaContext, "singeVersion", apiSingeVersion); // 1.xx RDG lua_register(_global.luaContext, "singeWantsCrosshairs", apiSingeWantsCrosshairs); // 2.00 lua_register(_global.luaContext, "soundFullStop", apiSoundFullStop); // 1.16 lua_register(_global.luaContext, "soundGetVolume", apiSoundGetVolume); // 1.16 lua_register(_global.luaContext, "soundIsPlaying", apiSoundIsPlaying); // 1.16 RDG lua_register(_global.luaContext, "soundLoad", apiSoundLoad); // 1.xx lua_register(_global.luaContext, "soundPause", apiSoundPause); // 1.16 RDG lua_register(_global.luaContext, "soundPlay", apiSoundPlay); // 1.xx lua_register(_global.luaContext, "soundResume", apiSoundResume); // 1.16 RDG lua_register(_global.luaContext, "soundSetVolume", apiSoundSetVolume); // 1.16 lua_register(_global.luaContext, "soundStop", apiSoundStop); // 1.xx RDG lua_register(_global.luaContext, "soundUnload", apiSoundUnload); // 2.00 lua_register(_global.luaContext, "spriteDraw", apiSpriteDraw); // 1.xx Handle first since 3.00. lua_register(_global.luaContext, "spriteGetFrame", apiSpriteGetFrame); // 2.10 lua_register(_global.luaContext, "spriteGetHeight", apiSpriteGetHeight); // 2.00 lua_register(_global.luaContext, "spriteGetWidth", apiSpriteGetWidth); // 2.00 lua_register(_global.luaContext, "spriteIsPlaying", apiSpriteIsPlaying); // 2.10 lua_register(_global.luaContext, "spriteLoad", apiSpriteLoad); // 1.xx lua_register(_global.luaContext, "spriteLoop", apiSpriteLoop); // 2.10 Handle first since 3.00. lua_register(_global.luaContext, "spritePause", apiSpritePause); // 2.10 lua_register(_global.luaContext, "spritePlay", apiSpritePlay); // 2.10 lua_register(_global.luaContext, "spriteQuality", apiSpriteQuality); // 2.10 Handle first since 3.00. lua_register(_global.luaContext, "spriteRotate", apiSpriteRotate); // 2.10 Handle first since 3.00. lua_register(_global.luaContext, "spriteRotateAndScale", apiSpriteRotateAndScale); // 2.10 Handle first since 3.00. lua_register(_global.luaContext, "spriteScale", apiSpriteScale); // 2.10 Handle first since 3.00. lua_register(_global.luaContext, "spriteSetFrame", apiSpriteSetFrame); // 2.10 Handle first since 3.00. lua_register(_global.luaContext, "spriteUnload", apiSpriteUnload); // 2.00 lua_register(_global.luaContext, "videoDraw", apiVideoDraw); // 2.00 lua_register(_global.luaContext, "videoGetAudioTrack", apiVideoGetAudioTrack); // 2.10 lua_register(_global.luaContext, "videoGetAudioTracks", apiVideoGetAudioTracks); // 2.10 lua_register(_global.luaContext, "videoGetFrame", apiVideoGetFrame); // 2.00 lua_register(_global.luaContext, "videoGetFrameCount", apiVideoGetFrameCount); // 2.00 lua_register(_global.luaContext, "videoGetHeight", apiVideoGetHeight); // 2.00 lua_register(_global.luaContext, "videoGetLanguage", apiVideoGetLanguage); // 2.10 lua_register(_global.luaContext, "videoGetLanguageDescription", apiVideoGetLanguageDescription); // 2.10 lua_register(_global.luaContext, "videoGetVolume", apiVideoGetVolume); // 2.00 lua_register(_global.luaContext, "videoGetWidth", apiVideoGetWidth); // 2.00 lua_register(_global.luaContext, "videoIsPlaying", apiVideoIsPlaying); // 2.00 lua_register(_global.luaContext, "videoLoad", apiVideoLoad); // 2.00 lua_register(_global.luaContext, "videoPause", apiVideoPause); // 2.00 lua_register(_global.luaContext, "videoPlay", apiVideoPlay); // 2.00 lua_register(_global.luaContext, "videoQuality", apiVideoQuality); // 2.10 lua_register(_global.luaContext, "videoRotate", apiVideoRotate); // 2.10 lua_register(_global.luaContext, "videoRotateAndScale", apiVideoRotateAndScale); // 2.10 lua_register(_global.luaContext, "videoScale", apiVideoScale); // 2.10 lua_register(_global.luaContext, "videoSeek", apiVideoSeek); // 2.00 lua_register(_global.luaContext, "videoSetAudioTrack", apiVideoSetAudioTrack); // 2.10 lua_register(_global.luaContext, "videoSetVolume", apiVideoSetVolume); // 2.00 lua_register(_global.luaContext, "videoUnload", apiVideoUnload); // 2.00 lua_register(_global.luaContext, "vldpGetHeight", apiDiscGetHeight); // 1.xx Same as discGetHeight. lua_register(_global.luaContext, "vldpGetPixel", apiVldpGetPixel); // 1.xx lua_register(_global.luaContext, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth. lua_register(_global.luaContext, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx // 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 { _global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer, false); } 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 (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) { if (videoWidth * conf->yResolution > videoHeight * conf->xResolution) { // Video is wider than the window: keep the width, shrink the height. conf->yResolution = conf->xResolution * videoHeight / videoWidth; } else { // Video is taller: keep the height, shrink the width. conf->xResolution = conf->yResolution * videoWidth / videoHeight; } _global.conf->xResolution = conf->xResolution; _global.conf->yResolution = conf->yResolution; _progTrace("Resizing window to %dx%d based on main video file", conf->xResolution, conf->yResolution); SDL_SetWindowSize(_global.window, conf->xResolution, conf->yResolution); SDL_SyncWindow(_global.window); SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); SDL_RenderClear(_global.renderer); } // Everything renders in video coordinates unless the user wants it stretched. if (!_global.conf->stretchVideo) { SDL_SetRenderLogicalPresentation(_global.renderer, videoWidth, videoHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX); } // Default render location is the entire window windowTarget.x = 0; windowTarget.y = 0; windowTarget.w = videoWidth; windowTarget.h = videoHeight; sindenWhite.x = -1; sindenBlack.x = -1; // Overscan compensation if (_global.conf->scaleFactor < SCALE_FACTOR_MAX) { windowTarget.w = videoWidth * _global.conf->scaleFactor / SCALE_FACTOR_MAX; windowTarget.h = videoHeight * _global.conf->scaleFactor / SCALE_FACTOR_MAX; windowTarget.x = (videoWidth - windowTarget.w) / 2; windowTarget.y = (videoHeight - windowTarget.h) / 2; } // Sinden Light Gun Border Setup if (_global.conf->sindenArgc > 0) { //***TODO*** ADD MOUSE SCALING TO COMPENSATE FOR BORDER switch (_global.conf->sindenArgc) { // WW - Just the width of the white border case SINDEN_WHITE: sindenWhite.x = _global.conf->sindenArgv[0]; break; // WW WB - Width of white border and then black border case SINDEN_WHITE_BLACK: sindenWhite.x = _global.conf->sindenArgv[0]; sindenBlack.x = _global.conf->sindenArgv[1]; break; // RW GW BW WW - Custom color "white" border and width case SINDEN_CUSTOM_WHITE: sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0]; sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1]; sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2]; sindenWhite.x = _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: sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0]; sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1]; sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2]; sindenWhite.x = _global.conf->sindenArgv[3]; sindenBlack.x = _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: sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0]; sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1]; sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2]; sindenWhite.x = _global.conf->sindenArgv[3]; sindenBlackColor.r = (uint8_t)_global.conf->sindenArgv[4]; sindenBlackColor.g = (uint8_t)_global.conf->sindenArgv[5]; sindenBlackColor.b = (uint8_t)_global.conf->sindenArgv[6]; sindenBlack.x = _global.conf->sindenArgv[7]; break; default: utilDie("Bad Sinden argument count: %d", _global.conf->sindenArgc); } // The white border is the inner one; the black border (if any) surrounds it. sindenWhite.y = sindenWhite.x; sindenWhite.w = videoWidth - (sindenWhite.x * 2); sindenWhite.h = videoHeight - (sindenWhite.y * 2); if (sindenBlack.x >= 0) { sindenBlack.y = sindenBlack.x; sindenBlack.w = videoWidth - (sindenBlack.x * 2); sindenBlack.h = videoHeight - (sindenBlack.y * 2); sindenWhite.x += sindenBlack.x; sindenWhite.y += sindenBlack.y; sindenWhite.w -= (sindenBlack.x * 2); sindenWhite.h -= (sindenBlack.y * 2); } windowTarget = sindenWhite; } // Create overlay surface and its texture x = (int32_t)(videoWidth * _global.overlayScaleX); y = (int32_t)(videoHeight * _global.overlayScaleY); _progTrace("Creating overlay of %dx%d", x, y); _global.overlay = SDL_CreateSurface(x, y, SDL_PIXELFORMAT_BGRA32); if (_global.overlay == NULL) { utilDie("%s", SDL_GetError()); } SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND); _global.overlayTexture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, x, y); if (_global.overlayTexture == NULL) { utilDie("%s", SDL_GetError()); } sceneResize(x, y); SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); _global.overlayDirty = true; // 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; } if ((_global.mouseCount < 1) && _global.mouseEnabled) { utilSay("Warning: No mice detected. Mouse input disabled."); _global.mouseEnabled = false; } 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); } // Grab mouse _progTrace("Grabbing mouse"); _setMouseCaptured(true); // 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(); _suppressHeldInput(); // Sound effect tracks: a fixed pool so scripts keep getting small channel numbers. _global.effectsVolume = AUDIO_MAX_VOLUME * _global.conf->volumeNonVldp / VOLUME_MAX; _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)); // Let us know when sounds end MIX_SetTrackStoppedCallback(_effectTracks[x], _effectStopped, (void *)(intptr_t)x); } // 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)); // The disc always starts parked on frame 1, paused, like discSearch(1). if (_global.videoHandle >= 0) { _progTrace("Parking laserdisc on frame 1"); _discSeek(1); videoPause(_global.videoHandle); _global.discStopped = false; _selectDefaultAudioTrack(_global.videoHandle); } // Start script _progTrace("Running %s", _global.conf->scriptFile); lua_pushcfunction(_global.luaContext, _luaTraceback); if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL) || lua_pcall(_global.luaContext, 0, 0, -2)) { utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1)); } lua_settop(_global.luaContext, 0); // Game Loop _progTrace("Script is running"); while (_global.running) { // SDL Event Loop while (SDL_PollEvent(&event)) { // Mouse positions arrive in window pixels; the game works in the video's coordinates. SDL_ConvertEventToRenderCoordinates(_global.renderer, &event); 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.controllerDeadZone) { 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) && (SDL_GetTicks() < _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; _processKey(event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN, 0, code); break; case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: _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) && (SDL_GetTicks() < _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; } } _processKey(event.type == SDL_EVENT_KEY_DOWN, event.key.key, event.key.scancode); 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)) { x = (int32_t)(event.motion.x * _global.overlayScaleX); y = (int32_t)(event.motion.y * _global.overlayScaleY); xr = (int32_t)(event.motion.xrel * _global.overlayScaleX); yr = (int32_t)(event.motion.yrel * _global.overlayScaleY); _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)) { _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)) { 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_QUIT: _progTrace("Quit requested"); _global.running = false; break; default: break; } } // Mouse Event Loop - drained even when unused so the queue never fills. while (ManyMouse_PollEvent(&mouseEvent)) { if (!_global.mouseEnabled || (_global.mouseMode != MOUSE_MANY) || (mouseEvent.device >= (unsigned)_global.mouseCount)) { continue; } mouse = &_global.mice[mouseEvent.device]; switch (mouseEvent.type) { case MANYMOUSE_EVENT_RELMOTION: // Integrate the motion into an absolute position clamped to the video. xr = 0; yr = 0; if (mouseEvent.item == 0) { xr = mouseEvent.value; mouse->x += xr; } else { yr = mouseEvent.value; 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. if (mouseEvent.maxval > mouseEvent.minval) { if (mouseEvent.item == 0) { mouse->x = (int32_t)((int64_t)(mouseEvent.value - mouseEvent.minval) * videoWidth / (mouseEvent.maxval - mouseEvent.minval)); } else { mouse->y = (int32_t)((int64_t)(mouseEvent.value - mouseEvent.minval) * videoHeight / (mouseEvent.maxval - mouseEvent.minval)); } x = (int32_t)(mouse->x * _global.overlayScaleX); y = (int32_t)(mouse->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; default: break; } } // Deliver sound completions on this thread. They wait out an engine pause. if (!_global.frozen) { videoLockAudio(); finishedCount = _global.soundQueueCount; memcpy(finished, _global.soundQueue, sizeof(int32_t) * (size_t)finishedCount); _global.soundQueueCount = 0; videoUnlockAudio(); for (x = 0; x < finishedCount; x++) { _callLua("onSoundCompleted", "i", finished[x]); } } // Update the disc video if (_global.videoHandle >= 0) { thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture); if (_global.conf->isFrameFile) { frameFileUpdate(_global.frameFileHandle, &_global.videoHandle); } // 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. if (!_global.frozen && (SDL_GetTicks() > frameClock)) { intReturn = OVERLAY_NOT_UPDATED; _callLua("onOverlayUpdate", ">i", &intReturn); if (intReturn == OVERLAY_UPDATED) { _global.refreshDisplay = true; } frameClock = SDL_GetTicks() + FRAME_TICK_MS; // Don't eat all the CPU. // Clear per-frame values. _global.keyboardLastDown = SDL_SCANCODE_UNKNOWN; _global.keyboardLastUp = SDL_SCANCODE_UNKNOWN; } // Update display if (_global.refreshDisplay || _global.overlayDirty || sceneIsEnabled()) { // Clear entire display to black SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); SDL_RenderClear(_global.renderer); // Sinden Gun Border if (sindenWhite.x >= 0) { if (sindenBlack.x >= 0) { SDL_SetRenderDrawColor(_global.renderer, sindenBlackColor.r, sindenBlackColor.g, sindenBlackColor.b, sindenBlackColor.a); SDL_RenderFillRect(_global.renderer, &sindenBlack); } SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a); SDL_RenderFillRect(_global.renderer, &sindenWhite); } // 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, &windowTarget); } else { SDL_RenderTexture(_global.renderer, _global.videoTexture, NULL, &windowTarget); } } // 3D scene modelUpdate(!_global.frozen); physicsUpdate(!_global.frozen); _physicsCallbacks(); sceneUpdateVideo(_sceneVideoSource); sceneTexture = sceneRender(); if (sceneTexture != NULL) { SDL_RenderTexture(_global.renderer, sceneTexture, NULL, &windowTarget); } // Overlay if (_global.overlayDirty) { SDL_UpdateTexture(_global.overlayTexture, NULL, _global.overlay->pixels, _global.overlay->pitch); _global.overlayDirty = false; } SDL_RenderTexture(_global.renderer, _global.overlayTexture, NULL, &windowTarget); if (_global.frozen) { _drawPauseIndicator(&windowTarget); } // Save it? if (_global.requestScreenShot) { _global.requestScreenShot = false; _progTrace("Taking screenshot"); _takeScreenshot(); } // Show it SDL_RenderPresent(_global.renderer); _global.refreshDisplay = false; } SDL_Delay(IDLE_SLEEP_MS); } // End game _progTrace("Script is shutting down"); _callLua("onShutdown", ""); // 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 Lua _progTrace("Stopping Lua"); lua_close(_global.luaContext); // Free overlay & overlay font _progTrace("Destroying overlay"); SDL_DestroyTexture(_global.pauseTexture); modelQuit(); physicsQuit(); sceneQuit(); SDL_DestroyTexture(_global.overlayTexture); SDL_DestroySurface(_global.overlay); _progTrace("Destroying console font"); SDL_DestroySurface(_global.consoleFontSurface); // Unload resources the script left behind HASH_ITER(hh, _global.fontList, font, fontTemp) { _progTrace("Unloading font handle %d", font->id); _fontDestroy(font); } 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); } // 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 controllers _progTrace("Stopping controllers"); _stopControllers(); // Stop mice _progTrace("Releasing mouse"); _setMouseCaptured(false); _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); } }