commit 6066141440b4e4db18c200d95a1b4c55ee76c048 Author: Sage Vaillancourt Date: Wed Mar 12 13:13:26 2025 -0400 Basic template Includes a few third-party libraries (with their licenses inline), and the Asheville font provided by the Playdate SDK. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66f1578 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pdx +.idea diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..674d207 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,4 @@ +std = "lua54+playdate" +stds.project = { + globals = {"playdate", "tiny"}, +} \ No newline at end of file diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..1e771b6 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,7 @@ +{ + "Lua.runtime.version": "Lua 5.4", + "Lua.diagnostics.disable": ["undefined-global", "lowercase-global"], + "Lua.diagnostics.globals": ["playdate", "import", "tiny"], + "Lua.workspace.library": ["/home/sage/Downloads/PlaydateSDK-2.6.2/CoreLibs"], + "Lua.workspace.preloadFileSize": 1000 +} \ No newline at end of file diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..c7a9bba --- /dev/null +++ b/.styluaignore @@ -0,0 +1 @@ +src/assets.lua diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..519c103 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +all: preprocess + pdc --skip-unknown src Game.pdx + +preprocess: + find ./src -name '*.lua2p' | xargs -L1 -I %% lua lib/preprocess-cl.lua %% + +check: preprocess + stylua -c --indent-type Spaces src/ + luacheck -d --globals tiny T Arr Maybe TextStyle --codes src/ --exclude-files src/test/ src/generated/ + +test: check + (cd src; find ./test -name '*.lua' | xargs -L1 -I %% lua %% -v) + +lint: + stylua --indent-type Spaces src/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcc95dd --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Game diff --git a/lib/__types.lua b/lib/__types.lua new file mode 100644 index 0000000..ede70e0 --- /dev/null +++ b/lib/__types.lua @@ -0,0 +1,4662 @@ +-- Playdate SDK Annotations +-- SDK Version: 2.6.2 +-- Generated by Benjamin Dumke-von der Ehe +-- https://github.com/balpha/playdate-types + +------------------------------------------- + +---@meta + +-- selene: allow(unused_variable) +-- selene: allow(unscoped_variables) +---@type pd_playdate_lib +playdate = playdate + +-- selene: allow(unscoped_variables) +-- selene: allow(unused_variable) +---@type pd_json_lib +json = json + +---@alias pd_pattern integer[] + +-- selene: allow(unused_variable) +-- selene: allow(unscoped_variables) +---@type table +kTextAlignment = kTextAlignment + +import = require + +---@alias pd_font_family table + +---@alias pd_font_family_paths table + +---@alias pd_source pd_fileplayer | pd_sampleplayer | pd_synth | pd_instrument + +---@alias pd_effect pd_bitcrusher | pd_twopolefilter | pd_onepolefilter | pd_ringmod | pd_overdrive | pd_delayline + +-- not completely foolproof, but this hack should catch some accidental assignments to read-only properties + +---@class READONLY_number : number +---@class READONLY_boolean : boolean +---@class READONLY_pd_size : pd_size +---@class READONLY_pd_point : pd_point + +---@class pd_event +---@field step number +---@field value number +---@field interpolate boolean? -- ? + +---@class pd_line_collision_info +---@field sprite pd_sprite +---@field entryPoint pd_point +---@field exitPoint pd_point +---@field ti1 number +---@field ti2 number + +---@class pd_sprite_collision_info +---@field sprite pd_sprite +---@field other pd_sprite +---@field type pd_collision_type +---@field overlaps boolean +---@field ti number +---@field move pd_vector2D +---@field normal pd_vector2D +---@field touch pd_point +---@field spriteRect pd_rect +---@field otherRect pd_rect +---@field bounce pd_point? +---@field slide pd_point? + +---@class pd_note_table +---@field step number +---@field note number +---@field length number +---@field velocity number + +---@class pd_stats_table +---@field kernel number +---@field serial number +---@field game number +---@field GC number +---@field wifi number +---@field audio number +---@field trace number +---@field idle number + +---@class pd_UNDOCUMENTED +---@class pd_button +---@class pd_text_alignment +---@class pd_flip +---@class pd_image_flip +---@class pd_color +---@class pd_dither_type +---@class pd_draw_mode +---@class pd_language +---@class pd_filemode +---@class pd_line_cap_style +---@class pd_polygon_fill_rule +---@class pd_stroke_location +---@class pd_font_variant +---@class pd_capitalization +---@class pd_collision_type +---@class pd_sound_format +---@class pd_waveform +---@class pd_lfo_type +---@class pd_sound_filter +---@class pd_wrap_mode +---@class pd_seek_mode + +---@class pd_time_table +---@field year number +---@field month number +---@field day number +---@field weekday number +---@field hour number +---@field minute number +---@field second number +---@field millisecond number + +---@class pd_file_time_table +---@field year number +---@field month number +---@field day number +---@field hour number +---@field minute number +---@field second number + +---@class pd_input_handler +--- Called immediately after the player presses the A Button. +---@field AButtonDown nil | (fun()) +--- Called after the A Button is held down for one second. This can be used for secondary actions (e.g., displaying a game world map, changing weapons). +---@field AButtonHeld nil | (fun()) +--- Called immediately after the player releases the A Button. +---@field AButtonUp nil | (fun()) +--- Called immediately after the player presses the B Button. +---@field BButtonDown nil | (fun()) +--- Called after the B Button is held down for one second. This can be used for secondary actions (e.g., displaying a game world map, changing weapons). +---@field BButtonHeld nil | (fun()) +--- Called immediately after the player releases the B Button. +---@field BButtonUp nil | (fun()) +--- Called immediately after the player presses the down direction on the d-pad. +---@field downButtonDown nil | (fun()) +--- Called immediately after the player releases the down direction on the d-pad. +---@field downButtonUp nil | (fun()) +--- Called immediately after the player presses the left direction on the d-pad. +---@field leftButtonDown nil | (fun()) +--- Called immediately after the player releases the left direction on the d-pad. +---@field leftButtonUp nil | (fun()) +--- Called immediately after the player presses the right direction on the d-pad. +---@field rightButtonDown nil | (fun()) +--- Called immediately after the player releases the right direction on the d-pad. +---@field rightButtonUp nil | (fun()) +--- Called immediately after the player presses the up direction on the d-pad. +---@field upButtonDown nil | (fun()) +--- Called immediately after the player releases the up direction on the d-pad. +---@field upButtonUp nil | (fun()) +--- For playdate.cranked(), `change` is the angle change in degrees. `acceleratedChange` is `change` multiplied by a value that increases as the crank moves faster, similar to the way mouse acceleration works. Negative values are anti-clockwise. +---@field cranked nil | (fun(change: number, acceleratedChange: number)) + +---@class pd_metadata +--- A unique identifier for your game, in reverse DNS notation. +---@field bundleID string +--- A game version number, formatted any way you wish, that is displayed to players. It is not used to compute when updates should occur. +---@field version string +--- A monotonically-increasing integer value used to indicate a unique version of your game. This can be set using an automated build process like Continuous Integration to avoid having to set the value by hand. +---@field buildNumber integer +--- A directory of images that will be used by the launcher. +---@field imagePath string +--- Optional. Should point to the path of a short audio file to be played as the game launch animation is taking place. +---@field launchSoundPath string? +--- Optional. A content warning that displays when the user launches your game for the first time. The user will have the option of backing out and not launching your game if they choose. +---@field contentWarning string? +--- Optional. A second content warning that displays on a second screen when the user launches your game for the first time. The user will have the option of backing out and not launching your game if they choose. Note: contentWarning2 will only display if a contentWarning attribute is also specified. +---@field contentWarning2 string? + +---@class tablelib +--- Returns the first index of `element` in the given array-style table. If the table does not contain `element`, the function returns nil. +---@field indexOfElement fun(table: table, element: any): number? +--- Returns the size of the given table as multiple values (`arrayCount`, `hashCount`). +---@field getsize fun(table: table): (number, number) +--- Returns a new Lua table with the array and hash parts preallocated to accommodate `arrayCount` and `hashCount` elements respectively. +--- +--- If you can make a decent estimation of how big your table will need to be, table.create() can be much more efficient than the alternative, especially in loops. For example, if you know your array is always going to contain approximately ten elements, say myArray = table.create( 10, 0 ) instead of myArray = {}. +---@field create fun(arrayCount: number, hashCount: number): table +--- shallowcopy returns a shallow copy of the `source` table. If a `destination` table is provided, it copies the contents of `source` into `destination` and returns `destination`. The copy will contain references to any nested tables. +---@field shallowcopy fun(source: table, destination?: table): table +--- deepcopy returns a deep copy of the `source` table. The copy will contain copies of any nested tables. +---@field deepcopy fun(source: table): table + +---@class pd_playdate_lib +--- Returns two values, the current API version of the Playdate runtime and the minimum API version supported by the runtime. +---@field apiVersion fun(): (number, number) +--- The playdate.metadata table contains the values in the current game’s pdxinfo file, keyed by variable name. To retrieve the version number of the game, for example, you would use playdate.metadata.version. +--- +--- Changing values in this table at run time has no effect. +---@field metadata pd_metadata +--- Implement this callback and Playdate OS will call it once per frame. This is the place to put the main update-and-draw code for your game. Playdate will attempt to call this function by default 30 times per second; that value can be changed by calling playdate.display.setRefreshRate(). +--- +--- If your update() function takes too long to execute, Playdate OS may not be able to call it as often as specified by the current refresh rate. In this case, Playdate OS will simply try and call it as often as it can, with a not-to-exceed rate of playdate.display.getRefreshRate() frames per second. +---@field update nil | (fun()) +--- Suspends callbacks to playdate.update() for the specified number of milliseconds. +--- +--- playdate.wait() is ideal for pausing game execution to, for example, show a message to the player. Because .update() will not be called, the screen will freeze during .wait(). Audio will continue to play. Animation during this wait period is possible, but you will need to explicitly call playdate.display.flush() once per frame. +--- While timers should pause during playdate.wait() (assuming playdate.timer.updateTimers() and playdate.frameTimer.updateTimers() are invoked during playdate.update()), animators will `not` pause during playdate.wait(). Be sure to account for this in your code. +---@field wait fun(milliseconds: number) +--- Stops per-frame callbacks to playdate.update(). Useful in conjunction with playdate.display.flush() if your program only does things in response to button presses. +---@field stop fun() +--- Resumes per-frame callbacks to playdate.update(). +---@field start fun() +--- Reinitializes the Playdate runtime and restarts the currently running game. The optional string arg passed in is available after restart in playdate.argv as if it had been passed in on the command line when launching the simulator. +---@field restart fun(arg?: string) +--- Called when the player chooses to exit the game via the System Menu or Menu button. +---@field gameWillTerminate nil | (fun()) +--- Called before the device goes to low-power sleep mode because of a low battery. +---@field deviceWillSleep nil | (fun()) +--- If your game is running on the Playdate when the device is locked, this function will be called. Implementing this function allows your game to take special action when the Playdate is locked, e.g., saving state. +---@field deviceWillLock nil | (fun()) +--- If your game is running on the Playdate when the device is unlocked, this function will be called. +---@field deviceDidUnlock nil | (fun()) +--- Called before the system pauses the game. (In the current version of Playdate OS, this only happens when the device’s Menu button is pushed.) Implementing these functions allows your game to take special action when it is paused, e.g., updating the menu image. +---@field gameWillPause nil | (fun()) +--- Called before the system resumes the game. +---@field gameWillResume nil | (fun()) +--- Returns a playdate.menu object. Use this to add your custom menu items. +---@field getSystemMenu fun(): pd_menu +---@field menu pd_menu_lib +--- While the game is paused it can optionally provide an image to be displayed alongside the System Menu. Use this function to set that image. +--- +--- `image` should be a 400 x 240 pixel playdate.graphics.image. All important content should be in the left half of the image in an area 200 pixels wide, as the menu will obscure the rest. The right side of the image will be visible briefly as the menu animates in and out. +--- +--- Optionally, `xOffset` can be provided which must be a number between 0 and 200 and will cause the menu image to animate to a position offset left by `xOffset` pixels as the menu is animated in. +--- +--- To remove a previously-set menu image, pass nil for the `image` argument. +---@field setMenuImage fun(image: pd_image, xOffset?: number) +--- Returns the current language of the system, which will be one of the constants `playdate.graphics.font.kLanguageEnglish` or `playdate.graphics.font.kLanguageJapanese`. +---@field getSystemLanguage fun(): pd_language +--- Returns `true` if the user has checked the "Reduce Flashing" option in Playdate Settings; `false` otherwise. Games should read this value and, if `true`, avoid visuals that could be problematic for people with sensitivities to flashing lights or patterns. +---@field getReduceFlashing fun(): boolean +--- Returns `true` if the user has checked the "Upside Down" option in Playdate Settings; `false` otherwise. (Upside Down mode can be convenient for players wanting to hold Playdate upside-down so they can use their left hand to operate the crank.) +--- +--- Typically your game doesn’t need to anything in regards to this setting. But it is available in case your game wants to take some special actions, display special instructions, etc. +--- +--- Reported d-pad directions are flipped when in Upside Down mode — RIGHT will be reported as LEFT, UP as DOWN, etc. — so that the d-pad will make sense to a user holding Playdate upside-down. However, the A and B buttons — since they are still labeled as "A" and "B" — retain their normal meanings and will be reported as usual. +---@field getFlipped fun(): boolean +--- The accelerometer is off by default, to save a bit of power. If you will be using the accelerometer in your game, you’ll first need to call playdate.startAccelerometer() then wait for the next update cycle before reading its values. If you won’t be using the accelerometer again for a while, calling playdate.stopAccelerometer() will put it back into a low-power idle state. +---@field startAccelerometer fun() +--- Puts the accelerometer into a low-power idle state. (Though, to be honest, the accelerometer draws so little power when it’s running you’d never notice the difference.) +---@field stopAccelerometer fun() +--- If the accelerometer has been turned on with playdate.startAccelerometer(), returns the x, y, and z values from the accelerometer as a list. Positive x points right, positive y points to the bottom of the screen, and positive z points through the screen away from the viewer. For example, with the device held upright this function returns the values (0,1,0). With it flat on its back, it returns (0,0,1). +---@field readAccelerometer fun(): (number, number, number) +--- Returns true if the accelerometer is currently running. +---@field accelerometerIsRunning fun(): boolean +--- Returns true if `button` is currently being pressed. +--- +--- `button` should be one of the constants: +--- +--- `playdate.kButtonA` +--- +--- `playdate.kButtonB` +--- +--- `playdate.kButtonUp` +--- +--- `playdate.kButtonDown` +--- +--- `playdate.kButtonLeft` +--- +--- `playdate.kButtonRight` +--- +--- Or one of the strings "a", "b", "up", "down", "left", "right". +---@field buttonIsPressed fun(button: pd_button): boolean +--- Returns true for `just one update cycle` if `button` was pressed. buttonJustPressed will not return true again until the button is released and pressed again. This is useful for, say, a player "jump" action, so the jump action is taken only once and not on every single update. +--- +--- `button` should be one of the constants listed in playdate.buttonIsPressed() +---@field buttonJustPressed fun(button: pd_button): boolean +--- Returns true for `just one update cycle` if `button` was released. buttonJustReleased will not return true again until the button is pressed and released again. +--- +--- `button` should be one of the constants listed in playdate.buttonIsPressed() +---@field buttonJustReleased fun(button: pd_button): boolean +--- Returns the above data in one call, with multiple return values (`current`, `pressed`, `released`) containing bitmasks indicating which buttons are currently down, and which were pressed and released since the last update. For example, if the d-pad left button and the A button are both down, the `current` value will be (`playdate.kButtonA`|`playdate.kButtonLeft`). +---@field getButtonState fun(): (integer, integer, integer) +--- When set, button up/down events on the D pad and the A and B buttons are added to a list instead of simply polled at the beginning of a frame, allowing the code to handle multiple taps on a given button in a single frame. At the default 30 FPS, a queue size of 5 should be adequate. At lower frame rates/longer frame times, the queue size should be extended until all button presses are caught. Additionally, when the button queue is enabled the button callbacks listed below are passed the event time as an argument. +---@field setButtonQueueSize fun(size: number) +--- Called immediately after the player presses the A Button. +---@field AButtonDown nil | (fun()) +--- Called after the A Button is held down for one second. This can be used for secondary actions (e.g., displaying a game world map, changing weapons). +---@field AButtonHeld nil | (fun()) +--- Called immediately after the player releases the A Button. +---@field AButtonUp nil | (fun()) +--- Called immediately after the player presses the B Button. +---@field BButtonDown nil | (fun()) +--- Called after the B Button is held down for one second. This can be used for secondary actions (e.g., displaying a game world map, changing weapons). +---@field BButtonHeld nil | (fun()) +--- Called immediately after the player releases the B Button. +---@field BButtonUp nil | (fun()) +--- Called immediately after the player presses the down direction on the d-pad. +---@field downButtonDown nil | (fun()) +--- Called immediately after the player releases the down direction on the d-pad. +---@field downButtonUp nil | (fun()) +--- Called immediately after the player presses the left direction on the d-pad. +---@field leftButtonDown nil | (fun()) +--- Called immediately after the player releases the left direction on the d-pad. +---@field leftButtonUp nil | (fun()) +--- Called immediately after the player presses the right direction on the d-pad. +---@field rightButtonDown nil | (fun()) +--- Called immediately after the player releases the right direction on the d-pad. +---@field rightButtonUp nil | (fun()) +--- Called immediately after the player presses the up direction on the d-pad. +---@field upButtonDown nil | (fun()) +--- Called immediately after the player releases the up direction on the d-pad. +---@field upButtonUp nil | (fun()) +--- Returns a boolean indicating whether or not the crank is folded into the unit. +--- +--- If your game requires the crank and :isCrankDocked() is true, you can use a crank alert to notify the user that the crank should be extended. +---@field isCrankDocked fun(): boolean +--- Returns the absolute position of the crank (in degrees). Zero is pointing straight up parallel to the device. Turning the crank clockwise (when looking at the right edge of an upright device) increases the angle, up to a maximum value 359.9999. The value then resets back to zero as the crank continues its rotation. +--- +--- local crankPosition = playdate.getCrankPosition() +---@field getCrankPosition fun(): number +--- Returns two values, `change` and `acceleratedChange`. `change` represents the angle change (in degrees) of the crank since the last time this function (or the playdate.cranked() callback) was called. Negative values are anti-clockwise. `acceleratedChange` is change multiplied by a value that increases as the crank moves faster, similar to the way mouse acceleration works. +--- +--- local change, acceleratedChange = playdate.getCrankChange() +---@field getCrankChange fun(): (number, number) +--- Returns the number of "ticks" — whose frequency is defined by the value of `ticksPerRevolution` — the crank has turned through since the last time this function was called. Tick boundaries are set at absolute positions along the crank’s rotation. Ticks can be positive or negative, depending upon the direction of rotation. +--- +--- For example, say you have a movie player and you want your movie to advance 6 frames for every one revolution of the crank. Calling playdate.getCrankTicks(6) during each update will give you a return value of 1 as the crank turns past each 60 degree increment. (Since we passed in a 6, each tick represents 360 ÷ 6 = 60 degrees.) So getCrankTicks(6) will return a 1 as the crank turns past the 0 degree absolute position, the 60 degree absolute position, and so on for the 120, 180, 240, and 300 degree positions. Otherwise, 0 will be returned. (-1 will be returned if the crank moves past one of these mentioned positions while going in a backward direction.) +--- +--- You must import `CoreLibs/crank` to use getCrankTicks(). +--- Example: Reading crank input using getCrankTicks +--- import "CoreLibs/crank" +--- +--- local ticksPerRevolution = 6 +--- +--- function playdate.update() +--- local crankTicks = playdate.getCrankTicks(ticksPerRevolution) +--- +--- if crankTicks == 1 then +--- print("Forward tick") +--- elseif crankTicks == -1 then +--- print("Backward tick") +--- end +--- end +---@field getCrankTicks fun(ticksPerRevolution: number): number +--- For playdate.cranked(), `change` is the angle change in degrees. `acceleratedChange` is `change` multiplied by a value that increases as the crank moves faster, similar to the way mouse acceleration works. Negative values are anti-clockwise. +---@field cranked nil | (fun(change: number, acceleratedChange: number)) +--- This function, if defined, is called when the crank is docked. +---@field crankDocked nil | (fun()) +--- This function, if defined, is called when the crank is undocked. +---@field crankUndocked nil | (fun()) +--- `True` disables the default crank docking/undocking sound effects. `False` re-enables them. Useful if the crank sounds seem out-of-place in your game. +--- +--- When your game terminates, crank sounds will automatically be re-enabled. +---@field setCrankSoundsDisabled fun(disable: boolean) +---@field inputHandlers pd_inputHandlers_lib +--- `True` disables the 3 minute auto-lock feature. `False` re-enables it and resets the timer back to 3 minutes. +--- +--- Auto-lock will automatically be re-enabled when your game terminates. +--- If disabling auto-lock, developers should look for opportunities to re-enable auto-lock when appropriate. (For example, if your game is an MP3 audio player, auto-lock could be re-enabled when the user pauses the audio.) +---@field setAutoLockDisabled fun(disable: boolean) +--- Returns the number of milliseconds the game has been `active` since launched. +---@field getCurrentTimeMilliseconds fun(): number +--- Resets the high-resolution timer. +---@field resetElapsedTime fun() +--- Returns the number of seconds since playdate.resetElapsedTime() was called. The value is a floating-point number with microsecond accuracy. +---@field getElapsedTime fun(): number +--- Returns the number of seconds and milliseconds elapsed since midnight (hour 0), January 1 2000 UTC, as a list: `(seconds, milliseconds)`. This function is suitable for seeding the random number generator: +--- +--- Sample code for seeding the random number generator +--- math.randomseed(playdate.getSecondsSinceEpoch()) +---@field getSecondsSinceEpoch fun(): (number, number) +--- Returns a table with values for the local time, accessible via the following keys: +--- +--- `year`: 4-digit year (until 10,000 AD) +--- +--- `month`: month of the year, where 1 is January and 12 is December +--- +--- `day`: day of the month, 1 - 31 +--- +--- `weekday`: day of the week, where 1 is Monday and 7 is Sunday +--- +--- `hour`: 0 - 23 +--- +--- `minute`: 0 - 59 +--- +--- `second`: 0 - 59 (or 60 on a leap second) +--- +--- `millisecond`: 0 - 999 +---@field getTime fun(): pd_time_table +--- Returns a table in the same format as playdate.getTime(), but in GMT rather than local time. +---@field getGMTTime fun(): pd_time_table +--- Returns the number of seconds and milliseconds between midnight (hour 0), January 1 2000 UTC and `time`, specified in local time, as a list: `(seconds, milliseconds)`. +--- +--- `time` should be a table of the same format as the one returned by playdate.getTime(). +---@field epochFromTime fun(time: pd_time_table): (number, number) +--- Returns the number of seconds and milliseconds between midnight (hour 0), January 1 2000 UTC and `time`, specified in GMT time, as a list: `(seconds, milliseconds)`. +--- +--- `time` should be a table of the same format as the one returned by playdate.getTime(). +---@field epochFromGMTTime fun(time: pd_time_table): (number, number) +--- Converts the epoch to a local date and time table, in the same format as the table returned by playdate.getTime(). +---@field timeFromEpoch fun(seconds: number, milliseconds: number): pd_time_table +--- Converts the epoch to a GMT date and time table, in the same format as the table returned by playdate.getTime(). +---@field GMTTimeFromEpoch fun(seconds: number, milliseconds: number): pd_time_table +--- Returns true if the user has set the 24-Hour Time preference in the Settings program. +---@field shouldDisplay24HourTime fun(): boolean +--- If the simulator is launched from the command line, any extra arguments passed there are available in the playdate.argv array. +---@field argv string[] +--- `flag` determines whether or not the print() function adds a newline to the end of the printed text. Default is `true`. +---@field setNewlinePrinted fun(flag: boolean) +--- Calculates the current frames per second and draws that value at `x, y`. +---@field drawFPS fun(x: number, y: number) +--- Returns the `measured, actual` refresh rate in frames per second. This value may be different from the `specified` refresh rate (see playdate.display.getRefreshRate()) by a little or a lot depending upon how much calculation is being done per frame. +---@field getFPS fun(): number +--- Returns a table containing percentages of time spent in each system task over the last interval, if more than zero. Possible keys are +--- +--- kernel +--- +--- serial +--- +--- game +--- +--- GC +--- +--- wifi +--- +--- audio +--- +--- trace +--- +--- idle +--- +--- playdate.getStats() only functions on a Playdate device. In the Simulator, this function returns nil. +---@field getStats fun(): pd_stats_table +--- setStatsInterval() sets the length of time for each sample frame of runtime stats. Set `seconds` to zero to disable stats collection. +---@field setStatsInterval fun(seconds: number) +---@field display pd_display_lib +---@field easingFunctions pd_easingFunctions_lib +---@field datastore pd_datastore_lib +---@field file pd_file_lib +---@field geometry pd_geometry_lib +---@field graphics pd_graphics_lib +---@field keyboard pd_keyboard_lib +---@field math pd_math_lib +---@field pathfinder pd_pathfinder_lib +--- Returns a table holding booleans with the following keys: +--- +--- `charging`: The battery is actively being charged +--- +--- `USB`: There is a powered USB cable connected +--- +--- `screws`: There is 5V being applied to the corner screws (via the dock, for example) +---@field getPowerStatus fun(): table +--- Returns a value from 0-100 denoting the current level of battery charge. 0 = empty; 100 = full. +---@field getBatteryPercentage fun(): number +--- Returns the battery’s current voltage level. +---@field getBatteryVoltage fun(): number +--- This variable—not a function, so don’t invoke with `()`—it is set to 1 when running inside of the Simulator and is `nil` otherwise. +---@field isSimulator boolean +---@field simulator pd_simulator_lib +--- Clears the simulator console. +---@field clearConsole fun() +--- Sets the color of the playdate.debugDraw() overlay image. Values are in the range 0-1. +---@field setDebugDrawColor fun(r: number, g: number, b: number, a: number) +--- Lets you act on keyboard keypresses when running in the Simulator ONLY. These can be useful for adding debugging functions that can be enabled via your keyboard. +--- +--- It is possible test a game on Playdate hardware and trap computer keyboard keypresses if you are using the Simulator’s Control Device with Simulator option. +--- +--- key is a string containing the character pressed or released on the keyboard. Note that: +--- +--- The key in question needs to have a textual representation or these functions will not be called. For instance, alphanumeric keys will call these functions; keyboard directional arrows will not. +--- +--- If the keypress in question is already in use by the Simulator for another purpose (say, to control the d-pad or A/B buttons), these functions will not be called. +--- +--- If `key` is an alphabetic character, the value will always be lowercase, even if the user deliberately typed an uppercase character. +---@field keyPressed nil | (fun(key: string)) +--- Lets you act on keyboard key releases when running in the Simulator ONLY. These can be useful for adding debugging functions that can be enabled via your keyboard. +---@field keyReleased nil | (fun(key: string)) +--- Called immediately after playdate.update(), any drawing performed during this callback is overlaid on the display in 50% transparent red (or another color selected with playdate.setDebugDrawColor()). +--- +--- White pixels are drawn in the debugDrawColor. Black pixels are transparent. +---@field debugDraw nil | (fun()) +---@field sound pd_sound_lib +---@field string pd_string_lib +---@field timer pd_timer_lib +---@field frameTimer pd_frameTimer_lib +---@field ui pd_ui_lib +--- Called when a msg command is received on the serial port. The text following the command is passed to the function as the string `message`. +--- +--- Running !msg in the simulator Lua console sends the command to the device if one is connected, otherwise it sends it to the game running in the simulator. +---@field serialMessageReceived nil | (fun(message: string)) +--- If `flag` is false, automatic garbage collection is disabled and the game should manually collect garbage with Lua’s collectgarbage() function. +---@field setCollectsGarbage fun(flag: boolean) +--- Force the Lua garbage collector to run for at least `ms` milliseconds every frame, so that garbage doesn’t pile up and cause the game to run out of memory and stall in emergency garbage collection. The default value is 1 millisecond. +--- +--- If your game isn’t generating a lot of garbage, it might be advantageous to set a smaller minimum GC time, granting more CPU bandwidth to your game. +---@field setMinimumGCTime fun(ms: number) +--- When the amount of used memory is less than min (scaled from 0-1, as a percentage of total system memory), the system will only run the collector for the minimum GC time, as set by playdate.setGCScaling(), every frame. If the used memory is more than max, the system will spend all free time running the collector. Between the two, the time used by the garbage collector is scaled proportionally. +--- +--- For example, if the scaling is set to a min of 0.4 and max of 0.7, and memory is half full, the collector will run for the minimum GC time plus 1/3 of whatever time is left before the next frame (because (0.5 - 0.4) / (0.7 - 0.4) = 1/3). +--- +--- The default behavior is a scaling of (0.0, 1.0). If set to (0.0, 0.0), the system will use all available extra time each frame running GC. +---@field setGCScaling fun(min: number, max: number) +---@field kButtonA pd_button +---@field kButtonB pd_button +---@field kButtonUp pd_button +---@field kButtonDown pd_button +---@field kButtonLeft pd_button +---@field kButtonRight pd_button + +---@class pd_json_lib +--- Takes the JSON encoded string and converts it to a Lua table. +--- +--- Equivalent to playdate->json->decode() in the C API. +---@field decode fun(string: string) +--- Reads the given playdate.file.file object or the file at the given path and converts it to a Lua table. +---@field decodeFile (fun(file: pd_file_file)) | (fun(path: string)) +--- Returns a string containing the JSON representation of the passed-in Lua table. +---@field encode fun(table: table): string +--- Returns a string containing the JSON representation of a Lua table, with human-readable formatting. +---@field encodePretty fun(table: table): string +--- Encodes the Lua table table to JSON and writes it to the given playdate.file.file object or the given path. If pretty is true, the output is formatted to make it human-readable. Otherwise, no additional whitespace is added. +--- +--- For a very simple way to serialize a table to a file, see playdate.datastore. +---@field encodeToFile (fun(file: pd_file_file, pretty?: boolean, table?: table)) | (fun(path: string, pretty?: boolean, table?: table)) + +---@class pd_menu_lib : pd_menu +---@field item pd_item_lib + +---@class pd_menu +--- `title` will be the title displayed by the menu item. +---@field addMenuItem fun(self: pd_menu, title: string, callback: fun()) +--- Creates a new menu item that can be checked or unchecked by the player. +--- +--- `title` will be the title displayed by the menu item. +--- +--- `initialValue` can be set to true or false, indicating the checked state of the menu item. Optional, defaults to false. +--- +--- If this menu item is interacted with while the system menu is open, `callback` will be called when the menu is closed, before playdate.gameWillResume is called. The callback function will be passed one argument, a boolean value, indicating the current value of the menu item. +--- +--- If the returned playdate.menu.item is nil, a second errorMessage return value will indicate the reason the operation failed. +--- +--- Playdate OS allows a maximum of three custom menu items to be added to the System Menu. +---@field addCheckmarkMenuItem fun(self: pd_menu, title: string, initialValue?: number, callback?: fun(boolean)): pd_item +--- Creates a menu item that allows the player to cycle through a set of options. +--- +--- `title` will be the title displayed by the menu item. +--- +--- `options` should be an array-style table of strings representing the states the menu item can have. Due to limited horizontal space, the option strings and title should be kept short for this type of menu item. +--- +--- `initialValue` can optionally be set to any of the values in the options array. +--- +--- If the value of this menu item is changed while the system menu is open, `callback` will be called when the menu is closed, before playdate.gameWillResume is called. The callback function will be passed one string argument indicating the currently selection option. +--- +--- If the returned playdate.menu.item is nil, a second errorMessage return value will indicate the reason the operation failed. +--- +--- Playdate OS allows a maximum of three custom menu items to be added to the System Menu. +---@field addOptionsMenuItem fun(self: pd_menu, title: string, options: string[], initalValue?: string, callback?: fun(string)): pd_item +--- Returns an array-style table containing all playdate.menu.items your game has added to the menu. +---@field getMenuItems fun(self: pd_menu): pd_item[] +--- Removes the specified playdate.menu.item from the menu. +---@field removeMenuItem fun(self: pd_menu, menuItem: pd_item) +--- Removes from the referenced menu object all playdate.menu.items added by your game. +--- +--- Items that were added to the System Menu by the operating system cannot be removed by this operation, or any other. +---@field removeAllMenuItems fun(self: pd_menu) + +---@class pd_inputHandlers_lib +--- Pushes a new input handler onto the stack. +--- +--- `handler:` A table containing one or more custom input functions. +--- +--- `masksPreviousHandlers:` If true, input functions not defined in `handler` will not be called. If missing or false, the previously-pushed input handler tables will be searched for input functions missing from `handler`, cascading down to the default playdate table. +---@field push fun(handler: pd_input_handler, masksPreviousHandlers?: boolean) +--- Pops the last input handler off of the stack. +---@field pop fun() + +---@class pd_display_lib +--- Sets the desired refresh rate in frames per second. The default is 30 fps, which is a recommended figure that balances animation smoothness with performance and power considerations. Maximum is 50 fps. +--- +--- If `rate` is 0, playdate.update() is called as soon as possible. Since the display refreshes line-by-line, and unchanged lines aren’t sent to the display, the update cycle will be faster than 30 times a second but at an indeterminate rate. playdate.getCurrentTimeMilliseconds() should then be used as a steady time base. +--- +--- Equivalent to playdate->display->setRefreshRate() in the C API. +---@field setRefreshRate fun(rate: number) +--- Returns the specified refresh rate in frames per second. See also playdate.getFPS() for `measured, actual` frame rate. +---@field getRefreshRate fun(): number +--- Sends the contents of the frame buffer to the display immediately. Useful if you have called playdate.stop() to disable update callbacks in, say, the case where your app updates the display only in reaction to button presses. +---@field flush fun() +--- Returns the height the Playdate display, taking the current display scale into account; e.g., if the scale is 2, the values returned will be based off of a 200 x 120-pixel screen rather than the native 400 x 240. (See playdate.display.setScale().) +--- +--- Equivalent to playdate->display->getHeight() in the C API. +---@field getHeight fun(): number +--- Returns the width the Playdate display, taking the current display scale into account; e.g., if the scale is 2, the values returned will be based off of a 200 x 120-pixel screen rather than the native 400 x 240. (See playdate.display.setScale().) +--- +--- Equivalent to playdate->display->getWidth() in the C API. +---@field getWidth fun(): number +--- Returns the values `(width, height)` describing the Playdate display size. Takes the current display scale into account; e.g., if the scale is 2, the values returned will be based off of a 200 x 120-pixel screen rather than the native 400 x 240. (See playdate.display.setScale().) +---@field getSize fun(): (number, number) +--- Returns the values `(x, y, width, height)` describing the Playdate display size. Takes the current display scale into account; e.g., if the scale is 2, the values returned will be based off of a 200 x 120-pixel screen rather than the native 400 x 240. (See playdate.display.setScale().) +---@field getRect fun(): pd_rect +--- Sets the display scale factor. Valid values for `scale` are 1, 2, 4, and 8. +--- +--- The top-left corner of the frame buffer is scaled up to fill the display; e.g., if the scale is set to 4, the pixels in rectangle [0,100] x [0,60] are drawn on the screen as 4 x 4 squares. +--- +--- Equivalent to playdate->display->setScale() in the C API. +---@field setScale fun(scale: number) +--- Gets the display scale factor. Valid values for `scale` are 1, 2, 4, and 8. +---@field getScale fun(): number +--- If the argument passed to setInverted() is true, the frame buffer will be drawn inverted (everything onscreen that was black will now be white, etc.) +--- +--- Equivalent to playdate->display->setInverted() in the C API. +---@field setInverted fun(flag: boolean) +--- Returns the current value of the display invert flag. +---@field getInverted fun(): boolean +--- Adds a mosaic effect to the display. Valid `x` and `y` values are between 0 and 3, inclusive. +--- +--- Equivalent to playdate->display->setMosaic() in the C API. +---@field setMosaic fun(x: number, y: number) +--- Returns the current mosaic effect settings as multiple values (`x`, `y`). +---@field getMosaic fun(): (number, number) +--- Offsets the entire display by `x`, `y`. Offset values can be negative. The "exposed" part of the display is black or white, according to the value set in playdate.graphics.setBackgroundColor(). This is an efficient way to make a "shake" effect without redrawing anything. +--- +--- This function is different from playdate.graphics.setDrawOffset(). +--- +--- Equivalent to playdate->display->setOffset() in the C API. +--- +--- Example: A screen shake effect using setOffset +--- -- You can copy and paste this example directly as your main.lua file to see it in action +--- import "CoreLibs/graphics" +--- import "CoreLibs/timer" +--- +--- -- This function relies on the use of timers, so the timer core library +--- -- must be imported, and updateTimers() must be called in the update loop +--- local function screenShake(shakeTime, shakeMagnitude) +--- -- Creating a value timer that goes from shakeMagnitude to 0, over +--- -- the course of 'shakeTime' milliseconds +--- local shakeTimer = playdate.timer.new(shakeTime, shakeMagnitude, 0) +--- -- Every frame when the timer is active, we shake the screen +--- shakeTimer.updateCallback = function(timer) +--- -- Using the timer value, so the shaking magnitude +--- -- gradually decreases over time +--- local magnitude = math.floor(timer.value) +--- local shakeX = math.random(-magnitude, magnitude) +--- local shakeY = math.random(-magnitude, magnitude) +--- playdate.display.setOffset(shakeX, shakeY) +--- end +--- -- Resetting the display offset at the end of the screen shake +--- shakeTimer.timerEndedCallback = function() +--- playdate.display.setOffset(0, 0) +--- end +--- end +--- +--- function playdate.update() +--- playdate.timer.updateTimers() +--- if playdate.buttonJustPressed(playdate.kButtonA) then +--- -- Shake the screen for 500ms, with the screen +--- -- shaking around by about 5 pixels on each side +--- screenShake(500, 5) +--- end +--- +--- -- A circle to be able to view what the shaking looks like +--- playdate.graphics.fillCircleAtPoint(200, 120, 10) +--- end +---@field setOffset fun(x: number, y: number) +--- getOffset() returns the current display offset as multiple values (`x`, `y`). +---@field getOffset fun(): (number, number) +--- Flips the display on the x or y axis, or both. +--- +--- Function arguments are booleans, and in Lua 0 evaluates to true. +--- +--- Equivalent to playdate->display->setFlipped() in the C API. +---@field setFlipped fun(x: number, y: number) +--- The simplest method for putting an image on the display. Copies the contents of the image at `path` directly to the frame buffer. The image must be 400x240 pixels with no transparency. +--- +--- Loading an image via playdate.graphics.image.new() and drawing it at a desired coordinate with playdate.graphics.image:draw() offers more flexibility. +---@field loadImage fun(path: string) + +---@class pd_easingFunctions_lib +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field linear fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inQuad fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outQuad fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutQuad fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInQuad fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inCubic fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outCubic fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutCubic fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInCubic fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inQuart fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outQuart fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutQuart fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInQuart fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inQuint fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outQuint fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutQuint fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInQuint fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inSine fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outSine fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutSine fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInSine fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inExpo fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outExpo fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutExpo fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInExpo fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inCirc fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outCirc fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutCirc fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInCirc fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInElastic fun(t: number, b: number, c: number, d: number, a?: number, p?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inBack fun(t: number, b: number, c: number, d: number, s?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outBack fun(t: number, b: number, c: number, d: number, s?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutBack fun(t: number, b: number, c: number, d: number, s?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInBack fun(t: number, b: number, c: number, d: number, s?: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outBounce fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inBounce fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field inOutBounce fun(t: number, b: number, c: number, d: number): number +--- `t` is elapsed time +--- +--- `b` is the beginning value +--- +--- `c` is the change (or end value - start value) +--- +--- `d` is the duration +--- +--- `a` - amplitude +--- +--- `p` - period parameter +--- +--- `s` - amount of "overshoot" +---@field outInBounce fun(t: number, b: number, c: number, d: number): number + +---@class pd_datastore_lib +--- Encodes the given table into the named file. (The .json extension should be omitted from the file name.) The default file name is "data". If `pretty-print` is true, the JSON will be nicely formatted. +---@field write fun(table: table, filename?: string, pretty-print?: boolean) +--- Returns a table instantiated with the data in the JSON-encoded file you specify. (The .json extension should be omitted.) The default file name is "data". If no file is found, this function returns nil. +---@field read fun(filename?: string): table +--- Deletes the specified datastore file. The default file name is "data". Returns false if the datastore file could not be deleted. +---@field delete fun(filename?: string): boolean +--- Saves a playdate.graphics.image to a file. If `path` doesn’t contain a folder name, the image is stored in a folder named "images". +--- +--- By default, this method writes out a PDI file, a custom image format used by Playdate that can be read back in using readImage(). If you want to write out a GIF file, append a .gif extension to your `path`. +--- +--- Because writeImage() doesn’t currently support GIF transparency, if you attempt to write a GIF from an image buffer you instantiated, you must call playdate.graphics.image.new( `width, height, bgcolor` ) with `bgcolor` set to playdate.graphics.kColorWhite or playdate.graphics.kColorBlack, otherwise your image will render improperly to the file. +---@field writeImage fun(image: pd_image, path: string) +--- Reads a playdate.graphics.image from a file in the data folder. If `path` doesn’t contain a folder name, the image is searched for in a folder named "images". +--- +--- readImage() can only load compiled pdi files. (writeImage() by default creates compiled pdi files.) +---@field readImage fun(path: string) + +---@class pd_file_lib +--- Returns a playdate.file.file corresponding to the opened file. `mode` should be one of the following: +--- +--- playdate.file.kFileRead: the file is opened for reading; the system first looks in the /Data/ folder for the given file, then in the game’s pdx folder if it isn’t found +--- +--- playdate.file.kFileWrite: the file is created if it doesn’t exist, truncated to zero length if it does, then opened for writing +--- +--- playdate.file.kFileAppend: the file is created if it doesn’t exist, opened for writing, with new data written to the end of the file +--- +--- If `mode` is not specified, the default is `playdate.file.kFileRead`. +--- +--- If the file couldn’t be opened, a second return value indicates the error. The filesystem has a limit of 64 simultaneous open files. +--- +--- Equivalent to playdate->file->open() in the C API. +---@field open fun(path: string, mode?: pd_filemode): pd_file_file +---@field file pd_file_file_lib +--- Returns an array containing the file names in the given directory path as strings. Folders are indicated by a slash / at the end of the filename. If `showhidden` is set, files beginning with a period will be included; otherwise, they are skipped. +--- +--- Call with no argument to get a list of all files and folders your game has access to. (For a game with default access permissions, listFiles(), listFiles("/"), and listFiles(".") should all return the same result.) +--- +--- Equivalent to playdate->file->listfiles() in the C API. +--- +--- Learn more about the Playdate filesystem. +---@field listFiles fun(path: string, showhidden?: boolean): string[] +--- Returns true if a file exists at the given path. Unlike the image or sound loading functions, this function requires `path` to include the file extension since it cannot be inferred from context. Additionally, note that asset files are compiled into a format easier for Playdate to use and will have a different extension: .wav and .aiff audio files are compiled to .pda format, and .gif and .png files become `.pdi`s. +---@field exists fun(path: string): boolean +--- Returns true if a directory exists at the given path. +---@field isdir fun(path: string): boolean +--- Creates a directory at the given path, under the /Data/ folder. See About the Playdate Filesystem for details. +--- +--- playdate.file.mkdir() will create all intermediate directories, if a succession of directories ("testdir/testdir/testdir/") is specified in `path`. +--- +--- Equivalent to playdate->file->mkdir() in the C API. +---@field mkdir fun(path: string) +--- Deletes the file at the given path. Returns true if successful, else false. +--- +--- If `recursive` is true, this function will delete the directory at `path` and its contents, otherwise the directory must be empty to be deleted. +---@field delete fun(path: string, recursive?: boolean): boolean +--- Returns the size of the file at the given path. +---@field getSize fun(path: string): number +--- Returns the type of the file at the given path. +---@field getType fun(path: string): pd_UNDOCUMENTED +--- Returns the modification date/time of the file at the given path, as a table with keys: +--- +--- `year`: 4-digit year (until 10,000 AD) +--- +--- `month`: month of the year, where 1 is January and 12 is December +--- +--- `day`: day of the month, 1 - 31 +--- +--- `hour`: 0 - 23 +--- +--- `minute`: 0 - 59 +--- +--- `second`: 0 - 59 (or 60 on a leap second) +---@field modtime fun(path: string): pd_file_time_table +--- Renames the file at `path`, if it exists, to the value of newPath. This can result in the file being moved to a new directory, but directories will not be created. Returns true if the operation was successful. +--- +--- Equivalent to playdate->file->rename() in the C API. +---@field rename fun(path: string, newPath: string): boolean +--- Loads the compiled `.pdz` file at the given location and returns the contents as a function. The .pdz extension on `path` is optional. +--- +--- `env`, if specified, is a table to use as the function’s global namespace instead of `_G`. +---@field load fun(path: string, env?: table): fun() +--- Runs the pdz file at the given location. Equivalent to playdate.file.load(path, env)(). +--- +--- The `.pdz` extension on `path` is optional. Values returned from the pdz file are left on the stack. +--- +--- `env`, if specified, is a table to use as the function’s global namespace instead of `_G`. +---@field run fun(path: string, env?: table) +---@field kFileRead pd_filemode +---@field kFileWrite pd_filemode +---@field kFileAppend pd_filemode +---@field kSeekSet pd_seek_mode +---@field kSeekFromCurrent pd_seek_mode +---@field kSeekFromEnd pd_seek_mode + +---@class pd_geometry_lib +---@field affineTransform pd_affineTransform_lib +---@field arc pd_arc_lib +---@field lineSegment pd_lineSegment_lib +---@field point pd_point_lib +---@field polygon pd_polygon_lib +---@field rect pd_rect_lib +---@field size pd_size_lib +--- Returns the square of the distance from point `(x1, y1)` to point `(x2, y2)`. +--- +--- Compared to geometry.point:squaredDistanceToPoint(), this version will be slightly faster. +---@field squaredDistanceToPoint fun(x1: number, y1: number, x2: number, y2: number): number +--- Returns the the distance from point `(x1, y1)` to point `(x2, y2)`. +--- +--- Compared to geometry.point:distanceToPoint(), this version will be slightly faster. +---@field distanceToPoint fun(x1: number, y1: number, x2: number, y2: number): number +---@field vector2D pd_vector2D_lib +---@field kUnflipped pd_flip +---@field kFlippedX pd_flip +---@field kFlippedY pd_flip +---@field kFlippedXY pd_flip + +---@class pd_graphics_lib +--- Pushes the current graphics state to the context stack and creates a new context. If a playdate.graphics.image is given, drawing functions are applied to the image instead of the screen buffer. +--- +--- If you draw into an image context with color set to `playdate.graphics.kColorClear`, those drawn pixels will be set to transparent. When you later draw the image into the framebuffer, those pixels will not be rendered, i.e., will act as transparent pixels in the image. +--- playdate.graphics.lockFocus(`image`) will reroute drawing into an image, without saving the overall graphics context. +--- +--- Equivalent to playdate->graphics->pushContext() in the C API. +---@field pushContext fun(image?: pd_image) +--- Pops a graphics context off the context stack and restores its state. +--- +--- Equivalent to playdate->graphics->popContext() in the C API. +---@field popContext fun() +--- Clears the entire display, setting the color to either the given `color` argument, or the current background color set in setBackgroundColor(color) if no argument is given. +--- +--- Equivalent to playdate->graphics->clear() in the C API. +---@field clear fun(color?: pd_color) +---@field image pd_image_lib +--- Returns the pair (`width`, `height`) for the image at `path` without actually loading the image. +---@field imageSizeAtPath fun(path: string): (number, number) +--- Returns true if the non-alpha-masked portions of `image1` and `image2` overlap if they were drawn at positions (`x1`, `y1`) and (`x2`, `y2`) and flipped according to `flip1` and `flip2`, which should each be one of the values listed in playdate.graphics.image:draw(). +---@field checkAlphaCollision fun(image1: pd_image, x1: number, y1: number, flip1: pd_image_flip, image2: pd_image, x2: number, y2: number, flip2: pd_image_flip): boolean +--- Sets and gets the current drawing color for primitives. +--- +--- `color` should be one of the constants: +--- +--- `playdate.graphics.kColorBlack` +--- +--- `playdate.graphics.kColorWhite` +--- +--- `playdate.graphics.kColorClear` +--- +--- `playdate.graphics.kColorXOR` +--- +--- This color applies to drawing primitive shapes such as lines and rectangles, not bitmap images. +--- +--- setColor() and setPattern() / setDitherPattern() are mutually exclusive. Setting a color will overwrite a pattern, and vice versa. +---@field setColor fun(color: pd_color) +--- Gets the current drawing color for primitives. +---@field getColor fun(): pd_color +--- Sets the color used for drawing the background, if necessary, before playdate.graphics.sprites are drawn on top. +--- +--- `color` should be one of the constants: +--- +--- `playdate.graphics.kColorBlack` +--- +--- `playdate.graphics.kColorWhite` +--- +--- `playdate.graphics.kColorClear` +--- +--- Use `kColorClear` if you intend to draw behind sprites. +--- +--- Equivalent to playdate->graphics->setBackgroundColor() in the C API. +---@field setBackgroundColor fun(color: pd_color) +--- Gets the color used for drawing the background, if necessary, before playdate.graphics.sprites are drawn on top. +---@field getBackgroundColor fun(): pd_color +--- Sets the 8x8 pattern used for drawing. The `pattern` argument is an array of 8 numbers describing the bitmap for each row; for example, `{ 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55 }` specifies a checkerboard pattern. An additional 8 numbers can be specified for an alpha mask bitmap. +--- +--- To "un-set" a pattern, call setColor(). setColor() and setPattern() / setDitherPattern() are mutually exclusive. Setting a pattern will overwrite a color, and vice versa. +--- +--- playdate.graphics.setPattern(image, [x, y]) +--- +--- Uses the given playdate.graphics.image to set the 8 x 8 pattern used for drawing. The optional `x`, `y` offset (default 0, 0) indicates the top left corner of the 8 x 8 pattern. +---@field setPattern fun(pattern: pd_pattern) +--- Sets the pattern used for drawing to a dithered pattern. If the current drawing color is white, the pattern is white pixels on a transparent background and (due to a bug) the `alpha` value is inverted: 1.0 is transparent and 0 is opaque. Otherwise, the pattern is black pixels on a transparent background and `alpha` 0 is transparent while 1.0 is opaque. +--- +--- The optional `ditherType` argument is a dither type as used in playdate.graphics.image:blurredImage(), and should be an ordered dither type; i.e., line, screen, or Bayer. +--- +--- The error-diffusing dither types Floyd-Steinberg (kDitherTypeFloydSteinberg), Burkes (kDitherTypeBurkes), and Atkinson (kDitherTypeAtkinson) are allowed but produce very unpredictable results here. +---@field setDitherPattern fun(alpha: number, ditherType?: pd_dither_type) +--- Draws a line from (`x1`, `y1`) to (`x2`, `y2`), or draws the playdate.geometry.lineSegment `ls`. +--- +--- Line width is specified by setLineWidth(). End cap style is specified by setLineCapStyle(). +--- +--- Equivalent to playdate->graphics->drawLine() in the C API. +---@field drawLine (fun(x1: number, y1: number, x2: number, y2: number)) | (fun(ls: pd_lineSegment)) +--- Specifies the shape of the endpoints drawn by drawLine. +--- +--- `style` should be one of these constants: +--- +--- `playdate.graphics.kLineCapStyleButt` +--- +--- `playdate.graphics.kLineCapStyleRound` +--- +--- `playdate.graphics.kLineCapStyleSquare` +--- +--- Equivalent to playdate->graphics->setLineCapStyle() in the C API. +---@field setLineCapStyle fun(style: pd_line_cap_style) +--- Draw a single pixel in the current color at (`x`, `y`). +--- +--- playdate.graphics.drawPixel(p) +--- +--- Draw a single pixel in the current color at playdate.geometry.point `p`. +---@field drawPixel fun(x: number, y: number) +--- Draws the rect `r` or the rect with origin (`x`, `y`) with a size of (`w`, `h`). +--- +--- Line width is specified by setLineWidth(). Stroke location is specified by setStrokeLocation(). +--- +--- Equivalent to playdate->graphics->drawRect() in the C API. +---@field drawRect (fun(x: number, y: number, w: number, h: number)) | (fun(r: pd_rect)) +--- Draws the filled rectangle `r` or the rect at (`x`, `y`) of the given width and height. +--- +--- Equivalent to playdate->graphics->fillRect() in the C API. +---@field fillRect (fun(x: number, y: number, width: number, height: number)) | (fun(r: pd_rect)) +--- Draws a rectangle with rounded corners in the rect `r` or the rect with origin (`x`, `y`) and size (`w`, `h`). +--- +--- `radius` defines the radius of the corners. +---@field drawRoundRect (fun(x: number, y: number, w: number, h: number, radius: number)) | (fun(r: pd_rect, radius: number)) +--- Draws a filled rectangle with rounded corners in the rect `r` or the rect with origin (`x`, `y`) and size (`w`, `h`). +--- +--- `radius` defines the radius of the corners. +---@field fillRoundRect (fun(x: number, y: number, w: number, h: number, radius: number)) | (fun(r: pd_rect, radius: number)) +--- Draws an arc using the current color. +--- +--- Angles are specified in degrees, not radians. +---@field drawArc (fun(arc: pd_arc)) | (fun(x: number, y: number, radius: number, startAngle: number, endAngle: number)) +--- Draws a circle at the point `(x, y)` (or `p`) with radius `radius`. +---@field drawCircleAtPoint (fun(x: number, y: number, radius: number)) | (fun(p: pd_point, radius: number)) +--- Draws a circle in the rect `r` or the rect with origin `(x, y)` and size `(width, height)`. +--- +--- If the rect is not a square, the circle will be drawn centered in the rect. +---@field drawCircleInRect (fun(x: number, y: number, width: number, height: number)) | (fun(r: pd_rect)) +--- Draws a filled circle at the point `(x, y)` (or `p`) with radius `radius`. +---@field fillCircleAtPoint (fun(x: number, y: number, radius: number)) | (fun(p: pd_point, radius: number)) +--- Draws a filled circle in the rect `r` or the rect with origin `(x, y)` and size `(width, height)`. +--- +--- If the rect is not a square, the circle will be drawn centered in the rect. +---@field fillCircleInRect (fun(x: number, y: number, width: number, height: number)) | (fun(r: pd_rect)) +--- Draws an ellipse in the rect `r` or the rect with origin `(x, y)` and size `(width, height)`. +--- +--- `startAngle` and `endAngle`, if provided, should be in degrees (not radians), and will cause only the segment of the ellipse between `startAngle` and `endAngle` to be drawn. +---@field drawEllipseInRect (fun(x: number, y: number, width: number, height: number, startAngle?: number, endAngle?: number)) | (fun(rect: pd_rect, startAngle?: number, endAngle?: number)) +--- Draws a filled ellipse in the rect `r` or the rect with origin `(x, y)` and size `(width, height)`. +--- +--- `startAngle` and `endAngle`, if provided, should be in degrees (not radians), and will cause only the segment of the ellipse between `startAngle` and `endAngle` to be drawn. +---@field fillEllipseInRect (fun(x: number, y: number, width: number, height: number, startAngle?: number, endAngle?: number)) | (fun(rect: pd_rect, startAngle?: number, endAngle?: number)) +--- ### Overload 1 ### +--- Draw the playdate.geometry.polygon `p`. Only draws a line between the first and last vertex if the polygon is closed. +--- +--- Line width is specified by setLineWidth(). +--- +--- ### Overload 2 ### +--- Draw the polygon specified by the given sequence of x,y coordinates, including an edge between the last vertex and the first. The Lua function table.unpack() can be used to turn an array into function arguments. +--- +--- Line width is specified by setLineWidth(). +---@field drawPolygon (fun(p: pd_polygon)) | (fun(x1: number, y1: number, x2: number, y2: number, ...: number)) +--- ### Overload 1 ### +--- Fills the polygon specified by a list of x,y coordinates. An edge between the last vertex and the first is assumed. +--- +--- Equivalent to playdate->graphics->fillPolygon() in the C API. +--- +--- ### Overload 2 ### +--- Fills the polygon specified by the playdate.geometry.polygon `p` with the currently selected color or pattern. The function throws an error if the polygon is not closed. +---@field fillPolygon (fun(x1: number, y1: number, x2: number, y2: number, ...: number)) | (fun(p: pd_polygon)) +--- Sets the winding rule for filling polygons, one of: +--- +--- `playdate.graphics.kPolygonFillNonZero` +--- +--- `playdate.graphics.kPolygonFillEvenOdd` +--- +--- See https://en.wikipedia.org/wiki/Nonzero-rule for an explanation of the winding rule. +---@field setPolygonFillRule fun(rule: pd_polygon_fill_rule) +--- Draws a triangle with vertices (`x1`, `y1`), (`x2`, `y2`), and (`x3`, `y3`). +---@field drawTriangle fun(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) +--- Draws a filled triangle with vertices (`x1`, `y1`), (`x2`, `y2`), and (`x3`, `y3`). +--- +--- Equivalent to playdate->graphics->fillTriangle() in the C API. +---@field fillTriangle fun(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) +---@field nineSlice pd_nineSlice_lib +--- Returns the Perlin value (from 0.0 to 1.0) at position `(x, y, z)`. +--- +--- If `repeat` is greater than 0, the pattern of noise will repeat at that point on all 3 axes. +--- +--- `octaves` is the number of octaves of noise to apply. Compute time increases linearly with each additional octave, but the results are a bit more organic, consisting of a combination of larger and smaller variations. +--- +--- When using more than one octave, `persistence` is a value from 0.0 - 1.0 describing the amount the amplitude is scaled each octave. The lower the value of `persistence`, the less influence each successive octave has on the final value. +---@field perlin fun(x: number, y: number, z: number, repeat: number, octaves?: number, persistence?: number): number +--- Returns an array of Perlin values at once, avoiding the performance penalty of calling `perlin()` multiple times in a loop. +--- +--- The parameters are the same as `perlin()` except: +--- +--- `count` is the number of values to be returned. +--- +--- `dx`, `dy`, and `dz` are how far to step along the x, y, and z axes in each iteration. +---@field perlinArray fun(count: number, x: number, dx: number, y?: number, dy?: number, z?: number, dz?: number, repeat?: number, octaves?: number, persistence?: number): number[] +--- You must import `CoreLibs/qrcode` to use this function. +--- This function uses playdate.timer internally, so be sure to call playdate.timer.updateTimers() in your main playdate.update() function, otherwise the callback will never be invoked. +--- +--- Asynchronously returns an image representing a QR code for the passed-in string to the function callback. The arguments passed to the callback are `image`, `errorMessage`. (If an `errorMessage` string is returned, `image` will be nil.) +--- +--- desiredEdgeDimension lets you specify an approximate edge dimension in pixels for the desired QR code, though the function has limited flexibility in sizing QR codes, based on the amount of information to be encoded, and the restrictions of a 1-bit screen. The function will attempt to generate a QR code `smaller` than desiredEdgeDimension if possible. (Note that QR codes always have the same width and height.) +--- +--- If you specify nil for desiredEdgeDimension, the returned image will balance small size with easy readability. If you specify 0, the returned image will be the smallest possible QR code for the specified string. +--- +--- generateQRCode() will return a reference to the timer it uses to run asynchronously. If you wish to stop execution of the background process generating the QR code, call :remove() on that returned timer. +--- +--- If you know ahead of time what data you plan to encode, it is much faster to pre-generate the QR code, store it as a .png file in your game, and draw the .png at runtime. You can use playdate.simulator.writeToFile() to create this .png file. +---@field generateQRCode fun(stringToEncode: string, desiredEdgeDimension: number, callback: fun(image: pd_image)): pd_timer +--- You must import `CoreLibs/graphics` to use this function. +--- +--- Draws an approximation of a sine wave between the points `startX, startY` and `endX, endY`. +--- +--- `startAmplitude`: The number of pixels above and below the line from `startX, startY` and `endX, endY` the peaks and valleys of the wave will be drawn at the start of the wave. +--- +--- `endAmplitude`: The number of pixels above and below the line from `startX, startY` and `endX, endY` the peaks and valleys of the wave will be drawn at the end of the wave. +--- +--- `period`: The distance between peaks, in pixels. +--- +--- `phaseShift`: If provided, specifies the wave’s offset, in pixels. +---@field drawSineWave fun(startX: number, startY: number, endX: number, endY: number, startAmplitude: number, endAmplitude: number, period: number, phaseShift?: number) +--- setClipRect() sets the clipping rectangle for all subsequent graphics drawing, including bitmaps. The argument can either be separate dimensions or a playdate.geometry.rect object. The clip rect is automatically cleared at the beginning of the playdate.update() callback. The function uses world coordinates; that is, the given rectangle will be translated by the current drawing offset. To use screen coordinates instead, use setScreenClipRect() +--- +--- Equivalent to playdate->graphics->setClipRect() in the C API. +---@field setClipRect (fun(x: number, y: number, width: number, height: number)) | (fun(rect: pd_rect)) | (fun(rect: pd_rect)) +--- getClipRect() returns multiple values (`x`, `y`, `width`, `height`) giving the current clipping rectangle. +---@field getClipRect fun(): pd_rect +--- Sets the clip rectangle as above, but uses screen coordinates instead of world coordinates—​that is, it ignores the current drawing offset. +--- +--- Equivalent to playdate->graphics->setScreenClipRect() in the C API. +---@field setScreenClipRect (fun(x: number, y: number, width: number, height: number)) | (fun(rect: pd_rect)) +--- Returns the clip rect as in getClipRect(), but using screen coordinates instead of world coordinates. +---@field getScreenClipRect fun(): pd_rect +--- Clears the current clipping rectangle, set with setClipRect(). +--- +--- Equivalent to playdate->graphics->clearClipRect() in the C API. +---@field clearClipRect fun() +--- Sets the current stencil to the given image. If `tile` is set, the the stencil will be tiled; in this case, the image width must be a multiple of 32 pixels. +--- +--- Equivalent to playdate->graphics->setStencilImage() in the C API. +---@field setStencilImage fun(image: pd_image, tile?: boolean) +--- ### Overload 1 ### +--- Sets a pattern to use for stenciled drawing, as an alternative to creating an image, drawing a pattern into the image, then using that in setStencilImage(). pattern should be a table of the form { row1, row2, row3, row4, row5, row6, row7, row8 }. +--- +--- ### Overload 2 ### +--- Sets a pattern to use for stenciled drawing, as an alternative to creating an image, drawing a pattern into the image, then using that in setStencilImage(). +--- +--- ### Overload 3 ### +--- Sets the stencil to a dither pattern specified by `level` and optional `ditherType` (defaults to playdate.graphics.image.kDitherTypeBayer8x8). +--- +--- ### Overload 4 ### +--- Sets the sprite’s stencil to the given pattern, tiled across the screen. +--- +--- ### Overload 5 ### +--- Sets the sprite’s stencil to the given pattern, tiled across the screen. pattern should be a table of the form { row1, row2, row3, row4, row5, row6, row7, row8 }. +---@field setStencilPattern (fun(pattern: pd_pattern)) | (fun(row1: number, row2: number, row3: number, row4: number, row5: number, row6: number, row7: number, row8: number)) | (fun(level: number, ditherType?: pd_dither_type)) | (fun(eightRows: number[])) | (fun(pattern: pd_pattern)) +--- Clears the stencil buffer. +---@field clearStencil fun() +--- `Deprecated.` +--- +--- Clears the stencil buffer. +---@field clearStencilImage fun() +--- Sets the current drawing mode for images. +--- +--- The draw mode applies to images and fonts (which are technically images). The draw mode does not apply to primitive shapes such as lines or rectangles. +--- +--- The available options for `mode` (demonstrated by drawing a two-color background image first, setting the specified draw mode, then drawing the Crankin' character on top) are: +--- +--- `playdate.graphics.kDrawModeCopy`: Images are drawn exactly as they are (black pixels are drawn black and white pixels are drawn white) +--- +--- `playdate.graphics.kDrawModeWhiteTransparent`: Any white portions of an image are drawn transparent (black pixels are drawn black and white pixels are drawn transparent) +--- +--- `playdate.graphics.kDrawModeBlackTransparent`: Any black portions of an image are drawn transparent (black pixels are drawn transparent and white pixels are drawn white) +--- +--- `playdate.graphics.kDrawModeFillWhite`: All non-transparent pixels are drawn white (black pixels are drawn white and white pixels are drawn white) +--- +--- `playdate.graphics.kDrawModeFillBlack`: All non-transparent pixels are drawn black (black pixels are drawn black and white pixels are drawn black) +--- +--- `playdate.graphics.kDrawModeXOR`: Pixels are drawn inverted on white backgrounds, creating an effect where any white pixels in the original image will always be visible, regardless of the background color, and any black pixels will appear transparent (on a white background, black pixels are drawn white and white pixels are drawn black) +--- +--- `playdate.graphics.kDrawModeNXOR`: Pixels are drawn inverted on black backgrounds, creating an effect where any black pixels in the original image will always be visible, regardless of the background color, and any white pixels will appear transparent (on a black background, black pixels are drawn white and white pixels are drawn black) +--- +--- `playdate.graphics.kDrawModeInverted`: Pixels are drawn inverted (black pixels are drawn white and white pixels are drawn black) +--- +--- Instead of the above-specified constants, you can also use one of the following strings: "copy", "inverted", "XOR", "NXOR", "whiteTransparent", "blackTransparent", "fillWhite", or "fillBlack". +--- +--- Equivalent to playdate->graphics->setDrawMode() in the C API. +---@field setImageDrawMode fun(mode: pd_draw_mode) +--- Gets the current drawing mode for images. +---@field getImageDrawMode fun(): pd_draw_mode +--- Sets the width of the line for drawLine, drawRect, drawPolygon, and drawArc when a playdate.geometry.arc is passed as the argument. This value is saved and restored when pushing and popping the graphics context. +---@field setLineWidth fun(width: number) +--- Gets the current line width. +---@field getLineWidth fun(): number +--- Specifies where the stroke is placed relative to the rectangle passed into drawRect. +--- +--- `location` is one of these constants: +--- +--- `playdate.graphics.kStrokeCentered` +--- +--- `playdate.graphics.kStrokeOutside` +--- +--- `playdate.graphics.kStrokeInside` +--- +--- This value is saved and restored when pushing and popping the graphics context. +---@field setStrokeLocation fun(location: pd_stroke_location) +--- Gets the current stroke position. +---@field getStrokeLocation fun(): pd_stroke_location +--- lockFocus() routes all drawing to the given playdate.graphics.image. playdate.graphics.unlockFocus() returns drawing to the frame buffer. +--- +--- If you draw into an image with color set to `playdate.graphics.kColorClear`, those drawn pixels will be set to transparent. When you later draw the image into the framebuffer, those pixels will not be rendered, i.e., will act as transparent pixels in the image. +--- playdate.graphics.pushContext(`image`) will also allow offscreen drawing into an image, with the additional benefit of being able to save and restore the graphics state. +---@field lockFocus fun(image: pd_image) +--- After calling unlockFocus(), drawing is routed to the frame buffer. +---@field unlockFocus fun() +---@field animation pd_animation_lib +---@field animator pd_animator_lib +--- setDrawOffset(x, y) offsets the origin point for all drawing calls to `x`, `y` (can be negative). So, for example, if the offset is set to -20, -20, an image drawn at 20, 20 will appear at the origin (in the upper left corner.) +--- +--- This is useful, for example, for centering a "camera" on a sprite that is moving around a world larger than the screen. +--- +--- The `x` and `y` arguments to .setDrawOffset() are always specified in the original, unaltered coordinate system. So, for instance, repeated calls to playdate.graphics.setDrawOffset(-10, -10) will leave the draw offset unchanged. Likewise, .setDrawOffset(0, 0) will always "disable" the offset. +--- It can be useful to have operations sometimes ignore the draw offsets. For example, you may want to have the score or some other heads-up display appear onscreen apart from scrolling content. A sprite can be set to ignore offsets by calling playdate.graphics.sprite:setIgnoresDrawOffset(true). playdate.graphics.image:drawIgnoringOffsets() lets you render an image using screen coordinates. +--- +--- Equivalent to playdate->graphics->setDrawOffset() in the C API. +---@field setDrawOffset fun(x: number, y: number) +--- getDrawOffset() returns multiple values (`x`, `y`) giving the current draw offset. +---@field getDrawOffset fun(): (number, number) +--- Returns a copy the contents of the `last completed frame`, i.e., a "screenshot", as a playdate.graphics.image. +--- +--- Display functions like setMosaic(), setInverted(), setScale(), and setOffset() do not affect the returned image. +---@field getDisplayImage fun(): pd_image +--- Returns a copy the contents of the working frame buffer — `the current frame, in-progress` — as a playdate.graphics.image. +--- +--- Display functions like setMosaic(), setInverted(), setScale(), and setOffset() do not affect the returned image. +---@field getWorkingImage fun(): pd_image +---@field imagetable pd_imagetable_lib +---@field tilemap pd_tilemap_lib +---@field sprite pd_sprite_lib +---@field font pd_font_lib +--- Sets the current font, a playdate.graphics.font. +--- +--- `variant` should be one of the strings "normal", "bold", or "italic", or one of the constants: +--- +--- `playdate.graphics.font.kVariantNormal` +--- +--- `playdate.graphics.font.kVariantBold` +--- +--- `playdate.graphics.font.kVariantItalic` +--- +--- If no variant is specified, `kFontVariantNormal` is used. +--- +--- Equivalent to playdate->graphics->setFont() in the C API. +---@field setFont fun(font: pd_font, variant?: pd_font_variant) +--- Returns the current font, a playdate.graphics.font. +---@field getFont fun(variant?: pd_font_variant): pd_font +--- Sets multiple font variants at once. fontFamily should be a table using the following format: +--- +--- local fontFamily = { +--- [playdate.graphics.font.kVariantNormal] = normal_font, +--- [playdate.graphics.font.kVariantBold] = bold_font, +--- [playdate.graphics.font.kVariantItalic] = italic_font +--- } +--- +--- All fonts and font variants need not be present in the table. +---@field setFontFamily fun(fontFamily: pd_font_family) +--- Sets the global font tracking (spacing between letters) in pixels. This value is added to the font’s own tracking value as specified in its .fnt file. +--- +--- See playdate.graphics.font:setTracking to adjust tracking on a specific font. +---@field setFontTracking fun(pixels: number) +--- Gets the global font tracking (spacing between letters) in pixels. +---@field getFontTracking fun(): number +--- Like getFont() but returns the system font rather than the currently set font. +--- +--- `variant` should be one of the strings "normal", "bold", or "italic", or one of the constants: +--- +--- `playdate.graphics.font.kVariantNormal` +--- +--- `playdate.graphics.font.kVariantBold` +--- +--- `playdate.graphics.font.kVariantItalic` +---@field getSystemFont fun(variant?: pd_font_variant): pd_font +--- Draws the text using the current font and font advance at location (`x`, `y`). If `width` and `height` are specified, drawing is constrained to the rectangle (x,y,width,height), using the given `wrapMode` and `alignment`, if provided. Alternatively, a playdate.geometry.rect object can be passed instead of x,y,width,height. Valid values for `wrapMode` are +--- +--- `playdate.graphics.kWrapClip` +--- +--- `playdate.graphics.kWrapCharacter` +--- +--- `playdate.graphics.kWrapWord` +--- +--- and values for `alignment` are +--- +--- `playdate.graphics.kAlignLeft` +--- +--- `playdate.graphics.kAlignCenter` +--- +--- `playdate.graphics.kAlignRight` +--- +--- The default wrap mode is playdate.graphics.kWrapWord and the default alignment is playdate.graphics.kAlignLeft. +--- +--- If `fontFamily` is provided, the text is draw using the given fonts instead of the currently set font. `fontFamily` should be a table of fonts using keys as specified in setFontFamily(fontFamily). +--- +--- The optional `leadingAdjustment` may be used to modify the spacing between lines of text. Pass nil to use the default leading for the font. +--- +--- Returns two numbers indicating the width and height of the drawn text. +--- +--- Styling text +--- +--- To draw bold text, surround the bold portion of text with asterisks. To draw italic text, surround the italic portion of text with underscores. For example: +--- +--- playdate.graphics.drawText("normal *bold* _italic_", x, y) +--- +--- which will output: "normal bold `italic`". Bold and italic font variations must be set using setFont() with the appropriate variant argument, otherwise the default Playdate fonts will be used. +--- +--- Escaping styling characters +--- +--- To draw an asterisk or underscore, use a double-asterisk or double-underscore. Styles may not be nested, but double-characters can be used inside of a styled portion of text. +--- +--- For a complete set of characters allowed in `text`, see playdate.graphics.font. In addition, the newline character \n is allowed and works as expected. +--- +--- Avoiding styling +--- +--- Use playdate.graphics.font:drawText(), which doesn’t support formatted text. +--- +--- Inverting text color +--- +--- To draw white-on-black text (assuming the font you are using is defined in the standard black-on-transparent manner), first call playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite), followed by the appropriate drawText() call. setImageDrawMode() affects how text is rendered because characters are technically images. +--- +--- Equivalent to playdate->graphics->drawText() in the C API. +---@field drawText (fun(text: string, x: number, y: number, width?: number, height?: number, fontFamily?: pd_font_family, leadingAdjustment?: number, wrapMode?: pd_wrap_mode, alignment?: pd_text_alignment): (number, number)) | (fun(text: string, rect: pd_rect, fontFamily?: pd_font_family, leadingAdjustment?: number, wrapMode?: pd_wrap_mode, alignment?: pd_text_alignment): (number, number)) +--- Draws the text found by doing a lookup of `key` in the .strings file corresponding to the current system language, or `language`, if specified. +--- +--- The optional `language` argument can be one of the strings "en", "jp", or one of the constants: +--- +--- `playdate.graphics.font.kLanguageEnglish` +--- +--- `playdate.graphics.font.kLanguageJapanese` +--- +--- Other arguments work the same as in drawText(). +--- +--- For more information about localization and strings files, see the Localization section. +---@field drawLocalizedText (fun(key: string, x: number, y: number, width?: number, height?: number, language?: pd_language, leadingAdjustment?: number, wrapMode?: pd_wrap_mode, alignment?: pd_text_alignment)) | (fun(key: string, rect: pd_rect, language?: pd_language, leadingAdjustment?: number)) +--- Returns a string found by doing a lookup of `key` in the .strings file corresponding to the current system language, or `language`, if specified. +--- +--- The optional `language` argument can be one of the strings "en", "jp", or one of the constants: +--- +--- `playdate.graphics.font.kLanguageEnglish` +--- +--- `playdate.graphics.font.kLanguageJapanese` +--- +--- For more information about localization and strings files, see the Localization section. +---@field getLocalizedText fun(key: string, language?: pd_language): string +--- Returns multiple values `(width, height)` giving the dimensions required to draw the text `str` using drawText(). Newline characters (\n) are respected. +--- +--- `fontFamily` should be a table of fonts using keys as specified in setFontFamily(fontFamily). If provided, fonts from `fontFamily` will be used for calculating the size of `str` instead of the currently set font. +---@field getTextSize fun(str: string, fontFamily?: pd_font_family, leadingAdjustment?: number): (number, number) +--- You must import `CoreLibs/graphics` to use this function. +--- +--- Draws the string `text` aligned to the left, right, or centered on the `x` coordinate. Pass one of `kTextAlignment.left`, `kTextAlignment.center`, `kTextAlignment.right` for the `alignment` parameter. +--- +--- For text formatting options, see drawText() +--- +--- To draw unstyled text using a single font, see playdate.graphics.font:drawTextAligned() +---@field drawTextAligned fun(text: string, x: number, y: number, alignment: pd_text_alignment, leadingAdjustment?: number) +--- You must import `CoreLibs/graphics` to use these functions. +--- +--- Draws the text using the current font and font advance into the rect defined by (`x`, `y`, `width`, `height`) (or `rect`). +--- +--- If `truncationString` is provided and the text cannot fit in the rect, `truncationString` will be appended to the last line. +--- +--- `alignment`, if provided, should be one of one of `kTextAlignment.left`, `kTextAlignment.center`, `kTextAlignment.right`. Pass nil for `leadingAdjustment` and `truncationString` if those parameters are not required. +--- +--- `font`, if provided, will cause the text to be drawn unstyled using font:drawText() rather than playdate.graphics.drawText() using the currently-set system fonts. +--- +--- For text formatting options, see drawText() +--- +--- Returns `width`, `height`, `textWasTruncated` +--- +--- `width` and `height` indicate the size in pixels of the drawn text. These values may be smaller than the width and height specified when calling the function. +--- +--- `textWasTruncated` indicates if the text was truncated to fit within the specified rect. +---@field drawTextInRect (fun(text: string, x: number, y: number, width: number, height: number, leadingAdjustment?: number, truncationString?: string, alignment?: pd_text_alignment, font?: pd_font): (number, number, boolean)) | (fun(text: string, rect: pd_rect, leadingAdjustment?: number, truncationString?: string, alignment?: pd_text_alignment, font?: pd_font): (number, number, boolean)) +--- You must import `CoreLibs/graphics` to use this function. +--- +--- Same as drawTextAligned() except localized text is drawn. +---@field drawLocalizedTextAligned fun(text: string, x: number, y: number, alignment: pd_text_alignment, language?: pd_language, leadingAdjustment?: number) +--- You must import `CoreLibs/graphics` to use these functions. +--- +--- Same as drawTextInRect() except localized text is drawn. +---@field drawLocalizedTextInRect (fun(text: string, x: number, y: number, width: number, height: number, leadingAdjustment?: number, truncationString?: string, alignment?: pd_text_alignment, font?: pd_font, language?: pd_language)) | (fun(text: string, rect: pd_rect, leadingAdjustment?: number, truncationString?: string, alignment?: pd_text_alignment, font?: pd_font, language?: pd_language)) +--- You must import `CoreLibs/graphics` to use this function. +--- +--- Returns `width`, `height` which indicate the minimum size required for `text` to be drawn using drawTextInRect(). The `width` returned will be less than or equal to `maxWidth`. +--- +--- `font`, if provided, will cause the text size to be calculated without bold or italic styling using the specified font. +---@field getTextSizeForMaxWidth fun(text: string, maxWidth: number, leadingAdjustment?: number, font?: pd_font): (number, number) +--- You must import `CoreLibs/graphics` to use this function. +--- +--- Generates an image containing `text`. This is useful if you need to redraw the same text frequently. +--- +--- `maxWidth` and `maxHeight` specify the maximum size of the returned image. +--- +--- `backgroundColor`, if specified, will cause the image’s background to be one of `playdate.graphics.kColorWhite`, `playdate.graphics.kColorBlack`, or `playdate.graphics.kColorClear`. +--- +--- `font`, if provided, will cause the text to be drawn without bold or italic styling using the specified font. +--- +--- The remaining arguments are the same as those in drawTextInRect(). +--- +--- Returns `image`, `textWasTruncated` +--- +--- `image` is a newly-created image containing the specified text, or nil if an image could not be created. The image’s dimensions may be smaller than `maxWidth`, `maxHeight`. +--- +--- `textWasTruncated` indicates if the text was truncated to fit within the specified width and height. +---@field imageWithText fun(text: string, maxWidth: number, maxHeight: number, backgroundColor?: pd_color, leadingAdjustment?: number, truncationString?: string, alignment?: pd_text_alignment, font?: pd_font): (pd_image, boolean) +---@field video pd_video_lib +---@field kColorBlack pd_color +---@field kColorWhite pd_color +---@field kColorClear pd_color +---@field kColorXOR pd_color +---@field kDrawModeCopy pd_draw_mode +---@field kDrawModeWhiteTransparent pd_draw_mode +---@field kDrawModeBlackTransparent pd_draw_mode +---@field kDrawModeFillWhite pd_draw_mode +---@field kDrawModeFillBlack pd_draw_mode +---@field kDrawModeXOR pd_draw_mode +---@field kDrawModeNXOR pd_draw_mode +---@field kDrawModeInverted pd_draw_mode +---@field kLineCapStyleButt pd_line_cap_style +---@field kLineCapStyleRound pd_line_cap_style +---@field kLineCapStyleSquare pd_line_cap_style +---@field kPolygonFillNonZero pd_polygon_fill_rule +---@field kPolygonFillEvenOdd pd_polygon_fill_rule +---@field kStrokeCentered pd_stroke_location +---@field kStrokeOutside pd_stroke_location +---@field kStrokeInside pd_stroke_location +---@field kVariantNormal pd_font_variant +---@field kVariantBold pd_font_variant +---@field kVariantItalic pd_font_variant +---@field kWrapClip pd_wrap_mode +---@field kWrapCharacter pd_wrap_mode +---@field kWrapWord pd_wrap_mode +---@field kAlignLeft pd_text_alignment +---@field kAlignCenter pd_text_alignment +---@field kAlignRight pd_text_alignment +---@field kImageUnflipped pd_image_flip +---@field kImageFlippedX pd_image_flip +---@field kImageFlippedY pd_image_flip +---@field kImageFlippedXY pd_image_flip + +---@class pd_keyboard_lib +--- Opens the keyboard, taking over input focus. +--- +--- `text`, if provided, will be used to set the initial text value of the keyboard. +---@field show fun(text?: string) +--- Hides the keyboard. +---@field hide fun() +--- Access or set the text value of the keyboard. +---@field text string +--- `behavior` should be one of the constants `playdate.keyboard.kCapitalizationNormal`, `playdate.keyboard.kCapitalizationWords`, or `playdate.keyboard.kCapitalizationSentences`. +--- +--- In the case of `playdate.keyboard.kCapitalizationWords`, the keyboard selection will automatically move to the upper case column after a space is entered. For `playdate.keyboard.kCapitalizationSentences` the selection will automatically move to the upper case column after a period and a space have been entered. +---@field setCapitalizationBehavior fun(behavior: pd_capitalization) +--- Returns the current x location of the left edge of the keyboard. +---@field left fun(): number +--- Returns the pixel width of the keyboard. +---@field width fun(): number +--- Returns true if the keyboard is currently being shown. +---@field isVisible fun(): boolean +--- If set, this function will be called when the keyboard is finished the opening animation. +---@field keyboardDidShowCallback nil | (fun()) +--- If set, this function will be called when the keyboard has finished the hide animation. +---@field keyboardDidHideCallback nil | (fun()) +--- If set, this function will be called when the keyboard starts to close. A Boolean argument will be passed to the callback, true if the user selected "OK" close the keyboard, false otherwise. +---@field keyboardWillHideCallback nil | (fun()) +--- If set, this function is called as the keyboard animates open or closed. Provided as a way to sync animations with the keyboard movement. +---@field keyboardAnimatingCallback nil | (fun()) +--- If set, this function will be called every time a character is entered or deleted. +---@field textChangedCallback nil | (fun()) +---@field kCapitalizationNormal pd_capitalization +---@field kCapitalizationWords pd_capitalization +---@field kCapitalizationSentences pd_capitalization + +---@class pd_math_lib +--- Returns a number that is the linear interpolation between `min` and `max` based on `t`, where `t = 0.0` will return `min` and `t = 1.0` will return `max`. +--- +--- You must import `CoreLibs/math` to use this function. +---@field lerp fun(min: number, max: number, t: number): number + +---@class pd_pathfinder_lib +---@field graph pd_graph_lib +---@field node pd_node_lib + +---@class pd_simulator_lib +--- Writes an image to a PNG file at the path specified. Only available on the Simulator. +--- +--- `path` represents a path on your development computer, not the Playdate filesystem. It’s recommended you prefix your path with ~/ to ensure you are writing to a writeable directory, for example, ~/myImageFile.png. Please include the .png file extension in your path name. Any directories in your path must already exist on your development computer in order for the file to be written. +---@field writeToFile fun(image: pd_image, path: string) +--- Quits the Playdate Simulator app. +---@field exit fun() +--- Returns the contents of the URL `url` as a string. +---@field getURL fun(url: string): string + +---@class pd_sound_lib +--- Returns the sample rate of the audio system (44100). The sample rate is determined by the hardware, and is not currently mutable. +---@field getSampleRate fun(): number +---@field sampleplayer pd_sampleplayer_lib +---@field fileplayer pd_fileplayer_lib +---@field sample pd_sample_lib +---@field channel pd_channel_lib +--- Returns a list of all sources currently playing. +---@field playingSources fun(): pd_source[] +---@field synth pd_synth_lib +---@field signal pd_signal_lib +---@field lfo pd_lfo_lib +---@field envelope pd_envelope_lib +--- Adds the given playdate.sound.effect to the default sound channel. +---@field addEffect fun(effect: pd_effect) +--- Removes the given effect from the default sound channel. +---@field removeEffect fun(effect: pd_effect) +---@field bitcrusher pd_bitcrusher_lib +---@field ringmod pd_ringmod_lib +---@field onepolefilter pd_onepolefilter_lib +---@field twopolefilter pd_twopolefilter_lib +---@field overdrive pd_overdrive_lib +---@field delayline pd_delayline_lib +---@field delaylinetap pd_delaylinetap_lib +---@field sequence pd_sequence_lib +---@field track pd_track_lib +---@field instrument pd_instrument_lib +---@field controlsignal pd_controlsignal_lib +---@field micinput pd_micinput_lib +--- Returns a pair of booleans (headphone, mic) indicating whether headphones are plugged in, and if so whether they have a microphone attached. If `changeCallback` is a function, it will be called every time the headphone state changes, until it is cleared by calling playdate.sound.getHeadphoneState(nil). If a change callback is set, the audio does not automatically switch from speaker to headphones when headphones are plugged in (and vice versa), so the callback should use playdate.sound.setOutputsActive() to change the output if needed. The callback is passed two booleans, matching the return values from getHeadphoneState(): the first true if headphones are connect, and the second true if the headphones have a microphone. +--- +--- Equivalent to playdate->sound->getHeadphoneState() in the C API. +---@field getHeadphoneState fun(changeCallback: nil | (fun(boolean, boolean))): (boolean, boolean) +--- Forces sound to be played on the headphones or on the speaker, regardless of whether headphones are plugged in or not. (With the caveat that it is not actually possible to play on the headphones if they’re not plugged in.) This function has no effect in the Simulator. +--- +--- Equivalent to playdate->sound->setOutputsActive() in the C API. +---@field setOutputsActive fun(headphones: boolean, speaker: boolean) +--- Returns the current time, in seconds, as measured by the audio device. The audio device uses its own time base in order to provide accurate timing. +--- +--- Equivalent to playdate->sound->getCurrentTime() in the C API. +---@field getCurrentTime fun(): number +--- Resets the audio output device time counter. +---@field resetTime fun() +---@field kFormat8bitMono pd_sound_format +---@field kFormat8bitStereo pd_sound_format +---@field kFormat16bitMono pd_sound_format +---@field kFormat16bitStereo pd_sound_format +---@field kWaveSine pd_waveform +---@field kWaveSquare pd_waveform +---@field kWaveSawtooth pd_waveform +---@field kWaveTriangle pd_waveform +---@field kWaveNoise pd_waveform +---@field kWavePOPhase pd_waveform +---@field kWavePODigital pd_waveform +---@field kWavePOVosim pd_waveform +---@field kLFOSquare pd_lfo_type +---@field kLFOSawtoothUp pd_lfo_type +---@field kLFOSawtoothDown pd_lfo_type +---@field kLFOTriangle pd_lfo_type +---@field kLFOSine pd_lfo_type +---@field kLFOSampleAndHold pd_lfo_type +---@field kFilterLowPass pd_sound_filter +---@field kFilterHighPass pd_sound_filter +---@field kFilterBandPass pd_sound_filter +---@field kFilterNotch pd_sound_filter +---@field kFilterPEQ pd_sound_filter +---@field kFilterLowShelf pd_sound_filter +---@field kFilterHighShelf pd_sound_filter + +---@class pd_string_lib +--- Generates a random string of uppercase letters +---@field UUID fun(length: number) +--- Returns a string with the whitespace removed from the beginning and ending of `string`. +---@field trimWhitespace fun(string: string): string +--- Returns a string with the whitespace removed from the beginning of `string`. +---@field trimLeadingWhitespace fun(string: string): string +--- Returns a string with the whitespace removed from the ending of `string`. +---@field trimTrailingWhitespace fun(string: string): string + +---@class pd_timer_lib : pd_timer +--- This should be called from the main playdate.update() loop to drive the timers. +---@field updateTimers fun() +--- ### Overload 1 ### +--- Returns a new playdate.timer that will run for `duration` milliseconds. `callback` is a function closure that will be called when the timer is complete. +--- +--- Accepts a variable number of arguments that will be passed to the callback function when it is called. If arguments are not provided, the timer itself will be passed to the callback instead. +--- +--- By default, timers start upon instantiation. To modify the behavior of a timer, see common timer methods and properties. +--- +--- ### Overload 2 ### +--- Returns a new playdate.timer that will run for `duration` milliseconds. If not specified, `startValue` and `endValue` will be 0, and a linear easing function will be used. +--- +--- By default, timers start upon instantiation. To modify the behavior of a timer, see common timer methods and properties. +---@field new (fun(duration: number, callback: fun(...), ...: any): pd_timer) | (fun(duration: number, startValue?: number, endValue?: number, easingFunction?: (fun(number, number, number, number): number)): pd_timer) +--- Performs the function `callback` after `delay` milliseconds. Accepts a variable number of arguments that will be passed to the callback function when it is called. If arguments are not provided, the timer itself will be passed to the callback instead. +---@field performAfterDelay fun(delay: number, callback: fun(...), ...: any) +--- Calls keyRepeatTimerWithDelay() below with standard values of `delayAfterInitialFiring` = 300 and `delayAfterSecondFiring` = 100. +---@field keyRepeatTimer fun(callback: fun(...), ...: any): pd_timer +--- returns a timer that fires at key-repeat intervals. The function `callback` will be called immediately, then again after `delayAfterInitialFiring` milliseconds, then repeatedly at `delayAfterSecondFiring` millisecond intervals. +---@field keyRepeatTimerWithDelay fun(delayAfterInitialFiring: number, delayAfterSecondFiring: number, callback: fun(...), ...: any): pd_timer +--- Returns an array listing all running timers. +--- +--- Note the "." syntax rather than ":". This is a class method, not an instance method. +---@field allTimers fun(): pd_timer[] + +---@class pd_timer +--- Current value calculated from the start and end values, the time elapsed, and the easing function. +---@field value number +--- The function used to calculate `value`. The function should be of the form `function(t, b, c, d)`, where `t` is elapsed time, `b` is the beginning value, `c` is the change (or end value - start value), and `d` is the duration. Many such functions are available in playdate.easingFunctions. +---@field easingFunction (fun(number, number, number, number): number) +--- For easing functions that take additional amplitude and period arguments (such as `inOutElastic`), set these to the desired values. +---@field easingAmplitude number +--- For easing functions that take additional amplitude and period arguments (such as `inOutElastic`), set these to the desired values. +---@field easingPeriod number +--- Set to provide an easing function to be used for the reverse portion of the timer. The function should be of the form `function(t, b, c, d)`, where `t` is elapsed time, `b` is the beginning value, `c` is the change (or end value - start value), and `d` is the duration. Many such functions are available in playdate.easingFunctions. +---@field reverseEasingFunction (fun(number, number, number, number): number) +--- Start value used when calculating `value`. +---@field startValue number +--- End value used when calculating `value`. +---@field endValue number +--- Pauses a timer. (There is no need to call :start() on a newly-instantiated timer: timers start automatically.) +---@field pause fun(self: pd_timer) +--- Resumes a previously paused timer. There is no need to call :start() on a newly-instantiated timer: timers start automatically. +---@field start fun(self: pd_timer) +--- Removes this timer from the list of timers. This happens automatically when a non-repeating timer reaches its end, but you can use this method to dispose of timers manually. +--- +--- Note that timers do not actually get removed until the next invocation of playdate.timer.updateTimers(). +---@field remove fun(self: pd_timer) +--- Resets a timer to its initial values. +---@field reset fun(self: pd_timer) +--- The number of milliseconds the timer has been running. Read-only. +---@field currentTime READONLY_number +--- Number of milliseconds to wait before starting the timer. +---@field delay number +--- If true, the timer is discarded once it is complete. Defaults to true. +---@field discardOnCompletion boolean +--- The number of milliseconds for which the timer will run. +---@field duration number +--- The number of milliseconds remaining in the timer. Read-only. +---@field timeLeft READONLY_number +--- If true, the timer will be paused. The update callback will not be called when the timer is paused. Can be set directly, or by using playdate.timer:pause() and playdate.timer:start(). Defaults to false. +---@field paused boolean +--- If true, the timer starts over from the beginning when it completes. Defaults to false. +---@field repeats boolean +--- If true, the timer plays in reverse once it has completed. The time to complete both the forward and reverse will be `duration` x 2. Defaults to false. +--- +--- Please note that `currentTime` will restart at 0 and count up to `duration` again when the reverse timer starts, but `value` will be calculated in reverse, from `endValue` to `startValue`. The same easing function (as opposed to the inverse of the easing function) will be used for the reverse timer unless an alternate is provided by setting `reverseEasingFunction`. +---@field reverses boolean +--- A Function of the form `function(timer)` or `function(...)` where "..." corresponds to the values in the table assigned to `timerEndedArgs`. Called when the timer has completed. +---@field timerEndedCallback nil | (fun()) +--- For repeating timers, this function will be called each time the timer completes, before it starts again. +--- +--- An array-style table of values that will be passed to the `timerEndedCallback` function. +---@field timerEndedArgs any[] +--- A callback function that will be called on every frame (every time `timer.updateAll()` is called). If the timer was created with arguments, those will be passed as arguments to the function provided. Otherwise, the timer is passed as the single argument. +---@field updateCallback nil | (fun()) + +---@class pd_frameTimer_lib : pd_frameTimer +--- This should be called from the main playdate.update() loop to drive the frame timers. +---@field updateTimers fun() +--- ### Overload 1 ### +--- Returns a new playdate.frameTimer that will run for `duration` frames. `callback` is a function closure that will be called when the timer is complete. +--- +--- Accepts a variable number of arguments that will be passed to the callback function when it is called. If arguments are not provided, the timer itself will be passed to the callback instead. +--- +--- By default, frame timers start upon instantiation. To modify the behavior of a frame timer, see common frame timer methods and properties. +--- +--- ### Overload 2 ### +--- Returns a new playdate.frameTimer that will run for `duration` number of frames. If not specified, `startValue` and `endValue` will be 0, and a linear easing function will be used. +--- +--- By default, frame timers start upon instantiation. To modify the behavior of a frame timer, see common frame timer methods and properties. +---@field new (fun(duration: number, callback: fun(...), ...: any): pd_frameTimer) | (fun(duration: number, startValue?: number, endValue?: number, easingFunction?: (fun(number, number, number, number): number)): pd_frameTimer) +--- Performs the function `callback` after the `delay` number of frames. Accepts a variable number of arguments that will be passed to the callback function when it is called. If arguments are not provided, the timer itself will be passed to the callback instead. +---@field performAfterDelay fun(delay: number, callback: fun(...), ...: any) +--- Returns an array listing all running frameTimers. +--- +--- Note the "." syntax rather than ":". This is a class method, not an instance method. +---@field allTimers fun(): pd_frameTimer[] + +---@class pd_frameTimer +--- Current value calculated from the start and end values, the current frame, and the easing function. +---@field value number +--- Start value used when calculating `value`. +---@field startValue number +--- End value used when calculating `value`. +---@field endValue number +--- The function used to calculate `value`. The function should be of the form `function(t, b, c, d)`, where `t` is elapsed time, `b` is the beginning value, `c` is the change (or `endValue - startValue`), and `d` is the duration. +---@field easingFunction (fun(number, number, number, number): number) +--- For easing functions in `CoreLibs/easing` that take additional amplitude and period arguments (such as `inOutElastic`), set these to desired values. +---@field easingAmplitude number +--- For easing functions in `CoreLibs/easing` that take additional amplitude and period arguments (such as `inOutElastic`), set these to desired values. +---@field easingPeriod number +--- Set to provide an easing function to be used for the reverse portion of the timer. The function should be of the form `function(t, b, c, d)`, where `t` is elapsed time, `b` is the beginning value, `c` is the change (or `endValue - startValue`), and `d` is the duration. +---@field reverseEasingFunction (fun(number, number, number, number): number) +--- Pauses a timer. +---@field pause fun(self: pd_frameTimer) +--- Resumes a timer. There is no need to call :start() on a newly-instantiated frame timer: frame timers start automatically. +---@field start fun(self: pd_frameTimer) +--- Removes this timer from the list of timers. This happens automatically when a non-repeating timer reaches it’s end, but you can use this method to dispose of timers manually. +---@field remove fun(self: pd_frameTimer) +--- Resets a timer to its initial values. +---@field reset fun(self: pd_frameTimer) +--- Number of frames to wait before starting the timer. +---@field delay number +--- If true, the timer is discarded once it is complete. Defaults to true. +---@field discardOnCompletion boolean +--- The number of frames for which the timer will run. +---@field duration number +--- The current frame. +---@field frame number +--- If true, the timer starts over from the beginning when it completes. Defaults to false. +---@field repeats boolean +--- If true, the timer plays in reverse once it has completed. The number of frames to complete both the forward and reverse will be `duration x 2`. Defaults to false. +--- +--- Please note that the frame counter will restart at 0 and count up to `duration` again when the reverse timer starts, but `value` will be calculated in reverse, from `endValue` to `startValue`. The same easing function (as opposed to the inverse of the easing function) will be used for the reverse timer unless an alternate is provided by setting `reverseEasingFunction`. +---@field reverses boolean +--- A Function of the form `function(timer)` or `function(...)` where "..." corresponds to the values in the table assigned to `timerEndedArgs`. Called when the timer has completed. +---@field timerEndedCallback nil | (fun()) +--- For repeating timers, this function will be called each time the timer completes, before it starts again. +--- +--- An array-style table of values that will be passed to the `timerEndedCallback` function. +---@field timerEndedArgs any[] +--- A function to be called on every frame update. If the frame timer was created with arguments, those will be passed as arguments to the function provided. Otherwise, the timer is passed as the single argument. +---@field updateCallback nil | (fun()) + +---@class pd_ui_lib +---@field crankIndicator pd_crankIndicator +---@field gridview pd_gridview_lib + +---@class pd_item_lib : pd_item + +---@class pd_item +--- Sets the callback function for this menu item. +---@field setCallback fun(self: pd_item, callback: fun()) +--- Sets the title displayed for this menu item. +--- +--- The title for a menu item can also be set using dot syntax. +---@field setTitle fun(self: pd_item, newTitle: string) +--- Returns the title displayed for this menu item. +---@field getTitle fun(self: pd_item): string +--- Sets the value for this menu item. The value is of a different type depending on the type of menu item: +--- +--- normal: integer +--- +--- checkmark: boolean +--- +--- options: string +--- +--- Values for any menu type can also be set using integers. +--- +--- The value for a menu item can also be set using dot syntax. +---@field setValue fun(self: pd_item, newValue: integer|boolean|string) +--- Returns the value for this menu item. +---@field getValue fun(self: pd_item): integer|boolean|string + +---@class pd_file_file_lib : pd_file_file +---@field kFileRead pd_filemode +---@field kFileWrite pd_filemode +---@field kFileAppend pd_filemode +---@field kSeekSet pd_seek_mode +---@field kSeekFromCurrent pd_seek_mode +---@field kSeekFromEnd pd_seek_mode + +---@class pd_file_file +--- Closes the file. +--- +--- Equivalent to playdate->file->close() in the C API. +---@field close fun(self: pd_file_file) +--- Writes the given string to the file and returns the number of bytes written if successful, or 0 and a second return value describing the error. If you wish to include line termination characters (\n, \r), please include them in the string. +---@field write fun(self: pd_file_file, string: string): number +--- Flushes any buffered data written to the file to the disk. +--- +--- Equivalent to playdate->file->flush() in the C API. +---@field flush fun(self: pd_file_file) +--- Returns the next line of the file, delimited by either \n or \r\n. The returned string does not include newline characters. +---@field readline fun(self: pd_file_file): string +--- Returns a buffer containing up to `numberOfBytes` bytes from the file, and the number of bytes read. If the read failed, the function returns nil and a second value describing the error. +--- +--- Equivalent to playdate->file->read() in the C API. +---@field read fun(self: pd_file_file, numberOfBytes: number): pd_UNDOCUMENTED +--- Sets the file read/write position to the given byte offset. whence, if given is one of the following: +--- +--- playdate.file.kSeekSet: offset is an absolute offset from the start of the file +--- +--- playdate.file.kSeekFromCurrent: offset is relative to the current position +--- +--- playdate.file.kSeekFromEnd: offset is an offset from the end of the file (negative values are before the end, positive are past the end) +--- +--- Equivalent to playdate->file->seek() in the C API. +---@field seek fun(self: pd_file_file, offset: number, whence?: pd_seek_mode) +--- Returns the current byte offset of the read/write position in the file. +--- +--- Equivalent to playdate->file->tell() in the C API. +---@field tell fun(self: pd_file_file): number + +---@class pd_affineTransform_lib : pd_affineTransform +--- ### Overload 1 ### +--- Returns a new playdate.geometry.affineTransform. Use new() instead to get a new copy of the identity transform. +--- +--- ### Overload 2 ### +--- Returns a new playdate.geometry.affineTransform that is the identity transform. +---@field new (fun(m11: number, m12: number, m21: number, m22: number, tx: number, ty: number): pd_affineTransform) | (fun(): pd_affineTransform) + +---@class pd_affineTransform +--- Returns a new copy of the affine transform. +---@field copy fun(self: pd_affineTransform): pd_affineTransform +--- Mutates the caller so that it is an affine transformation matrix constructed by inverting itself. +--- +--- Inversion is generally used to provide reverse transformation of points within transformed objects. Given the coordinates (x, y), which have been transformed by a given matrix to new coordinates (x’, y’), transforming the coordinates (x’, y’) by the inverse matrix produces the original coordinates (x, y). +---@field invert fun(self: pd_affineTransform) +--- Mutates the the caller, changing it to an identity transform matrix. +---@field reset fun(self: pd_affineTransform) +--- Mutates the the caller. The affine transform `af` is concatenated to the caller. +--- +--- Concatenation combines two affine transformation matrices by multiplying them together. You might perform several concatenations in order to create a single affine transform that contains the cumulative effects of several transformations. +--- +--- Note that matrix operations are not commutative — the order in which you concatenate matrices is important. That is, the result of multiplying matrix t1 by matrix t2 does not necessarily equal the result of multiplying matrix t2 by matrix t1. +---@field concat fun(self: pd_affineTransform, af: pd_affineTransform) +--- Mutates the caller by applying a translate transformation. x values are moved by `dx`, y values by `dy`. +---@field translate fun(self: pd_affineTransform, dx: number, dy: number) +--- Returns a copy of the calling affine transform with a translate transformation appended. +---@field translatedBy fun(self: pd_affineTransform, dx: number, dy: number): pd_affineTransform +--- Mutates the caller by applying a scaling transformation. +--- +--- If both parameters are passed, `sx` is used to scale the x values of the transform, `sy` is used to scale the y values. +--- +--- If only one parameter is passed, it is used to scale both x and y values. +---@field scale fun(self: pd_affineTransform, sx: number, sy?: number) +--- Returns a copy of the calling affine transform with a scaling transformation appended. +--- +--- If both parameters are passed, `sx` is used to scale the x values of the transform, `sy` is used to scale the y values. +--- +--- If only one parameter is passed, it is used to scale both x and y values. +---@field scaledBy fun(self: pd_affineTransform, sx: number, sy?: number): pd_affineTransform +--- ### Overload 1 ### +--- Mutates the caller by applying a rotation transformation. +--- +--- `angle` is the value, in degrees, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation. If the optional `x` and `y` arguments are given, the transform rotates around (`x`,`y`) instead of (0,0). +--- +--- ### Overload 2 ### +--- Mutates the caller by applying a rotation transformation. +--- +--- `angle` is the value, in degrees, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation. If the optional playdate.geometry.point `point` argument is given, the transform rotates around the `point` instead of (0,0). +---@field rotate (fun(self: pd_affineTransform, angle: number, x?: number, y?: number)) | (fun(self: pd_affineTransform, angle: number, point?: pd_point)) +--- ### Overload 1 ### +--- Returns a copy of the calling affine transform with a rotate transformation appended. +--- +--- `angle` is the value, in degrees, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation. If the optional `x` and `y` arguments are given, the transform rotates around (`x`,`y`) instead of (0,0). +--- +--- ### Overload 2 ### +--- Returns a copy of the calling affine transform with a rotate transformation appended. +--- +--- `angle` is the value, in degrees, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation. If the optional point `point` argument is given, the transform rotates around the `point` instead of (0,0). +---@field rotatedBy (fun(self: pd_affineTransform, angle: number, x?: number, y?: number): pd_affineTransform) | (fun(self: pd_affineTransform, angle: number, point?: pd_point): pd_affineTransform) +--- Mutates the caller, appending a skew transformation. `sx` is the value by which to skew the x axis, and `sy` the value for the y axis. Values are in degrees. +---@field skew fun(self: pd_affineTransform, sx: number, sy: number) +--- Returns the given transform with a skew transformation appended. `sx` is the value by which to skew the x axis, and `sy` the value for the y axis. Values are in degrees. +---@field skewedBy fun(self: pd_affineTransform, sx: number, sy: number): pd_affineTransform +--- Modifies the point `p` by applying the affine transform. +---@field transformPoint fun(self: pd_affineTransform, p: pd_point) +--- As above, but returns a new point rather than modifying `p`. +---@field transformedPoint fun(self: pd_affineTransform, p: pd_point): pd_point +--- Returns two values calculated by applying the affine transform to the point (`x`, `y`) +---@field transformXY fun(self: pd_affineTransform, x: number, y: number): (number, number) +--- Modifies the line segment `ls` by applying the affine transform. +---@field transformLineSegment fun(self: pd_affineTransform, ls: pd_lineSegment) +--- As above, but returns a new line segment rather than modifying `ls`. +---@field transformedLineSegment fun(self: pd_affineTransform, ls: pd_lineSegment): pd_lineSegment +--- Modifies the axis aligned bounding box `r` (a rect) by applying the affine transform. +---@field transformAABB fun(self: pd_affineTransform, r: pd_rect) +--- As above, but returns a new rect rather than modifying `r`. +---@field transformedAABB fun(self: pd_affineTransform, r: pd_rect): pd_rect +--- Modifies the polygon `p` by applying the affine transform. +---@field transformPolygon fun(self: pd_affineTransform, p: pd_polygon) +--- As above, but returns a new polygon rather than modifying `p`. +---@field transformedPolygon fun(self: pd_affineTransform, p: pd_polygon): pd_polygon +--- Returns the transform created by multiplying transform `t1` by transform `t2` +---@operator mul(pd_affineTransform): pd_affineTransform +--- Returns the vector2D created by applying the transform `t` to the vector2D v +---@operator mul(pd_vector2D): pd_vector2D +--- Returns the point created by applying the transform `t` to the point `p` +---@operator mul(pd_point): pd_point + +---@class pd_arc_lib : pd_arc + +---@class pd_arc +--- Returns a new playdate.geometry.arc. Angles should be specified in degrees. Zero degrees represents the top of the circle. +--- +--- If specified, `direction` should be true for clockwise, false for counterclockwise. If not specified, the direction is inferred from the start and end angles. +---@field new fun(x: number, y: number, radius: number, startAngle: number, endAngle: number, direction?: boolean): pd_arc +--- Returns a new copy of the arc. +---@field copy fun(self: pd_arc): pd_arc +--- Returns the length of the arc. +---@field length fun(self: pd_arc): number +--- Returns true if the direction of the arc is clockwise. +---@field isClockwise fun(self: pd_arc): boolean +--- Sets the direction of the arc. +---@field setIsClockwise fun(self: pd_arc, flag: boolean) +--- Returns a new point on the arc, distance pixels from the arc’s start angle. If extend is true, the returned point is allowed to project past the arc’s endpoints; otherwise, it is constrained to the arc’s initial point if distance is negative, or the end point if distance is greater than the arc’s length. +---@field pointOnArc fun(self: pd_arc, distance: number, extend?: boolean): pd_arc +--- You can directly read or write the `x`, `y`, `radius`, `startAngle`, `endAngle` and `clockwise` values of an arc. +---@field x number +--- You can directly read or write the `x`, `y`, `radius`, `startAngle`, `endAngle` and `clockwise` values of an arc. +---@field y number +--- You can directly read or write the `x`, `y`, `radius`, `startAngle`, `endAngle` and `clockwise` values of an arc. +---@field radius number +--- You can directly read or write the `x`, `y`, `radius`, `startAngle`, `endAngle` and `clockwise` values of an arc. +---@field startAngle number +--- You can directly read or write the `x`, `y`, `radius`, `startAngle`, `endAngle` and `clockwise` values of an arc. +---@field endAngle number +--- You can directly read or write the `x`, `y`, `radius`, `startAngle`, `endAngle` and `clockwise` values of an arc. +---@field clockwise boolean + +---@class pd_lineSegment_lib : pd_lineSegment +--- Returns a new playdate.geometry.lineSegment. +---@field new fun(x1: number, y1: number, x2: number, y2: number): pd_lineSegment +--- For use in inner loops where speed is the priority. +--- +--- Returns true if there is an intersection between the line segments defined by `(x1, y1)`, `(x2, y2)` and `(x3, y3)`, `(x4, y4)`. If there is an intersection, `x, y` values representing the intersection point are also returned. +---@field fast_intersection fun(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): boolean + +---@class pd_lineSegment +--- Returns a new copy of the line segment. +---@field copy fun(self: pd_lineSegment): pd_lineSegment +--- Returns the values `x1, y1, x2, y2`. +---@field unpack fun(self: pd_lineSegment): (number, number, number, number) +--- Returns the length of the line segment. +---@field length fun(self: pd_lineSegment): number +--- Modifies the line segment, offsetting its values by `dx`, `dy`. +---@field offset fun(self: pd_lineSegment, dx: number, dy: number) +--- Returns a new line segment, the given segment offset by `dx`, `dy`. +---@field offsetBy fun(self: pd_lineSegment, dx: number, dy: number): pd_lineSegment +--- Returns a playdate.geometry.point representing the mid point of the line segment. +---@field midPoint fun(self: pd_lineSegment): pd_point +--- Returns a playdate.geometry.point on the line segment, distance pixels from the start of the line. If extend is true, the returned point is allowed to project past the segment’s endpoints; otherwise, it is constrained to the line segment’s initial point if distance is negative, or the end point if distance is greater than the segment’s length. +---@field pointOnLine fun(self: pd_lineSegment, distance: number, extend?: boolean): pd_point +--- Returns a playdate.geometry.vector2D representation of the line segment. +---@field segmentVector fun(self: pd_lineSegment): pd_vector2D +--- Returns a playdate.geometry.point that is the closest point to point `p` that is on the line segment. +---@field closestPointOnLineToPoint fun(self: pd_lineSegment, p: pd_point): pd_point +--- Returns true if there is an intersection between the caller and the line segment `ls`. +--- +--- If there is an intersection, a playdate.geometry.point representing that point is also returned. +---@field intersectsLineSegment fun(self: pd_lineSegment, ls: pd_lineSegment): boolean +--- Returns the values (`intersects`, `intersectionPoints`). +--- +--- `intersects` is true if there is at least one intersection between the caller and poly. +--- +--- `intersectionPoints` is an array of playdate.geometry.points containing all intersection points between the caller and poly. +---@field intersectsPolygon fun(self: pd_lineSegment, poly: pd_polygon): (boolean, pd_point[]) +--- Returns the values (`intersects`, `intersectionPoints`). +--- +--- `intersects` is true if there is at least one intersection between the caller and rect. +--- +--- `intersectionPoints` is an array of playdate.geometry.points containing all intersection points between the caller and rect. +---@field intersectsRect fun(self: pd_lineSegment, rect: pd_rect): (boolean, pd_point[]) +--- You can directly read or write `x1`, `y1`, `x2`, or `y2` values to a lineSegment. +---@field x1 number +--- You can directly read or write `x1`, `y1`, `x2`, or `y2` values to a lineSegment. +---@field y1 number +--- You can directly read or write `x1`, `y1`, `x2`, or `y2` values to a lineSegment. +---@field x2 number +--- You can directly read or write `x1`, `y1`, `x2`, or `y2` values to a lineSegment. +---@field y2 number + +---@class pd_point_lib : pd_point +--- Returns a new playdate.geometry.point. +---@field new fun(x: number, y: number): pd_point + +---@class pd_point +--- Returns a new copy of the point. +---@field copy fun(self: pd_point): pd_point +--- Returns the values `x, y`. +---@field unpack fun(self: pd_point): (number, number) +--- Modifies the point, offsetting its values by `dx`, `dy`. +---@field offset fun(self: pd_point, dx: number, dy: number) +--- Returns a new point object, the given point offset by `dx`, `dy`. +---@field offsetBy fun(self: pd_point, dx: number, dy: number): pd_point +--- Returns the square of the distance to point `p`. +---@field squaredDistanceToPoint fun(self: pd_point, p: pd_point): number +--- Returns the distance to point `p`. +---@field distanceToPoint fun(self: pd_point, p: pd_point): number +--- Returns a new point by adding the vector `v` to point `p`. +---@operator add(pd_vector2D): pd_point +--- Returns the vector constructed by subtracting `p2` from `p1`. By this construction, `p2` + (`p1` - `p2`) == `p1`. +---@operator sub(pd_point): pd_vector2D +--- Returns a new polygon formed by applying the transform `t` to polygon `p`. +---@operator mul(pd_affineTransform): pd_point +--- Returns a new lineSegment connecting points `p1` and `p2`. +---@operator concat(pd_point): pd_lineSegment +--- You can directly read or write the `x` and `y` values of a point. +---@field x number +--- You can directly read or write the `x` and `y` values of a point. +---@field y number + +---@class pd_polygon_lib : pd_polygon +--- new(x1, y1, x2, y2, ..., xn, yn) returns a new playdate.geometry.polygon with vertices `(x1, y1)` through `(xn, yn)`. The Lua function table.unpack() can be used to turn an array into function arguments. +--- +--- new(p1, p2, ..., pn) does the same, except the points are expressed via point objects. +--- +--- new(numberOfVertices) returns a new playdate.geometry.polygon with space allocated for `numberOfVertices` vertices. All vertices are initially (0, 0). Vertex coordinates can be set with playdate.geometry.polygon:setPointAt(). +--- +--- If the polygon’s first and last points are coincident, the polygon will be considered closed. Alternatively, you may call :close() to automatically close the polygon. +--- To draw a polygon, use playdate.graphics.drawPolygon(). +---@field new (fun(x1: number, y1: number, x2: number, y2: number, x3?: number, y3?: number, ...: number): pd_polygon) | (fun(p1: pd_point, p2: pd_point, p3?: pd_point, p4?: pd_point, p5?: pd_point, p6?: pd_point, ...: pd_point): pd_polygon) | (fun(numberOfVertices: number): pd_polygon) + +---@class pd_polygon +--- Returns a copy of a polygon. +---@field copy fun(self: pd_polygon): pd_polygon +--- :close() closes a polygon. If the polygon’s first and last point aren’t coincident, a line segment will be generated to connect them. +---@field close fun(self: pd_polygon) +--- Returns true if the polygon is closed, false if not. +---@field isClosed fun(self: pd_polygon): boolean +--- Returns a boolean value, true if the point `p` or the point at `(x, y)` is contained within the caller polygon. +--- +--- fillrule is an optional argument that can be one of the values defined in playdate.graphics.setPolygonFillRule. By default `playdate.graphics.kPolygonFillEvenOdd` is used. +---@field containsPoint (fun(self: pd_polygon, p: pd_point, fillRule?: pd_polygon_fill_rule): boolean) | (fun(self: pd_polygon, x: number, y: number, fillRule?: pd_polygon_fill_rule): boolean) +--- Returns multiple values (`x`, `y`, `width`, `height`) giving the axis-aligned bounding box for the polygon. +---@field getBounds fun(self: pd_polygon): (number, number, number, number) +--- Returns the axis-aligned bounding box for the given polygon as a playdate.geometry.rect object. +---@field getBoundsRect fun(self: pd_polygon): pd_rect +--- Returns the number of points in the polygon. +---@field count fun(self: pd_polygon): number +--- Returns the total length of all line segments in the polygon. +---@field length fun(self: pd_polygon): number +--- Sets the polygon’s `n`-th point to (`x`, `y`). +---@field setPointAt fun(self: pd_polygon, n: number, x: number, y: number) +--- Returns the polygon’s `n`-th point. +---@field getPointAt fun(self: pd_polygon, n: number): pd_point +--- Returns true if the given polygon intersects the polygon `p`. +---@field intersects fun(self: pd_polygon, p: pd_polygon): boolean +--- Returns a playdate.geometry.point on one of the polygon’s line segments, distance pixels from the start of the polygon. If extend is true, the point is allowed to project past the polygon’s ends; otherwise, it is constrained to the polygon’s initial point if distance is negative, or the last point if distance is greater than the polygon’s length. +---@field pointOnPolygon fun(self: pd_polygon, distance: number, extend?: boolean): pd_point +--- Translates each point on the polygon by `dx`, `dy` pixels. +---@field translate fun(self: pd_polygon, dx: number, dy: number) + +---@class pd_rect_lib : pd_rect +--- Returns a new playdate.geometry.rect. +---@field new fun(x: number, y: number, width: number, height: number): pd_rect +--- For use in inner loops where speed is the priority. About 3x faster than intersection. +--- +--- Returns multiple values (`x, y, width, height`) representing the overlapping portion of the two rects defined by `x1, y1, w1, h1` and `x2, y2, w2, h2`. If there is no intersection, (0, 0, 0, 0) is returned. +---@field fast_intersection fun(x1: number, y1: number, w1: number, h1: number, x2: number, y2: number, w2: number, h2: number): (number, number, number, number) +--- For use in inner loops where speed is the priority. About 3x faster than union. +--- +--- Returns multiple values (`x, y, width, height`) representing the smallest possible rect that contains the two rects defined by `x1, y1, w1, h1` and `x2, y2, w2, h2`. +---@field fast_union fun(x1: number, y1: number, w1: number, h1: number, x2: number, y2: number, w2: number, h2: number): (number, number, number, number) + +---@class pd_rect +--- Returns a new copy of the rect. +---@field copy fun(self: pd_rect): pd_rect +--- Returns a new playdate.geometry.polygon version of the rect. +---@field toPolygon fun(self: pd_rect): pd_rect +--- Returns `x`, `y`, `width` and `height` as individual values. +---@field unpack fun(self: pd_rect): (number, number, number, number) +--- Returns true if a rectangle has zero width or height. +---@field isEmpty fun(self: pd_rect): boolean +--- Returns true if the `x`, `y`, `width`, and `height` values of the caller and `r2` are all equal. +---@field isEqual fun(self: pd_rect, r2: pd_rect): boolean +--- Returns true if `r2` intersects the caller. +---@field intersects fun(self: pd_rect, r2: pd_rect): boolean +--- Returns a rect representing the overlapping portion of the caller and `r2`. +---@field intersection fun(self: pd_rect, r2: pd_rect): pd_rect +--- Returns the smallest possible rect that contains both the source rect and `r2`. +---@field union fun(self: pd_rect, r2: pd_rect): pd_rect +--- Insets the rect by the given `dx` and `dy`. +---@field inset fun(self: pd_rect, dx: number, dy: number) +--- Returns a rect that is inset by the given `dx` and `dy`, with the same center point. +---@field insetBy fun(self: pd_rect, dx: number, dy: number): pd_rect +--- Offsets the rect by the given `dx` and `dy`. +---@field offset fun(self: pd_rect, dx: number, dy: number) +--- Returns a rect with its origin point offset by `dx`, `dy`. +---@field offsetBy fun(self: pd_rect, dx: number, dy: number): pd_rect +--- ### Overload 1 ### +--- Returns true if the rect `r2` is contained within the caller rect. +--- +--- ### Overload 2 ### +--- Returns true if the rect defined by `(x, y, width, height)` is contained within the caller rect. +---@field containsRect (fun(self: pd_rect, r2: pd_rect): boolean) | (fun(self: pd_rect, x: number, y: number, width: number, height: number): boolean) +--- ### Overload 1 ### +--- Returns true if the point `p` is contained within the caller rect. +--- +--- ### Overload 2 ### +--- Returns true if the point at `(x, y)` is contained within the caller rect. +---@field containsPoint (fun(self: pd_rect, p: pd_point): boolean) | (fun(self: pd_rect, x: number, y: number): boolean) +--- Returns a point at the center of the caller. +---@field centerPoint fun(self: pd_rect): pd_point +--- Flips the caller about the center of rect `r2`. +--- +--- `flip` should be one of the following constants: +--- +--- `playdate.geometry.kUnflipped` +--- +--- `playdate.geometry.kFlippedX` +--- +--- `playdate.geometry.kFlippedY` +--- +--- `playdate.geometry.kFlippedXY` +---@field flipRelativeToRect fun(self: pd_rect, r2: pd_rect, flip: pd_flip) +--- You can directly read or write `x`, `y`, `width`, or `height` values to a rect. +---@field x number +--- You can directly read or write `x`, `y`, `width`, or `height` values to a rect. +---@field y number +--- You can directly read or write `x`, `y`, `width`, or `height` values to a rect. +---@field width number +--- You can directly read or write `x`, `y`, `width`, or `height` values to a rect. +---@field height number +--- **READ-ONLY**. While you can directly read or write `x`, `y`, `width`, or `height` values to a rect, the values of `top`, `bottom`, `right`, `left`, `origin`, and `size` are read-only. +---@field top READONLY_number +--- **READ-ONLY**. While you can directly read or write `x`, `y`, `width`, or `height` values to a rect, the values of `top`, `bottom`, `right`, `left`, `origin`, and `size` are read-only. +---@field bottom READONLY_number +--- **READ-ONLY**. While you can directly read or write `x`, `y`, `width`, or `height` values to a rect, the values of `top`, `bottom`, `right`, `left`, `origin`, and `size` are read-only. +---@field right READONLY_number +--- **READ-ONLY**. While you can directly read or write `x`, `y`, `width`, or `height` values to a rect, the values of `top`, `bottom`, `right`, `left`, `origin`, and `size` are read-only. +---@field left READONLY_number +--- **READ-ONLY**. While you can directly read or write `x`, `y`, `width`, or `height` values to a rect, the values of `top`, `bottom`, `right`, `left`, `origin`, and `size` are read-only. +---@field origin READONLY_pd_point +--- **READ-ONLY**. While you can directly read or write `x`, `y`, `width`, or `height` values to a rect, the values of `top`, `bottom`, `right`, `left`, `origin`, and `size` are read-only. +---@field size READONLY_pd_size + +---@class pd_size_lib : pd_size +--- Returns a new playdate.geometry.size. +---@field new fun(width: number, height: number): pd_size + +---@class pd_size +--- Returns a new copy of the size. +---@field copy fun(self: pd_size): pd_size +--- Returns the values `width, height`. +---@field unpack fun(self: pd_size): (number, number) +--- You can directly read or write the `width` and `height` values of a `size`. +---@field width number +--- You can directly read or write the `width` and `height` values of a `size`. +---@field height number + +---@class pd_vector2D_lib : pd_vector2D +--- Returns a new playdate.geometry.vector2D. +---@field new fun(x: number, y: number): pd_vector2D +--- Returns a new playdate.geometry.vector2D. Angles should be specified in degrees. Zero degrees represents the top of the circle. +---@field newPolar fun(length: number, angle: number): pd_vector2D + +---@class pd_vector2D +--- Returns a new copy of the vector2D. +---@field copy fun(self: pd_vector2D): pd_vector2D +--- Returns the values `dx, dy`. +---@field unpack fun(self: pd_vector2D): (number, number) +--- Modifies the caller by adding vector `v`. +---@field addVector fun(self: pd_vector2D, v: pd_vector2D) +--- Modifies the caller, scaling it by amount `s`. +---@field scale fun(self: pd_vector2D, s: number) +--- Returns the given vector scaled by `s`. +---@field scaledBy fun(self: pd_vector2D, s: number): pd_vector2D +--- Modifies the caller by normalizing it so that its length is 1. If the vector is (0,0), the vector is unchanged. +---@field normalize fun(self: pd_vector2D) +--- Returns a new vector by normalizing the given vector. +---@field normalized fun(self: pd_vector2D): pd_vector2D +--- Returns the dot product of the caller and the vector `v`. +---@field dotProduct fun(self: pd_vector2D, v: pd_vector2D): number +--- Returns the magnitude of the caller. +---@field magnitude fun(self: pd_vector2D): number +--- Returns the square of the magnitude of the caller. +---@field magnitudeSquared fun(self: pd_vector2D): number +--- Modifies the caller by projecting it along the vector `v`. +---@field projectAlong fun(self: pd_vector2D, v: pd_vector2D) +--- Returns a new vector created by projecting the given vector along the vector `v`. +---@field projectedAlong fun(self: pd_vector2D, v: pd_vector2D): pd_vector2D +--- Returns the angle between the caller and the vector `v`. +---@field angleBetween fun(self: pd_vector2D, v: pd_vector2D): number +--- Returns a vector that is the left normal of the caller. +---@field leftNormal fun(self: pd_vector2D): pd_vector2D +--- Returns a vector that is the right normal of the caller. +---@field rightNormal fun(self: pd_vector2D): pd_vector2D +--- Returns the vector formed by negating the components of vector `v`. +---@operator unm: pd_vector2D +--- Returns the vector formed by adding vector `v2` to vector `v1`. +---@operator add(pd_vector2D): pd_vector2D +--- Returns the vector formed by subtracting vector `v2` from vector `v1`. +---@operator sub(pd_vector2D): pd_vector2D +--- Returns the vector `v1` scaled by `s`. +---@operator mul(number): pd_vector2D +--- Returns the dot product of the two vectors. +---@operator mul(pd_vector2D): number +--- Returns the vector transformed by transform `t`. +---@operator mul(pd_affineTransform): pd_vector2D +--- Returns the vector divided by scalar `s`. +---@operator div(number): pd_vector2D +--- You can directly read or write `dx`, or `dy` values to a vector2D. +---@field dx number +--- You can directly read or write `dx`, or `dy` values to a vector2D. +---@field dy number + +---@class pd_image_lib : pd_image +--- ### Overload 1 ### +--- Creates a new blank image of the given width and height. The image can be drawn on using playdate.graphics.pushContext() or playdate.graphics.lockFocus(). The optional `bgcolor` argument is one of the color constants as used in playdate.graphics.setColor(), defaulting to `kColorClear`. +--- +--- ### Overload 2 ### +--- Returns a playdate.graphics.image object from the data at `path`. If there is no file at `path`, the function returns nil and a second value describing the error. +---@field new (fun(width: number, height: number, bgcolor?: pd_color): pd_image) | (fun(path: string): pd_image) +---@field kDitherTypeNone pd_dither_type +---@field kDitherTypeDiagonalLine pd_dither_type +---@field kDitherTypeVerticalLine pd_dither_type +---@field kDitherTypeHorizontalLine pd_dither_type +---@field kDitherTypeScreen pd_dither_type +---@field kDitherTypeBayer2x2 pd_dither_type +---@field kDitherTypeBayer4x4 pd_dither_type +---@field kDitherTypeBayer8x8 pd_dither_type +---@field kDitherTypeFloydSteinberg pd_dither_type +---@field kDitherTypeBurkes pd_dither_type +---@field kDitherTypeAtkinson pd_dither_type + +---@class pd_image +--- Loads a new image from the data at `path` into an already-existing image, without allocating additional memory. The image at `path` must be of the same dimensions as the original. +--- +--- Returns `(success, [error])`. If the boolean `success` is false, `error` is also returned. +---@field load fun(self: pd_image, path: string): (boolean, string?) +--- Returns a new playdate.graphics.image that is an exact copy of the original. +---@field copy fun(self: pd_image): pd_image +--- Returns the pair (`width`, `height`) +---@field getSize fun(self: pd_image): (number, number) +--- Draws the image with its upper-left corner at location (`x`, `y`) or playdate.geometry.point `p`. +--- +--- The optional `flip` argument can be one of the following: +--- +--- `playdate.graphics.kImageUnflipped`: the image is drawn normally +--- +--- `playdate.graphics.kImageFlippedX`: the image is flipped left to right +--- +--- `playdate.graphics.kImageFlippedY`: the image is flipped top to bottom +--- +--- `playdate.graphics.kImageFlippedXY`: the image if flipped both ways; i.e., rotated 180 degrees +--- +--- Alternately, one of the strings "flipX", "flipY", or "flipXY" can be used for the `flip` argument. +--- +--- `sourceRect`, if specified, will cause only the part of the image within sourceRect to be drawn. `sourceRect` should be relative to the image’s bounds and can be a playdate.geometry.rect or four integers, (`x`, `y`, `w`, `h`), representing the rect. +---@field draw (fun(self: pd_image, x: number, y: number, flip?: pd_image_flip, sourceRect?: pd_rect)) | (fun(self: pd_image, p: pd_point, flip?: pd_image_flip, sourceRect?: pd_rect)) | (fun(self: pd_image, x: number, y: number, flip?: pd_image_flip, rx?: number, ry?: number, rw?: number, rh?: number)) | (fun(self: pd_image, p: pd_point, flip?: pd_image_flip, rx?: number, ry?: number, rw?: number, rh?: number)) +--- Draws the image at location `(x, y)` centered at the point within the image represented by `(ax, ay)` in unit coordinate space. For example, values of `ax = 0.0`, `ay = 0.0` represent the image’s top-left corner, `ax = 1.0`, `ay = 1.0` represent the bottom-right, and `ax = 0.5`, `ay = 0.5` represent the center of the image. +--- +--- The `flip` argument is optional; see playdate.graphics.image:draw() for valid values. +--- +--- You must import `CoreLibs/graphics` to use this method. +---@field drawAnchored fun(self: pd_image, x: number, y: number, ax: number, ay: number, flip?: pd_image_flip) +--- Draws the image centered at location `(x, y)`. +--- +--- The `flip` argument is optional; see playdate.graphics.image:draw() for valid values. +--- +--- You must import `CoreLibs/graphics` to use this method. +---@field drawCentered fun(self: pd_image, x: number, y: number, flip?: pd_image_flip) +--- Draws the image ignoring the currently-set drawOffset. +---@field drawIgnoringOffset (fun(self: pd_image, x: number, y: number, flip?: pd_image_flip)) | (fun(self: pd_image, p: pd_point, flip?: pd_image_flip)) +--- Erases the contents of the image, setting all pixels to white if `color` is `playdate.graphics.kColorWhite`, black if it’s `playdate.graphics.kColorBlack`, or clear if it’s `playdate.graphics.kColorClear`. If the image is cleared to black or white, the mask (if it exists) is set to fully opaque. If the image is cleared to kColorClear and the image doesn’t have a mask, a mask is added to it. +---@field clear fun(self: pd_image, color: pd_color) +--- Returns `playdate.graphics.kColorWhite` if the image is white at (`x`, `y`), `playdate.graphics.kColorBlack` if it’s black, or `playdate.graphics.kColorClear` if it’s transparent. +--- +--- The upper-left pixel of the image is at coordinate `(0, 0)`. +---@field sample fun(self: pd_image, x: number, y: number): pd_color +--- Draws this image centered at point `(x,y)` at (clockwise) `angle` degrees, scaled by optional argument `scale`, with an optional separate scaling for the y axis. +---@field drawRotated fun(self: pd_image, x: number, y: number, angle: number, scale?: number, yscale?: number) +--- Returns a new image containing this image rotated by (clockwise) `angle` degrees, scaled by optional argument `scale`, with an optional separate scaling for the y axis. +--- +--- Unless rotating by a multiple of 180 degrees, the new image will have different dimensions than the original. +---@field rotatedImage fun(self: pd_image, angle: number, scale?: number, yscale?: number): pd_image +--- Draws this image with its upper-left corner at point `(x,y)`, scaled by amount `scale`, with an optional separate scaling for the y axis. +---@field drawScaled fun(self: pd_image, x: number, y: number, scale: number, yscale?: number) +--- Returns a new image containing this image scaled by amount `scale`, with an optional separate scaling for the y axis. +---@field scaledImage fun(self: pd_image, scale: number, yscale?: number): pd_image +--- Draws this image centered at point `(x,y)` with the transform `xform` applied. +---@field drawWithTransform fun(self: pd_image, xform: pd_affineTransform, x: number, y: number) +--- Returns a new image containing the image with the transform `xform` applied. +---@field transformedImage fun(self: pd_image, xform: pd_affineTransform): pd_image +--- Draws the image as if it’s mapped onto a tilted plane, transforming the target coordinates to image coordinates using an affine transform: +--- +--- x' = dxx * x + dyx * y + dx +--- y' = dxy * x + dyy * y + dy +--- +--- `x, y, width, height`: The rectangle to fill +--- +--- `centerx, centery`: The point in the above rectangle [in (0,1)x(0,1) coordinates] for the center of the transform +--- +--- `dxx, dyx, dxy, dyy, dx, dy`: Defines an affine transform from geometry coordinates to image coordinates +--- +--- `z`: The distance from the viewer to the target plane — lower z means more exaggerated perspective +--- +--- `tiltAngle`: The tilt of the target plane about the x axis, in degrees +--- +--- `tile`: A boolean, indicating whether the image is tiled on the target plane +--- +--- The `Mode7Driver` demo in the `/Examples` folder of the SDK demonstrates the usage of this function. +---@field drawSampled fun(self: pd_image, x: number, y: number, width: number, height: number, centerx: number, centery: number, dxx: number, dyx: number, dxy: number, dyy: number, dx: number, dy: number, z: number, tiltAngle: number, tile: boolean) +--- Sets the image’s mask to a copy of `maskImage`. +---@field setMaskImage fun(self: pd_image, maskImage: pd_image) +--- If the image has a mask, returns the mask as a separate image. Otherwise, returns nil. +--- +--- The returned image references the original’s data, so drawing into this image alters the original image’s mask. +---@field getMaskImage fun(self: pd_image): pd_image +--- Adds a mask to the image if it doesn’t already have one. If `opaque` is true or not specified, the image mask applied will be completely white, so the image will be entirely opaque. If `opaque` is false, the mask will be completely black, so the image will be entirely transparent. +---@field addMask fun(self: pd_image, opaque?: boolean) +--- Removes the mask from the image if it has one. +---@field removeMask fun(self: pd_image) +--- Returns `true` if the image has a mask. +---@field hasMask fun(self: pd_image): boolean +--- Erases the contents of the image’s mask, so that the image is entirely opaque if `opaque` is 1, transparent otherwise. This function has no effect if the image doesn’t have a mask. +---@field clearMask fun(self: pd_image, opaque?: boolean) +--- Tiles the image into the given rectangle, using either listed dimensions or a playdate.geometry.rect object, and the optional flip style. +---@field drawTiled (fun(self: pd_image, x: number, y: number, width: number, height: number, flip?: pd_image_flip)) | (fun(self: pd_image, rect: pd_rect, flip?: pd_image_flip)) +--- Draws a blurred version of the image at (`x`, `y`). +--- +--- `radius`: A bigger radius means a more blurred result. Processing time is independent of the radius. +--- +--- `numPasses`: A box blur is used to blur the image. The more passes, the more closely the blur approximates a gaussian blur. However, higher values will take more time to process. +--- +--- `ditherType`: The algorithm to use when blurring the image, must be one of the values listed in playdate.graphics.image:blurredImage() +--- +--- `flip`: optional; see playdate.graphics.image:draw() for valid values. +--- +--- `xPhase`, `yPhase`: optional; integer values that affect the appearance of `playdate.graphics.image.kDitherTypeDiagonalLine`, `playdate.graphics.image.kDitherTypeVerticalLine`, `playdate.graphics.image.kDitherTypeHorizontalLine`, `playdate.graphics.image.kDitherTypeScreen`, `playdate.graphics.image.kDitherTypeBayer2x2`, `playdate.graphics.image.kDitherTypeBayer4x4`, and `playdate.graphics.image.kDitherTypeBayer8x8`. +---@field drawBlurred fun(self: pd_image, x: number, y: number, radius: number, numPasses: number, ditherType: pd_dither_type, flip?: pd_image_flip, xPhase?: number, yPhase?: number) +--- Returns a blurred copy of the caller. +--- +--- `radius`: A bigger radius means a more blurred result. Processing time is independent of the radius. +--- +--- `numPasses`: A box blur is used to blur the image. The more passes, the more closely the blur approximates a gaussian blur. However, higher values will take more time to process. +--- +--- `ditherType`: The original image is blurred into a greyscale image then dithered back to 1-bit using one of the following dithering algorithms: +--- +--- `playdate.graphics.image.kDitherTypeNone` +--- +--- `playdate.graphics.image.kDitherTypeDiagonalLine` +--- +--- `playdate.graphics.image.kDitherTypeVerticalLine` +--- +--- `playdate.graphics.image.kDitherTypeHorizontalLine` +--- +--- `playdate.graphics.image.kDitherTypeScreen` +--- +--- `playdate.graphics.image.kDitherTypeBayer2x2` +--- +--- `playdate.graphics.image.kDitherTypeBayer4x4` +--- +--- `playdate.graphics.image.kDitherTypeBayer8x8` +--- +--- `playdate.graphics.image.kDitherTypeFloydSteinberg` +--- +--- `playdate.graphics.image.kDitherTypeBurkes` +--- +--- `playdate.graphics.image.kDitherTypeAtkinson` +--- +--- `padEdges`: Boolean indicating whether the edges of the images should be padded to accommodate the blur radius. Defaults to false. +--- +--- `xPhase`, `yPhase`: optional; integer values that affect the appearance of `playdate.graphics.image.kDitherTypeDiagonalLine`, `playdate.graphics.image.kDitherTypeVerticalLine`, `playdate.graphics.image.kDitherTypeHorizontalLine`, `playdate.graphics.image.kDitherTypeScreen`, `playdate.graphics.image.kDitherTypeBayer2x2`, `playdate.graphics.image.kDitherTypeBayer4x4`, and `playdate.graphics.image.kDitherTypeBayer8x8`. +---@field blurredImage fun(self: pd_image, radius: number, numPasses: number, ditherType: pd_dither_type, padEdges?: boolean, xPhase?: number, yPhase?: number): pd_image +--- Draws a partially transparent image with its upper-left corner at location (`x`, `y`) +--- +--- `alpha`: The alpha value used to draw the image, with 1 being fully opaque, and 0 being completely transparent. +--- +--- `ditherType`: The caller is faded using one of the dithering algorithms listed in playdate.graphics.image:blurredImage() +---@field drawFaded fun(self: pd_image, x: number, y: number, alpha: number, ditherType: pd_dither_type) +--- Returns a faded version of the caller. +--- +--- `alpha`: The alpha value assigned to the caller, in the range 0.0 - 1.0. If an image mask already exists it is multiplied by `alpha`. +--- +--- `ditherType`: The caller is faded into a greyscale image and dithered with one of the dithering algorithms listed in playdate.graphics.image:blurredImage() +---@field fadedImage fun(self: pd_image, alpha: number, ditherType: pd_dither_type): pd_image +--- If `flag` is true, the image will be drawn with its colors inverted. If the image is being used as a stencil, its behavior is reversed: pixels are drawn where the stencil is black, nothing is drawn where the stencil is white. +---@field setInverted fun(self: pd_image, flag: boolean) +--- Returns a color-inverted copy of the caller. +---@field invertedImage fun(self: pd_image): pd_image +--- Returns an image that is a blend between the caller and `image`. +--- +--- `image`: the playdate.graphics.image to be blended with the caller. +--- +--- `alpha`: The alpha value assigned to the caller. `image` will have an alpha of (1 - `alpha`). +--- +--- `ditherType`: The caller and `image` are blended into a greyscale image and dithered with one of the dithering algorithms listed in playdate.graphics.image:blurredImage() +---@field blendWithImage fun(self: pd_image, image: pd_image, alpha: number, ditherType: pd_dither_type): pd_image +--- Returns an image created by applying a VCR pause effect to the calling image. +---@field vcrPauseFilterImage fun(self: pd_image): pd_image + +---@class pd_nineSlice_lib : pd_nineSlice +--- Returns a new 9 slice image from the image at imagePath with the stretchable region defined by other parameters. The arguments represent the origin and dimensions of the innermost ("center") slice. +---@field new fun(imagePath: string, innerX: number, innerY: number, innerWidth: number, innerHeight: number): pd_nineSlice + +---@class pd_nineSlice +--- Returns the size of the 9 slice image as a pair `(width, height)`. +---@field getSize fun(self: pd_nineSlice): (number, number) +--- Returns the minimum size of the 9 slice image as a pair `(width, height)`. +---@field getMinSize fun(self: pd_nineSlice): (number, number) +--- Draws the 9 slice image at the desired coordinates by stretching the defined region to achieve the width and height inputs. +---@field drawInRect (fun(self: pd_nineSlice, x: number, y: number, width: number, height: number)) | (fun(self: pd_nineSlice, rect: pd_rect)) + +---@class pd_animation_lib +---@field loop pd_loop_lib +---@field blinker pd_blinker_lib + +---@class pd_animator_lib : pd_animator +--- ### Overload 1 ### +--- Animates between two number or playdate.geometry.point values. +--- +--- `duration` is the total time of the animation in milliseconds. +--- +--- `startValue` and `endValue` should be either numbers or playdate.geometry.point +--- +--- `easingFunction`, if supplied, should be a value from playdate.easingFunctions. If your easing function requires additional variables `s`, `a`, or `p`, set them on the animator directly after creation. For example: +--- +--- local a = playdate.graphics.animator.new(1000, 0, 100, playdate.easingFunctions.inBack) +--- a.s = 1.9 +--- +--- `startTimeOffset`, if supplied, will shift the start time of the animation by the specified number of milliseconds. (If positive, the animation will be delayed. If negative, the animation will effectively have started before the moment the animator is instantiated.) +--- +--- Example: Using an animator to animate movement +--- -- You can copy and paste this example directly as your main.lua file to see it in action +--- import "CoreLibs/graphics" +--- import "CoreLibs/animator" +--- +--- -- We'll be demonstrating how to use an animator to animate a square moving across the screen +--- local square = playdate.graphics.image.new(20, 20, playdate.graphics.kColorBlack) +--- +--- -- 1000ms, or 1 second +--- local animationDuration = 1000 +--- -- We're animating from the left to the right of the screen +--- local startX, endX = -20, 400 +--- -- Setting an easing function to get a nice, smooth movement +--- local easingFunction = playdate.easingFunctions.inOutCubic +--- local animator = playdate.graphics.animator.new(animationDuration, startX, endX, easingFunction) +--- animator.repeatCount = -1 -- Make animator repeat forever +--- +--- function playdate.update() +--- -- Clear the screen +--- playdate.graphics.clear() +--- +--- -- By using :currentValue() as the x value, the square follows along with the animation +--- square:draw(animator:currentValue(), 120) +--- end +--- +--- ### Overload 2 ### +--- Creates a new Animator that will animate along the provided playdate.geometry.lineSegment +--- +--- Example: Using an animator to animate along a line +--- -- You can copy and paste this example directly as your main.lua file to see it in action +--- import "CoreLibs/graphics" +--- import "CoreLibs/animator" +--- +--- -- We'll be demonstrating how to use an animator to animate a square moving across the screen +--- local square = playdate.graphics.image.new(20, 20, playdate.graphics.kColorBlack) +--- +--- -- 1000ms, or 1 second +--- local animationDuration = 1000 +--- -- We're animating from the top left to the bottom right of the screen +--- local line = playdate.geometry.lineSegment.new(0, 0, 400, 240) +--- local animator = playdate.graphics.animator.new(animationDuration, line) +--- +--- function playdate.update() +--- -- Clear the screen +--- playdate.graphics.clear() +--- +--- -- We can use :currentValue() directly, as it returns a point +--- square:draw(animator:currentValue()) +--- end +--- +--- ### Overload 3 ### +--- Creates a new Animator that will animate along the provided playdate.geometry.arc +--- +--- ### Overload 4 ### +--- Creates a new Animator that will animate along the provided playdate.geometry.polygon +--- +--- ### Overload 5 ### +--- Creates a new Animator that will animate along each of the items in the `parts` array in order, which should be comprised of playdate.geometry.lineSegment, playdate.geometry.arc, or playdate.geometry.polygon objects. +--- +--- `durations` should be an array of durations, one for each item in `parts`. +--- +--- `easingFunctions` should be an array of playdate.easingFunctions, one for each item in `parts`. +--- +--- By default, animators do not repeat. If you would like them to, set the animator’s `repeatCount` property to the number of times the animation should repeat. It can be set to any positive number or -1 to indicate the animation should repeat forever. Note that a repeat count of 1 means the animation will play twice - once for the initial animation plus one repeat. +--- Example: Using an animator with parts +--- -- You can copy and paste this example directly as your main.lua file to see it in action +--- import "CoreLibs/graphics" +--- import "CoreLibs/animator" +--- +--- -- We'll be demonstrating how to animate something with parts +--- local square = playdate.graphics.image.new(20, 20, playdate.graphics.kColorBlack) +--- +--- -- First part will take 3 seconds, second part will take 1, and third part will take 2 +--- local animationDurations = {3000, 1000, 2000} +--- -- We'll first animate along a line, then an arc, and then a polygon +--- local animationParts = { +--- playdate.geometry.lineSegment.new(0, 0, 200, 80), +--- playdate.geometry.arc.new(200, 120, 40, 0, 180), +--- playdate.geometry.polygon.new(200, 160, 300, 90, 390, 230) +--- } +--- -- We must set the easing functions for each part, and they can all be different +--- local animationEasingFunctions = { +--- playdate.easingFunctions.outQuart, +--- playdate.easingFunctions.inOutCubic, +--- playdate.easingFunctions.outBounce +--- } +--- +--- -- To animate by parts, each argument must be arrays of equal length +--- local animator = playdate.graphics.animator.new(animationDurations, animationParts, animationEasingFunctions) +--- +--- function playdate.update() +--- -- Clear the screen +--- playdate.graphics.clear() +--- +--- -- We can use :currentValue() directly, as it returns a point +--- square:draw(animator:currentValue()) +--- end +---@field new (fun(duration: number, startValue: number, endValue: number, easingFunction?: (fun(number, number, number, number): number), startTimeOffset?: number): pd_animator) | (fun(duration: number, lineSegment: pd_lineSegment, easingFunction?: (fun(number, number, number, number): number), startTimeOffset?: number): pd_animator) | (fun(duration: number, arc: pd_arc, easingFunction?: (fun(number, number, number, number): number), startTimeOffset?: number): pd_animator) | (fun(duration: number, polygon: pd_polygon, easingFunction?: (fun(number, number, number, number): number), startTimeOffset?: number): pd_animator) | (fun(durations: number[], parts: (pd_lineSegment|pd_arc|pd_polygon)[], easingFunctions: (fun(number, number, number, number): number), startTimeOffset?: number): pd_animator) + +---@class pd_animator +--- Returns the current value of the animation, which will be either a number or a playdate.geometry.point, depending on the type of animator. +---@field currentValue fun(self: pd_animator): number|pd_point +--- Returns the value of the animation at the given number of milliseconds after the start time. The value will be either a number or a playdate.geometry.point, depending on the type of animator. +---@field valueAtTime fun(self: pd_animator, time: number): number|pd_point +--- Returns the current progress of the animation as a value from 0 to 1. +---@field progress fun(self: pd_animator): number +--- Resets the animation, setting its start time to the current time, and changes the animation’s duration if a new duration is given. +---@field reset fun(self: pd_animator, duration?: number) +--- Returns true if the animation is completed. Only returns true if this function or currentValue() has been called since the animation ended in order to allow animations to fully finish before true is returned. +---@field ended fun(self: pd_animator): boolean +--- For easing functions that take additional amplitude (such as `inOutElastic`), set these values on animator instances to the desired values. +---@field easingAmplitude number +--- For easing functions that take additional period arguments (such as `inOutElastic`), set these values on animator instances to the desired values. +---@field easingPeriod number +--- Indicates the number of times after the initial animation the animator should repeat; i.e., if repeatCount is set to 2, the animation will play through 3 times. +---@field repeatCount number +--- If set to true, after the animation reaches the end, it runs in reverse from the end to the start. The time to complete both the forward and reverse will be `duration` x 2. Defaults to false. +---@field reverses boolean + +---@class pd_imagetable_lib : pd_imagetable +--- ### Overload 1 ### +--- Returns a playdate.graphics.imagetable object from the data at `path`. If there is no file at `path`, the function returns nil and a second value describing the error. If the file at `path` is an animated GIF, successive frames of the GIF will be loaded as consecutive bitmaps in the imagetable. Any timing data in the animated GIF will be ignored. +--- +--- To load a matrix image table defined in frames-table-16-16.png, you call playdate.graphics.imagetable.new("frames"). +--- To load a sequential image table defined with the files frames-table-1.png, frames-table-2.png, etc., you call playdate.graphics.imagetable.new("frames"). +--- +--- ### Overload 2 ### +--- Returns an empty image table for loading images into via imagetable:load() or setting already-loaded images into with imagetable:setImage(). If set, `cellsWide` is used to locate images by x,y position. The optional `cellSize` argument gives the allocation size for the images, if load() will be used. (This is a weird technical detail, so ask us if you need guidance here.) +---@field new (fun(path: string): pd_imagetable) | (fun(count: number, cellsWide?: number, cellSize?: number): pd_imagetable) + +---@class pd_imagetable : {[integer]: pd_image} +--- ### Overload 1 ### +--- Returns the `n`-th playdate.graphics.image in the table (ordering left-to-right, top-to-bottom). The first image is at index 1. If .n_ or (`x`,`y`) is out of bounds, the function returns nil. See also imagetable[n]. +--- +--- ### Overload 2 ### +--- Returns the image in cell (`x`,`y`) in the original bitmap. The first image is at index 1. If `n` or (`x`,`y`) is out of bounds, the function returns nil. See also imagetable[n]. +---@field getImage (fun(self: pd_imagetable, n: integer): pd_image) | (fun(self: pd_imagetable, x: number, y: number): pd_image) +--- Sets the image at slot `n` in the image table by creating a reference to the data in `image`. +---@field setImage fun(self: pd_imagetable, n: integer, image: pd_image) +--- Loads a new image table from the data at `path` into an already-existing image table, without allocating additional memory. The image table at `path` must contain images of the same dimensions as the previous. +--- +--- Returns (success, [error]). If the boolean success is false, error is also returned. +---@field load fun(self: pd_imagetable, path: string): (boolean, string?) +--- Returns the number of images in the table. See also #imagetable. +---@field getLength fun(self: pd_imagetable): number +--- Returns the pair (`cellsWide`, `cellsHigh`). +---@field getSize fun(self: pd_imagetable): (number, number) +--- Equivalent to graphics.imagetable:getImage(n):draw(x,y,[flip]). +---@field drawImage fun(self: pd_imagetable, n: integer, x: number, y: number, flip?: pd_image_flip) + +---@class pd_tilemap_lib : pd_tilemap +--- Creates a new tilemap object. +---@field new fun(): pd_tilemap + +---@class pd_tilemap +--- Sets the tilemap’s playdate.graphics.imagetable to `table`, a playdate.graphics.imagetable. +---@field setImageTable fun(self: pd_tilemap, table: pd_imagetable) +--- Sets the tilemap’s width to `width`, then populates the tilemap with `data`, which should be a flat, one-dimensional array-like table containing index values to the tilemap’s imagetable. +---@field setTiles fun(self: pd_tilemap, data: integer[], width: number) +--- Returns `data`, `width` +--- `data` is a flat, one-dimensional array-like table containing index values to the tilemap’s imagetable. +--- `width` is the width of the tile map, in number of tiles. +---@field getTiles fun(self: pd_tilemap): (integer[], integer) +--- Draws the tile map at screen coordinate (`x`, `y`). +--- +--- `sourceRect`, if specified, will cause only the part of the tilemap within sourceRect to be drawn. `sourceRect` should be relative to the tilemap’s bounds and can be a playdate.geometry.rect or four integers, (`x`, `y`, `w`, `h`), representing the rect. +---@field draw (fun(self: pd_tilemap, x: number, y: number, sourceRect?: pd_rect)) | (fun(self: pd_tilemap, x: number, y: number, rx?: number, ry?: number, rw?: number, rh?: number)) +--- Draws the tilemap ignoring the currently-set drawOffset. +---@field drawIgnoringOffset (fun(self: pd_tilemap, x: number, y: number, sourceRect?: pd_rect)) | (fun(self: pd_tilemap, x: number, y: number, rx?: number, ry?: number, rw?: number, rh?: number)) +--- Sets the index of the tile at tilemap position (`x`, `y`). `index` is the (1-based) index of the image in the tilemap’s playdate.graphics.imagetable. +--- +--- Tilemaps and imagetables, like Lua arrays, are 1-based, not 0-based. tilemap:setTileAtPosition(1, 1, 2) will set the index of the tile in the top-leftmost position to 2. +---@field setTileAtPosition fun(self: pd_tilemap, x: number, y: number, index: integer) +--- Returns the image index of the tile at the given `x` and `y` coordinate. If `x` or `y` is out of bounds, returns nil. +--- +--- Tilemaps and imagetables, like Lua arrays, are 1-based, not 0-based. tilemap:getTileAtPosition(1, 1) will return the index of the top-leftmost tile. +---@field getTileAtPosition fun(self: pd_tilemap, x: number, y: number): integer? +--- Sets the tilemap’s width and height, in number of tiles. +---@field setSize fun(self: pd_tilemap, width: number, height: number) +--- Returns the size of the tile map, in tiles, as a pair, (`width`, `height`). +---@field getSize fun(self: pd_tilemap): (number, number) +--- Returns the size of the tilemap in pixels; that is, the size of the image multiplied by the number of rows and columns in the map. Returns multiple values (`width`, `height`). +---@field getPixelSize fun(self: pd_tilemap): (number, number) +--- Returns multiple values (`width`, `height`), the pixel width and height of an individual tile. +---@field getTileSize fun(self: pd_tilemap): (number, number) +--- This function returns an array of playdate.geometry.rect objects that describe the areas of the tilemap that should trigger collisions. You can also think of them as the "impassable" rects of your tilemap. These rects will be in tilemap coordinates, not pixel coordinates. +--- +--- `emptyIDs` is an array that contains the tile IDs of "empty" (or "passable") tiles in the tilemap — in other words, tile IDs that should not trigger a collision. Tiles with default IDs of 0 are treated as empty by default, so you do not need to include 0 in the array. +--- +--- For example, if you have a tilemap describing terrain, where tile ID 1 represents grass the player can walk over, and tile ID 2 represents mountains that the player can’t cross, you’d pass an array containing just the value 1. You’ll get a back an array of a minimal number of rects describing the areas where there are mountain tiles. +--- +--- You can then pass each of those rects into playdate.graphics.sprite.addEmptyCollisionSprite() to add an empty (invisible) sprite into the scene for the built-in collision detection methods. In this example, collide rects would be added around mountain tiles but not grass tiles. +--- +--- Alternatively, instead of calling getCollisionRects() at all, you can use the convenience function playdate.graphics.sprite.addWallSprites(), which is effectively a shortcut for calling getCollisionRects() and passing all the resulting rects to addEmptyCollisionSprite(). +---@field getCollisionRects fun(self: pd_tilemap, emptyIDs: integer[]): pd_rect + +---@class pd_sprite_lib : pd_sprite +--- This class method (note the "." syntax rather than ":") returns a new sprite object. A previously-loaded image or tilemap object can be optionally passed-in. +--- +--- To see your sprite onscreen, you will need to call :add() on your sprite to add it to the display list. +---@field new fun(image_or_tilemap?: pd_image|pd_tilemap): pd_sprite +--- You must import `CoreLibs/sprites` to use this function. +--- +--- A conveneince function that creates a sprite with an image of `text`, as generated by imageWithText(). +--- +--- The arguments are the same as those in imageWithText(). +--- +--- Returns `sprite`, `textWasTruncated` +--- +--- `sprite` is a newly-created sprite with its image set to an image of the text specified. The sprite’s dimensions may be smaller than `maxWidth`, `maxHeight`. +--- +--- `textWasTruncated` indicates if the text was truncated to fit within the specified width and height. +---@field spriteWithText fun(text: string, maxWidth: number, maxHeight: number, backgroundColor?: pd_color, leadingAdjustment?: number, truncationString?: string, alignment?: pd_text_alignment, font?: pd_font): (pd_sprite, boolean) +--- This class method (note the "." syntax rather than ":") calls the update() function on every sprite in the global sprite list and redraws all of the dirty rects. +--- +--- You will generally want to call playdate.graphics.sprite.update() once in your playdate.update() method, to ensure that your sprites are updated and drawn during every frame. Failure to do so may mean your sprites will not appear onscreen. +--- Be careful not confuse sprite.update() with sprite:update(): the former updates all sprites; the latter updates just the sprite being invoked. +---@field update fun() +--- Adds the given sprite to the display list, so that it is drawn in the current scene. Note that this is called with a period . instead of a colon :. +---@field addSprite fun(sprite: pd_sprite) +--- Removes the given sprite from the display list. As with add()/addSprite(), note that this is called with a period . instead of a colon :. +---@field removeSprite fun(sprite: pd_sprite) +--- You must import `CoreLibs/sprites` to use this function. +--- +--- A convenience function for drawing a background image behind your sprites. +--- +--- `drawCallback` is a routine you specify that implements your background drawing. The callback should be a function taking the arguments x, y, width, height, where `x, y, width, height` specify the region (in screen coordinates, not world coordinates) of the background region that needs to be updated. +--- +--- Some implementation details: setBackgroundDrawingCallback() creates a screen-sized sprite with a z-index set to the lowest possible value so it will draw behind other sprites, and adds the sprite to the display list so that it is drawn in the current scene. The background sprite ignores the drawOffset, and will not be automatically redrawn when the draw offset changes; use playdate.graphics.sprite.redrawBackground() if necessary in this case. `drawCallback` will be called from the newly-created background sprite’s playdate.graphics.sprite:draw() callback function and is where you should do your background drawing. This function returns the newly created playdate.graphics.sprite. +---@field setBackgroundDrawingCallback fun(drawCallback: fun(number, number, number, number)) +--- You must import `CoreLibs/sprites` to use this function. +--- +--- Marks the background sprite dirty, forcing the drawing callback to be run when playdate.graphics.sprite.update() is called. +---@field redrawBackground fun() +--- Sets the clip rect for sprites in the given z-index range. +---@field setClipRectsInRange (fun(x: number, y: number, width: number, height: number, startz: number, endz: number)) | (fun(rect: pd_rect, startz: number, endz: number)) +--- Clears sprite clip rects in the given z-index range. +---@field clearClipRectsInRange fun(startz: number, endz: number) +--- If set to true, causes all sprites to draw each frame, whether or not they have been marked dirty. This may speed up the performance of your game if the system’s dirty rect tracking is taking up too much time - for example if there are many sprites moving around on screen at once. +---@field setAlwaysRedraw fun(flag: boolean) +--- Return’s the sprites "always redraw" flag. +---@field getAlwaysRedraw fun(): boolean +--- Returns an array of all sprites in the display list. +---@field getAllSprites fun(): pd_sprite[] +--- You must import `CoreLibs/sprites` to use this function. +--- +--- Performs the function `f` on all sprites in the display list. `f` should take one argument, which will be a sprite. +---@field performOnAllSprites fun(f: fun(pd_sprite)) +--- Returns the number of sprites in the display list. +---@field spriteCount fun(): number +--- Removes all sprites from the global sprite list. +---@field removeAll fun() +--- Returns an array of array-style tables, each containing two sprites that have overlapping collide rects. All sprite pairs that are have overlapping collide rects (taking the sprites' group and collides-with masks into consideration) are returned. +--- +--- An example of iterating over the collisions array: +--- local collisions = gfx.sprite.allOverlappingSprites() +--- +--- for i = 1, #collisions do +--- local collisionPair = collisions[i] +--- local sprite1 = collisionPair[1] +--- local sprite2 = collisionPair[2] +--- -- do something with the colliding sprites +--- end +---@field allOverlappingSprites fun(): table<1|2,pd_sprite>[] +--- Returns all sprites with collision rects containing the point. +---@field querySpritesAtPoint (fun(x: number, y: number): pd_sprite[]) | (fun(p: pd_point): pd_sprite[]) +--- Returns all sprites with collision rects overlapping the rect. +---@field querySpritesInRect (fun(x: number, y: number, width: number, height: number): pd_sprite[]) | (fun(rect: pd_rect): pd_sprite[]) +--- Returns all sprites with collision rects intersecting the line segment. +---@field querySpritesAlongLine (fun(x1: number, y1: number, x2: number, y2: number): pd_sprite[]) | (fun(lineSegment: pd_lineSegment): pd_sprite[]) +--- Similar to `querySpritesAlongLine()`, but instead of sprites returns an array of `collisionInfo` tables containing information about sprites intersecting the line segment, and `len`, which is the number of collisions found. If you don’t need this information, use `querySpritesAlongLine()` as it will be faster. +--- +--- Each `collisionInfo` table contains: +--- +--- `sprite`: the sprite being intersected by the segment. +--- +--- `entryPoint`: a point representing the coordinates of the first intersection between sprite and the line segment. +--- +--- `exitPoint`: a point representing the coordinates of the second intersection between sprite and the line segment. +--- +--- `ti1` & `ti2`: numbers between 0 and 1 which indicate how far from the starting point of the line segment the collision happened; t1 for the entry point, t2 for the exit point. This can be useful for things like having a laser cause more damage if the impact is close. +---@field querySpriteInfoAlongLine (fun(x1: number, y1: number, x2: number, y2: number): (pd_line_collision_info[], integer)) | (fun(lineSegment: pd_lineSegment): (pd_line_collision_info[], integer)) +--- You must import `CoreLibs/sprites` to use this function. +--- +--- This convenience function adds an invisible sprite defined by the rectangle `x`, `y`, `w`, `h` (or the playdate.geometry.rect `r`) for the purpose of triggering collisions. This is useful for making areas impassable, triggering an event when a sprite enters a certain area, and so on. +---@field addEmptyCollisionSprite (fun(r: pd_rect)) | (fun(x: number, y: number, w: number, h: number)) +--- You must import `CoreLibs/sprites` to use this function. +--- +--- This convenience function automatically adds empty collision sprites necessary to restrict movement within a tilemap. +--- +--- `tilemap` is a playdate.graphics.tilemap. +--- +--- `emptyIDs` is an array of tile IDs that should be considered "passable" — in other words, not walls. Tiles with default IDs of 0 are treated as passable by default, so you do not need to include 0 in the array. +--- +--- `xOffset, yOffset` optionally indicate the distance the new sprites should be offset from (0,0). +--- +--- Returns an array-style table of the newly created sprites. +--- +--- Calling this function is effectively a shortcut for calling playdate.graphics.tilemap:getCollisionRects() and passing the resulting rects to addEmptyCollisionSprite(). +---@field addWallSprites fun(tilemap: pd_tilemap, emptyIDs: integer[], xOffset?: number, yOffset?: number): pd_sprite[] +---@field kCollisionTypeSlide pd_collision_type +---@field kCollisionTypeFreeze pd_collision_type +---@field kCollisionTypeOverlap pd_collision_type +---@field kCollisionTypeBounce pd_collision_type + +---@class pd_sprite +--- Sets the sprite’s image to image, which should be an instance of playdate.graphics.image. The .flip_ argument is optional; see playdate.graphics.image:draw() for valid values. Optional scale arguments are also accepted. Unless disabled with playdate.graphics.sprite:setRedrawOnImageChange(), the sprite is automatically marked for redraw if the image isn’t the previous image. +---@field setImage fun(self: pd_sprite, image: pd_image, flip?: pd_image_flip, scale?: number, yscale?: number) +--- Returns the playdate.graphics.image object that was set with setImage(). +---@field getImage fun(self: pd_sprite): pd_image +--- Adds the given sprite to the display list, so that it is drawn in the current scene. +---@field add fun(self: pd_sprite) +--- Removes the given sprite from the display list. +---@field remove fun(self: pd_sprite) +--- Moves the sprite and resets the bounds based on the image dimensions and center. +---@field moveTo fun(self: pd_sprite, x: number, y: number) +--- Returns the sprite’s current x, y position as multiple values (`x`, `y`). +---@field getPosition fun(self: pd_sprite): (number, number) +--- Can be used to directly read your sprite’s x position. +---@field x READONLY_number +--- Can be used to directly read your sprite’s y position. +---@field y READONLY_number +--- Moves the sprite by `x`, `y` pixels relative to its current position. +---@field moveBy fun(self: pd_sprite, x: number, y: number) +--- Sets the Z-index of the given sprite. Sprites with higher Z-indexes are drawn on top of those with lower Z-indexes. Valid values for `z` are in the range (-32768, 32767). +---@field setZIndex fun(self: pd_sprite, z: number) +--- Returns the Z-index of the given sprite. +---@field getZIndex fun(self: pd_sprite): number +--- Sprites that aren’t visible don’t get their draw() method called. +---@field setVisible fun(self: pd_sprite, flag: boolean) +--- Returns a boolean value, true if the sprite is visible. +---@field isVisible fun(self: pd_sprite): boolean +--- Sets the sprite’s drawing center as a fraction (ranging from 0.0 to 1.0) of the height and width. Default is 0.5, 0.5 (the center of the sprite). This means that when you call :moveTo(x, y), the center of your sprite will be positioned at `x`, `y`. If you want x and y to represent the upper left corner of your sprite, specify the center as 0, 0. +---@field setCenter fun(self: pd_sprite, x: number, y: number) +--- Returns multiple values (x, y) representing the sprite’s drawing center as a fraction (ranging from 0.0 to 1.0) of the height and width. +---@field getCenter fun(self: pd_sprite): (number, number) +--- Returns a playdate.geometry.point representing the sprite’s drawing center as a fraction (ranging from 0.0 to 1.0) of the height and width. +---@field getCenterPoint fun(self: pd_sprite): pd_point +--- Sets the sprite’s size. The method has no effect if the sprite has an image set. +---@field setSize fun(self: pd_sprite, width: number, height: number) +--- Returns multiple values `(width, height)`, the current size of the sprite. +---@field getSize fun(self: pd_sprite): (number, number) +--- Can be used to directly read your sprite’s width. +---@field width READONLY_number +--- Can be used to directly read your sprite’s height. +---@field height READONLY_number +--- Sets the scaling factor for the sprite, with an optional separate scaling for the y axis. If setImage() is called after this, the scale factor is applied to the new image. Only affects sprites that have an image set. +---@field setScale fun(self: pd_sprite, scale: number, yScale?: number) +--- Returns multiple values `(xScale, yScale)`, the current scaling of the sprite. +---@field getScale fun(self: pd_sprite): (number, number) +--- Sets the rotation for the sprite, in degrees clockwise, with an optional scaling factor. If setImage() is called after this, the rotation and scale is applied to the new image. Only affects sprites that have an image set. This function should be used with discretion, as it’s likely to be slow on the hardware. Consider pre-rendering rotated images for your sprites instead. +---@field setRotation fun(self: pd_sprite, angle: number, scale?: number, yScale?: number) +--- Returns the current rotation of the sprite. +---@field getRotation fun(self: pd_sprite): number +--- Returns a copy of the caller. +---@field copy fun(self: pd_sprite): pd_sprite +--- The sprite’s `updatesEnabled` flag (defaults to true) determines whether a sprite’s update() method will be called. By default, a sprite’s update method does nothing; however, you may choose to have your sprite do something on every frame by implementing an update method on your sprite instance, or implementing it in your sprite subclass. +---@field setUpdatesEnabled fun(self: pd_sprite, flag: boolean) +--- Returns a boolean value, true if updates are enabled on the sprite. +---@field updatesEnabled fun(self: pd_sprite): boolean +--- Sets the sprite’s tag, an integer value in the range of 0 to 255, useful for identifying sprites later, particularly when working with collisions. +---@field setTag fun(self: pd_sprite, tag: integer) +--- Returns the sprite’s tag, an integer value. +---@field getTag fun(self: pd_sprite): integer +--- Sets the mode for drawing the bitmap. See playdate.graphics.setImageDrawMode(mode) for valid modes. +---@field setImageDrawMode fun(self: pd_sprite, mode: pd_draw_mode) +--- Flips the bitmap. See playdate.graphics.image:draw() for valid flip values. +--- +--- If true is passed for the optional `flipCollideRect` argument, the sprite’s collideRect will be flipped as well. +--- +--- Calling setImage() will reset the sprite to its default, non-flipped orientation. So, if you call both setImage() and setImageFlip(), call setImage() first. +---@field setImageFlip fun(self: pd_sprite, flip: pd_image_flip, flipCollideRect?: pd_rect) +--- Returns one of the values listed at playdate.graphics.image:draw(). +---@field getImageFlip fun(self: pd_sprite): pd_image +--- When set to `true`, the sprite will draw in screen coordinates, ignoring the currently-set `drawOffset`. +--- +--- This only affects drawing, and should not be used on sprites being used for collisions, which will still happen in world-space. +---@field setIgnoresDrawOffset fun(self: pd_sprite, flag: boolean) +--- ### Overload 1 ### +--- setBounds() positions and sizes the sprite, used for drawing and for calculating dirty rects. `upper-left-x` and `upper-left-y` are relative to the overall display coordinate system. (If an image is attached to the sprite, the size will be defined by that image, and not by the `width` and `height` parameters passed in to setBounds().) +--- +--- In setBounds(), `x` and `y` always correspond to the upper left corner of the sprite, regardless of how a sprite’s center is defined. This makes it different from sprite:moveTo(), where `x` and `y` honor the sprite’s defined center (by default, at a point 50% along the sprite’s width and height.) +--- +--- ### Overload 2 ### +--- setBounds(rect) sets the bounds of the sprite with a playdate.geometry.rect object. +---@field setBounds (fun(self: pd_sprite, upper-left-x: number, upper-left-y: number, width: number, height: number)) | (fun(self: pd_sprite, rect: pd_rect)) +--- getBounds() returns multiple values (`x`, `y`, `width`, `height`). +---@field getBounds fun(self: pd_sprite): (number, number, number, number) +--- getBoundsRect() returns the sprite bounds as a playdate.geometry.rect object. +---@field getBoundsRect fun(self: pd_sprite): pd_rect +--- Marking a sprite opaque tells the sprite system that it doesn’t need to draw anything underneath the sprite, since it will be overdrawn anyway. If you set an image without a mask/alpha channel on the sprite, it automatically sets the opaque flag. +--- +--- Setting a sprite to opaque can have performance benefits. +---@field setOpaque fun(self: pd_sprite, flag: boolean) +--- Returns the sprite’s current opaque flag. +---@field isOpaque fun(self: pd_sprite): boolean +--- Sets the sprite’s contents to the given tilemap. Useful if you want to automate drawing of your tilemap, especially if interleaved by depth with other sprites being drawn. +---@field setTilemap fun(self: pd_sprite, tilemap: pd_tilemap) +--- You must import `CoreLibs/sprites` to use the setAnimator method. +--- +--- setAnimator assigns an playdate.graphics.animator to the sprite, which will cause the sprite to automatically update its position each frame while the animator is active. +--- +--- `animator` should be a playdate.graphics.animator created using playdate.geometry.points for its start and end values. +--- +--- `movesWithCollisions`, if provided and true will cause the sprite to move with collisions. A collision rect must be set on the sprite prior to passing true for this argument. +--- +--- `removeOnCollision`, if provided and true will cause the animator to be removed from the sprite when a collision occurs. +--- +--- setAnimator should be called only after any custom update method has been set on the sprite. +---@field setAnimator fun(self: pd_sprite, animator: pd_animator, moveWithCollisions?: boolean, removeOnCollision?: boolean) +--- Removes a playdate.graphics.animator assigned to the sprite +---@field removeAnimator fun(self: pd_sprite) +--- Sets the clipping rectangle for the sprite, using separate parameters or a playdate.geometry.rect object. Only areas within the rect will be drawn. +---@field setClipRect (fun(self: pd_sprite, x: number, y: number, width: number, height: number)) | (fun(self: pd_sprite, rect: pd_rect)) +--- Clears the sprite’s current clipping rectangle. +---@field clearClipRect fun(self: pd_sprite) +--- Specifies a stencil image to be set on the frame buffer before the sprite is drawn. If `tile` is set, the the stencil will be tiled; in this case, the image width must be a multiple of 32 pixels. +---@field setStencilImage fun(self: pd_sprite, stencil: pd_image, tile?: boolean) +--- Sets the sprite’s stencil to a dither pattern specified by `level` and optional `ditherType` (defaults to playdate.graphics.image.kDitherTypeBayer8x8). +---@field setStencilPattern fun(self: pd_sprite, level: number, ditherType?: pd_dither_type) +--- Clears the sprite’s stencil. +---@field clearStencil fun(self: pd_sprite) +--- Marks the rect defined by the sprite’s current bounds as needing a redraw. +---@field markDirty fun(self: pd_sprite) +--- Marks the given rectangle (in screen coordinates) as needing a redraw. playdate.graphics drawing functions now call this automatically, adding their drawn areas to the sprite’s dirty list, so there’s likely no need to call this manually any more. This behavior may change in the future, though. +---@field addDirtyRect fun(x: number, y: number, width: number, height: number) +--- By default, sprites are automatically marked for redraw when their image is changed via playdate.graphics.sprite:setImage(). If disabled by calling this function with a `false` argument, playdate.graphics.sprite.addDirtyRect() can be used to mark the (potentially smaller) area of the screen that needs to be redrawn. +---@field setRedrawsOnImageChange fun(self: pd_sprite, flag: boolean) +--- Removes all sprites in spriteArray from the global sprite list. +---@field removeSprites fun(spriteArray: pd_sprite[]) +--- If the sprite doesn’t have an image, the sprite’s draw function is called as needed to update the display. The rect passed in is the current dirty rect being updated by the display list. The rect coordinates passed in are relative to the sprite itself (i.e. x = 0, y = 0 refers to the top left corner of the sprite). Note that the callback is only called when the sprite is on screen and has a size specified via sprite:setSize() or sprite:setBounds(). +--- +--- Example: Overriding the sprite draw method +--- -- You can copy and paste this example directly as your main.lua file to see it in action +--- import "CoreLibs/graphics" +--- import "CoreLibs/sprites" +--- +--- local mySprite = playdate.graphics.sprite.new() +--- mySprite:moveTo(200, 120) +--- -- You MUST set a size first for anything to show up (either directly or by setting an image) +--- mySprite:setSize(30, 30) +--- mySprite:add() +--- +--- -- The x, y, width, and height arguments refer to the dirty rect being updated, NOT the sprite dimensions +--- function mySprite:draw(x, y, width, height) +--- -- Custom draw methods gives you more flexibility over what's drawn, but with the added benefits of sprites +--- +--- -- Here we're just modulating the circle radius over time +--- local spriteWidth, spriteHeight = self:getSize() +--- if not self.radius or self.radius > spriteWidth then +--- self.radius = 0 +--- end +--- self.radius += 1 +--- +--- -- Drawing coordinates are relative to the sprite (e.g. (0, 0) is the top left of the sprite) +--- playdate.graphics.fillCircleAtPoint(spriteWidth / 2, spriteHeight / 2, self.radius) +--- end +--- +--- function playdate.update() +--- -- Your custom draw method gets called here, but only if the sprite is dirty +--- playdate.graphics.sprite.update() +--- +--- -- You might need to manually mark it dirty +--- mySprite:markDirty() +--- end +---@field draw nil | (fun(self: pd_sprite, x: number, y: number, width: number, height: number)) +--- Called by playdate.graphics.sprite.update() (note the syntactic difference between the period and the colon) before sprites are drawn. Implementing :update() gives you the opportunity to perform some code upon every frame. +--- +--- The update method will only be called on sprites that have had add() called on them, and have their updates enabled. +--- Be careful not confuse sprite:update() with sprite.update(): the latter updates all sprites; the former updates just the sprite being invoked. +--- Example: Overriding the sprite update method +--- local mySprite = playdate.graphics.sprite.new() +--- mySprite:moveTo(200, 120) +--- mySprite:add() -- Sprite needs to be added to get drawn and updated +--- -- mySprite:remove() will make it so the sprite stops getting drawn/updated +--- +--- -- Option 1: override the update method using an anonymous function +--- mySprite.update = function(self) +--- print("This gets called every frame when I'm added to the display list") +--- -- Manipulate sprite using "self" +--- print(self.x) -- Prints 200.0 +--- print(self.y) -- Prints 120.0 +--- end +--- +--- -- Option 2: override the update method using a function stored in a variable +--- local function mySpriteUpdate(self) +--- print("This gets called every frame when I'm added to the display list") +--- -- Manipulate sprite using "self" +--- print(self.x) -- Prints 200.0 +--- print(self.y) -- Prints 120.0 +--- end +--- mySprite.update = mySpriteUpdate +--- +--- -- Option 3: override the update method by directly defining it +--- function mySprite:update() +--- print("This gets called every frame when I'm added to the display list") +--- -- Manipulate sprite using "self" +--- print(self.x) -- Prints 200.0 +--- print(self.y) -- Prints 120.0 +--- end +--- +--- function playdate.update() +--- -- Your custom update method gets called here every frame if the sprite has been added +--- playdate.graphics.sprite.update() +--- end +--- +--- -- VERY simplified psuedocode explanation of what's happening in sprite.update() (not real code) +--- local displayList = {} -- Added sprites are kept track of in a list +--- function playdate.graphics.sprite.update() +--- -- The display list is iterated over +--- for i=1, #displayList do +--- local sprite = displayList[i] +--- -- Checks if updates on the sprites are enabled +--- if sprite:updatesEnabled() then +--- -- The sprite update method is called +--- sprite:update() +--- end +--- ... +--- -- Redraw all of the dirty rects, handle collisions, etc. +--- end +--- end +---@field update nil | (fun(self: pd_sprite)) +--- setCollideRect() marks the area of the sprite, relative to its own internal coordinate system, to be checked for collisions with other sprites' collide rects. Note that the coordinate space is relative to the top-left corner of the bounds, regardless of where the sprite’s center/anchor is located. +--- +--- If you want to set the sprite’s collide rect to be the same size as the sprite itself, you can write sprite:setCollideRect( 0, 0, sprite:getSize() ). +--- setCollideRect() must be invoked on a sprite in order to get it to participate in collisions. +--- Very large sprites with very large collide rects should be avoided, as they will have a negative impact on performance and memory usage. +---@field setCollideRect (fun(self: pd_sprite, x: number, y: number, width: number, height: number)) | (fun(self: pd_sprite, rect: pd_rect)) +--- Returns the sprite’s collide rect set with setCollideRect(). Return value is a playdate.geometry.rect. +--- +--- This function return coordinates relative to the sprite itself; the sprite’s position has no bearing on these values. +---@field getCollideRect fun(self: pd_sprite): pd_rect +--- Returns the sprite’s collide rect as multiple values, (`x`, `y`, `width`, `height`). +--- +--- This function return coordinates relative to the sprite itself; the sprite’s position has no bearing on these values. +---@field getCollideBounds fun(self: pd_sprite): (number, number, number, number) +--- Clears the sprite’s collide rect set with setCollideRect(). +---@field clearCollideRect fun(self: pd_sprite) +--- Returns an array of sprites that have collide rects that are currently overlapping the calling sprite’s collide rect, taking the sprites' groups and collides-with masks into consideration. +---@field overlappingSprites fun(self: pd_sprite): pd_sprite[] +--- Returns a boolean value set to true if a pixel-by-pixel comparison of the sprite images shows that non-transparent pixels are overlapping, based on the current bounds of the sprites. +--- +--- This method may be used in conjunction with the standard collision architecture. Say, if overlappingSprites() or moveWithCollisions() report a collision of two sprite’s bounding rects, alphaCollision() could then be used to discern if a pixel-level collision occurred. +---@field alphaCollision fun(self: pd_sprite, anotherSprite: pd_sprite): boolean +--- The sprite’s `collisionsEnabled` flag (defaults to true) can be set to false in order to temporarily keep a sprite from colliding with any other sprite. +---@field setCollisionsEnabled fun(self: pd_sprite, flag: boolean) +--- Returns the sprite’s `collisionsEnabled` flag. +---@field collisionsEnabled fun(self: pd_sprite): boolean +--- Adds the sprite to one or more collision groups. A group is a collection of sprites that exhibit similar collision behavior. (An example: in Atari’s `Asteroids`, asteroid sprites would all be added to the same group, while the player’s spaceship might be in a different group.) Use setCollidesWithGroups() to define which groups a sprite should collide with. +--- +--- There are 32 groups, each defined by the integer 1 through 32. To add a sprite to only groups 1 and 3, for example, call mySprite:setGroups({1, 3}). +--- +--- Alternatively, use setGroupMask() to set group membership via a bitmask. +---@field setGroups fun(self: pd_sprite, groups: integer[]) +--- Pass in a group number or an array of group numbers to specify which groups this sprite can collide with. Groups are numbered 1 through 32. Use setGroups() to specify which groups a sprite belongs to. +--- +--- Alternatively, you can specify group collision behavior with a bitmask by using setCollidesWithGroupsMask(). +---@field setCollidesWithGroups fun(self: pd_sprite, groups: integer[]) +--- setGroupMask() sets the sprite’s group bitmask, which is 32 bits. In conjunction with the setCollidesWithGroupsMask() method, this controls which sprites can collide with each other. +--- +--- For large group mask numbers, pass the number as a hex value, eg. 0xFFFFFFFF to work around limitations in Lua’s integer sizes. +---@field setGroupMask fun(self: pd_sprite, mask: integer) +--- getGroupMask() returns the integer value of the sprite’s group bitmask. +---@field getGroupMask fun(self: pd_sprite): integer +--- Sets the sprite’s collides-with-groups bitmask, which is 32 bits. The mask specifies which other sprite groups this sprite can collide with. Sprites only collide if the moving sprite’s `collidesWithGroupsMask` matches at least one group of a potential collision sprite (i.e. a bitwise AND (&) between the moving sprite’s `collidesWithGroupsMask` and a potential collision sprite’s `groupMask` != zero) or if the moving sprite’s `collidesWithGroupsMask` and the other sprite’s `groupMask` are both set to 0x00000000 (the default values). +--- +--- For large mask numbers, pass the number as a hex value, eg. 0xFFFFFFFF to work around limitations in Lua’s integer sizes. +---@field setCollidesWithGroupsMask fun(self: pd_sprite, mask: integer) +--- Returns the integer value of the sprite’s collision bitmask. +---@field getCollidesWithGroupsMask fun(self: pd_sprite): integer +--- Resets the sprite’s group mask to 0x00000000. +---@field resetGroupMask fun(self: pd_sprite) +--- Resets the sprite’s collides-with-groups mask to 0x00000000. +---@field resetCollidesWithGroupsMask fun(self: pd_sprite) +--- Moves the sprite towards `goalX`, `goalY` or `goalPoint` taking collisions into account, which means the sprite’s final position may not be the same as `goalX`, `goalY` or `goalPoint`. +--- +--- Returns `actualX`, `actualY`, `collisions`, `length`. +--- +--- `actualX`, `actualY` +--- +--- +--- +--- the final position of the sprite. If no collisions occurred, this will be the same as `goalX`, `goalY`. +--- +--- +--- +--- +--- `collisions` +--- +--- +--- +--- an array of userdata objects containing information about all collisions that occurred. Each item in the array contains values for the following indices: +--- +--- - `sprite`: The sprite being moved. +--- +--- - `other`: The sprite colliding with the sprite being moved. +--- +--- - `type`: The result of `collisionResponse`. +--- +--- - `overlaps`: Boolean. True if the sprite was overlapping `other` when the collision started. False if it didn’t overlap but tunneled through `other`. +--- +--- - `ti`: A number between 0 and 1 indicating how far along the movement to the goal the collision occurred. +--- +--- - `move`: playdate.geometry.vector2D. The difference between the original coordinates and the actual ones when the collision happened. +--- +--- - `normal`: playdate.geometry.vector2D. The collision normal; usually -1, 0, or 1 in `x` and `y`. Use this value to determine things like if your character is touching the ground. +--- +--- - `touch`: playdate.geometry.point. The coordinates where the sprite started touching `other`. +--- +--- - `spriteRect`: playdate.geometry.rect. The rectangle the sprite occupied when the touch happened. +--- +--- - `otherRect`: playdate.geometry.rect. The rectangle other occupied when the touch happened. +--- +--- If the collision type was `playdate.graphics.sprite.kCollisionTypeBounce` the table also contains `bounce`, a playdate.geometry.point indicating the coordinates to which the sprite attempted to bounce (could be different than `actualX`, `actualY` if further collisions occurred). +--- +--- If the collision type was `playdate.graphics.sprite.kCollisionTypeSlide` the table also contains `slide`, a playdate.geometry.point indicating the coordinates to which the sprite attempted to slide. +--- +--- +--- +--- +--- `length` +--- +--- +--- +--- the length of the collisions array, equal to `#collisions` +--- +--- Note that the collision info items are only valid until the next call of `moveWithCollisions` or `checkCollisions`. To save collision information for later, the data should be copied out of the collision info userdata object. +--- +--- See also checkCollisions() to check for collisions without actually moving the sprite. +--- +--- Example: Using moveWithCollisions for a simple player collision example +--- -- You can copy and paste this example directly as your main.lua file to see it in action +--- import "CoreLibs/graphics" +--- import "CoreLibs/sprites" +--- +--- -- Creating a tags object, to keep track of tags more easily +--- TAGS = { +--- player = 1, +--- obstacle = 2, +--- coin = 3, +--- powerUp = 4 +--- } +--- +--- -- Creating a player sprite we can move around and collide things with +--- local playerImage = playdate.graphics.image.new(20, 20) +--- playdate.graphics.pushContext(playerImage) +--- playdate.graphics.fillCircleInRect(0, 0, playerImage:getSize()) +--- playdate.graphics.popContext() +--- local playerSprite = playdate.graphics.sprite.new(playerImage) +--- -- Setting a tag on the player, so we can check the tag to see if we're colliding against the player +--- playerSprite:setTag(TAGS.player) +--- playerSprite:moveTo(200, 120) +--- -- Remember to set a collision rect, or this all doesn't work! +--- playerSprite:setCollideRect(0, 0, playerSprite:getSize()) +--- playerSprite:add() +--- +--- -- Creating an obstacle sprite we can collide against +--- local obstacleImage = playdate.graphics.image.new(20, 20, playdate.graphics.kColorBlack) +--- local obstacleSprite = playdate.graphics.sprite.new(obstacleImage) +--- -- Setting a tag for the obstacle as well +--- obstacleSprite:setTag(TAGS.obstacle) +--- obstacleSprite:moveTo(300, 120) +--- -- Can't forget this! +--- obstacleSprite:setCollideRect(0, 0, obstacleSprite:getSize()) +--- obstacleSprite:add() +--- +--- function playdate.update() +--- playdate.graphics.sprite.update() +--- +--- -- Some simple movement code for the sake of demonstration +--- local moveSpeed = 3 +--- local goalX, goalY = playerSprite.x, playerSprite.y +--- if playdate.buttonIsPressed(playdate.kButtonUp) then +--- goalY -= moveSpeed +--- elseif playdate.buttonIsPressed(playdate.kButtonDown) then +--- goalY += moveSpeed +--- elseif playdate.buttonIsPressed(playdate.kButtonLeft) then +--- goalX -= moveSpeed +--- elseif playdate.buttonIsPressed(playdate.kButtonRight) then +--- goalX += moveSpeed +--- end +--- +--- -- Remember to use :moveWithCollisions(), and not :moveTo() or :moveBy(), or collisions won't happen! +--- -- To do a "moveBy" operation, sprite:moveBy(5, 5) == sprite:moveWithCollisions(sprite.x + 5, sprite.y + 5) +--- local actualX, actualY, collisions, numberOfCollisions = playerSprite:moveWithCollisions(goalX, goalY) +--- +--- -- If we get into this loop, there was a collision +--- for i=1, numberOfCollisions do +--- -- This is getting data about one of things we're currently colliding with. Since we could +--- -- be colliding with multiple things at once, we have to handle each collision individually +--- local collision = collisions[i] +--- +--- -- Always prints 'true', as the sprite property is the sprite being moved (in this case, the player) +--- print(collision.sprite == playerSprite) +--- -- Also prints 'true', as we set the tag on the player sprite to the player tag +--- print(collision.sprite:getTag() == TAGS.player) +--- +--- -- This gets the actual sprite object we're colliding with +--- local collidedSprite = collision.other +--- local collisionTag = collidedSprite:getTag() +--- -- Since we set a tag on the obstacle, we can check if we're colliding with that +--- if collisionTag == TAGS.obstacle then +--- print("Collided with an obstacle!") +--- +--- -- We can use the collision normal to check which side we collided with +--- local collisionNormal = collision.normal +--- if collisionNormal.x == -1 then +--- print("Touched left side!") +--- elseif collisionNormal.x == 1 then +--- print("Touched right side!") +--- end +--- +--- if collisionNormal.y == -1 then +--- print("Touched top!") +--- elseif collisionNormal.y == 1 then +--- print("Touched bottom!") +--- end +--- -- Handle some other collisions, like collecting a coin or a power up +--- elseif collisionTag == TAGS.coin then +--- print("Coin collected!") +--- elseif collisionTag == TAGS.powerUp then +--- print("Powered up!") +--- end +--- end +--- end +---@field moveWithCollisions (fun(self: pd_sprite, goalX: number, goalY: number): (number, number, pd_sprite_collision_info[], integer)) | (fun(self: pd_sprite, goalPoint: pd_point): (number, number, pd_sprite_collision_info[], integer)) +--- Returns the same values as moveWithCollisions() but does not actually move the sprite. +---@field checkCollisions (fun(self: pd_sprite, x: number, y: number): (number, number, pd_sprite_collision_info[], integer)) | (fun(self: pd_sprite, point: pd_point): (number, number, pd_sprite_collision_info[], integer)) +--- A callback that can be defined on a sprite to control the type of collision response that should happen when a collision with `other` occurs. This callback should return one of the following four values: +--- +--- `playdate.graphics.sprite.kCollisionTypeSlide`: Use for collisions that should slide over other objects, like Super Mario does over a platform or the ground. +--- +--- `playdate.graphics.sprite.kCollisionTypeFreeze`: Use for collisions where the sprite should stop moving as soon as it collides with `other`, such as an arrow hitting a wall. +--- +--- `playdate.graphics.sprite.kCollisionTypeOverlap`: Use for collisions in which you want to know about the collision but it should not impact the movement of the sprite, such as when collecting a coin. +--- +--- `playdate.graphics.sprite.kCollisionTypeBounce`: Use when the sprite should move away from `other`, like the ball in Pong or Arkanoid. +--- +--- The strings "slide", "freeze", "overlap", and "bounce" can be used instead of the constants. +--- +--- Feel free to return different values based on the value of `other`. For example, if `other` is a wall sprite, you may want to return "slide" or "bounce", but if it’s a coin you might return "overlap". +--- +--- If the callback is not present, or returns nil, `kCollisionTypeFreeze` is used. +--- +--- Instead of defining a callback, the collisionResponse property of a sprite can be set directly to one of the four collision response types. This will be faster, as the lua function will not need to be called, but does not allow for dynamic behavior. +--- +--- This method should not attempt to modify the sprites in any way. While it might be tempting to deal with collisions here, doing so will have unexpected and undesirable results. Instead, this function should return one of the collision response values as quickly as possible. If sprites need to be modified as the result of a collision, do so elsewhere, such as by inspecting the list of collisions returned by moveWithCollisions(). +---@field collisionResponse nil | ((fun(self: pd_sprite, other: pd_sprite): pd_collision_type) | (pd_collision_type)) + +---@class pd_font_lib : pd_font +--- Returns a playdate.graphics.font object from the data at `path`. If there is no file at `path`, the function returns nil. +---@field new fun(path: string): pd_font +--- Returns a font family table from the font files specified in `fontPaths`. `fontPaths` should be a table with the following format: +--- +--- local fontPaths = { +--- [playdate.graphics.font.kVariantNormal] = "path/to/normalFont", +--- [playdate.graphics.font.kVariantBold] = "path/to/boldFont", +--- [playdate.graphics.font.kVariantItalic] = "path/to/italicFont" +--- } +--- +--- The table returned is of the same format with font objects in place of the paths, and is appropriate to pass to the functions setFontFamily() and getTextSize(). +---@field newFamily fun(fontPaths: pd_font_family_paths): pd_font_family +---@field kLanguageEnglish pd_language +---@field kLanguageJapanese pd_language + +---@class pd_font +--- Draws a string at the specified `x, y` coordinate using this particular font instance. (Compare to playdate.graphics.drawText(text, x, y), which draws the string with whatever the "current font" is, as defined by playdate.graphics.setFont(font)). +--- +--- If `width` and `height` are specified, drawing is constrained to the rectangle (x,y,width,height), using the given wrapMode and alignment if provided. Alternatively, a playdate.geometry.rect object can be passed instead of x,y,width,height. Valid values for `wrapMode` are +--- +--- `playdate.graphics.kWrapClip` +--- +--- `playdate.graphics.kWrapCharacter` +--- +--- `playdate.graphics.kWrapWord` +--- +--- and values for `alignment` are +--- +--- `playdate.graphics.kAlignLeft` +--- +--- `playdate.graphics.kAlignCenter` +--- +--- `playdate.graphics.kAlignRight` +--- +--- The default wrap mode is playdate.graphics.kWrapWord and the default alignment is playdate.graphics.kAlignLeft. +--- +--- The optional `leadingAdjustment` may be used to modify the spacing between lines of text. +--- +--- The function returns two numbers indicating the width and height of the drawn text. +--- +--- font:drawText() does not support inline styles like bold and italics. Instead use playdate.graphics.drawText(). +---@field drawText (fun(self: pd_font, text: string, x: number, y: number, width?: number, height?: number, leadingAdjustment?: number, wrapMode?: pd_wrap_mode, alignment?: pd_text_alignment): (number, number)) | (fun(self: pd_font, text: string, rect: pd_rect, leadingAdjustment?: number, wrapMode?: pd_wrap_mode, alignment?: pd_text_alignment): (number, number)) +--- You must import `CoreLibs/graphics` to use this function. +--- +--- Draws the string `text` aligned to the left, right, or centered on the `x` coordinate. Pass one of `kTextAlignment.left`, `kTextAlignment.center`, `kTextAlignment.right` for the `alignment` parameter. (Compare to playdate.graphics.drawTextAligned(text, x, y, alignment), which draws the string with the "current font", as defined by playdate.graphics.setFont(font)). +---@field drawTextAligned fun(self: pd_font, text: string, x: number, y: number, alignment: pd_text_alignment, leadingAdjustment?: number) +--- Returns the pixel height of this font. +---@field getHeight fun(self: pd_font): number +--- Returns the pixel width of the text when rendered with this font. +---@field getTextWidth fun(self: pd_font, text: string): number +--- Sets the tracking of this font (spacing between letters), in pixels. +--- +--- Equivalent to playdate->graphics->setTextTracking() in the C API. +---@field setTracking fun(self: pd_font, pixels: number) +--- Returns the tracking of this font (spacing between letters), in pixels. +--- +--- Equivalent to playdate->graphics->getTextTracking() in the C API. +---@field getTracking fun(self: pd_font): number +--- Sets the leading (spacing between lines) of this font, in pixels. +--- +--- Equivalent to playdate->graphics->setTextLeading() in the C API. +---@field setLeading fun(self: pd_font, pixels: number) +--- Returns the leading (spacing between lines) of this font, in pixels. +---@field getLeading fun(self: pd_font): number +--- Returns the playdate.graphics.image containing the requested glyph. `character` can either be a string or a unicode codepoint number. +---@field getGlyph fun(self: pd_font, character: string|number): pd_image + +---@class pd_video_lib : pd_video +--- Returns a playdate.graphics.video object from the pdv file at `path`. If the file at `path` can’t be opened, the function returns nil. +---@field new fun(path: string): pd_video + +---@class pd_video +--- Returns the width and height of the video as multiple vlaues (`width`, `height`). +---@field getSize fun(self: pd_video): (number, number) +--- Returns the number of frames in the video. +---@field getFrameCount fun(self: pd_video): number +--- Returns the number of frames per second of the video source. This number is simply for record-keeping, it is not used internally—​the game code is responsible for figuring out which frame to show when. +---@field getFrameRate fun(self: pd_video): number +--- Sets the given image to the video render context. Future video:renderFrame() calls will draw into this image. +---@field setContext fun(self: pd_video, image: pd_image) +--- Returns the image into which the video will be rendered, creating it if needed. +---@field getContext fun(self: pd_video): pd_image +--- Sets the display framebuffer as the video’s render context. +---@field useScreenContext fun(self: pd_video) +--- Draws the given frame into the video’s render context. +---@field renderFrame fun(self: pd_video, number: integer) + +---@class pd_graph_lib : pd_graph +--- Returns a new empty playdate.pathfinder.graph object. +--- +--- If nodeCount is supplied, that number of nodes will be allocated and added to the graph. Their IDs will be set from 1 to nodeCount. +--- +--- coordinates, if supplied, should be a table containing tables of x, y values, indexed by node IDs. For example, {{10, 10}, {50, 30}, {20, 100}, {100, 120}, {160, 130}}. +---@field new fun(nodeCount?: number, coordinates?: table<1|2, number>[]): pd_graph +--- Convenience function that returns a new playdate.pathfinder.graph object containing nodes for for each grid position, even if not connected to any other nodes. This allows for easier graph modification once the graph is generated. Weights for connections between nodes are set to 10 for horizontal and vertical connections and 14 for diagonal connections (if included), as this tends to produce nicer paths than using uniform weights. Nodes have their indexes set from 1 to `width` * `height`, and have their `x, y` values set appropriately for the node’s position. +--- +--- `width`: The width of the grid to be created. +--- +--- `height`: The height of the grid to be created. +--- +--- `allowDiagonals`: If true, diagonal connections will also be created. +--- +--- `includedNodes`: A one-dimensional array of length `width` * `height`. Each entry should be a 1 or a 0 to indicate nodes that should be connected to their neighbors and nodes that should not have any connections added. If not provided, all nodes will be connected to their neighbors. +---@field new2DGrid fun(width: number, height: number, allowDiagonals?: boolean, includedNodes?: integer[]): pd_graph + +---@class pd_graph +--- Creates a new playdate.pathfinder.node and adds it to the graph. +--- +--- `id`: id value for the new node. +--- +--- `x`: Optional x value for the node. +--- +--- `y`: Optional y value for the node. +--- +--- `connectedNodes`: Array of existing nodes to create connections to from the new node. +--- +--- `weights`: Array of weights for the new connections. Array must be the same length as `connectedNodes`. Weights affect the path the A* algorithm will solve for. A longer, lighter-weighted path will be chosen over a shorter heavier path, if available. +--- +--- `addReciprocalConnections`: If true, connections will also be added in the reverse direction for each node. +---@field addNewNode fun(self: pd_graph, id: integer, x?: number, y?: number, connectedNodes?: pd_node[], weights?: number[], addReciprocalConnections?: boolean) +--- Creates `count` new nodes, adding them to the graph, and returns them in an array-style table. The new node’s `id_s will be assigned values 1 through _count`-1. +--- +--- This method is useful to improve performance if many nodes need to be allocated at once rather than one at a time, for example when creating a new graph. +---@field addNewNodes fun(self: pd_graph, count: number): pd_node[] +--- Adds an already-existing node to the graph. The node must have originally belonged to the same graph. +--- +--- `node`: Node to be added to the graph. +--- +--- `connectedNodes`: Array of existing nodes to create connections to from the new node. +--- +--- `weights`: Array of weights for the new connections. Array must be the same length as `connectedNodes`. Weights affect the path the A* algorithm will solve for. A longer, lighter-weighted path will be chosen over a shorter heavier path, if available. +--- +--- `addReciprocalConnections`: If true, connections will also be added in the reverse direction for each connection added. +---@field addNode fun(self: pd_graph, node: pd_node, connectedNodes?: pd_node[], weights?: number[], addReciprocalConnections?: boolean) +--- Adds an array of already-existing nodes to the graph. +---@field addNodes fun(self: pd_graph, nodes: pd_node[]) +--- Returns an array containing all nodes in the graph. +---@field allNodes fun(self: pd_graph): pd_node[] +--- Removes node from the graph. Also removes all connections to and from the node. +---@field removeNode fun(self: pd_graph, node: pd_node) +--- Returns the first node found with coordinates matching `x, y`, after removing it from the graph and removing all connections to and from the node. +---@field removeNodeWithXY fun(self: pd_graph, x: number, y: number): pd_node +--- Returns the first node found with a matching `id`, after removing it from the graph and removing all connections to and from the node. +---@field removeNodeWithID fun(self: pd_graph, id: integer): pd_node +--- Returns the first node found in the graph with a matching `id`, or nil if no such node is found. +---@field nodeWithID fun(self: pd_graph, id: integer): pd_node? +--- Returns the first node found in the graph with matching `x` and `y` values, or nil if no such node is found. +---@field nodeWithXY fun(self: pd_graph, x: number, y: number): pd_node? +--- connections should be a table of array-style tables. The keys of the outer table should correspond to node IDs, while the inner array should be a series if connecting node ID and weight combinations that will be assigned to that node. For example, {[1]={2, 10, 3, 12}, [2]={1, 20}, [3]={1, 20, 2, 10}} will create a connection from node ID 1 to node ID 2 with a weight of 10, and a connection to node ID 3 with a weight of 12, and so on for the other entries. +---@field addConnections fun(self: pd_graph, connections: number[][]) +--- Adds a connection from the node with id fromNodeID to the node with id toNodeID with a weight value of weight. Weights affect the path the A* algorithm will solve for. A longer, lighter-weighted path will be chosen over a shorter heavier path, if available. If addReciprocalConnection is true, the reverse connection will also be added. +---@field addConnectionToNodeWithID fun(self: pd_graph, fromNodeID: integer, toNodeID: integer, weight: number, addReciprocalConnection: boolean) +--- Removes all connections from all nodes in the graph. +---@field removeAllConnections fun(self: pd_graph) +--- Removes all connections from the matching node. +--- +--- If removeIncoming is true, all connections from other nodes to the calling node are also removed. False by default. Please note: this can signficantly increase the time this function takes as it requires a full search of the graph - O(1) vs O(n)). +---@field removeAllConnectionsFromNodeWithID fun(self: pd_graph, id: integer, removeIncoming?: boolean) +--- Returns an array of nodes representing the path from `startNode` to `goalNode`, or `nil` if no path can be found. +--- +--- `heuristicFunction`: If provided, this function should be of the form `function(startNode, goalNode)` and should return an integer value estimate or underestimate of the distance from `startNode` to `goalNode`. If not provided, a manhattan distance function will be used to calculate the estimate. This requires that the `x, y` values of the nodes in the graph have been set properly. +--- +--- `findPathToGoalAdjacentNodes`: If true, a path will be found to any node adjacent to the goal node, based on the `x, y` values of those nodes and the goal node. This does not rely on connections between adjacent nodes and the goal node, which can be entirely disconnected from the rest of the graph. +---@field findPath fun(self: pd_graph, startNode: pd_node, goalNode: pd_node, heuristicFunction?: nil | (fun(pd_node, pd_node): number), findPathToGoalAdjacentNodes?: boolean): pd_node[]? +--- Works the same as findPath, but looks up nodes to find a path between using startNodeID and goalNodeID and returns a list of nodeIDs rather than the nodes themselves. +---@field findPathWithIDs fun(self: pd_graph, startNodeID: integer, goalNodeID: integer, heuristicFunction?: nil | (fun(pd_node, pd_node): number), findPathToGoalAdjacentNodes?: boolean): integer[]? +--- Sets the matching node’s x and y values. +---@field setXYForNodeWithID fun(self: pd_graph, id: integer, x: number, y: number) + +---@class pd_node_lib : pd_node + +---@class pd_node +--- Adds a new connection between nodes. +--- +--- `node`: The node the new connection will point to. +--- +--- `weight`: Weight for the new connection. Weights affect the path the A* algorithm will solve for. A longer, lighter-weighted path will be chosen over a shorter heavier path, if available. +--- +--- `addReciprocalConnection`: If true, a second connection will be created with the same weight in the opposite direction. +---@field addConnection fun(self: pd_node, node: pd_node, weight: number, addReciprocalConnection: boolean) +--- Adds a new connection to each node in the nodes array. +--- +--- `nodes`: An array of nodes which the new connections will point to. +--- +--- `weights`: An array of weights for the new connections. Must be of the same length as the nodes array. Weights affect the path the A* algorithm will solve for. A longer, lighter-weighted path will be chosen over a shorter heavier path, if available. +--- +--- `addReciprocalConnections`: If true, connections will also be added in the reverse direction for each node. +---@field addConnections fun(self: pd_node, nodes: pd_node[], weights: number[], addReciprocalConnections: boolean) +--- Adds a connection to the first node found with matching `x` and `y` values, if it exists. +--- +--- `weight`: The weight for the new connection. Weights affect the path the A* algorithm will solve for. A longer, lighter-weighted path will be chosen over a shorter heavier path, if available. +--- +--- `addReciprocalConnections`: If true, a connection will also be added in the reverse direction, from the node at x, y to the caller. +---@field addConnectionToNodeWithXY fun(self: pd_node, x: number, y: number, weight: number, addReciprocalConnection: boolean) +--- Returns an array of nodes that have been added as connections to this node. +---@field connectedNodes fun(self: pd_node): pd_node[] +--- Removes a connection to node, if it exists. If `removeReciprocal` is true the reverse connection will also be removed, if it exists. +---@field removeConnection fun(self: pd_node, node: pd_node, removeReciprocal?: boolean) +--- Removes all connections from the calling node. +--- +--- If removeIncoming is true, all connections from other nodes to the calling node are also removed. False by default. Please note: this can signficantly increase the time this function takes as it requires a full search of the graph - O(1) vs O(n)). +---@field removeAllConnections fun(self: pd_node, removeIncoming?: boolean) +--- Sets the `x` and `y` values for the node. +---@field setXY fun(self: pd_node, x: number, y: number) +--- You can directly read or write `x`, `y` and `id` values on a `playdate.pathfinder.node`. +---@field x number +--- You can directly read or write `x`, `y` and `id` values on a `playdate.pathfinder.node`. +---@field y number +--- You can directly read or write `x`, `y` and `id` values on a `playdate.pathfinder.node`. +---@field id integer + +---@class pd_sampleplayer_lib : pd_sampleplayer +--- ### Overload 1 ### +--- Returns a new playdate.sound.sampleplayer object, with the sound data loaded in memory. If the sample can’t be loaded, the function returns nil and a second value containing the error. +--- +--- ### Overload 2 ### +--- Returns a new playdate.sound.sampleplayer object for playing the given sample. +---@field new (fun(path: string): pd_sampleplayer) | (fun(sample: pd_sample): pd_sampleplayer) + +---@class pd_sampleplayer +--- Returns a new playdate.sound.sampleplayer with the same sample, volume, and rate as the given sampleplayer. +---@field copy fun(self: pd_sampleplayer): pd_sampleplayer +--- Starts playing the sample. If `repeatCount` is greater than one, it loops the given number of times. If zero, it loops endlessly until it is stopped with playdate.sound.sampleplayer:stop(). If `rate` is set, the sample will be played at the given rate instead of the rate previous set with playdate.sound.sampleplayer.setRate(). +---@field play fun(self: pd_sampleplayer, repeatCount?: number, rate?: number) +--- Schedules the sound for playing at device time `when`. If `vol` is specified, the sample will be played at level `vol` (with optional separate right channel volume `rightvol`), otherwise it plays at the volume set by playdate.sound.sampleplayer.setVolume(). Note that the `when` argument is an offset in the audio device’s time scale, as returned by playdate.sound.getCurrentTime(); it is not relative to the current time! If `when` is less than the current audio time, the sample is played immediately. If `rate` is set, the sample will be played at the given rate instead of the rate previously set with playdate.sound.sampleplayer.setRate(). +--- +--- Only one event can be queued at a time. If playAt() is called while another event is queued, it will overwrite it with the new values. +--- +--- The function returns true if the sample was successfully added to the sound channel, otherwise false (i.e., if the channel is full). +---@field playAt fun(self: pd_sampleplayer, when: number, vol?: number, rightvol?: number, rate?: number): boolean +--- Sets the playback volume (0.0 - 1.0) for left and right channels. If the optional `right` argument is omitted, it is the same as `left`. If the sampleplayer is currently playing using the default volume (that is, it wasn’t triggered by playAt() with a volume given) it also changes the volume of the playing sample. +---@field setVolume fun(self: pd_sampleplayer, left: number, right?: number) +--- Returns the playback volume for the sampleplayer, a single value for mono sources or a pair of values (left, right) for stereo sources. +---@field getVolume fun(self: pd_sampleplayer): number +--- Sets a function to be called every time the sample loops. The sample object is passed to this function as the first argument, and the optional `arg` argument is passed as the second. +---@field setLoopCallback fun(self: pd_sampleplayer, callback: fun(pd_sample, any), arg?: any) +--- Sets the range of the sample to play. `start` and `end` are frame offsets from the beginning of the sample. +---@field setPlayRange fun(self: pd_sampleplayer, start: number, end: number) +--- Pauses or resumes playback. +---@field setPaused fun(self: pd_sampleplayer, flag: boolean) +--- Returns a boolean indicating whether the sample is playing. +---@field isPlaying fun(self: pd_sampleplayer): boolean +--- Stops playing the sample. +---@field stop fun(self: pd_sampleplayer) +--- Sets a function to be called when playback has completed. The sample object is passed to this function as the first argument, and the optional `arg` argument is passed as the second. +---@field setFinishCallback fun(self: pd_sampleplayer, func: fun(pd_sample, any), arg?: any) +--- Sets the sample to be played. +---@field setSample fun(self: pd_sampleplayer, sample: pd_sample) +--- Gets the sample to be played. +---@field getSample fun(self: pd_sampleplayer): pd_sample +--- Returns the length of the sampleplayer’s sample, in seconds. Length is not scaled by playback rate. +---@field getLength fun(self: pd_sampleplayer): number +--- Sets the playback rate for the sample. 1.0 is normal speed, 0.5 is down an octave, 2.0 is up an octave, etc. Sampleplayers can also play samples backwards, by setting a negative rate; note, however, this does not work with ADPCM-encoded files. +---@field setRate fun(self: pd_sampleplayer, rate: number) +--- Returns the playback rate for the sample. +---@field getRate fun(self: pd_sampleplayer): number +--- Sets the signal to use as a rate modulator, added to the rate set with playdate.sound.sampleplayer:setRate(). Set to `nil` to clear the modulator. +---@field setRateMod fun(self: pd_sampleplayer, signal: pd_signal?) +--- Sets the current offset of the sampleplayer, in seconds. This value is not adjusted for rate. +---@field setOffset fun(self: pd_sampleplayer, seconds: number) +--- Returns the current offset of the sampleplayer, in seconds. This value is not adjusted for rate. +---@field getOffset fun(self: pd_sampleplayer): number + +---@class pd_fileplayer_lib : pd_fileplayer +--- ### Overload 1 ### +--- Returns a fileplayer object, which can stream samples from disk. The file to play is set with the playdate.sound.fileplayer:load() function. +--- +--- If given, `buffersize` specifies the size in seconds of the fileplayer’s data buffer. A shorter value reduces the latency of a playdate.sound.fileplayer:setOffset() call, but increases the chance of a buffer underrun. +--- +--- ### Overload 2 ### +--- Returns a fileplayer object for streaming samples from the file at `path`. Note that the file isn’t loaded until playdate.sound.fileplayer:play() or playdate.sound.fileplayer:setBufferSize() is called, in order to reduce initialization overhead. +--- +--- If given, `buffersize` specifies the size in seconds of the fileplayer’s data buffer. A shorter value reduces the latency of a playdate.sound.fileplayer:setOffset() call, but increases the chance of a buffer underrun. +---@field new (fun(buffersize?: number): pd_fileplayer) | (fun(path: string, buffersize?: number): pd_fileplayer) + +---@class pd_fileplayer +--- Instructs the fileplayer to load the file at `path` when play() is called on it. The fileplayer must not be playing when this function is called. The fileplayer’s play offset is reset to the beginning of the file, and its loop range is cleared. +---@field load fun(self: pd_fileplayer, path: string) +--- Opens and starts playing the file, first creating and filling a 1/4 second playback buffer if a buffer size hasn’t been set yet. +--- +--- If repeatCount is set, playback repeats when it reaches the end of the file or the end of the loop range if one is set. After the loop has run `repeatCount` times, it continues playing to the end of the file. A `repeatCount` of zero loops endlessly. If repeatCount is not set, the file plays once. +--- +--- The function returns true if the file was successfully opened and the fileplayer added to the sound channel, otherwise false and a string describing the error. +---@field play fun(self: pd_fileplayer, repeatCount?: number): boolean +--- Stops playing the file, resets the playback offset to zero, and calls the finish callback. +---@field stop fun(self: pd_fileplayer) +--- Stops playing the file. A subsequent play() call resumes playback from where it was paused. +---@field pause fun(self: pd_fileplayer) +--- Returns a boolean indicating whether the fileplayer is playing. +---@field isPlaying fun(self: pd_fileplayer): boolean +--- Returns the length, in seconds, of the audio file. +---@field getLength fun(self: pd_fileplayer): number +--- Sets a function to be called when playback has completed. The fileplayer is passed as the first argument to `func`. The optional argument `arg` is passed as the second. +---@field setFinishCallback fun(self: pd_fileplayer, func: fun(pd_fileplayer, any), arg?: any) +--- Returns the fileplayer’s underrun flag, indicating that the player ran out of data. This can be checked in the finish callback function to check for an underrun error. +---@field didUnderrun fun(self: pd_fileplayer): boolean +--- By default, the fileplayer stops playback if it can’t provide data fast enough. Setting the flag to `false` tells the fileplayer to restart playback (after an audible stutter) as soon as data is available. +---@field setStopOnUnderrun fun(self: pd_fileplayer, flag: boolean) +--- Provides a way to loop a portion of an audio file. In the following code: +--- +--- local fp = playdate.sound.fileplayer.new( "myaudiofile" ) +--- fp:setLoopRange( 10, 20 ) +--- fp:play( 3 ) +--- +--- …the fileplayer will start playing from the beginning of the audio file, loop the 10-20 second range three times, and then stop playing. +--- +--- `start` and `end` are specified in seconds. If `end` is omitted, the end of the file is used. If the function `loopCallback` is provided, it is called every time the player loops, with the fileplayer as the first argument and the optional `arg` argument as the second. +--- +--- The fileplayer:play([repeatCount]) call needs to be invoked with a `repeatCount` value of 0 (infinite looping), or 2 or greater in order for the looping action to happen. +---@field setLoopRange fun(self: pd_fileplayer, start: number, end?: number, loopCallback?: fun(pd_fileplayer, any), arg?: any) +--- Sets a function to be called every time the fileplayer loops. The fileplayer object is passed to this function as the first argument, and `arg` as the second. +--- +--- The fileplayer:play([repeatCount]) call needs to be invoked with a `repeatCount` value of 0 (infinite looping), or 2 or greater in order for the loop callback to be invoked. +---@field setLoopCallback fun(self: pd_fileplayer, callback: fun(pd_fileplayer, any), arg?: any) +--- Sets the buffer size for the fileplayer, in seconds. Larger buffers protect against buffer underruns, but consume more memory. Calling this function also fills the output buffer if a source file has been set. On success, the function returns `true`; otherwise it returns `false` and a string describing the error. +---@field setBufferSize fun(self: pd_fileplayer, seconds: number): boolean +--- Sets the playback rate for the file. 1.0 is normal speed, 0.5 is down an octave, 2.0 is up an octave, etc. Unlike sampleplayers, fileplayers can’t play in reverse (i.e., rate < 0). +---@field setRate fun(self: pd_fileplayer, rate: number) +--- Returns the playback rate for the file. as set with setRate(). +---@field getRate fun(self: pd_fileplayer): number +--- Sets the signal to use as a rate modulator, added to the rate set with playdate.sound.fileplayer:setRate(). Set to `nil` to clear the modulator. +---@field setRateMod fun(self: pd_fileplayer, signal: pd_signal?) +--- Sets the playback volume (0.0 - 1.0). If a single value is passed in, both left side and right side volume are set to the given value. If two values are given, volumes are set separately. The optional `fadeSeconds` specifies the time it takes to fade from the current volume to the specified volume, in seconds. If the function `fadeCallback` is given, it is called when the volume fade has completed. The fileplayer object is passed as the first argument to the callback, and the optional `arg` argument is passed as the second. +---@field setVolume fun(self: pd_fileplayer, left: number, right?: number, fadeSeconds?: number, fadeCallback?: fun(pd_fileplayer, any), arg?: any) +--- Returns the current volume for the fileplayer, a single value for mono sources or a pair of values (left, right) for stereo sources. +---@field getVolume fun(self: pd_fileplayer): number +--- Sets the current offset of the fileplayer, in seconds. This value is not adjusted for rate. +---@field setOffset fun(self: pd_fileplayer, seconds: number) +--- Returns the current offset of the fileplayer, in seconds. This value is not adjusted for rate. +---@field getOffset fun(self: pd_fileplayer): number + +---@class pd_sample_lib : pd_sample +--- ### Overload 1 ### +--- Returns a new playdate.sound.sample object, with the sound data loaded in memory. If the sample can’t be loaded, the function returns nil and a second value containing the error. +--- +--- ### Overload 2 ### +--- Returns a new playdate.sound.sample object, with a buffer size of `seconds` in the given format. If `format` is not specified, it defaults to playdate.sound.kFormat16bitStereo. When used with playdate.sound.sample:load(), this allows you to swap in a different sample without re-allocating the buffer, which could lead to memory fragmentation. +---@field new (fun(path: string): pd_sample) | (fun(seconds: number, format?: pd_sound_format): pd_sample) + +---@class pd_sample +--- Returns a new subsample containing a subrange of the given sample. Offset values are in frames, not bytes. +---@field getSubsample fun(self: pd_sample, startOffset: number, endOffset: number): pd_sample +--- Loads the sound data from the file at `path` into an existing sample buffer. If there is no file at `path`, the function returns nil. +---@field load fun(self: pd_sample, path: string): pd_UNDOCUMENTED? +--- If the sample is ADPCM compressed, decompresses the sample data to 16-bit PCM data. This increases the sample’s memory footprint by 4x and does not affect the quality in any way, but it is necessary if you want to use the sample in a synth or play the file backwards. Returns true if successful, or false and an error message as a second return value if decompression failed. +---@field decompress fun(self: pd_sample): boolean +--- Returns the sample rate as an integer, such as 44100 or 22050. +---@field getSampleRate fun(self: pd_sample): number +--- Returns the format of the sample, one of +--- +--- `playdate.sound.kFormat8bitMono` +--- +--- `playdate.sound.kFormat8bitStereo` +--- +--- `playdate.sound.kFormat16bitMono` +--- +--- `playdate.sound.kFormat16bitStereo` +---@field getFormat fun(self: pd_sample): pd_sound_format +--- Returns two values, the length of the available sample data and the size of the allocated buffer. Both values are measured in seconds. For a sample loaded from disk, these will be the same; for a sample used for recording, the available data may be less than the allocated size. +---@field getLength fun(self: pd_sample): (number, number) +--- Convenience function: Creates a new sampleplayer for the sample and passes the function arguments to its play function. +---@field play fun(self: pd_sample, repeatCount?: number, rate?: number) +--- Convenience function: Creates a new sampleplayer for the sample and passes the function arguments to its playAt function. +---@field playAt fun(self: pd_sample, when: number, vol?: number, rightvol?: number, rate?: number) +--- Saves the sample to the given file. If filename has a .wav extension it will be saved in WAV format (and be unreadable by the Playdate sound functions), otherwise it will be saved in the Playdate pda format. +---@field save fun(self: pd_sample, filename: string) + +---@class pd_channel_lib : pd_channel +--- Returns a new channel object and adds it to the global list. +---@field new fun(): pd_channel + +---@class pd_channel +--- Removes the channel from the global list. +---@field remove fun(self: pd_channel) +--- Adds an effect to the channel. +---@field addEffect fun(self: pd_channel, effect: pd_effect) +--- Removes an effect from the channel. +---@field removeEffect fun(self: pd_channel, effect: pd_effect) +--- Adds a source to the channel. If a source is not assigned to a channel, it plays on the default global channel. +---@field addSource fun(self: pd_channel, source: pd_source) +--- Removes a source from the channel. +---@field removeSource fun(self: pd_channel, source: pd_source) +--- Sets the volume (0.0 - 1.0) for the channel. +---@field setVolume fun(self: pd_channel, volume: number) +--- Gets the volume (0.0 - 1.0) for the channel. +---@field getVolume fun(self: pd_channel): number +--- Sets the pan parameter for the channel. -1 is left, 0 is center, and 1 is right. +---@field setPan fun(self: pd_channel, pan: number) +--- Sets a signal to automate the pan parameter. Set to `nil` to clear the modulator. +---@field setPanMod fun(self: pd_channel, signal: pd_signal?) +--- Sets a signal to automate the volume parameter. Set to `nil` to clear the modulator. +---@field setVolumeMod fun(self: pd_channel, signal: pd_signal?) +--- Returns a signal that follows the volume of the channel before effects are applied. +---@field getDryLevelSignal fun(self: pd_channel): pd_signal +--- Returns a signal that follows the volume of the channel after effects are applied. +---@field getWetLevelSignal fun(self: pd_channel): pd_signal + +---@class pd_synth_lib : pd_synth +--- ### Overload 1 ### +--- Returns a new synth object to play a waveform or wavetable. See playdate.sound.synth:setWaveform for waveform values. +--- +--- ### Overload 2 ### +--- Returns a new synth object to play a Sample. Sample data must be uncompressed PCM, not ADPCM. An optional sustain region (measured in sample frames) defines a loop to play while the note is active. When the note ends, if an envelope has been set on the synth and the sustain range goes to the end of the sample (i.e. there’s no release section of the sample after the sustain range) then the sustain section continues looping during the envelope release; otherwise it plays through the end of the sample and stops. As a convenience, if sustainStart is greater than zero and sustainEnd isn’t given, it will be set to the length of the sample. +---@field new (fun(waveform?: pd_waveform): pd_synth) | (fun(sample: pd_sample, sustainStart?: number?, sustainEnd?: number?): pd_synth) + +---@class pd_synth +--- Returns a copy of the given synth. +---@field copy fun(self: pd_synth): pd_synth +--- Plays a note with the current waveform or sample. +--- +--- `pitch`: the pitch value is in Hertz. If a sample is playing, pitch=261.63 (C4) plays at normal speed +--- +--- in either function, a string like Db3 can be used instead of a number +--- +--- `volume`: 0 to 1, defaults to 1 +--- +--- `length`: in seconds. If omitted, note will play until you call noteOff() +--- +--- `when`: seconds since the sound engine started (see playdate.sound.getCurrentTime). Defaults to the current time. +--- +--- The function returns true if the synth was successfully added to the sound channel, otherwise false (i.e., if the channel is full). +--- +--- If `pitch` is zero, this function calls noteOff() instead of potentially adding a non-zero sample, or DC offset, to the output. +--- +--- Synths currently only have a buffer of one note event. If you call `playNote()` while another note is waiting to play, it will replace that note. To create a sequence of notes to play over a period of time, see playdate.sound.sequence. +---@field playNote fun(self: pd_synth, pitch: number, volume?: number, length?: number, when?: number): boolean +--- Identical to playNote but uses a note name like "C4", or MIDI note number (60=C4, 61=C#4, etc.). In the latter case, fractional values are allowed. +---@field playMIDINote fun(self: pd_synth, note: string|number, volume?: number, length?: number, when?: number) +--- Releases the note, if one is playing. The note will continue to be voiced through the release section of the synth’s envelope. +---@field noteOff fun(self: pd_synth) +--- Stops the synth immediately, without playing the release part of the envelope. +---@field stop fun(self: pd_synth) +--- Returns true if the synth is still playing, including the release phase of the envelope. +---@field isPlaying fun(self: pd_synth): boolean +--- Sets the signal to use as the amplitude modulator. Set to `nil` to clear the modulator. +---@field setAmplitudeMod fun(self: pd_synth, signal: pd_signal?) +--- Sets the attack time, decay time, sustain level, and release time for the sound envelope, and optionally the curvature. +---@field setADSR fun(self: pd_synth, attack: number, decay: number, sustain: number, release: number) +--- Sets the attack time, in seconds. +---@field setAttack fun(self: pd_synth, time: number) +--- Sets the decay time, in seconds. +---@field setDecay fun(self: pd_synth, time: number) +--- Sets the sustain level, as a proportion of the total level (0.0 to 1.0). +---@field setSustain fun(self: pd_synth, level: number) +--- Sets the release time, in seconds. +---@field setRelease fun(self: pd_synth, time: number) +--- Clears the synth’s envelope settings. +---@field clearEnvelope fun(self: pd_synth) +--- Smoothly changes the envelope’s shape from linear (amount=0) to exponential (amount=1). +---@field setEnvelopeCurvature fun(self: pd_synth, amount: number) +--- Returns the synth’s envelope as a playdate.sound.envelope object. +---@field getEnvelope fun(self: pd_synth): pd_envelope +--- Sets a function to be called when the synth stops playing. +---@field setFinishCallback fun(self: pd_synth, function: fun()) +--- Sets the signal to use as the frequency modulator. Set to `nil` to clear the modulator. +---@field setFrequencyMod fun(self: pd_synth, signal: pd_signal?) +--- Sets whether to use legato phrasing for the synth. If the legato flag is set and a new note starts while a previous note is still playing, the synth’s envelope remains in the sustain phase instead of starting a new attack. +---@field setLegato fun(self: pd_synth, flag: boolean) +--- Sets the synth volume. If a single value is passed in, sets both left side and right side volume to the given value. If two values are given, volumes are set separately. +--- +--- Volume values are between 0.0 and 1.0. +---@field setVolume fun(self: pd_synth, left: number, right?: number) +--- Returns the current volume for the synth, a single value for mono sources or a pair of values (left, right) for stereo sources. +--- +--- Volume values are between 0.0 and 1.0. +---@field getVolume fun(self: pd_synth): number +--- Sets the waveform or Sample the synth plays. If a sample is given, its data must be uncompressed PCM, not ADPCM. Otherwise `waveform` should be one of the following constants: +--- +--- `playdate.sound.kWaveSine` +--- +--- `playdate.sound.kWaveSquare` +--- +--- `playdate.sound.kWaveSawtooth` +--- +--- `playdate.sound.kWaveTriangle` +--- +--- `playdate.sound.kWaveNoise` +--- +--- `playdate.sound.kWavePOPhase` +--- +--- `playdate.sound.kWavePODigital` +--- +--- `playdate.sound.kWavePOVosim` +---@field setWaveform fun(self: pd_synth, waveform: pd_waveform|pd_sample) +--- Sets a wavetable for the synth to play. Sample data must be 16-bit mono uncompressed. samplesize is the number of samples in each waveform "cell" in the table and must be a power of 2. xsize is the number of cells across the wavetable. If the wavetable is two-dimensional, ysize gives the number of cells in the y direction. +--- +--- The synth’s "position" in the wavetable is set manually with setParameter() or automated with setParameterModulator(). In some cases it’s easier to use a parameter that matches the waveform position in the table, in others (notably when using envelopes and lfos) it’s more convenient to use a 0-1 scale, so there’s some redundancy here. Parameters are +--- +--- 1: x position, values are from 0 to the table width +--- +--- 2: x position, values are from 0 to 1, parameter is scaled up to table width +--- +--- For 2-D tables (rowwidth > 0): +--- +--- 3: y position, values are from 0 to the table height +--- +--- 4: y position, values are from 0 to 1, parameter is scaled up to table height +---@field setWavetable fun(self: pd_synth, sample: pd_sample, samplesize: integer, xsize: integer, ysize?: integer) +--- Sets the parameter at (1-based) position `num` to the given value. Unless otherwise specified, `value` ranges from 0 to 1. +---@field setParameter fun(self: pd_synth, parameter: integer, value: number) +--- Sets the signal to modulate the parameter. Set to `nil` to clear the modulator. +---@field setParameterMod fun(self: pd_synth, parameter: number, signal: pd_signal?) + +---@class pd_signal_lib : pd_signal + +---@class pd_signal +--- Adds a constant offset to the signal (lfo, envelope, etc.). +---@field setOffset fun(self: pd_signal, offset: number) +--- Multiplies the signal’s output by the given scale factor. The scale is applied before the offset. +---@field setScale fun(self: pd_signal, scale: number) +--- Returns the current output value of the signal. +---@field getValue fun(self: pd_signal): number + +---@class pd_lfo_lib : pd_lfo +--- Returns a new LFO object, which can be used to modulate sounds. See playdate.sound.lfo:setType() for LFO types. +---@field new fun(type?: pd_lfo_type): pd_lfo + +---@class pd_lfo : pd_signal +--- Sets the waveform of the LFO. Valid values are +--- +--- `playdate.sound.kLFOSquare` +--- +--- `playdate.sound.kLFOSawtoothUp` +--- +--- `playdate.sound.kLFOSawtoothDown` +--- +--- `playdate.sound.kLFOTriangle` +--- +--- `playdate.sound.kLFOSine` +--- +--- `playdate.sound.kLFOSampleAndHold` +---@field setType fun(self: pd_lfo, type: pd_lfo_type) +--- Sets the LFO type to arpeggio, where the given values are in half-steps from the center note. For example, the sequence (0, 4, 7, 12) plays the notes of a major chord. +---@field setArpeggio fun(self: pd_lfo, note1: number, ...: number) +--- Sets the center value of the LFO. +---@field setCenter fun(self: pd_lfo, center: number) +--- Sets the depth of the LFO’s modulation. +---@field setDepth fun(self: pd_lfo, depth: number) +--- Sets the rate of the LFO, in cycles per second. +---@field setRate fun(self: pd_lfo, rate: number) +--- Sets the current phase of the LFO, from 0 to 1. +---@field setPhase fun(self: pd_lfo, phase: number) +--- Sets the initial phase of the LFO, from 0 to 1. +---@field setStartPhase fun(self: pd_lfo, phase: number) +--- If an LFO is marked global, it is continuously updated whether or not it’s attached to any source. +---@field setGlobal fun(self: pd_lfo, flag: boolean) +--- If retrigger is on, the LFO’s phase is reset to its initial phase (default 0) when a synth using the LFO starts playing a note. +---@field setRetrigger fun(self: pd_lfo, flag: boolean) +--- Sets an initial holdoff time for the LFO where the LFO remains at its center value, and a ramp time where the value increases linearly to its maximum depth. Values are in seconds. +---@field setDelay fun(self: pd_lfo, holdoff: number, ramp: number) +--- Returns the current signal value of the LFO. +---@field getValue fun(self: pd_lfo): number + +---@class pd_envelope_lib : pd_envelope +--- Creates a new envelope with the given (optional) parameters. +---@field new fun(attack?: number, decay?: number, sustain?: number, release?: number): pd_envelope + +---@class pd_envelope : pd_signal +--- Sets the envelope attack time to `attack`, in seconds. +---@field setAttack fun(self: pd_envelope, attack: number) +--- Sets the envelope decay time to `decay`, in seconds. +---@field setDecay fun(self: pd_envelope, decay: number) +--- Sets the envelope sustain level to `sustain`, as a proportion of the maximum. For example, if the sustain level is 0.5, the signal value rises to its full value over the attack phase of the envelope, then drops to half its maximum over the decay phase, and remains there while the envelope is active. +---@field setSustain fun(self: pd_envelope, sustain: number) +--- Sets the envelope release time to `release`, in seconds. +---@field setRelease fun(self: pd_envelope, release: number) +--- Smoothly changes the envelope’s shape from linear (amount=0) to exponential (amount=1). +---@field setCurvature fun(self: pd_envelope, amount: number) +--- Changes the amount by which note velocity scales output level. At the default value of 1, output is proportional to velocity; at 0 velocity has no effect on output level. +---@field setVelocitySensitivity fun(self: pd_envelope, amount: number) +--- Scales the envelope rate according to the played note. For notes below start, the envelope’s set rate is used; for notes above end envelope rates are scaled by the scaling parameter. Between the two notes the scaling factor is interpolated from 1.0 to scaling. start and end are either MIDI note numbers or names like "C4". If omitted, the default range is C1 (36) to C5 (84). +---@field setRateScaling fun(self: pd_envelope, scaling: number, start?: number|string, end?: number|string) +--- Sets the scale value for the envelope. The transformed envelope has an initial value of `offset` and a maximum (minimum if `scale` is negative) of `offset` + `scale`. +---@field setScale fun(self: pd_envelope, scale: number) +--- Sets the offset value for the envelope. The transformed envelope has an initial value of `offset` and a maximum (minimum if `scale` is negative) of `offset` + `scale`. +---@field setOffset fun(self: pd_envelope, offset: number) +--- Sets whether to use legato phrasing for the envelope. If the legato flag is set, when the envelope is re-triggered before it’s released, it remains in the sustain phase instead of jumping back to the attack phase. +---@field setLegato fun(self: pd_envelope, flag: boolean) +--- If retrigger is on, the envelope always starts from 0 when a note starts playing, instead of the current value if it’s active. +---@field setRetrigger fun(self: pd_envelope, flag: boolean) +--- Triggers the envelope at the given `velocity`. If a `length` parameter is given, the envelope moves to the release phase after the given time. Otherwise, the envelope is held in the sustain phase until the trigger function is called again with `velocity` equal to zero. +---@field trigger fun(self: pd_envelope, velocity: number, length?: number) +--- If an envelope is marked global, it is continuously updated whether or not it’s attached to any source. +---@field setGlobal fun(self: pd_envelope, flag: boolean) +--- Returns the current signal value of the envelope. +---@field getValue fun(self: pd_envelope): number + +---@class pd_bitcrusher_lib : pd_bitcrusher +--- Creates a new bitcrusher filter. +---@field new fun(): pd_bitcrusher + +---@class pd_bitcrusher +--- Sets the wet/dry mix for the effect. A level of 1 (full wet) replaces the input with the effect output; 0 leaves the effect out of the mix. +---@field setMix fun(self: pd_bitcrusher, level: number) +--- Sets a signal to modulate the mix level. Set to `nil` to clear the modulator. +---@field setMixMod fun(self: pd_bitcrusher, signal: pd_signal?) +--- Sets the amount of crushing to `amt`. Valid values are 0 (no effect) to 1 (quantizing output to 1-bit). +---@field setAmount fun(self: pd_bitcrusher, amt: number) +--- Sets a signal to modulate the filter level. Set to `nil` to clear the modulator. +---@field setAmountMod fun(self: pd_bitcrusher, signal: pd_signal?) +--- Sets the number of samples to repeat; 0 is no undersampling, 1 effectively halves the sample rate. +---@field setUndersampling fun(self: pd_bitcrusher, amt: number) +--- Sets a signal to modulate the filter level. Set to `nil` to clear the modulator. +---@field setUndersamplingMod fun(self: pd_bitcrusher, signal: pd_signal?) + +---@class pd_ringmod_lib : pd_ringmod +--- Creates a new ring modulator filter. +---@field new fun(): pd_ringmod + +---@class pd_ringmod +--- Sets the wet/dry mix for the effect. A level of 1 (full wet) replaces the input with the effect output; 0 leaves the effect out of the mix. +---@field setMix fun(self: pd_ringmod, level: number) +--- Sets a signal to modulate the mix level. Set to `nil` to clear the modulator. +---@field setMixMod fun(self: pd_ringmod, signal: pd_signal?) +--- Sets the ringmod frequency to `f`. +---@field setFrequency fun(self: pd_ringmod, f: number) +--- Sets a signal to modulate the ringmod frequency. Set to `nil` to clear the modulator. +---@field setFrequencyMod fun(self: pd_ringmod, signal: pd_signal?) + +---@class pd_onepolefilter_lib : pd_onepolefilter +--- Returns a new one pole filter. +---@field new fun(): pd_onepolefilter + +---@class pd_onepolefilter +--- Sets the wet/dry mix for the effect. A level of 1 (full wet) replaces the input with the effect output; 0 leaves the effect out of the mix. +---@field setMix fun(self: pd_onepolefilter, level: number) +--- Sets a signal to modulate the mix level. Set to `nil` to clear the modulator. +---@field setMixMod fun(self: pd_onepolefilter, signal: pd_signal?) +--- Sets the filter’s single parameter (cutoff frequency) to `p`. +---@field setParameter fun(self: pd_onepolefilter, p: number) +--- Sets a modulator for the filter’s parameter. Set to `nil` to clear the modulator. +---@field setParameterMod fun(self: pd_onepolefilter, m: pd_signal) + +---@class pd_twopolefilter_lib : pd_twopolefilter +--- Creates a new two pole IIR filter of the given `type`: +--- +--- `playdate.sound.kFilterLowPass` (or the string "lowpass" or "lopass") +--- +--- `playdate.sound.kFilterHighPass` (or "highpass" or "hipass") +--- +--- `playdate.sound.kFilterBandPass` (or "bandpass") +--- +--- `playdate.sound.kFilterNotch` (or "notch") +--- +--- `playdate.sound.kFilterPEQ` (or "peq") +--- +--- `playdate.sound.kFilterLowShelf` (or "lowshelf" or "loshelf") +--- +--- `playdate.sound.kFilterHighShelf` (or "highshelf" or "hishelf") +---@field new fun(type: pd_sound_filter): pd_twopolefilter + +---@class pd_twopolefilter +--- Sets the wet/dry mix for the effect. A level of 1 (full wet) replaces the input with the effect output; 0 leaves the effect out of the mix. +---@field setMix fun(self: pd_twopolefilter, level: number) +--- Sets a signal to modulate the mix level. Set to `nil` to clear the modulator. +---@field setMixMod fun(self: pd_twopolefilter, signal: pd_signal?) +--- Sets the center frequency (in Hz) of the filter to `f`. +---@field setFrequency fun(self: pd_twopolefilter, f: number) +--- Sets a signal to modulate the filter frequency. Set to `nil` to clear the modulator. +---@field setFrequencyMod fun(self: pd_twopolefilter, signal: pd_signal?) +--- Sets the resonance of the filter to `r`. Valid values are in the range 0-1. This parameter has no effect on shelf type filters. +---@field setResonance fun(self: pd_twopolefilter, r: number) +--- Sets a signal to modulate the filter resonance. Set to `nil` to clear the modulator. +---@field setResonanceMod fun(self: pd_twopolefilter, signal: pd_signal?) +--- Sets the gain of the filter to `g`. Gain is only used in PEQ and shelf type filters. +---@field setGain fun(self: pd_twopolefilter, g: number) +--- Sets the type of the filter to `type`. +---@field setType fun(self: pd_twopolefilter, type: pd_sound_filter) + +---@class pd_overdrive_lib : pd_overdrive +--- Creates a new overdrive effect. +---@field new fun(): pd_overdrive + +---@class pd_overdrive +--- Sets the wet/dry mix for the effect. A level of 1 (full wet) replaces the input with the effect output; 0 leaves the effect out of the mix. +---@field setMix fun(self: pd_overdrive, level: number) +--- Sets a signal to modulate the mix level. Set to `nil` to clear the modulator. +---@field setMixMod fun(self: pd_overdrive, signal: pd_signal?) +--- Sets the gain of the filter. +---@field setGain fun(self: pd_overdrive, level: number) +--- Sets the level where the amplified input clips. +---@field setLimit fun(self: pd_overdrive, level: number) +--- Sets a signal to modulate the limit level. Set to `nil` to clear the modulator. +---@field setLimitMod fun(self: pd_overdrive, signal: pd_signal?) +--- Adds an offset to the upper and lower limits to create an asymmetric clipping. +---@field setOffset fun(self: pd_overdrive, level: number) +--- Sets a signal to modulate the offset value. Set to `nil` to clear the modulator. +---@field setOffsetMod fun(self: pd_overdrive, signal: pd_signal?) + +---@class pd_delayline_lib : pd_delayline +--- Creates a new delay line effect, with the given length (in seconds). +---@field new fun(length: number): pd_delayline + +---@class pd_delayline +--- Sets the wet/dry mix for the effect. A level of 1 (full wet) replaces the input with the effect output; 0 leaves the effect out of the mix, which is useful if you’re using taps for varying delays. +---@field setMix fun(self: pd_delayline, level: number) +--- Sets a signal to modulate the mix level. Set to `nil` to clear the modulator. +---@field setMixMod fun(self: pd_delayline, signal: pd_signal?) +--- Returns a new playdate.sound.delaylinetap on the delay line, at the given delay (which must be less than or equal to the delay line’s length). +---@field addTap fun(self: pd_delayline, delay: number): pd_delayline +--- Sets the feedback level of the delay line. +---@field setFeedback fun(self: pd_delayline, level: number) + +---@class pd_delaylinetap_lib : pd_delaylinetap + +---@class pd_delaylinetap +--- Sets the position of the tap on the delay line, up to the delay line’s length. +---@field setDelay fun(self: pd_delaylinetap, time: number) +--- Sets a signal to modulate the tap delay. If the signal is continuous (e.g. an envelope or a triangle LFO, but not a square LFO) playback is sped up or slowed down to compress or expand time. Set to `nil` to clear the modulator. +---@field setDelayMod fun(self: pd_delaylinetap, signal: pd_signal?) +--- Sets the tap’s volume. +---@field setVolume fun(self: pd_delaylinetap, level: number) +--- Returns the tap’s volume. +---@field getVolume fun(self: pd_delaylinetap): number +--- If set and the delay line is stereo, the tap outputs the delay line’s left channel to its right output and vice versa. +---@field setFlipChannels fun(self: pd_delaylinetap, flag: boolean) + +---@class pd_sequence_lib : pd_sequence +--- Creates a new sound sequence. If midi_path is given, it attempts to load data from the midi file into the sequence. +---@field new fun(midi_path?: string): pd_sequence + +---@class pd_sequence +--- Starts playing the sequence. finishCallback is an optional function to be called when the sequence finishes playing or is stopped. The sequence is passed to the callback as its single argument. +---@field play fun(self: pd_sequence, finishCallback?: fun(pd_sequence)) +--- Stops playing the sequence. +---@field stop fun(self: pd_sequence) +--- Returns true if the sequence is currently playing. +---@field isPlaying fun(self: pd_sequence): boolean +--- Returns the length of the longest track in the sequence, in steps. See also playdate.sound.track.getLength(). +---@field getLength fun(self: pd_sequence): number +--- Moves the play position for the sequence to step number step. If play is set, triggers the notes at that step. +---@field goToStep fun(self: pd_sequence, step: integer, play?: boolean) +--- Returns the step number the sequence is currently at. +---@field getCurrentStep fun(self: pd_sequence): integer +--- Sets the tempo of the sequence, in steps per second. +---@field setTempo fun(self: pd_sequence, stepsPerSecond: number) +--- Returns the tempo of the sequence, in steps per second. +---@field getTempo fun(self: pd_sequence): number +--- ### Overload 1 ### +--- Sets the looping range of the sequence. If `loops` is 0 or unset, the loop repeats endlessly. +--- +--- ### Overload 2 ### +--- Same as above, with startStep set to 0 and endStep set to sequence:getLength(). +---@field setLoops (fun(self: pd_sequence, startStep: integer, endStep: integer, loopCount?: number)) | (fun(self: pd_sequence, loopCount: number)) +--- Returns the number of tracks in the sequence. +---@field getTrackCount fun(self: pd_sequence): number +--- Adds the given playdate.sound.track to the sequence. If track omitted, the function creates and returns a new track. +---@field addTrack fun(self: pd_sequence, track?: pd_track): pd_track? +--- Sets the given playdate.sound.track object at position n in the sequence. +---@field setTrackAtIndex fun(self: pd_sequence, n: integer, track: pd_track) +--- Returns the playdate.sound.track object at position n in the sequence. +---@field getTrackAtIndex fun(self: pd_sequence, n: integer): pd_track +--- Sends an allNotesOff() message to each track’s instrument. +---@field allNotesOff fun(self: pd_sequence) + +---@class pd_track_lib : pd_track +--- Creates a new playdate.sound.track object. +---@field new fun(): pd_track + +---@class pd_track +--- Adds a single note event to the track, letting you specify step, note, length, and velocity directly. The second format allows you to pack them into a table, using the format returned by getNotes(). The note argument can be a MIDI note number or a note name like "Db3". length is the length of the note in steps, not time—​that is, it follows the sequence’s tempo. The default velocity is 1.0. +--- +--- See setNotes() for the ability to add more than one note at a time. +---@field addNote (fun(self: pd_track, step: integer, note: string|number, length: number, velocity?: number)) | (fun(self: pd_track, table: pd_note_table)) +--- Set multiple notes at once, each array element should be a table containing values for the keys The tables contain values for keys step, note, length, and velocity. +---@field setNotes fun(self: pd_track, list: pd_note_table[]) +--- Returns an array of tables representing the note events in the track. +--- +--- The tables contain values for keys step, note, length, and velocity. If step is given, the function returns only the notes at that step; if both step and endstep are set, it returns the notes between the two steps (including notes at endstep). n.b. The note field in the event tables is always a MIDI note number value, even if the note was added using the string notation. +---@field getNotes fun(self: pd_track, step?: integer, endstep?: integer): pd_note_table[] +--- Removes the note event at `step` playing `note`. +---@field removeNote fun(self: pd_track, step: integer, note: string|number) +--- Clears all notes from the track. +---@field clearNotes fun(self: pd_track) +--- Returns the length, in steps, of the track—​that is, the step where the last note in the track ends. +---@field getLength fun(self: pd_track): number +--- Returns the current number of notes active in the track. +---@field getNotesActive fun(self: pd_track): integer +--- Returns the maximum number of notes simultaneously active in the track. (Known bug: this currently only works for midi files) +---@field getPolyphony fun(self: pd_track): integer +--- Sets the playdate.sound.instrument that this track plays. If inst is a playdate.sound.synth, the function creates an instrument for the synth. +---@field setInstrument fun(self: pd_track, inst: pd_instrument|pd_synth) +--- Gets the playdate.sound.instrument that this track plays. +---@field getInstrument fun(self: pd_track): pd_instrument +--- Mutes or unmutes the track. +---@field setMuted fun(self: pd_track, flag: boolean) +--- Adds a playdate.sound.controlsignal object to the track. Note that the signal must be assigned to a modulation input for it to have any audible effect. The input can be anywhere in the sound engine—​it’s not required to belong to the track in any way. +---@field addControlSignal fun(self: pd_track, s: pd_controlsignal) +--- Returns an array of playdate.sound.controlsignal objects assigned to this track. +---@field getControlSignals fun(self: pd_track): pd_controlsignal[] + +---@class pd_instrument_lib : pd_instrument +--- Creates a new playdate.sound.instrument object. If synth is given, adds it as a voice for the instrument. +---@field new fun(synth?: pd_synth): pd_instrument + +---@class pd_instrument +--- Adds the given playdate.sound.synth to the instrument. If only the `note` argument is given, the voice is only used for that note, and is transposed to play at normal speed (i.e. rate=1.0 for samples, or C4 for synths). If `rangeend` is given, the voice is assigned to the range `note` to `rangeend`, inclusive, with the first note in the range transposed to rate=1.0/C4. The note and rangeend arguments can be MIDI note numbers or note names like "Db3". The final transpose argument transposes the note played, in half-tone units. +---@field addVoice fun(self: pd_instrument, v: pd_synth, note?: string|number, rangeend?: string|number, transpose?: number) +--- Sets the pitch bend to be applied to the voices in the instrument, as a fraction of the full range. +---@field setPitchBend fun(self: pd_instrument, amount: number) +--- Sets the pitch bend range for the voices in the instrument. The default range is 12, for a full octave. +---@field setPitchBendRange fun(self: pd_instrument, halfsteps: number) +--- Transposes all voices in the instrument. `halfsteps` can be a fractional value. +---@field setTranspose fun(self: pd_instrument, halfsteps: number) +--- Plays the given note on the instrument. A string like Db3 can be used instead of a pitch/note number. Fractional values are allowed. `vel` defaults to 1.0, fully on. If `length` isn’t specified, the note stays on until `instrument.noteOff(note)` is called. `when` is the number of seconds in the future to start playing the note, default is immediately. +---@field playNote fun(self: pd_instrument, frequency: number, vel?: number, length?: number, when?: number) +--- Identical to instrument:playNote() but `note` is a MIDI note number: 60=C4, 61=C#4, etc. Fractional values are allowed. +---@field playMIDINote fun(self: pd_instrument, note: string|number, vel?: number, length?: number, when?: number) +--- Stops the instrument voice playing note `note`. If `when` is given, the note is stopped `when` seconds in the future, otherwise it’s stopped immediately. +---@field noteOff fun(self: pd_instrument, note: string|number, when?: number) +--- Sends a stop signal to all playing notes. +---@field allNotesOff fun(self: pd_instrument) +--- Sets the instrument volume. If a single value is passed in, sets both left side and right side volume to the given value. If two values are given, volumes are set separately. +--- +--- Volume values are between 0.0 and 1.0. +---@field setVolume fun(self: pd_instrument, left: number, right?: number) +--- Returns the current volume for the synth, a single value for mono sources or a pair of values (left, right) for stereo sources. +--- +--- Volume values are between 0.0 and 1.0. +---@field getVolume fun(self: pd_instrument): number + +---@class pd_controlsignal_lib : pd_controlsignal +--- Creates a new control signal object, for automating effect parameters, channel pan and level, etc. +---@field new fun(): pd_controlsignal + +---@class pd_controlsignal : pd_signal +--- The signal’s event list is modified by getting and setting the events property of the object. This is an array of tables, each containing values for keys step and value, and optionally interpolate. +---@field events pd_event[] +--- addEvent is a simpler way of adding events one at a time than setting the entire `events` table. Arguments are either the values themselves in the given order, or a table containing values for step, value, and optionally interpolate. +---@field addEvent (fun(self: pd_controlsignal, step: number, value: number, interpolate?: boolean)) | (fun(self: pd_controlsignal, event: pd_event)) +--- Clears all events from the control signal. +---@field clearEvents fun(self: pd_controlsignal) +--- Sets the midi controller number for the control signal, if that’s something you want to do. The value has no effect on playback. +---@field setControllerType fun(self: pd_controlsignal, number: number) +--- Control signals in midi files are assigned a controller number, which describes the intent of the control. This function returns the controller number. +---@field getControllerType fun(self: pd_controlsignal): number +--- Returns the current output value of the control signal. +---@field getValue fun(self: pd_controlsignal): number + +---@class pd_micinput_lib +--- buffer should be a Sample created with the following code, with `secondsToRecord` replaced by a number specifying the record duration: +--- +--- local buffer = playdate.sound.sample.new(_secondsToRecord_, playdate.sound.kFormat16bitMono) +--- +--- completionCallback is a function called at the end of recording, when the buffer is full. It has one argument, the recorded sample. To override the device’s headset detection and force recording from either the internal mic or a headset mic or line in connected to a headset splitter, first call playdate.sound.micinput.startListening() with the required source. recordToSample() returns true on success, false on error. +---@field recordToSample fun(buffer: pd_sample, completionCallback: fun(pd_sample)): boolean +--- Stops a sample recording started with recordToSample, if it hasn’t already reached the end of the buffer. The recording’s completion callback is called immediately. +---@field stopRecording fun() +--- Starts monitoring the microphone input level. The optional `source` argument of "headset" or "device" causes the mic input to record from the given source. If no source is given, it uses the headset detection circuit to determine which source to use. The function returns the pair true and a string indicating which source it’s recording from on success, or false on error. +---@field startListening fun(source?: string): (boolean,string?) +--- Stops monitoring the microphone input level. +---@field stopListening fun() +--- Returns the current microphone input level, a value from 0.0 (quietest) to 1.0 (loudest). +---@field getLevel fun(): number +--- Returns the current microphone input source, either "headset" or "device". +---@field getSource fun(): string + +---@class pd_crankIndicator +--- Draws the next frame of the crank indicator animation, and is typically invoked in the playdate.update() callback. `xOffset` and `yOffset` can be used to alter the position of the indicator by a specified number of pixels if desired. To stop drawing the crank indicator, simply stop calling :draw() in playdate.update(). +--- +--- Note that if sprites are being used, this call should usually happen after playdate.graphics.sprite.update(). +---@field draw fun(self: pd_crankIndicator, xOffset?: number, yOffset?: number) +--- Boolean property specifying which direction to animate the crank. Defaults to true. +---@field clockwise boolean +--- Resets the crank animation to the beginning of its sequence. +---@field resetAnimation fun(self: pd_crankIndicator) +--- Returns `x`, `y`, `width`, `height` representing the bounds that the crank indicator draws within. If necessary, this rect could be passed into playdate.graphics.sprite.addDirtyRect(), or used to manually draw over the indicator image drawn by playdate.ui.crankIndicator:draw() when you want to stop showing the crank indicator. +---@field getBounds fun(self: pd_crankIndicator): (number, number, number, number) + +---@class pd_gridview_lib : pd_gridview +--- Returns a new playdate.ui.gridview with cells sized `cellWidth`, `cellHeight`. (Sizes are in pixels.) If cells should span the entire width of the grid (as in a list view), pass zero (0) for `cellWidth`. +---@field new fun(cellWidth: number, cellHeight: number): pd_gridview + +---@class pd_gridview +--- Override this method to draw the cells in the gridview. `selected` is a boolean, true if the cell being drawn is the currently-selected cell. +---@field drawCell fun(self: pd_gridview, section: integer, row: number, column: number, selected: boolean, x: number, y: number, width: number, height: number) +--- Override this method to draw section headers. This function will only be called if the header height has been set to a value greater than zero (0). +---@field drawSectionHeader fun(self: pd_gridview, section: integer, x: number, y: number, width: number, height: number) +--- Override this method to customize the drawing of horizontal dividers. This function will only be called if the horizontal divider height is greater than zero (0) and at least one divider has been added. +---@field drawHorizontalDivider fun(self: pd_gridview, x: number, y: number, width: number, height: number) +--- Draws the gridview in the specified rect. Ideally this should be called on every playdate.update() to accommodate scrolling. +---@field drawInRect fun(self: pd_gridview, x: number, y: number, width: number, height: number) +--- This read-only variable returns true if the gridview needs to be redrawn. This can be used to help optimize drawing in your app. Keep in mind that a gridview cannot know all reasons it may need to be redrawn, such as changes in your drawing callback functions, coordinate or size changes, or overlapping drawing, so you may need to additionally redraw at other times. +---@field needsDisplay READONLY_boolean +--- Sets the number of sections in the grid view. Each section contains at least one row, and row numbering starts at 1 in each section. +---@field setNumberOfSections fun(self: pd_gridview, num: integer) +--- Returns the number of sections in the grid view. +---@field getNumberOfSections fun(self: pd_gridview): number +--- Sets the number of rows in `section`. +---@field setNumberOfRowsInSection fun(self: pd_gridview, section: integer, num: integer) +--- Returns the number of rows in `section`. +---@field getNumberOfRowsInSection fun(self: pd_gridview, section: integer): number +--- Sets the number of columns in the gridview. 1 by default. +---@field setNumberOfColumns fun(self: pd_gridview, num: integer) +--- Returns the number of columns in the gridview. 1 by default. +---@field getNumberOfColumns fun(self: pd_gridview): number +--- Convenience method for list-style gridviews, or for setting the number of rows for multiple sections at a time. Pass in a list of numbers of rows for sections starting from section 1. +---@field setNumberOfRows fun(self: pd_gridview, count1: integer, ...: integer) +--- Sets the size of the cells in the gridview. If cells should span the entire width of the grid (as in a list view), pass zero (0) for `cellWidth`. +---@field setCellSize fun(self: pd_gridview, cellWidth: number, cellHeight: number) +--- Sets the amount of padding around cells. +---@field setCellPadding fun(self: pd_gridview, left: number, right: number, top: number, bottom: number) +--- Sets the amount of space the content is inset from the edges of the gridview. Useful if a background image is being used as a border. +---@field setContentInset fun(self: pd_gridview, left: number, right: number, top: number, bottom: number) +--- Returns multiple values (x, y, width, height) representing the bounds of the cell, not including padding, relative to the top-right corner of the grid view. +--- +--- If the grid view is configured with zero width cells (see playdate.ui.gridview:new), `gridWidth` is required, and should be the same value you would pass to playdate.ui.gridview:drawInRect. +---@field getCellBounds fun(self: pd_gridview, section: integer, row: number, column: number, gridWidth?: number): (number, number, number, number) +--- Sets the height of the section headers. 0 by default, which causes section headers not to be drawn. +---@field setSectionHeaderHeight fun(self: pd_gridview, height: number) +--- Returns the current height of the section headers. +---@field getSectionHeaderHeight fun(): number +--- Sets the amount of padding around section headers. +---@field setSectionHeaderPadding fun(self: pd_gridview, left: number, right: number, top: number, bottom: number) +--- Sets the height of the horizontal dividers. The default height is half the cell height specified when creating the grid view. +---@field setHorizontalDividerHeight fun(self: pd_gridview, height: number) +--- Returns the height of the horizontal dividers. +---@field getHorizontalDividerHeight fun(self: pd_gridview): number +--- Causes a horizontal divider to be drawn above the specified row. Drawing can be customized by overriding playdate.ui.gridview:drawHorizontalDivider. +---@field addHorizontalDividerAbove fun(self: pd_gridview, section: integer, row: number) +--- Removes all horizontal dividers from the grid view. +---@field removeHorizontalDividers fun(self: pd_gridview) +--- Controls the duration of scroll animations. 250ms by default. +---@field setScrollDuration fun(self: pd_gridview, ms: number) +--- 'set' scrolls to the coordinate `x`, `y`. +--- +--- If `animated` is true (or not provided) the new scroll position is animated to using playdate.ui.gridview.scrollEasingFunction and the value set in playdate.ui.gridview:setScrollDuration(). +---@field setScrollPosition fun(self: pd_gridview, x: number, y: number, animated?: boolean) +--- Returns the current scroll location as a pair `x`, `y`. +---@field getScrollPosition fun(self: pd_gridview): (number, number) +--- Scrolls to the specified cell, just enough so the cell is visible. +---@field scrollToCell fun(self: pd_gridview, section: integer, row: number, column: number, animated?: boolean) +--- Scrolls to the specified cell, so the cell is centered in the gridview, if possible. +---@field scrollCellToCenter fun(self: pd_gridview, section: integer, row: number, column: number, animated?: boolean) +--- Convenience function for list-style gridviews. Scrolls to the specified row in the list. +---@field scrollToRow fun(self: pd_gridview, row: number, animated?: boolean) +--- Scrolls to the top of the gridview. +---@field scrollToTop fun(self: pd_gridview, animated?: boolean) +--- Selects the cell at the given position. +---@field setSelection fun(self: pd_gridview, section: integer, row: number, column: number) +--- Returns the currently-selected cell as `section`, `row`, `column` +---@field getSelection fun(self: pd_gridview): (integer, integer, integer) +--- Convenience method for list-style gridviews. Selects the cell at `row` in section 1. +---@field setSelectedRow fun(self: pd_gridview, row: number) +--- Convenience method for list-style gridviews. Returns the selected cell at `row` in section 1. +---@field getSelectedRow fun(self: pd_gridview): integer +--- Selects the cell directly below the currently-selected cell. +--- +--- If `wrapSelection` is true, the selection will wrap around to the opposite end of the grid. If `scrollToSelection` is true (or not provided), the newly-selected cell will be scrolled to. If `animate` is true (or not provided), the scroll will be animated. +---@field selectNextRow fun(self: pd_gridview, wrapSelection: boolean, scrollToSelection?: boolean, animate?: boolean) +--- Identical to selectNextRow() but goes the other direction. +---@field selectPreviousRow fun(self: pd_gridview, wrapSelection: boolean, scrollToSelection?: boolean, animate?: boolean) +--- Selects the cell directly to the right of the currently-selected cell. +--- +--- If the last column is currently selected and `wrapSelection` is true, the selection will wrap around to the opposite side of the grid. If a wrap occurs and the gridview’s changeRowOnColumnWrap is true the row will also be advanced or moved back. +--- +--- If `scrollToSelection` is true (or not provided), the newly-selected cell will be scrolled to. If `animate` is true (or not provided), the scroll will be animated. +---@field selectNextColumn fun(self: pd_gridview, wrapSelection: boolean, scrollToSelection?: boolean, animate?: boolean) +--- Identical to selectNextColumn() but goes the other direction. +---@field selectPreviousColumn fun(self: pd_gridview, wrapSelection: boolean, scrollToSelection?: boolean, animate?: boolean) +--- A background image that draws behind the gridview’s cells. This image can be either a playdate.graphics.image which will be tiled or a playdate.nineSlice. +---@field backgroundImage pd_image +--- Read-only. True if the gridview is currently performing a scroll animation. +---@field isScrolling READONLY_boolean +--- The easing function used when performing scroll animations. The function should be of the form function(t, b, c, d), where t is elapsed time, b is the beginning value, c is the change, or end value - start value, and d is the duration. Many such functions are available in playdate.easingFunctions. playdate.easingFunctions.outCubic is the default. +---@field scrollEasingFunction (fun(number, number, number, number): number) +--- For easing functions that take additional amplitude and period arguments (such as `inOutElastic`), set these to the desired values. +---@field easingAmplitude number +--- For easing functions that take additional amplitude and period arguments (such as `inOutElastic`), set these to the desired values. +---@field easingPeriod number +--- Controls the behavior of playdate.ui.gridview:selectPreviousColumn() and playdate.ui.gridview:selectNextColumn() if the current selection is at the first or last column, respectively. If set to true, the selection switch to a new row to allow the selection to change. If false, the call will have no effect on the selection. True by default. +---@field changeRowOnColumnWrap boolean +--- If true, the gridview will attempt to center cells when scrolling. If false, the gridview will be scrolled just as much as necessary to make the cell visible. +---@field scrollCellsToCenter boolean + +---@class pd_loop_lib : pd_loop +--- Creates a new animation object. +--- +--- `imageTable` should be a playdate.graphics.imagetable, or nil. +--- +--- The following properties can be read or set directly, and have these defaults: +--- +--- `delay` : the value of `delay`, if passed, or 100ms (the delay before moving to the next frame) +--- +--- `startFrame` : 1 (the value the object resets to when the loop completes) +--- +--- `endFrame` : the number of images in `imageTable` if passed, or 1 (the last frame value in the loop) +--- +--- `frame` : 1 (the current frame counter) +--- +--- `step` : 1 (the value by which frame increments) +--- +--- `shouldLoop` : the value of `shouldLoop`, if passed, or true. (whether the object loops when it completes) +--- +--- `paused` : false (paused loops don’t change their frame value) +---@field new fun(delay?: number, imageTable?: pd_imagetable, shouldLoop?: boolean): pd_loop + +---@class pd_loop +--- Draw’s the loop’s current image at `x`, `y`. +--- +--- The `flip` argument is optional; see playdate.graphics.image:draw() for valid values. +---@field draw fun(self: pd_loop, x: number, y: number, flip?: pd_image_flip) +--- Returns a playdate.graphics.image from the caller’s `imageTable` if it exists. The image returned will be at the imageTable’s index that matches the caller’s `frame`. +---@field image fun(self: pd_loop): pd_image +--- Returns false if the loop has passed its last frame and does not loop. +---@field isValid fun(self: pd_loop): boolean +--- Sets the playdate.graphics.imagetable to be used for this animation loop, and sets the loop’s endFrame property to #imageTable. +---@field setImageTable fun(self: pd_loop, imageTable: pd_imagetable) + +---@class pd_blinker_lib : pd_blinker +--- Creates a new blinker object. Check the object’s on property to determine whether the blinker is on (true) or off (false). The default properties are: +--- +--- `onDuration`: 200 (the number of milliseconds the blinker is "on") +--- +--- `offDuration`: 200 (the number of milliseconds the blinker is "off") +--- +--- `loop`: false (should the blinker restart after completing) +--- +--- `cycles`: 6 (the number of changes the blinker goes through before it’s complete) +--- +--- `default`: true (the state the blinker will start in. Note: if default is true, blinker.on will return true when the blinker is in its `onDuration` phase. If default is false, blinker.on will return false when the blinker is in its `onDuration` phase.) +--- +--- Other informative properties: +--- +--- `counter`: Read this property to see which cycle the blinker is on (counts from `n` down to zero) +--- +--- `on`: Read this property to determine the current state of the blinker. The blinker always starts in the state specified by the default property. +--- +--- `running`: Read this property to see if the blinker is actively running +---@field new fun(onDuration?: number, offDuration?: number, loop?: boolean, cycles?: integer, default?: boolean): pd_blinker +--- Updates the state of all valid blinkers by calling :update() on each. +--- +--- If you intend to use blinkers, be sure to call :updateAll() once a cycle, ideally in your game’s playdate.update() function. +---@field updateAll fun() +--- Stops all blinkers. +---@field stopAll fun() + +---@class pd_blinker +--- Updates the caller’s state. +---@field update fun(self: pd_blinker) +--- Starts a blinker if it’s not running. Pass values for any property values you wish to modify. +---@field start fun(self: pd_blinker, onDuration?: number, offDuration?: number, loop?: boolean, cycles?: integer, default?: boolean) +--- Starts a blinker if it’s not running and sets its loop property to true. Equivalent to calling playdate.graphics.animation.blinker:start(nil, nil, true) +---@field startLoop fun(self: pd_blinker) +--- Stops a blinker if it’s running, returning the blinker’s on properly to the default value. +---@field stop fun(self: pd_blinker) +--- Flags the caller for removal from the global list of blinkers +---@field remove fun(self: pd_blinker) + diff --git a/lib/luaunit.lua b/lib/luaunit.lua new file mode 100644 index 0000000..e2c0bd7 --- /dev/null +++ b/lib/luaunit.lua @@ -0,0 +1,3452 @@ +--[[ + luaunit.lua + +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]]-- + +require("math") +local M={} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION='3.4' +M._VERSION=M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]]-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +-- this setting allow to remove entries from the stack-trace, for +-- example to hide a call to a framework which would be calling luaunit +M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE = 0 + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. + +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. + +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests +M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early +M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests + + + +M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod + +You may also control LuaUnit options with the following environment variables: +* LUAUNIT_OUTPUT: same as --output +* LUAUNIT_JUNIT_FNAME: same as --name ]] + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +--[[ Note on catching exit + +I have seen the case where running a big suite of test cases and one of them would +perform a os.exit(0), making the outside world think that the full test suite was executed +successfully. + +This is an attempt to mitigate this problem: we override os.exit() to now let a test +exit the framework while we are running. When we are not running, it behaves normally. +]] + +M.oldOsExit = os.exit +os.exit = function(...) + if M.LuaUnit and #M.LuaUnit.instances ~= 0 then + local msg = [[You are trying to exit but there is still a running instance of LuaUnit. +LuaUnit expects to run until the end before exiting with a complete status of successful/failed tests. + +To force exit LuaUnit while running, please call before os.exit (assuming lu is the luaunit module loaded): + + lu.unregisterCurrentSuite() + +]] + M.private.error_fmt(2, msg) + end + M.oldOsExit(...) +end + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = {pcall(func, ...)} + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, boolean = 2, string = 3, table = 4, other = 5 +} +local crossTypeComparison = { + number = function(a, b) return a < b end, + string = function(a, b) return a < b end, + other = function(a, b) return tostring(a) < tostring(b) end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex( t ) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key,_ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(math.floor(os.clock()*1E11)) + +local function randomizeTable( t ) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) +-- Split text into a list consisting of the strings in text, separated +-- by strings matching delimiter (which may _NOT_ be a pattern). +-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine( s ) + -- return true if s has a newline + return (string.find(s, '\n', 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString( prefix, s ) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, '\n', '\n' .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final ) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1,1) == '!' + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape( s ) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub( s, '.', { + ['&'] = "&", + ['"'] = """, + ["'"] = "'", + ['<'] = "<", + ['>'] = ">", + } ) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape( s ) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub( s, ']]>', ']]>' ) +end +M.private.xmlCDataEscape = xmlCDataEscape + + +local function lstrip( s ) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx,idx) + if c ~= ' ' and c ~= '\t' then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo( s ) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(':', 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(':', firstColon+1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon-1) +end +M.private.extractFileLineInfo = extractFileLineInfo + + +local function stripLuaunitTrace2( stackTrace, errMsg ) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine( s ) + -- return true if line of stack trace comes from inside luaunit + return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit( '\n', stackTrace ) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat( t, '\n') + +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr( v ) + --[[ Pretty string conversion, to display the full content of a variable of any type. + + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]]-- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter( err_msg, iter_msg ) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg..', ' + else + iter_msg = '' + end + + local RE_FILE_LINE = '.*:%d+: ' + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type( err_msg ) ~= 'string' then + err_msg = prettystr( err_msg ) + end + + if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match( '(.*:%d+: ).*' ) + if match then + err_msg = err_msg:gsub( match, match .. iter_msg ) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis, margin ) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + * margin: supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= 'number' or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= 'number' or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList( table_a, table_b, margin ) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return 'expected', 'actual' + end + return 'actual', 'expected' +end + +local function extendWithStrFmt( res, ... ) + table.insert( res, string.format( ... ) ) +end + +local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + + local k, v + + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList( table_a, table_b, margin ) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + + margin is supplied only for almost equality + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not M.private.is_table_equals(table_a[i], table_b[i], margin) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not M.private.is_table_equals(table_a[len_a-i], table_b[len_b-i], margin) then + commonBackTo = i - 1 + break + end + end + + + table.insert( result, 'List difference analysis:' ) + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + else + extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + end + + extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + else + extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if M.private.is_table_equals( table_a[ai], table_b[bi], margin) then + return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + else + extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) + extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert( result, '* Common parts:' ) + for i = 1, commonUntil do + insertABValue( i ) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert( result, '* Differing parts:' ) + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue( i ) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert( result, '* Present only in one list:' ) + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert( result, '* Common parts at the end of the lists' ) + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue( i, i-deltalv ) + else + insertABValue( i-deltalv, i ) + end + end + end + + return true, table.concat( result, '\n') +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = 'table 00-unknown ref' +local ref_generator = { value=1, [UNKNOWN_REF]=0 } + +local function table_ref( t ) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = '' + local mt = getmetatable( t ) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match( 'table: 0?x?[%x]+' ) then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable( t, mt ) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value+1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format('table %02d-%s', ref_generator[ref], ref) + else + return string.format('table %02d', ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable( tbl ) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= 'string' then + return string.format( '', prettystr(result) ) + end + result = strsplit( '\n', result ) + return M.private._table_tostring_format_multiline_string( result, indentLevel ) + + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs( tbl ) do + + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = '' + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<"..table_ref(k)..">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<"..table_ref(v)..">" + else + entry = entry .. + prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + end + +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) + local indentString = '\n'..string.rep(" ", indentLevel - 1) + return table.concat( tbl_str, indentString ) + +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + + +local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs( result ) do + totalLength = totalLength + string.len( v ) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}" + } + else + result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + end + if printTableRefs then + table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if M.private.is_table_equals(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected ) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + + elseif (type_a == 'table') --[[and (type_e == 'table')]] then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end + } +} + +local function _is_table_equals(actual, expected, cycleDetectTable, marginForAlmostEqual) + --[[Returns true if both table are equal. + + If argument marginForAlmostEqual is suppied, number comparison is done using alomstEqual instead + of strict equality. + + cycleDetectTable is an internal argument used during recursion on tables. + ]] + --print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected).. + -- '\n , '..prettystr(cycleDetectTable)..'\n , '..prettystr(marginForAlmostEqual)..' )') + + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a == 'number' then + if marginForAlmostEqual ~= nil then + return M.almostEquals(actual, expected, marginForAlmostEqual) + else + return actual == expected + end + elseif type_a ~= 'table' then + -- other types compare directly + return actual == expected + end + + cycleDetectTable = cycleDetectTable or { actual={}, expected={} } + if cycleDetectTable.actual[ actual ] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[ expected ] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[ expected ] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[ actual ] = true + cycleDetectTable.expected[ expected ] = true + + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable, marginForAlmostEqual) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return true +end +M.private._is_table_equals = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. '\n' .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end + +local function is_table_equals(actual, expected, marginForAlmostEqual) + return _is_table_equals(actual, expected, nil, marginForAlmostEqual) +end +M.private.is_table_equals = is_table_equals + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1 + M.STRIP_EXTRA_ENTRIES_IN_STACK_TRACE) +end +M.private.error_fmt = error_fmt + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis, margin) + -- margin is supplied only for almost equal verification + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == 'string' or type(expected) == 'table' then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + if margin then + result = result .. '\nwere not equal by the margin of: '..prettystr(margin) + end + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis, margin ) + if success then + result = table.concat( { result, mismatchResult }, '\n' ) + end + return result + end + return string.format("expected: %s, actual: %s", + prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall( f, ... ) then + failure( "Expected an error when calling function but no error generated", nil, 2 ) + end +end + +function M.fail( msg ) + -- stops a test due to a failure + failure( msg, nil, 2 ) +end + +function M.failIf( cond, msg ) + -- Fails a test with "msg" if condition is true + if cond then + failure( msg, nil, 2 ) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf( cond, msg ) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf( cond, msg ) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf( cond ) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + end + elseif type(actual) ~= type(expected) then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + elseif actual ~= expected then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + end +end + +function M.almostEquals( actual, expected, margin ) + if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then + error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end + if margin < 0 then + error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are close by margin + margin = margin or M.EPS + if type(margin) ~= 'number' then + error_fmt(2, 'almostEquals: margin must be a number, not %s', prettystr(margin)) + end + + if type(actual) == 'table' and type(expected) == 'table' then + -- handle almost equals for table + if not is_table_equals(actual, expected, margin) then + failure( errorMsgEquality(actual, expected, nil, margin), extra_msg_or_nil, 2 ) + end + elseif type(actual) == 'number' and type(expected) == 'number' and type(margin) == 'number' then + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. + 'Actual: %s, expected: %s, delta %s above margin of %s', + actual, expected, delta, margin) + end + else + error_fmt(3, 'almostEquals: must supply only number or table arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == 'table' and type(expected) == 'table' then + if not is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) +end + +function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. + ', delta %s below margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected ) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', + expected, actual) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', + sub, str) + end +end + +function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', + sub, str) + end +end + +function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) + -- Verify a full match for the string + if not strMatch( str, pattern, start, final ) then + pattern, str = prettystrPairs(pattern, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', + pattern, str) + end +end + +local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == 'table' then + if tr ~= 'table' then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains( partialMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find( error_msg, partialMsg, nil, true ) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', + partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch( error_msg, expectedMsg ) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): + +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs( + {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', + 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} +) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == 'nil' then + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', + typeExpected, type(value), prettystrPairs(value)) + else + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', + typeExpected, type(value), prettystrPairs(value)) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs( + {'Number', 'String', 'Table', 'Boolean', + 'Function', 'Userdata', 'Thread', 'Nil' } +) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M['is'..typeExpected] = isType + M['is_'..typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): + +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs( + {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', + 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} +) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', + typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, '\n', '') + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', + expected, actual) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + end +end + + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == -math.huge) then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else if (1/value ~= math.huge) then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == math.huge) then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else if (1/value ~= -math.huge) then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected, extra_msg_or_nil) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s does NOT contain the expected element %s', + t, expected) + end +end + +function M.assertNotTableContains(t, expected, extra_msg_or_nil) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, extra_msg_or_nil, 'Table %s DOES contain the unwanted element %s (at key %s)', + t, expected, prettystr(k)) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write[[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]] +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { 'assertEquals' , 'assert_equals' }, + { 'assertItemsEquals' , 'assert_items_equals' }, + { 'assertNotEquals' , 'assert_not_equals' }, + { 'assertAlmostEquals' , 'assert_almost_equals' }, + { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, + { 'assertEvalToTrue' , 'assert_eval_to_true' }, + { 'assertEvalToFalse' , 'assert_eval_to_false' }, + { 'assertStrContains' , 'assert_str_contains' }, + { 'assertStrIContains' , 'assert_str_icontains' }, + { 'assertNotStrContains' , 'assert_not_str_contains' }, + { 'assertNotStrIContains' , 'assert_not_str_icontains' }, + { 'assertStrMatches' , 'assert_str_matches' }, + { 'assertError' , 'assert_error' }, + { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, + { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, + { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, + { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, + { 'assertIs' , 'assert_is' }, + { 'assertNotIs' , 'assert_not_is' }, + { 'assertTableContains' , 'assert_table_contains' }, + { 'assertNotTableContains' , 'assert_not_table_contains' }, + { 'wrapFunctions' , 'WrapFunctions' }, + { 'wrapFunctions' , 'wrap_functions' }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { 'assertIsNumber' , 'assert_is_number' }, + { 'assertIsString' , 'assert_is_string' }, + { 'assertIsTable' , 'assert_is_table' }, + { 'assertIsBoolean' , 'assert_is_boolean' }, + { 'assertIsNil' , 'assert_is_nil' }, + { 'assertIsTrue' , 'assert_is_true' }, + { 'assertIsFalse' , 'assert_is_false' }, + { 'assertIsNaN' , 'assert_is_nan' }, + { 'assertIsInf' , 'assert_is_inf' }, + { 'assertIsPlusInf' , 'assert_is_plus_inf' }, + { 'assertIsMinusInf' , 'assert_is_minus_inf' }, + { 'assertIsPlusZero' , 'assert_is_plus_zero' }, + { 'assertIsMinusZero' , 'assert_is_minus_zero' }, + { 'assertIsFunction' , 'assert_is_function' }, + { 'assertIsThread' , 'assert_is_thread' }, + { 'assertIsUserdata' , 'assert_is_userdata' }, + + -- type assertions: assertIsXXX -> assertXxx + { 'assertIsNumber' , 'assertNumber' }, + { 'assertIsString' , 'assertString' }, + { 'assertIsTable' , 'assertTable' }, + { 'assertIsBoolean' , 'assertBoolean' }, + { 'assertIsNil' , 'assertNil' }, + { 'assertIsTrue' , 'assertTrue' }, + { 'assertIsFalse' , 'assertFalse' }, + { 'assertIsNaN' , 'assertNaN' }, + { 'assertIsInf' , 'assertInf' }, + { 'assertIsPlusInf' , 'assertPlusInf' }, + { 'assertIsMinusInf' , 'assertMinusInf' }, + { 'assertIsPlusZero' , 'assertPlusZero' }, + { 'assertIsMinusZero' , 'assertMinusZero'}, + { 'assertIsFunction' , 'assertFunction' }, + { 'assertIsThread' , 'assertThread' }, + { 'assertIsUserdata' , 'assertUserdata' }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { 'assertIsNumber' , 'assert_number' }, + { 'assertIsString' , 'assert_string' }, + { 'assertIsTable' , 'assert_table' }, + { 'assertIsBoolean' , 'assert_boolean' }, + { 'assertIsNil' , 'assert_nil' }, + { 'assertIsTrue' , 'assert_true' }, + { 'assertIsFalse' , 'assert_false' }, + { 'assertIsNaN' , 'assert_nan' }, + { 'assertIsInf' , 'assert_inf' }, + { 'assertIsPlusInf' , 'assert_plus_inf' }, + { 'assertIsMinusInf' , 'assert_minus_inf' }, + { 'assertIsPlusZero' , 'assert_plus_zero' }, + { 'assertIsMinusZero' , 'assert_minus_zero' }, + { 'assertIsFunction' , 'assert_function' }, + { 'assertIsThread' , 'assert_thread' }, + { 'assertIsUserdata' , 'assert_userdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { 'assertNotIsNumber' , 'assert_not_is_number' }, + { 'assertNotIsString' , 'assert_not_is_string' }, + { 'assertNotIsTable' , 'assert_not_is_table' }, + { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, + { 'assertNotIsNil' , 'assert_not_is_nil' }, + { 'assertNotIsTrue' , 'assert_not_is_true' }, + { 'assertNotIsFalse' , 'assert_not_is_false' }, + { 'assertNotIsNaN' , 'assert_not_is_nan' }, + { 'assertNotIsInf' , 'assert_not_is_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_is_function' }, + { 'assertNotIsThread' , 'assert_not_is_thread' }, + { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { 'assertNotIsNumber' , 'assertNotNumber' }, + { 'assertNotIsString' , 'assertNotString' }, + { 'assertNotIsTable' , 'assertNotTable' }, + { 'assertNotIsBoolean' , 'assertNotBoolean' }, + { 'assertNotIsNil' , 'assertNotNil' }, + { 'assertNotIsTrue' , 'assertNotTrue' }, + { 'assertNotIsFalse' , 'assertNotFalse' }, + { 'assertNotIsNaN' , 'assertNotNaN' }, + { 'assertNotIsInf' , 'assertNotInf' }, + { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, + { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, + { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, + { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, + { 'assertNotIsFunction' , 'assertNotFunction' }, + { 'assertNotIsThread' , 'assertNotThread' }, + { 'assertNotIsUserdata' , 'assertNotUserdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { 'assertNotIsNumber' , 'assert_not_number' }, + { 'assertNotIsString' , 'assert_not_string' }, + { 'assertNotIsTable' , 'assert_not_table' }, + { 'assertNotIsBoolean' , 'assert_not_boolean' }, + { 'assertNotIsNil' , 'assert_not_nil' }, + { 'assertNotIsTrue' , 'assert_not_true' }, + { 'assertNotIsFalse' , 'assert_not_false' }, + { 'assertNotIsNaN' , 'assert_not_nan' }, + { 'assertNotIsInf' , 'assert_not_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_function' }, + { 'assertNotIsThread' , 'assert_not_thread' }, + { 'assertNotIsUserdata' , 'assert_not_userdata' }, + + -- all assertions with Coroutine duplicate Thread assertions + { 'assertIsThread' , 'assertIsCoroutine' }, + { 'assertIsThread' , 'assertCoroutine' }, + { 'assertIsThread' , 'assert_is_coroutine' }, + { 'assertIsThread' , 'assert_coroutine' }, + { 'assertNotIsThread' , 'assertNotIsCoroutine' }, + { 'assertNotIsThread' , 'assertNotCoroutine' }, + { 'assertNotIsThread' , 'assert_not_is_coroutine' }, + { 'assertNotIsThread' , 'assert_not_coroutine' }, +} + +-- Create all aliases in M +for _,v in ipairs( list_of_funcs ) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable( t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = 'TapOutput' + + -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + + function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable( t, TapOutput_MT) + end + function TapOutput:startSuite() + print("1.."..self.result.selectedCount) + print('# Started on '..self.result.startDate) + end + function TapOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + + function TapOutput:updateStatus( node ) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print( prefixString( '# ', node.msg ) ) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print( prefixString( '# ', node.stackTrace ) ) + end + end + + function TapOutput:endTest( node ) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end + end + + function TapOutput:endSuite() + print( '# '..M.LuaUnit.statusLine( self.result ) ) + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = 'JUnitOutput' + + function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable( t, JUnitOutput_MT ) + end + + function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error('With Junit, an output filename must be supplied with --name!') + end + if string.sub(self.fname,-4) ~= '.xml' then + self.fname = self.fname..'.xml' + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: "..self.fname) + end + + print('# XML output to '..self.fname) + print('# Started on '..self.result.startDate) + end + function JUnitOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + function JUnitOutput:startTest(testName) + print('# Starting test: '..testName) + end + + function JUnitOutput:updateStatus( node ) + if node:isFailure() then + print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + end + end + + function JUnitOutput:endSuite() + print( '# '..M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write('\n') + self.fd:write(string.format( + ' \n', + self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION ) ) + self.fd:write(string.format(' \n', M.VERSION) ) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i,node in ipairs(self.result.allTests) do + self.fd:write(string.format(' \n', + node.className, node.testName, node.duration ) ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(' \n') + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(' \n') + self.fd:write(' \n') + + self.fd:write(' \n') + self.fd:write('\n') + self.fd:close() + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output + +-- Python Non verbose: + +For each test: . or F or E + +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace + + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Ruby: +Started + . + Finished in 0.002695 seconds. + + 1 tests, 2 assertions, 0 failures, 0 errors + +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. + +3 tests, 4 assertions, 1 failures, 0 errors + +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 + + +-- Maven + +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! + +Results : + +Failed tests: + testLireSymbole(math.AdditionTest) + +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 + + +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace + +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) + + +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = 'TextOutput' + + function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable( t, TextOutput_MT ) + end + + function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print( 'Started on '.. self.result.startDate ) + end + end + + function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write( " ", self.result.currentNode.testName, " ... " ) + end + end + + function TextOutput:endTest( node ) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.status ) + print( node.msg ) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end + end + + function TextOutput:displayOneFailedTest( index, fail ) + print(index..") "..fail.testName ) + print( fail.msg ) + print( fail.stackTrace ) + print() + end + + function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print( M.LuaUnit.statusLine( self.result ) ) + if self.result.notSuccessCount == 0 then + print('OK') + end + end + +-- class TextOutput end + + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = 'LuaUnit', + instances = {} +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + + function M.LuaUnit.new() + local newInstance = setmetatable( {}, LuaUnit_MT ) + return newInstance + end + + -----------------[[ Utility methods ]]--------------------- + + function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if 'function' == type(aObject) then + return aObject + end + end + + function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, '.', 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName + end + + function M.LuaUnit.isMethodTestName( s ) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.isTestName( s ) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName( k ) then + table.insert( testNames , k ) + end + end + table.sort( testNames ) + return testNames + end + + function M.LuaUnit.parseCmdLine( cmdLine ) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption( option ) + if option == '--help' or option == '-h' then + result['help'] = true + return + elseif option == '--version' then + result['version'] = true + return + elseif option == '--verbose' or option == '-v' then + result['verbosity'] = M.VERBOSITY_VERBOSE + return + elseif option == '--quiet' or option == '-q' then + result['verbosity'] = M.VERBOSITY_QUIET + return + elseif option == '--error' or option == '-e' then + result['quitOnError'] = true + return + elseif option == '--failure' or option == '-f' then + result['quitOnFailure'] = true + return + elseif option == '--shuffle' or option == '-s' then + result['shuffle'] = true + return + elseif option == '--output' or option == '-o' then + state = SET_OUTPUT + return state + elseif option == '--name' or option == '-n' then + state = SET_FNAME + return state + elseif option == '--repeat' or option == '-r' then + state = SET_REPEAT + return state + elseif option == '--pattern' or option == '-p' then + state = SET_PATTERN + return state + elseif option == '--exclude' or option == '-x' then + state = SET_EXCLUDE + return state + end + error('Unknown option: '..option,3) + end + + local function setArg( cmdArg, state ) + if state == SET_OUTPUT then + result['output'] = cmdArg + return + elseif state == SET_FNAME then + result['fname'] = cmdArg + return + elseif state == SET_REPEAT then + result['exeRepeat'] = tonumber(cmdArg) + or error('Malformed -r argument: '..cmdArg) + return + elseif state == SET_PATTERN then + if result['pattern'] then + table.insert( result['pattern'], cmdArg ) + else + result['pattern'] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = '!'..cmdArg + if result['pattern'] then + table.insert( result['pattern'], notArg ) + else + result['pattern'] = { notArg } + end + return + end + error('Unknown parse state: '.. state) + end + + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg( cmdArg, state, result ) + state = nil + else + if cmdArg:sub(1,1) == '-' then + state = parseOption( cmdArg ) + else + if result['testNames'] then + table.insert( result['testNames'], cmdArg ) + else + result['testNames'] = { cmdArg } + end + end + end + end + + if result['help'] then + M.LuaUnit.help() + end + + if result['version'] then + M.LuaUnit.version() + end + + if state ~= nil then + error('Missing argument after '..cmdLine[ #cmdLine ],2 ) + end + + return result + end + + function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) + end + + function M.LuaUnit.version() + print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') + os.exit(0) + end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + + local NodeStatus = { __class__ = 'NodeStatus' } -- class + local NodeStatus_MT = { __index = NodeStatus } -- metatable + M.NodeStatus = NodeStatus + + -- values of status + NodeStatus.SUCCESS = 'SUCCESS' + NodeStatus.SKIP = 'SKIP' + NodeStatus.FAIL = 'FAIL' + NodeStatus.ERROR = 'ERROR' + + function NodeStatus.new( number, testName, className ) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable( t, NodeStatus_MT ) + t:success() + return t + end + + function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil + end + + function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil + end + + function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS + end + + function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) + end + + function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP + end + + function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL + end + + function NodeStatus:isError() + return self.status == NodeStatus.ERROR + end + + function NodeStatus:statusXML() + if self:isError() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isFailure() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isSkipped() then + return table.concat({' ', xmlEscape(self.msg),'\n' } ) + end + return ' \n' -- (not XSD-compliant! normally shouldn't get here) + end + + --------------[[ Output methods ]]------------------------- + + local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = '' + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + end + return string.format('%d %s%s', number, singular, suffix) + end + + function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format('Ran %d tests in %0.3f seconds', + result.runCount, result.duration), + conditional_plural(result.successCount, 'success'), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, 'failure')) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, 'error')) + end + else + table.insert(s, '0 failures') + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ', ') + end + + function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), + startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() + end + + function M.LuaUnit:startClass( className, classInstance ) + self.result.currentClassName = className + self.output:startClass( className ) + self:setupClass( className, classInstance ) + end + + function M.LuaUnit:startTest( testName ) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new( + self.result.currentTestNumber, + testName, + self.result.currentClassName + ) + self.result.currentNode.startTime = os.clock() + table.insert( self.result.allTests, self.result.currentNode ) + self.output:startTest( testName ) + end + + function M.LuaUnit:updateStatus( err ) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail( err.msg, err.trace ) + table.insert( self.result.failedTests, node ) + elseif err.status == NodeStatus.ERROR then + node:error( err.msg, err.trace ) + table.insert( self.result.errorTests, node ) + elseif err.status == NodeStatus.SKIP then + node:skip( err.msg ) + table.insert( self.result.skippedTests, node ) + else + error('No such status: ' .. prettystr(err.status)) + end + + self.output:updateStatus( node ) + end + + function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest( node ) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in internalRunSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error('No such node status: ' .. prettystr(node.status)) + end + self.result.currentNode = nil + end + + function M.LuaUnit:endClass() + self:teardownClass( self.lastClassName, self.lastClassInstance ) + self.output:endClass() + end + + function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error('LuaUnit:endSuite() -- suite was already ended' ) + end + self.result.duration = os.clock()-self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() + end + + function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error( 'No such format: '..outputType,2) + end + + --------------[[ Runner ]]----------------- + + function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2) + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) + else + ok, err = xpcall( function () methodInstance() end, err_handler ) + end + if ok then + return {status = NodeStatus.SUCCESS} + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and 'iteration '..self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) + end + + + function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= 'function' then + self:unregisterSuite() + error( tostring(methodName)..' must be a function, not '..type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = '[TestFunctions]' + prettyFuncName = methodName + else + prettyFuncName = className..'.'..methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass( className, classInstance ) + self.lastClassName = className + self.lastClassInstance = classInstance + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction( classInstance.setUp ) or + self.asFunction( classInstance.Setup ) or + self.asFunction( classInstance.setup ) or + self.asFunction( classInstance.SetUp ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction( classInstance.tearDown ) or + self.asFunction( classInstance.TearDown ) or + self.asFunction( classInstance.teardown ) or + self.asFunction( classInstance.Teardown ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) + end + end + end + + self:endTest() + end + + function M.LuaUnit.expandOneClass( result, className, classInstance ) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then + table.insert( result, { className..'.'..methodName, classInstance } ) + end + end + end + + function M.LuaUnit.expandClasses( listOfNameAndInst ) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + + Input: a list of { name, instance } + + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i,v in ipairs( listOfNameAndInst ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert( result, { name, instance } ) + else + if type(instance) ~= 'table' then + error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + end + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + table.insert( result, { name, instance } ) + else + M.LuaUnit.expandOneClass( result, name, instance ) + end + end + end + + return result + end + + function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) + local included, excluded = {}, {} + for i, v in ipairs( listOfNameAndInst ) do + -- local name, instance = v[1], v[2] + if patternFilter( patternIncFilter, v[1] ) then + table.insert( included, v ) + else + table.insert( excluded, v ) + end + end + return included, excluded + end + + local function getKeyInListWithGlobalFallback( key, listOfNameAndInst ) + local result = nil + for i,v in ipairs( listOfNameAndInst ) do + if(listOfNameAndInst[i][1] == key) then + result = listOfNameAndInst[i][2] + break + end + end + if(not M.LuaUnit.asFunction( result ) ) then + result = _G[key] + end + return result + end + + function M.LuaUnit:setupSuite( listOfNameAndInst ) + local setupSuite = getKeyInListWithGlobalFallback("setupSuite", listOfNameAndInst) + if self.asFunction( setupSuite ) then + self:updateStatus( self:protectedCall( nil, setupSuite, 'setupSuite' ) ) + end + end + + function M.LuaUnit:teardownSuite(listOfNameAndInst) + local teardownSuite = getKeyInListWithGlobalFallback("teardownSuite", listOfNameAndInst) + if self.asFunction( teardownSuite ) then + self:updateStatus( self:protectedCall( nil, teardownSuite, 'teardownSuite') ) + end + end + + function M.LuaUnit:setupClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.setupClass ) then + self:updateStatus( self:protectedCall( instance, instance.setupClass, className..'.setupClass' ) ) + end + end + + function M.LuaUnit:teardownClass( className, instance ) + if type( instance ) == 'table' and self.asFunction( instance.teardownClass ) then + self:updateStatus( self:protectedCall( instance, instance.teardownClass, className..'.teardownClass' ) ) + end + end + + function M.LuaUnit:internalRunSuiteByInstances( listOfNameAndInst ) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + + This function is internal to LuaUnit. The official API to perform this action is runSuiteByInstances() + ]] + + local expandedList = self.expandClasses( listOfNameAndInst ) + if self.shuffle then + randomizeTable( expandedList ) + end + local filteredList, filteredOutList = self.applyPatternFilter( + self.patternIncludeFilter, expandedList ) + + self:startSuite( #filteredList, #filteredOutList ) + self:setupSuite( listOfNameAndInst ) + + for i,v in ipairs( filteredList ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction( nil, name, nil, instance ) + else + -- expandClasses() should have already taken care of sanitizing the input + assert( type(instance) == 'table' ) + local className, methodName = M.LuaUnit.splitClassMethod( name ) + assert( className ~= nil ) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction( className, methodName, instance, methodInstance ) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:teardownSuite( listOfNameAndInst ) + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + self:unregisterSuite() + os.exit(-2) + end + end + + function M.LuaUnit:internalRunSuiteByNames( listOfName ) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls internalRunSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i,name in ipairs( listOfName ) do + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if type(instance) ~= 'table' then + self:unregisterSuite() + error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + self:unregisterSuite() + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + self:unregisterSuite() + error( "No such name in global space: "..instanceName ) + end + + if (type(instance) ~= 'table' and type(instance) ~= 'function') then + self:unregisterSuite() + error( 'Name must match a function or a table: '..instanceName ) + end + + table.insert( listOfNameAndInst, { name, instance } ) + end + + self:internalRunSuiteByInstances( listOfNameAndInst ) + end + + function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + local runner = M.LuaUnit.new() + return runner:runSuite(...) + end + + function M.LuaUnit:registerSuite() + -- register the current instance into our global array of instances + -- print('-> Register suite') + M.LuaUnit.instances[ #M.LuaUnit.instances+1 ] = self + end + + function M.unregisterCurrentSuite() + -- force unregister the last registered suite + table.remove(M.LuaUnit.instances, #M.LuaUnit.instances) + end + + function M.LuaUnit:unregisterSuite() + -- print('<- Unregister suite') + -- remove our current instqances from the global array of instances + local instanceIdx = nil + for i, instance in ipairs(M.LuaUnit.instances) do + if instance == self then + instanceIdx = i + break + end + end + + if instanceIdx ~= nil then + table.remove(M.LuaUnit.instances, instanceIdx) + -- print('Unregister done') + end + + end + + function M.LuaUnit:initFromArguments( ... ) + --[[Parses all arguments from either command-line or direct call and set internal + flags of LuaUnit runner according to it. + + Return the list of names which were possibly passed on the command-line or as arguments + ]] + local args = {...} + if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument self to make it a command-line argument list + table.remove(args,1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + options.output = options.output or os.getenv('LUAUNIT_OUTPUT') + options.fname = options.fname or os.getenv('LUAUNIT_JUNIT_FNAME') + + if options.output then + if options.output:lower() == 'junit' and options.fname == nil then + print('With junit output, a filename must be supplied with -n or --name') + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + return options.testNames + end + + function M.LuaUnit:runSuite( ... ) + local testNames = self:initFromArguments(...) + self:registerSuite() + self:internalRunSuiteByNames( testNames or M.LuaUnit.collectTests() ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + function M.LuaUnit:runSuiteByInstances( listOfNameAndInst, commandLineArguments ) + --[[ + Run all test functions or tables provided as input. + + Input: a list of { name, instance } + instance can either be a function or a table containing test functions starting with the prefix "test" + + return the number of failures and errors, 0 meaning success + ]] + -- parse the command-line arguments + local testNames = self:initFromArguments( commandLineArguments ) + self:registerSuite() + self:internalRunSuiteByInstances( listOfNameAndInst ) + self:unregisterSuite() + return self.result.notSuccessCount + end + + + +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity( verbosity ) + -- set the verbosity value (as integer) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + + +return M \ No newline at end of file diff --git a/lib/preprocess-cl.lua b/lib/preprocess-cl.lua new file mode 100644 index 0000000..c4cae39 --- /dev/null +++ b/lib/preprocess-cl.lua @@ -0,0 +1,651 @@ +#!/bin/sh +_=[[ +exec lua "$0" "$@" +]]and nil +--============================================================== +--= +--= LuaPreprocess command line program +--= by Marcus 'ReFreezed' Thunström +--= +--= Requires preprocess.lua to be in the same folder! +--= +--= License: MIT (see the bottom of this file) +--= Website: http://refreezed.com/luapreprocess/ +--= Documentation: http://refreezed.com/luapreprocess/docs/command-line/ +--= +--= Tested with Lua 5.1, 5.2, 5.3, 5.4 and LuaJIT. +--= +--============================================================== +local help = [[ + +Script usage: + lua preprocess-cl.lua [options] [--] filepath1 [filepath2 ...] + OR + lua preprocess-cl.lua --outputpaths [options] [--] inputpath1 outputpath1 [inputpath2 outputpath2 ...] + + File paths can be "-" for usage of stdin/stdout. + +Examples: + lua preprocess-cl.lua --saveinfo=logs/info.lua --silent src/main.lua2p src/network.lua2p + lua preprocess-cl.lua --debug src/main.lua2p src/network.lua2p + lua preprocess-cl.lua --outputpaths --linenumbers src/main.lua2p output/main.lua src/network.lua2p output/network.lua + +Options: + --backtickstrings + Enable the backtick (`) to be used as string literal delimiters. + Backtick strings don't interpret any escape sequences and can't + contain other backticks. + + --data|-d="Any data." + A string with any data. If this option is present then the value + will be available through the global 'dataFromCommandLine' in the + processed files (and any message handler). Otherwise, + 'dataFromCommandLine' is nil. + + --faststrings + Force fast serialization of string values. (Non-ASCII characters + will look ugly.) + + --handler|-h=pathToMessageHandler + Path to a Lua file that's expected to return a function or a + table of functions. If it returns a function then it will be + called with various messages as it's first argument. If it's + a table, the keys should be the message names and the values + should be functions to handle the respective message. + (See 'Handler messages' and tests/quickTestHandler*.lua) + The file shares the same environment as the processed files. + + --help + Show this help. + + --jitsyntax + Allow LuaJIT-specific syntax, specifically literals for 64-bit + integers, complex numbers and binary numbers. + (https://luajit.org/ext_ffi_api.html#literals) + + --linenumbers + Add comments with line numbers to the output. + + --loglevel=levelName + Set maximum log level for the @@LOG() macro. Can be "off", + "error", "warning", "info", "debug" or "trace". The default is + "trace", which enables all logging. + + --macroprefix=prefix + String to prepend to macro names. + + --macrosuffix=suffix + String to append to macro names. + + --meta OR --meta=pathToSaveMetaprogramTo + Output the metaprogram to a temporary file (*.meta.lua). Useful if + an error happens when the metaprogram runs. This file is removed + if there's no error and --debug isn't enabled. + + --nogc + Stop the garbage collector. This may speed up the preprocessing. + + --nonil + Disallow !(expression) and outputValue() from outputting nil. + + --nostrictmacroarguments + Disable checks that macro arguments are valid Lua expressions. + + --novalidate + Disable validation of outputted Lua. + + --outputextension=fileExtension + Specify what file extension generated files should have. The + default is "lua". If any input files end in .lua then you must + specify another file extension with this option. (It's suggested + that you use .lua2p (as in "Lua To Process") as extension for + unprocessed files.) + + --outputpaths|-o + This flag makes every other specified path be the output path + for the previous path. + + --release + Enable release mode. Currently only disables the @@ASSERT() macro. + + --saveinfo|-i=pathToSaveProcessingInfoTo + Processing information includes what files had any preprocessor + code in them, and things like that. The format of the file is a + lua module that returns a table. Search this file for 'SavedInfo' + to see what information is saved. + + --silent + Only print errors to the console. (This flag is automatically + enabled if an output path is stdout.) + + --version + Print the version of LuaPreprocess to stdout and exit. + + --debug + Enable some preprocessing debug features. Useful if you want + to inspect the generated metaprogram (*.meta.lua). (This also + enables the --meta option.) + + -- + Stop options from being parsed further. Needed if you have paths + starting with "-" (except for usage of stdin/stdout). + +Handler messages: + "init" + Sent before any other message. + Arguments: + inputPaths: Array of file paths to process. Paths can be added or removed freely. + outputPaths: If the --outputpaths option is present this is an array of output paths for the respective path in inputPaths, otherwise it's nil. + + "insert" + Sent for each @insert"name" statement. The handler is expected to return a Lua code string. + Arguments: + path: The file being processed. + name: The name of the resource to be inserted (could be a file path or anything). + + "beforemeta" + Sent before a file's metaprogram runs, if a metaprogram is generated. + Arguments: + path: The file being processed. + luaString: The generated metaprogram. + + "aftermeta" + Sent after a file's metaprogram has produced output (before the output is written to a file). + Arguments: + path: The file being processed. + luaString: The produced Lua code. You can modify this and return the modified string. + + "filedone" + Sent after a file has finished processing and the output written to file. + Arguments: + path: The file being processed. + outputPath: Where the output of the metaprogram was written. + info: Info about the processed file. (See 'ProcessInfo' in preprocess.lua) + + "fileerror" + Sent if an error happens while processing a file (right before the program exits). + Arguments: + path: The file being processed. + error: The error message. + + "alldone" + Sent after all other messages (right before the program exits). + Arguments: + (none) +]] +--============================================================== + + + +local startTime = os.time() +local startClock = os.clock() + +local args = arg + +if not args[0] then error("Expected to run from the Lua interpreter.") end +local pp = dofile((args[0]:gsub("[^/\\]+$", "preprocess.lua"))) + +-- From args: +local addLineNumbers = false +local allowBacktickStrings = false +local allowJitSyntax = false +local canOutputNil = true +local customData = nil +local fastStrings = false +local hasOutputExtension = false +local hasOutputPaths = false +local isDebug = false +local outputExtension = "lua" +local outputMeta = false -- flag|path +local processingInfoPath = "" +local silent = false +local validate = true +local macroPrefix = "" +local macroSuffix = "" +local releaseMode = false +local maxLogLevel = "trace" +local strictMacroArguments = true + +--============================================================== +--= Local functions ============================================ +--============================================================== +local F = string.format + +local function formatBytes(n) + if n >= 1024*1024*1024 then + return F("%.2f GiB", n/(1024*1024*1024)) + elseif n >= 1024*1024 then + return F("%.2f MiB", n/(1024*1024)) + elseif n >= 1024 then + return F("%.2f KiB", n/(1024)) + elseif n == 1 then + return F("1 byte", n) + else + return F("%d bytes", n) + end +end + +local function printfNoise(s, ...) + print(s:format(...)) +end +local function printError(s) + io.stderr:write(s, "\n") +end +local function printfError(s, ...) + io.stderr:write(s:format(...), "\n") +end + +local function errorLine(err) + printError(pp.tryToFormatError(err)) + os.exit(1) +end + +local loadLuaFile = ( + (_VERSION >= "Lua 5.2" or jit) and function(path, env) + return loadfile(path, "bt", env) + end + or function(path, env) + local chunk, err = loadfile(path) + if not chunk then return nil, err end + + if env then setfenv(chunk, env) end + + return chunk + end +) + +--============================================================== +--= Preprocessor script ======================================== +--============================================================== + +io.stdout:setvbuf("no") +io.stderr:setvbuf("no") + +math.randomseed(os.time()) -- In case math.random() is used anywhere. +math.random() -- Must kickstart... + +local processOptions = true +local messageHandlerPath = "" +local pathsIn = {} +local pathsOut = {} + +for _, arg in ipairs(args) do + if processOptions and (arg:find"^%-%-?help$" or arg == "/?" or arg:find"^/[Hh][Ee][Ll][Pp]$") then + print("LuaPreprocess v"..pp.VERSION) + print((help:gsub("\t", " "))) + os.exit() + + elseif not (processOptions and arg:find"^%-.") then + local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn + table.insert(paths, arg) + + if arg == "-" and (not hasOutputPaths or paths == pathsOut) then + silent = true + end + + elseif arg == "--" then + processOptions = false + + elseif arg:find"^%-%-data=" or arg:find"^%-d=" then + customData = arg:gsub("^.-=", "") + + elseif arg == "--backtickstrings" then + allowBacktickStrings = true + + elseif arg == "--debug" then + isDebug = true + outputMeta = outputMeta or true + + elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then + messageHandlerPath = arg:gsub("^.-=", "") + + elseif arg == "--jitsyntax" then + allowJitSyntax = true + + elseif arg == "--linenumbers" then + addLineNumbers = true + + elseif arg == "--meta" then + outputMeta = true + elseif arg:find"^%-%-meta=" then + outputMeta = arg:gsub("^.-=", "") + + elseif arg == "--nonil" then + canOutputNil = false + + elseif arg == "--novalidate" then + validate = false + + elseif arg:find"^%-%-outputextension=" then + if hasOutputPaths then + errorLine("Cannot specify both --outputextension and --outputpaths") + end + hasOutputExtension = true + outputExtension = arg:gsub("^.-=", "") + + elseif arg == "--outputpaths" or arg == "-o" then + if hasOutputExtension then + errorLine("Cannot specify both --outputpaths and --outputextension") + elseif pathsIn[1] then + errorLine(arg.." must appear before any input path.") + end + hasOutputPaths = true + + elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then + processingInfoPath = arg:gsub("^.-=", "") + + elseif arg == "--silent" then + silent = true + + elseif arg == "--faststrings" then + fastStrings = true + + elseif arg == "--nogc" then + collectgarbage("stop") + + elseif arg:find"^%-%-macroprefix=" then + macroPrefix = arg:gsub("^.-=", "") + + elseif arg:find"^%-%-macrosuffix=" then + macroSuffix = arg:gsub("^.-=", "") + + elseif arg == "--release" then + releaseMode = true + + elseif arg:find"^%-%-loglevel=" then + maxLogLevel = arg:gsub("^.-=", "") + + elseif arg == "--version" then + io.stdout:write(pp.VERSION) + os.exit() + + elseif arg == "--nostrictmacroarguments" then + strictMacroArguments = false + + else + errorLine("Unknown option '"..arg:gsub("=.*", "").."'.") + end +end + +if silent then + printfNoise = function()end +end + +local header = "= LuaPreprocess v"..pp.VERSION..os.date(", %Y-%m-%d %H:%M:%S =", startTime) +printfNoise(("="):rep(#header)) +printfNoise("%s", header) +printfNoise(("="):rep(#header)) + +if hasOutputPaths and #pathsOut < #pathsIn then + errorLine("Missing output path for "..pathsIn[#pathsIn]) +end + + + +-- Prepare metaEnvironment. +pp.metaEnvironment.dataFromCommandLine = customData -- May be nil. + + + +-- Load message handler. +local messageHandler = nil + +local function hasMessageHandler(message) + if not messageHandler then + return false + + elseif type(messageHandler) == "function" then + return true + + elseif type(messageHandler) == "table" then + return messageHandler[message] ~= nil + + else + assert(false) + end +end + +local function sendMessage(message, ...) + if not messageHandler then + return + + elseif type(messageHandler) == "function" then + local returnValues = pp.pack(messageHandler(message, ...)) + return pp.unpack(returnValues, 1, returnValues.n) + + elseif type(messageHandler) == "table" then + local _messageHandler = messageHandler[message] + if not _messageHandler then return end + + local returnValues = pp.pack(_messageHandler(...)) + return pp.unpack(returnValues, 1, returnValues.n) + + else + assert(false) + end +end + +if messageHandlerPath ~= "" then + -- Make the message handler and the metaprogram share the same environment. + -- This way the message handler can easily define globals that the metaprogram uses. + local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment) + if not mainChunk then + errorLine("Could not load message handler...\n"..pp.tryToFormatError(err)) + end + + messageHandler = mainChunk() + + if type(messageHandler) == "function" then + -- void + elseif type(messageHandler) == "table" then + for message, _messageHandler in pairs(messageHandler) do + if type(message) ~= "string" then + errorLine(messageHandlerPath..": Table of handlers must only contain messages as keys.") + elseif type(_messageHandler) ~= "function" then + errorLine(messageHandlerPath..": Table of handlers must only contain functions as values.") + end + end + else + errorLine(messageHandlerPath..": File did not return a table or a function.") + end +end + + + +-- Init stuff. +sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better? + +if not hasOutputPaths then + for i, pathIn in ipairs(pathsIn) do + pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "").."."..outputExtension + end +end + +if not pathsIn[1] then + errorLine("No path(s) specified.") +elseif #pathsIn ~= #pathsOut then + errorLine(F("Number of input and output paths differ. (%d in, %d out)", #pathsIn, #pathsOut)) +end + +local pathsSetIn = {} +local pathsSetOut = {} + +for i = 1, #pathsIn do + if pathsSetIn [pathsIn [i]] then errorLine("Duplicate input path: " ..pathsIn [i]) end + if pathsSetOut[pathsOut[i]] then errorLine("Duplicate output path: "..pathsOut[i]) end + + pathsSetIn [pathsIn [i]] = true + pathsSetOut[pathsOut[i]] = true + + if pathsIn [i] ~= "-" and pathsSetOut[pathsIn [i]] then errorLine("Path is both input and output: "..pathsIn [i]) end + if pathsOut[i] ~= "-" and pathsSetIn [pathsOut[i]] then errorLine("Path is both input and output: "..pathsOut[i]) end +end + + + +-- Process files. + +-- :SavedInfo +local processingInfo = { + date = os.date("%Y-%m-%d %H:%M:%S", startTime), + files = {}, +} + +local byteCount = 0 +local lineCount = 0 +local lineCountCode = 0 +local tokenCount = 0 + +for i, pathIn in ipairs(pathsIn) do + local startClockForPath = os.clock() + printfNoise("Processing '%s'...", pathIn) + + local pathOut = pathsOut[i] + local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "")..".meta.lua" + + if not outputMeta or pathOut == "-" then + pathMeta = nil + end + + local info, err = pp.processFile{ + pathIn = pathIn, + pathMeta = pathMeta, + pathOut = pathOut, + + debug = isDebug, + addLineNumbers = addLineNumbers, + + backtickStrings = allowBacktickStrings, + jitSyntax = allowJitSyntax, + canOutputNil = canOutputNil, + fastStrings = fastStrings, + validate = validate, + strictMacroArguments = strictMacroArguments, + + macroPrefix = macroPrefix, + macroSuffix = macroSuffix, + + release = releaseMode, + logLevel = maxLogLevel, + + onInsert = (hasMessageHandler("insert") or nil) and function(name) + local lua = sendMessage("insert", pathIn, name) + + -- onInsert() is expected to return a Lua code string and so is the message + -- handler. However, if the handler is a single catch-all function we allow + -- the message to not be handled and we fall back to the default behavior of + -- treating 'name' as a path to a file to be inserted. If we didn't allow this + -- then it would be required for the "insert" message to be handled. I think + -- it's better if the user can choose whether to handle a message or not! + -- + if lua == nil and type(messageHandler) == "function" then + return assert(pp.readFile(name)) + end + + return lua + end, + + onBeforeMeta = messageHandler and function(lua) + sendMessage("beforemeta", pathIn, lua) + end, + + onAfterMeta = messageHandler and function(lua) + local luaModified = sendMessage("aftermeta", pathIn, lua) + + if type(luaModified) == "string" then + lua = luaModified + + elseif luaModified ~= nil then + error(F( + "%s: Message handler did not return a string for 'aftermeta'. (Got %s)", + messageHandlerPath, type(luaModified) + )) + end + + return lua + end, + + onDone = messageHandler and function(info) + sendMessage("filedone", pathIn, pathOut, info) + end, + + onError = function(err) + xpcall(function() + sendMessage("fileerror", pathIn, err) + end, function(err) + printfError("Additional error in 'fileerror' message handler...\n%s", pp.tryToFormatError(err)) + end) + os.exit(1) + end, + } + assert(info, err) -- The onError() handler above should have been called and we should have exited already. + + byteCount = byteCount + info.processedByteCount + lineCount = lineCount + info.lineCount + lineCountCode = lineCountCode + info.linesOfCode + tokenCount = tokenCount + info.tokenCount + + if processingInfoPath ~= "" then + + -- :SavedInfo + table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains. + + end + + printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock()-startClockForPath) + printfNoise(("-"):rep(#header)) +end + + + +-- Finalize stuff. +if processingInfoPath ~= "" then + printfNoise("Saving processing info to '%s'.", processingInfoPath) + + local luaParts = {"return"} + assert(pp.serialize(luaParts, processingInfo)) + local lua = table.concat(luaParts) + + local file = assert(io.open(processingInfoPath, "wb")) + file:write(lua) + file:close() +end + +printfNoise( + "All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)", + os.clock()-startClock, + #pathsIn, (#pathsIn == 1) and "" or "s", + lineCountCode, + lineCount, (lineCount == 1) and "" or "s", + tokenCount, (tokenCount == 1) and "" or "s", + formatBytes(byteCount) +) + +sendMessage("alldone") -- @Incomplete: Use pcall and format error message better? + + + +--[[!=========================================================== + +Copyright © 2018-2022 Marcus 'ReFreezed' Thunström + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +==============================================================]] + diff --git a/lib/preprocess.lua b/lib/preprocess.lua new file mode 100644 index 0000000..52d8a5e --- /dev/null +++ b/lib/preprocess.lua @@ -0,0 +1,3910 @@ +--[[============================================================ +--= +--= LuaPreprocess v1.21-dev - preprocessing library +--= by Marcus 'ReFreezed' Thunström +--= +--= License: MIT (see the bottom of this file) +--= Website: http://refreezed.com/luapreprocess/ +--= Documentation: http://refreezed.com/luapreprocess/docs/ +--= +--= Tested with Lua 5.1, 5.2, 5.3, 5.4 and LuaJIT. +--= +--============================================================== + + API: + + Global functions in metaprograms: + - copyTable + - escapePattern + - getIndentation + - isProcessing + - pack + - pairsSorted + - printf + - readFile, writeFile, fileExists + - run + - sortNatural, compareNatural + - tokenize, newToken, concatTokens, removeUselessTokens, eachToken, isToken, getNextUsefulToken + - toLua, serialize, evaluate + Only during processing: + - getCurrentPathIn, getCurrentPathOut + - getOutputSoFar, getOutputSoFarOnLine, getOutputSizeSoFar, getCurrentLineNumberInOutput, getCurrentIndentationInOutput + - loadResource, callMacro + - outputValue, outputLua, outputLuaTemplate + - startInterceptingOutput, stopInterceptingOutput + Macros: + - ASSERT + - LOG + Search this file for 'EnvironmentTable' and 'PredefinedMacros' for more info. + + Exported stuff from the library: + - (all the functions above) + - VERSION + - metaEnvironment + - processFile, processString + Search this file for 'ExportTable' for more info. + +---------------------------------------------------------------- + + How to metaprogram: + + The exclamation mark (!) is used to indicate what code is part of + the metaprogram. There are 4 main ways to write metaprogram code: + + !... The line will simply run during preprocessing. The line can span multiple actual lines if it contains brackets. + !!... The line will appear in both the metaprogram and the final program. The line must be an assignment. + !(...) The result of the parenthesis will be outputted as a literal if it's an expression, otherwise it'll just run. + !!(...) The result of the expression in the parenthesis will be outputted as Lua code. The result must be a string. + + Short examples: + + !if not isDeveloper then + sendTelemetry() + !end + + !!local tau = 2*math.pi -- The expression will be evaluated in the metaprogram and the result will appear in the final program as a literal. + + local bigNumber = !(5^10) + + local font = !!(isDeveloper and "loadDevFont()" or "loadUserFont()") + + -- See the full documentation for additional features (like macros): + -- http://refreezed.com/luapreprocess/docs/extra-functionality/ + +---------------------------------------------------------------- + + -- Example program: + + -- Normal Lua. + local n = 0 + doTheThing() + + -- Preprocessor lines. + local n = 0 + !if math.random() < 0.5 then + n = n+10 -- Normal Lua. + -- Note: In the final program, this will be in the + -- same scope as 'local n = 0' here above. + !end + + !for i = 1, 3 do + print("3 lines with print().") + !end + + -- Extended preprocessor line. (Lines are consumed until brackets + -- are balanced when the end of the line has been reached.) + !newClass{ -- Starts here. + name = "Entity", + props = {x=0, y=0}, + } -- Ends here. + + -- Preprocessor block. + !( + local dogWord = "Woof " + function getDogText() + return dogWord:rep(3) + end + ) + + -- Preprocessor inline block. (Expression that returns a value.) + local text = !("The dog said: "..getDogText()) + + -- Preprocessor inline block variant. (Expression that returns a Lua code string.) + _G.!!("myRandomGlobal"..math.random(5)) = 99 + + -- Dual code (both preprocessor line and final output). + !!local partial = "Hello" + local whole = partial .. !(partial..", world!") + print(whole) -- HelloHello, world! + + -- Beware in preprocessor blocks that only call a single function! + !( func() ) -- This will bee seen as an inline block and output whatever value func() returns as a literal. + !( func(); ) -- If that's not wanted then a trailing `;` will prevent that. This line won't output anything by itself. + -- When the full metaprogram is generated, `!(func())` translates into `outputValue(func())` + -- while `!(func();)` simply translates into `func();` (because `outputValue(func();)` would be invalid Lua code). + -- Though in this specific case a preprocessor line (without the parenthesis) would be nicer: + !func() + + -- For the full documentation, see: + -- http://refreezed.com/luapreprocess/docs/ + +--============================================================]] + + + +local PP_VERSION = "1.21.0-dev" + +local MAX_DUPLICATE_FILE_INSERTS = 1000 -- @Incomplete: Make this a parameter for processFile()/processString(). +local MAX_CODE_LENGTH_IN_MESSAGES = 60 + +local KEYWORDS = { + "and","break","do","else","elseif","end","false","for","function","if","in", + "local","nil","not","or","repeat","return","then","true","until","while", + -- Lua 5.2 + "goto", -- @Incomplete: A parameter to disable this for Lua 5.1? +} for i, v in ipairs(KEYWORDS) do KEYWORDS[v], KEYWORDS[i] = true, nil end + +local PREPROCESSOR_KEYWORDS = { + "file","insert","line", +} for i, v in ipairs(PREPROCESSOR_KEYWORDS) do PREPROCESSOR_KEYWORDS[v], PREPROCESSOR_KEYWORDS[i] = true, nil end + +local PUNCTUATION = { + "+", "-", "*", "/", "%", "^", "#", + "==", "~=", "<=", ">=", "<", ">", "=", + "(", ")", "{", "}", "[", "]", + ";", ":", ",", ".", "..", "...", + -- Lua 5.2 + "::", + -- Lua 5.3 + "//", "&", "|", "~", ">>", "<<", +} for i, v in ipairs(PUNCTUATION) do PUNCTUATION[v], PUNCTUATION[i] = true, nil end + +local ESCAPE_SEQUENCES_EXCEPT_QUOTES = { + ["\a"] = [[\a]], + ["\b"] = [[\b]], + ["\f"] = [[\f]], + ["\n"] = [[\n]], + ["\r"] = [[\r]], + ["\t"] = [[\t]], + ["\v"] = [[\v]], + ["\\"] = [[\\]], +} +local ESCAPE_SEQUENCES = { + ["\""] = [[\"]], + ["\'"] = [[\']], +} for k, v in pairs(ESCAPE_SEQUENCES_EXCEPT_QUOTES) do ESCAPE_SEQUENCES[k] = v end + +local USELESS_TOKENS = {whitespace=true, comment=true} + +local LOG_LEVELS = { + ["off" ] = 0, + ["error" ] = 1, + ["warning"] = 2, + ["info" ] = 3, + ["debug" ] = 4, + ["trace" ] = 5, +} + +local metaEnv = nil +local dummyEnv = {} + +-- Controlled by processFileOrString(): +local current_parsingAndMeta_isProcessing = false +local current_parsingAndMeta_isDebug = false + +-- Controlled by _processFileOrString(): +local current_anytime_isRunningMeta = false +local current_anytime_pathIn = "" +local current_anytime_pathOut = "" +local current_anytime_fastStrings = false +local current_parsing_insertCount = 0 +local current_parsingAndMeta_onInsert = nil +local current_parsingAndMeta_resourceCache = nil +local current_parsingAndMeta_addLineNumbers = false +local current_parsingAndMeta_macroPrefix = "" +local current_parsingAndMeta_macroSuffix = "" +local current_parsingAndMeta_strictMacroArguments = true +local current_meta_pathForErrorMessages = "" +local current_meta_output = nil -- Top item in current_meta_outputStack. +local current_meta_outputStack = nil +local current_meta_canOutputNil = true +local current_meta_releaseMode = false +local current_meta_maxLogLevel = "trace" +local current_meta_locationTokens = nil + + + +--============================================================== +--= Local Functions ============================================ +--============================================================== + +local assertarg +local countString, countSubString +local getLineNumber +local loadLuaString +local maybeOutputLineNumber +local sortNatural +local tableInsert, tableRemove, tableInsertFormat +local utf8GetCodepointAndLength + + + +local F = string.format + +local function tryToFormatError(err0) + local err, path, ln = nil + + if type(err0) == "string" then + do path, ln, err = err0:match"^(%a:[%w_/\\.]+):(%d+): (.*)" + if not err then path, ln, err = err0:match"^([%w_/\\.]+):(%d+): (.*)" + if not err then path, ln, err = err0:match"^(%S-):(%d+): (.*)" + end end end + end + + if err then + return F("Error @ %s:%s: %s", path, ln, err) + else + return "Error: "..tostring(err0) + end +end + + + +local function printf(s, ...) + print(F(s, ...)) +end + +-- printTokens( tokens [, filterUselessTokens ] ) +local function printTokens(tokens, filter) + for i, tok in ipairs(tokens) do + if not (filter and USELESS_TOKENS[tok.type]) then + printf("%d %-12s '%s'", i, tok.type, (F("%q", tostring(tok.value)):sub(2, -2):gsub("\\\n", "\\n"))) + end + end +end + +local function printError(s) + io.stderr:write(s, "\n") +end +local function printfError(s, ...) + printError(F(s, ...)) +end + +-- message = formatTraceback( [ level=1 ] ) +local function formatTraceback(level) + local buffer = {} + tableInsert(buffer, "stack traceback:\n") + + level = 1 + (level or 1) + local stack = {} + + while level < 1/0 do + local info = debug.getinfo(level, "nSl") + if not info then break end + + local isFile = info.source:find"^@" ~= nil + local sourceName = (isFile and info.source:sub(2) or info.short_src) + + local subBuffer = {"\t"} + tableInsertFormat(subBuffer, "%s:", sourceName) + + if info.currentline > 0 then + tableInsertFormat(subBuffer, "%d:", info.currentline) + end + + if (info.name or "") ~= "" then + tableInsertFormat(subBuffer, " in '%s'", info.name) + elseif info.what == "main" then + tableInsert(subBuffer, " in main chunk") + elseif info.what == "C" or info.what == "tail" then + tableInsert(subBuffer, " ?") + else + tableInsertFormat(subBuffer, " in <%s:%d>", sourceName:gsub("^.*[/\\]", ""), info.linedefined) + end + + tableInsert(stack, table.concat(subBuffer)) + level = level + 1 + end + + while stack[#stack] == "\t[C]: ?" do + stack[#stack] = nil + end + + for _, s in ipairs(stack) do + tableInsert(buffer, s) + tableInsert(buffer, "\n") + end + + return table.concat(buffer) +end + +-- printErrorTraceback( message [, level=1 ] ) +local function printErrorTraceback(message, level) + printError(tryToFormatError(message)) + printError(formatTraceback(1+(level or 1))) +end + +-- debugExit( ) +-- debugExit( messageValue ) +-- debugExit( messageFormat, ... ) +local function debugExit(...) + if select("#", ...) > 1 then + printfError(...) + elseif select("#", ...) == 1 then + printError(...) + end + os.exit(2) +end + + + +-- errorf( [ level=1, ] string, ... ) +local function errorf(sOrLevel, ...) + if type(sOrLevel) == "number" then + error(F(...), (sOrLevel == 0 and 0 or 1+sOrLevel)) + else + error(F(sOrLevel, ...), 2) + end +end + +-- local function errorLine(err) -- Unused. +-- if type(err) ~= "string" then error(err) end +-- error("\0"..err, 0) -- The 0 tells our own error handler not to print the traceback. +-- end +local function errorfLine(s, ...) + errorf(0, (current_parsingAndMeta_isProcessing and "\0" or "")..s, ...) -- The \0 tells our own error handler not to print the traceback. +end + +-- errorOnLine( path, lineNumber, agent=nil, s, ... ) +local function errorOnLine(path, ln, agent, s, ...) + s = F(s, ...) + if agent then + errorfLine("%s:%d: [%s] %s", path, ln, agent, s) + else + errorfLine("%s:%d: %s", path, ln, s) + end +end + +local errorInFile, runtimeErrorInFile +do + local function findStartOfLine(s, pos, canBeEmpty) + while pos > 1 do + if s:byte(pos-1) == 10--[[\n]] and (canBeEmpty or s:byte(pos) ~= 10--[[\n]]) then break end + pos = pos - 1 + end + return math.max(pos, 1) + end + local function findEndOfLine(s, pos) + while pos < #s do + if s:byte(pos+1) == 10--[[\n]] then break end + pos = pos + 1 + end + return math.min(pos, #s) + end + + local function _errorInFile(level, contents, path, pos, agent, s, ...) + s = F(s, ...) + + pos = math.min(math.max(pos, 1), #contents+1) + local ln = getLineNumber(contents, pos) + + local lineStart = findStartOfLine(contents, pos, true) + local lineEnd = findEndOfLine (contents, pos-1) + local linePre1Start = findStartOfLine(contents, lineStart-1, false) + local linePre1End = findEndOfLine (contents, linePre1Start-1) + local linePre2Start = findStartOfLine(contents, linePre1Start-1, false) + local linePre2End = findEndOfLine (contents, linePre2Start-1) + -- printfError("pos %d | lines %d..%d, %d..%d, %d..%d", pos, linePre2Start,linePre2End+1, linePre1Start,linePre1End+1, lineStart,lineEnd+1) -- DEBUG + + errorOnLine(path, ln, agent, "%s\n>\n%s%s%s>-%s^%s", + s, + (linePre2Start < linePre1Start and linePre2Start <= linePre2End) and F("> %s\n", (contents:sub(linePre2Start, linePre2End):gsub("\t", " "))) or "", + (linePre1Start < lineStart and linePre1Start <= linePre1End) and F("> %s\n", (contents:sub(linePre1Start, linePre1End):gsub("\t", " "))) or "", + ( lineStart <= lineEnd ) and F("> %s\n", (contents:sub(lineStart, lineEnd ):gsub("\t", " "))) or ">\n", + ("-"):rep(pos - lineStart + 3*countSubString(contents, lineStart, lineEnd, "\t", true)), + (level and "\n"..formatTraceback(1+level) or "") + ) + end + + -- errorInFile( contents, path, pos, agent, s, ... ) + --[[local]] function errorInFile(...) + _errorInFile(nil, ...) + end + + -- runtimeErrorInFile( level, contents, path, pos, agent, s, ... ) + --[[local]] function runtimeErrorInFile(level, ...) + _errorInFile(1+level, ...) + end +end + +-- errorAtToken( token, position=token.position, agent, s, ... ) +local function errorAtToken(tok, pos, agent, s, ...) + -- printErrorTraceback("errorAtToken", 2) -- DEBUG + errorInFile(current_parsingAndMeta_resourceCache[tok.file], tok.file, (pos or tok.position), agent, s, ...) +end + +-- errorAfterToken( token, agent, s, ... ) +local function errorAfterToken(tok, agent, s, ...) + -- printErrorTraceback("errorAfterToken", 2) -- DEBUG + errorInFile(current_parsingAndMeta_resourceCache[tok.file], tok.file, tok.position+#tok.representation, agent, s, ...) +end + +-- runtimeErrorAtToken( level, token, position=token.position, agent, s, ... ) +local function runtimeErrorAtToken(level, tok, pos, agent, s, ...) + -- printErrorTraceback("runtimeErrorAtToken", 2) -- DEBUG + runtimeErrorInFile(1+level, current_parsingAndMeta_resourceCache[tok.file], tok.file, (pos or tok.position), agent, s, ...) +end + +-- internalError( [ message|value ] ) +local function internalError(message) + message = message and " ("..tostring(message)..")" or "" + error("Internal error."..message, 2) +end + + + +local function cleanError(err) + if type(err) == "string" then + err = err:gsub("%z", "") + end + return err +end + + + +local function formatCodeForShortMessage(lua) + lua = lua:gsub("^%s+", ""):gsub("%s+$", ""):gsub("%s+", " ") + + if #lua > MAX_CODE_LENGTH_IN_MESSAGES then + lua = lua:sub(1, MAX_CODE_LENGTH_IN_MESSAGES/2) .. "..." .. lua:sub(-MAX_CODE_LENGTH_IN_MESSAGES/2) + end + + return lua +end + + + +local ERROR_UNFINISHED_STRINGLIKE = 1 + +local function parseStringlikeToken(s, ptr) + local reprStart = ptr + local reprEnd + + local valueStart + local valueEnd + + local longEqualSigns = s:match("^%[(=*)%[", ptr) + local isLong = longEqualSigns ~= nil + + -- Single line. + if not isLong then + valueStart = ptr + + local i = s:find("\n", ptr, true) + if not i then + reprEnd = #s + valueEnd = #s + ptr = reprEnd + 1 + else + reprEnd = i + valueEnd = i - 1 + ptr = reprEnd + 1 + end + + -- Multiline. + else + ptr = ptr + 1 + #longEqualSigns + 1 + valueStart = ptr + + local i1, i2 = s:find("]"..longEqualSigns.."]", ptr, true) + if not i1 then + return nil, ERROR_UNFINISHED_STRINGLIKE + end + + reprEnd = i2 + valueEnd = i1 - 1 + ptr = reprEnd + 1 + end + + local repr = s:sub(reprStart, reprEnd) + local v = s:sub(valueStart, valueEnd) + local tok = {type="stringlike", representation=repr, value=v, long=isLong} + + return tok, ptr +end + + + +local NUM_HEX_FRAC_EXP = ("^( 0[Xx] (%x*) %.(%x+) [Pp]([-+]?%x+) )"):gsub(" +", "") +local NUM_HEX_FRAC = ("^( 0[Xx] (%x*) %.(%x+) )"):gsub(" +", "") +local NUM_HEX_EXP = ("^( 0[Xx] (%x+) %.? [Pp]([-+]?%x+) )"):gsub(" +", "") +local NUM_HEX = ("^( 0[Xx] %x+ %.? )"):gsub(" +", "") +local NUM_DEC_FRAC_EXP = ("^( %d* %.%d+ [Ee][-+]?%d+ )"):gsub(" +", "") +local NUM_DEC_FRAC = ("^( %d* %.%d+ )"):gsub(" +", "") +local NUM_DEC_EXP = ("^( %d+ %.? [Ee][-+]?%d+ )"):gsub(" +", "") +local NUM_DEC = ("^( %d+ %.? )"):gsub(" +", "") + +-- tokens = _tokenize( luaString, path, allowPreprocessorTokens, allowBacktickStrings, allowJitSyntax ) +local function _tokenize(s, path, allowPpTokens, allowBacktickStrings, allowJitSyntax) + s = s:gsub("\r", "") -- Normalize line breaks. (Assume the input is either "\n" or "\r\n".) + + local tokens = {} + local ptr = 1 + local ln = 1 + + while ptr <= #s do + local tok + local tokenPos = ptr + + -- Whitespace. + if s:find("^%s", ptr) then + local i1, i2, whitespace = s:find("^(%s+)", ptr) + + ptr = i2+1 + tok = {type="whitespace", representation=whitespace, value=whitespace} + + -- Identifier/keyword. + elseif s:find("^[%a_]", ptr) then + local i1, i2, word = s:find("^([%a_][%w_]*)", ptr) + ptr = i2+1 + + if KEYWORDS[word] then + tok = {type="keyword", representation=word, value=word} + else + tok = {type="identifier", representation=word, value=word} + end + + -- Number (binary). + elseif s:find("^0b", ptr) then + if not allowJitSyntax then + errorInFile(s, path, ptr, "Tokenizer", "Encountered binary numeral. (Feature not enabled.)") + end + + local i1, i2, numStr = s:find("^(..[01]+)", ptr) + + -- @Copypaste from below. + if not numStr then + errorInFile(s, path, ptr, "Tokenizer", "Malformed number.") + end + + local numStrFallback = numStr + + do + if s:find("^[Ii]", i2+1) then -- Imaginary part of complex number. + numStr = s:sub(i1, i2+1) + i2 = i2 + 1 + + elseif s:find("^[Uu][Ll][Ll]", i2+1) then -- Unsigned 64-bit integer. + numStr = s:sub(i1, i2+3) + i2 = i2 + 3 + elseif s:find("^[Ll][Ll]", i2+1) then -- Signed 64-bit integer. + numStr = s:sub(i1, i2+2) + i2 = i2 + 2 + end + end + + local n = tonumber(numStr) or tonumber(numStrFallback) or tonumber(numStrFallback:sub(3), 2) + + if not n then + errorInFile(s, path, ptr, "Tokenizer", "Invalid number.") + end + + if s:find("^[%w_]", i2+1) then + -- This is actually not an error in Lua 5.2 and 5.3. Maybe we should issue a warning instead of an error here? + errorInFile(s, path, i2+1, "Tokenizer", "Malformed number.") + end + + ptr = i2 + 1 + tok = {type="number", representation=numStrFallback, value=n} + + -- Number. + elseif s:find("^%.?%d", ptr) then + local pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX_FRAC_EXP, false, true , s:find(NUM_HEX_FRAC_EXP, ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX_FRAC , false, true , s:find(NUM_HEX_FRAC , ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX_EXP , false, true , s:find(NUM_HEX_EXP , ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_HEX , true , false, s:find(NUM_HEX , ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_DEC_FRAC_EXP, false, false, s:find(NUM_DEC_FRAC_EXP, ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_DEC_FRAC , false, false, s:find(NUM_DEC_FRAC , ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_DEC_EXP , false, false, s:find(NUM_DEC_EXP , ptr) + if not i1 then pat, maybeInt, lua52Hex, i1, i2, numStr = NUM_DEC , true , false, s:find(NUM_DEC , ptr) + end end end end end end end + + if not numStr then + errorInFile(s, path, ptr, "Tokenizer", "Malformed number.") + end + + local numStrFallback = numStr + + if allowJitSyntax then + if s:find("^[Ii]", i2+1) then -- Imaginary part of complex number. + numStr = s:sub(i1, i2+1) + i2 = i2 + 1 + + elseif not maybeInt or numStr:find(".", 1, true) then + -- void + + elseif s:find("^[Uu][Ll][Ll]", i2+1) then -- Unsigned 64-bit integer. + numStr = s:sub(i1, i2+3) + i2 = i2 + 3 + elseif s:find("^[Ll][Ll]", i2+1) then -- Signed 64-bit integer. + numStr = s:sub(i1, i2+2) + i2 = i2 + 2 + end + end + + local n = tonumber(numStr) or tonumber(numStrFallback) + + -- Support hexadecimal floats in Lua 5.1. + if not n and lua52Hex then + -- Note: We know we're not running LuaJIT here as it supports hexadecimal floats, thus we use numStrFallback instead of numStr. + local _, intStr, fracStr, expStr + if pat == NUM_HEX_FRAC_EXP then _, intStr, fracStr, expStr = numStrFallback:match(NUM_HEX_FRAC_EXP) + elseif pat == NUM_HEX_FRAC then _, intStr, fracStr = numStrFallback:match(NUM_HEX_FRAC) ; expStr = "0" + elseif pat == NUM_HEX_EXP then _, intStr, expStr = numStrFallback:match(NUM_HEX_EXP) ; fracStr = "" + else internalError() end + + n = tonumber(intStr, 16) or 0 -- intStr may be "". + + local fracValue = 1 + for i = 1, #fracStr do + fracValue = fracValue/16 + n = n+tonumber(fracStr:sub(i, i), 16)*fracValue + end + + n = n*2^expStr:gsub("^+", "") + end + + if not n then + errorInFile(s, path, ptr, "Tokenizer", "Invalid number.") + end + + if s:find("^[%w_]", i2+1) then + -- This is actually not an error in Lua 5.2 and 5.3. Maybe we should issue a warning instead of an error here? + errorInFile(s, path, i2+1, "Tokenizer", "Malformed number.") + end + + ptr = i2+1 + tok = {type="number", representation=numStrFallback, value=n} + + -- Comment. + elseif s:find("^%-%-", ptr) then + local reprStart = ptr + ptr = ptr+2 + + tok, ptr = parseStringlikeToken(s, ptr) + if not tok then + local errCode = ptr + if errCode == ERROR_UNFINISHED_STRINGLIKE then + errorInFile(s, path, reprStart, "Tokenizer", "Unfinished long comment.") + else + errorInFile(s, path, reprStart, "Tokenizer", "Invalid comment.") + end + end + + if tok.long then + -- Check for nesting of [[...]], which is deprecated in Lua. + local chunk, err = loadLuaString("--"..tok.representation, "@", nil) + + if not chunk then + local lnInString, luaErr = err:match'^:(%d+): (.*)' + if luaErr then + errorOnLine(path, getLineNumber(s, reprStart)+tonumber(lnInString)-1, "Tokenizer", "Malformed long comment. (%s)", luaErr) + else + errorInFile(s, path, reprStart, "Tokenizer", "Malformed long comment.") + end + end + end + + tok.type = "comment" + tok.representation = s:sub(reprStart, ptr-1) + + -- String (short). + elseif s:find([=[^["']]=], ptr) then + local reprStart = ptr + local reprEnd + + local quoteChar = s:sub(ptr, ptr) + ptr = ptr+1 + + local valueStart = ptr + local valueEnd + + while true do + local c = s:sub(ptr, ptr) + + if c == "" then + errorInFile(s, path, reprStart, "Tokenizer", "Unfinished string.") + + elseif c == quoteChar then + reprEnd = ptr + valueEnd = ptr-1 + ptr = reprEnd+1 + break + + elseif c == "\\" then + -- Note: We don't have to look for multiple characters after + -- the escape, like \nnn - this algorithm works anyway. + if ptr+1 > #s then + errorInFile(s, path, reprStart, "Tokenizer", "Unfinished string after escape.") + end + ptr = ptr+2 + + elseif c == "\n" then + -- Can't have unescaped newlines. Lua, this is a silly rule! @Ugh + errorInFile(s, path, ptr, "Tokenizer", "Newlines must be escaped in strings.") + + else + ptr = ptr+1 + end + end + + local repr = s:sub(reprStart, reprEnd) + + local valueChunk = loadLuaString("return"..repr, nil, nil) + if not valueChunk then + errorInFile(s, path, reprStart, "Tokenizer", "Malformed string.") + end + + local v = valueChunk() + assert(type(v) == "string") + + tok = {type="string", representation=repr, value=valueChunk(), long=false} + + -- Long string. + elseif s:find("^%[=*%[", ptr) then + local reprStart = ptr + + tok, ptr = parseStringlikeToken(s, ptr) + if not tok then + local errCode = ptr + if errCode == ERROR_UNFINISHED_STRINGLIKE then + errorInFile(s, path, reprStart, "Tokenizer", "Unfinished long string.") + else + errorInFile(s, path, reprStart, "Tokenizer", "Invalid long string.") + end + end + + -- Check for nesting of [[...]], which is deprecated in Lua. + local valueChunk, err = loadLuaString("return"..tok.representation, "@", nil) + + if not valueChunk then + local lnInString, luaErr = err:match'^:(%d+): (.*)' + if luaErr then + errorOnLine(path, getLineNumber(s, reprStart)+tonumber(lnInString)-1, "Tokenizer", "Malformed long string. (%s)", luaErr) + else + errorInFile(s, path, reprStart, "Tokenizer", "Malformed long string.") + end + end + + local v = valueChunk() + assert(type(v) == "string") + + tok.type = "string" + tok.value = v + + -- Backtick string. + elseif s:find("^`", ptr) then + if not allowBacktickStrings then + errorInFile(s, path, ptr, "Tokenizer", "Encountered backtick string. (Feature not enabled.)") + end + + local i1, i2, repr, v = s:find("^(`([^`]*)`)", ptr) + if not i2 then + errorInFile(s, path, ptr, "Tokenizer", "Unfinished backtick string.") + end + + ptr = i2+1 + tok = {type="string", representation=repr, value=v, long=false} + + -- Punctuation etc. + elseif s:find("^%.%.%.", ptr) then -- 3 + local repr = s:sub(ptr, ptr+2) + tok = {type="punctuation", representation=repr, value=repr} + ptr = ptr+#repr + elseif s:find("^%.%.", ptr) or s:find("^[=~<>]=", ptr) or s:find("^::", ptr) or s:find("^//", ptr) or s:find("^<<", ptr) or s:find("^>>", ptr) then -- 2 + local repr = s:sub(ptr, ptr+1) + tok = {type="punctuation", representation=repr, value=repr} + ptr = ptr+#repr + elseif s:find("^[+%-*/%%^#<>=(){}[%];:,.&|~]", ptr) then -- 1 + local repr = s:sub(ptr, ptr) + tok = {type="punctuation", representation=repr, value=repr} + ptr = ptr+#repr + + -- Preprocessor entry. + elseif s:find("^!", ptr) then + if not allowPpTokens then + errorInFile(s, path, ptr, "Tokenizer", "Encountered preprocessor entry. (Feature not enabled.)") + end + + local double = s:find("^!", ptr+1) ~= nil + local repr = s:sub(ptr, ptr+(double and 1 or 0)) + + tok = {type="pp_entry", representation=repr, value=repr, double=double} + ptr = ptr+#repr + + -- Preprocessor keyword. + elseif s:find("^@", ptr) then + if not allowPpTokens then + errorInFile(s, path, ptr, "Tokenizer", "Encountered preprocessor keyword. (Feature not enabled.)") + end + + if s:find("^@@", ptr) then + ptr = ptr+2 + tok = {type="pp_keyword", representation="@@", value="insert"} + else + local i1, i2, repr, word = s:find("^(@([%a_][%w_]*))", ptr) + if not i1 then + errorInFile(s, path, ptr+1, "Tokenizer", "Expected an identifier.") + elseif not PREPROCESSOR_KEYWORDS[word] then + errorInFile(s, path, ptr+1, "Tokenizer", "Invalid preprocessor keyword '%s'.", word) + end + ptr = i2+1 + tok = {type="pp_keyword", representation=repr, value=word} + end + + -- Preprocessor symbol. + elseif s:find("^%$", ptr) then + if not allowPpTokens then + errorInFile(s, path, ptr, "Tokenizer", "Encountered preprocessor symbol. (Feature not enabled.)") + end + + local i1, i2, repr, word = s:find("^(%$([%a_][%w_]*))", ptr) + if not i1 then + errorInFile(s, path, ptr+1, "Tokenizer", "Expected an identifier.") + elseif KEYWORDS[word] then + errorInFile(s, path, ptr+1, "Tokenizer", "Invalid preprocessor symbol '%s'. (Must not be a Lua keyword.)", word) + end + ptr = i2+1 + tok = {type="pp_symbol", representation=repr, value=word} + + else + errorInFile(s, path, ptr, "Tokenizer", "Unknown character.") + end + + tok.line = ln + tok.position = tokenPos + tok.file = path + + ln = ln+countString(tok.representation, "\n", true) + tok.lineEnd = ln + + tableInsert(tokens, tok) + -- print(#tokens, tok.type, tok.representation) -- DEBUG + end + + return tokens +end + + + +-- luaString = _concatTokens( tokens, lastLn=nil, addLineNumbers, fromIndex=1, toIndex=#tokens ) +local function _concatTokens(tokens, lastLn, addLineNumbers, i1, i2) + local parts = {} + + if addLineNumbers then + for i = (i1 or 1), (i2 or #tokens) do + local tok = tokens[i] + lastLn = maybeOutputLineNumber(parts, tok, lastLn) + tableInsert(parts, tok.representation) + end + + else + for i = (i1 or 1), (i2 or #tokens) do + tableInsert(parts, tokens[i].representation) + end + end + + return table.concat(parts) +end + +local function insertTokenRepresentations(parts, tokens, i1, i2) + for i = i1, i2 do + tableInsert(parts, tokens[i].representation) + end +end + + + +local function readFile(path, isTextFile) + assertarg(1, path, "string") + assertarg(2, isTextFile, "boolean","nil") + + local file, err = io.open(path, "r"..(isTextFile and "" or "b")) + if not file then return nil, err end + + local contents = file:read"*a" + file:close() + return contents +end + +-- success, error = writeFile( path, [ isTextFile=false, ] contents ) +local function writeFile(path, isTextFile, contents) + assertarg(1, path, "string") + + if type(isTextFile) == "boolean" then + assertarg(3, contents, "string") + else + isTextFile, contents = false, isTextFile + assertarg(2, contents, "string") + end + + local file, err = io.open(path, "w"..(isTextFile and "" or "b")) + if not file then return false, err end + + file:write(contents) + file:close() + return true +end + +local function fileExists(path) + assertarg(1, path, "string") + + local file = io.open(path, "r") + if not file then return false end + + file:close() + return true +end + + + +-- assertarg( argumentNumber, value, expectedValueType1, ... ) +--[[local]] function assertarg(n, v, ...) + local vType = type(v) + + for i = 1, select("#", ...) do + if vType == select(i, ...) then return end + end + + local fName = debug.getinfo(2, "n").name + local expects = table.concat({...}, " or ") + + if fName == "" then fName = "?" end + + errorf(3, "bad argument #%d to '%s' (%s expected, got %s)", n, fName, expects, vType) +end + + + +-- count = countString( haystack, needle [, plain=false ] ) +--[[local]] function countString(s, needle, plain) + local count = 0 + local i = 0 + local _ + + while true do + _, i = s:find(needle, i+1, plain) + if not i then return count end + + count = count+1 + end +end + +-- count = countSubString( string, startPosition, endPosition, needle [, plain=false ] ) +--[[local]] function countSubString(s, pos, posEnd, needle, plain) + local count = 0 + + while true do + local _, i2 = s:find(needle, pos, plain) + if not i2 or i2 > posEnd then return count end + + count = count + 1 + pos = i2 + 1 + end +end + + + +local getfenv = getfenv or function(f) -- Assume Lua is version 5.2+ if getfenv() doesn't exist. + f = f or 1 + + if type(f) == "function" then + -- void + + elseif type(f) == "number" then + if f == 0 then return _ENV end + if f < 0 then error("bad argument #1 to 'getfenv' (level must be non-negative)") end + + f = debug.getinfo(1+f, "f") or error("bad argument #1 to 'getfenv' (invalid level)") + f = f.func + + else + error("bad argument #1 to 'getfenv' (number expected, got "..type(f)..")") + end + + for i = 1, 1/0 do + local name, v = debug.getupvalue(f, i) + if name == "_ENV" then return v end + if not name then return _ENV end + end +end + + + +-- (Table generated by misc/generateStringEscapeSequenceInfo.lua) +local UNICODE_RANGES_NOT_TO_ESCAPE = { + {from=32, to=126}, + {from=161, to=591}, + {from=880, to=887}, + {from=890, to=895}, + {from=900, to=906}, + {from=908, to=908}, + {from=910, to=929}, + {from=931, to=1154}, + {from=1162, to=1279}, + {from=7682, to=7683}, + {from=7690, to=7691}, + {from=7710, to=7711}, + {from=7744, to=7745}, + {from=7766, to=7767}, + {from=7776, to=7777}, + {from=7786, to=7787}, + {from=7808, to=7813}, + {from=7835, to=7835}, + {from=7922, to=7923}, + {from=8208, to=8208}, + {from=8210, to=8231}, + {from=8240, to=8286}, + {from=8304, to=8305}, + {from=8308, to=8334}, + {from=8336, to=8348}, + {from=8352, to=8383}, + {from=8448, to=8587}, + {from=8592, to=9254}, + {from=9312, to=10239}, + {from=10496, to=11007}, + {from=64256, to=64262}, +} + +local function shouldCodepointBeEscaped(cp) + for _, range in ipairs(UNICODE_RANGES_NOT_TO_ESCAPE) do -- @Speed: Don't use a loop? + if cp >= range.from and cp <= range.to then return false end + end + return true +end + +-- local cache = setmetatable({}, {__mode="kv"}) -- :SerializationCache (This doesn't seem to speed things up.) + +-- success, error = serialize( buffer, value ) +local function serialize(buffer, v) + --[[ :SerializationCache + if cache[v] then + tableInsert(buffer, cache[v]) + return true + end + local bufferStart = #buffer + 1 + --]] + + local vType = type(v) + + if vType == "table" then + local first = true + tableInsert(buffer, "{") + + local indices = {} + for i, item in ipairs(v) do + if not first then tableInsert(buffer, ",") end + first = false + + local ok, err = serialize(buffer, item) + if not ok then return false, err end + + indices[i] = true + end + + local keys = {} + for k, item in pairs(v) do + if indices[k] then + -- void + elseif type(k) == "table" then + return false, "Table keys cannot be tables." + else + tableInsert(keys, k) + end + end + + table.sort(keys, function(a, b) + return tostring(a) < tostring(b) + end) + + for _, k in ipairs(keys) do + local item = v[k] + + if not first then tableInsert(buffer, ",") end + first = false + + if not KEYWORDS[k] and type(k) == "string" and k:find"^[%a_][%w_]*$" then + tableInsert(buffer, k) + tableInsert(buffer, "=") + + else + tableInsert(buffer, "[") + + local ok, err = serialize(buffer, k) + if not ok then return false, err end + + tableInsert(buffer, "]=") + end + + local ok, err = serialize(buffer, item) + if not ok then return false, err end + end + + tableInsert(buffer, "}") + + elseif vType == "string" then + if v == "" then + tableInsert(buffer, '""') + return true + end + + local useApostrophe = v:find('"', 1, true) and not v:find("'", 1, true) + local quote = useApostrophe and "'" or '"' + + tableInsert(buffer, quote) + + if current_anytime_fastStrings or not v:find"[^\32-\126\t\n]" then + -- print(">> FAST", #v) -- DEBUG + + local s = v:gsub((useApostrophe and "[\t\n\\']" or '[\t\n\\"]'), function(c) + return ESCAPE_SEQUENCES[c] or internalError(c:byte()) + end) + tableInsert(buffer, s) + + else + -- print(">> SLOW", #v) -- DEBUG + local pos = 1 + + -- @Speed: There are optimizations to be made here! + while pos <= #v do + local c = v:sub(pos, pos) + local cp, len = utf8GetCodepointAndLength(v, pos) + + -- Named escape sequences. + if ESCAPE_SEQUENCES_EXCEPT_QUOTES[c] then tableInsert(buffer, ESCAPE_SEQUENCES_EXCEPT_QUOTES[c]) ; pos = pos+1 + elseif c == quote then tableInsert(buffer, [[\]]) ; tableInsert(buffer, quote) ; pos = pos+1 + + -- UTF-8 character. + elseif len == 1 and not shouldCodepointBeEscaped(cp) then tableInsert(buffer, v:sub(pos, pos )) ; pos = pos+1 -- @Speed: We can insert multiple single-byte characters sometimes! + elseif len and not shouldCodepointBeEscaped(cp) then tableInsert(buffer, v:sub(pos, pos+len-1)) ; pos = pos+len + + -- Anything else. + else + tableInsert(buffer, F((v:find("^%d", pos+1) and "\\%03d" or "\\%d"), v:byte(pos))) + pos = pos + 1 + end + end + end + + tableInsert(buffer, quote) + + elseif v == 1/0 then + tableInsert(buffer, "(1/0)") + elseif v == -1/0 then + tableInsert(buffer, "(-1/0)") + elseif v ~= v then + tableInsert(buffer, "(0/0)") -- NaN. + elseif v == 0 then + tableInsert(buffer, "0") -- In case it's actually -0 for some reason, which would be silly to output. + elseif vType == "number" then + if v < 0 then + tableInsert(buffer, " ") -- The space prevents an accidental comment if a "-" is right before. + end + tableInsert(buffer, tostring(v)) -- (I'm not sure what precision tostring() uses for numbers. Maybe we should use string.format() instead.) + + elseif vType == "boolean" or v == nil then + tableInsert(buffer, tostring(v)) + + else + return false, F("Cannot serialize value of type '%s'. (%s)", vType, tostring(v)) + end + + --[[ :SerializationCache + if v ~= nil then + cache[v] = table.concat(buffer, "", bufferStart, #buffer) + end + --]] + + return true +end + +-- luaString = toLua( value ) +-- Returns nil and a message on error. +local function toLua(v) + local buffer = {} + + local ok, err = serialize(buffer, v) + if not ok then return nil, err end + + return table.concat(buffer) +end + +-- value = evaluate( expression [, environment=getfenv() ] ) +-- Returns nil and a message on error. +local function evaluate(expr, env) + local chunk, err = loadLuaString("return("..expr.."\n)", "@", (env or getfenv(2))) + if not chunk then + return nil, F("Invalid expression '%s'. (%s)", expr, (err:gsub("^:%d+: ", ""))) + end + + local ok, valueOrErr = pcall(chunk) + if not ok then return nil, valueOrErr end + + return valueOrErr -- May be nil or false! +end + + + +local function escapePattern(s) + return (s:gsub("[-+*^?$.%%()[%]]", "%%%0")) +end + + + +local function outputLineNumber(parts, ln) + tableInsert(parts, "--[[@") + tableInsert(parts, ln) + tableInsert(parts, "]]") +end + +--[[local]] function maybeOutputLineNumber(parts, tok, lastLn) + if tok.line == lastLn or USELESS_TOKENS[tok.type] then return lastLn end + + outputLineNumber(parts, tok.line) + return tok.line +end +--[=[ +--[[local]] function maybeOutputLineNumber(parts, tok, lastLn, fromMetaToOutput) + if tok.line == lastLn or USELESS_TOKENS[tok.type] then return lastLn end + + if fromMetaToOutput then + tableInsert(parts, '__LUA"--[[@'..tok.line..']]"\n') + else + tableInsert(parts, "--[[@"..tok.line.."]]") + end + return tok.line +end +]=] + + + +local function isAny(v, ...) + for i = 1, select("#", ...) do + if v == select(i, ...) then return true end + end + return false +end + + + +local function errorIfNotRunningMeta(level) + if not current_anytime_isRunningMeta then + error("No file is being processed.", 1+level) + end +end + + + +local function copyArray(t) + local copy = {} + for i, v in ipairs(t) do + copy[i] = v + end + return copy +end + +local copyTable +do + local function deepCopy(t, copy, tableCopies) + for k, v in pairs(t) do + if type(v) == "table" then + local vCopy = tableCopies[v] + + if vCopy then + copy[k] = vCopy + else + vCopy = {} + tableCopies[v] = vCopy + copy[k] = deepCopy(v, vCopy, tableCopies) + end + + else + copy[k] = v + end + end + return copy + end + + -- copy = copyTable( table [, deep=false ] ) + --[[local]] function copyTable(t, deep) + local copy = {} + + if deep then + return deepCopy(t, copy, {[t]=copy}) + end + + for k, v in pairs(t) do copy[k] = v end + + return copy + end +end + + + +-- values = pack( value1, ... ) +-- values.n is the amount of values (which can be zero). +local pack = ( + (_VERSION >= "Lua 5.2" or jit) and table.pack + or function(...) + return {n=select("#", ...), ...} + end +) + +local unpack = (_VERSION >= "Lua 5.2") and table.unpack or _G.unpack + + + +--[[local]] loadLuaString = ( + (_VERSION >= "Lua 5.2" or jit) and function(lua, chunkName, env) + return load(lua, chunkName, "bt", env) + end + or function(lua, chunkName, env) + local chunk, err = loadstring(lua, chunkName) + if not chunk then return nil, err end + + if env then setfenv(chunk, env) end + + return chunk + end +) + +local loadLuaFile = ( + (_VERSION >= "Lua 5.2" or jit) and function(path, env) + return loadfile(path, "bt", env) + end + or function(path, env) + local chunk, err = loadfile(path) + if not chunk then return nil, err end + + if env then setfenv(chunk, env) end + + return chunk + end +) + +local function isLuaStringValidExpression(lua) + return loadLuaString("return("..lua.."\n)", "@", nil) ~= nil +end + + + +-- token, index = getNextUsableToken( tokens, startIndex, indexLimit=autoDependingOnDirection, direction ) +local function getNextUsableToken(tokens, iStart, iLimit, dir) + iLimit = ( + dir < 0 + and math.max((iLimit or 1 ), 1) + or math.min((iLimit or 1/0), #tokens) + ) + + for i = iStart, iLimit, dir do + if not USELESS_TOKENS[tokens[i].type] then + return tokens[i], i + end + end + + return nil +end + + + +-- bool = isToken( token, tokenType [, tokenValue=any ] ) +local function isToken(tok, tokType, v) + return tok.type == tokType and (v == nil or tok.value == v) +end + +-- bool = isTokenAndNotNil( token, tokenType [, tokenValue=any ] ) +local function isTokenAndNotNil(tok, tokType, v) + return tok ~= nil and tok.type == tokType and (v == nil or tok.value == v) +end + + + +--[[local]] function getLineNumber(s, pos) + return 1 + countSubString(s, 1, pos-1, "\n", true) +end + + + +-- text = getRelativeLocationText( tokenOfInterest, otherToken ) +-- text = getRelativeLocationText( tokenOfInterest, otherFilename, otherLineNumber ) +local function getRelativeLocationText(tokOfInterest, otherFilename, otherLn) + if type(otherFilename) == "table" then + return getRelativeLocationText(tokOfInterest, otherFilename.file, otherFilename.line) + end + + if not (tokOfInterest.file and tokOfInterest.line) then + return "at " + end + + if tokOfInterest.file ~= otherFilename then return F("at %s:%d", tokOfInterest.file, tokOfInterest.line) end + if tokOfInterest.line+1 == otherLn then return F("on the previous line") end + if tokOfInterest.line-1 == otherLn then return F("on the next line") end + if tokOfInterest.line ~= otherLn then return F("on line %d", tokOfInterest.line) end + return "on the same line" +end + + + +--[[local]] tableInsert = table.insert +--[[local]] tableRemove = table.remove + +--[[local]] function tableInsertFormat(t, s, ...) + tableInsert(t, F(s, ...)) +end + + + +-- length|nil = utf8GetCharLength( string [, position=1 ] ) +local function utf8GetCharLength(s, pos) + pos = pos or 1 + local b1, b2, b3, b4 = s:byte(pos, pos+3) + + if b1 > 0 and b1 <= 127 then + return 1 + + elseif b1 >= 194 and b1 <= 223 then + if not b2 then return nil end -- UTF-8 string terminated early. + if b2 < 128 or b2 > 191 then return nil end -- Invalid UTF-8 character. + return 2 + + elseif b1 >= 224 and b1 <= 239 then + if not b3 then return nil end -- UTF-8 string terminated early. + if b1 == 224 and (b2 < 160 or b2 > 191) then return nil end -- Invalid UTF-8 character. + if b1 == 237 and (b2 < 128 or b2 > 159) then return nil end -- Invalid UTF-8 character. + if (b2 < 128 or b2 > 191) then return nil end -- Invalid UTF-8 character. + if (b3 < 128 or b3 > 191) then return nil end -- Invalid UTF-8 character. + return 3 + + elseif b1 >= 240 and b1 <= 244 then + if not b4 then return nil end -- UTF-8 string terminated early. + if b1 == 240 and (b2 < 144 or b2 > 191) then return nil end -- Invalid UTF-8 character. + if b1 == 244 and (b2 < 128 or b2 > 143) then return nil end -- Invalid UTF-8 character. + if (b2 < 128 or b2 > 191) then return nil end -- Invalid UTF-8 character. + if (b3 < 128 or b3 > 191) then return nil end -- Invalid UTF-8 character. + if (b4 < 128 or b4 > 191) then return nil end -- Invalid UTF-8 character. + return 4 + end + + return nil -- Invalid UTF-8 character. +end + +-- codepoint, length = utf8GetCodepointAndLength( string [, position=1 ] ) +-- Returns nil if the text is invalid at the position. +--[[local]] function utf8GetCodepointAndLength(s, pos) + pos = pos or 1 + local len = utf8GetCharLength(s, pos) + if not len then return nil end + + -- 2^6=64, 2^12=4096, 2^18=262144 + if len == 1 then return s:byte(pos), len end + if len == 2 then local b1, b2 = s:byte(pos, pos+1) ; return (b1-192)*64 + (b2-128), len end + if len == 3 then local b1, b2, b3 = s:byte(pos, pos+2) ; return (b1-224)*4096 + (b2-128)*64 + (b3-128), len end + do local b1, b2, b3, b4 = s:byte(pos, pos+3) ; return (b1-240)*262144 + (b2-128)*4096 + (b3-128)*64 + (b4-128), len end +end + + + +-- for k, v in pairsSorted( table ) do +local function pairsSorted(t) + local keys = {} + for k in pairs(t) do + tableInsert(keys, k) + end + sortNatural(keys) + + local i = 0 + + return function() + i = i+1 + local k = keys[i] + if k ~= nil then return k, t[k] end + end +end + + + +-- sortNatural( array ) +-- aIsLessThanB = compareNatural( a, b ) +local compareNatural +do + local function pad(numStr) + return F("%03d%s", #numStr, numStr) + end + --[[local]] function compareNatural(a, b) + if type(a) == "number" and type(b) == "number" then + return a < b + else + return (tostring(a):gsub("%d+", pad) < tostring(b):gsub("%d+", pad)) + end + end + + --[[local]] function sortNatural(t, k) + table.sort(t, compareNatural) + end +end + + + +-- lua = _loadResource( resourceName, isParsing==true , nameToken, stats ) -- At parse time. +-- lua = _loadResource( resourceName, isParsing==false, errorLevel ) -- At metaprogram runtime. +local function _loadResource(resourceName, isParsing, nameTokOrErrLevel, stats) + local lua = current_parsingAndMeta_resourceCache[resourceName] + + if not lua then + if current_parsingAndMeta_onInsert then + lua = current_parsingAndMeta_onInsert(resourceName) + + if type(lua) == "string" then + -- void + elseif isParsing then + errorAtToken(nameTokOrErrLevel, nameTokOrErrLevel.position+1, "Parser/MetaProgram", "Expected a string from params.onInsert(). (Got %s)", type(lua)) + else + errorf(1+nameTokOrErrLevel, "Expected a string from params.onInsert(). (Got %s)", type(lua)) + end + + else + local err + lua, err = readFile(resourceName, true) + + if lua then + -- void + elseif isParsing then + errorAtToken(nameTokOrErrLevel, nameTokOrErrLevel.position+1, "Parser", "Could not read file '%s'. (%s)", resourceName, tostring(err)) + else + errorf(1+nameTokOrErrLevel, "Could not read file '%s'. (%s)", resourceName, tostring(err)) + end + end + + current_parsingAndMeta_resourceCache[resourceName] = lua + + if isParsing then + tableInsert(stats.insertedNames, resourceName) + end + + elseif isParsing then + current_parsing_insertCount = current_parsing_insertCount + 1 -- Note: We don't count insertions of newly encountered files. + + if current_parsing_insertCount > MAX_DUPLICATE_FILE_INSERTS then + errorAtToken( + nameTokOrErrLevel, nameTokOrErrLevel.position+1, "Parser", + "Too many duplicate inserts. We may be stuck in a recursive loop. (Unique files inserted so far: %s)", + stats.insertedNames[1] and table.concat(stats.insertedNames, ", ") or "none" + ) + end + end + + return lua +end + + + +--============================================================== +--= Preprocessor Functions ===================================== +--============================================================== + + + +-- :EnvironmentTable +---------------------------------------------------------------- + +metaEnv = copyTable(_G) -- Include all standard Lua stuff. +metaEnv._G = metaEnv + +local metaFuncs = {} + +-- printf() +-- printf( format, value1, ... ) +-- Print a formatted string to stdout. +metaFuncs.printf = printf + +-- readFile() +-- contents = readFile( path [, isTextFile=false ] ) +-- Get the entire contents of a binary file or text file. Returns nil and a message on error. +metaFuncs.readFile = readFile +metaFuncs.getFileContents = readFile -- @Deprecated + +-- writeFile() +-- success, error = writeFile( path, contents ) -- Writes a binary file. +-- success, error = writeFile( path, isTextFile, contents ) +-- Write an entire binary file or text file. +metaFuncs.writeFile = writeFile + +-- fileExists() +-- bool = fileExists( path ) +-- Check if a file exists. +metaFuncs.fileExists = fileExists + +-- toLua() +-- luaString = toLua( value ) +-- Convert a value to a Lua literal. Does not work with certain types, like functions or userdata. +-- Returns nil and a message on error. +metaFuncs.toLua = toLua + +-- serialize() +-- success, error = serialize( buffer, value ) +-- Same as toLua() except adds the result to an array instead of returning the Lua code as a string. +-- This could avoid allocating unnecessary strings. +metaFuncs.serialize = serialize + +-- evaluate() +-- value = evaluate( expression [, environment=getfenv() ] ) +-- Evaluate a Lua expression. The function is kind of the opposite of toLua(). Returns nil and a message on error. +-- Note that nil or false can also be returned as the first value if that's the value the expression results in! +metaFuncs.evaluate = evaluate + +-- escapePattern() +-- escapedString = escapePattern( string ) +-- Escape a string so it can be used in a pattern as plain text. +metaFuncs.escapePattern = escapePattern + +-- isToken() +-- bool = isToken( token, tokenType [, tokenValue=any ] ) +-- Check if a token is of a specific type, optionally also check it's value. +metaFuncs.isToken = isToken + +-- copyTable() +-- copy = copyTable( table [, deep=false ] ) +-- Copy a table, optionally recursively (deep copy). +-- Multiple references to the same table and self-references are preserved during deep copying. +metaFuncs.copyTable = copyTable + +-- unpack() +-- value1, ... = unpack( array [, fromIndex=1, toIndex=#array ] ) +-- Is _G.unpack() in Lua 5.1 and alias for table.unpack() in Lua 5.2+. +metaFuncs.unpack = unpack + +-- pack() +-- values = pack( value1, ... ) +-- Put values in a new array. values.n is the amount of values (which can be zero) +-- including nil values. Alias for table.pack() in Lua 5.2+. +metaFuncs.pack = pack + +-- pairsSorted() +-- for key, value in pairsSorted( table ) do +-- Same as pairs() but the keys are sorted (ascending). +metaFuncs.pairsSorted = pairsSorted + +-- sortNatural() +-- sortNatural( array ) +-- Sort an array using compareNatural(). +metaFuncs.sortNatural = sortNatural + +-- compareNatural() +-- aIsLessThanB = compareNatural( a, b ) +-- Compare two strings. Numbers in the strings are compared as numbers (as opposed to as strings). +-- Examples: +-- print( "foo9" < "foo10" ) -- false +-- print(compareNatural("foo9", "foo10")) -- true +metaFuncs.compareNatural = compareNatural + +-- run() +-- returnValue1, ... = run( path [, arg1, ... ] ) +-- Execute a Lua file. Similar to dofile(). +function metaFuncs.run(path, ...) + assertarg(1, path, "string") + + local main_chunk, err = loadLuaFile(path, metaEnv) + if not main_chunk then error(err, 0) end + + -- We want multiple return values while avoiding a tail call to preserve stack info. + local returnValues = pack(main_chunk(...)) + return unpack(returnValues, 1, returnValues.n) +end + +-- outputValue() +-- outputValue( value ) +-- outputValue( value1, value2, ... ) -- Outputted values will be separated by commas. +-- Output one or more values, like strings or tables, as literals. +-- Raises an error if no file or string is being processed. +function metaFuncs.outputValue(...) + errorIfNotRunningMeta(2) + + local argCount = select("#", ...) + if argCount == 0 then + error("No values to output.", 2) + end + + for i = 1, argCount do + local v = select(i, ...) + + if v == nil and not current_meta_canOutputNil then + local ln = debug.getinfo(2, "l").currentline + errorOnLine(current_meta_pathForErrorMessages, ln, "MetaProgram", "Trying to output nil which is disallowed through params.canOutputNil .") + end + + if i > 1 then + tableInsert(current_meta_output, (current_parsingAndMeta_isDebug and ", " or ",")) + end + + local ok, err = serialize(current_meta_output, v) + + if not ok then + local ln = debug.getinfo(2, "l").currentline + errorOnLine(current_meta_pathForErrorMessages, ln, "MetaProgram", "%s", err) + end + end +end + +-- outputLua() +-- outputLua( luaString1, ... ) +-- Output one or more strings as raw Lua code. +-- Raises an error if no file or string is being processed. +function metaFuncs.outputLua(...) + errorIfNotRunningMeta(2) + + local argCount = select("#", ...) + if argCount == 0 then + error("No Lua code to output.", 2) + end + + for i = 1, argCount do + local lua = select(i, ...) + assertarg(i, lua, "string") + tableInsert(current_meta_output, lua) + end +end + +-- outputLuaTemplate() +-- outputLuaTemplate( luaStringTemplate, value1, ... ) +-- Use a string as a template for outputting Lua code with values. +-- Question marks (?) are replaced with the values. +-- Raises an error if no file or string is being processed. +-- Examples: +-- outputLuaTemplate("local name, age = ?, ?", "Harry", 48) +-- outputLuaTemplate("dogs[?] = ?", "greyhound", {italian=false, count=5}) +function metaFuncs.outputLuaTemplate(lua, ...) + errorIfNotRunningMeta(2) + assertarg(1, lua, "string") + + local args = {...} -- @Memory + local n = 0 + local v, err + + lua = lua:gsub("%?", function() + n = n + 1 + v, err = toLua(args[n]) + + if not v then + errorf(3, "Bad argument %d: %s", 1+n, err) + end + + return v + end) + + tableInsert(current_meta_output, lua) +end + +-- getOutputSoFar() +-- luaString = getOutputSoFar( [ asTable=false ] ) +-- getOutputSoFar( buffer ) +-- Get Lua code that's been outputted so far. +-- If asTable is false then the full Lua code string is returned. +-- If asTable is true then an array of Lua code segments is returned. (This avoids allocating, possibly large, strings.) +-- If a buffer array is given then Lua code segments are added to it. +-- Raises an error if no file or string is being processed. +function metaFuncs.getOutputSoFar(bufferOrAsTable) + errorIfNotRunningMeta(2) + + -- Should there be a way to get the contents of current_meta_output etc.? :GetMoreOutputFromStack + + if type(bufferOrAsTable) == "table" then + for _, lua in ipairs(current_meta_outputStack[1]) do + tableInsert(bufferOrAsTable, lua) + end + -- Return nothing! + + else + return bufferOrAsTable and copyArray(current_meta_outputStack[1]) or table.concat(current_meta_outputStack[1]) + end +end + +local lineFragments = {} + +local function getOutputSoFarOnLine() + errorIfNotRunningMeta(2) + + local len = 0 + + -- Should there be a way to get the contents of current_meta_output etc.? :GetMoreOutputFromStack + for i = #current_meta_outputStack[1], 1, -1 do + local fragment = current_meta_outputStack[1][i] + + if fragment:find("\n", 1, true) then + len = len + 1 + lineFragments[len] = fragment:gsub(".*\n", "") + break + end + + len = len + 1 + lineFragments[len] = fragment + end + + return table.concat(lineFragments, 1, len) +end + +-- getOutputSoFarOnLine() +-- luaString = getOutputSoFarOnLine( ) +-- Get Lua code that's been outputted so far on the current line. +-- Raises an error if no file or string is being processed. +metaFuncs.getOutputSoFarOnLine = getOutputSoFarOnLine + +-- getOutputSizeSoFar() +-- size = getOutputSizeSoFar( ) +-- Get the amount of bytes outputted so far. +-- Raises an error if no file or string is being processed. +function metaFuncs.getOutputSizeSoFar() + errorIfNotRunningMeta(2) + + local size = 0 + + for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack + size = size + #lua + end + + return size +end + +-- getCurrentLineNumberInOutput() +-- lineNumber = getCurrentLineNumberInOutput( ) +-- Get the current line number in the output. +function metaFuncs.getCurrentLineNumberInOutput() + errorIfNotRunningMeta(2) + + local ln = 1 + + for _, lua in ipairs(current_meta_outputStack[1]) do -- :GetMoreOutputFromStack + ln = ln + countString(lua, "\n", true) + end + + return ln +end + +local function getIndentation(line, tabWidth) + if not tabWidth then + return line:match"^[ \t]*" + end + + local indent = 0 + + for i = 1, #line do + if line:sub(i, i) == "\t" then + indent = math.floor(indent/tabWidth)*tabWidth + tabWidth + elseif line:sub(i, i) == " " then + indent = indent + 1 + else + break + end + end + + return indent +end + +-- getIndentation() +-- string = getIndentation( line ) +-- size = getIndentation( line, tabWidth ) +-- Get indentation of a line, either as a string or as a size in spaces. +metaFuncs.getIndentation = getIndentation + +-- getCurrentIndentationInOutput() +-- string = getCurrentIndentationInOutput( ) +-- size = getCurrentIndentationInOutput( tabWidth ) +-- Get the indentation of the current line, either as a string or as a size in spaces. +function metaFuncs.getCurrentIndentationInOutput(tabWidth) + errorIfNotRunningMeta(2) + return (getIndentation(getOutputSoFarOnLine(), tabWidth)) +end + +-- getCurrentPathIn() +-- path = getCurrentPathIn( ) +-- Get what file is currently being processed, if any. +function metaFuncs.getCurrentPathIn() + return current_anytime_pathIn +end + +-- getCurrentPathOut() +-- path = getCurrentPathOut( ) +-- Get what file the currently processed file will be written to, if any. +function metaFuncs.getCurrentPathOut() + return current_anytime_pathOut +end + +-- tokenize() +-- tokens = tokenize( luaString [, allowPreprocessorCode=false ] ) +-- token = { +-- type=tokenType, representation=representation, value=value, +-- line=lineNumber, lineEnd=lineNumber, position=bytePosition, file=filePath, +-- ... +-- } +-- Convert Lua code to tokens. Returns nil and a message on error. (See newToken() for token types.) +function metaFuncs.tokenize(lua, allowPpCode) + local ok, errOrTokens = pcall(_tokenize, lua, "", allowPpCode, allowPpCode, true) -- @Incomplete: Make allowJitSyntax a parameter to tokenize()? + if not ok then + return nil, cleanError(errOrTokens) + end + return errOrTokens +end + +-- removeUselessTokens() +-- removeUselessTokens( tokens ) +-- Remove whitespace and comment tokens. +function metaFuncs.removeUselessTokens(tokens) + local len = #tokens + local offset = 0 + + for i, tok in ipairs(tokens) do + if USELESS_TOKENS[tok.type] then + offset = offset-1 + else + tokens[i+offset] = tok + end + end + + for i = len, len+offset+1, -1 do + tokens[i] = nil + end +end + +local function nextUsefulToken(tokens, i) + while true do + i = i+1 + local tok = tokens[i] + if not tok then return end + if not USELESS_TOKENS[tok.type] then return i, tok end + end +end + +-- eachToken() +-- for index, token in eachToken( tokens [, ignoreUselessTokens=false ] ) do +-- Loop through tokens. +function metaFuncs.eachToken(tokens, ignoreUselessTokens) + if ignoreUselessTokens then + return nextUsefulToken, tokens, 0 + else + return ipairs(tokens) + end +end + +-- getNextUsefulToken() +-- token, index = getNextUsefulToken( tokens, startIndex [, steps=1 ] ) +-- Get the next token that isn't a whitespace or comment. Returns nil if no more tokens are found. +-- Specify a negative steps value to get an earlier token. +function metaFuncs.getNextUsefulToken(tokens, i1, steps) + steps = (steps or 1) + + local i2, dir + if steps == 0 then + return tokens[i1], i1 + elseif steps < 0 then + i2, dir = 1, -1 + else + i2, dir = #tokens, 1 + end + + for i = i1, i2, dir do + local tok = tokens[i] + if not USELESS_TOKENS[tok.type] then + steps = steps-dir + if steps == 0 then return tok, i end + end + end + + return nil +end + +local numberFormatters = { + auto = function(n) return tostring(n) end, + integer = function(n) return F("%d", n) end, + int = function(n) return F("%d", n) end, + float = function(n) return F("%f", n):gsub("(%d)0+$", "%1") end, + scientific = function(n) return F("%e", n):gsub("(%d)0+e", "%1e"):gsub("0+(%d+)$", "%1") end, + SCIENTIFIC = function(n) return F("%E", n):gsub("(%d)0+E", "%1E"):gsub("0+(%d+)$", "%1") end, + e = function(n) return F("%e", n):gsub("(%d)0+e", "%1e"):gsub("0+(%d+)$", "%1") end, + E = function(n) return F("%E", n):gsub("(%d)0+E", "%1E"):gsub("0+(%d+)$", "%1") end, + hexadecimal = function(n) return (n == math.floor(n) and F("0x%x", n) or error("Hexadecimal floats not supported yet.", 3)) end, -- @Incomplete + HEXADECIMAL = function(n) return (n == math.floor(n) and F("0x%X", n) or error("Hexadecimal floats not supported yet.", 3)) end, + hex = function(n) return (n == math.floor(n) and F("0x%x", n) or error("Hexadecimal floats not supported yet.", 3)) end, + HEX = function(n) return (n == math.floor(n) and F("0x%X", n) or error("Hexadecimal floats not supported yet.", 3)) end, +} + +-- newToken() +-- token = newToken( tokenType, ... ) +-- Create a new token. Different token types take different arguments. +-- +-- commentToken = newToken( "comment", contents [, forceLongForm=false ] ) +-- identifierToken = newToken( "identifier", identifier ) +-- keywordToken = newToken( "keyword", keyword ) +-- numberToken = newToken( "number", number [, numberFormat="auto" ] ) +-- punctuationToken = newToken( "punctuation", symbol ) +-- stringToken = newToken( "string", contents [, longForm=false ] ) +-- whitespaceToken = newToken( "whitespace", contents ) +-- ppEntryToken = newToken( "pp_entry", isDouble ) +-- ppKeywordToken = newToken( "pp_keyword", ppKeyword ) -- ppKeyword can be "file", "insert", "line" or "@". +-- ppSymbolToken = newToken( "pp_symbol", identifier ) +-- +-- commentToken = { type="comment", representation=string, value=string, long=isLongForm } +-- identifierToken = { type="identifier", representation=string, value=string } +-- keywordToken = { type="keyword", representation=string, value=string } +-- numberToken = { type="number", representation=string, value=number } +-- punctuationToken = { type="punctuation", representation=string, value=string } +-- stringToken = { type="string", representation=string, value=string, long=isLongForm } +-- whitespaceToken = { type="whitespace", representation=string, value=string } +-- ppEntryToken = { type="pp_entry", representation=string, value=string, double=isDouble } +-- ppKeywordToken = { type="pp_keyword", representation=string, value=string } +-- ppSymbolToken = { type="pp_symbol", representation=string, value=string } +-- +-- Number formats: +-- "integer" E.g. 42 +-- "int" Same as integer, e.g. 42 +-- "float" E.g. 3.14 +-- "scientific" E.g. 0.7e+12 +-- "SCIENTIFIC" E.g. 0.7E+12 (upper case) +-- "e" Same as scientific, e.g. 0.7e+12 +-- "E" Same as SCIENTIFIC, e.g. 0.7E+12 (upper case) +-- "hexadecimal" E.g. 0x19af +-- "HEXADECIMAL" E.g. 0x19AF (upper case) +-- "hex" Same as hexadecimal, e.g. 0x19af +-- "HEX" Same as HEXADECIMAL, e.g. 0x19AF (upper case) +-- "auto" Note: Infinite numbers and NaN always get automatic format. +-- +function metaFuncs.newToken(tokType, ...) + if tokType == "comment" then + local comment, long = ... + long = not not (long or comment:find"[\r\n]") + assertarg(2, comment, "string") + + local repr + if long then + local equalSigns = "" + + while comment:find(F("]%s]", equalSigns), 1, true) do + equalSigns = equalSigns.."=" + end + + repr = F("--[%s[%s]%s]", equalSigns, comment, equalSigns) + + else + repr = F("--%s\n", comment) + end + + return {type="comment", representation=repr, value=comment, long=long} + + elseif tokType == "identifier" then + local ident = ... + assertarg(2, ident, "string") + + if ident == "" then + error("Identifier length is 0.", 2) + elseif not ident:find"^[%a_][%w_]*$" then + errorf(2, "Bad identifier format: '%s'", ident) + elseif KEYWORDS[ident] then + errorf(2, "Identifier must not be a keyword: '%s'", ident) + end + + return {type="identifier", representation=ident, value=ident} + + elseif tokType == "keyword" then + local keyword = ... + assertarg(2, keyword, "string") + + if not KEYWORDS[keyword] then + errorf(2, "Bad keyword '%s'.", keyword) + end + + return {type="keyword", representation=keyword, value=keyword} + + elseif tokType == "number" then + local n, numberFormat = ... + numberFormat = numberFormat or "auto" + assertarg(2, n, "number") + assertarg(3, numberFormat, "string") + + -- Some of these are technically multiple other tokens. We could raise an error but ehhh... + local numStr = ( + n ~= n and "(0/0)" or + n == 1/0 and "(1/0)" or + n == -1/0 and "(-1/0)" or + numberFormatters[numberFormat] and numberFormatters[numberFormat](n) or + errorf(2, "Invalid number format '%s'.", numberFormat) + ) + + return {type="number", representation=numStr, value=n} + + elseif tokType == "punctuation" then + local symbol = ... + assertarg(2, symbol, "string") + + -- Note: "!" and "!!" are of a different token type (pp_entry). + if not PUNCTUATION[symbol] then + errorf(2, "Bad symbol '%s'.", symbol) + end + + return {type="punctuation", representation=symbol, value=symbol} + + elseif tokType == "string" then + local s, long = ... + long = not not long + assertarg(2, s, "string") + + local repr + + if long then + local equalSigns = "" + + while s:find(F("]%s]", equalSigns), 1, true) do + equalSigns = equalSigns .. "=" + end + + repr = F("[%s[%s]%s]", equalSigns, s, equalSigns) + + else + repr = toLua(s) + end + + return {type="string", representation=repr, value=s, long=long} + + elseif tokType == "whitespace" then + local whitespace = ... + assertarg(2, whitespace, "string") + + if whitespace == "" then + error("String is empty.", 2) + elseif whitespace:find"%S" then + error("String contains non-whitespace characters.", 2) + end + + return {type="whitespace", representation=whitespace, value=whitespace} + + elseif tokType == "pp_entry" then + local double = ... + assertarg(2, double, "boolean") + + local symbol = double and "!!" or "!" + + return {type="pp_entry", representation=symbol, value=symbol, double=double} + + elseif tokType == "pp_keyword" then + local keyword = ... + assertarg(2, keyword, "string") + + if keyword == "@" then + return {type="pp_keyword", representation="@@", value="insert"} + elseif not PREPROCESSOR_KEYWORDS[keyword] then + errorf(2, "Bad preprocessor keyword '%s'.", keyword) + else + return {type="pp_keyword", representation="@"..keyword, value=keyword} + end + + elseif tokType == "pp_symbol" then + local ident = ... + assertarg(2, ident, "string") + + if ident == "" then + error("Identifier length is 0.", 2) + elseif not ident:find"^[%a_][%w_]*$" then + errorf(2, "Bad identifier format: '%s'", ident) + elseif KEYWORDS[ident] then + errorf(2, "Identifier must not be a keyword: '%s'", ident) + else + return {type="pp_symbol", representation="$"..ident, value=ident} + end + + else + errorf(2, "Invalid token type '%s'.", tostring(tokType)) + end +end + +-- concatTokens() +-- luaString = concatTokens( tokens ) +-- Concatenate tokens by their representations. +function metaFuncs.concatTokens(tokens) + return (_concatTokens(tokens, nil, false, nil, nil)) +end + +local recycledArrays = {} + +-- startInterceptingOutput() +-- startInterceptingOutput( ) +-- Start intercepting output until stopInterceptingOutput() is called. +-- The function can be called multiple times to intercept interceptions. +function metaFuncs.startInterceptingOutput() + errorIfNotRunningMeta(2) + + current_meta_output = tableRemove(recycledArrays) or {} + for i = 1, #current_meta_output do current_meta_output[i] = nil end + tableInsert(current_meta_outputStack, current_meta_output) +end + +local function _stopInterceptingOutput(errLevel) + errorIfNotRunningMeta(1+errLevel) + + local interceptedLua = tableRemove(current_meta_outputStack) + current_meta_output = current_meta_outputStack[#current_meta_outputStack] or error("Called stopInterceptingOutput() before calling startInterceptingOutput().", 1+errLevel) + tableInsert(recycledArrays, interceptedLua) + + return table.concat(interceptedLua) +end + +-- stopInterceptingOutput() +-- luaString = stopInterceptingOutput( ) +-- Stop intercepting output and retrieve collected code. +function metaFuncs.stopInterceptingOutput() + return (_stopInterceptingOutput(2)) +end + +-- loadResource() +-- luaString = loadResource( name ) +-- Load a Lua file/resource (using the same mechanism as @insert"name"). +-- Note that resources are cached after loading once. +function metaFuncs.loadResource(resourceName) + errorIfNotRunningMeta(2) + + return (_loadResource(resourceName, false, 2)) +end + +local function isCallable(v) + return type(v) == "function" + -- We use debug.getmetatable instead of _G.getmetatable because we don't want to + -- potentially invoke user code - we just want to know if the value is callable. + or (type(v) == "table" and debug.getmetatable(v) ~= nil and type(debug.getmetatable(v).__call) == "function") +end + +-- callMacro() +-- luaString = callMacro( function|macroName, argument1, ... ) +-- Call a macro function (which must be a global in metaEnvironment if macroName is given). +-- The arguments should be Lua code strings. +function metaFuncs.callMacro(nameOrFunc, ...) + errorIfNotRunningMeta(2) + + assertarg(1, nameOrFunc, "string","function") + local f + + if type(nameOrFunc) == "string" then + local nameResult = current_parsingAndMeta_macroPrefix .. nameOrFunc .. current_parsingAndMeta_macroSuffix + f = metaEnv[nameResult] + + if not isCallable(f) then + if nameOrFunc == nameResult + then errorf(2, "'%s' is not a macro/global function. (Got %s)", nameOrFunc, type(f)) + else errorf(2, "'%s' (resolving to '%s') is not a macro/global function. (Got %s)", nameOrFunc, nameResult, type(f)) end + end + + else + f = nameOrFunc + end + + return (metaEnv.__M()(f(...))) +end + +-- isProcessing() +-- bool = isProcessing( ) +-- Returns true if a file or string is currently being processed. +function metaFuncs.isProcessing() + return current_parsingAndMeta_isProcessing +end + +-- :PredefinedMacros + +-- ASSERT() +-- @@ASSERT( condition [, message=auto ] ) +-- Macro. Does nothing if params.release is set, otherwise calls error() if the +-- condition fails. The message argument is only evaluated if the condition fails. +function metaFuncs.ASSERT(conditionCode, messageCode) + errorIfNotRunningMeta(2) + if not conditionCode then error("missing argument #1 to 'ASSERT'", 2) end + + -- if not isLuaStringValidExpression(conditionCode) then + -- errorf(2, "Invalid condition expression: %s", formatCodeForShortMessage(conditionCode)) + -- end + + if current_meta_releaseMode then return end + + tableInsert(current_meta_output, "if not (") + tableInsert(current_meta_output, conditionCode) + tableInsert(current_meta_output, ") then error(") + + if messageCode then + tableInsert(current_meta_output, "(") + tableInsert(current_meta_output, messageCode) + tableInsert(current_meta_output, ")") + else + tableInsert(current_meta_output, F("%q", "Assertion failed: "..conditionCode)) + end + + tableInsert(current_meta_output, ") end") +end + +-- LOG() +-- @@LOG( logLevel, value ) -- [1] +-- @@LOG( logLevel, format, value1, ... ) -- [2] +-- +-- Macro. Does nothing if logLevel is lower than params.logLevel, +-- otherwise prints a value[1] or a formatted message[2]. +-- +-- logLevel can be "error", "warning", "info", "debug" or "trace" +-- (from highest to lowest priority). +-- +function metaFuncs.LOG(logLevelCode, valueOrFormatCode, ...) + errorIfNotRunningMeta(2) + if not logLevelCode then error("missing argument #1 to 'LOG'", 2) end + if not valueOrFormatCode then error("missing argument #2 to 'LOG'", 2) end + + local chunk = loadLuaString("return("..logLevelCode.."\n)", "@", dummyEnv) + if not chunk then errorf(2, "Invalid logLevel expression: %s", formatCodeForShortMessage(logLevelCode)) end + + local ok, logLevel = pcall(chunk) + if not ok then errorf(2, "logLevel must be a constant expression. Got: %s", formatCodeForShortMessage(logLevelCode)) end + if not LOG_LEVELS[logLevel] then errorf(2, "Invalid logLevel '%s'.", tostring(logLevel)) end + if logLevel == "off" then errorf(2, "Invalid logLevel '%s'.", tostring(logLevel)) end + + if LOG_LEVELS[logLevel] > LOG_LEVELS[current_meta_maxLogLevel] then return end + + tableInsert(current_meta_output, "print(") + + if ... then + tableInsert(current_meta_output, "string.format(") + tableInsert(current_meta_output, valueOrFormatCode) + for i = 1, select("#", ...) do + tableInsert(current_meta_output, ", ") + tableInsert(current_meta_output, (select(i, ...))) + end + tableInsert(current_meta_output, ")") + else + tableInsert(current_meta_output, valueOrFormatCode) + end + + tableInsert(current_meta_output, ")") +end + +-- Extra stuff used by the command line program: +metaFuncs.tryToFormatError = tryToFormatError + +---------------------------------------------------------------- + + + +for k, v in pairs(metaFuncs) do metaEnv[k] = v end + +metaEnv.__LUA = metaEnv.outputLua +metaEnv.__VAL = metaEnv.outputValue + +function metaEnv.__TOLUA(v) + return (assert(toLua(v))) +end +function metaEnv.__ISLUA(lua) + if type(lua) ~= "string" then + error("Value is not Lua code.", 2) + end + return lua +end + +local function finalizeMacro(lua) + if lua == nil then + return (_stopInterceptingOutput(2)) + elseif type(lua) ~= "string" then + errorf(2, "[Macro] Value is not Lua code. (Got %s)", type(lua)) + elseif current_meta_output[1] then + error("[Macro] Got Lua code from both value expression and outputLua(). Only one method may be used.", 2) -- It's also possible interception calls are unbalanced. + else + _stopInterceptingOutput(2) -- Returns "" because nothing was outputted. + return lua + end +end +function metaEnv.__M() + metaFuncs.startInterceptingOutput() + return finalizeMacro +end + +-- luaString = __ARG( locationTokenNumber, luaString|callback ) +-- callback = function( ) +function metaEnv.__ARG(locTokNum, v) + local lua + if type(v) == "string" then + lua = v + else + metaFuncs.startInterceptingOutput() + v() + lua = _stopInterceptingOutput(2) + end + + if current_parsingAndMeta_strictMacroArguments and not isLuaStringValidExpression(lua) then + runtimeErrorAtToken(2, current_meta_locationTokens[locTokNum], nil, "MacroArgument", "Argument result is not a valid Lua expression: %s", formatCodeForShortMessage(lua)) + end + + return lua +end + +function metaEnv.__EVAL(v) -- For symbols. + if isCallable(v) then + v = v() + end + return v +end + + + +local function getLineCountWithCode(tokens) + local lineCount = 0 + local lastLine = 0 + + for _, tok in ipairs(tokens) do + if not USELESS_TOKENS[tok.type] and tok.lineEnd > lastLine then + lineCount = lineCount+(tok.lineEnd-tok.line+1) + lastLine = tok.lineEnd + end + end + + return lineCount +end + + + +-- +-- Preprocessor expansions (symbols etc., not macros). +-- + +local function newTokenAt(tok, locTok) + tok.line = tok.line or locTok and locTok.line + tok.lineEnd = tok.lineEnd or locTok and locTok.lineEnd + tok.position = tok.position or locTok and locTok.position + tok.file = tok.file or locTok and locTok.file + return tok +end + +local function popTokens(tokenStack, lastIndexToPop) + for i = #tokenStack, lastIndexToPop, -1 do + tokenStack[i] = nil + end +end +local function popUseless(tokenStack) + for i = #tokenStack, 1, -1 do + if not USELESS_TOKENS[tokenStack[i].type] then break end + tokenStack[i] = nil + end +end + +local function advanceToken(tokens) + local tok = tokens[tokens.nextI] + tokens.nextI = tokens.nextI + 1 + return tok +end +local function advancePastUseless(tokens) + for i = tokens.nextI, #tokens do + if not USELESS_TOKENS[tokens[i].type] then break end + tokens.nextI = i + 1 + end +end + +-- outTokens = doEarlyExpansions( tokensToExpand, stats ) +local function doEarlyExpansions(tokensToExpand, stats) + -- + -- Here we expand simple things that makes it easier for + -- doLateExpansions*() to do more elaborate expansions. + -- + -- Expand expressions: + -- @file + -- @line + -- ` ... ` + -- $symbol + -- + local tokenStack = {} -- We process the last token first, and we may push new tokens onto the stack. + local outTokens = {} + + for i = #tokensToExpand, 1, -1 do + tableInsert(tokenStack, tokensToExpand[i]) + end + + while tokenStack[1] do + local tok = tokenStack[#tokenStack] + + -- Keyword. + if isToken(tok, "pp_keyword") then + local ppKeywordTok = tok + + -- @file + -- @line + if ppKeywordTok.value == "file" then + tableRemove(tokenStack) -- '@file' + tableInsert(outTokens, newTokenAt({type="string", value=ppKeywordTok.file, representation=F("%q",ppKeywordTok.file)}, ppKeywordTok)) + elseif ppKeywordTok.value == "line" then + tableRemove(tokenStack) -- '@line' + tableInsert(outTokens, newTokenAt({type="number", value=ppKeywordTok.line, representation=F(" %d ",ppKeywordTok.line)}, ppKeywordTok)) -- Is it fine for the representation to have spaces? Probably. + + else + -- Expand later. + tableInsert(outTokens, ppKeywordTok) + tableRemove(tokenStack) -- '@...' + end + + -- Backtick string. + elseif isToken(tok, "string") and tok.representation:find"^`" then + local stringTok = tok + stringTok.representation = toLua(stringTok.value)--F("%q", stringTok.value) + + tableInsert(outTokens, stringTok) + tableRemove(tokenStack) -- the string + + -- Symbol. (Should this expand later? Does it matter? Yeah, do this in the AST code instead. @Cleanup) + elseif isToken(tok, "pp_symbol") then + local ppSymbolTok = tok + + -- $symbol + tableRemove(tokenStack) -- '$symbol' + tableInsert(outTokens, newTokenAt({type="pp_entry", value="!!", representation="!!", double=true}, ppSymbolTok)) + tableInsert(outTokens, newTokenAt({type="punctuation", value="(", representation="(" }, ppSymbolTok)) + tableInsert(outTokens, newTokenAt({type="identifier", value="__EVAL", representation="__EVAL" }, ppSymbolTok)) + tableInsert(outTokens, newTokenAt({type="punctuation", value="(", representation="(" }, ppSymbolTok)) + tableInsert(outTokens, newTokenAt({type="identifier", value=ppSymbolTok.value, representation=ppSymbolTok.value}, ppSymbolTok)) + tableInsert(outTokens, newTokenAt({type="punctuation", value=")", representation=")" }, ppSymbolTok)) + tableInsert(outTokens, newTokenAt({type="punctuation", value=")", representation=")" }, ppSymbolTok)) + + -- Anything else. + else + tableInsert(outTokens, tok) + tableRemove(tokenStack) -- anything + end + end--while tokenStack + + return outTokens +end + +-- outTokens = doLateExpansions( tokensToExpand, stats, allowBacktickStrings, allowJitSyntax ) +local function doLateExpansions(tokensToExpand, stats, allowBacktickStrings, allowJitSyntax) + -- + -- Expand expressions: + -- @insert "name" + -- + local tokenStack = {} -- We process the last token first, and we may push new tokens onto the stack. + local outTokens = {} + + for i = #tokensToExpand, 1, -1 do + tableInsert(tokenStack, tokensToExpand[i]) + end + + while tokenStack[1] do + local tok = tokenStack[#tokenStack] + + -- Keyword. + if isToken(tok, "pp_keyword") then + local ppKeywordTok = tok + local tokNext, iNext = getNextUsableToken(tokenStack, #tokenStack-1, nil, -1) + + -- @insert "name" + if ppKeywordTok.value == "insert" and isTokenAndNotNil(tokNext, "string") and tokNext.file == ppKeywordTok.file then + local nameTok = tokNext + popTokens(tokenStack, iNext) -- the string + + local toInsertName = nameTok.value + local toInsertLua = _loadResource(toInsertName, true, nameTok, stats) + local toInsertTokens = _tokenize(toInsertLua, toInsertName, true, allowBacktickStrings, allowJitSyntax) + toInsertTokens = doEarlyExpansions(toInsertTokens, stats) + + for i = #toInsertTokens, 1, -1 do + tableInsert(tokenStack, toInsertTokens[i]) + end + + local lastTok = toInsertTokens[#toInsertTokens] + stats.processedByteCount = stats.processedByteCount + #toInsertLua + stats.lineCount = stats.lineCount + (lastTok and lastTok.line + countString(lastTok.representation, "\n", true) or 0) + stats.lineCountCode = stats.lineCountCode + getLineCountWithCode(toInsertTokens) + + -- @insert identifier ( argument1, ... ) + -- @insert identifier " ... " + -- @insert identifier { ... } + -- @insert identifier !( ... ) + -- @insert identifier !!( ... ) + elseif ppKeywordTok.value == "insert" and isTokenAndNotNil(tokNext, "identifier") and tokNext.file == ppKeywordTok.file then + local identTok = tokNext + tokNext, iNext = getNextUsableToken(tokenStack, iNext-1, nil, -1) + + if not (tokNext and ( + tokNext.type == "string" + or (tokNext.type == "punctuation" and isAny(tokNext.value, "(","{",".",":","[")) + or tokNext.type == "pp_entry" + )) then + errorAtToken(identTok, identTok.position+#identTok.representation, "Parser/Macro", "Expected '(' after macro name '%s'.", identTok.value) + end + + -- Expand later. + tableInsert(outTokens, tok) + tableRemove(tokenStack) -- '@insert' + + elseif ppKeywordTok.value == "insert" then + errorAtToken( + ppKeywordTok, (tokNext and tokNext.position or ppKeywordTok.position+#ppKeywordTok.representation), + "Parser", "Expected a string or identifier after %s.", ppKeywordTok.representation + ) + + else + errorAtToken(ppKeywordTok, nil, "Parser", "Internal error. (%s)", ppKeywordTok.value) + end + + -- Anything else. + else + tableInsert(outTokens, tok) + tableRemove(tokenStack) -- anything + end + end--while tokenStack + + return outTokens +end + +-- outTokens = doExpansions( params, tokensToExpand, stats ) +local function doExpansions(params, tokens, stats) + tokens = doEarlyExpansions(tokens, stats) + tokens = doLateExpansions (tokens, stats, params.backtickStrings, params.jitSyntax) -- Resources. + return tokens +end + + + +-- +-- Metaprogram generation. +-- + +local function AstSequence(locTok, tokens) return { + type = "sequence", + locationToken = locTok, + nodes = tokens or {}, +} end +local function AstLua(locTok, tokens) return { -- plain Lua + type = "lua", + locationToken = locTok, + tokens = tokens or {}, +} end +local function AstMetaprogram(locTok, tokens) return { -- `!(statements)` or `!statements` + type = "metaprogram", + locationToken = locTok, + originIsLine = false, + tokens = tokens or {}, +} end +local function AstExpressionCode(locTok, tokens) return { -- `!!(expression)` + type = "expressionCode", + locationToken = locTok, + tokens = tokens or {}, +} end +local function AstExpressionValue(locTok, tokens) return { -- `!(expression)` + type = "expressionValue", + locationToken = locTok, + tokens = tokens or {}, +} end +local function AstDualCode(locTok, valueTokens) return { -- `!!declaration` or `!!assignment` + type = "dualCode", + locationToken = locTok, + isDeclaration = false, + names = {}, + valueTokens = valueTokens or {}, +} end +-- local function AstSymbol(locTok) return { -- `$name` +-- type = "symbol", +-- locationToken = locTok, +-- name = "", +-- } end +local function AstMacro(locTok, calleeTokens) return { -- `@@callee(arguments)` or `@@callee{}` or `@@callee""` + type = "macro", + locationToken = locTok, + calleeTokens = calleeTokens or {}, + arguments = {}, -- []MacroArgument +} end +local function MacroArgument(locTok, nodes) return { + locationToken = locTok, + isComplex = false, + nodes = nodes or {}, +} end + +local astParseMetaBlockOrLine + +local function astParseMetaBlock(tokens) + local ppEntryTokIndex = tokens.nextI + local ppEntryTok = tokens[ppEntryTokIndex] + tokens.nextI = tokens.nextI + 2 -- '!(' or '!!(' + + local outTokens = {} + local depthStack = {} + + while true do + local tok = tokens[tokens.nextI] + + if not tok then + if depthStack[1] then + tok = depthStack[#depthStack].startToken + errorAtToken(tok, nil, "Parser/MetaBlock", "Could not find matching bracket before EOF. (Preprocessor line starts %s)", getRelativeLocationText(ppEntryTok, tok)) + end + break + end + + -- End of meta block. + if not depthStack[1] and isToken(tok, "punctuation", ")") then + tokens.nextI = tokens.nextI + 1 -- after ')' + break + + -- Nested metaprogram (not supported). + elseif tok.type:find"^pp_" then + errorAtToken(tok, nil, "Parser/MetaBlock", "Preprocessor token inside metaprogram (starting %s).", getRelativeLocationText(ppEntryTok, tok)) + + -- Continuation of meta block. + else + if isToken(tok, "punctuation", "(") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]")"}) + elseif isToken(tok, "punctuation", "[") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]"]"}) + elseif isToken(tok, "punctuation", "{") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]"}"}) + + elseif + isToken(tok, "punctuation", ")") or + isToken(tok, "punctuation", "]") or + isToken(tok, "punctuation", "}") + then + if not depthStack[1] then + errorAtToken(tok, nil, "Parser/MetaBlock", "Unexpected '%s'. (Preprocessor line starts %s)", tok.value, getRelativeLocationText(ppEntryTok, tok)) + elseif not isToken(tok, unpack(depthStack[#depthStack])) then + local startTok = depthStack[#depthStack].startToken + errorAtToken( + tok, nil, "Parser/MetaBlock", "Expected '%s' (to close '%s' %s) but got '%s'. (Preprocessor line starts %s)", + depthStack[#depthStack][2], startTok.value, getRelativeLocationText(startTok, tok), tok.value, getRelativeLocationText(ppEntryTok, tok) + ) + end + tableRemove(depthStack) + end + + tableInsert(outTokens, tok) + tokens.nextI = tokens.nextI + 1 -- after anything + end + end + + local lua = _concatTokens(outTokens, nil, false, nil, nil) + local chunk, err = loadLuaString("return 0,"..lua.."\n,0", "@", nil) + local isExpression = (chunk ~= nil) + + if not isExpression and ppEntryTok.double then + errorAtToken(tokens[ppEntryTokIndex+1], nil, "Parser/MetaBlock", "Invalid expression in preprocessor block.") + -- err = err:gsub("^:%d+: ", "") + -- errorAtToken(tokens[ppEntryTokIndex+1], nil, "Parser/MetaBlock", "Invalid expression in preprocessor block. (%s)", err) + elseif isExpression and not isLuaStringValidExpression(lua) then + if #lua > 100 then + lua = lua:sub(1, 50) .. "..." .. lua:sub(-50) + end + errorAtToken(tokens[ppEntryTokIndex+1], nil, "Parser/MetaBlock", "Ambiguous expression '%s'. (Comma-separated list?)", formatCodeForShortMessage(lua)) + end + + local astOutNode = ((ppEntryTok.double and AstExpressionCode) or (isExpression and AstExpressionValue or AstMetaprogram))(ppEntryTok, outTokens) + return astOutNode +end + +local function astParseMetaLine(tokens) + local ppEntryTok = tokens[tokens.nextI] + tokens.nextI = tokens.nextI + 1 -- '!' or '!!' + + local isDual = ppEntryTok.double + local astOutNode = (isDual and AstDualCode or AstMetaprogram)(ppEntryTok) + + if astOutNode.type == "metaprogram" then + astOutNode.originIsLine = true + end + + if isDual then + -- We expect the statement to look like any of these: + -- !!local x, y = ... + -- !!x, y = ... + local tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + + if isTokenAndNotNil(tokNext, "keyword", "local") then + astOutNode.isDeclaration = true + + tokens.nextI = iNext + 1 -- after 'local' + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + end + + local usedNames = {} + + while true do + if not isTokenAndNotNil(tokNext, "identifier") then + local tok = tokNext or tokens[#tokens] + errorAtToken( + tok, nil, "Parser/DualCodeLine", "Expected %sidentifier. (Preprocessor line starts %s)", + (astOutNode.names[1] and "" or "'local' or "), + getRelativeLocationText(ppEntryTok, tok) + ) + elseif usedNames[tokNext.value] then + errorAtToken( + tokNext, nil, "Parser/DualCodeLine", "Duplicate name '%s' in %s. (Preprocessor line starts %s)", + tokNext.value, + (astOutNode.isDeclaration and "declaration" or "assignment"), + getRelativeLocationText(ppEntryTok, tokNext) + ) + end + tableInsert(astOutNode.names, tokNext.value) + usedNames[tokNext.value] = tokNext + tokens.nextI = iNext + 1 -- after the identifier + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + + if not isTokenAndNotNil(tokNext, "punctuation", ",") then break end + tokens.nextI = iNext + 1 -- after ',' + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + end + + if not isTokenAndNotNil(tokNext, "punctuation", "=") then + local tok = tokNext or tokens[#tokens] + errorAtToken( + tok, nil, "Parser/DualCodeLine", "Expected '=' in %s. (Preprocessor line starts %s)", + (astOutNode.isDeclaration and "declaration" or "assignment"), + getRelativeLocationText(ppEntryTok, tok) + ) + end + tokens.nextI = iNext + 1 -- after '=' + end + + -- Find end of metaprogram line. + local outTokens = isDual and astOutNode.valueTokens or astOutNode.tokens + local depthStack = {} + + while true do + local tok = tokens[tokens.nextI] + + if not tok then + if depthStack[1] then + tok = depthStack[#depthStack].startToken + errorAtToken(tok, nil, "Parser/MetaLine", "Could not find matching bracket before EOF. (Preprocessor line starts %s)", getRelativeLocationText(ppEntryTok, tok)) + end + break + end + + -- End of meta line. + if + not depthStack[1] and ( + (tok.type == "whitespace" and tok.value:find("\n", 1, true)) or + (tok.type == "comment" and not tok.long) + ) + then + tableInsert(outTokens, tok) + tokens.nextI = tokens.nextI + 1 -- after the whitespace or comment + break + + -- Nested metaprogram (not supported). + elseif tok.type:find"^pp_" then + errorAtToken(tok, nil, "Parser/MetaLine", "Preprocessor token inside metaprogram (starting %s).", getRelativeLocationText(ppEntryTok, tok)) + + -- Continuation of meta line. + else + if isToken(tok, "punctuation", "(") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]")"}) + elseif isToken(tok, "punctuation", "[") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]"]"}) + elseif isToken(tok, "punctuation", "{") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]"}"}) + + elseif + isToken(tok, "punctuation", ")") or + isToken(tok, "punctuation", "]") or + isToken(tok, "punctuation", "}") + then + if not depthStack[1] then + errorAtToken(tok, nil, "Parser/MetaLine", "Unexpected '%s'. (Preprocessor line starts %s)", tok.value, getRelativeLocationText(ppEntryTok, tok)) + elseif not isToken(tok, unpack(depthStack[#depthStack])) then + local startTok = depthStack[#depthStack].startToken + errorAtToken( + tok, nil, "Parser/MetaLine", "Expected '%s' (to close '%s' %s) but got '%s'. (Preprocessor line starts %s)", + depthStack[#depthStack][2], startTok.value, getRelativeLocationText(startTok, tok), tok.value, getRelativeLocationText(ppEntryTok, tok) + ) + end + tableRemove(depthStack) + end + + tableInsert(outTokens, tok) + tokens.nextI = tokens.nextI + 1 -- after anything + end + end + + return astOutNode +end + +--[[local]] function astParseMetaBlockOrLine(tokens) + return isTokenAndNotNil(tokens[tokens.nextI+1], "punctuation", "(") + and astParseMetaBlock(tokens) + or astParseMetaLine (tokens) +end + +local function astParseMacro(params, tokens) + local macroStartTok = tokens[tokens.nextI] + tokens.nextI = tokens.nextI + 1 -- after '@insert' + + local astMacro = AstMacro(macroStartTok) + + -- + -- Callee. + -- + + -- Add 'ident' for start of (or whole) callee. + local tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + if not isTokenAndNotNil(tokNext, "identifier") then + printErrorTraceback("Internal error.") + errorAtToken(tokNext, nil, "Parser/Macro", "Internal error. (%s)", (tokNext and tokNext.type or "?")) + end + tokens.nextI = iNext + 1 -- after the identifier + tableInsert(astMacro.calleeTokens, tokNext) + local initialCalleeIdentTok = tokNext + + -- Add macro prefix and suffix. (Note: We only edit the initial identifier in the callee if there are more.) + initialCalleeIdentTok.value = current_parsingAndMeta_macroPrefix .. initialCalleeIdentTok.value .. current_parsingAndMeta_macroSuffix + initialCalleeIdentTok.representation = initialCalleeIdentTok.value + + -- Maybe add '.field[expr]:method' for rest of callee. + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + + while tokNext do + if isToken(tokNext, "punctuation", ".") or isToken(tokNext, "punctuation", ":") then + local punctTok = tokNext + tokens.nextI = iNext + 1 -- after '.' or ':' + tableInsert(astMacro.calleeTokens, tokNext) + + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + if not tokNext then + errorAfterToken(punctTok, "Parser/Macro", "Expected an identifier after '%s'.", punctTok.value) + end + tokens.nextI = iNext + 1 -- after the identifier + tableInsert(astMacro.calleeTokens, tokNext) + + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + + if punctTok.value == ":" then break end + + elseif isToken(tokNext, "punctuation", "[") then + local punctTok = tokNext + tokens.nextI = iNext + 1 -- after '[' + tableInsert(astMacro.calleeTokens, tokNext) + + local bracketBalance = 1 + + while true do + tokNext = advanceToken(tokens) -- anything + if not tokNext then + errorAtToken(punctTok, nil, "Parser/Macro", "Could not find matching bracket before EOF. (Macro starts %s)", getRelativeLocationText(macroStartTok, punctTok)) + end + tableInsert(astMacro.calleeTokens, tokNext) + + if isToken(tokNext, "punctuation", "[") then + bracketBalance = bracketBalance + 1 + elseif isToken(tokNext, "punctuation", "]") then + bracketBalance = bracketBalance - 1 + if bracketBalance == 0 then break end + elseif tokNext.type:find"^pp_" then + errorAtToken(tokNext, nil, "Parser/Macro", "Preprocessor token inside metaprogram/macro name expression (starting %s).", getRelativeLocationText(macroStartTok, tokNext)) + end + end + + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + + -- @UX: Validate that the contents form an expression. + + else + break + end + end + + -- + -- Arguments. + -- + + -- @insert identifier " ... " + if isTokenAndNotNil(tokNext, "string") then + tableInsert(astMacro.arguments, MacroArgument(tokNext, {AstLua(tokNext, {tokNext})})) -- The one and only argument for this macro variant. + tokens.nextI = iNext + 1 -- after the string + + -- @insert identifier { ... } -- Same as: @insert identifier ( { ... } ) + elseif isTokenAndNotNil(tokNext, "punctuation", "{") then + local macroArg = MacroArgument(tokNext) -- The one and only argument for this macro variant. + astMacro.arguments[1] = macroArg + + local astLuaInCurrentArg = AstLua(tokNext, {tokNext}) + tableInsert(macroArg.nodes, astLuaInCurrentArg) + + tokens.nextI = iNext + 1 -- after '{' + + -- + -- (Similar code as `@insert identifier()` below.) + -- + + -- Collect tokens for the table arg. + -- We're looking for the closing '}'. + local bracketDepth = 1 -- @Incomplete: Track all brackets! + + while true do + local tok = tokens[tokens.nextI] + + if not tok then + errorAtToken(macroArg.locationToken, nil, "Parser/MacroArgument", "Could not find end of table constructor before EOF.") + + -- Preprocessor block in macro. + elseif tok.type == "pp_entry" then + tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens)) + astLuaInCurrentArg = nil + + -- Nested macro. + elseif isToken(tok, "pp_keyword", "insert") then + tableInsert(macroArg.nodes, astParseMacro(params, tokens)) + astLuaInCurrentArg = nil + + -- Other preprocessor code in macro. (Not sure we ever get here.) + elseif tok.type:find"^pp_" then + errorAtToken(tok, nil, "Parser/MacroArgument", "Unsupported preprocessor code. (Macro starts %s)", getRelativeLocationText(macroStartTok, tok)) + + -- End of table and argument. + elseif bracketDepth == 1 and isToken(tok, "punctuation", "}") then + if not astLuaInCurrentArg then + astLuaInCurrentArg = AstLua(tok) + tableInsert(macroArg.nodes, astLuaInCurrentArg) + end + tableInsert(astLuaInCurrentArg.tokens, tok) + advanceToken(tokens) -- '}' + break + + -- Normal token. + else + if isToken(tok, "punctuation", "{") then + bracketDepth = bracketDepth + 1 + elseif isToken(tok, "punctuation", "}") then + bracketDepth = bracketDepth - 1 + end + + if not astLuaInCurrentArg then + astLuaInCurrentArg = AstLua(tok) + tableInsert(macroArg.nodes, astLuaInCurrentArg) + end + tableInsert(astLuaInCurrentArg.tokens, tok) + advanceToken(tokens) -- anything + end + end + + -- @insert identifier ( argument1, ... ) + elseif isTokenAndNotNil(tokNext, "punctuation", "(") then + -- Apply the same 'ambiguous syntax' rule as Lua. (Will comments mess this check up? @Check) + if isTokenAndNotNil(tokens[iNext-1], "whitespace") and tokens[iNext-1].value:find("\n", 1, true) then + errorAtToken(tokNext, nil, "Parser/Macro", "Ambiguous syntax near '(' - part of macro, or new statement?") + end + + local parensStartTok = tokNext + tokens.nextI = iNext + 1 -- after '(' + tokNext, iNext = getNextUsableToken(tokens, tokens.nextI, nil, 1) + + if isTokenAndNotNil(tokNext, "punctuation", ")") then + tokens.nextI = iNext + 1 -- after ')' + + else + for argNum = 1, 1/0 do + -- Collect tokens for this arg. + -- We're looking for the next comma at depth 0 or closing ')'. + local macroArg = MacroArgument(tokens[tokens.nextI]) + astMacro.arguments[argNum] = macroArg + + advancePastUseless(tokens) -- Trim leading useless tokens. + + local astLuaInCurrentArg = nil + local depthStack = {} + + while true do + local tok = tokens[tokens.nextI] + + if not tok then + errorAtToken(parensStartTok, nil, "Parser/Macro", "Could not find end of argument list before EOF.") + + -- Preprocessor block in macro. + elseif tok.type == "pp_entry" then + tableInsert(macroArg.nodes, astParseMetaBlockOrLine(tokens)) + astLuaInCurrentArg = nil + + -- Nested macro. + elseif isToken(tok, "pp_keyword", "insert") then + tableInsert(macroArg.nodes, astParseMacro(params, tokens)) + astLuaInCurrentArg = nil + + -- Other preprocessor code in macro. (Not sure we ever get here.) + elseif tok.type:find"^pp_" then + errorAtToken(tok, nil, "Parser/MacroArgument", "Unsupported preprocessor code. (Macro starts %s)", getRelativeLocationText(macroStartTok, tok)) + + -- End of argument. + elseif not depthStack[1] and (isToken(tok, "punctuation", ",") or isToken(tok, "punctuation", ")")) then + break + + -- Normal token. + else + if isToken(tok, "punctuation", "(") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]")"}) + elseif isToken(tok, "punctuation", "[") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]"]"}) + elseif isToken(tok, "punctuation", "{") then + tableInsert(depthStack, {startToken=tok, --[[1]]"punctuation", --[[2]]"}"}) + elseif isToken(tok, "keyword", "function") or isToken(tok, "keyword", "if") or isToken(tok, "keyword", "do") then + tableInsert(depthStack, {startToken=tok, --[[1]]"keyword", --[[2]]"end"}) + elseif isToken(tok, "keyword", "repeat") then + tableInsert(depthStack, {startToken=tok, --[[1]]"keyword", --[[2]]"until"}) + + elseif + isToken(tok, "punctuation", ")") or + isToken(tok, "punctuation", "]") or + isToken(tok, "punctuation", "}") or + isToken(tok, "keyword", "end") or + isToken(tok, "keyword", "until") + then + if not depthStack[1] then + errorAtToken(tok, nil, "Parser/MacroArgument", "Unexpected '%s'.", tok.value) + elseif not isToken(tok, unpack(depthStack[#depthStack])) then + local startTok = depthStack[#depthStack].startToken + errorAtToken( + tok, nil, "Parser/MacroArgument", "Expected '%s' (to close '%s' %s) but got '%s'.", + depthStack[#depthStack][2], startTok.value, getRelativeLocationText(startTok, tok), tok.value + ) + end + tableRemove(depthStack) + end + + if not astLuaInCurrentArg then + astLuaInCurrentArg = AstLua(tok) + tableInsert(macroArg.nodes, astLuaInCurrentArg) + end + tableInsert(astLuaInCurrentArg.tokens, tok) + advanceToken(tokens) -- anything + end + end + + if astLuaInCurrentArg then + -- Trim trailing useless tokens. + popUseless(astLuaInCurrentArg.tokens) + if not astLuaInCurrentArg.tokens[1] then + assert(tableRemove(macroArg.nodes) == astLuaInCurrentArg) + end + end + + if not macroArg.nodes[1] and current_parsingAndMeta_strictMacroArguments then + -- There were no useful tokens for the argument! + errorAtToken(macroArg.locationToken, nil, "Parser/MacroArgument", "Expected argument #%d.", argNum) + end + + -- Do next argument or finish arguments. + if isTokenAndNotNil(tokens[tokens.nextI], "punctuation", ")") then + tokens.nextI = tokens.nextI + 1 -- after ')' + break + end + + assert(isToken(advanceToken(tokens), "punctuation", ",")) -- The loop above should have continued otherwise! + end--for argNum + end + + -- @insert identifier !( ... ) -- Same as: @insert identifier ( !( ... ) ) + -- @insert identifier !!( ... ) -- Same as: @insert identifier ( !!( ... ) ) + elseif isTokenAndNotNil(tokNext, "pp_entry") then + tokens.nextI = iNext -- until '!' or '!!' + + if not isTokenAndNotNil(tokens[tokens.nextI+1], "punctuation", "(") then + errorAfterToken(tokNext, "Parser/Macro", "Expected '(' after '%s'.", tokNext.value) + end + + astMacro.arguments[1] = MacroArgument(tokNext, {astParseMetaBlock(tokens)}) -- The one and only argument for this macro variant. + + else + errorAfterToken(astMacro.calleeTokens[#astMacro.calleeTokens], "Parser/Macro", "Expected '(' after macro name.") + end + + return astMacro +end + +local function astParse(params, tokens) + -- @Robustness: Make sure everywhere that key tokens came from the same source file. + local astSequence = AstSequence(tokens[1]) + tokens.nextI = 1 + + while true do + local tok = tokens[tokens.nextI] + if not tok then break end + + if isToken(tok, "pp_entry") then + tableInsert(astSequence.nodes, astParseMetaBlockOrLine(tokens)) + + elseif isToken(tok, "pp_keyword", "insert") then + local astMacro = astParseMacro(params, tokens) + tableInsert(astSequence.nodes, astMacro) + + -- elseif isToken(tok, "pp_symbol") then -- We currently expand these in doEarlyExpansions(). + -- errorAtToken(tok, nil, "Parser", "Internal error: @Incomplete: Handle symbols.") + + else + local astLua = AstLua(tok) + tableInsert(astSequence.nodes, astLua) + + while true do + tableInsert(astLua.tokens, tok) + advanceToken(tokens) + + tok = tokens[tokens.nextI] + if not tok then break end + if tok.type:find"^pp_" then break end + end + end + end + + return astSequence +end + + + +-- lineNumber, lineNumberMeta = astNodeToMetaprogram( buffer, ast, lineNumber, lineNumberMeta, asMacroArgumentExpression ) +local function astNodeToMetaprogram(buffer, ast, ln, lnMeta, asMacroArgExpr) + if current_parsingAndMeta_addLineNumbers and not asMacroArgExpr then + lnMeta = maybeOutputLineNumber(buffer, ast.locationToken, lnMeta) + end + + -- + -- lua -> __LUA"lua" + -- + if ast.type == "lua" then + local lua = _concatTokens(ast.tokens, ln, current_parsingAndMeta_addLineNumbers, nil, nil) + ln = ast.tokens[#ast.tokens].line + + if not asMacroArgExpr then tableInsert(buffer, "__LUA") end + + if current_parsingAndMeta_isDebug then + if not asMacroArgExpr then tableInsert(buffer, "(") end + tableInsert(buffer, (F("%q", lua):gsub("\n", "n"))) + if not asMacroArgExpr then tableInsert(buffer, ")\n") end + else + tableInsert(buffer, F("%q", lua)) + if not asMacroArgExpr then tableInsert(buffer, "\n") end + end + + -- + -- !(expression) -> __VAL(expression) + -- + elseif ast.type == "expressionValue" then + if asMacroArgExpr + then tableInsert(buffer, "__TOLUA(") + else tableInsert(buffer, "__VAL((") end + + for _, tok in ipairs(ast.tokens) do + tableInsert(buffer, tok.representation) + end + + if asMacroArgExpr + then tableInsert(buffer, ")") + else tableInsert(buffer, "))\n") end + + -- + -- !!(expression) -> __LUA(expression) + -- + elseif ast.type == "expressionCode" then + if asMacroArgExpr + then tableInsert(buffer, "__ISLUA(") + else tableInsert(buffer, "__LUA((") end + + for _, tok in ipairs(ast.tokens) do + tableInsert(buffer, tok.representation) + end + + if asMacroArgExpr + then tableInsert(buffer, ")") + else tableInsert(buffer, "))\n") end + + -- + -- !(statements) -> statements + -- !statements -> statements + -- + elseif ast.type == "metaprogram" then + if asMacroArgExpr then internalError(ast.type) end + + if ast.originIsLine then + for i = 1, #ast.tokens-1 do + tableInsert(buffer, ast.tokens[i].representation) + end + + local lastTok = ast.tokens[#ast.tokens] + if lastTok.type == "whitespace" then + if current_parsingAndMeta_isDebug + then tableInsert(buffer, (F("\n__LUA(%q)\n", lastTok.value):gsub("\\\n", "\\n"))) -- Note: "\\\n" does not match "\n". + else tableInsert(buffer, (F("\n__LUA%q\n" , lastTok.value):gsub("\\\n", "\\n"))) end + else--if type == comment + tableInsert(buffer, lastTok.representation) + if current_parsingAndMeta_isDebug + then tableInsert(buffer, F('__LUA("\\n")\n')) + else tableInsert(buffer, F("__LUA'\\n'\n" )) end + end + + else + for _, tok in ipairs(ast.tokens) do + tableInsert(buffer, tok.representation) + end + tableInsert(buffer, "\n") + end + + -- + -- @@callee(argument1, ...) -> __LUA(__M(callee(__ARG(1,), ...))) + -- OR -> __LUA(__M(callee(__ARG(1,function()end), ...))) + -- + -- The code handling each argument will be different depending on the complexity of the argument. + -- + elseif ast.type == "macro" then + if not asMacroArgExpr then tableInsert(buffer, "__LUA(") end + + tableInsert(buffer, "__M()(") + for _, tok in ipairs(ast.calleeTokens) do + tableInsert(buffer, tok.representation) + end + tableInsert(buffer, "(") + + for argNum, macroArg in ipairs(ast.arguments) do + local argIsComplex = false -- If any part of the argument cannot be an expression then it's complex. + + for _, astInArg in ipairs(macroArg.nodes) do + if astInArg.type == "metaprogram" or astInArg.type == "dualCode" then + argIsComplex = true + break + end + end + + if argNum > 1 then + tableInsert(buffer, ",") + if current_parsingAndMeta_isDebug then tableInsert(buffer, " ") end + end + + local locTokNum = #current_meta_locationTokens + 1 + current_meta_locationTokens[locTokNum] = macroArg.nodes[1] and macroArg.nodes[1].locationToken or macroArg.locationToken or internalError() + + tableInsert(buffer, "__ARG(") + tableInsert(buffer, tostring(locTokNum)) + tableInsert(buffer, ",") + + if argIsComplex then + tableInsert(buffer, "function()\n") + for nodeNumInArg, astInArg in ipairs(macroArg.nodes) do + ln, lnMeta = astNodeToMetaprogram(buffer, astInArg, ln, lnMeta, false) + end + tableInsert(buffer, "end") + + elseif macroArg.nodes[1] then + for nodeNumInArg, astInArg in ipairs(macroArg.nodes) do + if nodeNumInArg > 1 then tableInsert(buffer, "..") end + ln, lnMeta = astNodeToMetaprogram(buffer, astInArg, ln, lnMeta, true) + end + + else + tableInsert(buffer, '""') + end + + tableInsert(buffer, ")") + end + + tableInsert(buffer, "))") + + if not asMacroArgExpr then tableInsert(buffer, ")\n") end + + -- + -- !!local names = values -> local names = values ; __LUA"local names = "__VAL(name1)__LUA", "__VAL(name2)... + -- !! names = values -> names = values ; __LUA"names = "__VAL(name1)__LUA", "__VAL(name2)... + -- + elseif ast.type == "dualCode" then + if asMacroArgExpr then internalError(ast.type) end + + -- Metaprogram. + if ast.isDeclaration then tableInsert(buffer, "local ") end + tableInsert(buffer, table.concat(ast.names, ", ")) + tableInsert(buffer, ' = ') + for _, tok in ipairs(ast.valueTokens) do + tableInsert(buffer, tok.representation) + end + + -- Final program. + tableInsert(buffer, '__LUA') + if current_parsingAndMeta_isDebug then tableInsert(buffer, '(') end + tableInsert(buffer, '"') -- string start + if current_parsingAndMeta_addLineNumbers then + ln = maybeOutputLineNumber(buffer, ast.locationToken, ln) + end + if ast.isDeclaration then tableInsert(buffer, "local ") end + tableInsert(buffer, table.concat(ast.names, ", ")) + tableInsert(buffer, ' = "') -- string end + if current_parsingAndMeta_isDebug then tableInsert(buffer, '); ') end + + for i, name in ipairs(ast.names) do + if i == 1 then -- void + elseif current_parsingAndMeta_isDebug then tableInsert(buffer, '; __LUA(", "); ') + else tableInsert(buffer, '__LUA", "' ) end + tableInsert(buffer, "__VAL(") + tableInsert(buffer, name) + tableInsert(buffer, ")") + end + + -- Use trailing semicolon if the user does. + for i = #ast.valueTokens, 1, -1 do + if isToken(ast.valueTokens[i], "punctuation", ";") then + if current_parsingAndMeta_isDebug + then tableInsert(buffer, '; __LUA(";")') + else tableInsert(buffer, '__LUA";"' ) end + break + elseif not isToken(ast.valueTokens[i], "whitespace") then + break + end + end + + if current_parsingAndMeta_isDebug + then tableInsert(buffer, '; __LUA("\\n")\n') + else tableInsert(buffer, '__LUA"\\n"\n' ) end + + -- + -- ... + -- + elseif ast.type == "sequence" then + for _, astChild in ipairs(ast.nodes) do + ln, lnMeta = astNodeToMetaprogram(buffer, astChild, ln, lnMeta, false) + end + + -- elseif ast.type == "symbol" then + -- errorAtToken(ast.locationToken, nil, nil, "AstSymbol") + + else + printErrorTraceback("Internal error.") + errorAtToken(ast.locationToken, nil, "Parsing", "Internal error. (%s, %s)", ast.type, tostring(asMacroArgExpr)) + end + + return ln, lnMeta +end + +local function astToLua(ast) + local buffer = {} + astNodeToMetaprogram(buffer, ast, 0, 0, false) + return table.concat(buffer) +end + + + +local function _processFileOrString(params, isFile) + if isFile then + if not params.pathIn then error("Missing 'pathIn' in params.", 2) end + if not params.pathOut then error("Missing 'pathOut' in params.", 2) end + + if params.pathOut == params.pathIn and params.pathOut ~= "-" then + error("'pathIn' and 'pathOut' are the same in params.", 2) + end + + if (params.pathMeta or "-") == "-" then -- Should it be possible to output the metaprogram to stdout? + -- void + elseif params.pathMeta == params.pathIn then + error("'pathIn' and 'pathMeta' are the same in params.", 2) + elseif params.pathMeta == params.pathOut then + error("'pathOut' and 'pathMeta' are the same in params.", 2) + end + + else + if not params.code then error("Missing 'code' in params.", 2) end + end + + -- Read input. + local luaUnprocessed, virtualPathIn + + if isFile then + virtualPathIn = params.pathIn + local err + + if virtualPathIn == "-" then + luaUnprocessed, err = io.stdin:read"*a" + else + luaUnprocessed, err = readFile(virtualPathIn, true) + end + + if not luaUnprocessed then + errorf("Could not read file '%s'. (%s)", virtualPathIn, err) + end + + current_anytime_pathIn = params.pathIn + current_anytime_pathOut = params.pathOut + + else + virtualPathIn = "" + luaUnprocessed = params.code + end + + current_anytime_fastStrings = params.fastStrings + current_parsing_insertCount = 0 + current_parsingAndMeta_resourceCache = {[virtualPathIn]=luaUnprocessed} -- The contents of files, unless params.onInsert() is specified in which case it's user defined. + current_parsingAndMeta_onInsert = params.onInsert + current_parsingAndMeta_addLineNumbers = params.addLineNumbers + current_parsingAndMeta_macroPrefix = params.macroPrefix or "" + current_parsingAndMeta_macroSuffix = params.macroSuffix or "" + current_parsingAndMeta_strictMacroArguments = params.strictMacroArguments ~= false + current_meta_locationTokens = {} + + local specialFirstLine, rest = luaUnprocessed:match"^(#[^\r\n]*\r?\n?)(.*)$" + if specialFirstLine then + specialFirstLine = specialFirstLine:gsub("\r", "") -- Normalize line breaks. (Assume the input is either "\n" or "\r\n".) + luaUnprocessed = rest + end + + -- Ensure there's a newline at the end of the code, otherwise there will be problems down the line. + if not (luaUnprocessed == "" or luaUnprocessed:find"\n%s*$") then + luaUnprocessed = luaUnprocessed .. "\n" + end + + local tokens = _tokenize(luaUnprocessed, virtualPathIn, true, params.backtickStrings, params.jitSyntax) + -- printTokens(tokens) -- DEBUG + + -- Gather info. + local lastTok = tokens[#tokens] + + local stats = { + processedByteCount = #luaUnprocessed, + lineCount = (specialFirstLine and 1 or 0) + (lastTok and lastTok.line + countString(lastTok.representation, "\n", true) or 0), + lineCountCode = getLineCountWithCode(tokens), + tokenCount = 0, -- Set later. + hasPreprocessorCode = false, + hasMetaprogram = false, + insertedNames = {}, + } + + for _, tok in ipairs(tokens) do + -- @Volatile: Make sure to update this when syntax is changed! + if isToken(tok, "pp_entry") or isToken(tok, "pp_keyword", "insert") or isToken(tok, "pp_symbol") then + stats.hasPreprocessorCode = true + stats.hasMetaprogram = true + break + elseif isToken(tok, "pp_keyword") or (isToken(tok, "string") and tok.representation:find"^`") then + stats.hasPreprocessorCode = true + -- Keep going as there may be metaprogram. + end + end + + -- Generate and run metaprogram. + ---------------------------------------------------------------- + + local shouldProcess = stats.hasPreprocessorCode or params.addLineNumbers + + if shouldProcess then + tokens = doExpansions(params, tokens, stats) + end + stats.tokenCount = #tokens + + current_meta_maxLogLevel = params.logLevel or "trace" + if not LOG_LEVELS[current_meta_maxLogLevel] then + errorf(2, "Invalid 'logLevel' value in params. (%s)", tostring(current_meta_maxLogLevel)) + end + + local lua + + if shouldProcess then + local luaMeta = astToLua(astParse(params, tokens)) + --[[ DEBUG :PrintCode + print("=META===============================") + print(luaMeta) + print("====================================") + --]] + + -- Run metaprogram. + current_meta_pathForErrorMessages = params.pathMeta or "" + current_meta_output = {} + current_meta_outputStack = {current_meta_output} + current_meta_canOutputNil = params.canOutputNil ~= false + current_meta_releaseMode = params.release + + if params.pathMeta then + local file, err = io.open(params.pathMeta, "wb") + if not file then errorf("Count not open '%s' for writing. (%s)", params.pathMeta, err) end + + file:write(luaMeta) + file:close() + end + + if params.onBeforeMeta then params.onBeforeMeta(luaMeta) end + + local main_chunk, err = loadLuaString(luaMeta, "@"..current_meta_pathForErrorMessages, metaEnv) + if not main_chunk then + local ln, _err = err:match"^.-:(%d+): (.*)" + errorOnLine(current_meta_pathForErrorMessages, (tonumber(ln) or 0), nil, "%s", (_err or err)) + end + + current_anytime_isRunningMeta = true + main_chunk() -- Note: Our caller should clean up current_meta_pathForErrorMessages etc. on error. + current_anytime_isRunningMeta = false + + if not current_parsingAndMeta_isDebug and params.pathMeta then + os.remove(params.pathMeta) + end + + if current_meta_outputStack[2] then + error("Called startInterceptingOutput() more times than stopInterceptingOutput().") + end + + lua = table.concat(current_meta_output) + --[[ DEBUG :PrintCode + print("=OUTPUT=============================") + print(lua) + print("====================================") + --]] + + current_meta_pathForErrorMessages = "" + current_meta_output = nil + current_meta_outputStack = nil + current_meta_canOutputNil = true + current_meta_releaseMode = false + + else + -- @Copypaste from above. + if not current_parsingAndMeta_isDebug and params.pathMeta then + os.remove(params.pathMeta) + end + + lua = luaUnprocessed + end + + current_meta_maxLogLevel = "trace" + current_meta_locationTokens = nil + + if params.onAfterMeta then + local luaModified = params.onAfterMeta(lua) + + if type(luaModified) == "string" then + lua = luaModified + elseif luaModified ~= nil then + errorf("onAfterMeta() did not return a string. (Got %s)", type(luaModified)) + end + end + + -- Write output file. + ---------------------------------------------------------------- + + local pathOut = isFile and params.pathOut or "" + + if isFile then + if pathOut == "-" then + io.stdout:write(specialFirstLine or "") + io.stdout:write(lua) + + else + local file, err = io.open(pathOut, "wb") + if not file then errorf("Count not open '%s' for writing. (%s)", pathOut, err) end + + file:write(specialFirstLine or "") + file:write(lua) + file:close() + end + end + + -- Check if the output is valid Lua. + if params.validate ~= false then + local luaToCheck = lua:gsub("^#![^\n]*", "") + local chunk, err = loadLuaString(luaToCheck, "@"..pathOut, nil) + + if not chunk then + local ln, _err = err:match"^.-:(%d+): (.*)" + errorOnLine(pathOut, (tonumber(ln) or 0), nil, "Output is invalid Lua. (%s)", (_err or err)) + end + end + + -- :ProcessInfo + local info = { + path = isFile and params.pathIn or "", + outputPath = isFile and params.pathOut or "", + processedByteCount = stats.processedByteCount, + lineCount = stats.lineCount, + linesOfCode = stats.lineCountCode, + tokenCount = stats.tokenCount, + hasPreprocessorCode = stats.hasPreprocessorCode, + hasMetaprogram = stats.hasMetaprogram, + insertedFiles = stats.insertedNames, + } + + if params.onDone then params.onDone(info) end + + current_anytime_pathIn = "" + current_anytime_pathOut = "" + current_anytime_fastStrings = false + current_parsingAndMeta_resourceCache = nil + current_parsingAndMeta_onInsert = nil + current_parsingAndMeta_addLineNumbers = false + current_parsingAndMeta_macroPrefix = "" + current_parsingAndMeta_macroSuffix = "" + current_parsingAndMeta_strictMacroArguments = true + + ---------------------------------------------------------------- + + if isFile then + return info + else + if specialFirstLine then + lua = specialFirstLine .. lua + end + return lua, info + end +end + +local function processFileOrString(params, isFile) + if current_parsingAndMeta_isProcessing then + error("Cannot process recursively.", 3) -- Note: We don't return failure in this case - it's a critical error! + end + + -- local startTime = os.clock() -- :DebugMeasureTime @Incomplete: Add processing time to returned info. + local returnValues = nil + + current_parsingAndMeta_isProcessing = true + current_parsingAndMeta_isDebug = params.debug + + local xpcallOk, xpcallErr = xpcall( + function() + returnValues = pack(_processFileOrString(params, isFile)) + end, + + function(err) + if type(err) == "string" and err:find("\0", 1, true) then + printError(tryToFormatError(cleanError(err))) + else + printErrorTraceback(err, 2) -- The level should be at error(). + end + + if params.onError then + local cbOk, cbErr = pcall(params.onError, err) + if not cbOk then + printfError("Additional error in params.onError()...\n%s", tryToFormatError(cbErr)) + end + end + + return err + end + ) + + current_parsingAndMeta_isProcessing = false + current_parsingAndMeta_isDebug = false + + -- Cleanup in case an error happened. + current_anytime_isRunningMeta = false + current_anytime_pathIn = "" + current_anytime_pathOut = "" + current_anytime_fastStrings = false + current_parsing_insertCount = 0 + current_parsingAndMeta_onInsert = nil + current_parsingAndMeta_resourceCache = nil + current_parsingAndMeta_addLineNumbers = false + current_parsingAndMeta_macroPrefix = "" + current_parsingAndMeta_macroSuffix = "" + current_parsingAndMeta_strictMacroArguments = true + current_meta_pathForErrorMessages = "" + current_meta_output = nil + current_meta_outputStack = nil + current_meta_canOutputNil = true + current_meta_releaseMode = false + current_meta_maxLogLevel = "trace" + current_meta_locationTokens = nil + + -- print("time", os.clock()-startTime) -- :DebugMeasureTime + if xpcallOk then + return unpack(returnValues, 1, returnValues.n) + else + return nil, cleanError(xpcallErr or "Unknown processing error.") + end +end + +local function processFile(params) + local returnValues = pack(processFileOrString(params, true)) + return unpack(returnValues, 1, returnValues.n) +end + +local function processString(params) + local returnValues = pack(processFileOrString(params, false)) + return unpack(returnValues, 1, returnValues.n) +end + + + +-- :ExportTable +local pp = { + + -- Processing functions. + ---------------------------------------------------------------- + + -- processFile() + -- Process a Lua file. Returns nil and a message on error. + -- + -- info = processFile( params ) + -- info: Table with various information. (See 'ProcessInfo' for more info.) + -- + -- params: Table with these fields: + -- pathIn = pathToInputFile -- [Required] Specify "-" to use stdin. + -- pathOut = pathToOutputFile -- [Required] Specify "-" to use stdout. (Note that if stdout is used then anything you print() in the metaprogram will end up there.) + -- pathMeta = pathForMetaprogram -- [Optional] You can inspect this temporary output file if an error occurs in the metaprogram. + -- + -- debug = boolean -- [Optional] Debug mode. The metaprogram file is formatted more nicely and does not get deleted automatically. + -- addLineNumbers = boolean -- [Optional] Add comments with line numbers to the output. + -- + -- backtickStrings = boolean -- [Optional] Enable the backtick (`) to be used as string literal delimiters. Backtick strings don't interpret any escape sequences and can't contain other backticks. (Default: false) + -- jitSyntax = boolean -- [Optional] Allow LuaJIT-specific syntax. (Default: false) + -- canOutputNil = boolean -- [Optional] Allow !(expression) and outputValue() to output nil. (Default: true) + -- fastStrings = boolean -- [Optional] Force fast serialization of string values. (Non-ASCII characters will look ugly.) (Default: false) + -- validate = boolean -- [Optional] Validate output. (Default: true) + -- strictMacroArguments = boolean -- [Optional] Check that macro arguments are valid Lua expressions. (Default: true) + -- + -- macroPrefix = prefix -- [Optional] String to prepend to macro names. (Default: "") + -- macroSuffix = suffix -- [Optional] String to append to macro names. (Default: "") + -- + -- release = boolean -- [Optional] Enable release mode. Currently only disables the @@ASSERT() macro when true. (Default: false) + -- logLevel = levelName -- [Optional] Maximum log level for the @@LOG() macro. Can be "off", "error", "warning", "info", "debug" or "trace". (Default: "trace", which enables all logging) + -- + -- onInsert = function( name ) -- [Optional] Called for each @insert"name" instruction. It's expected to return a Lua code string. By default 'name' is a path to a file to be inserted. + -- onBeforeMeta = function( luaString ) -- [Optional] Called before the metaprogram runs, if a metaprogram is generated. luaString contains the metaprogram. + -- onAfterMeta = function( luaString ) -- [Optional] Here you can modify and return the Lua code before it's written to 'pathOut'. + -- onError = function( error ) -- [Optional] You can use this to get traceback information. 'error' is the same value as what is returned from processFile(). + -- + processFile = processFile, + + -- processString() + -- Process Lua code. Returns nil and a message on error. + -- + -- luaString, info = processString( params ) + -- info: Table with various information. (See 'ProcessInfo' for more info.) + -- + -- params: Table with these fields: + -- code = luaString -- [Required] + -- pathMeta = pathForMetaprogram -- [Optional] You can inspect this temporary output file if an error occurs in the metaprogram. + -- + -- debug = boolean -- [Optional] Debug mode. The metaprogram file is formatted more nicely and does not get deleted automatically. + -- addLineNumbers = boolean -- [Optional] Add comments with line numbers to the output. + -- + -- backtickStrings = boolean -- [Optional] Enable the backtick (`) to be used as string literal delimiters. Backtick strings don't interpret any escape sequences and can't contain other backticks. (Default: false) + -- jitSyntax = boolean -- [Optional] Allow LuaJIT-specific syntax. (Default: false) + -- canOutputNil = boolean -- [Optional] Allow !(expression) and outputValue() to output nil. (Default: true) + -- fastStrings = boolean -- [Optional] Force fast serialization of string values. (Non-ASCII characters will look ugly.) (Default: false) + -- validate = boolean -- [Optional] Validate output. (Default: true) + -- strictMacroArguments = boolean -- [Optional] Check that macro arguments are valid Lua expressions. (Default: true) + -- + -- macroPrefix = prefix -- [Optional] String to prepend to macro names. (Default: "") + -- macroSuffix = suffix -- [Optional] String to append to macro names. (Default: "") + -- + -- release = boolean -- [Optional] Enable release mode. Currently only disables the @@ASSERT() macro when true. (Default: false) + -- logLevel = levelName -- [Optional] Maximum log level for the @@LOG() macro. Can be "off", "error", "warning", "info", "debug" or "trace". (Default: "trace", which enables all logging) + -- + -- onInsert = function( name ) -- [Optional] Called for each @insert"name" instruction. It's expected to return a Lua code string. By default 'name' is a path to a file to be inserted. + -- onBeforeMeta = function( luaString ) -- [Optional] Called before the metaprogram runs, if a metaprogram is generated. luaString contains the metaprogram. + -- onError = function( error ) -- [Optional] You can use this to get traceback information. 'error' is the same value as the second returned value from processString(). + -- + processString = processString, + + -- Values. + ---------------------------------------------------------------- + + VERSION = PP_VERSION, -- The version of LuaPreprocess. + metaEnvironment = metaEnv, -- The environment used for metaprograms. +} + +-- Include all functions from the metaprogram environment. +for k, v in pairs(metaFuncs) do pp[k] = v end + +return pp + + + +--[[!=========================================================== + +Copyright © 2018-2022 Marcus 'ReFreezed' Thunström + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +==============================================================]] + diff --git a/lib/tiny-debug.lua b/lib/tiny-debug.lua new file mode 100644 index 0000000..2a4f5fe --- /dev/null +++ b/lib/tiny-debug.lua @@ -0,0 +1,45 @@ +if not playdate.isSimulator then + return +end + +tinyTrackEntityAges = false +tinyLogSystemUpdateTime = false +tinyLogSystemChanges = false +tinyWarnWhenNonDataOnEntities = false + +getCurrentTimeMilliseconds = playdate.getCurrentTimeMilliseconds + +ENTITY_INIT_MS = { "ENTITY_INIT_MS" } +if tinyTrackEntityAges then + function tinyGetEntityAgeMs(entity) + return entity[ENTITY_INIT_MS] + end +end + + +if tinyWarnWhenNonDataOnEntities then + function checkForNonData(e, nested, tableCache) + nested = nested or false + tableCache = tableCache or {} + + local valType = type(e) + if valType == "table" then + if tableCache[e] then + return + end + tableCache[e] = true + for k, v in pairs(e) do + local keyWarning = checkForNonData(k, true, tableCache) + if keyWarning then + return keyWarning + end + local valueWarning = checkForNonData(v, true, tableCache) + if valueWarning then + return valueWarning + end + end + elseif valType == "function" or valType == "thread" or valType == "userdata" then + return valType + end + end +end \ No newline at end of file diff --git a/lib/tiny-types.lua b/lib/tiny-types.lua new file mode 100644 index 0000000..e5f5ba5 --- /dev/null +++ b/lib/tiny-types.lua @@ -0,0 +1,30 @@ +---@meta + +---@class World +World = {} + +function World:add(...) end + +function World:addEntity(entity) end + +function World:addSystem(system) end + +function World:remove(...) end + +function World:removeEntity(entity) end + +function World:removeSystem(system) end + +function World:refresh() end + +function World:update() end + +function World:clearEntities() end + +function World:clearSystems() end + +function World:getEntityCount() end + +function World:getSystemCount() end + +function World:setSystemIndex() end diff --git a/lib/tiny.lua b/lib/tiny.lua new file mode 100644 index 0000000..73c222c --- /dev/null +++ b/lib/tiny.lua @@ -0,0 +1,933 @@ +--[[ +Copyright (c) 2016 Calvin Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +---@class System +---@field world World field points to the World that the System belongs to. Useful for adding and removing Entities from the world dynamically via the System. +---@field active boolean flag for whether or not the System is updated automatically. Inactive Systems should be updated manually or not at all via system:update(dt). Defaults to true. +---@field entities table[] is an ordered list of Entities in the System. This list can be used to quickly iterate through all Entities in a System. +---@field interval number is an optional field that makes Systems update at certain intervals using buffered time, regardless of World update frequency. For example, to make a System update once a second, set the System's interval to 1. +---@field index number is the System's index in the World. Lower indexed Systems are processed before higher indices. The index is a read only field; to set the index, use tiny.setSystemIndex(world, system). +---@field indices table field is a table of Entity keys to their indices in the entities list. Most Systems can ignore this. +---@field modified boolean indicator for if the System has been modified in the last update. If so, the onModify callback will be called on the System in the next update, if it has one. This is usually managed by tiny-ecs, so users should mostly ignore this, too. + + +--- @module tiny-ecs +-- @author Calvin Rose +-- @license MIT +-- @copyright 2016 +local tiny = {} + +-- Local versions of standard lua functions +local tinsert = table.insert +local tremove = table.remove +local tsort = table.sort +local setmetatable = setmetatable +local type = type +local select = select + +-- Local versions of the library functions +local tiny_manageEntities +local tiny_manageSystems +local tiny_addEntity +local tiny_addSystem +local tiny_add +local tiny_removeEntity +local tiny_removeSystem + +--- Filter functions. +-- A Filter is a function that selects which Entities apply to a System. +-- Filters take two parameters, the System and the Entity, and return a boolean +-- value indicating if the Entity should be processed by the System. A truthy +-- value includes the entity, while a falsey (nil or false) value excludes the +-- entity. +-- +-- Filters must be added to Systems by setting the `filter` field of the System. +-- Filter's returned by tiny-ecs's Filter functions are immutable and can be +-- used by multiple Systems. +-- +-- local f1 = tiny.requireAll("position", "velocity", "size") +-- local f2 = tiny.requireAny("position", "velocity", "size") +-- +-- local e1 = { +-- position = {2, 3}, +-- velocity = {3, 3}, +-- size = {4, 4} +-- } +-- +-- local entity2 = { +-- position = {4, 5}, +-- size = {4, 4} +-- } +-- +-- local e3 = { +-- position = {2, 3}, +-- velocity = {3, 3} +-- } +-- +-- print(f1(nil, e1), f1(nil, e2), f1(nil, e3)) -- prints true, false, false +-- print(f2(nil, e1), f2(nil, e2), f2(nil, e3)) -- prints true, true, true +-- +-- Filters can also be passed as arguments to other Filter constructors. This is +-- a powerful way to create complex, custom Filters that select a very specific +-- set of Entities. +-- +-- -- Selects Entities with an "image" Component, but not Entities with a +-- -- "Player" or "Enemy" Component. +-- filter = tiny.requireAll("image", tiny.rejectAny("Player", "Enemy")) +-- +-- @section Filter + +-- A helper function to compile filters. +local filterJoin + +-- A helper function to filters from string +local filterBuildString + + +local function filterJoinRaw(invert, joining_op, ...) + local _args = {...} + + return function(system, e) + local acc + local args = _args + if joining_op == 'or' then + acc = false + for i = 1, #args do + local v = args[i] + if type(v) == "string" then + acc = acc or (e[v] ~= nil) + elseif type(v) == "function" then + acc = acc or v(system, e) + else + error 'Filter token must be a string or a filter function.' + end + end + else + acc = true + for i = 1, #args do + local v = args[i] + if type(v) == "string" then + acc = acc and (e[v] ~= nil) + elseif type(v) == "function" then + acc = acc and v(system, e) + else + error 'Filter token must be a string or a filter function.' + end + end + end + + -- computes a simple xor + if invert then + return not acc + else + return acc + end + end +end + +do + + function filterJoin(...) + local state, value = pcall(filterJoinRaw, ...) + if state then return value else return nil, value end + end + + local function buildPart(str) + local accum = {} + local subParts = {} + str = str:gsub('%b()', function(p) + subParts[#subParts + 1] = buildPart(p:sub(2, -2)) + return ('\255%d'):format(#subParts) + end) + for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do + if part:match('^\255%d+$') then + local partIndex = tonumber(part:match(part:sub(2))) + accum[#accum + 1] = ('%s(%s)') + :format(invert == '' and '' or 'not', subParts[partIndex]) + else + accum[#accum + 1] = ("(e[%s] %s nil)") + :format(make_safe(part), invert == '' and '~=' or '==') + end + if sep ~= '' then + accum[#accum + 1] = (sep == '|' and ' or ' or ' and ') + end + end + return table.concat(accum) + end + + function filterBuildString(str) + local source = ("return function(_, e) return %s end") + :format(buildPart(str)) + local loader, err = loadstring(source) + if err then + error(err) + end + return loader() + end + +end + +--- Makes a Filter that selects Entities with all specified Components and +-- Filters. +function tiny.requireAll(...) + return filterJoin(false, 'and', ...) +end + +--- Makes a Filter that selects Entities with at least one of the specified +-- Components and Filters. +function tiny.requireAny(...) + return filterJoin(false, 'or', ...) +end + +--- Makes a Filter that rejects Entities with all specified Components and +-- Filters, and selects all other Entities. +function tiny.rejectAll(...) + return filterJoin(true, 'and', ...) +end + +--- Makes a Filter that rejects Entities with at least one of the specified +-- Components and Filters, and selects all other Entities. +function tiny.rejectAny(...) + return filterJoin(true, 'or', ...) +end + +--- Makes a Filter from a string. Syntax of `pattern` is as follows. +-- +-- * Tokens are alphanumeric strings including underscores. +-- * Tokens can be separated by |, &, or surrounded by parentheses. +-- * Tokens can be prefixed with !, and are then inverted. +-- +-- Examples are best: +-- 'a|b|c' - Matches entities with an 'a' OR 'b' OR 'c'. +-- 'a&!b&c' - Matches entities with an 'a' AND NOT 'b' AND 'c'. +-- 'a|(b&c&d)|e - Matches 'a' OR ('b' AND 'c' AND 'd') OR 'e' +-- @param pattern +function tiny.filter(pattern) + local state, value = pcall(filterBuildString, pattern) + if state then return value else return nil, value end +end + +--- System functions. +-- A System is a wrapper around function callbacks for manipulating Entities. +-- Systems are implemented as tables that contain at least one method; +-- an update function that takes parameters like so: +-- +-- * `function system:update(dt)`. +-- +-- There are also a few other optional callbacks: +-- +-- * `function system:filter(entity)` - Returns true if this System should +-- include this Entity, otherwise should return false. If this isn't specified, +-- no Entities are included in the System. +-- * `function system:onAdd(entity)` - Called when an Entity is added to the +-- System. +-- * `function system:onRemove(entity)` - Called when an Entity is removed +-- from the System. +-- * `function system:onModify(dt)` - Called when the System is modified by +-- adding or removing Entities from the System. +-- * `function system:onAddToWorld(world)` - Called when the System is added +-- to the World, before any entities are added to the system. +-- * `function system:onRemoveFromWorld(world)` - Called when the System is +-- removed from the world, after all Entities are removed from the System. +-- * `function system:preWrap(dt)` - Called on each system before update is +-- called on any system. +-- * `function system:postWrap(dt)` - Called on each system in reverse order +-- after update is called on each system. The idea behind `preWrap` and +-- `postWrap` is to allow for systems that modify the behavior of other systems. +-- Say there is a DrawingSystem, which draws sprites to the screen, and a +-- PostProcessingSystem, that adds some blur and bloom effects. In the preWrap +-- method of the PostProcessingSystem, the System could set the drawing target +-- for the DrawingSystem to a special buffer instead the screen. In the postWrap +-- method, the PostProcessingSystem could then modify the buffer and render it +-- to the screen. In this setup, the PostProcessingSystem would be added to the +-- World after the drawingSystem (A similar but less flexible behavior could +-- be accomplished with a single custom update function in the DrawingSystem). +-- +-- For Filters, it is convenient to use `tiny.requireAll` or `tiny.requireAny`, +-- but one can write their own filters as well. Set the Filter of a System like +-- so: +-- system.filter = tiny.requireAll("a", "b", "c") +-- or +-- function system:filter(entity) +-- return entity.myRequiredComponentName ~= nil +-- end +-- +-- All Systems also have a few important fields that are initialized when the +-- system is added to the World. A few are important, and few should be less +-- commonly used. +-- +-- * The `world` field points to the World that the System belongs to. Useful +-- for adding and removing Entities from the world dynamically via the System. +-- * The `active` flag is whether or not the System is updated automatically. +-- Inactive Systems should be updated manually or not at all via +-- `system:update(dt)`. Defaults to true. +-- * The `entities` field is an ordered list of Entities in the System. This +-- list can be used to quickly iterate through all Entities in a System. +-- * The `interval` field is an optional field that makes Systems update at +-- certain intervals using buffered time, regardless of World update frequency. +-- For example, to make a System update once a second, set the System's interval +-- to 1. +-- * The `index` field is the System's index in the World. Lower indexed +-- Systems are processed before higher indices. The `index` is a read only +-- field; to set the `index`, use `tiny.setSystemIndex(world, system)`. +-- * The `indices` field is a table of Entity keys to their indices in the +-- `entities` list. Most Systems can ignore this. +-- * The `modified` flag is an indicator if the System has been modified in +-- the last update. If so, the `onModify` callback will be called on the System +-- in the next update, if it has one. This is usually managed by tiny-ecs, so +-- users should mostly ignore this, too. +-- +-- There is another option to (hopefully) increase performance in systems that +-- have items added to or removed from them often, and have lots of entities in +-- them. Setting the `nocache` field of the system might improve performance. +-- It is still experimental. There are some restriction to systems without +-- caching, however. +-- +-- * There is no `entities` table. +-- * Callbacks such onAdd, onRemove, and onModify will never be called +-- * Noncached systems cannot be sorted (There is no entities list to sort). +-- +-- @section System + +-- Use an empty table as a key for identifying Systems. Any table that contains +-- this key is considered a System rather than an Entity. +local systemTableKey = { "SYSTEM_TABLE_KEY" } +tiny.SKIP_PROCESS = { "SKIP_PROCESS_KEY" } + +-- Checks if a table is a System. +local function isSystem(table) + return table[systemTableKey] +end + +-- Update function for all Processing Systems. +local function processingSystemUpdate(system, dt) + local preProcess = system.preProcess + local process = system.process + local postProcess = system.postProcess + + local shouldSkipSystemProcess + if preProcess then + shouldSkipSystemProcess = preProcess(system, dt) + end + + if process and shouldSkipSystemProcess ~= tiny.SKIP_PROCESS then + if system.nocache then + local entities = system.world.entities + local filter = system.filter + if filter then + for i = 1, #entities do + local entity = entities[i] + if filter(system, entity) then + process(system, entity, dt) + end + end + end + else + local entities = system.entities + for i = 1, #entities do + process(system, entities[i], dt) + end + end + end + + if postProcess and shouldSkipSystemProcess ~= tiny.SKIP_PROCESS then + postProcess(system, dt) + end +end + +-- Sorts Systems by a function system.sortDelegate(entity1, entity2) on modify. +local function sortedSystemOnModify(system) + local entities = system.entities + local indices = system.indices + local sortDelegate = system.sortDelegate + if not sortDelegate then + local compare = system.compare + sortDelegate = function(e1, e2) + return compare(system, e1, e2) + end + system.sortDelegate = sortDelegate + end + tsort(entities, sortDelegate) + for i = 1, #entities do + indices[entities[i]] = i + end +end + +--- Creates a new System or System class from the supplied table. If `table` is +-- nil, creates a new table. +function tiny.system(table) + table = table or {} + table[systemTableKey] = true + return table +end + +--- Creates a new Processing System or Processing System class. Processing +-- Systems process each entity individual, and are usually what is needed. +-- Processing Systems have three extra callbacks besides those inheritted from +-- vanilla Systems. +-- +-- function system:preProcess(dt) -- Called before iteration. +-- function system:process(entity, dt) -- Process each entity. +-- function system:postProcess(dt) -- Called after iteration. +-- +-- Processing Systems have their own `update` method, so don't implement a +-- a custom `update` callback for Processing Systems. +-- @see system +function tiny.processingSystem(table) + table = table or {} + table[systemTableKey] = true + table.update = processingSystemUpdate + return table +end + +--- Creates a new Sorted System or Sorted System class. Sorted Systems sort +-- their Entities according to a user-defined method, `system:compare(e1, e2)`, +-- which should return true if `e1` should come before `e2` and false otherwise. +-- Sorted Systems also override the default System's `onModify` callback, so be +-- careful if defining a custom callback. However, for processing the sorted +-- entities, consider `tiny.sortedProcessingSystem(table)`. +-- @see system +function tiny.sortedSystem(table) + table = table or {} + table[systemTableKey] = true + table.onModify = sortedSystemOnModify + return table +end + +--- Creates a new Sorted Processing System or Sorted Processing System class. +-- Sorted Processing Systems have both the aspects of Processing Systems and +-- Sorted Systems. +-- @see system +-- @see processingSystem +-- @see sortedSystem +function tiny.sortedProcessingSystem(table) + table = table or {} + table[systemTableKey] = true + table.update = processingSystemUpdate + table.onModify = sortedSystemOnModify + return table +end + +--- World functions. +-- A World is a container that manages Entities and Systems. Typically, a +-- program uses one World at a time. +-- +-- For all World functions except `tiny.world(...)`, object-oriented syntax can +-- be used instead of the documented syntax. For example, +-- `tiny.add(world, e1, e2, e3)` is the same as `world:add(e1, e2, e3)`. +-- @section World + +-- Forward declaration +local worldMetaTable + +--- Creates a new World. +-- Can optionally add default Systems and Entities. Returns the new World along +-- with default Entities and Systems. +---@return World +function tiny.world(...) + local ret = setmetatable({ + + -- List of Entities to remove + entitiesToRemove = {}, + + -- List of Entities to change + entitiesToChange = {}, + + -- List of Entities to add + systemsToAdd = {}, + + -- List of Entities to remove + systemsToRemove = {}, + + -- Set of Entities + entities = {}, + + -- List of Systems + systems = {} + + }, worldMetaTable) + + tiny_add(ret, ...) + tiny_manageSystems(ret) + tiny_manageEntities(ret) + + return ret, ... +end + +--- Adds an Entity to the world. +-- Also call this on Entities that have changed Components such that they +-- match different Filters. Returns the Entity. +-- TODO: Track entity age when debugging? +-- TODO: Track debugName field when debugging? +function tiny.addEntity(world, entity) + local e2c = world.entitiesToChange + e2c[#e2c + 1] = entity + return entity +end +tiny_addEntity = tiny.addEntity + +if tinyTrackEntityAges then + local wrapped = tiny.addEntity + function tiny.addEntity(world, entity) + local added = wrapped(world, entity) + added[ENTITY_INIT_MS] = getCurrentTimeMilliseconds() + return added + end + tiny_addEntity = tiny.addEntity +end + +if tinyWarnWhenNonDataOnEntities then + local wrapped = tiny.addEntity + function tiny.addEntity(world, entity) + local added = wrapped(world, entity) + local nonDataType = checkForNonData(added) + if nonDataType then + print("Detected non-data type '" .. nonDataType .. "' on entity") + end + return added + end + tiny_addEntity = tiny.addEntity +end + +--- Adds a System to the world. Returns the System. +function tiny.addSystem(world, system) + if tinyLogSystemChanges then + print("addSystem '" .. (system.name or "unnamed") .. "'") + end + if system.world ~= nil then + error("System " .. system.name .. " already belongs to a World.") + end + local s2a = world.systemsToAdd + s2a[#s2a + 1] = system + system.world = world + return system +end +tiny_addSystem = tiny.addSystem + +--- Shortcut for adding multiple Entities and Systems to the World. Returns all +-- added Entities and Systems. +function tiny.add(world, ...) + for i = 1, select("#", ...) do + local obj = select(i, ...) + if obj then + if isSystem(obj) then + tiny_addSystem(world, obj) + else -- Assume obj is an Entity + tiny_addEntity(world, obj) + end + end + end + return ... +end +tiny_add = tiny.add + +--- Removes an Entity from the World. Returns the Entity. +function tiny.removeEntity(world, entity) + local e2r = world.entitiesToRemove + e2r[#e2r + 1] = entity + return entity +end +tiny_removeEntity = tiny.removeEntity + +--- Removes a System from the world. Returns the System. +function tiny.removeSystem(world, system) + if tinyLogSystemChanges then + print("removeSystem '" .. (system.name or "unnamed") .. "'") + end + if system.world ~= world then + error("System " .. system.name .. " does not belong to this World.") + end + local s2r = world.systemsToRemove + s2r[#s2r + 1] = system + return system +end +tiny_removeSystem = tiny.removeSystem + +--- Shortcut for removing multiple Entities and Systems from the World. Returns +-- all removed Systems and Entities +function tiny.remove(world, ...) + for i = 1, select("#", ...) do + local obj = select(i, ...) + if obj then + if isSystem(obj) then + tiny_removeSystem(world, obj) + else -- Assume obj is an Entity + tiny_removeEntity(world, obj) + end + end + end + return ... +end + +-- Adds and removes Systems that have been marked from the World. +function tiny_manageSystems(world) + local s2a, s2r = world.systemsToAdd, world.systemsToRemove + + -- Early exit + if #s2a == 0 and #s2r == 0 then + return + end + + world.systemsToAdd = {} + world.systemsToRemove = {} + + local worldEntityList = world.entities + local systems = world.systems + + -- Remove Systems + for i = 1, #s2r do + local system = s2r[i] + local index = system.index + local onRemove = system.onRemove + if onRemove and not system.nocache then + local entityList = system.entities + for j = 1, #entityList do + onRemove(system, entityList[j]) + end + end + tremove(systems, index) + for j = index, #systems do + systems[j].index = j + end + local onRemoveFromWorld = system.onRemoveFromWorld + if onRemoveFromWorld then + onRemoveFromWorld(system, world) + end + s2r[i] = nil + + -- Clean up System + if tinyLogSystemChanges then + print("Cleaning up system '" .. (system.name or "unnamed") .. "'") + end + system.world = nil + system.entities = nil + system.indices = nil + system.index = nil + end + + -- Add Systems + for i = 1, #s2a do + local system = s2a[i] + if systems[system.index or 0] ~= system then + if not system.nocache then + system.entities = {} + system.indices = {} + end + if system.active == nil then + system.active = true + end + system.modified = true + system.world = world + local index = #systems + 1 + system.index = index + systems[index] = system + local onAddToWorld = system.onAddToWorld + if onAddToWorld then + onAddToWorld(system, world) + end + + -- Try to add Entities + if not system.nocache then + local entityList = system.entities + local entityIndices = system.indices + local onAdd = system.onAdd + local filter = system.filter + if filter then + for j = 1, #worldEntityList do + local entity = worldEntityList[j] + if filter(system, entity) then + local entityIndex = #entityList + 1 + entityList[entityIndex] = entity + entityIndices[entity] = entityIndex + if onAdd then + onAdd(system, entity) + end + end + end + end + end + end + s2a[i] = nil + end +end + +-- Adds, removes, and changes Entities that have been marked. +function tiny_manageEntities(world) + + local e2r = world.entitiesToRemove + local e2c = world.entitiesToChange + + -- Early exit + if #e2r == 0 and #e2c == 0 then + return + end + + world.entitiesToChange = {} + world.entitiesToRemove = {} + + local entities = world.entities + local systems = world.systems + + -- Change Entities + for i = 1, #e2c do + local entity = e2c[i] + -- Add if needed + if not entities[entity] then + local index = #entities + 1 + entities[entity] = index + entities[index] = entity + end + for j = 1, #systems do + local system = systems[j] + if not system.nocache then + local ses = system.entities + local seis = system.indices + local index = seis[entity] + local filter = system.filter + if filter and filter(system, entity) then + if not index then + system.modified = true + index = #ses + 1 + ses[index] = entity + seis[entity] = index + local onAdd = system.onAdd + if onAdd then + onAdd(system, entity) + end + end + elseif index then + system.modified = true + local tmpEntity = ses[#ses] + ses[index] = tmpEntity + seis[tmpEntity] = index + seis[entity] = nil + ses[#ses] = nil + local onRemove = system.onRemove + if onRemove then + onRemove(system, entity) + end + end + end + end + e2c[i] = nil + end + + -- Remove Entities + for i = 1, #e2r do + local entity = e2r[i] + e2r[i] = nil + local listIndex = entities[entity] + if listIndex then + -- Remove Entity from world state + local lastEntity = entities[#entities] + entities[lastEntity] = listIndex + entities[entity] = nil + entities[listIndex] = lastEntity + entities[#entities] = nil + -- Remove from cached systems + for j = 1, #systems do + local system = systems[j] + if not system.nocache then + local ses = system.entities + local seis = system.indices + local index = seis[entity] + if index then + system.modified = true + local tmpEntity = ses[#ses] + ses[index] = tmpEntity + seis[tmpEntity] = index + seis[entity] = nil + ses[#ses] = nil + local onRemove = system.onRemove + if onRemove then + onRemove(system, entity) + end + end + end + end + end + end +end + +--- Manages Entities and Systems marked for deletion or addition. Call this +-- before modifying Systems and Entities outside of a call to `tiny.update`. +-- Do not call this within a call to `tiny.update`. +function tiny.refresh(world) + tiny_manageSystems(world) + tiny_manageEntities(world) + local systems = world.systems + for i = #systems, 1, -1 do + local system = systems[i] + if system.active then + local onModify = system.onModify + if onModify and system.modified then + onModify(system, 0) + end + system.modified = false + end + end +end + +--- Updates the World by dt (delta time). Takes an optional parameter, `filter`, +-- which is a Filter that selects Systems from the World, and updates only those +-- Systems. If `filter` is not supplied, all Systems are updated. Put this +-- function in your main loop. +function tiny.update(world, dt, filter) + + tiny_manageSystems(world) + tiny_manageEntities(world) + + local systems = world.systems + + -- Iterate through Systems IN REVERSE ORDER + for i = #systems, 1, -1 do + local system = systems[i] + if system.active then + -- Call the modify callback on Systems that have been modified. + local onModify = system.onModify + if onModify and system.modified then + onModify(system, dt) + end + local preWrap = system.preWrap + if preWrap and + ((not filter) or filter(world, system)) then + preWrap(system, dt) + end + end + end + + local tinyLogSystemUpdateTime = tinyLogSystemUpdateTime + -- Iterate through Systems IN ORDER + for i = 1, #systems do + local system = systems[i] + if system.active and ((not filter) or filter(world, system)) then + -- Update Systems that have an update method (most Systems) + local update = system.update + if update then + local currentMs = tinyLogSystemUpdateTime and getCurrentTimeMilliseconds() + local interval = system.interval + if interval then + local bufferedTime = (system.bufferedTime or 0) + dt + while bufferedTime >= interval do + bufferedTime = bufferedTime - interval + update(system, interval) + end + system.bufferedTime = bufferedTime + else + update(system, dt) + end + if tinyLogSystemUpdateTime then + local endTimeMs = getCurrentTimeMilliseconds() + print(tostring(endTimeMs - currentMs) .. "ms taken to update system '" .. system.name .. "'") + end + end + + system.modified = false + end + end + if tinyLogSystemUpdateTime then + print("") + end + + -- Iterate through Systems IN ORDER AGAIN + for i = 1, #systems do + local system = systems[i] + local postWrap = system.postWrap + if postWrap and system.active and + ((not filter) or filter(world, system)) then + postWrap(system, dt) + end + end + +end + +--- Removes all Entities from the World. +function tiny.clearEntities(world) + local el = world.entities + for i = 1, #el do + tiny_removeEntity(world, el[i]) + end +end + +--- Removes all Systems from the World. +function tiny.clearSystems(world) + local systems = world.systems + for i = #systems, 1, -1 do + tiny_removeSystem(world, systems[i]) + end +end + +--- Gets number of Entities in the World. +function tiny.getEntityCount(world) + return #world.entities +end + +--- Gets number of Systems in World. +function tiny.getSystemCount(world) + return #world.systems +end + +--- Sets the index of a System in the World, and returns the old index. Changes +-- the order in which they Systems processed, because lower indexed Systems are +-- processed first. Returns the old system.index. +function tiny.setSystemIndex(world, system, index) + tiny_manageSystems(world) + local oldIndex = system.index + local systems = world.systems + + if index < 0 then + index = tiny.getSystemCount(world) + 1 + index + end + + tremove(systems, oldIndex) + tinsert(systems, index, system) + + for i = oldIndex, index, index >= oldIndex and 1 or -1 do + systems[i].index = i + end + + return oldIndex +end + +-- Construct world metatable. +worldMetaTable = { + __index = { + add = tiny.add, + addEntity = tiny.addEntity, + addSystem = tiny.addSystem, + remove = tiny.remove, + removeEntity = tiny.removeEntity, + removeSystem = tiny.removeSystem, + refresh = tiny.refresh, + update = tiny.update, + clearEntities = tiny.clearEntities, + clearSystems = tiny.clearSystems, + getEntityCount = tiny.getEntityCount, + getSystemCount = tiny.getSystemCount, + setSystemIndex = tiny.setSystemIndex + }, + __tostring = function() + return "" + end +} + +_G.tiny = tiny +return tiny \ No newline at end of file diff --git a/src/assets/fonts/Asheville-Sans-14-Bold-table-20-20.png b/src/assets/fonts/Asheville-Sans-14-Bold-table-20-20.png new file mode 100755 index 0000000..2b3c6a8 Binary files /dev/null and b/src/assets/fonts/Asheville-Sans-14-Bold-table-20-20.png differ diff --git a/src/assets/fonts/Asheville-Sans-14-Bold.fnt b/src/assets/fonts/Asheville-Sans-14-Bold.fnt new file mode 100755 index 0000000..3ee7fa3 --- /dev/null +++ b/src/assets/fonts/Asheville-Sans-14-Bold.fnt @@ -0,0 +1,295 @@ +tracking=1 +space 3 +! 2 +" 5 +# 9 +$ 8 +% 12 +& 11 +' 3 +( 5 +) 5 +* 8 ++ 8 +, 3 +- 6 +. 2 +/ 6 +0 9 +1 4 +2 9 +3 9 +4 9 +5 9 +6 9 +7 9 +8 10 +9 9 +: 2 +; 2 +< 7 += 7 +> 7 +? 9 +@ 11 +A 10 +B 9 +C 9 +D 9 +E 8 +F 8 +G 9 +H 9 +I 2 +J 8 +K 10 +L 9 +M 12 +N 9 +O 9 +P 9 +Q 9 +R 9 +S 9 +T 10 +U 9 +V 10 +W 14 +X 8 +Y 8 +Z 8 +[ 3 +\ 6 +] 3 +^ 6 +_ 8 +` 3 +a 8 +b 8 +c 8 +d 8 +e 8 +f 6 +g 8 +h 8 +i 2 +j 4 +k 8 +l 2 +m 12 +n 8 +o 8 +p 8 +q 8 +r 6 +s 8 +t 6 +u 8 +v 8 +w 12 +x 9 +y 8 +z 8 +{ 6 +| 2 +} 6 +~ 10 +… 8 +¥ 8 +‼ 5 +™ 8 +© 11 +® 11 +。 16 +、 16 +ぁ 16 +あ 16 +ぃ 16 +い 16 +ぅ 16 +う 16 +ぇ 16 +え 16 +ぉ 16 +お 16 +か 16 +が 16 +き 16 +ぎ 16 +く 16 +ぐ 16 +け 16 +げ 16 +こ 16 +ご 16 +さ 16 +ざ 16 +し 16 +じ 16 +す 16 +ず 16 +せ 16 +ぜ 16 +そ 16 +ぞ 16 +た 16 +だ 16 +ち 16 +ぢ 16 +っ 16 +つ 16 +づ 16 +て 16 +で 16 +と 16 +ど 16 +な 16 +に 16 +ぬ 16 +ね 16 +の 16 +は 16 +ば 16 +ぱ 16 +ひ 16 +び 16 +ぴ 16 +ふ 16 +ぶ 16 +ぷ 16 +へ 16 +べ 16 +ぺ 16 +ほ 16 +ぼ 16 +ぽ 16 +ま 16 +み 16 +む 16 +め 16 +も 16 +ゃ 16 +や 16 +ゅ 16 +ゆ 16 +ょ 16 +よ 16 +ら 16 +り 16 +る 16 +れ 16 +ろ 16 +ゎ 16 +わ 16 +ゐ 16 +ゑ 16 +を 16 +ん 16 +ゔ 16 +ゕ 16 +ゖ 16 +゛ 1 +゜ 0 +ゝ 16 +ゞ 16 +ゟ 16 +゠ 16 +ァ 16 +ア 16 +ィ 16 +イ 16 +ゥ 16 +ウ 16 +ェ 16 +エ 16 +ォ 16 +オ 16 +カ 16 +ガ 16 +キ 16 +ギ 16 +ク 16 +グ 16 +ケ 16 +ゲ 16 +コ 16 +ゴ 16 +サ 16 +ザ 16 +シ 16 +ジ 16 +ス 16 +ズ 16 +セ 16 +ゼ 16 +ソ 16 +ゾ 16 +タ 16 +ダ 16 +チ 16 +ヂ 16 +ッ 16 +ツ 16 +ヅ 16 +テ 16 +デ 16 +ト 16 +ド 16 +ナ 16 +ニ 16 +ヌ 16 +ネ 16 +ノ 16 +ハ 16 +バ 16 +パ 16 +ヒ 16 +ビ 16 +ピ 16 +フ 16 +ブ 16 +プ 16 +ヘ 16 +ベ 16 +ペ 16 +ホ 16 +ボ 16 +ポ 16 +マ 16 +ミ 16 +ム 16 +メ 16 +モ 16 +ャ 16 +ヤ 16 +ュ 16 +ユ 16 +ョ 16 +ヨ 16 +ラ 16 +リ 16 +ル 16 +レ 16 +ロ 16 +ヮ 16 +ワ 16 +ヰ 16 +ヱ 16 +ヲ 16 +ン 16 +ヴ 16 +ヵ 16 +ヶ 16 +ヷ 16 +ヸ 16 +ヹ 16 +ヺ 16 +・ 16 +ー 16 +ヽ 16 +ヾ 16 +ヿ 16 +「 16 +」 16 +円 16 +� 13 \ No newline at end of file diff --git a/src/generated/all-systems.lua b/src/generated/all-systems.lua new file mode 100644 index 0000000..6d7b944 --- /dev/null +++ b/src/generated/all-systems.lua @@ -0,0 +1,7 @@ +import("../systems/camera-pan.lua") +import("../systems/collision-detection.lua") +import("../systems/collision-resolution.lua") +import("../systems/draw.lua") +import("../systems/gravity.lua") +import("../systems/input.lua") +import("../systems/velocity.lua") diff --git a/src/generated/all-systems.lua2p b/src/generated/all-systems.lua2p new file mode 100644 index 0000000..5a7dd6e --- /dev/null +++ b/src/generated/all-systems.lua2p @@ -0,0 +1,13 @@ +!( +function getAllSystems() + local p = io.popen('find src/systems -iname "*.lua" -maxdepth 1 -type f | sort -h') + local imports = "" + --Loop through all files + for file in p:lines() do + local varName = file:gsub(".*/(.*.lua)", "%1") + file = file:gsub("src/", "") + imports = imports .. 'import("../' .. file .. '")\n' + end + return imports:sub(1, #imports - 1) +end +)!!(getAllSystems()) \ No newline at end of file diff --git a/src/generated/assets.lua b/src/generated/assets.lua new file mode 100644 index 0000000..feb1ced --- /dev/null +++ b/src/generated/assets.lua @@ -0,0 +1,11 @@ +-- GENERATED FILE - DO NOT EDIT +-- Instead, edit the source file directly: assets.lua2p. + + + + +-- luacheck: ignore +---@type pd_font +AshevilleSans14Bold = playdate.graphics.font.new("assets/fonts/Asheville-Sans-14-Bold.pft") + + diff --git a/src/generated/assets.lua2p b/src/generated/assets.lua2p new file mode 100644 index 0000000..0538e2f --- /dev/null +++ b/src/generated/assets.lua2p @@ -0,0 +1,33 @@ +!(function dirLookup(dir, extension, newFunc, type, sep, indent, handle) + indent = indent or "" + sep = sep or "\n\n" + handle = handle ~= nil and handle or function(varName, value) + return varName .. ' = ' .. value + end + + local p = io.popen('find src/' .. dir .. ' -maxdepth 1 -type f | sort -h') + + local assetCode = "" + --Loop through all files + for file in p:lines() do + if file:find(extension) then + local varName = file:gsub(".*/(.*)." .. extension, "%1") + file = file:gsub("src/", "") + assetCode = assetCode .. indent .. '-- luacheck: ignore\n' + assetCode = assetCode .. indent .. '---@type ' .. type ..'\n' + assetCode = assetCode .. indent .. handle(varName, newFunc .. '("' .. file .. '")') .. sep + end + end + return assetCode +end +function generatedFileWarning() + -- Only in a function to make clear that THIS .lua2p is not the generated file! + return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p." +end)!!(generatedFileWarning()) + +!!(dirLookup('assets/images', 'png', 'playdate.graphics.image.new', 'pd_image')) +!!(dirLookup('assets/sounds', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer')) +!!(dirLookup('assets/music', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer')) +!!(dirLookup('assets/fonts', 'fnt', 'playdate.graphics.font.new', 'pd_font', nil, nil, function(varName, value) + return varName:gsub("[- ]", "") .. " = " .. value:gsub("fnt", "pft") +end)) diff --git a/src/generated/filter-types.lua b/src/generated/filter-types.lua new file mode 100644 index 0000000..af59ef7 --- /dev/null +++ b/src/generated/filter-types.lua @@ -0,0 +1,73 @@ +-- GENERATED FILE - DO NOT EDIT +-- Instead, edit the source file directly: filter-types.lua2p + +-- This file is composed of, essentially, "base types" + +local SOME_TABLE = {} + +---@alias AnyComponent any +---@alias BitMask number +---@alias ButtonState { receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean } +---@alias Collision { collisionBetween: Entity[] } +---@alias CrankState { crankChange: number, changeInLastHalfSecond: number } +---@alias Entity table +---@alias InRelations Entity[] +---@alias XyPair { x: number, y: number } + +T = { + bool = true, + number = 0, + numberArray = { 1, 2, 3 }, + str = "", + marker = SOME_TABLE, + ---@type fun(self) + SelfFunction = function() end, + ---@type pd_image + pd_image = SOME_TABLE, + ---@type pd_font + pd_font = SOME_TABLE, + + ---@type AnyComponent + AnyComponent = SOME_TABLE, + + ---@type BitMask + BitMask = 0, + + ---@type ButtonState + ButtonState = SOME_TABLE, + + ---@type Collision + Collision = SOME_TABLE, + + ---@type CrankState + CrankState = SOME_TABLE, + + ---@type Entity + Entity = SOME_TABLE, + + ---@type InRelations + InRelations = SOME_TABLE, + + ---@type XyPair + XyPair = SOME_TABLE, +} + +---@generic T +---@param t T +---@return nil | T +function Maybe(t) + return { maybe = t } +end + +---@generic T +---@param t T +---@return T[] +function Arr(t) + return { arrOf = t } +end + +TextStyle = { + Inverted = "INVERTED", + Bordered = "BORDERED", + None = "None", +} diff --git a/src/generated/filter-types.lua2p b/src/generated/filter-types.lua2p new file mode 100644 index 0000000..445d508 --- /dev/null +++ b/src/generated/filter-types.lua2p @@ -0,0 +1,98 @@ +!( +local types = {} +function generatedFileWarning() + -- Only in a function to make clear that THIS .lua2p is not the generated file! + return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: filter-types.lua2p" +end + +function t(name, type, value) + if not value then + if type == "number" then + value = 0 + elseif type == "string" then + value = "" + else + value = "SOME_TABLE" + end + end + types[#types + 1] = { name = name, type = type, value = value } + return "---@alias " .. name .. " " .. type +end + +function tMany(tObj) + local ret = "" + local keyValues = {} + for k, v in pairs(tObj) do + keyValues[#keyValues + 1] = { key = k, value = v } + end + table.sort(keyValues, function(a, b) + return a.key < b.key + end) + for _, kv in ipairs(keyValues) do + local k, v = kv.key, kv.value + if type(v) == "string" then + ret = ret .. t(k, v) .. "\n" + else + ret = ret .. t(k, v[1], v[2]) .. "\n" + end + end + return ret +end + +function dumpTypeObjects() + local ret = "" + for _, v in ipairs(types) do + local line = "\n\n ---@type " .. v.name .. "\n " .. v.name .. " = " .. v.value .. "," + ret = ret .. line + end + return ret +end +)!!(generatedFileWarning()) + +-- This file is composed of, essentially, "base types" + +local SOME_TABLE = {} + +!!(tMany({ + AnyComponent = "any", + Entity = "table", + XyPair = "{ x: number, y: number }", + Collision = "{ collisionBetween: Entity[] }", + BitMask = "number", + InRelations = "Entity[]", + ButtonState = "{ receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean }", + CrankState = "{ crankChange: number, changeInLastHalfSecond: number }", +})) +T = { + bool = true, + number = 0, + numberArray = { 1, 2, 3 }, + str = "", + marker = SOME_TABLE, + ---@type fun(self) + SelfFunction = function() end, + ---@type pd_image + pd_image = SOME_TABLE, + ---@type pd_font + pd_font = SOME_TABLE,!!(dumpTypeObjects()) +} + +---@generic T +---@param t T +---@return nil | T +function Maybe(t) + return { maybe = t } +end + +---@generic T +---@param t T +---@return T[] +function Arr(t) + return { arrOf = t } +end + +TextStyle = { + Inverted = "INVERTED", + Bordered = "BORDERED", + None = "None", +} diff --git a/src/main.lua b/src/main.lua new file mode 100644 index 0000000..e46a054 --- /dev/null +++ b/src/main.lua @@ -0,0 +1,47 @@ +require = import +import("CoreLibs/animation.lua") +import("CoreLibs/animator.lua") +import("CoreLibs/easing.lua") +import("CoreLibs/graphics.lua") +import("CoreLibs/object.lua") +import("CoreLibs/timer.lua") +import("CoreLibs/ui.lua") +import("CoreLibs/utilities/where.lua") + +import("../lib/tiny-debug.lua") +import("../lib/tiny.lua") +import("utils.lua") +import("tiny-tools.lua") + +local tiny = tiny +world = tiny.world() + +import("generated/filter-types.lua") +import("generated/assets.lua") +import("generated/all-systems.lua") + +local gfx = playdate.graphics +playdate.display.setRefreshRate(50) +gfx.setBackgroundColor(gfx.kColorWhite) + +local scenarios = { + default = function() + -- TODO: Add default entities + end, + textTestScenario = function() + world:addEntity({ position = { x = 50, y = 50 }, drawAsText = { text = "Hello, world!" } }) + end, +} + +scenarios.textTestScenario() + +function playdate.update() + local deltaSeconds = playdate.getElapsedTime() + playdate.resetElapsedTime() + gfx.clear(gfx.kColorWhite) + playdate.drawFPS(5, 5) + + world:update(deltaSeconds) + + gfx.setDrawOffset(0, 0) +end diff --git a/src/systems/camera-pan.lua b/src/systems/camera-pan.lua new file mode 100644 index 0000000..ae04568 --- /dev/null +++ b/src/systems/camera-pan.lua @@ -0,0 +1,57 @@ +local gfx = playdate.graphics + +Camera = { + pan = { + x = 0, + y = 0, + }, +} + +expireBelowScreenSystem = filteredSystem("expireBelowScreen", { position = T.XyPair, expireBelowScreenBy = T.number }) + +local focusPriority = {} + +cameraPanSystem = filteredSystem("cameraPan", { focusPriority = T.number, position = T.XyPair }, function(e, _) + if e.focusPriority >= focusPriority.priority then + focusPriority.position = e.position + end +end) + +function cameraPanSystem.preProcess() + focusPriority.priority = 0 + focusPriority.position = { x = 0, y = 0 } +end + +function cameraPanSystem:postProcess() + Camera.pan.x = math.max(0, focusPriority.position.x - 200) + Camera.pan.y = math.min(0, focusPriority.position.y - 120) + gfx.setDrawOffset(-Camera.pan.x, -Camera.pan.y) + + for _, entity in pairs(expireBelowScreenSystem.entities) do + if entity.position.y - (Camera.pan.y + 240) > entity.expireBelowScreenBy then + self.world:removeEntity(entity) + end + end +end + +local cameraTopIsh, cameraBottomIsh + +local enableNearCameraY = filteredSystem( + "enableNearCameraY", + { enableNearCameraY = Arr(T.Entity) }, + function(e, _, system) + if e.position.y > cameraTopIsh and e.position.y < cameraBottomIsh then + for _, enable in ipairs(e.enableNearCameraY) do + enable.velocity = e.velocity + system.world:addEntity(enable) + end + system.world:removeEntity(e) + end + end +) + +local within = 1000 +function enableNearCameraY:preProcess() + cameraTopIsh = Camera.pan.y - within + cameraBottomIsh = Camera.pan.y + 240 + within +end diff --git a/src/systems/collision-detection.lua b/src/systems/collision-detection.lua new file mode 100644 index 0000000..376dd15 --- /dev/null +++ b/src/systems/collision-detection.lua @@ -0,0 +1,39 @@ +collidingEntities = filteredSystem("collidingEntitites", { + velocity = T.XyPair, + position = T.XyPair, + size = T.XyPair, + canCollideWith = T.BitMask, + isSolid = Maybe(T.bool), +}) + +filteredSystem( + "collisionDetection", + { position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) }, + -- Here, the entity, e, refers to some entity that a moving object may be colliding *into* + function(e, _, system) + for _, collider in pairs(collidingEntities.entities) do + if + (e ~= collider) + and collider.canCollideWith + and e.canBeCollidedBy + and ((collider.canCollideWith & e.canBeCollidedBy) ~= 0) + then + local colliderTop = collider.position.y + local colliderBottom = collider.position.y + collider.size.y + local entityTop = e.position.y + local entityBottom = entityTop + e.size.y + + local withinY = (entityTop > colliderTop and entityTop < colliderBottom) + or (entityBottom > colliderTop and entityBottom < colliderBottom) + + if + withinY + and collider.position.x < e.position.x + e.size.x + and collider.position.x + collider.size.x > e.position.x + then + system.world:addEntity({ collisionBetween = { e, collider } }) + end + end + end + end +) diff --git a/src/systems/collision-resolution.lua b/src/systems/collision-resolution.lua new file mode 100644 index 0000000..4a54df7 --- /dev/null +++ b/src/systems/collision-resolution.lua @@ -0,0 +1,4 @@ +filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system) + local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2] + system.world:removeEntity(e) +end) diff --git a/src/systems/draw.lua b/src/systems/draw.lua new file mode 100644 index 0000000..7f33ca2 --- /dev/null +++ b/src/systems/draw.lua @@ -0,0 +1,45 @@ +local gfx = playdate.graphics + +filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _) + gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) +end) + +filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e) + if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then + return + end + e.drawAsSprite:draw(e.position.x, e.position.y) +end) + +local xMargin = 4 + +filteredSystem( + "drawText", + { position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } }, + function(e) + local font = e.drawAsText.font or AshevilleSans14Bold + local textHeight = font:getHeight() + local textWidth = font:getTextWidth(e.drawAsText.text) + + local bgLeftEdge = e.position.x - xMargin - textWidth / 2 + local bgTopEdge = e.position.y - 2 + local bgWidth, bgHeight = textWidth + (xMargin * 2), textHeight + 2 + + if e.drawAsText.style == TextStyle.Inverted then + gfx.fillRect(bgLeftEdge, bgTopEdge, textWidth + (xMargin * 2), textHeight + 2) + gfx.setImageDrawMode(gfx.kDrawModeInverted) + elseif e.drawAsText.style == TextStyle.Bordered then + gfx.setColor(gfx.kColorWhite) + gfx.fillRect(bgLeftEdge, bgTopEdge, bgWidth, bgHeight) + + gfx.setImageDrawMode(gfx.kDrawModeCopy) + gfx.setColor(gfx.kColorBlack) + gfx.drawRect(bgLeftEdge, bgTopEdge, bgWidth, bgHeight) + end + + font:drawTextAligned(e.drawAsText.text, e.position.x, e.position.y, kTextAlignment.center) + if e.drawAsText.style == TextStyle.Inverted then + gfx.setImageDrawMode(gfx.kDrawModeCopy) + end + end +) diff --git a/src/systems/gravity.lua b/src/systems/gravity.lua new file mode 100644 index 0000000..78af4ef --- /dev/null +++ b/src/systems/gravity.lua @@ -0,0 +1,17 @@ +local min = math.min + +world:addEntity({ gravity = -300 }) + +gravities = filteredSystem("gravities", { gravity = T.number }) + +filteredSystem("changeGravity", { changeGravityTo = T.number }, function(e, _, _) + for _, ge in pairs(gravities.entities) do + ge.gravity = e.changeGravityTo + end +end) + +filteredSystem("fall", { velocity = T.XyPair, mass = T.number }, function(e, dt) + for _, ge in pairs(gravities.entities) do + e.velocity.y = min(400, e.velocity.y - (ge.gravity * dt * e.mass) - (0.5 * dt * dt)) + end +end) diff --git a/src/systems/input.lua b/src/systems/input.lua new file mode 100644 index 0000000..aa49d75 --- /dev/null +++ b/src/systems/input.lua @@ -0,0 +1,66 @@ +local buttonJustPressed = playdate.buttonJustPressed + +---@type ButtonState +local buttonState = {} + +buttonInputSystem = filteredSystem("buttonInput", { canReceiveButtons = T.marker }, function(e, _, system) + e.buttonState = buttonState + system.world:addEntity(e) +end) + +function buttonInputSystem:preProcess() + if #self.entities == 0 then + return + end + buttonState.upJustPressed = buttonJustPressed(playdate.kButtonUp) + buttonState.downJustPressed = buttonJustPressed(playdate.kButtonDown) + buttonState.rightJustPressed = buttonJustPressed(playdate.kButtonRight) + buttonState.leftJustPressed = buttonJustPressed(playdate.kButtonLeft) + buttonState.aJustPressed = buttonJustPressed(playdate.kButtonA) + buttonState.bJustPressed = buttonJustPressed(playdate.kButtonB) + + buttonState.receivedInputThisFrame = buttonState.upJustPressed + or buttonState.downJustPressed + or buttonState.rightJustPressed + or buttonState.leftJustPressed + or buttonState.aJustPressed + or buttonState.bJustPressed +end + +local crankState = {} + +crankInputSystem = filteredSystem( + "crankInput", + tiny.requireAny("canReceiveCrank", "launchedByCrank"), + function(e, _, system) + e.crankState = crankState + system.world:addEntity(e) + end +) + +crankHistory = filteredSystem("crankHistory", { crankChange = T.number, msOccurred = T.number }) + +function crankInputSystem:preProcess() + if #self.entities == 0 then + return + end + local currentMs = playdate.getCurrentTimeMilliseconds() + local crankChange = playdate.getCrankChange() + + self.world:addEntity({ + crankChange = crankChange, + msOccurred = currentMs, + }) + + local changeInLastHalfSecond = 0 + for _, v in pairs(crankHistory.entities) do + if currentMs - v.msOccurred > 500 then + self.world:removeEntity(v) + else + changeInLastHalfSecond = changeInLastHalfSecond + v.crankChange + end + end + + crankState.crankChange = crankChange + crankState.changeInLastHalfSecond = changeInLastHalfSecond +end diff --git a/src/systems/velocity.lua b/src/systems/velocity.lua new file mode 100644 index 0000000..97b70f7 --- /dev/null +++ b/src/systems/velocity.lua @@ -0,0 +1,15 @@ +local sqrt = math.sqrt + +filteredSystem("velocity", { position = T.XyPair, velocity = T.XyPair }, function(e, dt, system) + if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 2 then + e.velocity = nil + if e.spawnEntitiesWhenStopped then + e:spawnEntitiesWhenStopped(system.world) + e.spawnEntitiesWhenStopped = nil + end + system.world:addEntity(e) + else + e.position.x = e.position.x + (e.velocity.x * dt) + e.position.y = e.position.y + (e.velocity.y * dt) + end +end) diff --git a/src/tiny-tools.lua b/src/tiny-tools.lua new file mode 100644 index 0000000..45b70fe --- /dev/null +++ b/src/tiny-tools.lua @@ -0,0 +1,34 @@ +---@generic T +---@param shape T | fun() +---@param process fun(entity: T, dt: number, system: System) +---@return System | { entities: T[] } +function filteredSystem(name, shape, process) + assert(type(name) == "string") + assert(type(shape) == "table" or type(shape) == "function") + assert(process == nil or type(process) == "function") + + local system = tiny.processingSystem() + system.name = name + if type(shape) == "table" then + local keys = {} + for key, value in pairs(shape) do + local isTable = type(value) == "table" + local isMaybe = isTable and value.maybe ~= nil + + if not isMaybe then + -- ^ Don't require any Maybe types + keys[#keys + 1] = key + end + end + system.filter = tiny.requireAll(table.unpack(keys)) + elseif type(shape) == "function" then + system.filter = shape + end + if not process then + return world:addSystem(system) + end + function system:process(e, dt) + process(e, dt, self) + end + return world:addSystem(system) +end diff --git a/src/utils.lua b/src/utils.lua new file mode 100644 index 0000000..7f55fcf --- /dev/null +++ b/src/utils.lua @@ -0,0 +1,74 @@ +Utils = {} + +--- Returns up to `n` random values from the given array. Will return fewer if `n > #fromArr` +---@generic T +---@param fromArr T[] +---@param n number +---@return T[] +function Utils.getNDifferentValues(fromArr, n) + assert(n >= 0, "n must be a non-negative integer") + if n > #fromArr then + n = #fromArr + end + local found = 0 + local indexes = {} + while found < n do + local randomIndex = math.random(#fromArr) + if not indexes[randomIndex] then + found = found + 1 + indexes[randomIndex] = true + end + end + + local randoms = {} + for i in pairs(indexes) do + randoms[#randoms + 1] = fromArr[i] + end + return randoms +end + +--- Track the number of instances of a given element, instead of needing multiple copies. +---@class CountSet +---@field private data table +---@field private elementCount number +CountSet = {} + +function CountSet.new() + return setmetatable({ data = {}, elementCount = 0 }, { __index = CountSet }) +end + +function CountSet:add(element) + local existing = self.data[element] + if existing then + self.data[element] = existing + 1 + else + self.data[element] = 1 + end + self.elementCount = self.elementCount + 1 +end + +function CountSet:balancedRandomPop() + if self.elementCount == 0 then + return + end + local toPop = math.random(self.elementCount) + for element, count in pairs(self.data) do + toPop = toPop - count + if toPop <= 0 then + local newCount = count - 1 + if newCount == 0 then + self.data[element] = nil + else + self.data[element] = newCount + end + self.elementCount = self.elementCount - 1 + return element + end + end +end + +function CountSet:iterRandom() + return function() + return self:balancedRandomPop() + end +end