singe/src/singe.c

10233 lines
343 KiB
C

/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#include <string.h>
#include <math.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <SDL3_ttf/SDL_ttf.h>
#include "../thirdparty/lua/src/lua.h"
#include "../thirdparty/lua/src/lualib.h"
#include "../thirdparty/lua/src/lauxlib.h"
#include "../thirdparty/uthash/src/uthash.h"
#include "../thirdparty/manymouse/manymouse.h"
#include "rotoZoom.h"
#include "../thirdparty/luafilesystem/src/lfs.h"
#include "../thirdparty/luasec/src/context.h"
#include "../thirdparty/luasec/src/ssl.h"
#include "../thirdparty/luasec/src/x509.h"
#include "../thirdparty/luasocket/src/luasocket.h"
#include "../thirdparty/luasocket/src/mime.h"
#ifndef _WIN32
#include "../thirdparty/luasocket/src/unix.h"
// There is no serial.h, so fake it.
LUASOCKET_API int luaopen_socket_serial(lua_State *L);
#endif
// There is no header for rs232 binding. Make our own.
int luaopen_luars232(lua_State *L);
// Nor for lsqlite3.
int luaopen_lsqlite3(lua_State *L);
// 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 "hdr.h"
#include "ktx2.h"
#include "nav.h"
#include "model.h"
#include "physics.h"
#include "particles.h"
#include "videoPlayer.h"
#include "singe.h"
// We have to do the embedding here so the Lua module
// definitions can find their length properly. They
// can't be external to this source file.
#define EMBED_HERE
#include "embedded.h"
// soundSetVolume/soundGetVolume use this legacy scale; the mixer uses MIX_MAX_VOLUME.
#define AUDIO_MAX_VOLUME 63
#define 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)
// Codes below the gamepad range are keyboard scancodes, so every key SDL defines must sit under it.
SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE > SDL_SCANCODE_ENDCALL);
#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 COLOR_BYTE_MAX 255
#define NAV_DRAW_VERTICES 4096 // Starting room for navDraw, doubled until the mesh fits
#define SOUND_QUEUE_SIZE 64
#define MS_PER_SECOND_NUMBER 1000.0
#define INPUT_GRACE_MS 1000 // Presses this soon after a script starts or focus arrives were held over from before
#define EFFECT_TRACKS 16 // Sound effect "channels" scripts can play at once
#define SOUND_DEFAULT_NEAR 1.0f // A positioned sound is at full volume within this distance ...
#define SOUND_DEFAULT_FAR 30.0f // ... and silent beyond this
#define SOUND_FADE_FRACTION 0.2f // Of the far distance, over which it fades to silence
#define SOUND_LOOP_FOREVER -1
#define SOUND_DIRECTION_EPSILON 0.0001f // Closer than this to the listener has no direction
#define HEIGHTMAP_MAX 1025 // Pixels per side of a heightmap image
#define WATCH_INTERVAL_MS 1000 // How often --reload checks the script files
#define AUDIO_CALIBRATION_FILE "audio.cfg" // Per-machine audio delay, in the data root
#define CONTROLS_FILE "controls.cfg" // Input mappings, built in and overridden per game
#define SOUND_CHANNEL_NONE -1 // soundPlay's answer when every channel is busy (SOUND_ERROR_INVALID)
#define COLOR_COMPONENTS 4 // r, g, b, a
#define BYTES_PER_KIB 1024
#define SHAPE_SIZES 3 // a, b, c sizing a primitive shape
#define VEHICLE_DRIVE_INPUTS 4 // forward, right, brake, hand brake
#define MESH_SEGMENTS_DEFAULT 24 // Around a cone or cylinder
#define MESH_SPHERE_SEGMENTS_DEFAULT 32 // Around a sphere or torus
#define EMITTER_DEFAULT_BOUNCE 0.5f // emitterSetCollide's optional arguments
#define EMITTER_DEFAULT_FRICTION 0.2f
#define EMITTER_DEFAULT_FLOOR 0.0f
#define WATER_DEFAULT_LINEAR_DRAG 0.5f // bodySetWater's optional arguments
#define WATER_DEFAULT_ANGULAR_DRAG 0.1f
#define LISTENER_CAMERA -1 // soundSetListener's default: the scene camera
#define EFFECT_TAG "effects"
#define 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 QUAD_VERTICES 4 // A textured quad for SDL_RenderGeometry ...
#define QUAD_INDICES 6 // ... as two triangles
#define TRAIL_POINT_FLOATS 3 // x, y, z per recorded trail point
#define COLOR_KEY_VALUE 0
#define 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;
// How an io function hooked through the vfs uses its file name.
typedef enum IoHookModeE {
IO_HOOK_READ = 0,
IO_HOOK_OPEN = 1, // The mode string decides
IO_HOOK_WRITE = 2
} IoHookModeE;
typedef enum OverlayResultE {
OVERLAY_NOT_UPDATED = 0,
OVERLAY_UPDATED = 1
} OverlayResultE;
// Index into _global.controlMappings and the SWITCH_* value handed to scripts.
typedef enum InputE {
INPUT_UP = 0,
INPUT_LEFT,
INPUT_DOWN,
INPUT_RIGHT,
INPUT_1P_START,
INPUT_2P_START,
INPUT_ACTION_1,
INPUT_ACTION_2,
INPUT_ACTION_3,
INPUT_1P_COIN,
INPUT_2P_COIN,
INPUT_SKILL_EASY,
INPUT_SKILL_MEDIUM,
INPUT_SKILL_HARD,
INPUT_SERVICE,
INPUT_TEST_MODE,
INPUT_RESET_CPU,
INPUT_SCREENSHOT,
INPUT_QUIT,
INPUT_PAUSE,
INPUT_CONSOLE,
// Added in Singe 2.00
INPUT_ACTION_4,
INPUT_TILT,
INPUT_GRAB,
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;
// Renderer textures made from an emitter's frames for drawing 2D particles, refreshed when they change.
typedef struct ParticleTexturesS {
int32_t id;
uint32_t version;
int32_t count;
SDL_Texture **textures;
ParticleBlendE blend; // The blend mode the textures were last set to
UT_hash_handle hh;
} ParticleTexturesT;
typedef struct SpriteS {
int32_t id;
IMG_Animation *animation; // NULL for still images
SDL_Surface *originalSurface; // Owned unless it points into animation->frames
SDL_Surface *surface; // What gets drawn: originalSurface, or a transformed copy
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;
uint64_t loopMs; // One pass through every frame's (clamped) delay
UT_hash_handle hh;
} SpriteT;
// A loose script file --reload watches.
typedef struct WatchedFileS {
char *name;
int64_t modified;
} WatchedFileT;
typedef struct SoundS {
int32_t id;
MIX_Audio *audio;
UT_hash_handle hh;
} SoundT;
// Where a playing effect channel sits: in the scene (following a node or placed by hand), or
// panned across the speakers, or neither.
typedef struct EffectS {
int32_t node; // Followed while it exists; LISTENER_CAMERA when the position is given by hand
Vec3T position; // World position when not following a node
float nearBy; // Full volume within this distance ...
float farOff; // ... silent beyond this
float relative[3]; // The last position handed to the mixer, listener space
float gain; // The last distance gain
float baseGain; // The effects volume the track gain was last built from
bool positioned;
} EffectT;
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_GPUDevice *device;
bool reloadRequested; // singeReload, F5 or a watched file changing
WatchedFileT *watched;
int32_t watchedCount;
uint64_t watchTick; // When the watched files were last checked
SDL_Texture *videoTexture;
SDL_Surface *consoleFontSurface;
SDL_Gamepad *controllers[MAX_CONTROLLERS];
int32_t controllerDeadZone;
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;
int32_t listenerNode; // For positioned sounds; LISTENER_CAMERA for the scene camera
KeyboardModeE keyboardMode;
bool keyboardState[SDL_SCANCODE_COUNT];
bool keySuppressed[SDL_SCANCODE_COUNT]; // Held before this script started; not a press for it
bool buttonSuppressed[MAX_CONTROLLERS][CONTROLLER_BUTTON_COUNT];
uint64_t inputGraceUntil; // Until then, new presses are treated as held over too
int32_t keyboardLastDown;
int32_t keyboardLastUp;
int32_t frameFileHandle;
int32_t videoHandle; // -1 when the game has no disc
int32_t canvasWidth; // World size: the disc's, or the configured canvas
int32_t canvasHeight;
FontQualityE fontQuality;
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;
ParticleTexturesT *particleTextures; // 2D particle textures by emitter
Vec3T *navDrawVertices; // navDraw's triangle list, kept between frames
int32_t navDrawCapacity;
SDL_Vertex *quadVertices; // Scratch for 2D particle quads and trails, grown as needed
int32_t *quadIndices;
int32_t quadCapacity; // In quads
int32_t *frameStarts; // Per emitter frame, where its particles' quads begin in the index scratch
int32_t frameStartCapacity;
SoundT *soundList;
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];
static EffectT _effects[EFFECT_TRACKS];
// One entry per InputE, in order. The config name is the controls.cfg table; the switch name is the Lua constant.
static const InputNameT _inputNames[INPUT_COUNT] = {
{ "INPUT_UP", "SWITCH_UP" },
{ "INPUT_LEFT", "SWITCH_LEFT" },
{ "INPUT_DOWN", "SWITCH_DOWN" },
{ "INPUT_RIGHT", "SWITCH_RIGHT" },
{ "INPUT_1P_START", "SWITCH_START1" },
{ "INPUT_2P_START", "SWITCH_START2" },
{ "INPUT_ACTION_1", "SWITCH_BUTTON1" },
{ "INPUT_ACTION_2", "SWITCH_BUTTON2" },
{ "INPUT_ACTION_3", "SWITCH_BUTTON3" },
{ "INPUT_1P_COIN", "SWITCH_COIN1" },
{ "INPUT_2P_COIN", "SWITCH_COIN2" },
{ "INPUT_SKILL_EASY", "SWITCH_SKILL1" },
{ "INPUT_SKILL_MEDIUM", "SWITCH_SKILL2" },
{ "INPUT_SKILL_HARD", "SWITCH_SKILL3" },
{ "INPUT_SERVICE", "SWITCH_SERVICE" },
{ "INPUT_TEST_MODE", "SWITCH_TEST" },
{ "INPUT_RESET_CPU", "SWITCH_RESET" },
{ "INPUT_SCREENSHOT", "SWITCH_SCREENSHOT" },
{ "INPUT_QUIT", "SWITCH_QUIT" },
{ "INPUT_PAUSE", "SWITCH_PAUSE" },
{ "INPUT_CONSOLE", "SWITCH_CONSOLE" },
{ "INPUT_ACTION_4", "SWITCH_BUTTON4" },
{ "INPUT_TILT", "SWITCH_TILT" },
{ "INPUT_GRAB", "SWITCH_GRAB" }
};
// 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 int32_t _argAnimation(lua_State *L, const char *method, int32_t model, int32_t index);
static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t index);
static bool _argBoolean(lua_State *L, const char *method, int32_t index);
static int32_t _argChannel(lua_State *L, const char *method, int32_t index);
static uint8_t _argColorByte(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 _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun);
static int32_t _argInteger(lua_State *L, const char *method, int32_t index);
static int32_t _argBody(lua_State *L, const char *method, int32_t index);
static int32_t _argEmitter(lua_State *L, const char *method, int32_t index);
static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Surface **surface, Ktx2ImageT *ktx2);
static int32_t _argMaterial(lua_State *L, const char *method, int32_t index);
static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index);
static int32_t _argMesh(lua_State *L, const char *method, int32_t index);
static int32_t _argNav(lua_State *L, const char *method, int32_t index);
static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index);
static int32_t _argNode(lua_State *L, const char *method, int32_t index);
static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun);
static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b);
static int32_t _argPlayer(lua_State *L, const char *method, int32_t index);
static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index);
static int32_t _argSoft(lua_State *L, const char *method, int32_t index);
static int32_t _argVehicle(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 int32_t _argView(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 bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2);
static int32_t _effectTrackFree(void);
static float _effectGain(const EffectT *effect, float distance);
static void _effectReset(int32_t channel);
static void _effectStopped(void *userdata, MIX_Track *track);
static int32_t _controllerSlot(SDL_JoystickID which);
static bool _delayAndPump(uint32_t ms);
static void _deliverKey(bool down, int32_t keysym, int32_t scancode);
static int64_t _discGetFrame(void);
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 _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target);
static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY);
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 _loadControlMappings(void);
static void _loadControlsFile(const char *path);
static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length);
static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface);
static int32_t _luaCallOriginal(lua_State *L);
static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn));
static int32_t _luaDofile(lua_State *L);
static int32_t _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, bool watch);
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 int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength);
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 _overlayResize(int32_t width, int32_t height);
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 _quadIndex(int32_t *indices, int32_t slot, int32_t quad);
static void _quadScratch(int32_t quads, int32_t frames);
static void _navCallbacks(void);
static void _physicsCallbacks(void);
static void _particleTexturesDestroy(ParticleTexturesT *cache);
static void _particleTexturesFree(int32_t emitter);
static void _particleTexturesFreeAll(void);
static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view);
static void _registerApi(lua_State *L);
static void _reloadScript(void);
static void _resetScriptState(void);
static void _runScript(bool fatal);
static SDL_Texture *_sceneVideoSource(int32_t player);
static ConfigT *_scriptConfFromTable(lua_State *L, const char *method);
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 int32_t _soundQueueDrain(int32_t *finished);
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 _subsystemsInit(void);
static void _subsystemsQuit(void);
static void _suppressHeldInput(void);
static uint32_t _surfaceCornerKey(SDL_Surface *surface);
static void _surfaceUnpack(SDL_Surface **surface);
static void _takeScreenshot(void);
static void _unloadScriptResources(void);
static void _updatePauseState(void);
static void _updateSounds(void);
static void _videoDestroy(VideoT *video);
static void _watchFile(const char *name);
static bool _watchedChanged(void);
static int32_t apiAnimationGetTime(lua_State *L);
static int32_t apiAnimationIsPlaying(lua_State *L);
static int32_t apiAnimationPause(lua_State *L);
static int32_t apiAnimationPlay(lua_State *L);
static int32_t apiAnimationPlayLayer(lua_State *L);
static int32_t apiAnimationResume(lua_State *L);
static int32_t apiAnimationSetLayerMask(lua_State *L);
static int32_t apiAnimationSetLayerWeight(lua_State *L);
static int32_t apiAnimationSetTime(lua_State *L);
static int32_t apiAnimationStop(lua_State *L);
static int32_t apiBodyApplyForce(lua_State *L);
static int32_t apiBodyApplyImpulse(lua_State *L);
static int32_t apiBodyDelete(lua_State *L);
static int32_t apiBodyGetAngularVelocity(lua_State *L);
static int32_t apiBodyGetVelocity(lua_State *L);
static int32_t apiBodyIsResting(lua_State *L);
static int32_t apiBodyNew(lua_State *L);
static int32_t apiBodySetAngularVelocity(lua_State *L);
static int32_t apiBodySetBounce(lua_State *L);
static int32_t apiBodySetBuoyancy(lua_State *L);
static int32_t apiBodySetCurrent(lua_State *L);
static int32_t apiBodySetEnabled(lua_State *L);
static int32_t apiBodySetFriction(lua_State *L);
static int32_t apiBodySetMass(lua_State *L);
static int32_t apiBodySetTrigger(lua_State *L);
static int32_t apiBodySetVelocity(lua_State *L);
static int32_t apiBodySetWater(lua_State *L);
static int32_t apiCameraSet(lua_State *L);
static int32_t apiCameraSetOrthographic(lua_State *L);
static int32_t apiCameraSetPerspective(lua_State *L);
static int32_t 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 apiEmitterBurst(lua_State *L);
static int32_t apiEmitterClear(lua_State *L);
static int32_t apiEmitterDelete(lua_State *L);
static int32_t apiEmitterDraw(lua_State *L);
static int32_t apiEmitterGetCount(lua_State *L);
static int32_t apiEmitterIsActive(lua_State *L);
static int32_t apiEmitterNew(lua_State *L);
static int32_t apiEmitterSetBlend(lua_State *L);
static int32_t apiEmitterSetCollide(lua_State *L);
static int32_t apiEmitterSetColor(lua_State *L);
static int32_t apiEmitterSetDirection(lua_State *L);
static int32_t apiEmitterSetDrag(lua_State *L);
static int32_t apiEmitterSetFrames(lua_State *L);
static int32_t apiEmitterSetGravity(lua_State *L);
static int32_t apiEmitterSetLayer(lua_State *L);
static int32_t apiEmitterSetLife(lua_State *L);
static int32_t apiEmitterSetLit(lua_State *L);
static int32_t apiEmitterSetLocal(lua_State *L);
static int32_t apiEmitterSetMax(lua_State *L);
static int32_t apiEmitterSetPosition(lua_State *L);
static int32_t apiEmitterSetRadius(lua_State *L);
static int32_t apiEmitterSetRate(lua_State *L);
static int32_t apiEmitterSetSize(lua_State *L);
static int32_t apiEmitterSetSoftness(lua_State *L);
static int32_t apiEmitterSetSpeed(lua_State *L);
static int32_t apiEmitterSetSpin(lua_State *L);
static int32_t apiEmitterSetSpread(lua_State *L);
static int32_t apiEmitterSetTexture(lua_State *L);
static int32_t apiEmitterSetTrail(lua_State *L);
static int32_t apiEmitterStart(lua_State *L);
static int32_t apiEmitterStop(lua_State *L);
static int32_t apiFontLoad(lua_State *L);
static int32_t apiFontPrint(lua_State *L);
static int32_t apiFontQuality(lua_State *L);
static int32_t apiFontSelect(lua_State *L);
static int32_t apiFontToSprite(lua_State *L);
static int32_t apiFontUnload(lua_State *L);
static int32_t 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 apiLineDraw(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 apiMaterialSetEmissiveMap(lua_State *L);
static int32_t apiMaterialSetFilter(lua_State *L);
static int32_t apiMaterialSetMetallic(lua_State *L);
static int32_t apiMaterialSetMetallicRoughnessMap(lua_State *L);
static int32_t apiMaterialSetNormalMap(lua_State *L);
static int32_t apiMaterialSetOcclusionMap(lua_State *L);
static int32_t apiMaterialSetRoughness(lua_State *L);
static int32_t apiMaterialSetTexture(lua_State *L);
static int32_t apiMaterialSetTiling(lua_State *L);
static int32_t apiMaterialSetUnlit(lua_State *L);
static int32_t apiMaterialSetVideo(lua_State *L);
static int32_t apiMaterialSetView(lua_State *L);
static int32_t apiMeshBox(lua_State *L);
static int32_t apiMeshCone(lua_State *L);
static int32_t apiMeshCylinder(lua_State *L);
static int32_t apiMeshDelete(lua_State *L);
static int32_t apiMeshHeightmap(lua_State *L);
static int32_t apiMeshNew(lua_State *L);
static int32_t apiMeshPlane(lua_State *L);
static int32_t apiMeshSphere(lua_State *L);
static int32_t apiMeshTorus(lua_State *L);
static int32_t 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 apiNavAddNode(lua_State *L);
static int32_t apiNavAgentDelete(lua_State *L);
static int32_t apiNavAgentGetVelocity(lua_State *L);
static int32_t apiNavAgentIsArrived(lua_State *L);
static int32_t apiNavAgentMoveTo(lua_State *L);
static int32_t apiNavAgentNew(lua_State *L);
static int32_t apiNavAgentSetPlayer(lua_State *L);
static int32_t apiNavAgentStop(lua_State *L);
static int32_t apiNavBuild(lua_State *L);
static int32_t apiNavDelete(lua_State *L);
static int32_t apiNavDraw(lua_State *L);
static int32_t apiNavLoad(lua_State *L);
static int32_t apiNavNearest(lua_State *L);
static int32_t apiNavNew(lua_State *L);
static int32_t apiNavPath(lua_State *L);
static int32_t apiNavRandomPoint(lua_State *L);
static int32_t apiNavRaycast(lua_State *L);
static int32_t apiNavSave(lua_State *L);
static int32_t apiNodeDelete(lua_State *L);
static int32_t apiNodeFind(lua_State *L);
static int32_t apiNodeGetChildren(lua_State *L);
static int32_t apiNodeGetMorph(lua_State *L);
static int32_t apiNodeGetMorphs(lua_State *L);
static int32_t apiNodeGetName(lua_State *L);
static int32_t apiNodeGetParent(lua_State *L);
static int32_t apiNodeGetPosition(lua_State *L);
static int32_t apiNodeGetQuaternion(lua_State *L);
static int32_t apiNodeGetRotation(lua_State *L);
static int32_t apiNodeGetScale(lua_State *L);
static int32_t apiNodeGetWorldPosition(lua_State *L);
static int32_t apiNodeLookAt(lua_State *L);
static int32_t apiNodeMove(lua_State *L);
static int32_t apiNodeNew(lua_State *L);
static int32_t apiNodeRotate(lua_State *L);
static int32_t apiNodeSetBillboard(lua_State *L);
static int32_t apiNodeSetMaterial(lua_State *L);
static int32_t apiNodeSetMesh(lua_State *L);
static int32_t apiNodeSetMorph(lua_State *L);
static int32_t apiNodeSetName(lua_State *L);
static int32_t apiNodeSetParent(lua_State *L);
static int32_t apiNodeSetPosition(lua_State *L);
static int32_t apiNodeSetQuaternion(lua_State *L);
static int32_t apiNodeSetRotation(lua_State *L);
static int32_t apiNodeSetScale(lua_State *L);
static int32_t apiNodeSetShadow(lua_State *L);
static int32_t apiNodeSetSprite(lua_State *L);
static int32_t apiNodeSetSpriteFrame(lua_State *L);
static int32_t apiNodeSetText(lua_State *L);
static int32_t apiNodeSetVisible(lua_State *L);
static int32_t 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 apiPhysicsSet2D(lua_State *L);
static int32_t apiPhysicsSetDebug(lua_State *L);
static int32_t apiPhysicsSetEnabled(lua_State *L);
static int32_t apiPhysicsSetGravity(lua_State *L);
static int32_t apiPlayerDelete(lua_State *L);
static int32_t apiPlayerGetGround(lua_State *L);
static int32_t apiPlayerGetVelocity(lua_State *L);
static int32_t apiPlayerIsOnGround(lua_State *L);
static int32_t apiPlayerIsSwimming(lua_State *L);
static int32_t apiPlayerJump(lua_State *L);
static int32_t apiPlayerMove(lua_State *L);
static int32_t apiPlayerNew(lua_State *L);
static int32_t apiPlayerSetEnabled(lua_State *L);
static int32_t apiPlayerSetGravityScale(lua_State *L);
static int32_t apiPlayerSetMass(lua_State *L);
static int32_t apiPlayerSetPosition(lua_State *L);
static int32_t apiPlayerSetPush(lua_State *L);
static int32_t apiPlayerSetSlope(lua_State *L);
static int32_t apiPlayerSetStep(lua_State *L);
static int32_t apiPlayerSetSwim(lua_State *L);
static int32_t apiPlayerSetVelocity(lua_State *L);
static int32_t apiRagdollActivate(lua_State *L);
static int32_t apiRagdollApplyImpulse(lua_State *L);
static int32_t apiRagdollDeactivate(lua_State *L);
static int32_t apiRagdollDelete(lua_State *L);
static int32_t apiRagdollIsActive(lua_State *L);
static int32_t apiRagdollIsResting(lua_State *L);
static int32_t apiRagdollNew(lua_State *L);
static int32_t apiRagdollSetJoint(lua_State *L);
static int32_t apiRagdollSetStrength(lua_State *L);
static int32_t apiSceneEnable(lua_State *L);
static int32_t apiSceneGetSize(lua_State *L);
static int32_t apiSceneGetStats(lua_State *L);
static int32_t apiSceneProject(lua_State *L);
static int32_t apiSceneSetAmbient(lua_State *L);
static int32_t apiSceneSetAntialias(lua_State *L);
static int32_t apiSceneSetBackground(lua_State *L);
static int32_t apiSceneSetBloom(lua_State *L);
static int32_t apiSceneSetEnvironment(lua_State *L);
static int32_t apiSceneSetExposure(lua_State *L);
static int32_t apiSceneSetFog(lua_State *L);
static int32_t apiSceneSetShadowCascades(lua_State *L);
static int32_t apiSceneSetShadowDistance(lua_State *L);
static int32_t apiSceneSetShadowSize(lua_State *L);
static int32_t apiSceneSetSky(lua_State *L);
static int32_t apiSceneSetSkyIntensity(lua_State *L);
static int32_t apiSceneSetTonemap(lua_State *L);
static int32_t apiSceneUnproject(lua_State *L);
static int32_t 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 apiSingeReload(lua_State *L);
static int32_t apiSingeScreenshot(lua_State *L);
static int32_t apiSingeSetAudioCalibration(lua_State *L);
static int32_t apiSingeSetAudioDelay(lua_State *L);
static int32_t apiSingeSetGameName(lua_State *L);
static int32_t apiSingeSetPauseFlag(lua_State *L);
static int32_t apiSingeSetPauseKeyEnabled(lua_State *L);
static int32_t apiSingeVersion(lua_State *L);
static int32_t apiSingeWantsCrosshairs(lua_State *L);
static int32_t apiSoftDelete(lua_State *L);
static int32_t apiSoftNew(lua_State *L);
static int32_t apiSoftPin(lua_State *L);
static int32_t apiSoftSetDamping(lua_State *L);
static int32_t apiSoftSetMass(lua_State *L);
static int32_t apiSoftSetPressure(lua_State *L);
static int32_t apiSoftSetStiffness(lua_State *L);
static int32_t apiSoftUnpin(lua_State *L);
static int32_t apiSoundFullStop(lua_State *L);
static int32_t apiSoundGetPosition(lua_State *L);
static int32_t apiSoundGetVolume(lua_State *L);
static int32_t apiSoundIsPlaying(lua_State *L);
static int32_t apiSoundLoad(lua_State *L);
static int32_t apiSoundPause(lua_State *L);
static int32_t apiSoundPlay(lua_State *L);
static int32_t apiSoundResume(lua_State *L);
static int32_t apiSoundSetListener(lua_State *L);
static int32_t apiSoundSetNode(lua_State *L);
static int32_t apiSoundSetPan(lua_State *L);
static int32_t apiSoundSetPosition(lua_State *L);
static int32_t apiSoundSetRange(lua_State *L);
static int32_t apiSoundSetVolume(lua_State *L);
static int32_t apiSoundStop(lua_State *L);
static int32_t apiSoundUnload(lua_State *L);
static int32_t 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 apiTerrainGetHeight(lua_State *L);
static int32_t apiVehicleAddWheel(lua_State *L);
static int32_t apiVehicleDelete(lua_State *L);
static int32_t apiVehicleDrive(lua_State *L);
static int32_t apiVehicleGetGear(lua_State *L);
static int32_t apiVehicleGetRpm(lua_State *L);
static int32_t apiVehicleGetSpeed(lua_State *L);
static int32_t apiVehicleGetWheelSlip(lua_State *L);
static int32_t apiVehicleIsWheelOnGround(lua_State *L);
static int32_t apiVehicleNew(lua_State *L);
static int32_t apiVehicleSetAntiRoll(lua_State *L);
static int32_t apiVehicleSetBrakes(lua_State *L);
static int32_t apiVehicleSetEngine(lua_State *L);
static int32_t apiVehicleSetGears(lua_State *L);
static int32_t apiVehicleSetSteering(lua_State *L);
static int32_t apiVehicleSetRudder(lua_State *L);
static int32_t apiVehicleSetSuspension(lua_State *L);
static int32_t apiVehicleSetThrust(lua_State *L);
static int32_t apiVehicleSetWheel(lua_State *L);
static int32_t apiVideoDraw(lua_State *L);
static int32_t apiVideoGetAudioTrack(lua_State *L);
static int32_t apiVideoGetAudioTracks(lua_State *L);
static int32_t apiVideoGetFrame(lua_State *L);
static int32_t apiVideoGetFrameCount(lua_State *L);
static int32_t apiVideoGetHeight(lua_State *L);
static int32_t apiVideoGetLanguage(lua_State *L);
static int32_t apiVideoGetLanguageDescription(lua_State *L);
static int32_t apiVideoGetVolume(lua_State *L);
static int32_t apiVideoGetWidth(lua_State *L);
static int32_t apiVideoIsPlaying(lua_State *L);
static int32_t apiVideoLoad(lua_State *L);
static int32_t apiVideoPause(lua_State *L);
static int32_t apiVideoPlay(lua_State *L);
static int32_t apiVideoQuality(lua_State *L);
static int32_t apiVideoRotate(lua_State *L);
static int32_t apiVideoRotateAndScale(lua_State *L);
static int32_t apiVideoScale(lua_State *L);
static int32_t apiVideoSeek(lua_State *L);
static int32_t apiVideoSetAudioTrack(lua_State *L);
static int32_t apiVideoSetVolume(lua_State *L);
static int32_t apiVideoUnload(lua_State *L);
static int32_t apiViewDelete(lua_State *L);
static int32_t apiViewNew(lua_State *L);
static int32_t apiViewSetCamera(lua_State *L);
static int32_t apiVldpGetPixel(lua_State *L);
static int32_t apiVldpSetVerbose(lua_State *L);
#define MODL(name, array) { name, { (const char *)array }, sizeof(array) }
#define MODC(name, openf) { name, { (const char *)openf }, 0 }
// Lua Modules
static const LuaModuleT _luaModules[] = {
// LuaFileSystem
MODC("lfs", _luaopenLfs),
// 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),
};
// ===== 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;
}
// An animation by name or by its number from 1, as modelGetAnimations lists them.
static int32_t _argAnimation(lua_State *L, const char *method, int32_t model, int32_t index) {
int32_t animation;
if (lua_type(L, index) == LUA_TSTRING) {
animation = modelAnimationIndex(model, lua_tostring(L, index));
if (animation < 0) {
_luaDie(L, method, "Model %d has no animation named %s.", model, lua_tostring(L, index));
}
return animation;
}
return _argInteger(L, method, index) - 1;
}
// A layer number above the base, 1 to ANIMATION_LAYERS.
static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t index) {
int32_t layer = _argInteger(L, method, index);
if ((layer < 1) || (layer > ANIMATION_LAYERS)) {
_luaDie(L, method, "Layer must be 1 to %d: %d", ANIMATION_LAYERS, layer);
}
return layer;
}
static bool _argBoolean(lua_State *L, const char *method, int32_t index) {
if (!lua_isboolean(L, index)) {
_luaDie(L, method, "Argument %d must be a boolean.", index);
}
return lua_toboolean(L, index) != 0;
}
// An effect channel number as soundPlay returned it.
static int32_t _argChannel(lua_State *L, const char *method, int32_t index) {
int32_t channel = _argInteger(L, method, index);
if ((channel < 0) || (channel >= EFFECT_TRACKS)) {
_luaDie(L, method, "Invalid channel: %d", channel);
}
return channel;
}
// A colour component argument, clamped to 0..255.
static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) {
return (uint8_t)SDL_clamp(_argInteger(L, method, index), 0, COLOR_BYTE_MAX);
}
// 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;
}
// A handle argument the given test accepts, named for the message: "No material 7."
static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun) {
int32_t handle = _argInteger(L, method, index);
if (!valid(handle)) {
_luaDie(L, method, "No %s %d.", noun, handle);
}
return handle;
}
static int32_t _argInteger(lua_State *L, const char *method, int32_t index) {
return (int32_t)_argNumber(L, method, index);
}
// A morph target of a node's mesh, by name or by number from 1, checked.
static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t index) {
int32_t target;
if (lua_type(L, index) == LUA_TSTRING) {
target = meshFindMorph(nodeGetMesh(node), lua_tostring(L, index));
if (target < 0) {
_luaDie(L, method, "Node %d has no morph target named %s.", node, lua_tostring(L, index));
}
return target;
}
target = _argInteger(L, method, index) - 1;
if ((target < 0) || (target >= nodeGetMorphCount(node))) {
_luaDie(L, method, "Node %d has no morph target %d.", node, target + 1);
}
return target;
}
// A node that carries a physics body, checked.
static int32_t _argBody(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, bodyExists, "body");
}
// An emitter handle argument, checked.
static int32_t _argEmitter(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, emitterValid, "emitter");
}
// A texture argument for a material map: a loaded sprite (its surface comes back), or the name of a
// KTX2 file (transcoded into ktx2, true comes back and the caller frees it).
static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Surface **surface, Ktx2ImageT *ktx2) {
const char *name;
char *bytes;
size_t size = 0;
bool ok;
*surface = NULL;
memset(ktx2, 0, sizeof(*ktx2));
if (lua_type(L, index) != LUA_TSTRING) {
*surface = _argSprite(L, method, index)->originalSurface;
return false;
}
name = lua_tostring(L, index);
bytes = vfsRead(name, &size);
if (bytes == NULL) {
_luaDie(L, method, "Unable to read %s", name);
}
if (!ktx2Is(bytes, size)) {
free(bytes);
_luaDie(L, method, "%s is not a KTX2 texture (load other pictures with spriteLoad).", name);
}
ok = ktx2Transcode(bytes, size, sceneCompressedFormat(), ktx2);
free(bytes);
if (!ok) {
_luaDie(L, method, "%s: %s", name, SDL_GetError());
}
return true;
}
// A material handle argument, checked.
static int32_t _argMaterial(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, materialValid, "material");
}
// A mesh handle argument, checked.
static int32_t _argMesh(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, meshValid, "mesh");
}
// A navigation mesh handle from navNew or navLoad.
static int32_t _argNav(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, navValid, "navigation mesh");
}
// An agent handle from navAgentNew.
static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, navAgentValid, "navigation agent");
}
// A scene node handle argument, checked.
static int32_t _argNode(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, nodeValid, "node");
}
// A node argument carrying the given kind of thing: "Node 7 has no body."
static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun) {
int32_t node = _argNode(L, method, index);
if (!exists(node)) {
_luaDie(L, method, "Node %d has no %s.", node, noun);
}
return node;
}
// r, g, b at index onwards when the script gave them; the caller's defaults otherwise.
static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b) {
if (lua_gettop(L) < index + 2) {
return;
}
*r = _argColorByte(L, method, index);
*g = _argColorByte(L, method, index + 1);
*b = _argColorByte(L, method, index + 2);
}
// A node carrying a player, checked.
static int32_t _argPlayer(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, playerExists, "player");
}
// A node carrying a ragdoll, checked.
static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, ragdollExists, "ragdoll");
}
// A node carrying a soft body, checked.
static int32_t _argSoft(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, softExists, "soft body");
}
// A node carrying a vehicle, checked.
static int32_t _argVehicle(lua_State *L, const char *method, int32_t index) {
return _argNodeWith(L, method, index, vehicleExists, "vehicle");
}
static 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));
}
// A view handle from viewNew.
static int32_t _argView(lua_State *L, const char *method, int32_t index) {
return _argHandle(L, method, index, viewValid, "view");
}
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, container and data directory.
c = cloneConf(base);
free(c->container);
c->container = NULL;
c->disc = false;
c->isFrameFile = false;
free(c->videoFile);
c->videoFile = NULL;
free(c->dataDir);
c->dataDir = NULL;
// 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);
}
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. Room for all of them (the results reuse it) is checked once, up front.
luaL_checkstack(_global.luaContext, (int)strlen(sig) + 1, "Too many arguments");
va_start(vl, sig);
while ((*sig) && (!done)) {
switch (*sig++) {
case 'd': // Double
lua_pushnumber(_global.luaContext, va_arg(vl, double));
break;
case 'i': // Int
lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Promoted type for varargs.
break;
case 'b': // Boolean (passed as an int)
lua_pushboolean(_global.luaContext, va_arg(vl, int32_t));
break;
case 's': // String
lua_pushstring(_global.luaContext, va_arg(vl, char *));
break;
case '>':
done = true;
break;
default:
utilDie("Invalid argument option (%c)", *(sig - 1));
}
if (!done) {
narg++;
}
}
// Do the call. Script errors are fatal, like every other error in Singe.
nres = (int32_t)strlen(sig);
if (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 SOUND_CHANNEL_NONE when all are busy.
static int32_t _effectTrackFree(void) {
int32_t x = 0;
for (x = 0; x < EFFECT_TRACKS; x++) {
if (!MIX_TrackPlaying(_effectTracks[x]) && !MIX_TrackPaused(_effectTracks[x])) {
return x;
}
}
return SOUND_CHANNEL_NONE;
}
// The volume of a positioned sound at a distance: full within near, inverse distance beyond it,
// and fading out over the last fifth before far.
static float _effectGain(const EffectT *effect, float distance) {
float gain = 1.0f;
float fadeStart;
if (distance > effect->nearBy) {
gain = effect->nearBy / distance;
}
fadeStart = effect->farOff * (1.0f - SOUND_FADE_FRACTION);
if (distance > fadeStart) {
gain *= SDL_clamp((effect->farOff - distance) / (effect->farOff - fadeStart), 0.0f, 1.0f);
}
return gain;
}
// A channel about to play afresh: no position, no pan, the default range.
static void _effectReset(int32_t channel) {
EffectT *effect = &_effects[channel];
memset(effect, 0, sizeof(*effect));
effect->node = LISTENER_CAMERA;
effect->nearBy = SOUND_DEFAULT_NEAR;
effect->farOff = SOUND_DEFAULT_FAR;
effect->gain = 1.0f;
MIX_SetTrack3DPosition(_effectTracks[channel], NULL);
}
// 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;
}
}
// Liang-Barsky clip of a line to the overlay. False when none of it is on the overlay; otherwise
// the ends are moved onto it.
static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2) {
const double dx = *x2 - *x1;
const double dy = *y2 - *y1;
double t0 = 0.0;
double t1 = 1.0;
double r = 0.0;
double p[4];
double q[4];
int32_t i = 0;
// Each edge as the parametric distance to it: left, right, top, bottom.
p[0] = -dx;
q[0] = *x1;
p[1] = dx;
q[1] = (_global.overlay->w - 1) - *x1;
p[2] = -dy;
q[2] = *y1;
p[3] = dy;
q[3] = (_global.overlay->h - 1) - *y1;
for (i = 0; i < 4; i++) {
if (p[i] == 0.0) {
// Parallel to this edge: outside it means gone altogether.
if (q[i] < 0.0) {
return false;
}
continue;
}
r = q[i] / p[i];
if (p[i] < 0.0) {
if (r > t1) {
return false;
}
t0 = SDL_max(t0, r);
} else {
if (r < t0) {
return false;
}
t1 = SDL_min(t1, r);
}
}
*x2 = (int32_t)lround(*x1 + t1 * dx);
*y2 = (int32_t)lround(*y1 + t1 * dy);
*x1 = (int32_t)lround(*x1 + t0 * dx);
*y1 = (int32_t)lround(*y1 + t0 * dy);
return true;
}
// 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);
}
}
}
}
// The disc's current frame, whichever kind it is: a frame file counts across all its segments.
// Only with a disc (videoHandle >= 0).
static int64_t _discGetFrame(void) {
if (_global.conf->isFrameFile) {
return frameFileGetFrame(_global.frameFileHandle, _global.videoHandle);
}
return videoGetFrame(_global.videoHandle);
}
// Seeks the laserdisc, whichever kind it is, held within its ends (a frame file clamps inside its
// segments itself) rather than wrapping around.
static void _discSeek(int64_t frame) {
int64_t actualFrame = 0;
int64_t count = 0;
if (_global.conf->isFrameFile) {
frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &actualFrame);
} else if (_global.videoHandle >= 0) {
count = videoGetFrameCount(_global.videoHandle);
if (count > 0) {
frame = SDL_clamp(frame, 0, count - 1);
}
videoSeek(_global.videoHandle, frame);
}
}
// Splash screens: fade in the Kangaroo Punch logo, cross fade to the Singe logo, fade out.
static void _doLogos(void) {
int32_t i = 0;
int32_t w = 0;
int32_t h = 0;
bool keepGoing = true;
SDL_Surface *surfKangaroo = NULL;
SDL_Surface *surfSinge = NULL;
SDL_Texture *texKangaroo = NULL;
SDL_Texture *texSinge = NULL;
SDL_FRect rectKangaroo;
SDL_FRect rectSinge;
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED;
SDL_GetRenderLogicalPresentation(_global.renderer, &w, &h, &mode);
texKangaroo = _loadEmbeddedTexture(kangarooPunchLogo_png, kangarooPunchLogo_png_len, &surfKangaroo);
texSinge = _loadEmbeddedTexture(singeLogo_png, singeLogo_png_len, &surfSinge);
// Both logos are fitted into the same space, so the cross fade neither stretches nor jumps.
SDL_SetRenderLogicalPresentation(_global.renderer, LOGO_SPACE_WIDTH, LOGO_SPACE_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
_fitRect(surfKangaroo->w, surfKangaroo->h, LOGO_SPACE_WIDTH, LOGO_SPACE_HEIGHT, LOGO_MARGIN, &rectKangaroo);
_fitRect(surfSinge->w, surfSinge->h, LOGO_SPACE_WIDTH, LOGO_SPACE_HEIGHT, LOGO_MARGIN, &rectSinge);
// Fade in to white with Kangaroo logo
for (i = 0; keepGoing && (i < LOGO_FADE_STEPS); i++) {
SDL_SetRenderDrawColor(_global.renderer, (uint8_t)i, (uint8_t)i, (uint8_t)i, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
SDL_SetTextureAlphaMod(texKangaroo, (uint8_t)i);
SDL_RenderTexture(_global.renderer, texKangaroo, NULL, &rectKangaroo);
SDL_RenderPresent(_global.renderer);
keepGoing = _delayAndPump(LOGO_FADE_STEP_MS);
}
keepGoing = keepGoing && _delayAndPump(LOGO_HOLD_MS);
// Cross fade to Singe logo
for (i = 0; keepGoing && (i < LOGO_FADE_STEPS); i++) {
SDL_RenderClear(_global.renderer);
SDL_SetTextureAlphaMod(texKangaroo, (uint8_t)(LOGO_FADE_STEPS - 1 - i));
SDL_RenderTexture(_global.renderer, texKangaroo, NULL, &rectKangaroo);
SDL_SetTextureAlphaMod(texSinge, (uint8_t)i);
SDL_RenderTexture(_global.renderer, texSinge, NULL, &rectSinge);
SDL_RenderPresent(_global.renderer);
keepGoing = _delayAndPump(LOGO_FADE_STEP_MS);
}
keepGoing = keepGoing && _delayAndPump(LOGO_HOLD_MS);
// Fade to black
for (i = LOGO_FADE_STEPS - 1; keepGoing && (i >= 0); i--) {
SDL_SetRenderDrawColor(_global.renderer, (uint8_t)i, (uint8_t)i, (uint8_t)i, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
SDL_SetTextureAlphaMod(texSinge, (uint8_t)i);
SDL_RenderTexture(_global.renderer, texSinge, NULL, &rectSinge);
SDL_RenderPresent(_global.renderer);
keepGoing = _delayAndPump(LOGO_FADE_STEP_MS);
}
SDL_DestroyTexture(texSinge);
SDL_DestroyTexture(texKangaroo);
SDL_DestroySurface(surfSinge);
SDL_DestroySurface(surfKangaroo);
SDL_SetRenderLogicalPresentation(_global.renderer, w, h, mode);
}
// Bresenham line into the overlay, clipped to it first so a line from far off screen costs only
// its visible part. The overlay must be locked by the caller.
static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel) {
int32_t x = 0;
int32_t y = 0;
int32_t dx = 0;
int32_t dy = 0;
int32_t incX = 0;
int32_t incY = 0;
int32_t balance = 0;
if (!_clipLine(&x1, &y1, &x2, &y2)) {
return;
}
x = x1;
y = y1;
dx = abs(x2 - x1);
dy = abs(y2 - y1);
incX = (x2 >= x1) ? 1 : -1;
incY = (y2 >= y1) ? 1 : -1;
if (dx >= dy) {
dy <<= 1;
balance = dy - dx;
dx <<= 1;
while (x != x2) {
_putPixel(x, y, pixel);
if (balance >= 0) {
y += incY;
balance -= dx;
}
balance += dy;
x += incX;
}
} else {
dx <<= 1;
balance = dx - dy;
dy <<= 1;
while (y != y2) {
_putPixel(x, y, pixel);
if (balance >= 0) {
x += incX;
balance -= dy;
}
balance += dx;
y += incY;
}
}
_putPixel(x, y, pixel);
}
// Draws the 2D emitters queued this frame on one layer: a textured, tinted, rotated quad per particle
// through SDL_RenderGeometry, in overlay pixels mapped onto the window target. Every quad is built
// in one pass; with several frames the indices are bucketed by frame so each texture is one call.
static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target) {
int32_t emitters[PARTICLES_QUEUE_MAX];
int32_t n = particlesQueued2D(layer, emitters, (int32_t)SDL_arraysize(emitters));
float scaleX = target->w / (float)_global.overlay->w;
float scaleY = target->h / (float)_global.overlay->h;
SDL_Vertex *vertices = NULL;
SDL_Vertex *v = NULL;
int32_t *indices = NULL;
int32_t *starts = NULL;
ParticleTexturesT *cache = NULL;
ParticleViewT *particle = NULL;
EmitterViewT view;
SDL_FColor color;
float c = 0.0f;
float s = 0.0f;
float half = 0.0f;
float dx = 0.0f;
float dy = 0.0f;
int32_t begin = 0;
int32_t count = 0;
int32_t e = 0;
int32_t i = 0;
int32_t f = 0;
int32_t frame = 0;
for (e = 0; e < n; e++) {
if (!particlesViewEmitter(emitters[e], &view) || (view.count == 0) || (view.frameCount == 0)) {
continue;
}
cache = _particleTexturesGet(&view);
if (cache->blend != view.blend) {
for (f = 0; f < cache->count; f++) {
SDL_SetTextureBlendMode(cache->textures[f], (view.blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND);
}
cache->blend = view.blend;
}
_quadScratch(view.count, cache->count);
vertices = _global.quadVertices;
indices = _global.quadIndices;
starts = _global.frameStarts;
memset(starts, 0, (size_t)cache->count * sizeof(int32_t));
for (i = 0; i < view.count; i++) {
particle = &view.particles[i];
v = &vertices[i * QUAD_VERTICES];
c = SDL_cosf(DEGREES_TO_RADIANS(particle->angle));
s = SDL_sinf(DEGREES_TO_RADIANS(particle->angle));
half = particle->size * 0.5f;
color.r = particle->colour[0];
color.g = particle->colour[1];
color.b = particle->colour[2];
color.a = particle->colour[3];
for (f = 0; f < QUAD_VERTICES; f++) {
dx = ((f == 1) || (f == 2)) ? half : -half;
dy = (f >= 2) ? half : -half;
v[f].position.x = target->x + (particle->position.x + dx * c - dy * s) * scaleX;
v[f].position.y = target->y + (particle->position.y + dx * s + dy * c) * scaleY;
v[f].color = color;
v[f].tex_coord.x = ((f == 1) || (f == 2)) ? 1.0f : 0.0f;
v[f].tex_coord.y = (f >= 2) ? 1.0f : 0.0f;
}
starts[SDL_clamp(particle->frame, 0, cache->count - 1)]++;
}
// Each frame's quads start where the earlier frames' end; filling advances each start to its end.
begin = 0;
for (f = 0; f < cache->count; f++) {
count = starts[f];
starts[f] = begin;
begin += count;
}
for (i = 0; i < view.count; i++) {
frame = SDL_clamp(view.particles[i].frame, 0, cache->count - 1);
_quadIndex(indices, starts[frame]++, i);
}
begin = 0;
for (f = 0; f < cache->count; f++) {
if (starts[f] > begin) {
SDL_RenderGeometry(_global.renderer, cache->textures[f], vertices, view.count * QUAD_VERTICES, indices + begin * QUAD_INDICES, (starts[f] - begin) * QUAD_INDICES);
}
begin = starts[f];
}
_drawTrails2D(&view, cache, target, scaleX, scaleY);
}
}
// The 2D emitter's trails: a ribbon of quads through each particle's recorded points, fading
// toward the tail, textured by the middle column of the first frame so the disc's edge softens it.
static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY) {
SDL_Vertex *vertices = NULL;
SDL_Vertex *v = NULL;
int32_t *indices = NULL;
const float *points = NULL;
SDL_FColor color;
int32_t segments = 0;
int32_t count = 0;
int32_t i = 0;
int32_t j = 0;
int32_t k = 0;
float half = view->trailWidth * 0.5f;
float x0 = 0.0f;
float y0 = 0.0f;
float x1 = 0.0f;
float y1 = 0.0f;
float dx = 0.0f;
float dy = 0.0f;
float length = 0.0f;
float nx = 0.0f;
float ny = 0.0f;
float side = 0.0f;
bool head = false;
if ((view->trailLength < 2) || (view->trailCounts == NULL) || (view->count == 0)) {
return;
}
_quadScratch(view->count * (view->trailLength - 1), 0);
vertices = _global.quadVertices;
indices = _global.quadIndices;
for (i = 0; i < view->count; i++) {
points = view->trailPoints + (size_t)i * (size_t)view->trailLength * TRAIL_POINT_FLOATS;
count = view->trailCounts[i];
color.r = view->particles[i].colour[0];
color.g = view->particles[i].colour[1];
color.b = view->particles[i].colour[2];
for (j = 1; j < count; j++) {
x0 = view->trailOffset.x + points[(j - 1) * TRAIL_POINT_FLOATS];
y0 = view->trailOffset.y + points[(j - 1) * TRAIL_POINT_FLOATS + 1];
x1 = view->trailOffset.x + points[j * TRAIL_POINT_FLOATS];
y1 = view->trailOffset.y + points[j * TRAIL_POINT_FLOATS + 1];
dx = x1 - x0;
dy = y1 - y0;
length = SDL_sqrtf(dx * dx + dy * dy);
nx = (length > 0.0f) ? -dy / length * half : 0.0f;
ny = (length > 0.0f) ? dx / length * half : 0.0f;
v = &vertices[segments * QUAD_VERTICES];
for (k = 0; k < QUAD_VERTICES; k++) {
head = (k == 1) || (k == 2);
side = (k >= 2) ? 1.0f : -1.0f;
v[k].position.x = target->x + ((head ? x1 : x0) + nx * side) * scaleX;
v[k].position.y = target->y + ((head ? y1 : y0) + ny * side) * scaleY;
v[k].color = color;
v[k].color.a = view->particles[i].colour[3] * (float)(head ? j : j - 1) / (float)(count - 1);
v[k].tex_coord.x = 0.5f;
v[k].tex_coord.y = (side < 0.0f) ? 0.0f : 1.0f;
}
_quadIndex(indices, segments, segments);
segments++;
}
}
if (segments > 0) {
SDL_RenderGeometry(_global.renderer, cache->textures[0], vertices, segments * QUAD_VERTICES, indices, segments * QUAD_INDICES);
}
}
// 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;
IoHookModeE mode;
} ioHooks[] = { { "input", IO_HOOK_READ }, { "lines", IO_HOOK_READ }, { "open", IO_HOOK_OPEN }, { "output", IO_HOOK_WRITE } };
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)) {
return _luaCallOriginal(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)) {
return _luaCallOriginal(L);
}
if (!vfsIsDirectory(name)) {
return luaL_error(L, "cannot open %s: No such file or directory", name);
}
list = vfsList(name, &count);
lua_createtable(L, count, 0);
for (i = 0; i < count; i++) {
lua_pushstring(L, list[i]);
lua_rawseti(L, -2, i + 1);
}
vfsListFree(list, count);
lua_pushinteger(L, 0);
lua_pushcclosure(L, _lfsDirIterator, 2);
return 1;
}
// The iterator lfs.dir returns for a packed directory: upvalues are the entry table and the index.
static int32_t _lfsDirIterator(lua_State *L) {
lua_Integer index = lua_tointeger(L, lua_upvalueindex(2)) + 1;
lua_pushinteger(L, index);
lua_copy(L, -1, lua_upvalueindex(2));
lua_pop(L, 1);
lua_rawgeti(L, lua_upvalueindex(1), index);
return 1;
}
// lfs.mkdir through the vfs: inside a packed game the directory is made where its files will be
// written, the data overlay; a plain filesystem path goes to the original (upvalue 1).
static int32_t _lfsMkdir(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
char *path = NULL;
bool ok = false;
if (vfsIsFilesystem(name)) {
return _luaCallOriginal(L);
}
path = vfsFilePath(name, true);
if (path == NULL) {
lua_pushnil(L);
lua_pushfstring(L, "%s reaches outside the game", name);
return 2;
}
ok = utilMkDirP(path, DIRECTORY_MODE);
free(path);
if (!ok) {
lua_pushnil(L);
lua_pushfstring(L, "cannot create %s", name);
return 2;
}
lua_pushboolean(L, true);
return 1;
}
// lfs.rmdir through the vfs: removes the overlay directory of a packed name; the packed copy
// itself is read only and stays.
static int32_t _lfsRmdir(lua_State *L) {
const char *name = luaL_checkstring(L, 1);
char *path = NULL;
bool ok = false;
if (vfsIsFilesystem(name)) {
return _luaCallOriginal(L);
}
path = vfsFilePath(name, true);
if (path == NULL) {
lua_pushnil(L);
lua_pushfstring(L, "%s reaches outside the game", name);
return 2;
}
ok = (rmdir(path) == 0);
free(path);
if (!ok) {
lua_pushnil(L);
lua_pushfstring(L, "cannot remove %s", name);
return 2;
}
lua_pushboolean(L, true);
return 1;
}
// The 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;
}
// The built-in controls.cfg, then every override in turn (the working directory, above and inside
// the data directory, beside the script), each place only once, in a throwaway Lua state that has
// the framework but not the API. Leaves the dead zone and the switch mappings behind.
static void _loadControlMappings(void) {
lua_State *L = NULL;
char *scriptDir = utilGetUpToLastPathComponent(_global.conf->scriptFile);
char *candidates[4];
bool seen = false;
int32_t c = 0;
int32_t x = 0;
int32_t y = 0;
_progTrace("Creating Lua context for Singe setup");
L = luaL_newstate();
_global.luaContext = L;
_startLuaContext(L);
// Load framework - NOTE! SINGE API NOT AVAILABLE AT THIS POINT!
// Any calls in the framework need to be wrapped with nil checks!
_progTrace("Loading Singe framework");
if (luaL_loadbuffer(L, (const char *)Framework_singe, Framework_singe_len, "Framework.singe") || lua_pcall(L, 0, 0, 0)) {
utilDie("%s", lua_tostring(L, -1));
}
_progTrace("Loading default control mappings");
if (luaL_loadbuffer(L, (const char *)controls_cfg, controls_cfg_len, CONTROLS_FILE) || lua_pcall(L, 0, 0, 0)) {
utilDie("%s", lua_tostring(L, -1));
}
candidates[0] = strdup(CONTROLS_FILE);
candidates[1] = utilCreateString("%s..%c%s", _global.conf->dataDir, utilGetPathSeparator(), CONTROLS_FILE);
candidates[2] = utilCreateString("%s%s", _global.conf->dataDir, CONTROLS_FILE);
candidates[3] = utilCreateString("%s%s", scriptDir, CONTROLS_FILE);
for (c = 0; c < (int32_t)SDL_arraysize(candidates); c++) {
// Without -d the data directory is the script's, so the same file would run twice.
seen = false;
for (x = 0; x < c; x++) {
if (strcmp(candidates[x], candidates[c]) == 0) {
seen = true;
}
}
if (!seen) {
_loadControlsFile(candidates[c]);
}
}
for (c = 0; c < (int32_t)SDL_arraysize(candidates); c++) {
free(candidates[c]);
}
free(scriptDir);
// Parse results
lua_getglobal(L, "DEAD_ZONE");
if (lua_isnumber(L, -1)) {
_global.controllerDeadZone = (int32_t)lua_tonumber(L, -1);
}
lua_pop(L, 1);
_progTrace("Controller dead zone is %d", _global.controllerDeadZone);
for (x = 0; x < INPUT_COUNT; x++) {
// Each INPUT_* table holds { name = ..., value = ... } entries; collect the values.
lua_getglobal(L, _inputNames[x].configName);
if (!lua_istable(L, -1)) {
utilSay("Configuration option %s missing!", _inputNames[x].configName);
lua_pop(L, 1);
continue;
}
y = (int32_t)lua_rawlen(L, -1);
_global.controlMappings[x].input = (int32_t *)calloc((size_t)(y + 1), sizeof(int32_t));
if (!_global.controlMappings[x].input) {
utilDie("Unable to allocate memory for control mappings.");
}
_global.controlMappings[x].inputCount = 0;
lua_pushnil(L);
while (lua_next(L, -2)) {
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "value");
if (lua_isnumber(L, -1) && (_global.controlMappings[x].inputCount < y)) {
_global.controlMappings[x].input[_global.controlMappings[x].inputCount++] = (int32_t)lua_tonumber(L, -1);
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_close(L);
_global.luaContext = NULL;
}
// Runs a controls.cfg if it exists.
static void _loadControlsFile(const char *path) {
if (vfsExists(path)) {
_progTrace("Loading %s", path);
if (_luaLoadFile(_global.luaContext, path, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, 0)) {
utilDie("%s", lua_tostring(_global.luaContext, -1));
}
}
}
static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length) {
SDL_Surface *surface = IMG_LoadTyped_IO(SDL_IOFromConstMem(data, length), true, "PNG");
if (surface == NULL) {
utilDie("%s", SDL_GetError());
}
_surfaceUnpack(&surface);
return surface;
}
// Loads an embedded PNG as a texture. The surface stays alive so callers can read its size.
static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface) {
SDL_Texture *texture = NULL;
*surface = _loadEmbeddedPng(data, length);
texture = SDL_CreateTextureFromSurface(_global.renderer, *surface);
if (texture == NULL) {
utilDie("%s", SDL_GetError());
}
return texture;
}
// Hands a hooked call to the original function kept as upvalue 1, returning all it returns.
static int32_t _luaCallOriginal(lua_State *L) {
lua_pushvalue(L, lua_upvalueindex(1));
lua_insert(L, 1);
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
return lua_gettop(L);
}
// Reports a script level error with the calling Lua line and exits.
static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) {
va_list args;
char *message = NULL;
va_start(args, fmt);
message = _luaFormat(L, method, fmt, args);
va_end(args);
if (_global.conf->scriptTracing) {
utilTrace("%s", message);
}
utilDie("%s", message);
}
// dofile(name) through the vfs; without a name the original reads stdin.
static int32_t _luaDofile(lua_State *L) {
const char *name = luaL_optstring(L, 1, NULL);
if (name == NULL) {
return _luaCallOriginal(L);
}
lua_settop(L, 1);
if (_luaLoadFile(L, name, NULL, _global.conf->reload) != LUA_OK) {
return lua_error(L);
}
lua_call(L, 0, LUA_MULTRET);
return lua_gettop(L) - 1;
}
// require() of a game's own module: name.lua, name/init.lua, or name.singe under the script's
// directory, then relative to the game root.
static int32_t _luaFileSearcher(lua_State *L) {
static const char *const patterns[] = { "%s%s.lua", "%s%s/init.lua", "%s%s.singe", NULL };
char *module = strdup(lua_tostring(L, 1));
char *scriptDir = utilGetUpToLastPathComponent(_global.conf->scriptFile);
const char *prefixes[2];
char *name = NULL;
char *p = NULL;
int32_t x = 0;
int32_t y = 0;
prefixes[0] = scriptDir;
prefixes[1] = "";
for (p = module; *p != 0; p++) {
if (*p == '.') {
*p = '/';
}
}
for (y = 0; y < 2; y++) {
for (x = 0; patterns[x] != NULL; x++) {
name = utilCreateString(patterns[x], prefixes[y], module);
if (vfsExists(name)) {
if (_luaLoadFile(L, name, NULL, _global.conf->reload) != LUA_OK) {
lua_pushfstring(L, "error loading module '%s' from file '%s':\n\t%s", lua_tostring(L, 1), name, lua_tostring(L, -1));
free(name);
free(module);
free(scriptDir);
return lua_error(L);
}
lua_pushstring(L, name);
free(name);
free(module);
free(scriptDir);
return 2;
}
free(name);
}
}
lua_pushfstring(L, "\n\tno file '%s%s.lua' in the game", scriptDir, module);
free(module);
free(scriptDir);
return 1;
}
// Formats "line:method: message" for tracing and errors. Caller frees.
static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args) {
lua_Debug ar;
int32_t line = 0;
char *body = NULL;
char *message = NULL;
if (lua_getstack(L, 1, &ar) && lua_getinfo(L, "Sl", &ar)) {
line = ar.currentline;
}
body = utilCreateStringVArgs(fmt, args);
if (!body) {
utilDie("Unable to allocate trace string.");
}
message = utilCreateString("%d:%s: %s", line, method, body);
if (!message) {
utilDie("Unable to allocate trace string.");
}
free(body);
return message;
}
// io.open and friends with a name resolved through the vfs. Upvalues: the original, and how the name is used.
static int32_t _luaIoHook(lua_State *L) {
IoHookModeE mode = (IoHookModeE)lua_tointeger(L, lua_upvalueindex(2));
const char *modeString = NULL;
char *path = NULL;
bool writing = (mode == IO_HOOK_WRITE);
if (lua_type(L, 1) == LUA_TSTRING) {
if (mode == IO_HOOK_OPEN) {
modeString = luaL_optstring(L, 2, "r");
writing = (strpbrk(modeString, "wa+") != NULL);
}
path = vfsFilePath(lua_tostring(L, 1), writing);
if (path == NULL) {
// A name inside a packed game that resolves nowhere: io.open reports it the Lua way, the others raise.
if (mode != IO_HOOK_OPEN) {
return luaL_error(L, "%s reaches outside the game", lua_tostring(L, 1));
}
lua_pushnil(L);
lua_pushfstring(L, "%s reaches outside the game", lua_tostring(L, 1));
return 2;
}
lua_pushstring(L, path);
lua_replace(L, 1);
free(path);
}
return _luaCallOriginal(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) {
return _luaCallOriginal(L);
}
if (_luaLoadFile(L, name, mode, _global.conf->reload) != LUA_OK) {
lua_pushnil(L);
lua_insert(L, -2);
return 2;
}
if (!lua_isnone(L, 3)) {
lua_pushvalue(L, 3);
if (lua_setupvalue(L, -2, 1) == NULL) {
lua_pop(L, 1);
}
}
return 1;
}
// Loads a chunk from the vfs, leaving the function or an error message on the stack. Returns the
// Lua status. With watch, a loose file joins the list --reload checks.
static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bool watch) {
char *data = NULL;
char *chunkName = NULL;
size_t bytes = 0;
int32_t status = LUA_ERRFILE;
data = vfsRead(name, &bytes);
if (data == NULL) {
lua_pushfstring(L, "cannot open %s", name);
return status;
}
chunkName = utilCreateString("@%s", name);
status = luaL_loadbufferx(L, data, bytes, chunkName, mode);
free(chunkName);
free(data);
if ((status == LUA_OK) && watch) {
_watchFile(name);
}
return status;
}
// A fresh Lua state with the standard libraries, the vfs hooks, the constants and the whole API.
static void _createScriptContext(void) {
_progTrace("Creating Lua context for script");
_global.luaContext = luaL_newstate();
_startLuaContext(_global.luaContext);
_registerApi(_global.luaContext);
}
// 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;
}
// Lua panic handler: something went wrong outside a protected call.
static int32_t _luaPanic(lua_State *L) {
lua_Debug ar;
int32_t level = 0;
utilSay("Singe has panicked! Very bad!");
utilSay("Error: %s", lua_tostring(L, -1));
utilSay("Stack trace:");
while (lua_getstack(L, level, &ar) != 0) {
lua_getinfo(L, "nSl", &ar);
utilSay(" %d: function `%s' at line %d %s", level, ar.name ? ar.name : "?", ar.currentline, ar.short_src);
level++;
}
utilSay("Trace complete.");
return 0;
}
// package.searchers entry serving the embedded Lua modules.
// https://leiradel.github.io/2020/03/01/Embedding-Lua-Modules.html
static int32_t _luaSearcher(lua_State *L) {
const char *modname = lua_tostring(L, 1);
size_t i = 0;
for (i = 0; i < sizeof(_luaModules) / sizeof(_luaModules[0]); i++) {
if (strcmp(modname, _luaModules[i].name) == 0) {
if (_luaModules[i].length != 0) {
// It's a Lua module, return the chunk that defines the module.
if (luaL_loadbufferx(L, _luaModules[i].source, _luaModules[i].length, modname, "t") != LUA_OK) {
return lua_error(L);
}
} else {
// It's a native module, return the native function that defines the module.
lua_pushcfunction(L, _luaModules[i].openf);
}
return 1;
}
}
// A searcher explains itself with a string when it has nothing.
lua_pushfstring(L, "\n\tno embedded module '%s'", modname);
return 1;
}
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;
}
// materialSetXxxMap(material[, image[, strength]]): a loaded sprite's surface or a KTX2 file as one
// of the material's maps; nil (or nothing) clears it. Strength is only for the normal and
// occlusion maps.
static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength) {
int32_t material = 0;
SDL_Surface *surface = NULL;
Ktx2ImageT ktx2;
bool ok = false;
float strength = 1.0f;
_argCheck(L, method, 1, hasStrength ? 3 : 2);
material = _argMaterial(L, method, 1);
if (hasStrength && (lua_gettop(L) >= 3)) {
strength = (float)_argNumber(L, method, 3);
}
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2) && _argMapImage(L, method, 2, &surface, &ktx2)) {
ok = materialSetMap(material, map, &ktx2, strength);
ktx2Free(&ktx2);
if (!ok) {
_luaDie(L, method, "Unable to upload the texture.");
}
return 0;
}
if (!materialSetMapSurface(material, map, surface, strength)) {
_luaDie(L, method, "%s", SDL_GetError());
}
return 0;
}
// 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;
}
// Replaces the overlay surface and texture (and the scene's targets) at a new size; its contents are lost.
static void _overlayResize(int32_t width, int32_t height) {
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();
}
// 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); // Every defined scancode is below the gamepad range
// Physical state is tracked even while the game is frozen.
if (down) {
_global.keyboardLastDown = scancode;
} else {
_global.keyboardLastUp = scancode;
}
if (keyboard && (scancode >= 0) && (scancode < SDL_SCANCODE_COUNT)) {
_global.keyboardState[scancode] = down;
}
_heldListUpdate(_global.physicalKeys, &_global.physicalKeyCount, down, keysym, scancode);
// Engine owned switches act on the press. Keyboard mappings only count in MODE_NORMAL, so
// full mode keeps every key for the game; gamepad and mouse buttons cannot be typed, so they
// always count.
if ((_global.keyboardMode == KEYBOARD_NORMAL) || !keyboard) {
engine = _engineSwitch(scancode);
}
if (down && _global.conf->reload && (scancode == SDL_SCANCODE_F5)) {
_global.reloadRequested = true;
}
if (down) {
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;
}
// Writes the two triangles of quad number quad (corners quad * 4 onwards) at index slot.
static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad) {
int32_t *out = indices + slot * QUAD_INDICES;
int32_t first = quad * QUAD_VERTICES;
out[0] = first + 0;
out[1] = first + 1;
out[2] = first + 2;
out[3] = first + 0;
out[4] = first + 2;
out[5] = first + 3;
}
// Vertex and index room for this many textured quads, and start slots for this many frames,
// kept between frames and grown only when a bigger emitter comes along.
static void _quadScratch(int32_t quads, int32_t frames) {
if (quads > _global.quadCapacity) {
_global.quadVertices = SDL_realloc(_global.quadVertices, (size_t)quads * QUAD_VERTICES * sizeof(SDL_Vertex));
_global.quadIndices = SDL_realloc(_global.quadIndices, (size_t)quads * QUAD_INDICES * sizeof(int32_t));
if ((_global.quadVertices == NULL) || (_global.quadIndices == NULL)) {
utilDie("Out of memory drawing particles.");
}
_global.quadCapacity = quads;
}
if (frames > _global.frameStartCapacity) {
_global.frameStarts = SDL_realloc(_global.frameStarts, (size_t)frames * sizeof(int32_t));
if (_global.frameStarts == NULL) {
utilDie("Out of memory drawing particles.");
}
_global.frameStartCapacity = frames;
}
}
// Frees the renderer textures kept for an emitter's 2D drawing.
static void _particleTexturesDestroy(ParticleTexturesT *cache) {
int32_t f = 0;
HASH_DEL(_global.particleTextures, cache);
for (f = 0; f < cache->count; f++) {
SDL_DestroyTexture(cache->textures[f]);
}
SDL_free(cache->textures);
SDL_free(cache);
}
static void _particleTexturesFree(int32_t emitter) {
ParticleTexturesT *cache = NULL;
HASH_FIND_INT(_global.particleTextures, &emitter, cache);
if (cache != NULL) {
_particleTexturesDestroy(cache);
}
}
// Every emitter's textures; the emitters themselves go with particlesQuit.
static void _particleTexturesFreeAll(void) {
ParticleTexturesT *cache = NULL;
ParticleTexturesT *temp = NULL;
HASH_ITER(hh, _global.particleTextures, cache, temp) {
_particleTexturesDestroy(cache);
}
}
// The textures for an emitter's frames, made on first use and remade when the frames change.
static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view) {
ParticleTexturesT *cache = NULL;
int32_t f = 0;
HASH_FIND_INT(_global.particleTextures, &view->id, cache);
if ((cache != NULL) && (cache->version != view->textureVersion)) {
_particleTexturesDestroy(cache);
cache = NULL;
}
if (cache != NULL) {
return cache;
}
cache = SDL_calloc(1, sizeof(ParticleTexturesT));
if (cache == NULL) {
utilDie("Out of memory for particle textures.");
}
cache->id = view->id;
cache->version = view->textureVersion;
cache->count = view->frameCount;
cache->blend = view->blend;
cache->textures = SDL_calloc((size_t)view->frameCount, sizeof(SDL_Texture *));
if (cache->textures == NULL) {
utilDie("Out of memory for particle textures.");
}
for (f = 0; f < view->frameCount; f++) {
cache->textures[f] = SDL_CreateTextureFromSurface(_global.renderer, view->frames[f]);
if (cache->textures[f] == NULL) {
utilDie("%s", SDL_GetError());
}
SDL_SetTextureScaleMode(cache->textures[f], SDL_SCALEMODE_LINEAR);
SDL_SetTextureBlendMode(cache->textures[f], (view->blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND);
}
HASH_ADD_INT(_global.particleTextures, id, cache);
return cache;
}
// onNavArrived(agent) for every agent that reached its target this frame.
static void _navCallbacks(void) {
int32_t agents[NAV_ARRIVAL_QUEUE];
int32_t count = navPollArrived(agents, (int32_t)SDL_arraysize(agents));
int32_t x = 0;
for (x = 0; x < count; x++) {
_callLua("onNavArrived", "i", agents[x]);
}
}
// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z,
// speed) and onTrigger(trigger, other, entered), when the script defines them. Physics keeps what
// one batch cannot hold, so the queue is drained until empty.
static void _physicsCallbacks(void) {
PhysicsEventT events[PHYSICS_MAX_EVENTS];
PhysicsEventT *event = NULL;
int32_t count = 0;
int32_t x = 0;
while ((count = physicsGetEvents(events, (int32_t)SDL_arraysize(events))) > 0) {
for (x = 0; x < count; x++) {
event = &events[x];
if (event->type == PHYSICS_EVENT_COLLISION) {
_callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed);
} else {
_callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0);
}
}
}
}
// Every Singe call a script may make. Comments give the version each call appeared in.
static void _registerApi(lua_State *L) {
lua_register(L, "animationGetTime", apiAnimationGetTime); // 3.00
lua_register(L, "animationIsPlaying", apiAnimationIsPlaying); // 3.00
lua_register(L, "animationPause", apiAnimationPause); // 3.00
lua_register(L, "animationPlay", apiAnimationPlay); // 3.00
lua_register(L, "animationPlayLayer", apiAnimationPlayLayer); // 3.00
lua_register(L, "animationResume", apiAnimationResume); // 3.00
lua_register(L, "animationSetLayerMask", apiAnimationSetLayerMask); // 3.00
lua_register(L, "animationSetLayerWeight", apiAnimationSetLayerWeight); // 3.00
lua_register(L, "animationSetTime", apiAnimationSetTime); // 3.00
lua_register(L, "animationStop", apiAnimationStop); // 3.00
lua_register(L, "bodyApplyForce", apiBodyApplyForce); // 3.00
lua_register(L, "bodyApplyImpulse", apiBodyApplyImpulse); // 3.00
lua_register(L, "bodyDelete", apiBodyDelete); // 3.00
lua_register(L, "bodyGetAngularVelocity", apiBodyGetAngularVelocity); // 3.00
lua_register(L, "bodyGetVelocity", apiBodyGetVelocity); // 3.00
lua_register(L, "bodyIsResting", apiBodyIsResting); // 3.00
lua_register(L, "bodyNew", apiBodyNew); // 3.00
lua_register(L, "bodySetAngularVelocity", apiBodySetAngularVelocity); // 3.00
lua_register(L, "bodySetBounce", apiBodySetBounce); // 3.00
lua_register(L, "bodySetBuoyancy", apiBodySetBuoyancy); // 3.00
lua_register(L, "bodySetCurrent", apiBodySetCurrent); // 3.00
lua_register(L, "bodySetEnabled", apiBodySetEnabled); // 3.00
lua_register(L, "bodySetFriction", apiBodySetFriction); // 3.00
lua_register(L, "bodySetMass", apiBodySetMass); // 3.00
lua_register(L, "bodySetTrigger", apiBodySetTrigger); // 3.00
lua_register(L, "bodySetVelocity", apiBodySetVelocity); // 3.00
lua_register(L, "bodySetWater", apiBodySetWater); // 3.00
lua_register(L, "cameraSet", apiCameraSet); // 3.00
lua_register(L, "cameraSetOrthographic", apiCameraSetOrthographic); // 3.00
lua_register(L, "cameraSetPerspective", apiCameraSetPerspective); // 3.00
lua_register(L, "colorBackground", apiColorBackground); // 1.xx
lua_register(L, "colorForeground", apiColorForeground); // 1.xx
lua_register(L, "controllerGetAxis", apiControllerGetAxis); // 2.00
lua_register(L, "controllerGetButton", apiControllerGetButton); // 2.10
lua_register(L, "debugPrint", apiDebugPrint); // 1.xx
lua_register(L, "discAudio", apiDiscAudio); // 1.xx
lua_register(L, "discChangeSpeed", apiDiscChangeSpeed); // 1.xx
lua_register(L, "discGetAudioTrack", apiDiscGetAudioTrack); // 2.10
lua_register(L, "discGetAudioTracks", apiDiscGetAudioTracks); // 2.10
lua_register(L, "discGetFrame", apiDiscGetFrame); // 1.xx
lua_register(L, "discGetHeight", apiDiscGetHeight); // 2.00
lua_register(L, "discGetLanguage", apiDiscGetLanguage); // 2.10
lua_register(L, "discGetState", apiDiscGetState); // 1.xx RDG
lua_register(L, "discGetWidth", apiDiscGetWidth); // 2.00
lua_register(L, "discPause", apiDiscPause); // 1.xx
lua_register(L, "discPauseAtFrame", apiDiscSearch); // 1.18 Same as discSearch.
lua_register(L, "discPlay", apiDiscPlay); // 1.xx
lua_register(L, "discSearch", apiDiscSearch); // 1.xx
lua_register(L, "discSearchBlanking", apiDiscSearchBlanking); // 1.xx
lua_register(L, "discSetAudioTrack", apiDiscSetAudioTrack); // 2.10
lua_register(L, "discSetFPS", apiDiscSetFPS); // 1.xx
lua_register(L, "discSkipBackward", apiDiscSkipBackward); // 1.xx
lua_register(L, "discSkipBlanking", apiDiscSkipBlanking); // 1.xx
lua_register(L, "discSkipForward", apiDiscSkipForward); // 1.xx
lua_register(L, "discSkipToFrame", apiDiscSkipToFrame); // 1.xx
lua_register(L, "discStepBackward", apiDiscStepBackward); // 1.xx
lua_register(L, "discStepForward", apiDiscStepForward); // 1.xx
lua_register(L, "discStop", apiDiscStop); // 1.xx
lua_register(L, "emitterBurst", apiEmitterBurst); // 3.00
lua_register(L, "emitterClear", apiEmitterClear); // 3.00
lua_register(L, "emitterDelete", apiEmitterDelete); // 3.00
lua_register(L, "emitterDraw", apiEmitterDraw); // 3.00
lua_register(L, "emitterGetCount", apiEmitterGetCount); // 3.00
lua_register(L, "emitterIsActive", apiEmitterIsActive); // 3.00
lua_register(L, "emitterNew", apiEmitterNew); // 3.00
lua_register(L, "emitterSetBlend", apiEmitterSetBlend); // 3.00
lua_register(L, "emitterSetCollide", apiEmitterSetCollide); // 3.00
lua_register(L, "emitterSetColor", apiEmitterSetColor); // 3.00
lua_register(L, "emitterSetDirection", apiEmitterSetDirection); // 3.00
lua_register(L, "emitterSetDrag", apiEmitterSetDrag); // 3.00
lua_register(L, "emitterSetFrames", apiEmitterSetFrames); // 3.00
lua_register(L, "emitterSetGravity", apiEmitterSetGravity); // 3.00
lua_register(L, "emitterSetLayer", apiEmitterSetLayer); // 3.00
lua_register(L, "emitterSetLife", apiEmitterSetLife); // 3.00
lua_register(L, "emitterSetLit", apiEmitterSetLit); // 3.00
lua_register(L, "emitterSetLocal", apiEmitterSetLocal); // 3.00
lua_register(L, "emitterSetMax", apiEmitterSetMax); // 3.00
lua_register(L, "emitterSetPosition", apiEmitterSetPosition); // 3.00
lua_register(L, "emitterSetRadius", apiEmitterSetRadius); // 3.00
lua_register(L, "emitterSetRate", apiEmitterSetRate); // 3.00
lua_register(L, "emitterSetSize", apiEmitterSetSize); // 3.00
lua_register(L, "emitterSetSoftness", apiEmitterSetSoftness); // 3.00
lua_register(L, "emitterSetSpeed", apiEmitterSetSpeed); // 3.00
lua_register(L, "emitterSetSpin", apiEmitterSetSpin); // 3.00
lua_register(L, "emitterSetSpread", apiEmitterSetSpread); // 3.00
lua_register(L, "emitterSetTexture", apiEmitterSetTexture); // 3.00
lua_register(L, "emitterSetTrail", apiEmitterSetTrail); // 3.00
lua_register(L, "emitterStart", apiEmitterStart); // 3.00
lua_register(L, "emitterStop", apiEmitterStop); // 3.00
lua_register(L, "fontLoad", apiFontLoad); // 1.xx
lua_register(L, "fontPrint", apiFontPrint); // 1.xx
lua_register(L, "fontQuality", apiFontQuality); // 1.xx
lua_register(L, "fontSelect", apiFontSelect); // 1.xx
lua_register(L, "fontToSprite", apiFontToSprite); // 1.xx
lua_register(L, "fontUnload", apiFontUnload); // 2.00
lua_register(L, "jointBall", apiJointBall); // 3.00
lua_register(L, "jointDelete", apiJointDelete); // 3.00
lua_register(L, "jointHinge", apiJointHinge); // 3.00
lua_register(L, "jointSetLimits", apiJointSetLimits); // 3.00
lua_register(L, "jointSlider", apiJointSlider); // 3.00
lua_register(L, "keyboardGetLastDown", apiKeyboardGetLastDown); // 2.10
lua_register(L, "keyboardGetLastUp", apiKeyboardGetLastUp); // 2.10
lua_register(L, "keyboardGetMode", apiKeyboardGetMode); // 1.xx RDG
lua_register(L, "keyboardGetModifiers", apiKeyboardGetModifiers); // 2.10
lua_register(L, "keyboardIsDown", apiKeyboardIsDown); // 2.10
lua_register(L, "keyboardSetMode", apiKeyboardSetMode); // 1.xx RDG
lua_register(L, "lightNew", apiLightNew); // 3.00
lua_register(L, "lightSetColor", apiLightSetColor); // 3.00
lua_register(L, "lightSetCone", apiLightSetCone); // 3.00
lua_register(L, "lightSetIntensity", apiLightSetIntensity); // 3.00
lua_register(L, "lightSetRange", apiLightSetRange); // 3.00
lua_register(L, "lightSetShadow", apiLightSetShadow); // 3.00
lua_register(L, "lineDraw", apiLineDraw); // 3.00
lua_register(L, "materialDelete", apiMaterialDelete); // 3.00
lua_register(L, "materialNew", apiMaterialNew); // 3.00
lua_register(L, "materialSetBlend", apiMaterialSetBlend); // 3.00
lua_register(L, "materialSetColor", apiMaterialSetColor); // 3.00
lua_register(L, "materialSetDoubleSided", apiMaterialSetDoubleSided); // 3.00
lua_register(L, "materialSetEmissive", apiMaterialSetEmissive); // 3.00
lua_register(L, "materialSetEmissiveMap", apiMaterialSetEmissiveMap); // 3.00
lua_register(L, "materialSetFilter", apiMaterialSetFilter); // 3.00
lua_register(L, "materialSetMetallic", apiMaterialSetMetallic); // 3.00
lua_register(L, "materialSetMetallicRoughnessMap", apiMaterialSetMetallicRoughnessMap); // 3.00
lua_register(L, "materialSetNormalMap", apiMaterialSetNormalMap); // 3.00
lua_register(L, "materialSetOcclusionMap", apiMaterialSetOcclusionMap); // 3.00
lua_register(L, "materialSetRoughness", apiMaterialSetRoughness); // 3.00
lua_register(L, "materialSetTexture", apiMaterialSetTexture); // 3.00
lua_register(L, "materialSetTiling", apiMaterialSetTiling); // 3.00
lua_register(L, "materialSetUnlit", apiMaterialSetUnlit); // 3.00
lua_register(L, "materialSetVideo", apiMaterialSetVideo); // 3.00
lua_register(L, "materialSetView", apiMaterialSetView); // 3.00
lua_register(L, "meshBox", apiMeshBox); // 3.00
lua_register(L, "meshCone", apiMeshCone); // 3.00
lua_register(L, "meshCylinder", apiMeshCylinder); // 3.00
lua_register(L, "meshDelete", apiMeshDelete); // 3.00
lua_register(L, "meshHeightmap", apiMeshHeightmap); // 3.00
lua_register(L, "meshNew", apiMeshNew); // 3.00
lua_register(L, "meshPlane", apiMeshPlane); // 3.00
lua_register(L, "meshSphere", apiMeshSphere); // 3.00
lua_register(L, "meshTorus", apiMeshTorus); // 3.00
lua_register(L, "modelDelete", apiModelDelete); // 3.00
lua_register(L, "modelGetAnimations", apiModelGetAnimations); // 3.00
lua_register(L, "modelInstance", apiModelInstance); // 3.00
lua_register(L, "modelLoad", apiModelLoad); // 3.00
lua_register(L, "mouseGetPosition", apiMouseGetPosition); // 2.00
lua_register(L, "mouseHowMany", apiMouseHowMany); // 1.18 RDG
lua_register(L, "mouseSetCaptured", apiMouseSetCaptured); // 2.00
lua_register(L, "mouseSetEnabled", apiMouseSetEnabled); // 3.00 mouseEnable/mouseDisable are framework aliases.
lua_register(L, "mouseSetMode", apiMouseSetMode); // 1.18 RDG
lua_register(L, "navAddNode", apiNavAddNode); // 3.00
lua_register(L, "navAgentDelete", apiNavAgentDelete); // 3.00
lua_register(L, "navAgentGetVelocity", apiNavAgentGetVelocity); // 3.00
lua_register(L, "navAgentIsArrived", apiNavAgentIsArrived); // 3.00
lua_register(L, "navAgentMoveTo", apiNavAgentMoveTo); // 3.00
lua_register(L, "navAgentNew", apiNavAgentNew); // 3.00
lua_register(L, "navAgentSetPlayer", apiNavAgentSetPlayer); // 3.00
lua_register(L, "navAgentStop", apiNavAgentStop); // 3.00
lua_register(L, "navBuild", apiNavBuild); // 3.00
lua_register(L, "navDelete", apiNavDelete); // 3.00
lua_register(L, "navDraw", apiNavDraw); // 3.00
lua_register(L, "navLoad", apiNavLoad); // 3.00
lua_register(L, "navNearest", apiNavNearest); // 3.00
lua_register(L, "navNew", apiNavNew); // 3.00
lua_register(L, "navPath", apiNavPath); // 3.00
lua_register(L, "navRandomPoint", apiNavRandomPoint); // 3.00
lua_register(L, "navRaycast", apiNavRaycast); // 3.00
lua_register(L, "navSave", apiNavSave); // 3.00
lua_register(L, "nodeDelete", apiNodeDelete); // 3.00
lua_register(L, "nodeFind", apiNodeFind); // 3.00
lua_register(L, "nodeGetChildren", apiNodeGetChildren); // 3.00
lua_register(L, "nodeGetMorph", apiNodeGetMorph); // 3.00
lua_register(L, "nodeGetMorphs", apiNodeGetMorphs); // 3.00
lua_register(L, "nodeGetName", apiNodeGetName); // 3.00
lua_register(L, "nodeGetParent", apiNodeGetParent); // 3.00
lua_register(L, "nodeGetPosition", apiNodeGetPosition); // 3.00
lua_register(L, "nodeGetQuaternion", apiNodeGetQuaternion); // 3.00
lua_register(L, "nodeGetRotation", apiNodeGetRotation); // 3.00
lua_register(L, "nodeGetScale", apiNodeGetScale); // 3.00
lua_register(L, "nodeGetWorldPosition", apiNodeGetWorldPosition); // 3.00
lua_register(L, "nodeLookAt", apiNodeLookAt); // 3.00
lua_register(L, "nodeMove", apiNodeMove); // 3.00
lua_register(L, "nodeNew", apiNodeNew); // 3.00
lua_register(L, "nodeRotate", apiNodeRotate); // 3.00
lua_register(L, "nodeSetBillboard", apiNodeSetBillboard); // 3.00
lua_register(L, "nodeSetMaterial", apiNodeSetMaterial); // 3.00
lua_register(L, "nodeSetMesh", apiNodeSetMesh); // 3.00
lua_register(L, "nodeSetMorph", apiNodeSetMorph); // 3.00
lua_register(L, "nodeSetName", apiNodeSetName); // 3.00
lua_register(L, "nodeSetParent", apiNodeSetParent); // 3.00
lua_register(L, "nodeSetPosition", apiNodeSetPosition); // 3.00
lua_register(L, "nodeSetQuaternion", apiNodeSetQuaternion); // 3.00
lua_register(L, "nodeSetRotation", apiNodeSetRotation); // 3.00
lua_register(L, "nodeSetScale", apiNodeSetScale); // 3.00
lua_register(L, "nodeSetShadow", apiNodeSetShadow); // 3.00
lua_register(L, "nodeSetSprite", apiNodeSetSprite); // 3.00
lua_register(L, "nodeSetSpriteFrame", apiNodeSetSpriteFrame); // 3.00
lua_register(L, "nodeSetText", apiNodeSetText); // 3.00
lua_register(L, "nodeSetVisible", apiNodeSetVisible); // 3.00
lua_register(L, "overlayBox", apiOverlayBox); // 2.00
lua_register(L, "overlayCircle", apiOverlayCircle); // 2.00
lua_register(L, "overlayClear", apiOverlayClear); // 1.xx
lua_register(L, "overlayEllipse", apiOverlayEllipse); // 2.00
lua_register(L, "overlayGetHeight", apiOverlayGetHeight); // 1.xx
lua_register(L, "overlayGetWidth", apiOverlayGetWidth); // 1.xx
lua_register(L, "overlayLine", apiOverlayLine); // 2.00
lua_register(L, "overlayPlot", apiOverlayPlot); // 2.00
lua_register(L, "overlayPrint", apiOverlayPrint); // 1.xx
lua_register(L, "overlaySetResolution", apiOverlaySetResolution); // 2.00
lua_register(L, "physicsRaycast", apiPhysicsRaycast); // 3.00
lua_register(L, "physicsSet2D", apiPhysicsSet2D); // 3.00
lua_register(L, "physicsSetDebug", apiPhysicsSetDebug); // 3.00
lua_register(L, "physicsSetEnabled", apiPhysicsSetEnabled); // 3.00
lua_register(L, "physicsSetGravity", apiPhysicsSetGravity); // 3.00
lua_register(L, "playerDelete", apiPlayerDelete); // 3.00
lua_register(L, "playerGetGround", apiPlayerGetGround); // 3.00
lua_register(L, "playerGetVelocity", apiPlayerGetVelocity); // 3.00
lua_register(L, "playerIsOnGround", apiPlayerIsOnGround); // 3.00
lua_register(L, "playerIsSwimming", apiPlayerIsSwimming); // 3.00
lua_register(L, "playerJump", apiPlayerJump); // 3.00
lua_register(L, "playerMove", apiPlayerMove); // 3.00
lua_register(L, "playerNew", apiPlayerNew); // 3.00
lua_register(L, "playerSetEnabled", apiPlayerSetEnabled); // 3.00
lua_register(L, "playerSetGravityScale", apiPlayerSetGravityScale); // 3.00
lua_register(L, "playerSetMass", apiPlayerSetMass); // 3.00
lua_register(L, "playerSetPosition", apiPlayerSetPosition); // 3.00
lua_register(L, "playerSetPush", apiPlayerSetPush); // 3.00
lua_register(L, "playerSetSlope", apiPlayerSetSlope); // 3.00
lua_register(L, "playerSetStep", apiPlayerSetStep); // 3.00
lua_register(L, "playerSetSwim", apiPlayerSetSwim); // 3.00
lua_register(L, "playerSetVelocity", apiPlayerSetVelocity); // 3.00
lua_register(L, "ragdollActivate", apiRagdollActivate); // 3.00
lua_register(L, "ragdollApplyImpulse", apiRagdollApplyImpulse); // 3.00
lua_register(L, "ragdollDeactivate", apiRagdollDeactivate); // 3.00
lua_register(L, "ragdollDelete", apiRagdollDelete); // 3.00
lua_register(L, "ragdollIsActive", apiRagdollIsActive); // 3.00
lua_register(L, "ragdollIsResting", apiRagdollIsResting); // 3.00
lua_register(L, "ragdollNew", apiRagdollNew); // 3.00
lua_register(L, "ragdollSetJoint", apiRagdollSetJoint); // 3.00
lua_register(L, "ragdollSetStrength", apiRagdollSetStrength); // 3.00
lua_register(L, "sceneEnable", apiSceneEnable); // 3.00
lua_register(L, "sceneGetSize", apiSceneGetSize); // 3.00
lua_register(L, "sceneGetStats", apiSceneGetStats); // 3.00
lua_register(L, "sceneProject", apiSceneProject); // 3.00
lua_register(L, "sceneSetAmbient", apiSceneSetAmbient); // 3.00
lua_register(L, "sceneSetAntialias", apiSceneSetAntialias); // 3.00
lua_register(L, "sceneSetBackground", apiSceneSetBackground); // 3.00
lua_register(L, "sceneSetBloom", apiSceneSetBloom); // 3.00
lua_register(L, "sceneSetEnvironment", apiSceneSetEnvironment); // 3.00
lua_register(L, "sceneSetExposure", apiSceneSetExposure); // 3.00
lua_register(L, "sceneSetFog", apiSceneSetFog); // 3.00
lua_register(L, "sceneSetShadowCascades", apiSceneSetShadowCascades); // 3.00
lua_register(L, "sceneSetShadowDistance", apiSceneSetShadowDistance); // 3.00
lua_register(L, "sceneSetShadowSize", apiSceneSetShadowSize); // 3.00
lua_register(L, "sceneSetSky", apiSceneSetSky); // 3.00
lua_register(L, "sceneSetSkyIntensity", apiSceneSetSkyIntensity); // 3.00
lua_register(L, "sceneSetTonemap", apiSceneSetTonemap); // 3.00
lua_register(L, "sceneUnproject", apiSceneUnproject); // 3.00
lua_register(L, "scriptExecute", apiScriptExecute); // 2.00
lua_register(L, "scriptPush", apiScriptPush); // 2.00
lua_register(L, "singeGetAudioCalibration", apiSingeGetAudioCalibration); // 3.00
lua_register(L, "singeGetAudioDelay", apiSingeGetAudioDelay); // 3.00
lua_register(L, "singeGetAudioLatency", apiSingeGetAudioLatency); // 3.00
lua_register(L, "singeGetDataPath", apiSingeGetDataPath); // 2.00
lua_register(L, "singeGetHeight", apiSingeGetHeight); // 1.xx
lua_register(L, "singeGetPauseFlag", apiSingeGetPauseFlag); // 1.xx RDG
lua_register(L, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG
lua_register(L, "singeGetTicks", apiSingeGetTicks); // 3.00
lua_register(L, "singeGetWidth", apiSingeGetWidth); // 1.xx
lua_register(L, "singeQuit", apiSingeQuit); // 1.xx RDG
lua_register(L, "singeReload", apiSingeReload); // 3.00
lua_register(L, "singeScreenshot", apiSingeScreenshot); // 1.xx
lua_register(L, "singeSetAudioCalibration", apiSingeSetAudioCalibration); // 3.00
lua_register(L, "singeSetAudioDelay", apiSingeSetAudioDelay); // 3.00
lua_register(L, "singeSetGameName", apiSingeSetGameName); // 1.15 RDG
lua_register(L, "singeSetPauseFlag", apiSingeSetPauseFlag); // 1.xx RDG
lua_register(L, "singeSetPauseKeyEnabled", apiSingeSetPauseKeyEnabled); // 3.00 singeEnablePauseKey/singeDisablePauseKey are framework aliases.
lua_register(L, "singeVersion", apiSingeVersion); // 1.xx RDG
lua_register(L, "singeWantsCrosshairs", apiSingeWantsCrosshairs); // 2.00
lua_register(L, "softDelete", apiSoftDelete); // 3.00
lua_register(L, "softNew", apiSoftNew); // 3.00
lua_register(L, "softPin", apiSoftPin); // 3.00
lua_register(L, "softSetDamping", apiSoftSetDamping); // 3.00
lua_register(L, "softSetMass", apiSoftSetMass); // 3.00
lua_register(L, "softSetPressure", apiSoftSetPressure); // 3.00
lua_register(L, "softSetStiffness", apiSoftSetStiffness); // 3.00
lua_register(L, "softUnpin", apiSoftUnpin); // 3.00
lua_register(L, "soundFullStop", apiSoundFullStop); // 1.16
lua_register(L, "soundGetPosition", apiSoundGetPosition); // 3.00
lua_register(L, "soundGetVolume", apiSoundGetVolume); // 1.16
lua_register(L, "soundIsPlaying", apiSoundIsPlaying); // 1.16 RDG
lua_register(L, "soundLoad", apiSoundLoad); // 1.xx
lua_register(L, "soundPause", apiSoundPause); // 1.16 RDG
lua_register(L, "soundPlay", apiSoundPlay); // 1.xx
lua_register(L, "soundResume", apiSoundResume); // 1.16 RDG
lua_register(L, "soundSetListener", apiSoundSetListener); // 3.00
lua_register(L, "soundSetNode", apiSoundSetNode); // 3.00
lua_register(L, "soundSetPan", apiSoundSetPan); // 3.00
lua_register(L, "soundSetPosition", apiSoundSetPosition); // 3.00
lua_register(L, "soundSetRange", apiSoundSetRange); // 3.00
lua_register(L, "soundSetVolume", apiSoundSetVolume); // 1.16
lua_register(L, "soundStop", apiSoundStop); // 1.xx RDG
lua_register(L, "soundUnload", apiSoundUnload); // 2.00
lua_register(L, "spriteDraw", apiSpriteDraw); // 1.xx Handle first since 3.00.
lua_register(L, "spriteGetFrame", apiSpriteGetFrame); // 2.10
lua_register(L, "spriteGetHeight", apiSpriteGetHeight); // 2.00
lua_register(L, "spriteGetWidth", apiSpriteGetWidth); // 2.00
lua_register(L, "spriteIsPlaying", apiSpriteIsPlaying); // 2.10
lua_register(L, "spriteLoad", apiSpriteLoad); // 1.xx
lua_register(L, "spriteLoop", apiSpriteLoop); // 2.10 Handle first since 3.00.
lua_register(L, "spritePause", apiSpritePause); // 2.10
lua_register(L, "spritePlay", apiSpritePlay); // 2.10
lua_register(L, "spriteQuality", apiSpriteQuality); // 2.10 Handle first since 3.00.
lua_register(L, "spriteRotate", apiSpriteRotate); // 2.10 Handle first since 3.00.
lua_register(L, "spriteRotateAndScale", apiSpriteRotateAndScale); // 2.10 Handle first since 3.00.
lua_register(L, "spriteScale", apiSpriteScale); // 2.10 Handle first since 3.00.
lua_register(L, "spriteSetFrame", apiSpriteSetFrame); // 2.10 Handle first since 3.00.
lua_register(L, "spriteUnload", apiSpriteUnload); // 2.00
lua_register(L, "terrainGetHeight", apiTerrainGetHeight); // 3.00
lua_register(L, "vehicleAddWheel", apiVehicleAddWheel); // 3.00
lua_register(L, "vehicleDelete", apiVehicleDelete); // 3.00
lua_register(L, "vehicleDrive", apiVehicleDrive); // 3.00
lua_register(L, "vehicleGetGear", apiVehicleGetGear); // 3.00
lua_register(L, "vehicleGetRpm", apiVehicleGetRpm); // 3.00
lua_register(L, "vehicleGetSpeed", apiVehicleGetSpeed); // 3.00
lua_register(L, "vehicleGetWheelSlip", apiVehicleGetWheelSlip); // 3.00
lua_register(L, "vehicleIsWheelOnGround", apiVehicleIsWheelOnGround); // 3.00
lua_register(L, "vehicleNew", apiVehicleNew); // 3.00
lua_register(L, "vehicleSetAntiRoll", apiVehicleSetAntiRoll); // 3.00
lua_register(L, "vehicleSetBrakes", apiVehicleSetBrakes); // 3.00
lua_register(L, "vehicleSetEngine", apiVehicleSetEngine); // 3.00
lua_register(L, "vehicleSetGears", apiVehicleSetGears); // 3.00
lua_register(L, "vehicleSetSteering", apiVehicleSetSteering); // 3.00
lua_register(L, "vehicleSetRudder", apiVehicleSetRudder); // 3.00
lua_register(L, "vehicleSetSuspension", apiVehicleSetSuspension); // 3.00
lua_register(L, "vehicleSetThrust", apiVehicleSetThrust); // 3.00
lua_register(L, "vehicleSetWheel", apiVehicleSetWheel); // 3.00
lua_register(L, "videoDraw", apiVideoDraw); // 2.00
lua_register(L, "videoGetAudioTrack", apiVideoGetAudioTrack); // 2.10
lua_register(L, "videoGetAudioTracks", apiVideoGetAudioTracks); // 2.10
lua_register(L, "videoGetFrame", apiVideoGetFrame); // 2.00
lua_register(L, "videoGetFrameCount", apiVideoGetFrameCount); // 2.00
lua_register(L, "videoGetHeight", apiVideoGetHeight); // 2.00
lua_register(L, "videoGetLanguage", apiVideoGetLanguage); // 2.10
lua_register(L, "videoGetLanguageDescription", apiVideoGetLanguageDescription); // 2.10
lua_register(L, "videoGetVolume", apiVideoGetVolume); // 2.00
lua_register(L, "videoGetWidth", apiVideoGetWidth); // 2.00
lua_register(L, "videoIsPlaying", apiVideoIsPlaying); // 2.00
lua_register(L, "videoLoad", apiVideoLoad); // 2.00
lua_register(L, "videoPause", apiVideoPause); // 2.00
lua_register(L, "videoPlay", apiVideoPlay); // 2.00
lua_register(L, "videoQuality", apiVideoQuality); // 2.10
lua_register(L, "videoRotate", apiVideoRotate); // 2.10
lua_register(L, "videoRotateAndScale", apiVideoRotateAndScale); // 2.10
lua_register(L, "videoScale", apiVideoScale); // 2.10
lua_register(L, "videoSeek", apiVideoSeek); // 2.00
lua_register(L, "videoSetAudioTrack", apiVideoSetAudioTrack); // 2.10
lua_register(L, "videoSetVolume", apiVideoSetVolume); // 2.00
lua_register(L, "videoUnload", apiVideoUnload); // 2.00
lua_register(L, "viewDelete", apiViewDelete); // 3.00
lua_register(L, "viewNew", apiViewNew); // 3.00
lua_register(L, "viewSetCamera", apiViewSetCamera); // 3.00
lua_register(L, "vldpGetHeight", apiDiscGetHeight); // 1.xx Same as discGetHeight.
lua_register(L, "vldpGetPixel", apiVldpGetPixel); // 1.xx
lua_register(L, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth.
lua_register(L, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx
}
// Runs the game again from its script without leaving: every script-owned thing goes (sounds,
// the Lua state, fonts, sprites, videos, the scene, physics, navigation, particles, the overlay
// back to its default size), the engine (window, GPU device, disc, controllers) stays, and the
// script runs afresh. An error in it is traced and the game sits empty until the next reload.
static void _reloadScript(void) {
_progTrace("Reloading %s", _global.conf->scriptFile);
_global.reloadRequested = false;
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
lua_close(_global.luaContext);
_unloadScriptResources();
_subsystemsQuit();
_subsystemsInit();
_overlayResize((int32_t)(_global.canvasWidth * OVERLAY_SCALE_DEFAULT), (int32_t)(_global.canvasHeight * OVERLAY_SCALE_DEFAULT));
_resetScriptState();
_createScriptContext();
_runScript(false);
_global.refreshDisplay = true;
}
// Loads and runs the game script under the traceback handler. A failure is fatal when the game
// starts; on a reload it is reported and the game sits empty until the next one.
static void _runScript(bool fatal) {
_progTrace("Running %s", _global.conf->scriptFile);
lua_pushcfunction(_global.luaContext, _luaTraceback);
if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, -2)) {
if (fatal) {
utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1));
}
utilSay("Error running script: %s", lua_tostring(_global.luaContext, -1));
}
lua_settop(_global.luaContext, 0);
}
// 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;
}
// scriptExecute and scriptPush: the games.dat style table at argument 1 as a config, with the data
// directory the launched game will write to. Caller destroys it.
static ConfigT *_scriptConfFromTable(lua_State *L, const char *method) {
ConfigT *conf = NULL;
_argCheck(L, method, 1, 1);
if (!lua_istable(L, 1)) {
_luaDie(L, method, "Argument 1 must be a table.");
}
conf = _buildConfFromTable(L, _global.conf);
conf->dataDir = resolveDataDir(conf);
if (conf->dataDir == NULL) {
_luaDie(L, method, "Unable to create the data directory for %s.", conf->scriptFile);
}
return conf;
}
// 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, DEBUG_NONE);
lua_setglobal(L, "DEBUG_NONE");
lua_pushinteger(L, DEBUG_SHAPES);
lua_setglobal(L, "DEBUG_SHAPES");
lua_pushinteger(L, DEBUG_CONSTRAINTS);
lua_setglobal(L, "DEBUG_CONSTRAINTS");
lua_pushinteger(L, DEBUG_CONTACTS);
lua_setglobal(L, "DEBUG_CONTACTS");
lua_pushinteger(L, DEBUG_VELOCITIES);
lua_setglobal(L, "DEBUG_VELOCITIES");
lua_pushinteger(L, DEBUG_STATIC);
lua_setglobal(L, "DEBUG_STATIC");
lua_pushinteger(L, DEBUG_ALL);
lua_setglobal(L, "DEBUG_ALL");
// Particles
lua_pushinteger(L, PARTICLE_ALPHA);
lua_setglobal(L, "PARTICLE_ALPHA");
lua_pushinteger(L, PARTICLE_ADD);
lua_setglobal(L, "PARTICLE_ADD");
lua_pushinteger(L, PARTICLE_OVER);
lua_setglobal(L, "PARTICLE_OVER");
lua_pushinteger(L, PARTICLE_UNDER);
lua_setglobal(L, "PARTICLE_UNDER");
// Billboards
lua_pushinteger(L, BILLBOARD_NONE);
lua_setglobal(L, "BILLBOARD_NONE");
lua_pushinteger(L, BILLBOARD_ALL);
lua_setglobal(L, "BILLBOARD_ALL");
lua_pushinteger(L, BILLBOARD_Y);
lua_setglobal(L, "BILLBOARD_Y");
// Particle collisions
lua_pushinteger(L, COLLIDE_NONE);
lua_setglobal(L, "COLLIDE_NONE");
lua_pushinteger(L, COLLIDE_FLOOR);
lua_setglobal(L, "COLLIDE_FLOOR");
lua_pushinteger(L, COLLIDE_SCENE);
lua_setglobal(L, "COLLIDE_SCENE");
// Texture filtering
lua_pushinteger(L, FILTER_LINEAR);
lua_setglobal(L, "FILTER_LINEAR");
lua_pushinteger(L, FILTER_NEAREST);
lua_setglobal(L, "FILTER_NEAREST");
// Tone curves
lua_pushinteger(L, TONEMAP_NONE);
lua_setglobal(L, "TONEMAP_NONE");
lua_pushinteger(L, TONEMAP_NEUTRAL);
lua_setglobal(L, "TONEMAP_NEUTRAL");
lua_pushinteger(L, TONEMAP_ACES);
lua_setglobal(L, "TONEMAP_ACES");
// Vehicles
lua_pushinteger(L, VEHICLE_CAR);
lua_setglobal(L, "VEHICLE_CAR");
lua_pushinteger(L, VEHICLE_MOTORCYCLE);
lua_setglobal(L, "VEHICLE_MOTORCYCLE");
lua_pushinteger(L, VEHICLE_TANK);
lua_setglobal(L, "VEHICLE_TANK");
lua_pushinteger(L, VEHICLE_BOAT);
lua_setglobal(L, "VEHICLE_BOAT");
// Soft bodies
lua_pushinteger(L, SOFT_CLOTH);
lua_setglobal(L, "SOFT_CLOTH");
lua_pushinteger(L, SOFT_BODY);
lua_setglobal(L, "SOFT_BODY");
lua_pushinteger(L, SOFT_ROPE);
lua_setglobal(L, "SOFT_ROPE");
lua_pushinteger(L, SOUND_CHANNEL_NONE);
lua_setglobal(L, "SOUND_ERROR_INVALID");
lua_pushinteger(L, SOUND_CHANNEL_NONE);
lua_setglobal(L, "SOUND_REMOVE_HANDLE");
// Input code layout so Framework.singe can build the GAMEPAD_N and MOUSE_N tables.
lua_pushinteger(L, CODE_GAMEPAD_BASE);
lua_setglobal(L, "SINGE_GAMEPAD_BASE");
lua_pushinteger(L, CODE_GAMEPAD_STRIDE);
lua_setglobal(L, "SINGE_GAMEPAD_STRIDE");
lua_pushinteger(L, CODE_AXIS_STRIDE);
lua_setglobal(L, "SINGE_AXIS_STRIDE");
lua_pushinteger(L, CODE_GAMEPAD_BUTTON_OFFSET);
lua_setglobal(L, "SINGE_GAMEPAD_BUTTON_OFFSET");
lua_pushinteger(L, CODE_MOUSE_BASE);
lua_setglobal(L, "SINGE_MOUSE_BASE");
lua_pushinteger(L, CODE_MOUSE_STRIDE);
lua_setglobal(L, "SINGE_MOUSE_STRIDE");
lua_pushinteger(L, MAX_CONTROLLERS);
lua_setglobal(L, "SINGE_MAX_CONTROLLERS");
lua_pushinteger(L, 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 BGRA32, one uint32_t per pixel (_overlayResize
// makes it so), and must be locked by the caller.
static void _putPixel(int32_t x, int32_t y, uint32_t pixel) {
SDL_Surface *surface = _global.overlay;
uint32_t *row = NULL;
if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) {
return;
}
row = (uint32_t *)((uint8_t *)surface->pixels + (size_t)y * (size_t)surface->pitch);
row[x] = pixel;
}
// colorXxx(r, g, b[, a]) with components clamped to 0..255.
static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha) {
int32_t n = lua_gettop(L);
uint8_t value[COLOR_COMPONENTS] = { 0, 0, 0, defaultAlpha };
int32_t x = 0;
_argCheck(L, method, 3, COLOR_COMPONENTS);
for (x = 0; x < n; x++) {
value[x] = _argColorByte(L, method, x + 1);
}
color->r = value[0];
color->g = value[1];
color->b = value[2];
color->a = value[3];
_luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a);
}
// 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;
}
}
// Everything a script sets about itself, back to what a script may expect at its start: colours,
// font quality, keyboard mode, listener, pause, the effect channels and their completion queue,
// what the previous script believed was held, and --reload's file list. Keys physically down now
// were held over from before this script, so they are ignored until released. Nothing here calls
// into Lua, so it is safe between one state closing and the next opening.
static void _resetScriptState(void) {
int32_t finished[SOUND_QUEUE_SIZE];
int32_t x = 0;
for (x = 0; x < EFFECT_TRACKS; x++) {
_effectReset(x);
}
_soundQueueDrain(finished);
_global.colorForeground.r = SDL_ALPHA_OPAQUE;
_global.colorForeground.g = SDL_ALPHA_OPAQUE;
_global.colorForeground.b = SDL_ALPHA_OPAQUE;
_global.colorForeground.a = SDL_ALPHA_OPAQUE;
memset(&_global.colorBackground, 0, sizeof(_global.colorBackground));
_global.fontQuality = FONT_QUALITY_SOLID;
_global.keyboardMode = KEYBOARD_NORMAL;
_global.listenerNode = LISTENER_CAMERA;
_global.pauseEnabled = true;
_global.frozen = false;
if (_global.pauseState) {
_global.pauseState = false;
_updatePauseState();
}
memset(_global.switchHeld, 0, sizeof(_global.switchHeld));
memset(_global.axisCode, 0, sizeof(_global.axisCode));
_global.heldKeyCount = 0;
_global.physicalKeyCount = 0;
_global.keyboardLastDown = SDL_SCANCODE_UNKNOWN;
_global.keyboardLastUp = SDL_SCANCODE_UNKNOWN;
_suppressHeldInput();
for (x = 0; x < _global.watchedCount; x++) {
free(_global.watched[x].name);
}
free(_global.watched);
_global.watched = NULL;
_global.watchedCount = 0;
}
// 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 >= 0) && (_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);
}
// Takes the channels the mixer thread reported finished since the last call, under its lock.
static int32_t _soundQueueDrain(int32_t *finished) {
int32_t count = 0;
videoLockAudio();
count = _global.soundQueueCount;
memcpy(finished, _global.soundQueue, sizeof(int32_t) * (size_t)count);
_global.soundQueueCount = 0;
videoUnlockAudio();
return count;
}
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, -1, (lua_Integer)(i - 2));
lua_rawseti(L, -2, (lua_Integer)i);
}
lua_pushcfunction(L, _luaSearcher);
lua_rawseti(L, -2, 1);
lua_pushcfunction(L, _luaFileSearcher);
lua_rawseti(L, -2, 2);
lua_pop(L, 2);
}
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;
}
}
// The 3D side a script builds on: scene, physics, particles and navigation, in dependency order.
static void _subsystemsInit(void) {
sceneInit(_global.device, _global.renderer);
physicsInit();
particlesInit();
navInit();
}
static void _subsystemsQuit(void) {
_particleTexturesFreeAll();
modelQuit();
navQuit();
particlesQuit();
physicsQuit();
sceneQuit();
}
// 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;
}
}
// 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);
}
// Everything a script loads: fonts, sounds, sprites and videos, whatever it forgot to unload.
static void _unloadScriptResources(void) {
FontT *font;
FontT *fontTemp;
SoundT *sound;
SoundT *soundTemp;
SpriteT *sprite;
SpriteT *spriteTemp;
VideoT *video;
VideoT *videoTemp;
HASH_ITER(hh, _global.fontList, font, fontTemp) {
_progTrace("Unloading font handle %d", font->id);
_fontDestroy(font);
}
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);
}
}
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);
}
}
// Positioned sounds: every playing channel that sits in the scene is handed to the mixer in the
// listener's frame (the camera's view, or a chosen node's) as a unit direction, since the mixer's
// own distance curve is flat inside one unit, and its gain is the range curve here. A channel
// following a node that has gone falls back to plain playback.
static void _updateSounds(void) {
Mat4T view = sceneGetView();
Vec3T listener = vec3(0.0f, 0.0f, 0.0f);
QuatT inverse = quatIdentity();
Vec3T scale;
Vec3T world;
Vec3T relative;
MIX_Point3D point;
EffectT *effect = NULL;
float baseGain = _mixerGain(_global.effectsVolume);
float distance = 0.0f;
float gain = 0.0f;
bool useNode = false;
int32_t x = 0;
if ((_global.listenerNode != LISTENER_CAMERA) && nodeValid(_global.listenerNode) && nodeGetWorldTransform(_global.listenerNode, &listener, &inverse, &scale)) {
inverse.x = -inverse.x;
inverse.y = -inverse.y;
inverse.z = -inverse.z;
useNode = true;
}
for (x = 0; x < EFFECT_TRACKS; x++) {
effect = &_effects[x];
if (!effect->positioned) {
continue;
}
if (effect->node != LISTENER_CAMERA) {
if (!nodeValid(effect->node)) {
effect->positioned = false;
MIX_SetTrack3DPosition(_effectTracks[x], NULL);
MIX_SetTrackGain(_effectTracks[x], baseGain);
continue;
}
world = nodeGetWorldPosition(effect->node);
} else {
world = effect->position;
}
relative = useNode ? quatRotate(inverse, vec3Subtract(world, listener)) : mat4TransformPoint(view, world);
distance = vec3Length(relative);
if (distance > SOUND_DIRECTION_EPSILON) {
relative = vec3Scale(relative, 1.0f / distance);
}
gain = _effectGain(effect, distance);
// The mixer only hears about changes.
if ((gain == effect->gain) && (baseGain == effect->baseGain) && (relative.x == effect->relative[0]) && (relative.y == effect->relative[1]) && (relative.z == effect->relative[2])) {
continue;
}
effect->gain = gain;
effect->baseGain = baseGain;
effect->relative[0] = relative.x;
effect->relative[1] = relative.y;
effect->relative[2] = relative.z;
point.x = relative.x;
point.y = relative.y;
point.z = relative.z;
MIX_SetTrack3DPosition(_effectTracks[x], &point);
MIX_SetTrackGain(_effectTracks[x], baseGain * gain);
}
}
static void _videoDestroy(VideoT *video) {
HASH_DEL(_global.videoList, video);
videoUnload(video->handle);
SDL_DestroySurface(video->transformedSurface);
free(video);
}
// --reload keeps the modification time of every loose script file the game loads.
static void _watchFile(const char *name) {
int64_t size = 0;
int64_t modified = 0;
int32_t x;
if (!vfsIsFilesystem(name) || !vfsStat(name, &size, &modified)) {
_progTrace("Not watching %s (packed or unreadable)", name);
return;
}
for (x = 0; x < _global.watchedCount; x++) {
if (strcmp(_global.watched[x].name, name) == 0) {
_global.watched[x].modified = modified;
return;
}
}
_progTrace("Watching %s", name);
_global.watched = realloc(_global.watched, sizeof(WatchedFileT) * (size_t)(_global.watchedCount + 1));
if (_global.watched == NULL) {
utilDie("Out of memory watching %s", name);
}
_global.watched[_global.watchedCount].name = strdup(name);
_global.watched[_global.watchedCount].modified = modified;
_global.watchedCount++;
}
// Once a second: whether any watched file's modification time moved.
static bool _watchedChanged(void) {
uint64_t now = SDL_GetTicks();
int32_t x;
if (now < _global.watchTick + WATCH_INTERVAL_MS) {
return false;
}
_global.watchTick = now;
for (x = 0; x < _global.watchedCount; x++) {
int64_t size = 0;
int64_t modified = 0;
if (vfsStat(_global.watched[x].name, &size, &modified) && (modified != _global.watched[x].modified)) {
_progTrace("%s changed", _global.watched[x].name);
return true;
}
}
return false;
}
// ===== 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 [, fade]]]): on a model instance's root node
static int32_t apiAnimationPlay(lua_State *L) {
int32_t root;
int32_t model;
int32_t index;
bool loop = true;
float speed = 1.0f;
float fade = 0.0f;
_argCheck(L, "animationPlay", 2, 5);
root = _argNode(L, "animationPlay", 1);
model = modelRootOf(root);
if (model < 0) {
_luaDie(L, "animationPlay", "Node %d is not a model instance.", root);
}
index = _argAnimation(L, "animationPlay", model, 2);
if (lua_gettop(L) >= 3) {
loop = _argBoolean(L, "animationPlay", 3);
}
if (lua_gettop(L) >= 4) {
speed = (float)_argNumber(L, "animationPlay", 4);
}
if (lua_gettop(L) >= 5) {
fade = (float)_argNumber(L, "animationPlay", 5);
}
if (!animationPlay(root, index, loop, speed, fade)) {
_luaDie(L, "animationPlay", "Model %d has no animation %d.", model, index + 1);
}
_luaTrace(L, "animationPlay", "node %d animation %d%s x%.2f fade %.2f", root, index + 1, loop ? " looping" : "", speed, fade);
return 0;
}
// animationPlayLayer(node, layer, nameOrIndex [, loop [, speed [, fade]]]): a clip blended over the base
static int32_t apiAnimationPlayLayer(lua_State *L) {
int32_t root;
int32_t model;
int32_t layer;
int32_t index;
bool loop = true;
float speed = 1.0f;
float fade = 0.0f;
_argCheck(L, "animationPlayLayer", 3, 6);
root = _argNode(L, "animationPlayLayer", 1);
model = modelRootOf(root);
if (model < 0) {
_luaDie(L, "animationPlayLayer", "Node %d is not a model instance.", root);
}
layer = _argAnimationLayer(L, "animationPlayLayer", 2);
index = _argAnimation(L, "animationPlayLayer", model, 3);
if (lua_gettop(L) >= 4) {
loop = _argBoolean(L, "animationPlayLayer", 4);
}
if (lua_gettop(L) >= 5) {
speed = (float)_argNumber(L, "animationPlayLayer", 5);
}
if (lua_gettop(L) >= 6) {
fade = (float)_argNumber(L, "animationPlayLayer", 6);
}
if (!animationPlayLayer(root, layer, index, loop, speed, fade)) {
_luaDie(L, "animationPlayLayer", "Model %d has no animation %d.", model, index + 1);
}
return 0;
}
// animationResume(node)
static int32_t apiAnimationResume(lua_State *L) {
int32_t root;
_argCheck(L, "animationResume", 1, 1);
root = _argNode(L, "animationResume", 1);
if (!animationResume(root)) {
_luaDie(L, "animationResume", "Node %d has no animation to resume.", root);
}
return 0;
}
// animationSetLayerMask(node, layer [, maskNode]): limits the layer to the subtree under maskNode; none lifts it
static int32_t apiAnimationSetLayerMask(lua_State *L) {
int32_t root;
int32_t layer;
int32_t mask = ANIMATION_NO_MASK;
_argCheck(L, "animationSetLayerMask", 2, 3);
root = _argNode(L, "animationSetLayerMask", 1);
layer = _argAnimationLayer(L, "animationSetLayerMask", 2);
if ((lua_gettop(L) >= 3) && !lua_isnil(L, 3)) {
mask = _argNode(L, "animationSetLayerMask", 3);
}
if (!animationSetLayerMask(root, layer, mask)) {
_luaDie(L, "animationSetLayerMask", "Node %d is not a model instance, or node %d is not part of it.", root, mask);
}
return 0;
}
// animationSetLayerWeight(node, layer, weight): 0 to 1
static int32_t apiAnimationSetLayerWeight(lua_State *L) {
int32_t root;
int32_t layer;
_argCheck(L, "animationSetLayerWeight", 3, 3);
root = _argNode(L, "animationSetLayerWeight", 1);
layer = _argAnimationLayer(L, "animationSetLayerWeight", 2);
if (!animationSetLayerWeight(root, layer, (float)_argNumber(L, "animationSetLayerWeight", 3))) {
_luaDie(L, "animationSetLayerWeight", "Node %d is not a model instance.", root);
}
return 0;
}
// animationSetTime(node, seconds)
static int32_t apiAnimationSetTime(lua_State *L) {
int32_t root;
_argCheck(L, "animationSetTime", 2, 2);
root = _argNode(L, "animationSetTime", 1);
if (!animationSetTime(root, _argNumber(L, "animationSetTime", 2))) {
_luaDie(L, "animationSetTime", "Node %d has no animation.", root);
}
return 0;
}
// animationStop(node [, layer]): every layer, or one of the layers above the base
static int32_t apiAnimationStop(lua_State *L) {
int32_t root;
int32_t layer = ANIMATION_ALL_LAYERS;
_argCheck(L, "animationStop", 1, 2);
root = _argNode(L, "animationStop", 1);
if (lua_gettop(L) >= 2) {
layer = _argAnimationLayer(L, "animationStop", 2);
}
if (!animationStop(root, layer)) {
_luaDie(L, "animationStop", "Node %d is not a model instance.", root);
}
return 0;
}
// bodyApplyForce(node, fx, fy, fz [, px, py, pz]): this step, at the centre or a world point
static int32_t apiBodyApplyForce(lua_State *L) {
int32_t node;
Vec3T force;
Vec3T at;
_argCheck(L, "bodyApplyForce", 4, 7);
node = _argBody(L, "bodyApplyForce", 1);
force = _argVec3(L, "bodyApplyForce", 2);
if (lua_gettop(L) >= 7) {
at = _argVec3(L, "bodyApplyForce", 5);
bodyApplyForce(node, force, &at);
} else {
bodyApplyForce(node, force, NULL);
}
return 0;
}
// bodyApplyImpulse(node, ix, iy, iz [, px, py, pz]): an instant change of momentum
static int32_t apiBodyApplyImpulse(lua_State *L) {
int32_t node;
Vec3T impulse;
Vec3T at;
_argCheck(L, "bodyApplyImpulse", 4, 7);
node = _argBody(L, "bodyApplyImpulse", 1);
impulse = _argVec3(L, "bodyApplyImpulse", 2);
if (lua_gettop(L) >= 7) {
at = _argVec3(L, "bodyApplyImpulse", 5);
bodyApplyImpulse(node, impulse, &at);
} else {
bodyApplyImpulse(node, impulse, NULL);
}
return 0;
}
// bodyDelete(node)
static int32_t apiBodyDelete(lua_State *L) {
_argCheck(L, "bodyDelete", 1, 1);
bodyDelete(_argBody(L, "bodyDelete", 1));
return 0;
}
// x, y, z = bodyGetAngularVelocity(node): radians per second about each axis
static int32_t apiBodyGetAngularVelocity(lua_State *L) {
_argCheck(L, "bodyGetAngularVelocity", 1, 1);
return _pushVec3(L, bodyGetAngularVelocity(_argBody(L, "bodyGetAngularVelocity", 1)));
}
// x, y, z = bodyGetVelocity(node): units per second
static int32_t apiBodyGetVelocity(lua_State *L) {
_argCheck(L, "bodyGetVelocity", 1, 1);
return _pushVec3(L, bodyGetVelocity(_argBody(L, "bodyGetVelocity", 1)));
}
// resting = bodyIsResting(node): asleep
static int32_t apiBodyIsResting(lua_State *L) {
_argCheck(L, "bodyIsResting", 1, 1);
lua_pushboolean(L, bodyIsResting(_argBody(L, "bodyIsResting", 1)));
return 1;
}
// bodyNew(node, type, shape [, a [, b [, c]]]): BODY_* and SHAPE_*; a, b, c size the primitive shapes
static int32_t apiBodyNew(lua_State *L) {
int32_t node = 0;
int32_t type = 0;
int32_t shape = 0;
float dims[SHAPE_SIZES] = { 0.0f, 0.0f, 0.0f };
int32_t x = 0;
_argCheck(L, "bodyNew", 3, 6);
node = _argNode(L, "bodyNew", 1);
type = _argInteger(L, "bodyNew", 2);
shape = _argInteger(L, "bodyNew", 3);
if ((type < BODY_STATIC) || (type > BODY_KINEMATIC)) {
_luaDie(L, "bodyNew", "Unknown body type %d.", type);
}
if ((shape < SHAPE_BOX) || (shape > SHAPE_MESH)) {
_luaDie(L, "bodyNew", "Unknown shape %d.", shape);
}
for (x = 0; x < SHAPE_SIZES; x++) {
if (lua_gettop(L) >= 4 + x) {
dims[x] = (float)_argNumber(L, "bodyNew", 4 + x);
}
}
if (!physicsAvailable()) {
_luaDie(L, "bodyNew", "Physics is not available on this machine.");
}
if (!bodyNew(node, (BodyTypeE)type, (ShapeTypeE)shape, dims[0], dims[1], dims[2])) {
_luaDie(L, "bodyNew", "Unable to create the body.");
}
_luaTrace(L, "bodyNew", "node %d type %d shape %d", node, type, shape);
return 0;
}
// bodySetAngularVelocity(node, x, y, z)
static int32_t apiBodySetAngularVelocity(lua_State *L) {
_argCheck(L, "bodySetAngularVelocity", 4, 4);
bodySetAngularVelocity(_argBody(L, "bodySetAngularVelocity", 1), _argVec3(L, "bodySetAngularVelocity", 2));
return 0;
}
// bodySetBounce(node, 0..1)
static int32_t apiBodySetBounce(lua_State *L) {
_argCheck(L, "bodySetBounce", 2, 2);
bodySetBounce(_argBody(L, "bodySetBounce", 1), (float)_argNumber(L, "bodySetBounce", 2));
return 0;
}
// bodySetBuoyancy(node, factor): 1 floats neutrally in water, more floats, less sinks
static int32_t apiBodySetBuoyancy(lua_State *L) {
_argCheck(L, "bodySetBuoyancy", 2, 2);
bodySetBuoyancy(_argBody(L, "bodySetBuoyancy", 1), (float)_argNumber(L, "bodySetBuoyancy", 2));
return 0;
}
// bodySetCurrent(node, x, y, z): the flow inside a water volume
static int32_t apiBodySetCurrent(lua_State *L) {
_argCheck(L, "bodySetCurrent", 4, 4);
bodySetCurrent(_argBody(L, "bodySetCurrent", 1), _argVec3(L, "bodySetCurrent", 2));
return 0;
}
// bodySetEnabled(node, bool): out of the world and back
static int32_t apiBodySetEnabled(lua_State *L) {
_argCheck(L, "bodySetEnabled", 2, 2);
bodySetEnabled(_argBody(L, "bodySetEnabled", 1), _argBoolean(L, "bodySetEnabled", 2));
return 0;
}
// bodySetFriction(node, friction)
static int32_t apiBodySetFriction(lua_State *L) {
_argCheck(L, "bodySetFriction", 2, 2);
bodySetFriction(_argBody(L, "bodySetFriction", 1), (float)_argNumber(L, "bodySetFriction", 2));
return 0;
}
// bodySetMass(node, kilograms): dynamic bodies
static int32_t apiBodySetMass(lua_State *L) {
int32_t node;
_argCheck(L, "bodySetMass", 2, 2);
node = _argBody(L, "bodySetMass", 1);
if (!bodySetMass(node, (float)_argNumber(L, "bodySetMass", 2))) {
_luaDie(L, "bodySetMass", "Node %d is not a dynamic body, or the mass is not positive.", node);
}
return 0;
}
// bodySetTrigger(node, bool): a trigger reports what enters and leaves it (onTrigger) and pushes nothing
static int32_t apiBodySetTrigger(lua_State *L) {
_argCheck(L, "bodySetTrigger", 2, 2);
bodySetTrigger(_argBody(L, "bodySetTrigger", 1), _argBoolean(L, "bodySetTrigger", 2));
return 0;
}
// bodySetVelocity(node, x, y, z)
static int32_t apiBodySetVelocity(lua_State *L) {
_argCheck(L, "bodySetVelocity", 4, 4);
bodySetVelocity(_argBody(L, "bodySetVelocity", 1), _argVec3(L, "bodySetVelocity", 2));
return 0;
}
// bodySetWater(node, density, linearDrag, angularDrag): a static body becomes a water volume
static int32_t apiBodySetWater(lua_State *L) {
_argCheck(L, "bodySetWater", 2, 4);
if (!bodySetWater(_argBody(L, "bodySetWater", 1), (float)_argNumber(L, "bodySetWater", 2), (lua_gettop(L) >= 3) ? (float)_argNumber(L, "bodySetWater", 3) : WATER_DEFAULT_LINEAR_DRAG, (lua_gettop(L) >= 4) ? (float)_argNumber(L, "bodySetWater", 4) : WATER_DEFAULT_ANGULAR_DRAG)) {
_luaDie(L, "bodySetWater", "Water needs a static body.");
}
return 0;
}
// cameraSet(node): any node can be the camera (it looks down its own -Z); -1 restores the default view.
static int32_t apiCameraSet(lua_State *L) {
int32_t node;
_argCheck(L, "cameraSet", 1, 1);
node = _argInteger(L, "cameraSet", 1);
if (!cameraSet(node)) {
_luaDie(L, "cameraSet", "No node %d.", node);
}
_luaTrace(L, "cameraSet", "%d", node);
return 0;
}
// cameraSetOrthographic(height, near, far)
static int32_t apiCameraSetOrthographic(lua_State *L) {
_argCheck(L, "cameraSetOrthographic", 3, 3);
cameraSetOrthographic((float)_argNumber(L, "cameraSetOrthographic", 1), (float)_argNumber(L, "cameraSetOrthographic", 2), (float)_argNumber(L, "cameraSetOrthographic", 3));
return 0;
}
// cameraSetPerspective(fovDegrees, near, far)
static int32_t apiCameraSetPerspective(lua_State *L) {
_argCheck(L, "cameraSetPerspective", 3, 3);
cameraSetPerspective((float)_argNumber(L, "cameraSetPerspective", 1), (float)_argNumber(L, "cameraSetPerspective", 2), (float)_argNumber(L, "cameraSetPerspective", 3));
return 0;
}
// colorBackground(r, g, b[, a]) Default alpha is transparent so overlayPrint shows the video through.
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)) {
frame = _discGetFrame();
}
_luaTrace(L, "discGetFrame", "%" PRId64, frame);
lua_pushinteger(L, frame);
return 1;
}
// height = discGetHeight() Also registered as vldpGetHeight.
static int32_t apiDiscGetHeight(lua_State *L) {
int32_t height = _global.canvasHeight;
_luaTrace(L, "discGetHeight", "%d", height);
lua_pushinteger(L, height);
return 1;
}
// code = discGetLanguage(track)
static int32_t apiDiscGetLanguage(lua_State *L) {
int32_t track = 0;
const char *language = "unk";
_argCheck(L, "discGetLanguage", 1, 1);
track = _argInteger(L, "discGetLanguage", 1);
if (_global.videoHandle >= 0) {
if ((track < 0) || (track >= videoGetAudioTracks(_global.videoHandle))) {
_luaDie(L, "discGetLanguage", "Invalid audio track: %d", track);
}
language = videoGetLanguage(_global.videoHandle, track);
}
_luaTrace(L, "discGetLanguage", "%d %s", track, language);
lua_pushstring(L, language);
return 1;
}
// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED, or DISC_EJECTED (no disc).
static int32_t apiDiscGetState(lua_State *L) {
DiscStateE state = DISC_PAUSED;
if (_global.videoHandle < 0) {
state = DISC_EJECTED;
} else if (_global.discStopped) {
state = DISC_STOPPED;
} else if (videoIsPlaying(_global.videoHandle)) {
state = DISC_PLAYING;
}
_luaTrace(L, "discGetState", "%d", state);
lua_pushinteger(L, state);
return 1;
}
// width = discGetWidth() Also registered as vldpGetWidth.
static int32_t apiDiscGetWidth(lua_State *L) {
int32_t width = _global.canvasWidth;
_luaTrace(L, "discGetWidth", "%d", width);
lua_pushinteger(L, width);
return 1;
}
// discPause()
static int32_t apiDiscPause(lua_State *L) {
if (_global.discStopped) {
_luaTrace(L, "discPause", "Ignored. Disc is stopped.");
return 0;
}
if (_global.videoHandle >= 0) {
videoPause(_global.videoHandle);
}
_luaTrace(L, "discPause", "Paused.");
return 0;
}
// discPlay()
static int32_t apiDiscPlay(lua_State *L) {
if (_global.videoHandle >= 0) {
videoPlay(_global.videoHandle);
}
_global.discStopped = false;
_luaTrace(L, "discPlay", "Playing.");
return 0;
}
// discSearch(frame) Seeks, shows the frame, and pauses. Also registered as discPauseAtFrame.
static int32_t apiDiscSearch(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSearch", 1, 1);
frame = _argInteger64(L, "discSearch", 1);
_discSeek(frame);
if (_global.videoHandle >= 0) {
videoPause(_global.videoHandle);
}
_global.discStopped = false;
_luaTrace(L, "discSearch", "%" PRId64, frame);
return 0;
}
static int32_t apiDiscSearchBlanking(lua_State *L) {
return _apiUnimplemented(L, "discSearchBlanking");
}
// discSetAudioTrack(track)
static int32_t apiDiscSetAudioTrack(lua_State *L) {
int32_t track = 0;
_argCheck(L, "discSetAudioTrack", 1, 1);
track = _argInteger(L, "discSetAudioTrack", 1);
if (_global.videoHandle >= 0) {
if ((track < 0) || (track >= videoGetAudioTracks(_global.videoHandle))) {
_luaDie(L, "discSetAudioTrack", "Invalid audio track: %d", track);
}
videoSetAudioTrack(_global.videoHandle, track);
}
_luaTrace(L, "discSetAudioTrack", "%d", track);
return 0;
}
static int32_t apiDiscSetFPS(lua_State *L) {
return _apiUnimplemented(L, "discSetFPS");
}
// discSkipBackward(frames) Play/pause state is unchanged.
static int32_t apiDiscSkipBackward(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSkipBackward", 1, 1);
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discSkipBackward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() - _argInteger64(L, "discSkipBackward", 1);
_discSeek(frame);
_luaTrace(L, "discSkipBackward", "%" PRId64, frame);
return 0;
}
static int32_t apiDiscSkipBlanking(lua_State *L) {
return _apiUnimplemented(L, "discSkipBlanking");
}
// discSkipForward(frames) Play/pause state is unchanged.
static int32_t apiDiscSkipForward(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSkipForward", 1, 1);
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discSkipForward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() + _argInteger64(L, "discSkipForward", 1);
_discSeek(frame);
_luaTrace(L, "discSkipForward", "%" PRId64, frame);
return 0;
}
// discSkipToFrame(frame) Seeks and plays no matter the disc state.
static int32_t apiDiscSkipToFrame(lua_State *L) {
int64_t frame = 0;
_argCheck(L, "discSkipToFrame", 1, 1);
frame = _argInteger64(L, "discSkipToFrame", 1);
_discSeek(frame);
if (_global.videoHandle >= 0) {
videoPlay(_global.videoHandle);
}
_global.discStopped = false;
_luaTrace(L, "discSkipToFrame", "%" PRId64, frame);
return 0;
}
// discStepBackward() Go back a frame and pause. Ignored while stopped, like the skips.
static int32_t apiDiscStepBackward(lua_State *L) {
int64_t frame = 0;
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discStepBackward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() - 1;
_discSeek(frame);
videoPause(_global.videoHandle);
_luaTrace(L, "discStepBackward", "%" PRId64, frame);
return 0;
}
// discStepForward() Go forward a frame and pause. Ignored while stopped, like the skips.
static int32_t apiDiscStepForward(lua_State *L) {
int64_t frame = 0;
if (_global.discStopped || (_global.videoHandle < 0)) {
_luaTrace(L, "discStepForward", "Ignored. Disc is stopped.");
return 0;
}
frame = _discGetFrame() + 1;
_discSeek(frame);
videoPause(_global.videoHandle);
_luaTrace(L, "discStepForward", "%" PRId64, frame);
return 0;
}
// discStop() Pauses and shows the classic blue screen until the next play or search.
static int32_t apiDiscStop(lua_State *L) {
if (_global.discStopped) {
_luaTrace(L, "discStop", "Ignored. Disc is stopped.");
return 0;
}
if (_global.videoHandle >= 0) {
videoPause(_global.videoHandle);
}
_global.discStopped = true;
_global.refreshDisplay = true;
_luaTrace(L, "discStop", "Stopped.");
return 0;
}
static int32_t apiEmitterBurst(lua_State *L) {
_argCheck(L, "emitterBurst", 2, 2);
emitterBurst(_argEmitter(L, "emitterBurst", 1), _argInteger(L, "emitterBurst", 2));
return 0;
}
static int32_t apiEmitterClear(lua_State *L) {
_argCheck(L, "emitterClear", 1, 1);
emitterClear(_argEmitter(L, "emitterClear", 1));
return 0;
}
static int32_t apiEmitterDelete(lua_State *L) {
int32_t emitter = 0;
_argCheck(L, "emitterDelete", 1, 1);
emitter = _argEmitter(L, "emitterDelete", 1);
_particleTexturesFree(emitter);
emitterDelete(emitter);
return 0;
}
static int32_t apiEmitterDraw(lua_State *L) {
int32_t emitter = 0;
_argCheck(L, "emitterDraw", 1, 1);
emitter = _argEmitter(L, "emitterDraw", 1);
if (emitterIs3D(emitter)) {
_luaDie(L, "emitterDraw", "Emitter %d is a 3D emitter; it draws itself in the scene.", emitter);
}
particlesQueue2D(emitter);
return 0;
}
static int32_t apiEmitterGetCount(lua_State *L) {
_argCheck(L, "emitterGetCount", 1, 1);
lua_pushinteger(L, emitterGetCount(_argEmitter(L, "emitterGetCount", 1)));
return 1;
}
static int32_t apiEmitterIsActive(lua_State *L) {
_argCheck(L, "emitterIsActive", 1, 1);
lua_pushboolean(L, emitterIsActive(_argEmitter(L, "emitterIsActive", 1)));
return 1;
}
static int32_t apiEmitterNew(lua_State *L) {
int32_t node = -1;
_argCheck(L, "emitterNew", 0, 1);
if (lua_gettop(L) == 1) {
node = _argNode(L, "emitterNew", 1);
if (!sceneAvailable()) {
_luaDie(L, "emitterNew", "3D particles need the 3D scene, which this machine cannot provide.");
}
}
lua_pushinteger(L, emitterNew(node));
return 1;
}
static int32_t apiEmitterSetBlend(lua_State *L) {
int32_t emitter = 0;
int32_t blend = 0;
_argCheck(L, "emitterSetBlend", 2, 2);
emitter = _argEmitter(L, "emitterSetBlend", 1);
blend = _argInteger(L, "emitterSetBlend", 2);
if ((blend != PARTICLE_ALPHA) && (blend != PARTICLE_ADD)) {
_luaDie(L, "emitterSetBlend", "Blend must be PARTICLE_ALPHA or PARTICLE_ADD.");
}
emitterSetBlend(emitter, (ParticleBlendE)blend);
return 0;
}
// emitterSetCollide(emitter, COLLIDE_* [, bounce [, friction [, floor]]])
static int32_t apiEmitterSetCollide(lua_State *L) {
int32_t emitter = 0;
int32_t mode = 0;
float bounce = EMITTER_DEFAULT_BOUNCE;
float friction = EMITTER_DEFAULT_FRICTION;
float floor = EMITTER_DEFAULT_FLOOR;
_argCheck(L, "emitterSetCollide", 2, 5);
emitter = _argEmitter(L, "emitterSetCollide", 1);
mode = _argInteger(L, "emitterSetCollide", 2);
if ((mode != COLLIDE_NONE) && (mode != COLLIDE_FLOOR) && (mode != COLLIDE_SCENE)) {
_luaDie(L, "emitterSetCollide", "Mode must be COLLIDE_NONE, COLLIDE_FLOOR or COLLIDE_SCENE.");
}
if (lua_gettop(L) >= 3) {
bounce = (float)_argNumber(L, "emitterSetCollide", 3);
}
if (lua_gettop(L) >= 4) {
friction = (float)_argNumber(L, "emitterSetCollide", 4);
}
if (lua_gettop(L) >= 5) {
floor = (float)_argNumber(L, "emitterSetCollide", 5);
}
emitterSetCollide(emitter, (ParticleCollideE)mode, bounce, friction, floor);
return 0;
}
static int32_t apiEmitterSetColor(lua_State *L) {
float start[COLOR_COMPONENTS];
float finish[COLOR_COMPONENTS];
int32_t c = 0;
_argCheck(L, "emitterSetColor", 9, 9);
for (c = 0; c < COLOR_COMPONENTS; c++) {
start[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 2 + c) / (float)COLOR_BYTE_MAX, 0.0f, 1.0f);
finish[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 6 + c) / (float)COLOR_BYTE_MAX, 0.0f, 1.0f);
}
emitterSetColor(_argEmitter(L, "emitterSetColor", 1), start, finish);
return 0;
}
static int32_t apiEmitterSetDirection(lua_State *L) {
int32_t emitter = 0;
Vec3T direction;
_argCheck(L, "emitterSetDirection", 3, 4);
emitter = _argEmitter(L, "emitterSetDirection", 1);
direction = vec3((float)_argNumber(L, "emitterSetDirection", 2), (float)_argNumber(L, "emitterSetDirection", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "emitterSetDirection", 4) : 0.0f);
emitterSetDirection(emitter, direction);
return 0;
}
static int32_t apiEmitterSetDrag(lua_State *L) {
_argCheck(L, "emitterSetDrag", 2, 2);
emitterSetDrag(_argEmitter(L, "emitterSetDrag", 1), (float)_argNumber(L, "emitterSetDrag", 2));
return 0;
}
static int32_t apiEmitterSetFrames(lua_State *L) {
_argCheck(L, "emitterSetFrames", 3, 3);
emitterSetFrames(_argEmitter(L, "emitterSetFrames", 1), _argInteger(L, "emitterSetFrames", 2), _argInteger(L, "emitterSetFrames", 3));
return 0;
}
static int32_t apiEmitterSetGravity(lua_State *L) {
int32_t emitter = 0;
Vec3T gravity;
_argCheck(L, "emitterSetGravity", 3, 4);
emitter = _argEmitter(L, "emitterSetGravity", 1);
gravity = vec3((float)_argNumber(L, "emitterSetGravity", 2), (float)_argNumber(L, "emitterSetGravity", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "emitterSetGravity", 4) : 0.0f);
emitterSetGravity(emitter, gravity);
return 0;
}
static int32_t apiEmitterSetLayer(lua_State *L) {
int32_t emitter = 0;
int32_t layer = 0;
_argCheck(L, "emitterSetLayer", 2, 2);
emitter = _argEmitter(L, "emitterSetLayer", 1);
layer = _argInteger(L, "emitterSetLayer", 2);
if ((layer != PARTICLE_OVER) && (layer != PARTICLE_UNDER)) {
_luaDie(L, "emitterSetLayer", "Layer must be PARTICLE_OVER or PARTICLE_UNDER.");
}
emitterSetLayer(emitter, (ParticleLayerE)layer);
return 0;
}
static int32_t apiEmitterSetLife(lua_State *L) {
_argCheck(L, "emitterSetLife", 3, 3);
emitterSetLife(_argEmitter(L, "emitterSetLife", 1), (float)_argNumber(L, "emitterSetLife", 2), (float)_argNumber(L, "emitterSetLife", 3));
return 0;
}
// emitterSetLit(emitter, lit): 3D particles shaded by the scene's lights
static int32_t apiEmitterSetLit(lua_State *L) {
_argCheck(L, "emitterSetLit", 2, 2);
emitterSetLit(_argEmitter(L, "emitterSetLit", 1), _argBoolean(L, "emitterSetLit", 2));
return 0;
}
static int32_t apiEmitterSetLocal(lua_State *L) {
_argCheck(L, "emitterSetLocal", 2, 2);
emitterSetLocal(_argEmitter(L, "emitterSetLocal", 1), _argBoolean(L, "emitterSetLocal", 2));
return 0;
}
static int32_t apiEmitterSetMax(lua_State *L) {
int32_t emitter = 0;
int32_t count = 0;
_argCheck(L, "emitterSetMax", 2, 2);
emitter = _argEmitter(L, "emitterSetMax", 1);
count = _argInteger(L, "emitterSetMax", 2);
if (count < 1) {
_luaDie(L, "emitterSetMax", "An emitter needs room for at least one particle.");
}
emitterSetMax(emitter, count);
return 0;
}
static int32_t apiEmitterSetPosition(lua_State *L) {
int32_t emitter = 0;
_argCheck(L, "emitterSetPosition", 3, 3);
emitter = _argEmitter(L, "emitterSetPosition", 1);
if (emitterIs3D(emitter)) {
_luaDie(L, "emitterSetPosition", "Emitter %d follows its node; move the node instead.", emitter);
}
emitterSetPosition(emitter, vec3((float)_argNumber(L, "emitterSetPosition", 2), (float)_argNumber(L, "emitterSetPosition", 3), 0.0f));
return 0;
}
static int32_t apiEmitterSetRadius(lua_State *L) {
_argCheck(L, "emitterSetRadius", 2, 2);
emitterSetRadius(_argEmitter(L, "emitterSetRadius", 1), (float)_argNumber(L, "emitterSetRadius", 2));
return 0;
}
static int32_t apiEmitterSetRate(lua_State *L) {
_argCheck(L, "emitterSetRate", 2, 2);
emitterSetRate(_argEmitter(L, "emitterSetRate", 1), (float)_argNumber(L, "emitterSetRate", 2));
return 0;
}
static int32_t apiEmitterSetSize(lua_State *L) {
_argCheck(L, "emitterSetSize", 3, 4);
emitterSetSize(_argEmitter(L, "emitterSetSize", 1), (float)_argNumber(L, "emitterSetSize", 2), (float)_argNumber(L, "emitterSetSize", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "emitterSetSize", 4) : 0.0f);
return 0;
}
// emitterSetSoftness(emitter, distance): 3D particles fade over this distance where they meet geometry
static int32_t apiEmitterSetSoftness(lua_State *L) {
_argCheck(L, "emitterSetSoftness", 2, 2);
emitterSetSoftness(_argEmitter(L, "emitterSetSoftness", 1), (float)_argNumber(L, "emitterSetSoftness", 2));
return 0;
}
static int32_t apiEmitterSetSpeed(lua_State *L) {
_argCheck(L, "emitterSetSpeed", 3, 3);
emitterSetSpeed(_argEmitter(L, "emitterSetSpeed", 1), (float)_argNumber(L, "emitterSetSpeed", 2), (float)_argNumber(L, "emitterSetSpeed", 3));
return 0;
}
static int32_t apiEmitterSetSpin(lua_State *L) {
_argCheck(L, "emitterSetSpin", 3, 3);
emitterSetSpin(_argEmitter(L, "emitterSetSpin", 1), (float)_argNumber(L, "emitterSetSpin", 2), (float)_argNumber(L, "emitterSetSpin", 3));
return 0;
}
static int32_t apiEmitterSetSpread(lua_State *L) {
_argCheck(L, "emitterSetSpread", 2, 2);
emitterSetSpread(_argEmitter(L, "emitterSetSpread", 1), (float)_argNumber(L, "emitterSetSpread", 2));
return 0;
}
static int32_t apiEmitterSetTexture(lua_State *L) {
int32_t emitter = 0;
SpriteT *sprite = NULL;
_argCheck(L, "emitterSetTexture", 1, 2);
emitter = _argEmitter(L, "emitterSetTexture", 1);
if ((lua_gettop(L) == 2) && !lua_isnil(L, 2)) {
sprite = _argSprite(L, "emitterSetTexture", 2);
if (sprite->animation != NULL) {
emitterSetTexture(emitter, sprite->animation->frames, sprite->animation->count);
} else {
emitterSetTexture(emitter, &sprite->originalSurface, 1);
}
} else {
emitterSetTexture(emitter, NULL, 0);
}
return 0;
}
// emitterSetTrail(emitter, length, width): a ribbon of the last length positions behind each particle
static int32_t apiEmitterSetTrail(lua_State *L) {
_argCheck(L, "emitterSetTrail", 3, 3);
emitterSetTrail(_argEmitter(L, "emitterSetTrail", 1), _argInteger(L, "emitterSetTrail", 2), (float)_argNumber(L, "emitterSetTrail", 3));
return 0;
}
static int32_t apiEmitterStart(lua_State *L) {
_argCheck(L, "emitterStart", 1, 1);
emitterStart(_argEmitter(L, "emitterStart", 1));
return 0;
}
static int32_t apiEmitterStop(lua_State *L) {
_argCheck(L, "emitterStop", 1, 1);
emitterStop(_argEmitter(L, "emitterStop", 1));
return 0;
}
// id = fontLoad(filename, points) The new font becomes current.
static int32_t apiFontLoad(lua_State *L) {
const char *name = NULL;
int32_t points = 0;
FontT *font = NULL;
SDL_IOStream *io = NULL;
_argCheck(L, "fontLoad", 2, 2);
name = _argString(L, "fontLoad", 1);
points = _argInteger(L, "fontLoad", 2);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "fontLoad", "Unable to open %s", name);
}
font = (FontT *)calloc(1, sizeof(FontT));
if (!font) {
_luaDie(L, "fontLoad", "Unable to allocate new font.");
}
font->font = TTF_OpenFontIO(io, true, (float)points);
if (!font->font) {
_luaDie(L, "fontLoad", "%s", SDL_GetError());
}
font->id = _global.nextFontId++;
_global.fontCurrent = font;
HASH_ADD_INT(_global.fontList, id, font);
_luaTrace(L, "fontLoad", "%s %d", name, font->id);
lua_pushinteger(L, font->id);
return 1;
}
// fontPrint(x, y, text) Uses the current font.
static int32_t apiFontPrint(lua_State *L) {
const char *message = NULL;
SDL_Surface *text = NULL;
SDL_Rect dest;
_argCheck(L, "fontPrint", 3, 3);
dest.x = _argInteger(L, "fontPrint", 1);
dest.y = _argInteger(L, "fontPrint", 2);
message = _argString(L, "fontPrint", 3);
text = _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", "Unable to create the node.");
}
_luaTrace(L, "lightNew", "%d type %d under %d", node, type, parent);
lua_pushinteger(L, node);
return 1;
}
// lightSetColor(node, r, g, b)
static int32_t apiLightSetColor(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetColor", 4, 4);
node = _argNode(L, "lightSetColor", 1);
if (!lightSetColor(node, _argColorByte(L, "lightSetColor", 2), _argColorByte(L, "lightSetColor", 3), _argColorByte(L, "lightSetColor", 4))) {
_luaDie(L, "lightSetColor", "Node %d is not a light.", node);
}
return 0;
}
// lightSetCone(node, innerDegrees, outerDegrees) for spot lights
static int32_t apiLightSetCone(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetCone", 3, 3);
node = _argNode(L, "lightSetCone", 1);
if (!lightSetCone(node, (float)_argNumber(L, "lightSetCone", 2), (float)_argNumber(L, "lightSetCone", 3))) {
_luaDie(L, "lightSetCone", "Node %d is not a light.", node);
}
return 0;
}
// lightSetIntensity(node, intensity)
static int32_t apiLightSetIntensity(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetIntensity", 2, 2);
node = _argNode(L, "lightSetIntensity", 1);
if (!lightSetIntensity(node, (float)_argNumber(L, "lightSetIntensity", 2))) {
_luaDie(L, "lightSetIntensity", "Node %d is not a light.", node);
}
return 0;
}
// lightSetRange(node, range): 0 for unlimited
static int32_t apiLightSetRange(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetRange", 2, 2);
node = _argNode(L, "lightSetRange", 1);
if (!lightSetRange(node, (float)_argNumber(L, "lightSetRange", 2))) {
_luaDie(L, "lightSetRange", "Node %d is not a light.", node);
}
return 0;
}
// lightSetShadow(node, bool): this light casts shadows (a point light's cost six passes)
static int32_t apiLightSetShadow(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetShadow", 2, 2);
node = _argNode(L, "lightSetShadow", 1);
if (!lightSetShadow(node, _argBoolean(L, "lightSetShadow", 2))) {
_luaDie(L, "lightSetShadow", "Node %d is not a light.", node);
}
return 0;
}
// lineDraw(x0, y0, z0, x1, y1, z1 [, r, g, b]): a world-space line over the scene for this frame, white by default
static int32_t apiLineDraw(lua_State *L) {
uint8_t r = COLOR_BYTE_MAX;
uint8_t g = COLOR_BYTE_MAX;
uint8_t b = COLOR_BYTE_MAX;
_argCheck(L, "lineDraw", 6, 9);
_argOptionalColor(L, "lineDraw", 7, &r, &g, &b);
sceneDrawLine(_argVec3(L, "lineDraw", 1), _argVec3(L, "lineDraw", 4), r, g, b);
return 0;
}
// materialDelete(material): a sprite node's own material (from nodeGetMaterial) stays with its node
static int32_t apiMaterialDelete(lua_State *L) {
int32_t material = 0;
_argCheck(L, "materialDelete", 1, 1);
material = _argMaterial(L, "materialDelete", 1);
if (!materialDelete(material)) {
_luaDie(L, "materialDelete", "Material %d belongs to a sprite node; clear the sprite instead.", material);
}
return 0;
}
// material = materialNew(): white, half rough
static int32_t apiMaterialNew(lua_State *L) {
int32_t material;
material = materialNew();
if (material < 0) {
_luaDie(L, "materialNew", "3D is not available on this machine.");
}
_luaTrace(L, "materialNew", "%d", material);
lua_pushinteger(L, material);
return 1;
}
// materialSetBlend(material, bool): alpha blended instead of opaque
static int32_t apiMaterialSetBlend(lua_State *L) {
_argCheck(L, "materialSetBlend", 2, 2);
materialSetBlend(_argMaterial(L, "materialSetBlend", 1), _argBoolean(L, "materialSetBlend", 2));
return 0;
}
// materialSetColor(material, r, g, b [, a])
static int32_t apiMaterialSetColor(lua_State *L) {
int32_t material = 0;
uint8_t a = SDL_ALPHA_OPAQUE;
_argCheck(L, "materialSetColor", 4, 5);
material = _argMaterial(L, "materialSetColor", 1);
if (lua_gettop(L) >= 5) {
a = _argColorByte(L, "materialSetColor", 5);
}
materialSetColor(material, _argColorByte(L, "materialSetColor", 2), _argColorByte(L, "materialSetColor", 3), _argColorByte(L, "materialSetColor", 4), a);
return 0;
}
// materialSetDoubleSided(material, bool)
static int32_t apiMaterialSetDoubleSided(lua_State *L) {
_argCheck(L, "materialSetDoubleSided", 2, 2);
materialSetDoubleSided(_argMaterial(L, "materialSetDoubleSided", 1), _argBoolean(L, "materialSetDoubleSided", 2));
return 0;
}
// materialSetEmissive(material, r, g, b)
static int32_t apiMaterialSetEmissive(lua_State *L) {
_argCheck(L, "materialSetEmissive", 4, 4);
materialSetEmissive(_argMaterial(L, "materialSetEmissive", 1), _argColorByte(L, "materialSetEmissive", 2), _argColorByte(L, "materialSetEmissive", 3), _argColorByte(L, "materialSetEmissive", 4));
return 0;
}
// materialSetEmissiveMap(material[, image]) nil clears.
static int32_t apiMaterialSetEmissiveMap(lua_State *L) {
return _materialSetMap(L, "materialSetEmissiveMap", MAP_EMISSIVE, false);
}
static int32_t apiMaterialSetFilter(lua_State *L) {
int32_t material = 0;
int32_t filter = 0;
_argCheck(L, "materialSetFilter", 2, 2);
material = _argMaterial(L, "materialSetFilter", 1);
filter = _argInteger(L, "materialSetFilter", 2);
if ((filter != FILTER_LINEAR) && (filter != FILTER_NEAREST)) {
_luaDie(L, "materialSetFilter", "Filter must be FILTER_LINEAR or FILTER_NEAREST.");
}
materialSetFilter(material, (MaterialFilterE)filter);
return 0;
}
// materialSetMetallic(material, 0..1)
static int32_t apiMaterialSetMetallic(lua_State *L) {
_argCheck(L, "materialSetMetallic", 2, 2);
materialSetMetallic(_argMaterial(L, "materialSetMetallic", 1), (float)_argNumber(L, "materialSetMetallic", 2));
return 0;
}
// materialSetMetallicRoughnessMap(material[, image]) roughness in G, metallic in B; nil clears.
static int32_t apiMaterialSetMetallicRoughnessMap(lua_State *L) {
return _materialSetMap(L, "materialSetMetallicRoughnessMap", MAP_METALLIC_ROUGHNESS, false);
}
// materialSetNormalMap(material[, image[, strength]]) nil clears; strength defaults to 1.
static int32_t apiMaterialSetNormalMap(lua_State *L) {
return _materialSetMap(L, "materialSetNormalMap", MAP_NORMAL, true);
}
// materialSetOcclusionMap(material[, image[, strength]]) occlusion in R; nil clears; strength defaults to 1.
static int32_t apiMaterialSetOcclusionMap(lua_State *L) {
return _materialSetMap(L, "materialSetOcclusionMap", MAP_OCCLUSION, true);
}
// materialSetRoughness(material, 0..1)
static int32_t apiMaterialSetRoughness(lua_State *L) {
_argCheck(L, "materialSetRoughness", 2, 2);
materialSetRoughness(_argMaterial(L, "materialSetRoughness", 1), (float)_argNumber(L, "materialSetRoughness", 2));
return 0;
}
// materialSetTexture(material [, image]): a sprite's image (or a KTX2 file) as the base colour; none clears it
static int32_t apiMaterialSetTexture(lua_State *L) {
return _materialSetMap(L, "materialSetTexture", MAP_BASE, false);
}
// materialSetTiling(material, u, v): how many times its textures repeat across a surface
static int32_t apiMaterialSetTiling(lua_State *L) {
_argCheck(L, "materialSetTiling", 3, 3);
materialSetTiling(_argMaterial(L, "materialSetTiling", 1), (float)_argNumber(L, "materialSetTiling", 2), (float)_argNumber(L, "materialSetTiling", 3));
return 0;
}
// materialSetUnlit(material, bool): shows the base colour as is
static int32_t apiMaterialSetUnlit(lua_State *L) {
_argCheck(L, "materialSetUnlit", 2, 2);
materialSetUnlit(_argMaterial(L, "materialSetUnlit", 1), _argBoolean(L, "materialSetUnlit", 2));
return 0;
}
// materialSetVideo(material [, video]): the disc (or a loaded video) as the base colour texture
static int32_t apiMaterialSetVideo(lua_State *L) {
int32_t material;
int32_t player;
VideoT *video;
_argCheck(L, "materialSetVideo", 1, 2);
material = _argMaterial(L, "materialSetVideo", 1);
if (lua_gettop(L) >= 2) {
video = _argVideo(L, "materialSetVideo", 2);
player = video->handle;
} else {
if (_global.videoHandle < 0) {
_luaDie(L, "materialSetVideo", "This game has no disc.");
}
player = _global.videoHandle;
}
materialSetVideo(material, player);
return 0;
}
// materialSetView(material [, view]): a rendered view as the base colour texture; none restores the texture
static int32_t apiMaterialSetView(lua_State *L) {
int32_t material;
int32_t view = -1;
_argCheck(L, "materialSetView", 1, 2);
material = _argMaterial(L, "materialSetView", 1);
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
view = _argView(L, "materialSetView", 2);
}
materialSetView(material, view);
return 0;
}
// mesh = meshBox(width, height, depth)
static int32_t apiMeshBox(lua_State *L) {
int32_t mesh;
_argCheck(L, "meshBox", 3, 3);
mesh = meshBox((float)_argNumber(L, "meshBox", 1), (float)_argNumber(L, "meshBox", 2), (float)_argNumber(L, "meshBox", 3));
if (mesh < 0) {
_luaDie(L, "meshBox", "Unable to create the mesh.");
}
_luaTrace(L, "meshBox", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshCone(radius, height [, segments])
static int32_t apiMeshCone(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SEGMENTS_DEFAULT;
_argCheck(L, "meshCone", 2, 3);
if (lua_gettop(L) >= 3) {
segments = _argInteger(L, "meshCone", 3);
}
mesh = meshCone((float)_argNumber(L, "meshCone", 1), (float)_argNumber(L, "meshCone", 2), segments);
if (mesh < 0) {
_luaDie(L, "meshCone", "Unable to create the mesh.");
}
_luaTrace(L, "meshCone", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshCylinder(radius, height [, segments])
static int32_t apiMeshCylinder(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SEGMENTS_DEFAULT;
_argCheck(L, "meshCylinder", 2, 3);
if (lua_gettop(L) >= 3) {
segments = _argInteger(L, "meshCylinder", 3);
}
mesh = meshCylinder((float)_argNumber(L, "meshCylinder", 1), (float)_argNumber(L, "meshCylinder", 2), segments);
if (mesh < 0) {
_luaDie(L, "meshCylinder", "Unable to create the mesh.");
}
_luaTrace(L, "meshCylinder", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// meshDelete(mesh)
static int32_t apiMeshDelete(lua_State *L) {
_argCheck(L, "meshDelete", 1, 1);
meshDelete(_argMesh(L, "meshDelete", 1));
return 0;
}
// mesh = meshHeightmap(image, sizeX, sizeY, sizeZ): a terrain from a greyscale image (its red channel,
// black low and white sizeY high), sizeX by sizeZ across, one vertex per pixel
static int32_t apiMeshHeightmap(lua_State *L) {
const char *name = NULL;
SDL_IOStream *io = NULL;
SDL_Surface *image = NULL;
SDL_Surface *rgba = NULL;
const uint8_t *row = NULL;
float *heights = NULL;
int32_t bytesPerPixel = 0;
int32_t columns = 0;
int32_t rows = 0;
int32_t x = 0;
int32_t y = 0;
int32_t mesh = 0;
_argCheck(L, "meshHeightmap", 4, 4);
name = _argString(L, "meshHeightmap", 1);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "meshHeightmap", "Unable to open %s", name);
}
image = IMG_Load_IO(io, true);
if (image == NULL) {
_luaDie(L, "meshHeightmap", "%s: %s", name, SDL_GetError());
}
rgba = SDL_ConvertSurface(image, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(image);
if (rgba == NULL) {
_luaDie(L, "meshHeightmap", "%s", SDL_GetError());
}
if ((rgba->w < 2) || (rgba->h < 2) || (rgba->w > HEIGHTMAP_MAX) || (rgba->h > HEIGHTMAP_MAX)) {
SDL_DestroySurface(rgba);
_luaDie(L, "meshHeightmap", "%s must be 2 to %d pixels each way.", name, HEIGHTMAP_MAX);
}
columns = rgba->w - 1;
rows = rgba->h - 1;
heights = SDL_malloc(sizeof(float) * (size_t)rgba->w * (size_t)rgba->h);
if (heights == NULL) {
utilDie("Out of memory reading a heightmap.");
}
bytesPerPixel = (int32_t)SDL_BYTESPERPIXEL(rgba->format);
for (y = 0; y < rgba->h; y++) {
row = (const uint8_t *)rgba->pixels + (size_t)y * (size_t)rgba->pitch;
for (x = 0; x < rgba->w; x++) {
heights[y * rgba->w + x] = row[x * bytesPerPixel] / (float)COLOR_BYTE_MAX;
}
}
SDL_DestroySurface(rgba);
mesh = meshHeightmap(heights, columns, rows, (float)_argNumber(L, "meshHeightmap", 2), (float)_argNumber(L, "meshHeightmap", 3), (float)_argNumber(L, "meshHeightmap", 4));
SDL_free(heights);
if (mesh < 0) {
_luaDie(L, "meshHeightmap", "Unable to make a mesh from %s (is 3D available?).", name);
}
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshNew(positions, normals, uvs, indices): tables of numbers; normals and uvs may be nil
static int32_t apiMeshNew(lua_State *L) {
float *positions;
float *normals;
float *uvs;
float *indexValues;
uint32_t *indices;
int32_t positionCount;
int32_t normalCount;
int32_t uvCount;
int32_t indexCount;
int32_t vertexCount;
int32_t x;
int32_t mesh;
_argCheck(L, "meshNew", 4, 4);
positions = _argFloatTable(L, "meshNew", 1, &positionCount);
normals = _argFloatTable(L, "meshNew", 2, &normalCount);
uvs = _argFloatTable(L, "meshNew", 3, &uvCount);
indexValues = _argFloatTable(L, "meshNew", 4, &indexCount);
vertexCount = positionCount / 3;
if ((positionCount == 0) || (positionCount % 3 != 0)) {
_luaDie(L, "meshNew", "Positions must hold three numbers per vertex.");
}
if ((normals != NULL) && (normalCount != positionCount)) {
_luaDie(L, "meshNew", "Normals must hold three numbers per vertex, or be nil.");
}
if ((uvs != NULL) && (uvCount != vertexCount * 2)) {
_luaDie(L, "meshNew", "Texture coordinates must hold two numbers per vertex, or be nil.");
}
if ((indexCount < 3) || (indexCount % 3 != 0)) {
_luaDie(L, "meshNew", "Indices must hold three vertex numbers per triangle.");
}
indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t));
if (indices == NULL) {
_luaDie(L, "meshNew", "Out of memory.");
}
// Scripts number vertices from 1, as they appear in the positions table.
for (x = 0; x < indexCount; x++) {
if ((indexValues[x] < 1.0f) || (indexValues[x] > (float)vertexCount)) {
_luaDie(L, "meshNew", "Index %d refers to vertex %d; there are %d.", x + 1, (int32_t)indexValues[x], vertexCount);
}
indices[x] = (uint32_t)indexValues[x] - 1;
}
mesh = meshNew(positions, normals, uvs, vertexCount, indices, indexCount);
SDL_free(positions);
SDL_free(normals);
SDL_free(uvs);
SDL_free(indexValues);
SDL_free(indices);
if (mesh < 0) {
_luaDie(L, "meshNew", "Unable to create the mesh.");
}
_luaTrace(L, "meshNew", "%d (%d vertices, %d triangles)", mesh, vertexCount, indexCount / 3);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshPlane(width, depth): flat in XZ, facing up
static int32_t apiMeshPlane(lua_State *L) {
int32_t mesh;
_argCheck(L, "meshPlane", 2, 4);
if (lua_gettop(L) >= 3) {
// meshPlane(width, depth, columns[, rows]): subdivided, for cloth.
int32_t columns = _argInteger(L, "meshPlane", 3);
int32_t rows = (lua_gettop(L) == 4) ? _argInteger(L, "meshPlane", 4) : columns;
mesh = meshGrid((float)_argNumber(L, "meshPlane", 1), (float)_argNumber(L, "meshPlane", 2), columns, rows);
} else {
mesh = meshPlane((float)_argNumber(L, "meshPlane", 1), (float)_argNumber(L, "meshPlane", 2));
}
if (mesh < 0) {
_luaDie(L, "meshPlane", "Unable to create the mesh.");
}
_luaTrace(L, "meshPlane", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshSphere(radius [, segments])
static int32_t apiMeshSphere(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SPHERE_SEGMENTS_DEFAULT;
_argCheck(L, "meshSphere", 1, 2);
if (lua_gettop(L) >= 2) {
segments = _argInteger(L, "meshSphere", 2);
}
mesh = meshSphere((float)_argNumber(L, "meshSphere", 1), segments);
if (mesh < 0) {
_luaDie(L, "meshSphere", "Unable to create the mesh.");
}
_luaTrace(L, "meshSphere", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// mesh = meshTorus(radius, tubeRadius [, segments])
static int32_t apiMeshTorus(lua_State *L) {
int32_t mesh = 0;
int32_t segments = MESH_SPHERE_SEGMENTS_DEFAULT;
_argCheck(L, "meshTorus", 2, 3);
if (lua_gettop(L) >= 3) {
segments = _argInteger(L, "meshTorus", 3);
}
mesh = meshTorus((float)_argNumber(L, "meshTorus", 1), (float)_argNumber(L, "meshTorus", 2), segments);
if (mesh < 0) {
_luaDie(L, "meshTorus", "Unable to create the mesh.");
}
_luaTrace(L, "meshTorus", "%d", mesh);
lua_pushinteger(L, mesh);
return 1;
}
// 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;
}
// navAddNode(nav, node): the node's mesh and its children's become walkable geometry for navBuild
static int32_t apiNavAddNode(lua_State *L) {
_argCheck(L, "navAddNode", 2, 2);
if (!navAddNode(_argNav(L, "navAddNode", 1), _argNode(L, "navAddNode", 2))) {
_luaDie(L, "navAddNode", "The mesh is already built.");
}
return 0;
}
static int32_t apiNavAgentDelete(lua_State *L) {
_argCheck(L, "navAgentDelete", 1, 1);
navAgentDelete(_argNavAgent(L, "navAgentDelete", 1));
return 0;
}
// x, y, z = navAgentGetVelocity(agent)
static int32_t apiNavAgentGetVelocity(lua_State *L) {
Vec3T velocity = vec3(0.0f, 0.0f, 0.0f);
_argCheck(L, "navAgentGetVelocity", 1, 1);
navAgentGetVelocity(_argNavAgent(L, "navAgentGetVelocity", 1), &velocity);
return _pushVec3(L, velocity);
}
static int32_t apiNavAgentIsArrived(lua_State *L) {
_argCheck(L, "navAgentIsArrived", 1, 1);
lua_pushboolean(L, navAgentIsArrived(_argNavAgent(L, "navAgentIsArrived", 1)));
return 1;
}
// navAgentMoveTo(agent, x, y, z): true when a path exists
static int32_t apiNavAgentMoveTo(lua_State *L) {
int32_t agent;
_argCheck(L, "navAgentMoveTo", 4, 4);
agent = _argNavAgent(L, "navAgentMoveTo", 1);
lua_pushboolean(L, navAgentMoveTo(agent, vec3((float)_argNumber(L, "navAgentMoveTo", 2), (float)_argNumber(L, "navAgentMoveTo", 3), (float)_argNumber(L, "navAgentMoveTo", 4))));
return 1;
}
// agent = navAgentNew(nav, node, radius, height, speed): the node walks the mesh from where it stands
static int32_t apiNavAgentNew(lua_State *L) {
int32_t nav;
int32_t node;
int32_t agent;
_argCheck(L, "navAgentNew", 5, 5);
nav = _argNav(L, "navAgentNew", 1);
node = _argNode(L, "navAgentNew", 2);
agent = navAgentNew(nav, node, (float)_argNumber(L, "navAgentNew", 3), (float)_argNumber(L, "navAgentNew", 4), (float)_argNumber(L, "navAgentNew", 5));
if (agent < 0) {
_luaDie(L, "navAgentNew", "No agent available (is the mesh built, and are fewer than %d agents on it?).", NAV_MAX_CROWD_AGENTS);
}
lua_pushinteger(L, agent);
return 1;
}
// navAgentSetPlayer(agent, player): steer the player controller on the node instead of placing the node
static int32_t apiNavAgentSetPlayer(lua_State *L) {
_argCheck(L, "navAgentSetPlayer", 2, 2);
navAgentSetPlayer(_argNavAgent(L, "navAgentSetPlayer", 1), _argBoolean(L, "navAgentSetPlayer", 2));
return 0;
}
static int32_t apiNavAgentStop(lua_State *L) {
_argCheck(L, "navAgentStop", 1, 1);
navAgentStop(_argNavAgent(L, "navAgentStop", 1));
return 0;
}
// navBuild(nav): bakes the walkable mesh from everything added
static int32_t apiNavBuild(lua_State *L) {
int32_t nav;
_argCheck(L, "navBuild", 1, 1);
nav = _argNav(L, "navBuild", 1);
if (!navBuild(nav)) {
_luaDie(L, "navBuild", "Unable to bake navigation mesh %d (nothing walkable added, or already built).", nav);
}
return 0;
}
static int32_t apiNavDelete(lua_State *L) {
_argCheck(L, "navDelete", 1, 1);
navDelete(_argNav(L, "navDelete", 1));
return 0;
}
// navDraw(nav [, r, g, b]): the baked mesh's triangles as lines over the scene for this frame, cyan by default
static int32_t apiNavDraw(lua_State *L) {
int32_t nav = 0;
int32_t capacity = 0;
int32_t count = 0;
int32_t x = 0;
Vec3T *grown = NULL;
Vec3T *vertices = NULL;
uint8_t r = 0;
uint8_t g = COLOR_BYTE_MAX;
uint8_t b = COLOR_BYTE_MAX;
_argCheck(L, "navDraw", 1, 4);
nav = _argNav(L, "navDraw", 1);
_argOptionalColor(L, "navDraw", 2, &r, &g, &b);
// The triangle list is kept between frames (this is drawn every frame) and doubled until the mesh fits.
capacity = SDL_max(_global.navDrawCapacity, NAV_DRAW_VERTICES);
for (;;) {
if (capacity > _global.navDrawCapacity) {
grown = SDL_realloc(_global.navDrawVertices, (size_t)capacity * sizeof(Vec3T));
if (grown == NULL) {
_luaDie(L, "navDraw", "Out of memory.");
}
_global.navDrawVertices = grown;
_global.navDrawCapacity = capacity;
}
vertices = _global.navDrawVertices;
count = navGetPolygons(nav, vertices, capacity);
if (count + 3 <= capacity) {
break;
}
capacity *= 2;
}
for (x = 0; x + 2 < count; x += 3) {
sceneDrawLine(vertices[x], vertices[x + 1], r, g, b);
sceneDrawLine(vertices[x + 1], vertices[x + 2], r, g, b);
sceneDrawLine(vertices[x + 2], vertices[x], r, g, b);
}
return 0;
}
// nav = navLoad(name, agentRadius, agentHeight): a mesh navSave wrote, from the game or its data folder
static int32_t apiNavLoad(lua_State *L) {
const char *name = NULL;
char *data = NULL;
char *path = NULL;
size_t size = 0;
int32_t nav = 0;
_argCheck(L, "navLoad", 3, 3);
name = _argString(L, "navLoad", 1);
data = vfsRead(name, &size);
if (data == NULL) {
// Not in the game: maybe in its data folder, where navSave writes.
path = utilCreateString("%s%s", _global.conf->dataDir, name);
data = utilReadFile(path, &size);
free(path);
}
if (data == NULL) {
_luaDie(L, "navLoad", "Unable to read %s", name);
}
nav = navLoad(data, size, (float)_argNumber(L, "navLoad", 2), (float)_argNumber(L, "navLoad", 3));
free(data);
if (nav < 0) {
_luaDie(L, "navLoad", "%s is not a navigation mesh, or every mesh slot is in use.", name);
}
lua_pushinteger(L, nav);
return 1;
}
// x, y, z = navNearest(nav, x, y, z): the closest walkable point, or nil
static int32_t apiNavNearest(lua_State *L) {
Vec3T out;
_argCheck(L, "navNearest", 4, 4);
if (!navNearest(_argNav(L, "navNearest", 1), vec3((float)_argNumber(L, "navNearest", 2), (float)_argNumber(L, "navNearest", 3), (float)_argNumber(L, "navNearest", 4)), &out)) {
lua_pushnil(L);
return 1;
}
return _pushVec3(L, out);
}
// nav = navNew(agentRadius, agentHeight, maxSlope, maxStep)
static int32_t apiNavNew(lua_State *L) {
int32_t nav;
_argCheck(L, "navNew", 4, 4);
nav = navNew((float)_argNumber(L, "navNew", 1), (float)_argNumber(L, "navNew", 2), (float)_argNumber(L, "navNew", 3), (float)_argNumber(L, "navNew", 4));
if (nav < 0) {
_luaDie(L, "navNew", "No navigation mesh slot available.");
}
lua_pushinteger(L, nav);
return 1;
}
// points = navPath(nav, x0, y0, z0, x1, y1, z1): a table of {x, y, z} corners, or nil for no path
static int32_t apiNavPath(lua_State *L) {
Vec3T points[NAV_MAX_PATH];
int32_t count = 0;
int32_t x = 0;
_argCheck(L, "navPath", 7, 7);
count = navPath(_argNav(L, "navPath", 1), vec3((float)_argNumber(L, "navPath", 2), (float)_argNumber(L, "navPath", 3), (float)_argNumber(L, "navPath", 4)), vec3((float)_argNumber(L, "navPath", 5), (float)_argNumber(L, "navPath", 6), (float)_argNumber(L, "navPath", 7)), points, (int32_t)SDL_arraysize(points));
if (count < 0) {
lua_pushnil(L);
return 1;
}
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_createtable(L, 3, 0);
lua_pushnumber(L, points[x].x);
lua_rawseti(L, -2, 1);
lua_pushnumber(L, points[x].y);
lua_rawseti(L, -2, 2);
lua_pushnumber(L, points[x].z);
lua_rawseti(L, -2, 3);
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// x, y, z = navRandomPoint(nav)
static int32_t apiNavRandomPoint(lua_State *L) {
Vec3T out;
_argCheck(L, "navRandomPoint", 1, 1);
if (!navRandomPoint(_argNav(L, "navRandomPoint", 1), &out)) {
lua_pushnil(L);
return 1;
}
return _pushVec3(L, out);
}
// blocked, x, y, z = navRaycast(nav, x0, y0, z0, x1, y1, z1): whether a straight walk leaves the mesh, and where
static int32_t apiNavRaycast(lua_State *L) {
Vec3T hit = vec3(0.0f, 0.0f, 0.0f);
bool blocked;
_argCheck(L, "navRaycast", 7, 7);
blocked = navRaycast(_argNav(L, "navRaycast", 1), vec3((float)_argNumber(L, "navRaycast", 2), (float)_argNumber(L, "navRaycast", 3), (float)_argNumber(L, "navRaycast", 4)), vec3((float)_argNumber(L, "navRaycast", 5), (float)_argNumber(L, "navRaycast", 6), (float)_argNumber(L, "navRaycast", 7)), &hit);
lua_pushboolean(L, blocked);
if (!blocked) {
return 1;
}
return 1 + _pushVec3(L, hit);
}
// navSave(nav, name): the baked mesh into the game's data folder, for navLoad
static int32_t apiNavSave(lua_State *L) {
int32_t nav;
char *path;
bool ok;
_argCheck(L, "navSave", 2, 2);
nav = _argNav(L, "navSave", 1);
path = utilCreateString("%s%s", _global.conf->dataDir, _argString(L, "navSave", 2));
ok = navSave(nav, path);
if (!ok) {
_luaDie(L, "navSave", "Unable to write %s", path);
}
free(path);
return 0;
}
// nodeDelete(node): the node and everything under it
static int32_t apiNodeDelete(lua_State *L) {
int32_t node;
_argCheck(L, "nodeDelete", 1, 1);
node = _argNode(L, "nodeDelete", 1);
if (!nodeDelete(node)) {
_luaDie(L, "nodeDelete", "The root node cannot be deleted.");
}
_luaTrace(L, "nodeDelete", "%d", node);
return 0;
}
// node = nodeFind(name [, root]): by name, below root; nil when absent
static int32_t apiNodeFind(lua_State *L) {
const char *name = NULL;
int32_t root = SCENE_ROOT_NODE;
int32_t found = 0;
_argCheck(L, "nodeFind", 1, 2);
name = _argString(L, "nodeFind", 1);
if (lua_gettop(L) >= 2) {
root = _argNode(L, "nodeFind", 2);
}
found = nodeFind(root, name);
if (found < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, found);
}
return 1;
}
// children = nodeGetChildren(node): a table of handles
static int32_t apiNodeGetChildren(lua_State *L) {
int32_t node;
int32_t count;
int32_t x;
_argCheck(L, "nodeGetChildren", 1, 1);
node = _argNode(L, "nodeGetChildren", 1);
count = nodeGetChildCount(node);
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_pushinteger(L, nodeGetChild(node, x));
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// weight = nodeGetMorph(node, nameOrIndex)
static int32_t apiNodeGetMorph(lua_State *L) {
int32_t node;
int32_t target;
_argCheck(L, "nodeGetMorph", 2, 2);
node = _argNode(L, "nodeGetMorph", 1);
target = _argMorph(L, "nodeGetMorph", node, 2);
lua_pushnumber(L, nodeGetMorphWeight(node, target));
return 1;
}
// names = nodeGetMorphs(node): the mesh's morph target names, in order (empty strings when unnamed)
static int32_t apiNodeGetMorphs(lua_State *L) {
int32_t node;
int32_t count;
int32_t x;
_argCheck(L, "nodeGetMorphs", 1, 1);
node = _argNode(L, "nodeGetMorphs", 1);
count = nodeGetMorphCount(node);
lua_createtable(L, count, 0);
for (x = 0; x < count; x++) {
lua_pushstring(L, meshGetMorphName(nodeGetMesh(node), x));
lua_rawseti(L, -2, x + 1);
}
return 1;
}
// name = nodeGetName(node)
static int32_t apiNodeGetName(lua_State *L) {
_argCheck(L, "nodeGetName", 1, 1);
lua_pushstring(L, nodeGetName(_argNode(L, "nodeGetName", 1)));
return 1;
}
// parent = nodeGetParent(node): nil for the root
static int32_t apiNodeGetParent(lua_State *L) {
int32_t parent;
_argCheck(L, "nodeGetParent", 1, 1);
parent = nodeGetParent(_argNode(L, "nodeGetParent", 1));
if (parent < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, parent);
}
return 1;
}
// x, y, z = nodeGetPosition(node)
static int32_t apiNodeGetPosition(lua_State *L) {
_argCheck(L, "nodeGetPosition", 1, 1);
return _pushVec3(L, nodeGetPosition(_argNode(L, "nodeGetPosition", 1)));
}
// x, y, z, w = nodeGetQuaternion(node)
static int32_t apiNodeGetQuaternion(lua_State *L) {
QuatT q;
_argCheck(L, "nodeGetQuaternion", 1, 1);
q = nodeGetRotation(_argNode(L, "nodeGetQuaternion", 1));
lua_pushnumber(L, q.x);
lua_pushnumber(L, q.y);
lua_pushnumber(L, q.z);
lua_pushnumber(L, q.w);
return 4;
}
// x, y, z = nodeGetRotation(node): Euler degrees
static int32_t apiNodeGetRotation(lua_State *L) {
float x;
float y;
float z;
_argCheck(L, "nodeGetRotation", 1, 1);
quatToEuler(nodeGetRotation(_argNode(L, "nodeGetRotation", 1)), &x, &y, &z);
lua_pushnumber(L, x);
lua_pushnumber(L, y);
lua_pushnumber(L, z);
return 3;
}
// x, y, z = nodeGetScale(node)
static int32_t apiNodeGetScale(lua_State *L) {
_argCheck(L, "nodeGetScale", 1, 1);
return _pushVec3(L, nodeGetScale(_argNode(L, "nodeGetScale", 1)));
}
// x, y, z = nodeGetWorldPosition(node): as of the last rendered frame
static int32_t apiNodeGetWorldPosition(lua_State *L) {
_argCheck(L, "nodeGetWorldPosition", 1, 1);
return _pushVec3(L, nodeGetWorldPosition(_argNode(L, "nodeGetWorldPosition", 1)));
}
// nodeLookAt(node, x, y, z): points the node's -Z at a world point
static int32_t apiNodeLookAt(lua_State *L) {
_argCheck(L, "nodeLookAt", 4, 4);
nodeLookAt(_argNode(L, "nodeLookAt", 1), _argVec3(L, "nodeLookAt", 2));
return 0;
}
// nodeMove(node, dx, dy, dz): along the node's own axes
static int32_t apiNodeMove(lua_State *L) {
_argCheck(L, "nodeMove", 4, 4);
nodeMove(_argNode(L, "nodeMove", 1), _argVec3(L, "nodeMove", 2));
return 0;
}
// node = nodeNew([parent]): a new node at its parent's origin
static int32_t apiNodeNew(lua_State *L) {
int32_t parent = SCENE_ROOT_NODE;
int32_t node;
_argCheck(L, "nodeNew", 0, 1);
if (lua_gettop(L) >= 1) {
parent = _argNode(L, "nodeNew", 1);
}
node = nodeNew(parent);
if (node < 0) {
_luaDie(L, "nodeNew", "Unable to create the node.");
}
_luaTrace(L, "nodeNew", "%d under %d", node, parent);
lua_pushinteger(L, node);
return 1;
}
// nodeRotate(node, dx, dy, dz): degrees, about the node's own axes
static int32_t apiNodeRotate(lua_State *L) {
_argCheck(L, "nodeRotate", 4, 4);
nodeRotate(_argNode(L, "nodeRotate", 1), _argEuler(L, "nodeRotate", 2));
return 0;
}
// nodeSetBillboard(node, BILLBOARD_*): the node turns to face the camera
static int32_t apiNodeSetBillboard(lua_State *L) {
int32_t node = 0;
int32_t mode = 0;
_argCheck(L, "nodeSetBillboard", 2, 2);
node = _argNode(L, "nodeSetBillboard", 1);
mode = _argInteger(L, "nodeSetBillboard", 2);
if ((mode != BILLBOARD_NONE) && (mode != BILLBOARD_ALL) && (mode != BILLBOARD_Y)) {
_luaDie(L, "nodeSetBillboard", "Mode must be BILLBOARD_NONE, BILLBOARD_ALL or BILLBOARD_Y.");
}
nodeSetBillboard(node, (BillboardE)mode);
return 0;
}
// nodeSetMaterial(node, material): a new material on whatever mesh the node has (nil for none)
static int32_t apiNodeSetMaterial(lua_State *L) {
int32_t material = -1;
_argCheck(L, "nodeSetMaterial", 2, 2);
if (!lua_isnil(L, 2)) {
material = _argMaterial(L, "nodeSetMaterial", 2);
}
nodeSetMaterial(_argNode(L, "nodeSetMaterial", 1), material);
return 0;
}
// nodeSetMesh(node, mesh [, material])
static int32_t apiNodeSetMesh(lua_State *L) {
int32_t node;
int32_t mesh;
int32_t material = -1;
_argCheck(L, "nodeSetMesh", 2, 3);
node = _argNode(L, "nodeSetMesh", 1);
mesh = _argMesh(L, "nodeSetMesh", 2);
if (lua_gettop(L) >= 3) {
material = _argMaterial(L, "nodeSetMesh", 3);
}
nodeSetMesh(node, mesh, material);
return 0;
}
// nodeSetMorph(node, nameOrIndex, weight): how much of a morph target shows, usually 0 to 1
static int32_t apiNodeSetMorph(lua_State *L) {
int32_t node;
int32_t target;
_argCheck(L, "nodeSetMorph", 3, 3);
node = _argNode(L, "nodeSetMorph", 1);
target = _argMorph(L, "nodeSetMorph", node, 2);
nodeSetMorphWeight(node, target, (float)_argNumber(L, "nodeSetMorph", 3));
return 0;
}
// nodeSetName(node, name)
static int32_t apiNodeSetName(lua_State *L) {
_argCheck(L, "nodeSetName", 2, 2);
nodeSetName(_argNode(L, "nodeSetName", 1), _argString(L, "nodeSetName", 2));
return 0;
}
// nodeSetParent(node, parent): keeps the local transform
static int32_t apiNodeSetParent(lua_State *L) {
int32_t node;
int32_t parent;
_argCheck(L, "nodeSetParent", 2, 2);
node = _argNode(L, "nodeSetParent", 1);
parent = _argNode(L, "nodeSetParent", 2);
if (!nodeSetParent(node, parent)) {
_luaDie(L, "nodeSetParent", "Node %d cannot go under node %d.", node, parent);
}
return 0;
}
// nodeSetPosition(node, x, y, z)
static int32_t apiNodeSetPosition(lua_State *L) {
_argCheck(L, "nodeSetPosition", 4, 4);
nodeSetPosition(_argNode(L, "nodeSetPosition", 1), _argVec3(L, "nodeSetPosition", 2));
return 0;
}
// nodeSetQuaternion(node, x, y, z, w)
static int32_t apiNodeSetQuaternion(lua_State *L) {
QuatT q;
_argCheck(L, "nodeSetQuaternion", 5, 5);
q.x = (float)_argNumber(L, "nodeSetQuaternion", 2);
q.y = (float)_argNumber(L, "nodeSetQuaternion", 3);
q.z = (float)_argNumber(L, "nodeSetQuaternion", 4);
q.w = (float)_argNumber(L, "nodeSetQuaternion", 5);
nodeSetRotation(_argNode(L, "nodeSetQuaternion", 1), q);
return 0;
}
// nodeSetRotation(node, x, y, z): Euler degrees
static int32_t apiNodeSetRotation(lua_State *L) {
_argCheck(L, "nodeSetRotation", 4, 4);
nodeSetRotation(_argNode(L, "nodeSetRotation", 1), _argEuler(L, "nodeSetRotation", 2));
return 0;
}
// nodeSetScale(node, s) or nodeSetScale(node, x, y, z)
static int32_t apiNodeSetScale(lua_State *L) {
int32_t node;
Vec3T scale;
_argCheck(L, "nodeSetScale", 2, 4);
node = _argNode(L, "nodeSetScale", 1);
if (lua_gettop(L) == 2) {
scale.x = (float)_argNumber(L, "nodeSetScale", 2);
scale.y = scale.x;
scale.z = scale.x;
} else {
scale = _argVec3(L, "nodeSetScale", 2);
}
nodeSetScale(node, scale);
return 0;
}
// nodeSetShadow(node, bool): whether the node's mesh casts shadows
static int32_t apiNodeSetShadow(lua_State *L) {
_argCheck(L, "nodeSetShadow", 2, 2);
nodeSetShadow(_argNode(L, "nodeSetShadow", 1), _argBoolean(L, "nodeSetShadow", 2));
return 0;
}
// nodeSetSprite(node, sprite [, height [, lit]]): the sprite's picture (every frame of an animated one)
// on a quad height world units tall, wide by its aspect; nil clears it
static int32_t apiNodeSetSprite(lua_State *L) {
int32_t node;
SpriteT *sprite = NULL;
SDL_Surface **frames;
int32_t count;
float height = 1.0f;
bool lit = true;
float width;
_argCheck(L, "nodeSetSprite", 1, 4);
node = _argNode(L, "nodeSetSprite", 1);
if ((lua_gettop(L) < 2) || lua_isnil(L, 2)) {
nodeSetSprite(node, NULL, 0, 0.0f, 0.0f, true);
return 0;
}
sprite = _argSprite(L, "nodeSetSprite", 2);
if (lua_gettop(L) >= 3) {
height = (float)_argNumber(L, "nodeSetSprite", 3);
}
if (lua_gettop(L) >= 4) {
lit = _argBoolean(L, "nodeSetSprite", 4);
}
if (sprite->animation != NULL) {
frames = sprite->animation->frames;
count = sprite->animation->count;
} else {
frames = &sprite->originalSurface;
count = 1;
}
width = (frames[0]->h > 0) ? height * (float)frames[0]->w / (float)frames[0]->h : height;
if (!nodeSetSprite(node, frames, count, width, height, lit)) {
_luaDie(L, "nodeSetSprite", "Unable to put sprite %d on node %d (is 3D available?).", sprite->id, node);
}
return 0;
}
// nodeSetSpriteFrame(node, frame): which frame of the node's animated sprite shows, from 0
static int32_t apiNodeSetSpriteFrame(lua_State *L) {
int32_t node;
int32_t frame;
_argCheck(L, "nodeSetSpriteFrame", 2, 2);
node = _argNode(L, "nodeSetSpriteFrame", 1);
frame = _argInteger(L, "nodeSetSpriteFrame", 2);
if (!nodeSetSpriteFrame(node, frame)) {
_luaDie(L, "nodeSetSpriteFrame", "Node %d has no sprite frame %d.", node, frame);
}
return 0;
}
// nodeSetText(node, text [, height]): the text in the selected font and colour on a quad height world
// units tall (per line of the font), unlit; nil clears it
static int32_t apiNodeSetText(lua_State *L) {
int32_t node;
SDL_Surface *surface;
float height = 1.0f;
float width;
bool ok;
_argCheck(L, "nodeSetText", 1, 3);
node = _argNode(L, "nodeSetText", 1);
if ((lua_gettop(L) < 2) || lua_isnil(L, 2)) {
nodeSetSprite(node, NULL, 0, 0.0f, 0.0f, false);
return 0;
}
if (lua_gettop(L) >= 3) {
height = (float)_argNumber(L, "nodeSetText", 3);
}
surface = _renderText(L, "nodeSetText", _argString(L, "nodeSetText", 2));
width = (surface->h > 0) ? height * (float)surface->w / (float)surface->h : height;
ok = nodeSetSprite(node, &surface, 1, width, height, false);
SDL_DestroySurface(surface);
if (!ok) {
_luaDie(L, "nodeSetText", "Unable to put text on node %d (is 3D available?).", node);
}
return 0;
}
// nodeSetVisible(node, bool): hides the node and its children
static int32_t apiNodeSetVisible(lua_State *L) {
_argCheck(L, "nodeSetVisible", 2, 2);
nodeSetVisible(_argNode(L, "nodeSetVisible", 1), _argBoolean(L, "nodeSetVisible", 2));
return 0;
}
// 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);
}
_overlayResize(width, height);
_luaTrace(L, "overlaySetResolution", "%d %d", width, height);
return 0;
}
// node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz [, maxDistance]): nil when nothing is hit
static int32_t apiPhysicsRaycast(lua_State *L) {
Vec3T origin;
Vec3T direction;
float maxDistance = 0.0f;
int32_t node;
Vec3T point;
Vec3T normal;
_argCheck(L, "physicsRaycast", 6, 7);
origin = _argVec3(L, "physicsRaycast", 1);
direction = _argVec3(L, "physicsRaycast", 4);
if (lua_gettop(L) >= 7) {
maxDistance = (float)_argNumber(L, "physicsRaycast", 7);
}
if (!physicsRaycast(origin, direction, maxDistance, &node, &point, &normal)) {
lua_pushnil(L);
return 1;
}
lua_pushinteger(L, node);
_pushVec3(L, point);
_pushVec3(L, normal);
return 7;
}
// physicsSet2D(bool): bodies made from now on stay in the XY plane (2D games)
static int32_t apiPhysicsSet2D(lua_State *L) {
_argCheck(L, "physicsSet2D", 1, 1);
physicsSet2D(_argBoolean(L, "physicsSet2D", 1));
return 0;
}
// physicsSetDebug(mask): DEBUG_* flags added together, drawn as lines over the scene each frame; DEBUG_NONE stops
static int32_t apiPhysicsSetDebug(lua_State *L) {
_argCheck(L, "physicsSetDebug", 1, 1);
physicsSetDebug((uint32_t)_argInteger(L, "physicsSetDebug", 1));
return 0;
}
// physicsSetEnabled(bool): pauses the simulation without losing it
static int32_t apiPhysicsSetEnabled(lua_State *L) {
_argCheck(L, "physicsSetEnabled", 1, 1);
physicsSetEnabled(_argBoolean(L, "physicsSetEnabled", 1));
return 0;
}
// physicsSetGravity(x, y, z): default 0, -9.81, 0
static int32_t apiPhysicsSetGravity(lua_State *L) {
_argCheck(L, "physicsSetGravity", 3, 3);
physicsSetGravity(_argVec3(L, "physicsSetGravity", 1));
return 0;
}
static int32_t apiPlayerDelete(lua_State *L) {
_argCheck(L, "playerDelete", 1, 1);
playerDelete(_argPlayer(L, "playerDelete", 1));
return 0;
}
// ground, nx, ny, nz = playerGetGround(node): the node stood on (nil in the air) and the normal.
static int32_t apiPlayerGetGround(lua_State *L) {
Vec3T normal;
int32_t ground;
_argCheck(L, "playerGetGround", 1, 1);
ground = playerGetGround(_argPlayer(L, "playerGetGround", 1), &normal);
if (ground < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, ground);
}
_pushVec3(L, normal);
return 4;
}
static int32_t apiPlayerGetVelocity(lua_State *L) {
_argCheck(L, "playerGetVelocity", 1, 1);
_pushVec3(L, playerGetVelocity(_argPlayer(L, "playerGetVelocity", 1)));
return 3;
}
static int32_t apiPlayerIsOnGround(lua_State *L) {
_argCheck(L, "playerIsOnGround", 1, 1);
lua_pushboolean(L, playerIsOnGround(_argPlayer(L, "playerIsOnGround", 1)));
return 1;
}
static int32_t apiPlayerIsSwimming(lua_State *L) {
_argCheck(L, "playerIsSwimming", 1, 1);
lua_pushboolean(L, playerIsSwimming(_argPlayer(L, "playerIsSwimming", 1)));
return 1;
}
static int32_t apiPlayerJump(lua_State *L) {
_argCheck(L, "playerJump", 2, 2);
lua_pushboolean(L, playerJump(_argPlayer(L, "playerJump", 1), (float)_argNumber(L, "playerJump", 2)));
return 1;
}
// playerMove(node, vx, vz), playerMove(node, vx, vy, vz) when swimming, or in a 2D world playerMove(node, vx)
static int32_t apiPlayerMove(lua_State *L) {
int32_t node = 0;
float vx = 0.0f;
float vy = 0.0f;
float vz = 0.0f;
_argCheck(L, "playerMove", 2, 4);
node = _argPlayer(L, "playerMove", 1);
vx = (float)_argNumber(L, "playerMove", 2);
if (lua_gettop(L) == 3) {
vz = (float)_argNumber(L, "playerMove", 3);
} else if (lua_gettop(L) == 4) {
vy = (float)_argNumber(L, "playerMove", 3);
vz = (float)_argNumber(L, "playerMove", 4);
}
playerMove(node, vec3(vx, vy, vz));
return 0;
}
// playerNew(node, radius, height) for a capsule, or playerNew(node, SHAPE_*, a, b, c). Three arguments
// always mean a capsule, so a sphere or cylinder spells out every size (SHAPE_HULL needs none).
static int32_t apiPlayerNew(lua_State *L) {
int32_t node = 0;
int32_t shape = SHAPE_CAPSULE;
float dims[SHAPE_SIZES] = { 0.0f, 0.0f, 0.0f };
int32_t first = 2;
int32_t x = 0;
_argCheck(L, "playerNew", 2, 5);
node = _argNode(L, "playerNew", 1);
if (lua_isinteger(L, 2) && (lua_tointeger(L, 2) >= SHAPE_BOX) && (lua_tointeger(L, 2) <= SHAPE_MESH) && (lua_gettop(L) != 3)) {
shape = (int32_t)lua_tointeger(L, 2);
first = 3;
}
for (x = 0; x < SHAPE_SIZES; x++) {
if (lua_gettop(L) >= first + x) {
dims[x] = (float)_argNumber(L, "playerNew", first + x);
}
}
if (shape == SHAPE_MESH) {
_luaDie(L, "playerNew", "A player needs a convex shape; use SHAPE_HULL.");
}
if (!physicsAvailable()) {
_luaDie(L, "playerNew", "Physics is not available on this machine.");
}
if (!playerNew(node, (ShapeTypeE)shape, dims[0], dims[1], dims[2])) {
_luaDie(L, "playerNew", "Unable to create the player.");
}
_luaTrace(L, "playerNew", "node %d shape %d", node, shape);
return 0;
}
static int32_t apiPlayerSetEnabled(lua_State *L) {
_argCheck(L, "playerSetEnabled", 2, 2);
playerSetEnabled(_argPlayer(L, "playerSetEnabled", 1), _argBoolean(L, "playerSetEnabled", 2));
return 0;
}
static int32_t apiPlayerSetGravityScale(lua_State *L) {
_argCheck(L, "playerSetGravityScale", 2, 2);
playerSetGravityScale(_argPlayer(L, "playerSetGravityScale", 1), (float)_argNumber(L, "playerSetGravityScale", 2));
return 0;
}
static int32_t apiPlayerSetMass(lua_State *L) {
_argCheck(L, "playerSetMass", 2, 2);
playerSetMass(_argPlayer(L, "playerSetMass", 1), (float)_argNumber(L, "playerSetMass", 2));
return 0;
}
static int32_t apiPlayerSetPosition(lua_State *L) {
_argCheck(L, "playerSetPosition", 4, 4);
playerSetPosition(_argPlayer(L, "playerSetPosition", 1), _argVec3(L, "playerSetPosition", 2));
return 0;
}
static int32_t apiPlayerSetPush(lua_State *L) {
_argCheck(L, "playerSetPush", 2, 2);
playerSetPush(_argPlayer(L, "playerSetPush", 1), (float)_argNumber(L, "playerSetPush", 2));
return 0;
}
static int32_t apiPlayerSetSlope(lua_State *L) {
_argCheck(L, "playerSetSlope", 2, 2);
playerSetSlope(_argPlayer(L, "playerSetSlope", 1), (float)_argNumber(L, "playerSetSlope", 2));
return 0;
}
static int32_t apiPlayerSetStep(lua_State *L) {
_argCheck(L, "playerSetStep", 2, 2);
playerSetStep(_argPlayer(L, "playerSetStep", 1), (float)_argNumber(L, "playerSetStep", 2));
return 0;
}
// playerSetSwim(node, sinkSpeed, drag): how the player behaves in water
static int32_t apiPlayerSetSwim(lua_State *L) {
_argCheck(L, "playerSetSwim", 3, 3);
playerSetSwim(_argPlayer(L, "playerSetSwim", 1), (float)_argNumber(L, "playerSetSwim", 2), (float)_argNumber(L, "playerSetSwim", 3));
return 0;
}
static int32_t apiPlayerSetVelocity(lua_State *L) {
_argCheck(L, "playerSetVelocity", 4, 4);
playerSetVelocity(_argPlayer(L, "playerSetVelocity", 1), _argVec3(L, "playerSetVelocity", 2));
return 0;
}
static int32_t apiRagdollActivate(lua_State *L) {
_argCheck(L, "ragdollActivate", 1, 1);
if (!ragdollActivate(_argRagdoll(L, "ragdollActivate", 1))) {
_luaDie(L, "ragdollActivate", "Unable to build the ragdoll's bodies.");
}
return 0;
}
// ragdollApplyImpulse(node, jointName, x, y, z)
static int32_t apiRagdollApplyImpulse(lua_State *L) {
_argCheck(L, "ragdollApplyImpulse", 5, 5);
lua_pushboolean(L, ragdollApplyImpulse(_argRagdoll(L, "ragdollApplyImpulse", 1), _argString(L, "ragdollApplyImpulse", 2), _argVec3(L, "ragdollApplyImpulse", 3)));
return 1;
}
static int32_t apiRagdollDeactivate(lua_State *L) {
_argCheck(L, "ragdollDeactivate", 1, 1);
ragdollDeactivate(_argRagdoll(L, "ragdollDeactivate", 1));
return 0;
}
static int32_t apiRagdollDelete(lua_State *L) {
_argCheck(L, "ragdollDelete", 1, 1);
ragdollDelete(_argRagdoll(L, "ragdollDelete", 1));
return 0;
}
static int32_t apiRagdollIsActive(lua_State *L) {
_argCheck(L, "ragdollIsActive", 1, 1);
lua_pushboolean(L, ragdollIsActive(_argRagdoll(L, "ragdollIsActive", 1)));
return 1;
}
static int32_t apiRagdollIsResting(lua_State *L) {
_argCheck(L, "ragdollIsResting", 1, 1);
lua_pushboolean(L, ragdollIsResting(_argRagdoll(L, "ragdollIsResting", 1)));
return 1;
}
// ragdollNew(node): node is a model instance with a skin
static int32_t apiRagdollNew(lua_State *L) {
int32_t node = 0;
_argCheck(L, "ragdollNew", 1, 1);
node = _argNode(L, "ragdollNew", 1);
if (!physicsAvailable()) {
_luaDie(L, "ragdollNew", "Physics is not available on this machine.");
}
if (!ragdollNew(node)) {
_luaDie(L, "ragdollNew", "Node %d has no skinned model with bones under it.", node);
}
return 0;
}
// ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
static int32_t apiRagdollSetJoint(lua_State *L) {
_argCheck(L, "ragdollSetJoint", 5, 5);
if (!ragdollSetJoint(_argRagdoll(L, "ragdollSetJoint", 1), _argString(L, "ragdollSetJoint", 2), (float)_argNumber(L, "ragdollSetJoint", 3), (float)_argNumber(L, "ragdollSetJoint", 4), (float)_argNumber(L, "ragdollSetJoint", 5))) {
_luaDie(L, "ragdollSetJoint", "No bone by that name.");
}
return 0;
}
static int32_t apiRagdollSetStrength(lua_State *L) {
_argCheck(L, "ragdollSetStrength", 2, 2);
ragdollSetStrength(_argRagdoll(L, "ragdollSetStrength", 1), (float)_argNumber(L, "ragdollSetStrength", 2));
return 0;
}
// sceneEnable(bool): 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;
sceneGetSize(&width, &height);
lua_pushinteger(L, width);
lua_pushinteger(L, height);
return 2;
}
// sceneGetStats(): last frame's draws collected, inside the view, and draw calls made
static int32_t apiSceneGetStats(lua_State *L) {
int32_t total = 0;
int32_t drawn = 0;
int32_t batches = 0;
int64_t textureBytes = 0;
sceneGetStats(&total, &drawn, &batches, &textureBytes);
lua_pushinteger(L, total);
lua_pushinteger(L, drawn);
lua_pushinteger(L, batches);
lua_pushinteger(L, textureBytes / BYTES_PER_KIB);
return 4;
}
// 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;
}
// sceneSetBloom(threshold, strength): the glow of everything brighter than the threshold; strength 0 for none
static int32_t apiSceneSetBloom(lua_State *L) {
_argCheck(L, "sceneSetBloom", 2, 2);
sceneSetBloom((float)_argNumber(L, "sceneSetBloom", 1), (float)_argNumber(L, "sceneSetBloom", 2));
return 0;
}
// sceneSetEnvironment(lit): whether the sky lights the scene
static int32_t apiSceneSetEnvironment(lua_State *L) {
_argCheck(L, "sceneSetEnvironment", 1, 1);
sceneSetEnvironment(_argBoolean(L, "sceneSetEnvironment", 1));
return 0;
}
static int32_t apiSceneSetExposure(lua_State *L) {
_argCheck(L, "sceneSetExposure", 1, 1);
sceneSetExposure((float)_argNumber(L, "sceneSetExposure", 1));
return 0;
}
// sceneSetFog(r, g, b, near, far) or sceneSetFog() for none
static int32_t apiSceneSetFog(lua_State *L) {
_argCheck(L, "sceneSetFog", 0, 5);
if (lua_gettop(L) == 0) {
sceneSetFog(0, 0, 0, 0.0f, 0.0f);
return 0;
}
if (lua_gettop(L) != 5) {
_luaDie(L, "sceneSetFog", "Expected r, g, b, near, far or nothing.");
}
sceneSetFog((uint8_t)_argInteger(L, "sceneSetFog", 1), (uint8_t)_argInteger(L, "sceneSetFog", 2), (uint8_t)_argInteger(L, "sceneSetFog", 3), (float)_argNumber(L, "sceneSetFog", 4), (float)_argNumber(L, "sceneSetFog", 5));
return 0;
}
// sceneSetShadowCascades(count): 1 to 4 maps along the view for a directional light's shadow
static int32_t apiSceneSetShadowCascades(lua_State *L) {
_argCheck(L, "sceneSetShadowCascades", 1, 1);
sceneSetShadowCascades(_argInteger(L, "sceneSetShadowCascades", 1));
return 0;
}
// sceneSetShadowDistance(distance): how far from the camera cascaded shadows reach
static int32_t apiSceneSetShadowDistance(lua_State *L) {
_argCheck(L, "sceneSetShadowDistance", 1, 1);
sceneSetShadowDistance((float)_argNumber(L, "sceneSetShadowDistance", 1));
return 0;
}
// sceneSetShadowSize(size): shadow map texels per side, 256 to 4096 (default 1024)
static int32_t apiSceneSetShadowSize(lua_State *L) {
_argCheck(L, "sceneSetShadowSize", 1, 1);
sceneSetShadowSize(_argInteger(L, "sceneSetShadowSize", 1));
return 0;
}
// sceneSetSky(file) from an equirectangular image (Radiance .hdr keeps its range; PNG or JPEG is
// decoded from sRGB), or sceneSetSky() / sceneSetSky(nil) for none
static int32_t apiSceneSetSky(lua_State *L) {
float *rgb = NULL;
int32_t width = 0;
int32_t height = 0;
_argCheck(L, "sceneSetSky", 0, 1);
if ((lua_gettop(L) == 0) || lua_isnil(L, 1)) {
sceneSetSky(NULL, 0, 0);
return 0;
}
rgb = hdrLoad(_argString(L, "sceneSetSky", 1), &width, &height);
if (rgb == NULL) {
_luaDie(L, "sceneSetSky", "%s", SDL_GetError());
}
if (!sceneSetSky(rgb, width, height)) {
SDL_free(rgb);
_luaDie(L, "sceneSetSky", "Unable to build the sky (is 3D available?).");
}
SDL_free(rgb);
_luaTrace(L, "sceneSetSky", "%dx%d", width, height);
return 0;
}
// sceneSetSkyIntensity(scale): brightness of the sky and its light, 1 as loaded
static int32_t apiSceneSetSkyIntensity(lua_State *L) {
_argCheck(L, "sceneSetSkyIntensity", 1, 1);
sceneSetSkyIntensity((float)_argNumber(L, "sceneSetSkyIntensity", 1));
return 0;
}
static int32_t apiSceneSetTonemap(lua_State *L) {
int32_t tonemap = 0;
_argCheck(L, "sceneSetTonemap", 1, 1);
tonemap = _argInteger(L, "sceneSetTonemap", 1);
if ((tonemap != TONEMAP_NONE) && (tonemap != TONEMAP_NEUTRAL) && (tonemap != TONEMAP_ACES)) {
_luaDie(L, "sceneSetTonemap", "Tonemap must be TONEMAP_NONE, TONEMAP_NEUTRAL or TONEMAP_ACES.");
}
sceneSetTonemap((SceneTonemapE)tonemap);
return 0;
}
// x, y, z = sceneUnproject(sx, sy, distance): the world point that far along the ray through an overlay point
static int32_t apiSceneUnproject(lua_State *L) {
_argCheck(L, "sceneUnproject", 3, 3);
return _pushVec3(L, sceneUnproject((float)_argNumber(L, "sceneUnproject", 1), (float)_argNumber(L, "sceneUnproject", 2), (float)_argNumber(L, "sceneUnproject", 3)));
}
// scriptExecute(config) Runs another script after this one ends.
static int32_t apiScriptExecute(lua_State *L) {
ConfigT *conf = _scriptConfFromTable(L, "scriptExecute");
queueScript(conf);
destroyConf(&conf);
_global.running = false;
_luaTrace(L, "scriptExecute", "Queued.");
return 0;
}
// scriptPush(config) Runs another script, then returns to this one.
static int32_t apiScriptPush(lua_State *L) {
ConfigT *conf = _scriptConfFromTable(L, "scriptPush");
queueScript(conf);
destroyConf(&conf);
queueScript(_global.conf);
_global.running = false;
_luaTrace(L, "scriptPush", "Queued.");
return 0;
}
// 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;
}
// singeReload(): runs the game again from its script at the end of this frame
static int32_t apiSingeReload(lua_State *L) {
_luaTrace(L, "singeReload", "Reload requested.");
_global.reloadRequested = true;
return 0;
}
// singeScreenshot() Saved after the next frame is drawn.
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;
}
static int32_t apiSoftDelete(lua_State *L) {
_argCheck(L, "softDelete", 1, 1);
softDelete(_argSoft(L, "softDelete", 1));
return 0;
}
// softNew(node, SOFT_CLOTH | SOFT_BODY) from the node's mesh, or softNew(node, SOFT_ROPE, x, y, z, segments, radius)
static int32_t apiSoftNew(lua_State *L) {
int32_t node = 0;
int32_t kind = 0;
bool ok = false;
_argCheck(L, "softNew", 2, 7);
node = _argNode(L, "softNew", 1);
kind = _argInteger(L, "softNew", 2);
if (!physicsAvailable()) {
_luaDie(L, "softNew", "Physics is not available on this machine.");
}
if (kind == SOFT_ROPE) {
_argCheck(L, "softNew", 7, 7);
ok = softNewRope(node, _argVec3(L, "softNew", 3), _argInteger(L, "softNew", 6), (float)_argNumber(L, "softNew", 7));
} else if ((kind == SOFT_CLOTH) || (kind == SOFT_BODY)) {
_argCheck(L, "softNew", 2, 2);
ok = softNew(node, (SoftKindE)kind);
} else {
_luaDie(L, "softNew", "Unknown soft body kind %d.", kind);
}
if (!ok) {
_luaDie(L, "softNew", "Unable to make the soft body: the node needs a mesh (or a rope needs a length).");
}
_luaTrace(L, "softNew", "node %d kind %d", node, kind);
return 0;
}
// softPin(node, x, y, z[, otherNode]): hold the nearest particle there, or to that node
static int32_t apiSoftPin(lua_State *L) {
int32_t follow = -1;
_argCheck(L, "softPin", 4, 5);
if (lua_gettop(L) == 5) {
follow = _argNode(L, "softPin", 5);
}
softPin(_argSoft(L, "softPin", 1), _argVec3(L, "softPin", 2), follow);
return 0;
}
static int32_t apiSoftSetDamping(lua_State *L) {
_argCheck(L, "softSetDamping", 2, 2);
softSetDamping(_argSoft(L, "softSetDamping", 1), (float)_argNumber(L, "softSetDamping", 2));
return 0;
}
static int32_t apiSoftSetMass(lua_State *L) {
_argCheck(L, "softSetMass", 2, 2);
softSetMass(_argSoft(L, "softSetMass", 1), (float)_argNumber(L, "softSetMass", 2));
return 0;
}
static int32_t apiSoftSetPressure(lua_State *L) {
_argCheck(L, "softSetPressure", 2, 2);
softSetPressure(_argSoft(L, "softSetPressure", 1), (float)_argNumber(L, "softSetPressure", 2));
return 0;
}
static int32_t apiSoftSetStiffness(lua_State *L) {
_argCheck(L, "softSetStiffness", 3, 3);
softSetStiffness(_argSoft(L, "softSetStiffness", 1), (float)_argNumber(L, "softSetStiffness", 2), (float)_argNumber(L, "softSetStiffness", 3));
return 0;
}
static int32_t apiSoftUnpin(lua_State *L) {
_argCheck(L, "softUnpin", 4, 4);
softUnpin(_argSoft(L, "softUnpin", 1), _argVec3(L, "softUnpin", 2));
return 0;
}
// soundFullStop() Halts every sound effect channel.
static int32_t apiSoundFullStop(lua_State *L) {
_luaTrace(L, "soundFullStop", "Halting all channels.");
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
return 0;
}
// soundGetPosition(channel): where a positioned channel sits relative to the listener (x right, y up,
// z back, unit length) and its distance gain; a channel with no position returns 0, 0, 0, 1.
static int32_t apiSoundGetPosition(lua_State *L) {
int32_t channel;
EffectT *effect;
_argCheck(L, "soundGetPosition", 1, 1);
channel = _argChannel(L, "soundGetPosition", 1);
effect = &_effects[channel];
lua_pushnumber(L, effect->relative[0]);
lua_pushnumber(L, effect->relative[1]);
lua_pushnumber(L, effect->relative[2]);
lua_pushnumber(L, effect->gain);
return 4;
}
// volume = soundGetVolume() 0 to AUDIO_MAX_VOLUME.
static int32_t apiSoundGetVolume(lua_State *L) {
_luaTrace(L, "soundGetVolume", "%d", _global.effectsVolume);
lua_pushinteger(L, _global.effectsVolume);
return 1;
}
// playing = soundIsPlaying(channel)
static int32_t apiSoundIsPlaying(lua_State *L) {
int32_t channel = 0;
bool playing = false;
_argCheck(L, "soundIsPlaying", 1, 1);
channel = _argChannel(L, "soundIsPlaying", 1);
playing = MIX_TrackPlaying(_effectTracks[channel]);
_luaTrace(L, "soundIsPlaying", "%d %d", channel, playing);
lua_pushboolean(L, playing);
return 1;
}
// id = soundLoad(filename)
static int32_t apiSoundLoad(lua_State *L) {
const char *name = NULL;
SoundT *sound = NULL;
SDL_IOStream *io = NULL;
_argCheck(L, "soundLoad", 1, 1);
name = _argString(L, "soundLoad", 1);
io = vfsOpenIO(name);
if (io == NULL) {
_luaDie(L, "soundLoad", "Unable to open %s", name);
}
sound = (SoundT *)calloc(1, sizeof(SoundT));
if (!sound) {
_luaDie(L, "soundLoad", "Unable to allocate new sound.");
}
sound->audio = MIX_LoadAudio_IO(videoGetMixer(), io, 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 = _argChannel(L, "soundPause", 1);
playing = MIX_TrackPlaying(_effectTracks[channel]);
MIX_PauseTrack(_effectTracks[channel]);
_luaTrace(L, "soundPause", "%d %d", channel, playing);
lua_pushboolean(L, playing);
return 1;
}
// channel = soundPlay(id) Returns -1 (SOUND_ERROR_INVALID) when every channel is busy.
// soundPlay(id [, loops]): loops 0 (the default) plays once, N repeats N more times, -1 forever
static int32_t apiSoundPlay(lua_State *L) {
SoundT *sound = NULL;
int32_t channel = SOUND_CHANNEL_NONE;
int32_t loops = 0;
SDL_PropertiesID options;
_argCheck(L, "soundPlay", 1, 2);
sound = _argSound(L, "soundPlay", 1);
if (lua_gettop(L) >= 2) {
loops = _argInteger(L, "soundPlay", 2);
if (loops < SOUND_LOOP_FOREVER) {
_luaDie(L, "soundPlay", "Loops must be -1 (forever), 0 (once) or a repeat count: %d", loops);
}
}
channel = _effectTrackFree();
if (channel >= 0) {
_effectReset(channel);
MIX_SetTrackAudio(_effectTracks[channel], sound->audio);
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume));
options = SDL_CreateProperties();
SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loops);
if (!MIX_PlayTrack(_effectTracks[channel], options)) {
SDL_DestroyProperties(options);
_luaDie(L, "soundPlay", "%s", SDL_GetError());
}
SDL_DestroyProperties(options);
}
_luaTrace(L, "soundPlay", "%d %d loops %d", sound->id, channel, loops);
lua_pushinteger(L, channel);
return 1;
}
// wasPaused = soundResume(channel)
static int32_t apiSoundResume(lua_State *L) {
int32_t channel = 0;
bool paused = false;
_argCheck(L, "soundResume", 1, 1);
channel = _argChannel(L, "soundResume", 1);
paused = MIX_TrackPaused(_effectTracks[channel]);
MIX_ResumeTrack(_effectTracks[channel]);
_luaTrace(L, "soundResume", "%d %d", channel, paused);
lua_pushboolean(L, paused);
return 1;
}
// soundSetListener([node]): positioned sounds are heard from this node; none means the scene camera
static int32_t apiSoundSetListener(lua_State *L) {
_argCheck(L, "soundSetListener", 0, 1);
_global.listenerNode = ((lua_gettop(L) >= 1) && !lua_isnil(L, 1)) ? _argNode(L, "soundSetListener", 1) : LISTENER_CAMERA;
_luaTrace(L, "soundSetListener", "%d", _global.listenerNode);
return 0;
}
// soundSetNode(channel [, node]): the channel follows the node through the scene; none stops following
static int32_t apiSoundSetNode(lua_State *L) {
int32_t channel;
EffectT *effect;
_argCheck(L, "soundSetNode", 1, 2);
channel = _argChannel(L, "soundSetNode", 1);
effect = &_effects[channel];
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
effect->node = _argNode(L, "soundSetNode", 2);
effect->positioned = true;
} else {
effect->node = LISTENER_CAMERA;
effect->positioned = false;
MIX_SetTrack3DPosition(_effectTracks[channel], NULL);
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume));
}
return 0;
}
// soundSetPan(channel, pan): -1 left to 1 right for sounds that are not in the scene
static int32_t apiSoundSetPan(lua_State *L) {
int32_t channel;
float pan;
MIX_StereoGains gains;
_argCheck(L, "soundSetPan", 2, 2);
channel = _argChannel(L, "soundSetPan", 1);
pan = SDL_clamp((float)_argNumber(L, "soundSetPan", 2), -1.0f, 1.0f);
_effects[channel].positioned = false;
_effects[channel].node = LISTENER_CAMERA;
gains.left = SDL_min(1.0f, 1.0f - pan);
gains.right = SDL_min(1.0f, 1.0f + pan);
MIX_SetTrackStereo(_effectTracks[channel], &gains);
MIX_SetTrackGain(_effectTracks[channel], _mixerGain(_global.effectsVolume));
return 0;
}
// soundSetPosition(channel, x, y, z): places the channel in the scene, in world units
static int32_t apiSoundSetPosition(lua_State *L) {
int32_t channel;
EffectT *effect;
_argCheck(L, "soundSetPosition", 4, 4);
channel = _argChannel(L, "soundSetPosition", 1);
effect = &_effects[channel];
effect->node = LISTENER_CAMERA;
effect->position = vec3((float)_argNumber(L, "soundSetPosition", 2), (float)_argNumber(L, "soundSetPosition", 3), (float)_argNumber(L, "soundSetPosition", 4));
effect->positioned = true;
return 0;
}
// soundSetRange(channel, near, far): full volume within near, silent beyond far
static int32_t apiSoundSetRange(lua_State *L) {
int32_t channel;
EffectT *effect;
float nearBy;
float farOff;
_argCheck(L, "soundSetRange", 3, 3);
channel = _argChannel(L, "soundSetRange", 1);
nearBy = (float)_argNumber(L, "soundSetRange", 2);
farOff = (float)_argNumber(L, "soundSetRange", 3);
if ((nearBy <= 0.0f) || (farOff <= nearBy)) {
_luaDie(L, "soundSetRange", "Range needs 0 < near < far: %.2f %.2f", nearBy, farOff);
}
effect = &_effects[channel];
effect->nearBy = nearBy;
effect->farOff = farOff;
return 0;
}
// soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel.
static int32_t apiSoundSetVolume(lua_State *L) {
int32_t volume = 0;
_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 = _argChannel(L, "soundStop", 1);
playing = MIX_TrackPlaying(_effectTracks[channel]);
MIX_StopTrack(_effectTracks[channel], 0);
_luaTrace(L, "soundStop", "%d %d", channel, playing);
lua_pushboolean(L, playing);
return 1;
}
// soundUnload(id)
static int32_t apiSoundUnload(lua_State *L) {
SoundT *sound = NULL;
_argCheck(L, "soundUnload", 1, 1);
sound = _argSound(L, "soundUnload", 1);
_luaTrace(L, "soundUnload", "%d", sound->id);
_soundDestroy(sound);
return 0;
}
// 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;
// Whole loops (after a long pause, say) land on the same frame, so they need not be stepped.
if (sprite->loop && (sprite->ticks >= sprite->loopMs)) {
sprite->ticks %= sprite->loopMs;
}
while (sprite->animating) {
delay = sprite->animation->delays[sprite->currentFrame];
if (delay < ANIMATION_MIN_DELAY_MS) {
delay = ANIMATION_MIN_DELAY_MS;
}
if (sprite->ticks < (uint64_t)delay) {
break;
}
sprite->ticks -= (uint64_t)delay;
sprite->currentFrame++;
newFrame = true;
if (sprite->currentFrame >= sprite->animation->count) {
if (sprite->loop) {
sprite->currentFrame = 0;
} else {
sprite->currentFrame = sprite->animation->count - 1;
sprite->animating = false;
}
}
}
if (newFrame) {
sprite->originalSurface = sprite->animation->frames[sprite->currentFrame];
_spriteRebuildSurface(sprite);
}
}
if (!stretched) {
dest.w = sprite->surface->w;
dest.h = sprite->surface->h;
}
if (center) {
// Move sprite so the drawing coordinate is the center of the sprite
dest.x -= dest.w / 2;
dest.y -= dest.h / 2;
}
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 (a pixel copy; a blit would premultiply the alpha).
sprite->originalSurface = SDL_DuplicateSurface(sprite->animation->frames[0]);
if (sprite->originalSurface == NULL) {
_luaDie(L, "spriteLoad", "%s", SDL_GetError());
}
_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->loopMs += (uint64_t)SDL_max(sprite->animation->delays[x], ANIMATION_MIN_DELAY_MS);
}
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());
}
if (sprite->animation == NULL) {
// An animation's frames were keyed above, this one included.
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;
}
// height = terrainGetHeight(node, x, z): the terrain's height at a world x, z, or nil off it
static int32_t apiTerrainGetHeight(lua_State *L) {
float height = 0.0f;
_argCheck(L, "terrainGetHeight", 3, 3);
if (!terrainGetHeight(_argNode(L, "terrainGetHeight", 1), (float)_argNumber(L, "terrainGetHeight", 2), (float)_argNumber(L, "terrainGetHeight", 3), &height)) {
lua_pushnil(L);
return 1;
}
lua_pushnumber(L, height);
return 1;
}
// index = vehicleAddWheel(vehicle, wheelNode, radius, width, suspensionLength)
static int32_t apiVehicleAddWheel(lua_State *L) {
int32_t index = 0;
_argCheck(L, "vehicleAddWheel", 5, 5);
index = vehicleAddWheel(_argVehicle(L, "vehicleAddWheel", 1), _argNode(L, "vehicleAddWheel", 2), (float)_argNumber(L, "vehicleAddWheel", 3), (float)_argNumber(L, "vehicleAddWheel", 4), (float)_argNumber(L, "vehicleAddWheel", 5));
if (index < 0) {
_luaDie(L, "vehicleAddWheel", "Unable to add the wheel (too many?).");
}
lua_pushinteger(L, index);
return 1;
}
static int32_t apiVehicleDelete(lua_State *L) {
_argCheck(L, "vehicleDelete", 1, 1);
vehicleDelete(_argVehicle(L, "vehicleDelete", 1));
return 0;
}
// vehicleDrive(vehicle, forward, right, brake, handBrake)
static int32_t apiVehicleDrive(lua_State *L) {
int32_t node = 0;
float in[VEHICLE_DRIVE_INPUTS] = { 0.0f, 0.0f, 0.0f, 0.0f };
int32_t x = 0;
_argCheck(L, "vehicleDrive", 3, 5);
node = _argVehicle(L, "vehicleDrive", 1);
for (x = 0; x < VEHICLE_DRIVE_INPUTS; x++) {
if (lua_gettop(L) >= 2 + x) {
in[x] = (float)_argNumber(L, "vehicleDrive", 2 + x);
}
}
vehicleDrive(node, in[0], in[1], in[2], in[3]);
return 0;
}
static int32_t apiVehicleGetGear(lua_State *L) {
_argCheck(L, "vehicleGetGear", 1, 1);
lua_pushinteger(L, vehicleGetGear(_argVehicle(L, "vehicleGetGear", 1)));
return 1;
}
static int32_t apiVehicleGetRpm(lua_State *L) {
_argCheck(L, "vehicleGetRpm", 1, 1);
lua_pushnumber(L, (lua_Number)vehicleGetRpm(_argVehicle(L, "vehicleGetRpm", 1)));
return 1;
}
static int32_t apiVehicleGetSpeed(lua_State *L) {
_argCheck(L, "vehicleGetSpeed", 1, 1);
lua_pushnumber(L, (lua_Number)vehicleGetSpeed(_argVehicle(L, "vehicleGetSpeed", 1)));
return 1;
}
static int32_t apiVehicleGetWheelSlip(lua_State *L) {
_argCheck(L, "vehicleGetWheelSlip", 2, 2);
lua_pushnumber(L, (lua_Number)vehicleGetWheelSlip(_argVehicle(L, "vehicleGetWheelSlip", 1), _argInteger(L, "vehicleGetWheelSlip", 2)));
return 1;
}
static int32_t apiVehicleIsWheelOnGround(lua_State *L) {
_argCheck(L, "vehicleIsWheelOnGround", 2, 2);
lua_pushboolean(L, vehicleIsWheelOnGround(_argVehicle(L, "vehicleIsWheelOnGround", 1), _argInteger(L, "vehicleIsWheelOnGround", 2)));
return 1;
}
// vehicleNew(node, VEHICLE_CAR | VEHICLE_MOTORCYCLE | VEHICLE_TANK | VEHICLE_BOAT): the node must carry a dynamic body
static int32_t apiVehicleNew(lua_State *L) {
int32_t node = 0;
int32_t kind = 0;
_argCheck(L, "vehicleNew", 2, 2);
node = _argBody(L, "vehicleNew", 1);
kind = _argInteger(L, "vehicleNew", 2);
if ((kind < VEHICLE_CAR) || (kind > VEHICLE_BOAT)) {
_luaDie(L, "vehicleNew", "Unknown vehicle kind %d.", kind);
}
if (!vehicleNew(node, (VehicleKindE)kind)) {
_luaDie(L, "vehicleNew", "Unable to create the vehicle: the node needs a dynamic body, in a 3D world.");
}
_luaTrace(L, "vehicleNew", "node %d kind %d", node, kind);
return 0;
}
static int32_t apiVehicleSetAntiRoll(lua_State *L) {
_argCheck(L, "vehicleSetAntiRoll", 2, 2);
vehicleSetAntiRoll(_argVehicle(L, "vehicleSetAntiRoll", 1), (float)_argNumber(L, "vehicleSetAntiRoll", 2));
return 0;
}
static int32_t apiVehicleSetBrakes(lua_State *L) {
_argCheck(L, "vehicleSetBrakes", 3, 3);
vehicleSetBrakes(_argVehicle(L, "vehicleSetBrakes", 1), (float)_argNumber(L, "vehicleSetBrakes", 2), (float)_argNumber(L, "vehicleSetBrakes", 3));
return 0;
}
// vehicleSetEngine(vehicle, maxTorque, maxRpm[, minRpm])
static int32_t apiVehicleSetEngine(lua_State *L) {
_argCheck(L, "vehicleSetEngine", 3, 4);
vehicleSetEngine(_argVehicle(L, "vehicleSetEngine", 1), (float)_argNumber(L, "vehicleSetEngine", 2), (float)_argNumber(L, "vehicleSetEngine", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "vehicleSetEngine", 4) : VEHICLE_DEFAULT_MIN_RPM);
return 0;
}
// vehicleSetGears(vehicle, {ratio, ...}[, reverseRatio][, automatic])
static int32_t apiVehicleSetGears(lua_State *L) {
int32_t node = 0;
int32_t count = 0;
float *ratios = NULL;
float reverse = VEHICLE_DEFAULT_REVERSE_GEAR;
bool automatic = true;
_argCheck(L, "vehicleSetGears", 2, 4);
node = _argVehicle(L, "vehicleSetGears", 1);
ratios = _argFloatTable(L, "vehicleSetGears", 2, &count);
if (lua_gettop(L) >= 3) {
reverse = (float)_argNumber(L, "vehicleSetGears", 3);
}
if (lua_gettop(L) == 4) {
automatic = _argBoolean(L, "vehicleSetGears", 4);
}
if ((count < 1) || (count > VEHICLE_MAX_GEARS) || !vehicleSetGears(node, ratios, count, reverse, automatic)) {
SDL_free(ratios);
_luaDie(L, "vehicleSetGears", "Give one to %d forward gear ratios.", VEHICLE_MAX_GEARS);
}
SDL_free(ratios);
return 0;
}
static int32_t apiVehicleSetSteering(lua_State *L) {
_argCheck(L, "vehicleSetSteering", 2, 2);
vehicleSetSteering(_argVehicle(L, "vehicleSetSteering", 1), (float)_argNumber(L, "vehicleSetSteering", 2));
return 0;
}
// vehicleSetRudder(boat, maxTorque)
static int32_t apiVehicleSetRudder(lua_State *L) {
_argCheck(L, "vehicleSetRudder", 2, 2);
vehicleSetRudder(_argVehicle(L, "vehicleSetRudder", 1), (float)_argNumber(L, "vehicleSetRudder", 2));
return 0;
}
static int32_t apiVehicleSetSuspension(lua_State *L) {
_argCheck(L, "vehicleSetSuspension", 3, 3);
vehicleSetSuspension(_argVehicle(L, "vehicleSetSuspension", 1), (float)_argNumber(L, "vehicleSetSuspension", 2), (float)_argNumber(L, "vehicleSetSuspension", 3));
return 0;
}
// vehicleSetThrust(boat, maxForce, x, y, z): the propeller's push and where it pushes, in the hull
static int32_t apiVehicleSetThrust(lua_State *L) {
_argCheck(L, "vehicleSetThrust", 5, 5);
vehicleSetThrust(_argVehicle(L, "vehicleSetThrust", 1), (float)_argNumber(L, "vehicleSetThrust", 2), _argVec3(L, "vehicleSetThrust", 3));
return 0;
}
// vehicleSetWheel(vehicle, index, steered, driven)
static int32_t apiVehicleSetWheel(lua_State *L) {
_argCheck(L, "vehicleSetWheel", 4, 4);
if (!vehicleSetWheel(_argVehicle(L, "vehicleSetWheel", 1), _argInteger(L, "vehicleSetWheel", 2), _argBoolean(L, "vehicleSetWheel", 3), _argBoolean(L, "vehicleSetWheel", 4))) {
_luaDie(L, "vehicleSetWheel", "No such wheel.");
}
return 0;
}
// videoDraw(id, x, y, x2, y2) - Stretch the frame into the rectangle
// videoDraw(id, x, y, centered) - Draw with the video's rotation and scale
static int32_t apiVideoDraw(lua_State *L) {
int32_t n = lua_gettop(L);
VideoT *video = NULL;
bool center = false;
bool newFrame = false;
int64_t frame = 0;
const uint8_t *pixels = NULL;
int32_t pitch = 0;
SDL_Surface *source = NULL;
SDL_Surface *rgba = NULL;
SDL_Rect dest;
_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 = SDL_clamp(_argInteger(L, "videoSetVolume", 2), 0, VIDEO_VOLUME_MAX);
right = SDL_clamp(_argInteger(L, "videoSetVolume", 3), 0, VIDEO_VOLUME_MAX);
videoSetVolume(video->handle, left, right);
_luaTrace(L, "videoSetVolume", "%d %d %d", video->id, left, right);
return 0;
}
// videoUnload(id)
static int32_t apiVideoUnload(lua_State *L) {
VideoT *video = NULL;
_argCheck(L, "videoUnload", 1, 1);
video = _argVideo(L, "videoUnload", 1);
_luaTrace(L, "videoUnload", "%d", video->id);
_videoDestroy(video);
return 0;
}
// viewDelete(view)
static int32_t apiViewDelete(lua_State *L) {
_argCheck(L, "viewDelete", 1, 1);
viewDelete(_argView(L, "viewDelete", 1));
return 0;
}
// view = viewNew(width, height): a camera rendered to a texture every frame, for materialSetView
static int32_t apiViewNew(lua_State *L) {
int32_t view;
_argCheck(L, "viewNew", 2, 2);
view = viewNew(_argInteger(L, "viewNew", 1), _argInteger(L, "viewNew", 2));
if (view < 0) {
_luaDie(L, "viewNew", "No view available (is 3D available, and are fewer than %d in use?).", MAX_VIEWS);
}
lua_pushinteger(L, view);
return 1;
}
// viewSetCamera(view [, node]): the node the view looks from; none for the default view
static int32_t apiViewSetCamera(lua_State *L) {
int32_t view;
int32_t camera = -1;
_argCheck(L, "viewSetCamera", 1, 2);
view = _argView(L, "viewSetCamera", 1);
if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) {
camera = _argNode(L, "viewSetCamera", 2);
}
viewSetCamera(view, camera);
return 0;
}
// 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, VFS_GAMES_DAT, NULL, false) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
utilDie("%s: %s", base->container, lua_tostring(L, -1));
}
lua_getglobal(L, "GAMES");
if (!lua_istable(L, -1) || (lua_rawgeti(L, -1, conf->entry) != LUA_TTABLE)) {
utilDie("%s: games.dat has no entry %d.", base->container, conf->entry);
}
lua_replace(L, 1);
lua_settop(L, 1);
result = _buildConfFromTable(L, base);
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;
SDL_FRect windowTarget;
SDL_FRect scaledTarget;
SDL_FRect sindenWhite;
SDL_FRect sindenBlack;
SDL_Texture *sceneTexture = NULL;
SDL_Color sindenWhiteColor = { COLOR_BYTE_MAX, COLOR_BYTE_MAX, COLOR_BYTE_MAX, SDL_ALPHA_OPAQUE };
SDL_Color sindenBlackColor = { 0, 0, 0, SDL_ALPHA_OPAQUE };
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.frameFileHandle = -1;
_global.videoHandle = -1;
_global.mouseMode = MOUSE_SINGLE;
_global.controllerDeadZone = CONTROLLER_DEAD_ZONE_DEFAULT;
_global.running = true;
_global.discStopped = true;
_global.mouseEnabled = true;
_global.window = window;
_global.renderer = renderer;
_global.device = device;
_subsystemsInit();
// 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());
_loadControlMappings();
// Show splash screens
if (!_global.conf->noLogos) {
_progTrace("Showing splash screens");
_doLogos();
}
// Start Lua for game
_createScriptContext();
// 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 (_global.conf->resolutionWasCalculated && !_global.conf->fullScreen && !_global.conf->fullScreenWindow) {
if (videoWidth * _global.conf->yResolution > videoHeight * _global.conf->xResolution) {
// Video is wider than the window: keep the width, shrink the height.
_global.conf->yResolution = _global.conf->xResolution * videoHeight / videoWidth;
} else {
// Video is taller: keep the height, shrink the width.
_global.conf->xResolution = _global.conf->yResolution * videoWidth / videoHeight;
}
_progTrace("Resizing window to %dx%d based on main video file", _global.conf->xResolution, _global.conf->yResolution);
SDL_SetWindowSize(_global.window, _global.conf->xResolution, _global.conf->yResolution);
SDL_SyncWindow(_global.window);
SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(_global.renderer);
}
// Everything renders in video coordinates, letterboxed unless the user wants it stretched, and
// either way mouse positions convert back into those coordinates.
SDL_SetRenderLogicalPresentation(_global.renderer, videoWidth, videoHeight, _global.conf->stretchVideo ? SDL_LOGICAL_PRESENTATION_STRETCH : 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;
// 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;
}
// Overscan compensation shrinks whatever the game is drawn into (the whole window, or the inside
// of the Sinden border) about its centre.
if (_global.conf->scaleFactor < SCALE_FACTOR_MAX) {
scaledTarget.w = windowTarget.w * (float)_global.conf->scaleFactor / (float)SCALE_FACTOR_MAX;
scaledTarget.h = windowTarget.h * (float)_global.conf->scaleFactor / (float)SCALE_FACTOR_MAX;
scaledTarget.x = windowTarget.x + (windowTarget.w - scaledTarget.w) / 2.0f;
scaledTarget.y = windowTarget.y + (windowTarget.h - scaledTarget.h) / 2.0f;
windowTarget = scaledTarget;
}
// Create overlay surface and its texture
x = (int32_t)(videoWidth * OVERLAY_SCALE_DEFAULT);
y = (int32_t)(videoHeight * OVERLAY_SCALE_DEFAULT);
_progTrace("Creating overlay of %dx%d", x, y);
_overlayResize(x, y);
// Mouse setup
_global.mouseEnabled = !_global.conf->noMouse;
_progTrace("Initializing ManyMouse");
_global.mouseCount = ManyMouse_Init();
_progTrace("Mouse Driver: %s", ManyMouse_DriverName());
_progTrace("Mice Found: %d", _global.mouseCount);
if (_global.mouseCount < 0) {
_global.mouseCount = 0;
}
if (_global.mouseCount > MAX_MICE) {
_global.mouseCount = MAX_MICE;
}
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();
// 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);
}
// The script's own defaults, now that everything they touch exists.
_resetScriptState();
// Load overlay font
_progTrace("Loading console font");
_global.consoleFontSurface = _loadEmbeddedPng(font_png, font_png_len);
_global.consoleFontWidth = _global.consoleFontSurface->w / CONSOLE_FONT_GLYPHS;
_global.consoleFontHeight = _global.consoleFontSurface->h;
_progTrace("Console font is %dx%d, %s", _global.consoleFontSurface->w, _global.consoleFontSurface->h, SDL_GetPixelFormatName(_global.consoleFontSurface->format));
// The glyph background is whatever colour the top left pixel has (magenta in the shipped font).
SDL_SetSurfaceColorKey(_global.consoleFontSurface, true, _surfaceCornerKey(_global.consoleFontSurface));
// 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
_runScript(true);
// Game Loop
_progTrace("Script is running");
while (_global.running) {
// A reload asked for by the script, F5 or a changed file happens here, between frames.
if (_global.conf->reload && _watchedChanged()) {
_global.reloadRequested = true;
}
if (_global.reloadRequested) {
_reloadScript();
}
// SDL Event Loop
while (SDL_PollEvent(&event)) {
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)) {
// Positions arrive in window pixels; the game works in the video's coordinates.
SDL_ConvertEventToRenderCoordinates(_global.renderer, &event);
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) {
finishedCount = _soundQueueDrain(finished);
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();
navUpdate(!_global.frozen);
_navCallbacks();
particlesUpdate(!_global.frozen);
sceneUpdateVideo(_sceneVideoSource);
sceneTexture = sceneRender();
_updateSounds();
if (sceneTexture != NULL) {
SDL_RenderTexture(_global.renderer, sceneTexture, NULL, &windowTarget);
}
// 2D particles beneath the overlay, then the overlay, then the ones above it
_drawParticles2D(PARTICLE_UNDER, &windowTarget);
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);
_drawParticles2D(PARTICLE_OVER, &windowTarget);
particlesClearQueue2D();
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);
_subsystemsQuit();
SDL_DestroyTexture(_global.overlayTexture);
SDL_DestroySurface(_global.overlay);
_progTrace("Destroying console font");
SDL_DestroySurface(_global.consoleFontSurface);
// Unload resources the script left behind
_unloadScriptResources();
// Unload background video
if (_global.conf->isFrameFile) {
_progTrace("Unloading framefile");
frameFileUnload(_global.frameFileHandle);
} else {
if (_global.videoHandle >= 0) {
_progTrace("Unloading main video file");
videoUnload(_global.videoHandle);
}
}
// Stop 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);
}
for (x = 0; x < _global.watchedCount; x++) {
free(_global.watched[x].name);
}
free(_global.watched);
SDL_free(_global.navDrawVertices);
SDL_free(_global.quadVertices);
SDL_free(_global.quadIndices);
SDL_free(_global.frameStarts);
}