More handy tools for game developers!

This commit is contained in:
Scott Duensing 2026-09-11 15:46:39 -05:00
parent 9af9f7e35e
commit e7b24ea7b3
26 changed files with 6069 additions and 0 deletions

View file

@ -819,6 +819,49 @@ API Changes
became 24. A framefile naming a file that does not exist still fails became 24. A framefile naming a file that does not exist still fails
at startup, as it always has. at startup, as it always has.
- The game kit: five things every game was writing for itself.
saveGet, saveSet, saveGetAll, saveSetAll, saveDelete, saveClear and
saveFlush keep a table between runs, as JSON in the game's own data
directory. Values may be numbers, strings, booleans or tables of
them, nested; whole numbers come back whole. Tables are copied in and
out, so what you set is what is saved and what you read back is yours
to change. saveGetAll and saveSetAll read and replace the whole save
in one step, which is how an old save is moved to a new layout without
a moment in between where it is empty. One write at the end of any
frame that changed something, and again at shutdown; saveFlush writes
now, for a cabinet that may lose power. The file is renamed over the
old one, so a write that is interrupted leaves the previous save
rather than half of a new one.
timerAfter, timerEvery, timerCancel and timerIsActive run something
later without counting frames. They use the engine's clock, which is
the virtual one under --deterministic, so a timed game repeats
exactly, and they fire with the rest of the frame's callbacks.
tweenValue moves a number from one value to another over time on a
curve, with tweenCancel and tweenIsActive beside it and twenty-two
EASE_ curves to pick from. It hands the number to your function
rather than moving a node itself, so it works on anything.
collideRects, collideCircles, collideRectCircle, collidePointRect,
collidePointCircle, collidePointPolygon and collideSegments answer
whether two flat things touch. Plain functions, no handles. Jolt
already answered this in three dimensions; nothing answered it for the
overlay, where light gun hitboxes live.
statsEnable and statsIsEnabled show the developer's overlay: frame
time and its worst, frame rate, Lua memory, what is alive, how many
draw batches, and what the disc is doing. Drawn with SDL's own debug
font, so it needs no asset, and drawn after singeScreenshot takes its
picture, so it never lands in a screenshot.
- Four more Lua libraries are bundled: middleclass for classes, lume
for the small things (lerp, clamp, round, shuffle, split, serialize),
inspect for printing a table in a readable shape, and bump for moving
things that must not pass through each other -- which is what the
collide calls deliberately do not do.
- Subtitles carried inside the video file: discGetSubtitleTracks(), - Subtitles carried inside the video file: discGetSubtitleTracks(),
discGetSubtitleLanguage(track) and srtLoadTrack(track), which reads discGetSubtitleLanguage(track) and srtLoadTrack(track), which reads
one track out of the disc's own container and loads it exactly as one track out of the disc's own container and loads it exactly as

View file

@ -261,6 +261,10 @@ singeEmbedLua(thirdparty/copas/src/copas/timer.lua "copas_")
singeEmbedLua(thirdparty/binaryheap.lua/src/binaryheap.lua "") singeEmbedLua(thirdparty/binaryheap.lua/src/binaryheap.lua "")
singeEmbedLua(thirdparty/timerwheel.lua/src/timerwheel/timerwheel.lua "") singeEmbedLua(thirdparty/timerwheel.lua/src/timerwheel/timerwheel.lua "")
singeEmbedLua(thirdparty/json.lua/json.lua "") singeEmbedLua(thirdparty/json.lua/json.lua "")
singeEmbedLua(thirdparty/middleclass/middleclass.lua "")
singeEmbedLua(thirdparty/lume/lume.lua "")
singeEmbedLua(thirdparty/inspect.lua/inspect.lua "")
singeEmbedLua(thirdparty/bump.lua/bump.lua "")
# Optional HTML manual for browsing: cmake --build . --target docs # Optional HTML manual for browsing: cmake --build . --target docs
if(ASCIIDOCTOR) if(ASCIIDOCTOR)
@ -276,6 +280,8 @@ endif()
# ===== Sources ===== # ===== Sources =====
set(SINGE_SOURCE set(SINGE_SOURCE
src/collide.c
src/collide.h
src/common.h src/common.h
src/decode.c src/decode.c
src/decode.h src/decode.h
@ -290,6 +296,12 @@ set(SINGE_SOURCE
src/pack.h src/pack.h
src/particles.c src/particles.c
src/particles.h src/particles.h
src/persist.c
src/persist.h
src/scheduler.c
src/scheduler.h
src/stats.c
src/stats.h
src/physics.h src/physics.h
src/navRecast.cpp src/navRecast.cpp
src/physicsJolt.cpp src/physicsJolt.cpp

View file

@ -11,6 +11,7 @@ arg_parser 1.21 BSD-2-Clause http://savannah.nongnu.org/pro
basis_universal 2.50 Apache-2.0 https://github.com/BinomialLLC/basis_universal basis_universal 2.50 Apache-2.0 https://github.com/BinomialLLC/basis_universal
binaryheap.lua 0.4 MIT http://tieske.github.io/binaryheap.lua binaryheap.lua 0.4 MIT http://tieske.github.io/binaryheap.lua
brotli 1.2.0 MIT https://github.com/google/brotli brotli 1.2.0 MIT https://github.com/google/brotli
bump.lua 3.1.7 MIT https://github.com/kikito/bump.lua
cgltf 1.15 MIT https://github.com/jkuhlmann/cgltf cgltf 1.15 MIT https://github.com/jkuhlmann/cgltf
copas 4.12.0 MIT https://lunarmodules.github.io/copas copas 4.12.0 MIT https://lunarmodules.github.io/copas
dav1d 1.5.4 BSD-2-Clause https://code.videolan.org/videolan/dav1d dav1d 1.5.4 BSD-2-Clause https://code.videolan.org/videolan/dav1d
@ -20,6 +21,7 @@ freetype 2.13.2 FTL https://freetype.org (bundled
game-music-emu 0.6.6 LGPL-2.1 https://github.com/libgme/game-music-emu (bundled with SDL3_mixer) game-music-emu 0.6.6 LGPL-2.1 https://github.com/libgme/game-music-emu (bundled with SDL3_mixer)
harfbuzz 14.4.0 MIT https://harfbuzz.github.io (bundled with SDL3_ttf) harfbuzz 14.4.0 MIT https://harfbuzz.github.io (bundled with SDL3_ttf)
highway 1.4.0 Apache-2.0 https://github.com/google/highway (bundled with libjxl) highway 1.4.0 Apache-2.0 https://github.com/google/highway (bundled with libjxl)
inspect.lua 3.1.0 MIT https://github.com/kikito/inspect.lua
JoltPhysics 5.6.0 MIT https://github.com/jrouwe/JoltPhysics JoltPhysics 5.6.0 MIT https://github.com/jrouwe/JoltPhysics
json.lua 0.1.2 MIT https://github.com/rxi/json.lua json.lua 0.1.2 MIT https://github.com/rxi/json.lua
libjpeg 9f IJG https://ijg.org (bundled with SDL3_image) libjpeg 9f IJG https://ijg.org (bundled with SDL3_image)
@ -34,7 +36,9 @@ lua 5.4.9 MIT https://www.lua.org
luafilesystem 1.9.0 MIT https://lunarmodules.github.io/luafilesystem luafilesystem 1.9.0 MIT https://lunarmodules.github.io/luafilesystem
luasec 1.3.2 MIT https://github.com/lunarmodules/luasec luasec 1.3.2 MIT https://github.com/lunarmodules/luasec
luasocket 3.1.0 MIT https://lunarmodules.github.io/luasocket luasocket 3.1.0 MIT https://lunarmodules.github.io/luasocket
lume 2.3.0 MIT https://github.com/rxi/lume
manymouse 0.0.3 Zlib https://icculus.org/manymouse manymouse 0.0.3 Zlib https://icculus.org/manymouse
middleclass 4.1.1 MIT https://github.com/kikito/middleclass
openssl 3.5.8 Apache-2.0 https://www.openssl.org openssl 3.5.8 Apache-2.0 https://www.openssl.org
opus 1.4 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer) opus 1.4 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer)
opusfile 0.12 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer) opusfile 0.12 BSD-3-Clause https://opus-codec.org (bundled with SDL3_mixer)

View file

@ -3137,6 +3137,10 @@ install any additional software:
| lrandom | `require("random")` for a Mersenne Twister generator, separate from `math.random` | lrandom | `require("random")` for a Mersenne Twister generator, separate from `math.random`
| Lua CJSON | `require("cjson")` for JSON encoding and decoding, beside the `json` module above | Lua CJSON | `require("cjson")` for JSON encoding and decoding, beside the `json` module above
| LPeg | `require("lpeg")` for parsing expression grammars | LPeg | `require("lpeg")` for parsing expression grammars
| middleclass | `require("middleclass")` for classes, if you would rather build entities out of them than out of tables
| lume | `require("lume")` for the small things every game needs: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice`
| inspect | `require("inspect")` to print a table in a shape a person can read, for when `debugPrint` is the debugger
| bump | `require("bump")` for moving things that must not pass through each other -- it answers where a mover ends up, which <<collide,the collide calls>> deliberately do not
| RmlUi | The GUI's own object model, the `rmlui` global (see <<gui,GUI>>) | RmlUi | The GUI's own object model, the `rmlui` global (see <<gui,GUI>>)
|=== |===
@ -4460,6 +4464,180 @@ end
---- ----
[#color] [#color]
[#collide]
=== Collide
Whether two flat things touch. Jolt answers this in three dimensions for bodies
(see <<body,Body>>); these answer it for the overlay, which is where a light
gun's hitboxes and a 2D game's everything live.
They are plain functions: no handles, no state, nothing to create or free, and
nothing to update each frame. Coordinates are whatever you are already drawing
in -- overlay pixels, usually. A rectangle is a corner and a size, the way every
drawing call in the engine takes one. Touching exactly at an edge counts as
touching, and a rectangle with no width or height touches nothing at all.
None of these move anything. When you want to know not just *whether* two things
met but *where the mover ends up* -- sliding along a wall rather than stopping
dead in it -- `require("bump")` and let it do that; it is bundled for exactly
that reason.
==== collideCircles
[source,text]
----
touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
----
Whether two circles overlap. Cheaper than it looks: nothing takes a square root to answer a yes or no question.
*Parameters:*
* `x1`, `y1`, `radius1` -- numbers; the first circle.
* `x2`, `y2`, `radius2` -- numbers; the second.
*Returns:* `true` or `false`.
*Since:* 3.00.
*See also:* <<colliderectcircle,collideRectCircle>>
==== collidePointCircle
[source,text]
----
inside = collidePointCircle(pointX, pointY, x, y, radius)
----
Whether a point falls within a circle. The edge counts as inside.
*Parameters:*
* `pointX`, `pointY` -- numbers; the point.
* `x`, `y`, `radius` -- numbers; the circle.
*Returns:* `true` or `false`.
*Since:* 3.00.
==== collidePointPolygon
[source,text]
----
inside = collidePointPolygon(pointX, pointY, points)
----
Whether a point falls within a polygon of any shape, concave ones included. The table is a flat list of `x`, `y`, `x`, `y` -- the shape a hitbox already has in most games -- and the winding does not matter. Fewer than three corners contains nothing. At most 256 corners.
*Parameters:*
* `pointX`, `pointY` -- numbers; the point.
* `points` -- table; an even number of numbers, x and y in turn.
*Returns:* `true` or `false`.
*Since:* 3.00.
.Example
[source,lua]
----
-- Did the shot land on the dragon, whose outline is not a rectangle?
local dragon = { 120,40, 200,60, 210,140, 150,180, 100,120 }
function onInputPressed(key)
if key == SWITCH_BUTTON1 then
local x, y = mouseGetPosition()
if collidePointPolygon(x, y, dragon) then
hit()
end
end
end
----
==== collidePointRect
[source,text]
----
inside = collidePointRect(pointX, pointY, x, y, width, height)
----
Whether a point falls within a rectangle. The edge counts as inside.
*Parameters:*
* `pointX`, `pointY` -- numbers; the point.
* `x`, `y`, `width`, `height` -- numbers; the rectangle.
*Returns:* `true` or `false`.
*Since:* 3.00.
.Example
[source,lua]
----
-- A hitbox the size of the sprite it belongs to.
if collidePointRect(mouseGetPosition()) then end -- (needs the rectangle too; see below)
local x, y = mouseGetPosition()
if collidePointRect(x, y, enemyX, enemyY, spriteGetWidth(enemy), spriteGetHeight(enemy)) then
kill(enemy)
end
----
==== collideRectCircle
[source,text]
----
touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
----
Whether a rectangle and a circle overlap, by finding the rectangle's nearest point to the circle's centre.
*Parameters:*
* `x`, `y`, `width`, `height` -- numbers; the rectangle.
* `circleX`, `circleY`, `radius` -- numbers; the circle.
*Returns:* `true` or `false`.
*Since:* 3.00.
==== collideRects
[source,text]
----
touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
----
Whether two rectangles overlap. The one you will reach for most.
*Parameters:*
* `x1`, `y1`, `width1`, `height1` -- numbers; the first rectangle.
* `x2`, `y2`, `width2`, `height2` -- numbers; the second.
*Returns:* `true` or `false`.
*Since:* 3.00.
*See also:* <<collidepointrect,collidePointRect>>
==== collideSegments
[source,text]
----
crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
----
Whether two line segments cross: a shot along a path against a wall, or the step a mover wants to take against the edge it must not pass. Segments lying along one another answer `false`, because "where do they cross" has no single answer then.
*Parameters:*
* `ax1`, `ay1`, `ax2`, `ay2` -- numbers; the first segment's ends.
* `bx1`, `by1`, `bx2`, `by2` -- numbers; the second's.
*Returns:* `true` or `false`.
*Since:* 3.00.
=== Color === Color
The foreground and background colors are engine globals rather than per-call arguments: set them with `colorForeground` and `colorBackground`, and every later drawing call reads them until they change. The foreground color is used by the `overlay*` shape primitives and by TrueType text from `fontPrint` and `fontToSprite`; the background color is used by `overlayClear` and by `FONT_QUALITY_SHADED` text. `overlayPrint` uses neither. Channels are integers from `0` to `255`, clamped into that range, and both colors accept an optional alpha. See the <<overlay,Overlay>> and <<font,Font>> sections for the calls that consume them. The foreground and background colors are engine globals rather than per-call arguments: set them with `colorForeground` and `colorBackground`, and every later drawing call reads them until they change. The foreground color is used by the `overlay*` shape primitives and by TrueType text from `fontPrint` and `fontToSprite`; the background color is used by `overlayClear` and by `FONT_QUALITY_SHADED` text. `overlayPrint` uses neither. Channels are integers from `0` to `255`, clamped into that range, and both colors accept an optional alpha. See the <<overlay,Overlay>> and <<font,Font>> sections for the calls that consume them.
@ -12765,6 +12943,202 @@ end
---- ----
[#scene] [#scene]
[#save]
=== Save
Anything the game should still know next time it starts: high scores, the
player's options, how far they got. One table per game, kept as JSON in the
game's own data directory (see <<singegetdatapath,`singeGetDataPath`>>), so a
packed `.game` stays read only and two games never tread on each other.
Keys are yours. Values may be numbers, strings, booleans, or tables of those,
nested as deep as you like -- a whole high score table goes in as one value. A
function, or a handle to something the engine owns, does not: neither means
anything on the next run, and saving one is an error rather than a surprise
later.
**What you set is what is saved, and what you get back is yours.** Tables are
copied on the way in and on the way out, so changing a table you read back
changes nothing until you save it again, and changing one you saved does not
reach into the save afterwards. Keeping a preview the player then cancels is
therefore just a matter of not saving it.
Writing happens once at the end of any frame that changed something, however
many keys you set in it, and again when the game shuts down -- including
anything set inside <<onshutdown,`onShutdown`>>. For a cabinet that may lose
power at any moment, <<saveflush,`saveFlush`>> writes immediately. The file is
written beside the old one and renamed over it, so losing power during a write
leaves the previous save rather than half of a new one.
Whole numbers come back as whole numbers. JSON has only one kind of number and
Lua has two, so a score saved as `100` would otherwise return as `100.0` and
print with a decimal point.
==== saveClear
[source,text]
----
saveClear()
----
Forgets everything and writes the empty save out. For a service menu's "reset high scores".
*Parameters:*
* None.
*Returns:* nothing.
*Since:* 3.00.
==== saveDelete
[source,text]
----
saveDelete(key)
----
Removes one key. Deleting a key that was never there is not an error.
*Parameters:*
* `key` -- the key to remove.
*Returns:* nothing.
*Since:* 3.00.
==== saveFlush
[source,text]
----
saveFlush()
----
Writes the save out now rather than at the end of the frame. Use it after something you would hate to lose -- a new high score on a cabinet that is switched off at the wall.
*Parameters:*
* None.
*Returns:* nothing.
*Since:* 3.00.
*See also:* <<saveset,saveSet>>
==== saveGet
[source,text]
----
value = saveGet(key [, default])
----
What was saved under that key. When nothing was, the default comes back, or `nil` when there is no default -- which is how a first run is told apart from a later one.
*Parameters:*
* `key` -- the key to read.
* `default` -- optional; what to answer when the key is not there.
*Returns:* the saved value, the default, or `nil`.
*Since:* 3.00.
*See also:* <<saveset,saveSet>>
.Example
[source,lua]
----
-- Count the runs, and remember the best score.
local runs = saveGet("runs", 0) + 1
saveSet("runs", runs)
local best = saveGet("highScores", {})
best[#best + 1] = { name = initials, score = score }
table.sort(best, function(a, b) return a.score > b.score end)
while #best > 10 do table.remove(best) end
saveSet("highScores", best)
saveFlush()
----
==== saveGetAll
[source,text]
----
everything = saveGetAll()
----
A copy of the whole save, for reading: dumping it while you are debugging, or walking keys whose names the game does not know in advance. Changing what comes back changes nothing -- <<savesetall,`saveSetAll`>> is what writes a whole table.
*Parameters:*
* None.
*Returns:* a table; empty when nothing has been saved.
*Since:* 3.00.
*See also:* <<savesetall,saveSetAll>>, <<saveget,saveGet>>
.Example
[source,lua]
----
-- What is in there? inspect is bundled for exactly this.
debugPrint(require("inspect")(saveGetAll()))
----
==== saveSet
[source,text]
----
saveSet(key, value)
----
Keeps a value until the game is uninstalled. The write happens at the end of the frame, so setting twenty keys in a loop writes one file.
*Parameters:*
* `key` -- the key to write.
* `value` -- a number, a string, a boolean, or a table of those.
*Returns:* nothing.
*Since:* 3.00.
*See also:* <<saveget,saveGet>>, <<saveflush,saveFlush>>
==== saveSetAll
[source,text]
----
saveSetAll(everything)
----
Replaces the whole save with this table, in one step. Keys that were there and are not in the table you give are gone.
Moving an old save to a new layout is why it exists. Doing it with <<saveclear,`saveClear`>> and a loop of <<saveset,`saveSet`>> leaves a moment where the save is empty, and a machine switched off in that moment loses everything; this has no such moment, because the file is written once at the end of the frame from the finished table.
*Parameters:*
* `everything` -- table; what the save should now contain. The same values `saveSet` takes.
*Returns:* nothing.
*Since:* 3.00.
*See also:* <<savegetall,saveGetAll>>, <<saveclear,saveClear>>
.Example
[source,lua]
----
-- Version 1 kept one score in two keys; version 2 keeps a table of them.
if saveGet("version", 1) < 2 then
local old = saveGetAll()
saveSetAll({
version = 2,
scores = { { name = old.hiName, score = old.hiScore } },
runs = old.runs,
})
saveFlush()
end
----
=== Scene === Scene
The scene is the 3D layer drawn between the disc video and the overlay. It is off until `sceneEnable(true)`, is sized like the overlay, and clears to `sceneSetBackground` every frame. The `sceneSet*` calls set the whole layer's look: ambient light, sky, fog, exposure and tone curve, bloom, antialiasing and shadow quality; colors are integers from `0` to `255` and distances are world units. `sceneProject` and `sceneUnproject` bridge world space and overlay coordinates, and `sceneGetStats` reports last frame's work. See <<scenes3d,3D Scenes>>. The scene is the 3D layer drawn between the disc video and the overlay. It is off until `sceneEnable(true)`, is sized like the overlay, and clears to `sceneSetBackground` every frame. The `sceneSet*` calls set the whole layer's look: ambient light, sky, fog, exposure and tone curve, bloom, antialiasing and shadow quality; colors are integers from `0` to `255` and distances are world units. `sceneProject` and `sceneUnproject` bridge world space and overlay coordinates, and `sceneGetStats` reports last frame's work. See <<scenes3d,3D Scenes>>.
@ -15708,6 +16082,68 @@ The height applies to the cue on screen straight away and to every cue after it.
srtPosition(70) srtPosition(70)
---- ----
[#stats]
=== Stats
The developer's overlay, in the top left corner: how long the frame took and its
worst over the last two seconds, the frame rate, Lua's memory, how many sprites,
sounds and timers are alive, how many of the scene's nodes were drawn and in how
many batches, how much texture memory it is holding, and what the disc is doing.
It is drawn with SDL's own debug font, so it needs no font file, no asset and no
GUI document, and it appears even on a machine where the 3D device never came
up. It is drawn after <<singescreenshot,`singeScreenshot`>> takes its picture,
so it never lands in a screenshot.
Leave it off in a shipped game. Nothing stops you putting it behind a key in
your own service menu.
==== statsEnable
[source,text]
----
statsEnable(enabled)
----
Shows or hides the overlay. While it is shown the picture is redrawn every frame, so the numbers keep moving even when nothing else on screen does.
*Parameters:*
* `enabled` -- boolean.
*Returns:* nothing.
*Since:* 3.00.
*See also:* <<statsisenabled,statsIsEnabled>>
.Example
[source,lua]
----
-- A developer key, behind a flag so the shipped game has no such key.
function onInputPressed(key)
if DEVELOPER and key == SWITCH_SERVICE then
statsEnable(not statsIsEnabled())
end
end
----
==== statsIsEnabled
[source,text]
----
shown = statsIsEnabled()
----
Whether the overlay is on.
*Parameters:*
* None.
*Returns:* `true` or `false`.
*Since:* 3.00.
[#terrain] [#terrain]
=== Terrain === Terrain
@ -15754,6 +16190,207 @@ function onOverlayUpdate()
end end
---- ----
[#timer]
=== Timer
Something to happen later, without counting frames yourself. Timers run on the
engine's clock, which is the virtual one under `--deterministic`, so a timed
game repeats exactly between runs. They fire with the rest of the frame's
callbacks, on the same thread as everything else -- there is nothing to lock and
nothing that can fire while your script is halfway through something.
A timer that has fallen behind -- a long load, a breakpoint -- fires once when
the game catches up rather than firing the twenty times it missed.
Everything is forgotten when the script reloads, so a timer from the last run
never fires into the next one.
==== timerAfter
[source,text]
----
id = timerAfter(milliseconds, function)
----
Calls the function once, later. The function is given the timer's own id, so one function can serve several timers.
*Parameters:*
* `milliseconds` -- number; zero or less fires on the next frame.
* `function` -- called as `function(id)`.
*Returns:* a number; the timer's id.
*Since:* 3.00.
*See also:* <<timercancel,timerCancel>>, <<timerevery,timerEvery>>
.Example
[source,lua]
----
-- Let the explosion finish before the game over screen.
soundPlay(bang)
timerAfter(1500, function()
showGameOver()
end)
----
==== timerCancel
[source,text]
----
timerCancel(id)
----
Stops a timer. Cancelling one that has already fired is not an error, so nothing has to check first.
*Parameters:*
* `id` -- number; from `timerAfter` or `timerEvery`.
*Returns:* nothing.
*Since:* 3.00.
==== timerEvery
[source,text]
----
id = timerEvery(milliseconds, function)
----
Calls the function over and over until it is cancelled.
*Parameters:*
* `milliseconds` -- number; how long between calls.
* `function` -- called as `function(id)`.
*Returns:* a number; the timer's id.
*Since:* 3.00.
*See also:* <<timercancel,timerCancel>>
.Example
[source,lua]
----
-- Tick the countdown once a second, and stop it when it runs out.
local left = 30
local clock
clock = timerEvery(1000, function()
left = left - 1
if left <= 0 then
timerCancel(clock)
outOfTime()
end
end)
----
==== timerIsActive
[source,text]
----
running = timerIsActive(id)
----
Whether the timer will fire again: `true` for a repeating one until it is cancelled, `false` for a one shot that has already gone off.
*Parameters:*
* `id` -- number.
*Returns:* `true` or `false`.
*Since:* 3.00.
[#tween]
=== Tween
Moving a number from one value to another over time, on a curve. Fades, slides,
camera moves, a health bar draining, a menu sliding in -- all the same thing.
`tweenValue` hands you the number and lets you decide what it means. That is
deliberate: the engine has nodes, sprites, GUI elements, materials and lights,
and a tween that knew about each of them would be five sets of calls to keep in
step. One that hands over a number is none, and it works on things the engine
has never heard of.
The easings are the usual ones. `EASE_LINEAR` is no curve at all;
`EASE_QUAD_*`, `EASE_CUBIC_*`, `EASE_SINE_*` and `EASE_EXPO_*` run from gentle
to sharp; `EASE_BACK_*` overshoots and comes back; `EASE_ELASTIC_*` springs;
`EASE_BOUNCE_*` drops and bounces. Each comes in `_IN` (slow at the start),
`_OUT` (slow at the end) and `_IN_OUT` (both).
==== tweenCancel
[source,text]
----
tweenCancel(id)
----
Stops a tween where it is. Neither function is called again, and the value stays wherever it had reached.
*Parameters:*
* `id` -- number; from `tweenValue`.
*Returns:* nothing.
*Since:* 3.00.
==== tweenIsActive
[source,text]
----
running = tweenIsActive(id)
----
Whether the tween is still running.
*Parameters:*
* `id` -- number.
*Returns:* `true` or `false`.
*Since:* 3.00.
==== tweenValue
[source,text]
----
id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
----
Moves a number from `from` to `to` over `milliseconds`, calling `onUpdate(value, progress)` every frame -- `progress` runs 0 to 1 and ignores the curve -- and `onDone()` once at the end. The last `onUpdate` always lands exactly on `to`, so nothing finishes a pixel short.
*Parameters:*
* `from`, `to` -- numbers.
* `milliseconds` -- number; zero or less arrives on the next frame.
* `easing` -- one of the `EASE_` values.
* `onUpdate` -- called as `function(value, progress)`.
* `onDone` -- optional; called with no arguments when it finishes.
*Returns:* a number; the tween's id.
*Since:* 3.00.
*See also:* <<tweencancel,tweenCancel>>
.Example
[source,lua]
----
-- Slide the title in and fade the overlay up at the same time.
tweenValue(-200, 40, 600, EASE_BACK_OUT, function(x)
titleX = x
end)
tweenValue(0, 255, 600, EASE_SINE_OUT, function(alpha)
setOverlayOpacity(math.floor(alpha))
end, function()
startAttractLoop()
end)
----
[#vehicle] [#vehicle]
=== Vehicle === Vehicle

166
src/collide.c Normal file
View file

@ -0,0 +1,166 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Singe: two dimensional overlap tests. Every one of these is a few lines, and every game was
// writing its own; having them in one place means they agree with each other about what touching
// at the edge means (it counts) and about what a zero sized rectangle does (nothing).
#include <math.h>
#include <stddef.h>
#include "common.h"
#include "collide.h"
#define POLYGON_CORNERS_MIN 3
static double _clamp(double value, double low, double high);
static double _cross(double ax, double ay, double bx, double by);
// ===== Internal helpers =====
static double _clamp(double value, double low, double high) {
if (value < low) {
return low;
}
if (value > high) {
return high;
}
return value;
}
// The z of the cross product of two vectors in the plane: positive when b turns left of a.
static double _cross(double ax, double ay, double bx, double by) {
return (ax * by) - (ay * bx);
}
// ===== Public =====
bool collideCircles(double x1, double y1, double radius1, double x2, double y2, double radius2) {
double dx = x2 - x1;
double dy = y2 - y1;
double reach = radius1 + radius2;
if (reach < 0) {
return false;
}
// Squared, so nothing has to take a square root to answer a yes or no question.
return ((dx * dx) + (dy * dy)) <= (reach * reach);
}
bool collidePointCircle(double pointX, double pointY, double x, double y, double radius) {
double dx = pointX - x;
double dy = pointY - y;
if (radius < 0) {
return false;
}
return ((dx * dx) + (dy * dy)) <= (radius * radius);
}
// The crossing number test: a ray runs right from the point, and a point is inside when it passes
// an odd number of edges. Concave outlines and holes in the winding are handled the same way.
bool collidePointPolygon(double pointX, double pointY, const double *points, int32_t count) {
bool inside = false;
int32_t x = 0;
int32_t y = 0;
if ((points == NULL) || (count < POLYGON_CORNERS_MIN)) {
return false;
}
for (x = 0, y = count - 1; x < count; y = x++) {
double xi = points[x * 2];
double yi = points[x * 2 + 1];
double xj = points[y * 2];
double yj = points[y * 2 + 1];
// The edge straddles the ray's row, and the crossing is to the right of the point.
if (((yi > pointY) != (yj > pointY)) && (pointX < (((xj - xi) * (pointY - yi) / (yj - yi)) + xi))) {
inside = !inside;
}
}
return inside;
}
bool collidePointRect(double pointX, double pointY, double x, double y, double width, double height) {
if ((width <= 0) || (height <= 0)) {
return false;
}
return (pointX >= x) && (pointX <= (x + width)) && (pointY >= y) && (pointY <= (y + height));
}
// The nearest point of the rectangle to the circle's centre is inside the circle.
bool collideRectCircle(double x, double y, double width, double height, double circleX, double circleY, double radius) {
double nearestX = 0;
double nearestY = 0;
if ((width <= 0) || (height <= 0) || (radius < 0)) {
return false;
}
nearestX = _clamp(circleX, x, x + width);
nearestY = _clamp(circleY, y, y + height);
return collidePointCircle(circleX, circleY, nearestX, nearestY, radius);
}
bool collideRects(double x1, double y1, double width1, double height1, double x2, double y2, double width2, double height2) {
if ((width1 <= 0) || (height1 <= 0) || (width2 <= 0) || (height2 <= 0)) {
return false;
}
return (x1 <= (x2 + width2)) && (x2 <= (x1 + width1)) && (y1 <= (y2 + height2)) && (y2 <= (y1 + height1));
}
// Each segment's ends must fall on opposite sides of the other's line. Segments that lie along one
// another answer false: "they cross here" has no single answer then, and a game asking this wants
// a point rather than a line.
bool collideSegments(double ax1, double ay1, double ax2, double ay2, double bx1, double by1, double bx2, double by2) {
double ax = ax2 - ax1;
double ay = ay2 - ay1;
double bx = bx2 - bx1;
double by = by2 - by1;
double d = _cross(ax, ay, bx, by);
double t = 0;
double u = 0;
if (d == 0) {
return false;
}
t = _cross(bx1 - ax1, by1 - ay1, bx, by) / d;
u = _cross(bx1 - ax1, by1 - ay1, ax, ay) / d;
return (t >= 0) && (t <= 1) && (u >= 0) && (u <= 1);
}

53
src/collide.h Normal file
View file

@ -0,0 +1,53 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef COLLIDE_H
#define COLLIDE_H
#include <stdint.h>
#include <stdbool.h>
// Two dimensional overlap tests, in overlay coordinates. Jolt answers this question in three
// dimensions for bodies; nothing answered it for the flat things a game draws, which is where a
// light gun's hitboxes and a 2D game's everything live. Pure functions: no handles, no state.
//
// A rectangle is a corner and a size, the way every drawing call in the engine takes one, and a
// size of zero or less never touches anything.
bool collideCircles(double x1, double y1, double radius1, double x2, double y2, double radius2);
bool collidePointCircle(double pointX, double pointY, double x, double y, double radius);
// A polygon as a flat run of x, y pairs; fewer than three corners never contains anything. The
// winding does not matter and the edge counts as inside.
bool collidePointPolygon(double pointX, double pointY, const double *points, int32_t count);
bool collidePointRect(double pointX, double pointY, double x, double y, double width, double height);
bool collideRectCircle(double x, double y, double width, double height, double circleX, double circleY, double radius);
bool collideRects(double x1, double y1, double width1, double height1, double x2, double y2, double width2, double height2);
// Whether two line segments cross, for a shot along a path or a wall a mover must not pass through.
bool collideSegments(double ax1, double ay1, double ax2, double ay2, double bx1, double by1, double bx2, double by2);
#endif // COLLIDE_H

View file

@ -73,6 +73,10 @@
// json // json
#include "generated/json_lua.h" #include "generated/json_lua.h"
#include "generated/middleclass_lua.h"
#include "generated/lume_lua.h"
#include "generated/inspect_lua.h"
#include "generated/bump_lua.h"
// Copas // Copas
#include "generated/copas_lua.h" #include "generated/copas_lua.h"

128
src/persist.c Normal file
View file

@ -0,0 +1,128 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Singe: the save file. Small, and written rarely, so it is read and written whole rather than
// kept open; what matters is that a write either lands or leaves the old one alone.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"
#include "persist.h"
#include "util.h"
#define SAVE_NAME "save.json"
#define SAVE_TEMPORARY "save.json.new"
#define SAVE_MAX (16 * 1024 * 1024) // A save larger than this is a mistake, not a save
static char *_path = NULL;
static char *_temporary = NULL;
void persistClose(void) {
free(_path);
free(_temporary);
_path = NULL;
_temporary = NULL;
}
void persistOpen(const char *dataPath) {
persistClose();
if ((dataPath == NULL) || (dataPath[0] == 0)) {
return;
}
_path = utilCreateString("%s%c%s", dataPath, utilGetPathSeparator(), SAVE_NAME);
_temporary = utilCreateString("%s%c%s", dataPath, utilGetPathSeparator(), SAVE_TEMPORARY);
}
const char *persistPath(void) {
return _path;
}
char *persistRead(void) {
FILE *file = NULL;
char *text = NULL;
long size = 0;
size_t got = 0;
if (_path == NULL) {
return NULL;
}
file = fopen(_path, "rb");
if (file == NULL) {
return NULL;
}
if ((fseek(file, 0, SEEK_END) != 0) || ((size = ftell(file)) < 0) || (size > SAVE_MAX) || (fseek(file, 0, SEEK_SET) != 0)) {
fclose(file);
return NULL;
}
text = (char *)malloc((size_t)size + 1);
if (text == NULL) {
fclose(file);
utilDie("Unable to allocate the save file.");
}
got = fread(text, 1, (size_t)size, file);
fclose(file);
text[got] = 0;
return text;
}
bool persistWrite(const char *text, size_t length) {
FILE *file = NULL;
if ((_path == NULL) || (text == NULL)) {
return false;
}
file = fopen(_temporary, "wb");
if (file == NULL) {
return false;
}
if ((length > 0) && (fwrite(text, 1, length, file) != length)) {
fclose(file);
remove(_temporary);
return false;
}
// Flushed and closed before the rename: a rename over a file whose bytes are still in a buffer
// is the very thing this is trying to avoid.
if (fflush(file) != 0) {
fclose(file);
remove(_temporary);
return false;
}
fclose(file);
// Windows will not rename onto an existing file, so the old one goes first. The window between
// the two is the one risk left, and it is smaller than writing in place.
remove(_path);
if (rename(_temporary, _path) != 0) {
remove(_temporary);
return false;
}
return true;
}

55
src/persist.h Normal file
View file

@ -0,0 +1,55 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef PERSIST_H
#define PERSIST_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
// Keeping something between runs. This half owns the file: where it lives, and writing it in a
// way that survives losing power in the middle, which a cabinet does. What goes in it is a Lua
// table, so turning one into text and back belongs with the binding, where a Lua state exists.
// The file the game saves into, under the game's own data directory. NULL until persistOpen.
const char *persistPath(void);
// Names the file for this game. Reading and writing before this do nothing.
void persistOpen(const char *dataPath);
// What was saved, or NULL when there is nothing yet. free() it.
char *persistRead(void);
// Writes the text, replacing whatever was there. A temporary file beside it is written and closed
// first and then renamed over the old one, so a game that loses power keeps its previous save
// rather than half of a new one. False when it could not be written, which is worth telling
// somebody about: a full disk should not be silent.
bool persistWrite(const char *text, size_t length);
void persistClose(void);
#endif // PERSIST_H

280
src/scheduler.c Normal file
View file

@ -0,0 +1,280 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Singe: timers and tweens. See scheduler.h for why they share a file.
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"
#include "scheduler.h"
#include "util.h"
#define SLOTS_FIRST 16 // Grown by doubling from here
#define BACK_OVERSHOOT 1.70158 // How far past the target the "back" easings go, the usual constant
#define ELASTIC_PERIOD 0.3
#define ELASTIC_AMPLITUDE 0.1
#define BOUNCE_SCALE 7.5625 // The bounce curve's four arcs, as everyone writes them
#define BOUNCE_SPLIT 2.75
#define HALF 0.5
typedef struct {
SchedulerKindE kind;
int64_t interval; // Timer: how often. Tween: how long
uint64_t due; // Timer: when it next fires. Tween: when it ends
uint64_t started; // Tween only
double from;
double to;
SchedulerEaseE easing;
bool repeating;
bool used;
} SlotT;
static SlotT *_slots = NULL;
static int32_t _count = 0;
static int32_t _capacity = 0;
static int32_t _allocate(void);
static double _ease(SchedulerEaseE easing, double t);
static double _easeBounceOut(double t);
// ===== Internal helpers =====
// The first free slot, growing the table when there is none. Handles are indexes, so a slot never
// moves while anything might still name it.
static int32_t _allocate(void) {
SlotT *grown = NULL;
int32_t want = 0;
int32_t x = 0;
for (x = 0; x < _count; x++) {
if (!_slots[x].used) {
memset(&_slots[x], 0, sizeof(_slots[x]));
_slots[x].used = true;
return x;
}
}
if (_count == _capacity) {
want = (_capacity == 0) ? SLOTS_FIRST : (_capacity * 2);
grown = (SlotT *)realloc(_slots, sizeof(SlotT) * (size_t)want);
if (grown == NULL) {
utilDie("Unable to grow the timer table.");
}
_slots = grown;
_capacity = want;
}
memset(&_slots[_count], 0, sizeof(_slots[_count]));
_slots[_count].used = true;
return _count++;
}
// A progress between zero and one, curved. Every one of these is the textbook form; they are here
// rather than in a script because every game wants them and none should have to type them.
static double _ease(SchedulerEaseE easing, double t) {
if (t <= 0) {
return 0;
}
if (t >= 1) {
return 1;
}
switch (easing) {
case SCHEDULER_EASE_QUAD_IN: return t * t;
case SCHEDULER_EASE_QUAD_OUT: return 1 - ((1 - t) * (1 - t));
case SCHEDULER_EASE_QUAD_IN_OUT: return (t < HALF) ? (2 * t * t) : (1 - (pow((-2 * t) + 2, 2) / 2));
case SCHEDULER_EASE_CUBIC_IN: return t * t * t;
case SCHEDULER_EASE_CUBIC_OUT: return 1 - pow(1 - t, 3);
case SCHEDULER_EASE_CUBIC_IN_OUT: return (t < HALF) ? (4 * t * t * t) : (1 - (pow((-2 * t) + 2, 3) / 2));
case SCHEDULER_EASE_SINE_IN: return 1 - cos((t * M_PI) / 2);
case SCHEDULER_EASE_SINE_OUT: return sin((t * M_PI) / 2);
case SCHEDULER_EASE_SINE_IN_OUT: return -(cos(M_PI * t) - 1) / 2;
case SCHEDULER_EASE_EXPO_IN: return pow(2, (10 * t) - 10);
case SCHEDULER_EASE_EXPO_OUT: return 1 - pow(2, -10 * t);
case SCHEDULER_EASE_EXPO_IN_OUT: return (t < HALF) ? (pow(2, (20 * t) - 10) / 2) : ((2 - pow(2, (-20 * t) + 10)) / 2);
case SCHEDULER_EASE_BACK_IN: return ((BACK_OVERSHOOT + 1) * t * t * t) - (BACK_OVERSHOOT * t * t);
case SCHEDULER_EASE_BACK_OUT: return 1 + ((BACK_OVERSHOOT + 1) * pow(t - 1, 3)) + (BACK_OVERSHOOT * pow(t - 1, 2));
case SCHEDULER_EASE_BACK_IN_OUT: return (t < HALF)
? ((pow(2 * t, 2) * (((BACK_OVERSHOOT * 1.525) + 1) * 2 * t - (BACK_OVERSHOOT * 1.525))) / 2)
: (((pow((2 * t) - 2, 2) * ((((BACK_OVERSHOOT * 1.525) + 1) * ((t * 2) - 2)) + (BACK_OVERSHOOT * 1.525))) + 2) / 2);
case SCHEDULER_EASE_ELASTIC_IN: return -pow(2, (10 * t) - 10) * sin((((t * 10) - 10.75) * (2 * M_PI)) / (ELASTIC_PERIOD * 10));
case SCHEDULER_EASE_ELASTIC_OUT: return (pow(2, -10 * t) * sin((((t * 10) - 0.75) * (2 * M_PI)) / (ELASTIC_PERIOD * 10))) + 1;
case SCHEDULER_EASE_ELASTIC_IN_OUT: return (t < HALF)
? (-(pow(2, (20 * t) - 10) * sin((((20 * t) - 11.125) * (2 * M_PI)) / 4.5)) / 2)
: (((pow(2, (-20 * t) + 10) * sin((((20 * t) - 11.125) * (2 * M_PI)) / 4.5)) / 2) + 1);
case SCHEDULER_EASE_BOUNCE_IN: return 1 - _easeBounceOut(1 - t);
case SCHEDULER_EASE_BOUNCE_OUT: return _easeBounceOut(t);
case SCHEDULER_EASE_BOUNCE_IN_OUT: return (t < HALF)
? ((1 - _easeBounceOut(1 - (2 * t))) / 2)
: ((1 + _easeBounceOut((2 * t) - 1)) / 2);
default: return t;
}
}
// Four arcs, each smaller and lower than the last.
static double _easeBounceOut(double t) {
if (t < (1 / BOUNCE_SPLIT)) {
return BOUNCE_SCALE * t * t;
}
if (t < (2 / BOUNCE_SPLIT)) {
t -= 1.5 / BOUNCE_SPLIT;
return (BOUNCE_SCALE * t * t) + 0.75;
}
if (t < (2.5 / BOUNCE_SPLIT)) {
t -= 2.25 / BOUNCE_SPLIT;
return (BOUNCE_SCALE * t * t) + 0.9375;
}
t -= 2.625 / BOUNCE_SPLIT;
return (BOUNCE_SCALE * t * t) + 0.984375;
}
// ===== Public =====
void schedulerCancel(int32_t handle) {
if ((handle < 0) || (handle >= _count)) {
return;
}
_slots[handle].used = false;
}
int32_t schedulerCount(void) {
int32_t count = 0;
int32_t x = 0;
for (x = 0; x < _count; x++) {
if (_slots[x].used) {
count++;
}
}
return count;
}
bool schedulerIsActive(int32_t handle) {
if ((handle < 0) || (handle >= _count)) {
return false;
}
return _slots[handle].used;
}
void schedulerReset(void) {
free(_slots);
_slots = NULL;
_count = 0;
_capacity = 0;
}
int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now) {
int32_t handle = _allocate();
SlotT *slot = &_slots[handle];
if (milliseconds < 0) {
milliseconds = 0;
}
slot->kind = SCHEDULER_TIMER;
slot->interval = milliseconds;
slot->due = now + (uint64_t)milliseconds;
slot->repeating = repeating;
return handle;
}
int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEaseE easing, uint64_t now) {
int32_t handle = _allocate();
SlotT *slot = &_slots[handle];
if (milliseconds < 0) {
milliseconds = 0;
}
if ((easing < 0) || (easing >= SCHEDULER_EASE_COUNT)) {
easing = SCHEDULER_EASE_LINEAR;
}
slot->kind = SCHEDULER_TWEEN;
slot->interval = milliseconds;
slot->started = now;
slot->due = now + (uint64_t)milliseconds;
slot->from = from;
slot->to = to;
slot->easing = easing;
return handle;
}
void schedulerUpdate(uint64_t now, SchedulerFireT fire, void *context) {
double progress = 0;
double value = 0;
bool finished = false;
int32_t x = 0;
// By index rather than over a copy: a script's callback may start or cancel anything, and a
// slot that is freed while this runs is simply skipped on the way past.
for (x = 0; x < _count; x++) {
SlotT *slot = &_slots[x];
if (!slot->used) {
continue;
}
if (slot->kind == SCHEDULER_TIMER) {
if (now < slot->due) {
continue;
}
if (slot->repeating) {
// One interval on from now rather than from when it was due: a timer that fell
// behind during a long load catches up on the next tick, not with a burst.
slot->due = now + (uint64_t)((slot->interval > 0) ? slot->interval : 1);
} else {
slot->used = false;
}
if (fire != NULL) {
fire(context, x, SCHEDULER_TIMER, 0, 0, !slot->repeating);
}
continue;
}
progress = (slot->interval > 0) ? ((double)(now - slot->started) / (double)slot->interval) : 1.0;
finished = (progress >= 1.0);
if (finished) {
progress = 1.0;
}
value = slot->from + ((slot->to - slot->from) * _ease(slot->easing, progress));
if (finished) {
slot->used = false;
}
if (fire != NULL) {
fire(context, x, SCHEDULER_TWEEN, value, progress, finished);
}
}
}

95
src/scheduler.h Normal file
View file

@ -0,0 +1,95 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef SCHEDULER_H
#define SCHEDULER_H
#include <stdint.h>
#include <stdbool.h>
// Timers and tweens. They are the same machine: something a script asked for, that wants the
// frame clock, and that has a Lua function to call later. Keeping them together means one set of
// handles, one reset when the script reloads, and one place that decides what "later" means.
//
// Nothing here knows about Lua. A script's function is held elsewhere, under the handle this
// hands back, and the handle is what comes back again when the thing is due -- the way midiIoPoll
// hands a message to whoever asked for it. Time is whatever the caller passes in, which is the
// engine's clock, virtual under --deterministic, so a timed game repeats exactly.
#define SCHEDULER_NO_HANDLE -1
typedef enum {
SCHEDULER_TIMER, // Fires once, or over and over at a fixed interval
SCHEDULER_TWEEN // Fires every frame with a value between two, then once more at the end
} SchedulerKindE;
// What a caller is told when something is due. "value" and "progress" mean nothing for a timer.
typedef void (*SchedulerFireT)(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished);
// The easings a tween may use. Everything after SCHEDULER_EASE_LINEAR is the same curve in three
// shapes: applied to the start, to the end, or to both halves.
typedef enum {
SCHEDULER_EASE_LINEAR,
SCHEDULER_EASE_QUAD_IN, SCHEDULER_EASE_QUAD_OUT, SCHEDULER_EASE_QUAD_IN_OUT,
SCHEDULER_EASE_CUBIC_IN, SCHEDULER_EASE_CUBIC_OUT, SCHEDULER_EASE_CUBIC_IN_OUT,
SCHEDULER_EASE_SINE_IN, SCHEDULER_EASE_SINE_OUT, SCHEDULER_EASE_SINE_IN_OUT,
SCHEDULER_EASE_EXPO_IN, SCHEDULER_EASE_EXPO_OUT, SCHEDULER_EASE_EXPO_IN_OUT,
SCHEDULER_EASE_BACK_IN, SCHEDULER_EASE_BACK_OUT, SCHEDULER_EASE_BACK_IN_OUT,
SCHEDULER_EASE_ELASTIC_IN, SCHEDULER_EASE_ELASTIC_OUT, SCHEDULER_EASE_ELASTIC_IN_OUT,
SCHEDULER_EASE_BOUNCE_IN, SCHEDULER_EASE_BOUNCE_OUT, SCHEDULER_EASE_BOUNCE_IN_OUT,
SCHEDULER_EASE_COUNT
} SchedulerEaseE;
// Stops one. A handle that names nothing is not an error: a script cancelling a timer that has
// already fired should not have to check first.
void schedulerCancel(int32_t handle);
// How many timers and tweens are running, for the stats overlay.
int32_t schedulerCount(void);
// Whether a handle still names something that will fire again.
bool schedulerIsActive(int32_t handle);
// Forgets everything, for a script reload. Nothing is fired on the way out.
void schedulerReset(void);
// A timer. An interval of zero or less fires on the next frame. Repeating timers keep their
// handle until they are cancelled.
int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now);
// A tween. A duration of zero or less arrives at "to" on the next frame and finishes there.
int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEaseE easing, uint64_t now);
// Moves everything to "now" and reports what is due through the callback. A repeating timer that
// fell several intervals behind -- a long load, a breakpoint -- fires once rather than catching up,
// because a game wants the next tick, not twenty of them at once.
void schedulerUpdate(uint64_t now, SchedulerFireT fire, void *context);
#endif // SCHEDULER_H

View file

@ -58,13 +58,17 @@ int luaopen_lpeg(lua_State *L);
// There is no header for ssl.config binding. Make our own. // There is no header for ssl.config binding. Make our own.
LSEC_API int luaopen_ssl_config(lua_State *L); LSEC_API int luaopen_ssl_config(lua_State *L);
#include "collide.h"
#include "decode.h" #include "decode.h"
#include "main.h" #include "main.h"
#include "midiIo.h" #include "midiIo.h"
#include "util.h" #include "util.h"
#include "frameFile.h" #include "frameFile.h"
#include "vfs.h" #include "vfs.h"
#include "persist.h"
#include "scene.h" #include "scene.h"
#include "scheduler.h"
#include "stats.h"
#include "hdr.h" #include "hdr.h"
#include "ktx2.h" #include "ktx2.h"
#include "nav.h" #include "nav.h"
@ -169,6 +173,13 @@ SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE >= SDL_SCANCODE_RESER
#define MIDI_BEND_MAX 16383 // Two seven bit halves, 8192 being no bend at all #define MIDI_BEND_MAX 16383 // Two seven bit halves, 8192 being no bend at all
#define MIDI_BYTE_MAX 255 #define MIDI_BYTE_MAX 255
#define MIDI_MESSAGE_BYTES 3 // The longest message the numeric form of midiSend takes #define MIDI_MESSAGE_BYTES 3 // The longest message the numeric form of midiSend takes
#define SAVE_TABLE "singeSave" // The save table, in the Lua registry
#define SAVE_CALLBACKS "singeSchedule" // Timer and tween functions, by handle
#define SAVE_DEPTH_MAX 32 // Nested tables a save may hold
#define SAVE_POLYGON_MIN 3 // Corners a hitbox needs before it holds anything
#define SAVE_POLYGON_MAX 256
#define SCHEDULE_UPDATE 1 // Slots of a schedule entry's callback table
#define SCHEDULE_DONE 2
#define SPRITE_VECTOR_MAX 8192 // Pixels a side an SVG may be asked for #define SPRITE_VECTOR_MAX 8192 // Pixels a side an SVG may be asked for
#define SUBTITLE_RML "Singe/subtitle.rml" #define SUBTITLE_RML "Singe/subtitle.rml"
#define SUBTITLE_SLOT "slot" // The element the engine writes a cue or a banner into #define SUBTITLE_SLOT "slot" // The element the engine writes a cue or a banner into
@ -596,6 +607,8 @@ typedef struct GlobalS {
int32_t videoRotate; // Live --rotate degrees; vldpSetRotate moves it int32_t videoRotate; // Live --rotate degrees; vldpSetRotate moves it
SDL_Texture *rotateTexture; // The frame, drawn unrotated, when videoRotate is not zero SDL_Texture *rotateTexture; // The frame, drawn unrotated, when videoRotate is not zero
uint64_t videoScaleClock; // vldpSetScale is throttled to one change per VIDEO_SCALE_THROTTLE_MS uint64_t videoScaleClock; // vldpSetScale is throttled to one change per VIDEO_SCALE_THROTTLE_MS
bool saveDirty; // The save table changed this frame and wants writing
double statsFrameMs; // How long the last frame took, for the overlay
bool discMonochrome; // vldpSetMonochrome: the disc picture is shown in luma only bool discMonochrome; // vldpSetMonochrome: the disc picture is shown in luma only
bool discBlend; // vldpSetBlend: the disc picture is smoothed down its rows bool discBlend; // vldpSetBlend: the disc picture is smoothed down its rows
bool discLumaOn; // vldpSetLuma, and the level it was given bool discLumaOn; // vldpSetLuma, and the level it was given
@ -795,10 +808,20 @@ static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMa
static float _mixerGain(int32_t volume, int32_t maximum); static float _mixerGain(int32_t volume, int32_t maximum);
static int32_t _mouseCode(int32_t device, int32_t button); static int32_t _mouseCode(int32_t device, int32_t button);
static void _musicDestroy(MusicT *music); static void _musicDestroy(MusicT *music);
static bool _saveCjson(lua_State *L, const char *name);
static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth);
static void _saveNormalise(lua_State *L, int32_t depth);
static void _saveCheckValue(lua_State *L, const char *method, int32_t index);
static void _saveTable(lua_State *L);
static void _saveWrite(void);
static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished);
static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex);
static void _scheduleReset(lua_State *L);
static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index); static uint8_t _midiChannel(lua_State *L, const char *method, int32_t index);
static uint8_t _midiData(lua_State *L, const char *method, int32_t index); static uint8_t _midiData(lua_State *L, const char *method, int32_t index);
static void _midiReceived(void *context, const uint8_t *bytes, size_t size); static void _midiReceived(void *context, const uint8_t *bytes, size_t size);
static void _navCallbacks(void); static void _navCallbacks(void);
static void _statsDraw(void);
static void _noteInput(void); static void _noteInput(void);
static void _overlayApplyOpacity(void); static void _overlayApplyOpacity(void);
static uint32_t _overlayColor(const SDL_Color *color); static uint32_t _overlayColor(const SDL_Color *color);
@ -919,6 +942,13 @@ static int32_t apiBodySetWater(lua_State *L);
static int32_t apiCameraSet(lua_State *L); static int32_t apiCameraSet(lua_State *L);
static int32_t apiCameraSetOrthographic(lua_State *L); static int32_t apiCameraSetOrthographic(lua_State *L);
static int32_t apiCameraSetPerspective(lua_State *L); static int32_t apiCameraSetPerspective(lua_State *L);
static int32_t apiCollideCircles(lua_State *L);
static int32_t apiCollidePointCircle(lua_State *L);
static int32_t apiCollidePointPolygon(lua_State *L);
static int32_t apiCollidePointRect(lua_State *L);
static int32_t apiCollideRectCircle(lua_State *L);
static int32_t apiCollideRects(lua_State *L);
static int32_t apiCollideSegments(lua_State *L);
static int32_t apiColorBackground(lua_State *L); static int32_t apiColorBackground(lua_State *L);
static int32_t apiColorForeground(lua_State *L); static int32_t apiColorForeground(lua_State *L);
static int32_t apiControllerDoRumble(lua_State *L); static int32_t apiControllerDoRumble(lua_State *L);
@ -1193,6 +1223,13 @@ static int32_t apiRagdollSetJoint(lua_State *L);
static int32_t apiRagdollSetStrength(lua_State *L); static int32_t apiRagdollSetStrength(lua_State *L);
static int32_t apiRatioGetX(lua_State *L); static int32_t apiRatioGetX(lua_State *L);
static int32_t apiRatioGetY(lua_State *L); static int32_t apiRatioGetY(lua_State *L);
static int32_t apiSaveClear(lua_State *L);
static int32_t apiSaveDelete(lua_State *L);
static int32_t apiSaveFlush(lua_State *L);
static int32_t apiSaveGet(lua_State *L);
static int32_t apiSaveGetAll(lua_State *L);
static int32_t apiSaveSet(lua_State *L);
static int32_t apiSaveSetAll(lua_State *L);
static int32_t apiSceneEnable(lua_State *L); static int32_t apiSceneEnable(lua_State *L);
static int32_t apiSceneGetSize(lua_State *L); static int32_t apiSceneGetSize(lua_State *L);
static int32_t apiSceneGetStats(lua_State *L); static int32_t apiSceneGetStats(lua_State *L);
@ -1301,7 +1338,16 @@ static int32_t apiSrtEnable(lua_State *L);
static int32_t apiSrtLoad(lua_State *L); static int32_t apiSrtLoad(lua_State *L);
static int32_t apiSrtLoadTrack(lua_State *L); static int32_t apiSrtLoadTrack(lua_State *L);
static int32_t apiSrtPosition(lua_State *L); static int32_t apiSrtPosition(lua_State *L);
static int32_t apiStatsEnable(lua_State *L);
static int32_t apiStatsIsEnabled(lua_State *L);
static int32_t apiTerrainGetHeight(lua_State *L); static int32_t apiTerrainGetHeight(lua_State *L);
static int32_t apiTimerAfter(lua_State *L);
static int32_t apiTimerCancel(lua_State *L);
static int32_t apiTimerEvery(lua_State *L);
static int32_t apiTimerIsActive(lua_State *L);
static int32_t apiTweenCancel(lua_State *L);
static int32_t apiTweenIsActive(lua_State *L);
static int32_t apiTweenValue(lua_State *L);
static int32_t apiVehicleAddWheel(lua_State *L); static int32_t apiVehicleAddWheel(lua_State *L);
static int32_t apiVehicleDelete(lua_State *L); static int32_t apiVehicleDelete(lua_State *L);
static int32_t apiVehicleDrive(lua_State *L); static int32_t apiVehicleDrive(lua_State *L);
@ -1410,6 +1456,14 @@ static const LuaModuleT _luaModules[] = {
MODL("timerwheel", timerwheel_lua), MODL("timerwheel", timerwheel_lua),
// json // json
MODL("json", json_lua), MODL("json", json_lua),
// middleclass, for games that want classes to build their entities out of
MODL("middleclass", middleclass_lua),
// lume, the small things every game needs: lerp, clamp, round, shuffle, split, serialise
MODL("lume", lume_lua),
// inspect, for printing a table in a shape a person can read
MODL("inspect", inspect_lua),
// bump, which answers what collide* deliberately does not: where a mover ends up
MODL("bump", bump_lua),
// Copas // Copas
MODL("copas", copas_lua), MODL("copas", copas_lua),
MODL("copas.ftp", copas_ftp_lua), MODL("copas.ftp", copas_ftp_lua),
@ -4338,6 +4392,41 @@ static int32_t _mouseCode(int32_t device, int32_t button) {
} }
// Everything the developer's overlay shows, gathered from wherever it lives. Drawn after the
// screenshot is taken, so a reference shot never has the overlay in it.
static void _statsDraw(void) {
StatsT stats;
int64_t textureBytes = 0;
SpriteT *sprite = NULL;
SpriteT *spriteNext = NULL;
SoundT *sound = NULL;
SoundT *soundNext = NULL;
if (!statsIsEnabled()) {
return;
}
memset(&stats, 0, sizeof(stats));
stats.frameMilliseconds = _global.statsFrameMs;
stats.worstMilliseconds = statsWorstMilliseconds();
stats.framesPerSecond = statsFramesPerSecond();
stats.luaKilobytes = (_global.luaContext != NULL) ? (double)lua_gc(_global.luaContext, LUA_GCCOUNT) : 0;
stats.timers = schedulerCount();
HASH_ITER(hh, _global.spriteList, sprite, spriteNext) {
stats.sprites++;
}
HASH_ITER(hh, _global.soundList, sound, soundNext) {
stats.sounds++;
}
sceneGetStats(&stats.nodesTotal, &stats.nodesDrawn, &stats.batches, &textureBytes);
stats.textureKilobytes = textureBytes / BYTES_PER_KIB;
if (_global.videoHandle >= 0) {
stats.discFrame = videoGetFrame(_global.videoHandle);
stats.discState = _global.discStopped ? "stopped" : (videoIsPlaying(_global.videoHandle) ? "playing" : "paused");
}
statsDraw(_global.renderer, &stats);
}
// onNavArrived(agent) for every agent that reached its target this frame. // onNavArrived(agent) for every agent that reached its target this frame.
static void _navCallbacks(void) { static void _navCallbacks(void) {
int32_t agents[NAV_ARRIVAL_QUEUE]; int32_t agents[NAV_ARRIVAL_QUEUE];
@ -4662,6 +4751,52 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, _inputNames[x].switchName); lua_setglobal(L, _inputNames[x].switchName);
} }
// The easings tweenValue takes.
lua_pushinteger(L, SCHEDULER_EASE_LINEAR);
lua_setglobal(L, "EASE_LINEAR");
lua_pushinteger(L, SCHEDULER_EASE_QUAD_IN);
lua_setglobal(L, "EASE_QUAD_IN");
lua_pushinteger(L, SCHEDULER_EASE_QUAD_OUT);
lua_setglobal(L, "EASE_QUAD_OUT");
lua_pushinteger(L, SCHEDULER_EASE_QUAD_IN_OUT);
lua_setglobal(L, "EASE_QUAD_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_CUBIC_IN);
lua_setglobal(L, "EASE_CUBIC_IN");
lua_pushinteger(L, SCHEDULER_EASE_CUBIC_OUT);
lua_setglobal(L, "EASE_CUBIC_OUT");
lua_pushinteger(L, SCHEDULER_EASE_CUBIC_IN_OUT);
lua_setglobal(L, "EASE_CUBIC_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_SINE_IN);
lua_setglobal(L, "EASE_SINE_IN");
lua_pushinteger(L, SCHEDULER_EASE_SINE_OUT);
lua_setglobal(L, "EASE_SINE_OUT");
lua_pushinteger(L, SCHEDULER_EASE_SINE_IN_OUT);
lua_setglobal(L, "EASE_SINE_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_EXPO_IN);
lua_setglobal(L, "EASE_EXPO_IN");
lua_pushinteger(L, SCHEDULER_EASE_EXPO_OUT);
lua_setglobal(L, "EASE_EXPO_OUT");
lua_pushinteger(L, SCHEDULER_EASE_EXPO_IN_OUT);
lua_setglobal(L, "EASE_EXPO_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BACK_IN);
lua_setglobal(L, "EASE_BACK_IN");
lua_pushinteger(L, SCHEDULER_EASE_BACK_OUT);
lua_setglobal(L, "EASE_BACK_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BACK_IN_OUT);
lua_setglobal(L, "EASE_BACK_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_ELASTIC_IN);
lua_setglobal(L, "EASE_ELASTIC_IN");
lua_pushinteger(L, SCHEDULER_EASE_ELASTIC_OUT);
lua_setglobal(L, "EASE_ELASTIC_OUT");
lua_pushinteger(L, SCHEDULER_EASE_ELASTIC_IN_OUT);
lua_setglobal(L, "EASE_ELASTIC_IN_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BOUNCE_IN);
lua_setglobal(L, "EASE_BOUNCE_IN");
lua_pushinteger(L, SCHEDULER_EASE_BOUNCE_OUT);
lua_setglobal(L, "EASE_BOUNCE_OUT");
lua_pushinteger(L, SCHEDULER_EASE_BOUNCE_IN_OUT);
lua_setglobal(L, "EASE_BOUNCE_IN_OUT");
lua_pushinteger(L, FONT_QUALITY_SOLID); lua_pushinteger(L, FONT_QUALITY_SOLID);
lua_setglobal(L, "FONT_QUALITY_SOLID"); lua_setglobal(L, "FONT_QUALITY_SOLID");
lua_pushinteger(L, FONT_QUALITY_SHADED); lua_pushinteger(L, FONT_QUALITY_SHADED);
@ -4952,6 +5087,13 @@ static void _registerApi(lua_State *L) {
lua_register(L, "cameraSetOrthographic", apiCameraSetOrthographic); // 3.00 lua_register(L, "cameraSetOrthographic", apiCameraSetOrthographic); // 3.00
lua_register(L, "cameraSetPerspective", apiCameraSetPerspective); // 3.00 lua_register(L, "cameraSetPerspective", apiCameraSetPerspective); // 3.00
lua_register(L, "colorBackground", apiColorBackground); // 1.xx lua_register(L, "colorBackground", apiColorBackground); // 1.xx
lua_register(L, "collideCircles", apiCollideCircles); // 3.00
lua_register(L, "collidePointCircle", apiCollidePointCircle); // 3.00
lua_register(L, "collidePointPolygon", apiCollidePointPolygon); // 3.00
lua_register(L, "collidePointRect", apiCollidePointRect); // 3.00
lua_register(L, "collideRectCircle", apiCollideRectCircle); // 3.00
lua_register(L, "collideRects", apiCollideRects); // 3.00
lua_register(L, "collideSegments", apiCollideSegments); // 3.00
lua_register(L, "colorForeground", apiColorForeground); // 1.xx lua_register(L, "colorForeground", apiColorForeground); // 1.xx
lua_register(L, "controllerDoRumble", apiControllerDoRumble); // Hypseus lua_register(L, "controllerDoRumble", apiControllerDoRumble); // Hypseus
lua_register(L, "controllerGetAxis", apiControllerGetAxis); // 2.00 lua_register(L, "controllerGetAxis", apiControllerGetAxis); // 2.00
@ -5232,6 +5374,13 @@ static void _registerApi(lua_State *L) {
lua_register(L, "ragdollSetStrength", apiRagdollSetStrength); // 3.00 lua_register(L, "ragdollSetStrength", apiRagdollSetStrength); // 3.00
lua_register(L, "ratioGetX", apiRatioGetX); // Hypseus lua_register(L, "ratioGetX", apiRatioGetX); // Hypseus
lua_register(L, "ratioGetY", apiRatioGetY); // Hypseus lua_register(L, "ratioGetY", apiRatioGetY); // Hypseus
lua_register(L, "saveClear", apiSaveClear); // 3.00
lua_register(L, "saveDelete", apiSaveDelete); // 3.00
lua_register(L, "saveFlush", apiSaveFlush); // 3.00
lua_register(L, "saveGet", apiSaveGet); // 3.00
lua_register(L, "saveGetAll", apiSaveGetAll); // 3.00
lua_register(L, "saveSet", apiSaveSet); // 3.00
lua_register(L, "saveSetAll", apiSaveSetAll); // 3.00
lua_register(L, "sceneEnable", apiSceneEnable); // 3.00 lua_register(L, "sceneEnable", apiSceneEnable); // 3.00
lua_register(L, "sceneGetSize", apiSceneGetSize); // 3.00 lua_register(L, "sceneGetSize", apiSceneGetSize); // 3.00
lua_register(L, "sceneGetStats", apiSceneGetStats); // 3.00 lua_register(L, "sceneGetStats", apiSceneGetStats); // 3.00
@ -5345,7 +5494,16 @@ static void _registerApi(lua_State *L) {
lua_register(L, "srtLoadTrack", apiSrtLoadTrack); // 3.00 lua_register(L, "srtLoadTrack", apiSrtLoadTrack); // 3.00
lua_register(L, "srtPosition", apiSrtPosition); // Hypseus lua_register(L, "srtPosition", apiSrtPosition); // Hypseus
lua_register(L, "statsEnable", apiStatsEnable); // 3.00
lua_register(L, "statsIsEnabled", apiStatsIsEnabled); // 3.00
lua_register(L, "terrainGetHeight", apiTerrainGetHeight); // 3.00 lua_register(L, "terrainGetHeight", apiTerrainGetHeight); // 3.00
lua_register(L, "timerAfter", apiTimerAfter); // 3.00
lua_register(L, "timerCancel", apiTimerCancel); // 3.00
lua_register(L, "timerEvery", apiTimerEvery); // 3.00
lua_register(L, "timerIsActive", apiTimerIsActive); // 3.00
lua_register(L, "tweenCancel", apiTweenCancel); // 3.00
lua_register(L, "tweenIsActive", apiTweenIsActive); // 3.00
lua_register(L, "tweenValue", apiTweenValue); // 3.00
lua_register(L, "vehicleAddWheel", apiVehicleAddWheel); // 3.00 lua_register(L, "vehicleAddWheel", apiVehicleAddWheel); // 3.00
lua_register(L, "vehicleDelete", apiVehicleDelete); // 3.00 lua_register(L, "vehicleDelete", apiVehicleDelete); // 3.00
lua_register(L, "vehicleDrive", apiVehicleDrive); // 3.00 lua_register(L, "vehicleDrive", apiVehicleDrive); // 3.00
@ -5424,6 +5582,9 @@ static void _releaseAxis(int32_t axisIndex) {
static void _reloadScript(void) { static void _reloadScript(void) {
_progTrace("Reloading %s", _global.conf->scriptFile); _progTrace("Reloading %s", _global.conf->scriptFile);
_global.reloadRequested = false; _global.reloadRequested = false;
// The save table lives in the Lua state that is about to be closed, so anything set since the
// last end of frame is written now rather than lost.
_saveWrite();
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0); MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
MIX_StopTag(videoGetMixer(), MUSIC_TAG, 0); MIX_StopTag(videoGetMixer(), MUSIC_TAG, 0);
guiDetachLua(); guiDetachLua();
@ -5490,6 +5651,7 @@ static void _resetScriptState(void) {
_global.guiHover = GUI_NO_HANDLE; _global.guiHover = GUI_NO_HANDLE;
_scorePanelReset(); _scorePanelReset();
_subtitleReset(); _subtitleReset();
_scheduleReset(_global.luaContext);
if (_global.guiTextActive) { if (_global.guiTextActive) {
SDL_StopTextInput(_global.window); SDL_StopTextInput(_global.window);
_global.guiTextActive = false; _global.guiTextActive = false;
@ -6233,6 +6395,256 @@ static void _midiReceived(void *context, const uint8_t *bytes, size_t size) {
} }
// Pushes one of cjson's functions. The save file is JSON because a person should be able to read
// and fix one, and cjson is already bundled and already knows every corner of the format.
static bool _saveCjson(lua_State *L, const char *name) {
lua_getglobal(L, "require");
lua_pushstring(L, "cjson");
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
lua_pop(L, 1);
return false;
}
lua_getfield(L, -1, name);
lua_remove(L, -2);
if (!lua_isfunction(L, -1)) {
lua_pop(L, 1);
return false;
}
return true;
}
// A deep copy of the value at "index", pushed on the stack. Everything going into the save and
// everything coming out of it is copied, so the table the engine keeps is its own: what a script
// sets is what is saved, and what it gets back is its own to change. Sharing the table instead
// made an edit through saveGet or saveGetAll persist or not depending on whether something unrelated
// saved later in the same run, which is not a rule anyone could hold in their head.
static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth) {
int32_t source = lua_absindex(L, index);
if (!lua_istable(L, source)) {
lua_pushvalue(L, source);
return;
}
if (depth >= SAVE_DEPTH_MAX) {
_luaDie(L, method, "A saved table is nested more than %d deep, or refers to itself.", SAVE_DEPTH_MAX);
}
lua_newtable(L);
lua_pushnil(L);
while (lua_next(L, source) != 0) {
// The table, its key and its value are on the stack; the copy of the value goes beside
// them, then the key is put back on top so the pair can be written into the new table.
_saveCopy(L, -1, method, depth + 1);
lua_pushvalue(L, -3);
lua_insert(L, -2);
lua_rawset(L, -5);
lua_pop(L, 1);
}
}
// JSON has one kind of number and Lua 5.4 has two, so everything comes back from a decode as a
// float: a score saved as 100 would return 100.0 and print with a decimal point. Whole numbers are
// put back to integers as they are read, which is what the game that saved them had. Recursive,
// because a save is usually a table of tables, with a depth limit for a file somebody hand edited
// into a spiral.
static void _saveNormalise(lua_State *L, int32_t depth) {
lua_Number number = 0;
lua_Integer whole = 0;
if (depth > SAVE_DEPTH_MAX) {
return;
}
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_istable(L, -1)) {
_saveNormalise(L, depth + 1);
} else if (lua_isnumber(L, -1) && !lua_isinteger(L, -1)) {
number = lua_tonumber(L, -1);
whole = (lua_Integer)number;
if ((lua_Number)whole == number) {
// The key is still below the value, so the value is replaced in place rather than
// set again, which would disturb the traversal.
lua_pop(L, 1);
lua_pushinteger(L, whole);
lua_pushvalue(L, -2);
lua_pushvalue(L, -2);
lua_rawset(L, -5);
}
}
lua_pop(L, 1);
}
}
// The save table, read from the file the first time anything asks for it. Always leaves a table on
// the stack: a save that will not parse is reported and then treated as an empty one, because a
// game that cannot start is worse than a game that lost its high scores.
static void _saveTable(lua_State *L) {
char *text = NULL;
lua_getfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
if (lua_istable(L, -1)) {
return;
}
lua_pop(L, 1);
lua_newtable(L);
text = persistRead();
if (text != NULL) {
if (_saveCjson(L, "decode")) {
lua_pushstring(L, text);
if ((lua_pcall(L, 1, 1, 0) == LUA_OK) && lua_istable(L, -1)) {
lua_remove(L, -2);
_saveNormalise(L, 0);
} else {
utilSay("Warning: %s could not be read; starting with an empty save.", persistPath());
lua_pop(L, 1);
}
}
free(text);
}
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
}
// Writes the save table out. Called at the end of a frame that changed it, and at shutdown.
static void _saveWrite(void) {
lua_State *L = _global.luaContext;
const char *text = NULL;
size_t size = 0;
if ((L == NULL) || !_global.saveDirty) {
return;
}
_global.saveDirty = false;
if (!_saveCjson(L, "encode")) {
utilSay("Warning: the save file needs the cjson module, which did not load.");
return;
}
_saveTable(L);
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
utilSay("Warning: the save could not be turned into JSON: %s", lua_tostring(L, -1));
lua_pop(L, 1);
return;
}
text = lua_tolstring(L, -1, &size);
if ((text != NULL) && !persistWrite(text, size)) {
utilSay("Warning: %s could not be written.", persistPath());
}
lua_pop(L, 1);
}
// A value a script wants kept. Tables are allowed because a save is usually a table of them; a
// function or a handle to something the engine owns is not, because neither means anything on the
// next run.
static void _saveCheckValue(lua_State *L, const char *method, int32_t index) {
int32_t type = lua_type(L, index);
if ((type != LUA_TNUMBER) && (type != LUA_TSTRING) && (type != LUA_TBOOLEAN) && (type != LUA_TTABLE)) {
_luaDie(L, method, "A saved value is a number, a string, a boolean or a table of them, not a %s.", lua_typename(L, type));
}
}
// A timer or a tween has fired. Called from the frame loop by way of the scheduler, never from
// another thread, so this is an ordinary Lua call.
static void _scheduleFired(void *context, int32_t handle, SchedulerKindE kind, double value, double progress, bool finished) {
lua_State *L = _global.luaContext;
int32_t handler;
int32_t slot = (finished && (kind == SCHEDULER_TWEEN)) ? SCHEDULE_DONE : SCHEDULE_UPDATE;
(void)context;
if (L == NULL) {
return;
}
if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS)) {
lua_pop(L, 1);
return;
}
lua_rawgeti(L, -1, handle);
lua_remove(L, -2);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return;
}
// A finished tween calls its update one last time at the end value, and then the optional
// second function; a game that only wants the end result gives just that one.
if ((slot == SCHEDULE_DONE) && (kind == SCHEDULER_TWEEN)) {
lua_rawgeti(L, -1, SCHEDULE_UPDATE);
if (lua_isfunction(L, -1)) {
lua_pushcfunction(L, _luaTraceback);
lua_insert(L, -2);
handler = lua_gettop(L) - 1;
lua_pushnumber(L, value);
lua_pushnumber(L, progress);
if (lua_pcall(L, 2, 0, handler) != LUA_OK) {
utilDie("Error in a tween: %s", lua_tostring(L, -1));
}
lua_remove(L, handler);
} else {
lua_pop(L, 1);
}
}
lua_rawgeti(L, -1, slot);
if (!lua_isfunction(L, -1)) {
lua_pop(L, 2);
return;
}
lua_pushcfunction(L, _luaTraceback);
lua_insert(L, -2);
handler = lua_gettop(L) - 1;
if (kind == SCHEDULER_TWEEN) {
lua_pushnumber(L, value);
lua_pushnumber(L, progress);
if (lua_pcall(L, 2, 0, handler) != LUA_OK) {
utilDie("Error in a tween: %s", lua_tostring(L, -1));
}
} else {
lua_pushinteger(L, handle);
if (lua_pcall(L, 1, 0, handler) != LUA_OK) {
utilDie("Error in a timer: %s", lua_tostring(L, -1));
}
}
lua_remove(L, handler);
lua_pop(L, 1);
if (finished) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_pushnil(L);
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
}
}
// Remembers the functions a timer or tween will call, under its handle.
static void _scheduleRemember(lua_State *L, int32_t handle, int32_t updateIndex, int32_t doneIndex) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_newtable(L);
lua_pushvalue(L, updateIndex);
lua_rawseti(L, -2, SCHEDULE_UPDATE);
if ((doneIndex > 0) && lua_isfunction(L, doneIndex)) {
lua_pushvalue(L, doneIndex);
lua_rawseti(L, -2, SCHEDULE_DONE);
}
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
}
// Everything a timer or tween left behind, for a script reload.
static void _scheduleReset(lua_State *L) {
schedulerReset();
if (L == NULL) {
return;
}
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
}
// An empty sprite record for a loader to fill. // An empty sprite record for a loader to fill.
static SpriteT *_spriteNew(lua_State *L, const char *method) { static SpriteT *_spriteNew(lua_State *L, const char *method) {
SpriteT *sprite = (SpriteT *)calloc(1, sizeof(SpriteT)); SpriteT *sprite = (SpriteT *)calloc(1, sizeof(SpriteT));
@ -7495,6 +7907,102 @@ static int32_t apiCameraSetPerspective(lua_State *L) {
// colorBackground(r, g, b[, a]) Default alpha is transparent so overlayPrint shows the video through. // colorBackground(r, g, b[, a]) Default alpha is transparent so overlayPrint shows the video through.
// touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
static int32_t apiCollideCircles(lua_State *L) {
_argCheck(L, "collideCircles", 6, 6);
lua_pushboolean(L, collideCircles(_argNumber(L, "collideCircles", 1), _argNumber(L, "collideCircles", 2), _argNumber(L, "collideCircles", 3),
_argNumber(L, "collideCircles", 4), _argNumber(L, "collideCircles", 5), _argNumber(L, "collideCircles", 6)));
return 1;
}
// inside = collidePointCircle(pointX, pointY, x, y, radius)
static int32_t apiCollidePointCircle(lua_State *L) {
_argCheck(L, "collidePointCircle", 5, 5);
lua_pushboolean(L, collidePointCircle(_argNumber(L, "collidePointCircle", 1), _argNumber(L, "collidePointCircle", 2),
_argNumber(L, "collidePointCircle", 3), _argNumber(L, "collidePointCircle", 4), _argNumber(L, "collidePointCircle", 5)));
return 1;
}
// inside = collidePointPolygon(pointX, pointY, points)
// The points are a flat list, x then y, which is the shape a hitbox table in the game library
// already has.
static int32_t apiCollidePointPolygon(lua_State *L) {
double points[SAVE_POLYGON_MAX * 2];
int32_t corners = 0;
int32_t count = 0;
int32_t x = 0;
_argCheck(L, "collidePointPolygon", 3, 3);
if (!lua_istable(L, 3)) {
_luaDie(L, "collidePointPolygon", "The third argument is a table of x, y pairs.");
}
count = (int32_t)lua_rawlen(L, 3);
if ((count % 2) != 0) {
_luaDie(L, "collidePointPolygon", "A polygon is x, y pairs, so the table has an even number of entries, not %d.", count);
}
corners = count / 2;
if (corners > SAVE_POLYGON_MAX) {
_luaDie(L, "collidePointPolygon", "A polygon may have %d corners, not %d.", SAVE_POLYGON_MAX, corners);
}
for (x = 0; x < count; x++) {
lua_rawgeti(L, 3, x + 1);
if (!lua_isnumber(L, -1)) {
_luaDie(L, "collidePointPolygon", "Entry %d of the polygon is not a number.", x + 1);
}
points[x] = lua_tonumber(L, -1);
lua_pop(L, 1);
}
lua_pushboolean(L, collidePointPolygon(_argNumber(L, "collidePointPolygon", 1), _argNumber(L, "collidePointPolygon", 2), points, corners));
return 1;
}
// inside = collidePointRect(pointX, pointY, x, y, width, height)
static int32_t apiCollidePointRect(lua_State *L) {
_argCheck(L, "collidePointRect", 6, 6);
lua_pushboolean(L, collidePointRect(_argNumber(L, "collidePointRect", 1), _argNumber(L, "collidePointRect", 2), _argNumber(L, "collidePointRect", 3),
_argNumber(L, "collidePointRect", 4), _argNumber(L, "collidePointRect", 5), _argNumber(L, "collidePointRect", 6)));
return 1;
}
// touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
static int32_t apiCollideRectCircle(lua_State *L) {
_argCheck(L, "collideRectCircle", 7, 7);
lua_pushboolean(L, collideRectCircle(_argNumber(L, "collideRectCircle", 1), _argNumber(L, "collideRectCircle", 2), _argNumber(L, "collideRectCircle", 3),
_argNumber(L, "collideRectCircle", 4), _argNumber(L, "collideRectCircle", 5), _argNumber(L, "collideRectCircle", 6),
_argNumber(L, "collideRectCircle", 7)));
return 1;
}
// touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
static int32_t apiCollideRects(lua_State *L) {
_argCheck(L, "collideRects", 8, 8);
lua_pushboolean(L, collideRects(_argNumber(L, "collideRects", 1), _argNumber(L, "collideRects", 2), _argNumber(L, "collideRects", 3), _argNumber(L, "collideRects", 4),
_argNumber(L, "collideRects", 5), _argNumber(L, "collideRects", 6), _argNumber(L, "collideRects", 7), _argNumber(L, "collideRects", 8)));
return 1;
}
// crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
static int32_t apiCollideSegments(lua_State *L) {
_argCheck(L, "collideSegments", 8, 8);
lua_pushboolean(L, collideSegments(_argNumber(L, "collideSegments", 1), _argNumber(L, "collideSegments", 2), _argNumber(L, "collideSegments", 3), _argNumber(L, "collideSegments", 4),
_argNumber(L, "collideSegments", 5), _argNumber(L, "collideSegments", 6), _argNumber(L, "collideSegments", 7), _argNumber(L, "collideSegments", 8)));
return 1;
}
static int32_t apiColorBackground(lua_State *L) { static int32_t apiColorBackground(lua_State *L) {
_readColor(L, "colorBackground", &_global.colorBackground, SDL_ALPHA_TRANSPARENT); _readColor(L, "colorBackground", &_global.colorBackground, SDL_ALPHA_TRANSPARENT);
@ -11475,6 +11983,106 @@ static int32_t apiRatioGetY(lua_State *L) {
} }
// saveClear() Forgets everything and writes the empty save out.
static int32_t apiSaveClear(lua_State *L) {
_argCheck(L, "saveClear", 0, 0);
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
_global.saveDirty = true;
_luaTrace(L, "saveClear", "Cleared.");
return 0;
}
// saveDelete(key) Removes one key.
static int32_t apiSaveDelete(lua_State *L) {
_argCheck(L, "saveDelete", 1, 1);
_saveTable(L);
lua_pushvalue(L, 1);
lua_pushnil(L);
lua_rawset(L, -3);
lua_pop(L, 1);
_global.saveDirty = true;
return 0;
}
// saveFlush() Writes the save out now rather than at the end of the frame, for a cabinet that may
// lose power at any moment.
static int32_t apiSaveFlush(lua_State *L) {
_argCheck(L, "saveFlush", 0, 0);
_global.saveDirty = true;
_saveWrite();
return 0;
}
// value = saveGet(key [, default]) What was saved under that key, or the default when nothing was.
static int32_t apiSaveGet(lua_State *L) {
_argCheck(L, "saveGet", 1, 2);
_saveTable(L);
lua_pushvalue(L, 1);
lua_rawget(L, -2);
if (lua_isnil(L, -1) && (lua_gettop(L) >= 4)) {
lua_pop(L, 1);
lua_pushvalue(L, 2);
} else {
_saveCopy(L, -1, "saveGet", 0);
}
return 1;
}
// everything = saveGetAll() A copy of the whole save table, for reading: dumping it while
// debugging, or walking keys the game does not know the names of, which is what moving an old save
// to a new layout looks like. Changing what comes back changes nothing; saveSetAll writes it.
static int32_t apiSaveGetAll(lua_State *L) {
_argCheck(L, "saveGetAll", 0, 0);
_saveTable(L);
_saveCopy(L, -1, "saveGetAll", 0);
return 1;
}
// saveSet(key, value) Keeps a value until the game is uninstalled. The write happens once at the
// end of the frame however many keys were set in it.
static int32_t apiSaveSet(lua_State *L) {
_argCheck(L, "saveSet", 2, 2);
_saveCheckValue(L, "saveSet", 2);
_saveTable(L);
lua_pushvalue(L, 1);
_saveCopy(L, 2, "saveSet", 0);
lua_rawset(L, -3);
lua_pop(L, 1);
_global.saveDirty = true;
return 0;
}
// saveSetAll(everything) Replaces the whole save with this table, in one step. Keys that were
// there and are not in the table given are gone. Moving a save to a new layout is the reason it
// exists: read it with saveGetAll, change it, put it back, with no moment in between where the
// save is empty and a crash would lose it.
static int32_t apiSaveSetAll(lua_State *L) {
_argCheck(L, "saveSetAll", 1, 1);
if (!lua_istable(L, 1)) {
_luaDie(L, "saveSetAll", "saveSetAll takes a table of everything to keep, not a %s.", lua_typename(L, lua_type(L, 1)));
}
_saveCopy(L, 1, "saveSetAll", 0);
lua_setfield(L, LUA_REGISTRYINDEX, SAVE_TABLE);
_global.saveDirty = true;
_luaTrace(L, "saveSetAll", "Replaced.");
return 0;
}
static int32_t apiSceneEnable(lua_State *L) { static int32_t apiSceneEnable(lua_State *L) {
bool enabled; bool enabled;
@ -13210,6 +13818,26 @@ static int32_t apiSrtPosition(lua_State *L) {
// height = terrainGetHeight(node, x, z): the terrain's height at a world x, z, or nil off it // height = terrainGetHeight(node, x, z): the terrain's height at a world x, z, or nil off it
// statsEnable(enabled) The developer's overlay: frame time, what is alive, what the disc is doing.
static int32_t apiStatsEnable(lua_State *L) {
_argCheck(L, "statsEnable", 1, 1);
statsSetEnabled(_argBoolean(L, "statsEnable", 1));
_global.refreshDisplay = true;
_luaTrace(L, "statsEnable", "%d", statsIsEnabled());
return 0;
}
// shown = statsIsEnabled()
static int32_t apiStatsIsEnabled(lua_State *L) {
_argCheck(L, "statsIsEnabled", 0, 0);
lua_pushboolean(L, statsIsEnabled());
return 1;
}
static int32_t apiTerrainGetHeight(lua_State *L) { static int32_t apiTerrainGetHeight(lua_State *L) {
float height = 0.0f; float height = 0.0f;
@ -13224,6 +13852,117 @@ static int32_t apiTerrainGetHeight(lua_State *L) {
// index = vehicleAddWheel(vehicle, wheelNode, radius, width, suspensionLength) // index = vehicleAddWheel(vehicle, wheelNode, radius, width, suspensionLength)
// id = timerAfter(milliseconds, function) Calls the function once, later. The handle comes back so
// it can be cancelled; the function is given it too, so one function can serve several timers.
static int32_t apiTimerAfter(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "timerAfter", 2, 2);
if (!lua_isfunction(L, 2)) {
_luaDie(L, "timerAfter", "The second argument is the function to call.");
}
handle = schedulerTimer(_argInteger64(L, "timerAfter", 1), false, utilTicks());
_scheduleRemember(L, handle, 2, 0);
lua_pushinteger(L, handle);
return 1;
}
// timerCancel(id) Stops one. A timer that has already fired is not an error to cancel.
static int32_t apiTimerCancel(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "timerCancel", 1, 1);
handle = _argInteger(L, "timerCancel", 1);
schedulerCancel(handle);
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_pushnil(L);
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
return 0;
}
// id = timerEvery(milliseconds, function) Calls the function over and over until it is cancelled.
static int32_t apiTimerEvery(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "timerEvery", 2, 2);
if (!lua_isfunction(L, 2)) {
_luaDie(L, "timerEvery", "The second argument is the function to call.");
}
handle = schedulerTimer(_argInteger64(L, "timerEvery", 1), true, utilTicks());
_scheduleRemember(L, handle, 2, 0);
lua_pushinteger(L, handle);
return 1;
}
// running = timerIsActive(id) Whether it will fire again. True for a repeating timer until it is
// cancelled, false for a one shot that has been.
static int32_t apiTimerIsActive(lua_State *L) {
_argCheck(L, "timerIsActive", 1, 1);
lua_pushboolean(L, schedulerIsActive(_argInteger(L, "timerIsActive", 1)));
return 1;
}
// tweenCancel(id) Stops a tween where it is. Neither function is called again.
static int32_t apiTweenCancel(lua_State *L) {
int32_t handle = 0;
_argCheck(L, "tweenCancel", 1, 1);
handle = _argInteger(L, "tweenCancel", 1);
schedulerCancel(handle);
luaL_getsubtable(L, LUA_REGISTRYINDEX, SAVE_CALLBACKS);
lua_pushnil(L);
lua_rawseti(L, -2, handle);
lua_pop(L, 1);
return 0;
}
// running = tweenIsActive(id)
static int32_t apiTweenIsActive(lua_State *L) {
_argCheck(L, "tweenIsActive", 1, 1);
lua_pushboolean(L, schedulerIsActive(_argInteger(L, "tweenIsActive", 1)));
return 1;
}
// id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
// Moves a number from one value to another over time, calling onUpdate(value, progress) every frame
// and onDone() at the end. What the number means is the game's business: a position, an opacity,
// a volume, a camera angle. That is why this hands over a number rather than moving a node itself.
static int32_t apiTweenValue(lua_State *L) {
int32_t handle = 0;
int32_t easing = 0;
_argCheck(L, "tweenValue", 5, 6);
easing = _argInteger(L, "tweenValue", 4);
if ((easing < 0) || (easing >= SCHEDULER_EASE_COUNT)) {
_luaDie(L, "tweenValue", "Easing is one of the EASE_ values, 0 to %d, not %d.", SCHEDULER_EASE_COUNT - 1, easing);
}
if (!lua_isfunction(L, 5)) {
_luaDie(L, "tweenValue", "The fifth argument is the function called with each value.");
}
if ((lua_gettop(L) >= 6) && !lua_isfunction(L, 6)) {
_luaDie(L, "tweenValue", "The sixth argument, when given, is the function called at the end.");
}
handle = schedulerTween(_argNumber(L, "tweenValue", 1), _argNumber(L, "tweenValue", 2), _argInteger64(L, "tweenValue", 3), (SchedulerEaseE)easing, utilTicks());
_scheduleRemember(L, handle, 5, (lua_gettop(L) >= 6) ? 6 : 0);
lua_pushinteger(L, handle);
return 1;
}
static int32_t apiVehicleAddWheel(lua_State *L) { static int32_t apiVehicleAddWheel(lua_State *L) {
int32_t index = 0; int32_t index = 0;
@ -14413,6 +15152,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
ManyMouseEvent mouseEvent; ManyMouseEvent mouseEvent;
MouseT *mouse = NULL; MouseT *mouse = NULL;
char *audioFile = NULL; char *audioFile = NULL;
uint64_t frameStartNS = 0;
int32_t finished[SOUND_QUEUE_SIZE]; int32_t finished[SOUND_QUEUE_SIZE];
int32_t finishedCount = 0; int32_t finishedCount = 0;
@ -14441,6 +15181,9 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
_progTrace("Deterministic mode: %d ms a frame, generators seeded with %d", _global.conf->deterministicStep, _global.conf->deterministicStep); _progTrace("Deterministic mode: %d ms a frame, generators seeded with %d", _global.conf->deterministicStep, _global.conf->deterministicStep);
} }
vfsInit(_global.conf->container, _global.conf->dataDirBase, _global.conf->dataDir); vfsInit(_global.conf->container, _global.conf->dataDirBase, _global.conf->dataDir);
// The save file lives in the game's own data directory, so every game keeps its own and a
// packed game carries none of it inside the .game.
persistOpen(_global.conf->dataDir);
videoSetAudioDelay(_global.conf->audioDelayMs); videoSetAudioDelay(_global.conf->audioDelayMs);
videoSetAudioCalibration(_loadAudioCalibration()); videoSetAudioCalibration(_loadAudioCalibration());
utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay()); utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay());
@ -14654,6 +15397,10 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
// Game Loop // Game Loop
_progTrace("Script is running"); _progTrace("Script is running");
while (_global.running) { while (_global.running) {
// The overlay's clock is the real one even in deterministic mode: a developer asking how
// long a frame took wants the answer in the seconds they are sitting through.
frameStartNS = SDL_GetTicksNS();
// One frame of simulated time, the only place the virtual clock moves. Off it, nothing happens. // One frame of simulated time, the only place the virtual clock moves. Off it, nothing happens.
utilTickAdvance(); utilTickAdvance();
@ -14919,6 +15666,20 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
// drawing: a game that never redraws still has to be given its messages. // drawing: a game that never redraws still has to be given its messages.
midiIoPoll(_midiReceived, NULL); midiIoPoll(_midiReceived, NULL);
// Timers and tweens, for the same reason.
schedulerUpdate(utilTicks(), _scheduleFired, NULL);
// One write for however many keys the frame changed.
_saveWrite();
// How long that took, for the overlay. Keeping it while the overlay is off means turning
// it on shows the last two seconds rather than an empty graph.
_global.statsFrameMs = (double)(SDL_GetTicksNS() - frameStartNS) / (double)SDL_NS_PER_MS;
statsSample(_global.statsFrameMs);
if (statsIsEnabled()) {
_global.refreshDisplay = true;
}
// --idleexit: nothing has been touched for that long, so an attract cabinet lets go. // --idleexit: nothing has been touched for that long, so an attract cabinet lets go.
if ((_global.conf->idleExitSeconds > 0) && ((utilTicks() - _global.idleClock) >= ((uint64_t)_global.conf->idleExitSeconds * MS_PER_SECOND))) { if ((_global.conf->idleExitSeconds > 0) && ((utilTicks() - _global.idleClock) >= ((uint64_t)_global.conf->idleExitSeconds * MS_PER_SECOND))) {
_progTrace("Idle for %d seconds; quitting", _global.conf->idleExitSeconds); _progTrace("Idle for %d seconds; quitting", _global.conf->idleExitSeconds);
@ -15059,6 +15820,9 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
_progTrace("Taking screenshot"); _progTrace("Taking screenshot");
_takeScreenshot(); _takeScreenshot();
} }
// The developer's overlay goes on after the screenshot, so a reference shot is of the
// game rather than of the overlay.
_statsDraw();
// Show it // Show it
SDL_RenderPresent(_global.renderer); SDL_RenderPresent(_global.renderer);
_global.refreshDisplay = false; _global.refreshDisplay = false;
@ -15074,6 +15838,10 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
_progTrace("Script is shutting down"); _progTrace("Script is shutting down");
_callLua("onShutdown", ""); _callLua("onShutdown", "");
// Anything the script saved on its way out, including in onShutdown itself.
_saveWrite();
persistClose();
// Stop all sounds // Stop all sounds
_progTrace("Stopping all audio"); _progTrace("Stopping all audio");
for (x = 0; x < EFFECT_TRACKS; x++) { for (x = 0; x < EFFECT_TRACKS; x++) {

142
src/stats.c Normal file
View file

@ -0,0 +1,142 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Singe: the developer's overlay. See stats.h.
#include <string.h>
#include "common.h"
#include "stats.h"
#define HISTORY 120 // Frames kept, about two seconds at sixty
#define MARGIN 8
#define LINE_HEIGHT (SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE + 2)
#define PANEL_WIDTH 300 // Wide enough for the longest line, which is the frame times
#define LINES_MAX 6 // What statsDraw writes; the backdrop is sized from it
#define TEXT_MAX 96
#define MS_PER_SECOND 1000.0
#define BACKDROP_ALPHA 170
static bool _enabled = false;
static double _history[HISTORY];
static int32_t _next = 0;
static int32_t _samples = 0;
static void _line(SDL_Renderer *renderer, int32_t index, const char *text);
// ===== Internal helpers =====
static void _line(SDL_Renderer *renderer, int32_t index, const char *text) {
SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
SDL_RenderDebugText(renderer, (float)(MARGIN + 4), (float)(MARGIN + 4 + (index * LINE_HEIGHT)), text);
}
// ===== Public =====
void statsDraw(SDL_Renderer *renderer, const StatsT *stats) {
SDL_FRect backdrop;
char text[TEXT_MAX];
int32_t line = 0;
if (!_enabled || (renderer == NULL)) {
return;
}
backdrop.x = MARGIN;
backdrop.y = MARGIN;
backdrop.w = PANEL_WIDTH;
backdrop.h = (float)((LINES_MAX * LINE_HEIGHT) + 8);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, BACKDROP_ALPHA);
SDL_RenderFillRect(renderer, &backdrop);
SDL_snprintf(text, sizeof(text), "%6.2f ms worst %6.2f %5.1f fps", stats->frameMilliseconds, stats->worstMilliseconds, stats->framesPerSecond);
_line(renderer, line++, text);
SDL_snprintf(text, sizeof(text), "lua %8.0f KB", stats->luaKilobytes);
_line(renderer, line++, text);
SDL_snprintf(text, sizeof(text), "sprites %-5d sounds %-5d timers %d", stats->sprites, stats->sounds, stats->timers);
_line(renderer, line++, text);
SDL_snprintf(text, sizeof(text), "nodes %d of %d in %d batches", stats->nodesDrawn, stats->nodesTotal, stats->batches);
_line(renderer, line++, text);
SDL_snprintf(text, sizeof(text), "textures %6" PRId64 " KB", stats->textureKilobytes);
_line(renderer, line++, text);
if (stats->discState != NULL) {
SDL_snprintf(text, sizeof(text), "disc %-9" PRId64 " %s", stats->discFrame, stats->discState);
} else {
SDL_strlcpy(text, "disc none", sizeof(text));
}
_line(renderer, line++, text);
}
double statsFramesPerSecond(void) {
double total = 0;
int32_t x = 0;
if (_samples == 0) {
return 0;
}
for (x = 0; x < _samples; x++) {
total += _history[x];
}
if (total <= 0) {
return 0;
}
return (MS_PER_SECOND * _samples) / total;
}
bool statsIsEnabled(void) {
return _enabled;
}
void statsSample(double milliseconds) {
_history[_next] = milliseconds;
_next = (_next + 1) % HISTORY;
if (_samples < HISTORY) {
_samples++;
}
}
void statsSetEnabled(bool enabled) {
_enabled = enabled;
}
double statsWorstMilliseconds(void) {
double worst = 0;
int32_t x = 0;
for (x = 0; x < _samples; x++) {
if (_history[x] > worst) {
worst = _history[x];
}
}
return worst;
}

73
src/stats.h Normal file
View file

@ -0,0 +1,73 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef STATS_H
#define STATS_H
#include <stdint.h>
#include <stdbool.h>
#include <SDL3/SDL.h>
// The developer's overlay: how long the frame took, how much is alive, what the disc is doing.
// Drawn with SDL's own debug text, so it needs no font, no asset and no GUI document, and works on
// a machine where the 3D device never came up.
//
// What a game can tell it about itself. Every field is filled by the engine each frame; nothing
// here reaches into another subsystem, so this file stays a renderer rather than a second place
// that knows how the engine is put together.
typedef struct {
double frameMilliseconds;
double worstMilliseconds; // Over the frames kept, about two seconds
double framesPerSecond;
double luaKilobytes;
int32_t sprites;
int32_t sounds;
int32_t timers; // Timers and tweens together
int32_t nodesDrawn; // Of nodesTotal, after culling
int32_t nodesTotal;
int32_t batches;
int64_t textureKilobytes;
int64_t discFrame;
const char *discState; // NULL for a game with no disc
} StatsT;
// Draws the overlay over whatever is already on the renderer. Does nothing when it is off.
void statsDraw(SDL_Renderer *renderer, const StatsT *stats);
bool statsIsEnabled(void);
// Takes one frame's length, in milliseconds, and keeps the running figures. Call it every frame,
// on or off: turning the overlay on should show the last second, not an empty one.
void statsSample(double milliseconds);
void statsSetEnabled(bool enabled);
// The figures statsSample has been keeping.
double statsFramesPerSecond(void);
double statsWorstMilliseconds(void);
#endif // STATS_H

20
thirdparty/bump.lua/MIT-LICENSE.txt vendored Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2012 Enrique García Cota
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.

565
thirdparty/bump.lua/README.md vendored Normal file
View file

@ -0,0 +1,565 @@
# bump.lua
[![Build Status](https://travis-ci.org/kikito/bump.lua.svg?branch=master)](https://travis-ci.org/kikito/bump.lua)
[![Coverage Status](https://coveralls.io/repos/github/kikito/bump.lua/badge.svg?branch=master)](https://coveralls.io/github/kikito/bump.lua?branch=master)
Lua collision-detection library for axis-aligned rectangles. Its main features are:
* bump.lua only does axis-aligned bounding-box (AABB) collisions. If you need anything more complicated than that (circles, polygons, etc.) give [HardonCollider](https://github.com/vrld/HardonCollider) a look.
* Handles tunnelling - all items are treated as "bullets". The fact that we only use AABBs allows doing this fast.
* Strives to be fast while being economic in memory.
* It's centered on *detection*, but it also offers some (minimal & basic) *collision response*.
* Can also return the items that touch a point, a segment or a rectangular zone.
* bump.lua is _gameistic_ instead of realistic.
The demos are LÖVE based, but this library can be used in any Lua-compatible environment.
`bump` is ideal for:
* Tile-based games, and games where most entities can be represented as axis-aligned rectangles.
* Games which require some physics, but not a full realistic simulation - like a platformer.
* Examples of genres: top-down games (Zelda), shoot 'em ups, fighting games (Street Fighter), platformers (Super Mario).
`bump` is not a good match for:
* Games that require polygons for the collision detection.
* Games that require highly realistic simulations of physics - things "stacking up", "rolling over slides", etc.
* Games that require very fast objects colliding realistically against each other (in bump, being _gameistic_, objects are moved and collided _one at a time_).
* Simulations where the order in which the collisions are resolved isn't known.
## Example
```lua
local bump = require 'bump'
-- The grid cell size can be specified via the initialize method
-- By default, the cell size is 64
local world = bump.newWorld(50)
-- create two rectangles
local A = {name="A"}
local B = {name="B"}
-- insert both rectangles into bump
world:add(A, 0, 0, 64, 256) -- x,y, width, height
world:add(B, 0, -100, 32, 32)
-- Try to move B to 0,64. If it collides with A, "slide over it"
local actualX, actualY, cols, len = world:move(B, 0,64)
-- prints "Attempted to move to 0,64, but ended up in 0,-32 due to 1 collisions"
if len > 0 then
print(("Attempted to move to 0,64, but ended up in %d,%d due to %d collisions"):format(actualX, actualY, len))
else
print("Moved B to 100,100 without collisions")
end
-- prints the new coordinates of B: 0, -32, 32, 32
print(world:getRect(B))
-- prints "Collision with A"
for i=1,len do -- If more than one simultaneous collision, they are sorted out by proximity
local col = cols[i]
print(("Collision with %s."):format(col.other.name))
end
-- remove A and B from the world
world:remove(A)
world:remove(B)
```
## Demos
There is a demo showing movement, collision detection and basic slide-based resolution in this branch:
http://github.com/kikito/bump.lua/tree/simpledemo
![simpledemo](https://kikito.github.io/bump.lua/img/bump-simpledemo.gif)
There's a more complex demo showing more advanced movement mechanics (i.e. acceleration, bouncing) in this other
repo:
http://github.com/kikito/bump.lua/tree/demo
![demo](https://kikito.github.io/bump.lua/img/bump-demo.gif)
You will need [LÖVE](http://love2d.org) in order to try any of them.
## Basic API - Adding, removing and moving items
### Requiring the library
``` lua
local bump = require 'bump'
```
The following methods (`bump.newWorld`, `world:add`, `world:remove`, `world:update`, `world:move` & `world:check`) are *basic* for
working with bump, as well as the 4 collision responses. If you want to use bump.lua effectively, you will need to understand at least
these.
### Creating a world
``` lua
local world = bump.newWorld(cellSize)
```
The first thing to do with bump is creating a world. That is done with `bump.newWorld`.
* `cellSize`. Is an optional number. It defaults to 64. It represents the size of the sides
of the (squared) cells that will be used internally to provide the data. In tile-based games, it's usually a multiple of
the tile side size. So in a game where tiles are 32x32, `cellSize` will be 32, 64 or 128. In more sparse games, it can be
higher.
Don't worry too much about `cellSize` at the beginning, you can tweak it later on to see if bigger/smaller numbers
give you better results (you can't change the value of `cellSize` in runtime, but you can create as many worlds as you want,
each one with a different `cellSize` if the need arises.)
The rest of the methods we have are for the worlds that we create.
### Adding items to the world
``` lua
world:add(item, x,y,w,h)
```
`world:add` is what you need to insert a new item in a world. "Items" are "anything that matters to your collision". It can be the player character,
a tile, a missile etc. In fact, you can insert items that don't participate in the collision at all - like puffs of smoke or background tiles. This
can be handy if you want to use the bump world as a spatial database in addition to a collision detector (see the "queries section" below for more details).
Each `item` will have an associated "rectangle" in the `world`.
* `item` is the new item being inserted (usually a table representing a game object, like `player` or `ground_tile`).
* `x,y,w,h`: the rectangle associated to `item` in the world. They are all mandatory. `w` & `h` are the "width" and "height"
of the box. `x` and `y` depend on the host system's coordinate system. For example, in [LÖVE](http://love2d.org) &
[Corona SDK](http://coronalabs.com/products/corona-sdk/) they represent "left" & "top", while in [Cocos2d-x](http://cocos2d-x.org/wiki/Lua)
they represent "left" & "bottom".
`world:add` returns no values. It generates no collisions - you can call `world:check(item)` if you want to get the collisions it creates right after it's added.
If you try to add an item to a world that already contains it, you will get an error.
### Removing items from the world
``` lua
world:remove(item)
```
bump.lua stores *hard references* to any items that you add (with `world:add`). If you decide that a item is no longer necessary, in addition to removing it
from your "entity list", you must also remove it from the world using `world:remove`. Otherwise it will still be there, and other objects might still collide
with it.
* `item` must be something previously inserted in the world with `world:add(item, l,t,w,h)`. If this is not the case, `world:remove` will raise an error.
Once removed from the world, the item will stop existing in that world. It won't trigger any collisions with other objects any more. Attempting to move it
with `world:move` or checking collisions with `world:check` will raise an error.
It is OK to remove an object from the world and later add it again. In fact, some bump methods do this internally.
This method returns nothing.
### Changing the position and dimensions of items in the world
``` lua
world:update(item, x,y,<w>,<h>)
```
Even if your "player" has attributes like `player.x` and `player.y`, changing those will not automatically change them inside `world`. `update` is one of
the ways to do so: it changes the rect representing `item` inside `world`.
* `item` must be something previously inserted in the world with `world:add(item, l,t,w,h)`. Otherwise, `world:update` will raise an error.
* `x,y,w,h` the new dimensions of `item`. `x` and `y` are mandatory. `w` and `h` will default to the values the world already had for `item`.
This method always changes the rect associated to `item`, ignoring all collisions (use `world:move` for that). It returns nothing.
You may use `world:update` if you want to "teleport" your items around. A lot of time, however, you want to move them taking collisions into account.
In order to do that, you have `world:move`.
### Moving an item in the world, with collision resolution
``` lua
local actualX, actualY, cols, len = world:move(item, goalX, goalY, <filter>)
```
This is probably the most useful method of bump. It moves the item inside the world towards a desired position, but taking collisions into account.
* `item` must be something previously inserted in the world with `world:add(item, l,t,w,h)`. Otherwise, `world:move` will raise an error.
* `goalX, goalY` are the *desired* `x` and `y` coordinates. The item will end up in those coordinates if it doesn't collide with anything.
If, however, it collides with 1 or more other items, it can end up in a different set of coordinates.
* `filter` is an optional function. If provided, it must have this signature: `local type = filter(item, other)`. By default, `filter` always returns `"slide"`.
* `item` is the item being moved (the same one passed to `world:move` on the first param).
* `other` is an item (different from `item`) which can collide with `item`.
* `type` is a value which defines how `item` collides with `other`.
* If `type` is `false` or `nil`, `item` will ignore `other` completely (there will be no collision).
* If `type` is `"touch"`, `"cross"`, `"slide"` or `"bounce"`, `item` will respond to the collisions in different ways (explained below).
* Any other value (unless handled in an advanced way) will provoke an error.
* `actualX, actualY` are the coordinates where the object ended up after colliding with other objects in the world while trying to get to
`goalX, goalY`. They can be equal to `goalX, goalY` if, for example, no collisions happened.
* `len` is the amount of collisions produced. It is equivalent to `#cols`.
* `cols` is an array of all the collisions that were detected. Each collision is a table. The most important item in that table is `cols[i].other`, which
points to the item that collided with `item`. A full description of what's inside of each collision can be found on the "Advanced API" section.
The usual way you would use move is: calculate a "desirable" `goalX, goalY` point for an item (maybe using its velocity), pass it to move, and then use `actualX, actualY`
as the real "updates". For example, here's how a player would move:
``` lua
function movePlayer(player, dt)
local goalX, goalY = player.x + player.vx * dt, player.y + player.vy * dt
local actualX, actualY, cols, len = world:move(player, goalX, goalY)
player.x, player.y = actualX, actualY
-- deal with the collisions
for i=1,len do
print('collided with ' .. tostring(cols[i].other))
end
end
```
Notice that if `filter` returns `nil` or `false`, it is guaranteed that `other` will not produce a collision. But the opposite is not true: it is possible that `filter` returns
`"slide"`, and yet no collision is produced. This is because `filter` is applied to *all the neighbors of `item`*, that is, all the items that "touch" the same cells as item. Some
of them might be on the same cells, but still not collide with item..
#### Collision Resolution
For each of the collisions returned by `world:move`, the most interesting attribute is `cols[i].other`. Often it's enough with it - for example if `item`
is one of those bullets that disappear when impacting the player you must make the bullet disappear (and decrease the player's health).
`world:move()` returns a list (instead of a single collision element) because in some cases you might want to "skip" some
collisions, or react to several of them in a single frame.
For example, imagine a player which collides on the same frame with a coin first, an enemy fireball, and the floor.
* since `cols[1].other` will be a coin, you will want to make the coin disappear (maybe with a sound) and increase the player's score.
* `cols[2].other` will be a fireball, so you will want to decrease the player's health and make the fireball disappear.
* `cols[3].other` will be a ground tile, so you will need to stop the player from "falling down", and maybe align it with the ground.
The first two can be handled just by using `col.other`, but "aligning the player with the ground" requires *collision resolution*.
bump.lua comes with 4 built-in ways to handle collisions: `touch`, `cross`, `slide` & `bounce`. You can select which one is used on each collision by returning
their name in the `filter` param of `world:move` or `world:check`. You can also choose to ignore a collision by returning `nil` or `false`.
![touch](img/touch.png)
This is the type of collision for things like arrows or bullets; things that "get stuck" on their targets.
Collisions of this type have their `type` attribute set to `"touch"` and don't have any additional information apart from the the default one, shared by all collisions (see below).
![cross](img/cross.png)
This type of collision is for cases where you want to detect a collision but you don't want any response. It is useful for things like: detecting that the player has entered a new area,
or consumables (i.e. coins) which usually don't affect the player's trajectory, but it's still useful to know then they are collided with.
Collisions of this type have their `type` attribute set to `"cross"` and don't have any additional information apart from the the default one, shared by all collisions (see below).
![slide](img/slide.png)
This is the default collision type used in bump. It's what you want to use for solid objects which "slide over other objects", like Super Mario does over a platform or the ground.
Collisions of this type have their `type` attribute set to `"slide"`. They also have a special attribute called `col.slide`, which is a 2D vector with two components: `col.slide.x` &
`col.slide.y`. It represents the x and y coordinates to which the `item` "attempted to slide to". They are different from `actualX` & `actualY` since other collisions later on can
modify them.
![bounce](img/bounce.png)
A good example of this behavior is Arkanoid's ball; you can use this type of collision for things that "move away" after touching others.
Collisions of this type have their `type` attribute set to `"bounce"`. They also have a special attributes called `col.bounce`. It is a 2D vector which represents the x and y
coordinates to which the `item` "attempted to bounce".
The [Grenades](https://github.com/kikito/bump.lua/blob/demo/entities/grenade.lua) and the [Debris](https://github.com/kikito/bump.lua/blob/demo/entities/debris.lua) in the
demo use `"bounce"` to resolve their collisions.
Here's an example of a filter displaying all these behaviors:
```lua
local playerFilter = function(item, other)
if other.isCoin then return 'cross'
elseif other.isWall then return 'slide'
elseif other.isExit then return 'touch'
elseif other.isSpring then return 'bounce'
end
-- else return nil
end
```
The code above will make a character work more or less like super-mario, collision-wise. It'll go through coins, collide with walls, bounce over springs, etc., ignoring things it should
not collide with like clouds in the background.
You could then use the collisions returned like so:
``` lua
function movePlayer(player, dt)
local goalX, goalY = player.vx * dt, player.vy * dt
local actualX, actualY, cols, len = world:move(player, goalX, goalY, playerFilter)
player.x, player.y = actualX, actualY
for i=1,len do
local other = cols[i].other
if other.isCoin then
takeCoin(other)
elseif other.isExit then
changeLevel()
elseif other.isSpring then
highJump()
end
end
end
```
### Checking for collisions without moving
``` lua
local actualX, actualY, cols, len = world:check(item, goalX, goalY, <filter>)
```
It returns the position where `item` would end up, and the collisions it would encounter, should it attempt to move to `goalX, goalY` with the specified `filter`.
Notice that `check` has the same parameters and return values as `move`. The difference is that the former does not update the position of `item` in the world - you
would have to call `world:update` in order to do that. In fact, `world:move` is implemented by calling `world:check` first, and then `world:update` immediately after.
The equivalent code to the previous example using `check` would be:
``` lua
function movePlayer(player, dt)
local goalX, goalY = player.vx * dt, player.vy * dt
local actualX, actualY, cols, len = world:check(player, goalX, goalY)
world:update(player, actualX, actualY) -- update the player's rectangle in the world
player.x, player.y = actualX, actualY
... <deal with the collisions as before>
end
```
`world:check` is useful for things like "planning ahead" or "studying alternatives", when moving is still not fully decided.
### Collision info
Here's the info contained on every collision item contained in the `cols` variables mentioned above:
```lua
cols[i] = {
item = the item being moved / checked
other = an item colliding with the item being moved
type = the result of `filter(other)`. It's usually "touch", "cross", "slide" or "bounce"
overlaps = boolean. True if item "was overlapping" other when the collision started.
False if it didn't but "tunneled" through other
ti = Number between 0 and 1. How far along the movement to the goal did the collision occur?
move = Vector({x=number,y=number}). The difference between the original coordinates and the actual ones.
normal = Vector({x=number,y=number}). The collision normal; usually -1,0 or 1 in `x` and `y`
touch = Vector({x=number,y=number}). The coordinates where item started touching other
itemRect = The rectangle item occupied when the touch happened({x = N, y = N, w = N, h = N})
otherRect = The rectangle other occupied when the touch happened({x = N, y = N, w = N, h = N})
}
```
Note that collisions of type `slide` and `bounce` have some additional fields. They are described
on each response's section above.
Most of this info is useful only if you are doing semi-advanced stuff with collisions, but they could have some uses.
For example, `cols[i].normal` could be used to "detect if a player is on ground or not". `cols[i].touch` could be used to
"spawn a puff of dust when a player touches ground after a fall", and so on.
## Intermediate API - Querying the world
The following methods are not required for basic usage of bump.lua, but are quite handy, and you would be missing out some
nice features of this lib if you were not using it.
Sometimes it is desirable to know "which items are in a certain area". This is called "querying the world".
Bump allows querying the world via a point, a rectangular zone, and a straight line segment.
This makes it useful not only as a collision detection library, but also as a lightweight spatial dictionary. In particular,
you can use bump to "only draw the things that are needed" on the screen. In order to do this, you would have to add all your
"visible" objects into bump, even if they don't collide with anything (this is usually OK, just ignore them with your filters when
you do the collisions).
### Querying with a point
``` lua
local items, len = world:queryPoint(x,y, filter)
```
Returns the items that touch a given point.
It is useful for things like clicking with the mouse and getting the items affected.
* `x,y` are the coordinates of the point that is being checked
* `items` is the list items from the ones inserted on the world (like `player`) that contain the point `x,y`.
If no items touch the point, then `items` will be an empty table. If not empty, then the order of these items is random.
* `filter` is an optional function. It takes one parameter (an item). `queryPoint` will not return the items that return
`false` or `nil` on `filter(item)`. By default, all items touched by the point are returned.
* `len` is the length of the items list. It is equivalent to `#items`, but it's slightly faster to use `len` instead.
### Querying with a rectangle
``` lua
local items, len = world:queryRect(l,t,w,h, filter)
```
Returns the items that touch a given rectangle.
Useful for things like selecting what to display on the screen, as mentioned above, or selecting a group of units with the mouse in a strategy game.
* `l,t,w,h` is a rectangle. The items that intersect with it will be returned.
* `filter` is an optional function. When provided, it is used to "filter out" which items are returned - if `filter(item)` returns
`false` or `nil`, that item is ignored. By default, all items are included.
* `items` is a list of items, like in `world:queryPoint`. But instead of for a point `x,y` for a rectangle `l,t,w,h`.
* `len` is equivalent to `#items`
### Querying with a segment
``` lua
local items, len = world:querySegment(x1,y1,x2,y2,filter)
```
Returns the items that touch a segment.
It's useful for things like line-of-sight or modelling bullets or lasers.
* `x1,y1,x2,y2` are the start and end coordinates of the segment.
* `filter` is an optional function. When provided, it is used to "filter out" which items are returned - if `filter(item)` returns
`false` or `nil`, that item is ignored. By default, all items are included.
* `items` is a list of items, similar to `world:queryPoint`, intersecting with the given segment. The difference is that
in `world:querySegment` the items are sorted by proximity. The ones closest to `x1,y1` appear first, while the ones farther
away appear later.
* `len` is equivalent to `#items`.
### Querying with a segment (with more detailed info)
``` lua
local itemInfo, len = world:querySegmentWithCoords(x1,y1,x2,y2)
```
An extended version of `world:querySegment` which returns the collision points of the segment with the items,
in addition to the items.
It is useful if you need to **actually show** the lasers/bullets or if you need to show some impact effects (i.e. spawning some particles
where a bullet hits a wall). If you don't need the actual points of contact between the segment and the bounding rectangles, use
`world:querySegment`, since it's faster.
* `x1,y1,x2,y2,filter` same as in `world:querySegment`.
* `itemInfo` is a list of tables. Each element in the table has the following elements: `item`, `x1`, `y1`, `x2`, `y2`, `t0` and `t1`.
* `info.item` is the item being intersected by the segment.
* `info.x1,info.y1` are the coordinates of the first intersection between `item` and the segment.
* `info.x2,info.y2` are the coordinates of the second intersection between `item` and the segment.
* `info.ti1` & `info.ti2` are numbers between 0 and 1 which say "how far from the starting point of the segment did the impact happen".
* `len` is equivalent to `#itemInfo`.
Most people will only need `info.item`, `info.x1` and `info.y1`. `info.x2` and `info.y2` are useful if you also need to show "the exit point
of a shoot", for example. `info.ti1` and `info.ti2` give an idea about the distance to the origin, so they can be used for things like
calculating the intensity of a shooting that becomes weaker with distance.
## Advanced API
The following methods are advanced and/or used internally by the library; most people will not need them.
``` lua
local result = world:hasItem(item)
```
Returns whether the world contains the given item or not. This function does not throw an error if `item` is not included in `world`; it just returns `false`.
``` lua
local count = world:countItems()
```
Returns the number of items inserted in the world. Useful for debugging.
``` lua
local items, len = world:getItems()
```
Builds and returns an array containing all the items in the world (as well as its length). This can be useful if you want to draw or update all the items in the world, without
doing any queries. Notice that the order in which the items will be returned is non-deterministic.
``` lua
local x,y,w,h = world:getRect(item)
```
Given an item, obtain the coordinates of its bounding rect. Useful for debugging/testing things.
``` lua
local cell_count = world:countCells()
```
Returns the number of cells being used. Useful for testing/debugging.
``` lua
local cx,cy = world:toCell(x,y)
```
Given a point, return the coordinates of the cell that contains it using the world's `cellSize`. Useful mostly for debugging bump, or drawing
debug info.
``` lua
local x,y = world:toWorld(x,y)
```
The inverse of `world:toCell`. Given the coordinates of a cell, return the coordinates of its main corner (top-left in LÖVE and Corona SDK, bottom-left in Cocos2d-x) in the game world.
``` lua
local cols, len = world:project(item, x,y,w,h, goalX, goalY, filter)
```
Moves a the given imaginary rectangle towards goalX and goalY, providing a list of collisions as they happen *in that straight path*.
This method is useful mostly when creating new collision responses, although it could be also used as a query method.
You could use this method to implement your own collision response algorithm (this was the only way to
do it in previous versions of bump)
```lua
bump.responses.touch
bump.responses.cross
bump.responses.slide
bump.responses.bounce
```
These are the functions bump uses to resolve collisions by default. You can use these functions' source as a base to build your own response function, if you feel adventurous.
```lua
world:addResponse(name, response)
```
This is how you register a new type of response in the world. All worlds come with the 4 pre-defined responses already installed, but you can add your own: if you register the
response `'foo'`, if your filter returns `'foo'` in a collision your world will handle it with `response`. This, however, is advanced stuff, and you
will have to read the source code of the default responses in order to know how to do that.
```lua
bump.rect.getNearestCorner
bump.rect.getSegmentIntersectionIndices
bump.rect.getDiff
bump.rect.containsPoint
bump.rect.isIntersecting
bump.rect.getSquareDistance
bump.rect.detectCollision
```
bump.lua comes with some rectangle-related functions in the `bump.rect` namespace. These are **not** part of the official API and can change at any moment. However, feel free to
use them if you are implementing your own collision responses.
## Installation
Just copy the bump.lua file wherever you want it. Then require it where you need it:
``` lua
local bump = require 'bump'
```
If you copied bump.lua to a file not accessible from the root folder (for example a lib folder), change the code accordingly:
``` lua
local bump = require 'lib.bump'
```
Please make sure that you read the license, too (for your convenience it's now included at the beginning of the bump.lua file.
## License
bump.lua is licensed under the MIT license.
## Specs
Specs for this project can be run using [busted](http://olivinelabs.com/busted).
## Changelog
See CHANGELOG.md for details

773
thirdparty/bump.lua/bump.lua vendored Normal file
View file

@ -0,0 +1,773 @@
local bump = {
_VERSION = 'bump v3.1.7',
_URL = 'https://github.com/kikito/bump.lua',
_DESCRIPTION = 'A collision detection library for Lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota
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.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsRect(x,y,w,h)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Rectangle functions
------------------------------------------
local function rect_getNearestCorner(x,y,w,h, px, py)
return nearest(px, x, x+w), nearest(py, y, y+h)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the rect
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx, dy = x2-x1, y2-y1
local nx, ny
local nx1, ny1, nx2, ny2 = 0,0,0,0
local p, q, r
for side = 1,4 do
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
end
if p == 0 then
if q <= 0 then return nil end
else
r = q / p
if p < 0 then
if r > ti2 then return nil
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
end
else -- p > 0
if r < ti1 then return nil
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
end
end
end
end
return ti1,ti2, nx1,ny1, nx2,ny2
end
-- Calculates the minkowsky difference between 2 rects, which is another rect
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
return x2 - x1 - w1,
y2 - y1 - h1,
w1 + w2,
h1 + h2
end
local function rect_containsPoint(x,y,w,h, px,py)
return px - x > DELTA and py - y > DELTA and
x + w - px > DELTA and y + h - py > DELTA
end
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
return dx*dx + dy*dy
end
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
goalX = goalX or x1
goalY = goalY or y1
local dx, dy = goalX - x1, goalY - y1
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
local overlaps, ti, nx, ny
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
ti = -wi * hi -- ti is the negative area of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
-- item tunnels into other
if ti1
and ti1 < 1
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
and (0 < ti1 + DELTA
or 0 == ti1 and ti2 > 0)
then
ti, nx, ny = ti1, nx1, ny1
overlaps = false
end
end
if not ti then return end
local tx, ty
if overlaps then
if dx == 0 and dy == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
if abs(px) < abs(py) then py = 0 else px = 0 end
nx, ny = sign(px), sign(py)
tx, ty = x1 + px, y1 + py
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
if not ti1 then return end
tx, ty = x1 + dx * ti1, y1 + dy * ti1
end
else -- tunnel
tx, ty = x1 + dx * ti, y1 + dy * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy},
normal = {x = nx, y = ny},
touch = {x = tx, y = ty},
itemRect = {x = x1, y = y1, w = w1, h = h1},
otherRect = {x = x2, y = y2, w = w2, h = h2}
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy)
return (cx - 1)*cellSize, (cy-1)*cellSize
end
local function grid_toCell(cellSize, x, y)
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local cx,cy = cx1,cy1
f(cx, cy)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) > 1 do
if tx < ty then
tx, cx = tx + dx, cx + stepX
f(cx, cy)
else
-- Addition: include both cells when going through corners
if tx == ty then f(cx + stepX, cy) end
ty, cy = ty + dy, cy + stepY
f(cx, cy)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
end
local function grid_toCellRect(cellSize, x,y,w,h)
local cx,cy = grid_toCell(cellSize, x, y)
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
return cx, cy, cr - cx + 1, cb - cy + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
return col.touch.x, col.touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x ~= 0 then
goalX = tch.x
else
goalY = tch.y
end
end
col.slide = {x = goalX, y = goalY}
x,y = tch.x, tch.y
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local tx, ty = tch.x, tch.y
local bx, by = tx, ty
if move.x ~= 0 or move.y ~= 0 then
local bnx, bny = goalX - tx, goalY - ty
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
bx, by = tx + bnx, ty + bny
end
col.bounce = {x = bx, y = by}
x,y = tch.x, tch.y
goalX, goalY = bx, by
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b) return a.weight < b.weight end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy)
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
local row = self.rows[cy]
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
local cell = row[cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy)
local row = self.rows[cy]
if not row or not row[cx] or not row[cx].items[item] then return false end
local cell = row[cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
local items_dict = {}
for cy=ct,ct+ch-1 do
local row = self.rows[cy]
if row then
for cx=cl,cl+cw-1 do
local cell = row[cx]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
local row = self.rows[cy]
if not row then return end
local cell = row[cx]
if not cell or visited[cell] then return end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
local visited, itemInfo, itemInfoLen = {},{},0
for i=1,len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
rect = self.rects[item]
l,t,w,h = rect.x,rect.y,rect.w,rect.h
ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:project(item, x,y,w,h, goalX, goalY, filter)
assertIsRect(x,y,w,h)
goalX = goalX or x
goalY = goalY or y
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then visited[item] = true end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
local tl, tt = min(goalX, x), min(goalY, y)
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
local tw, th = tr-tl, tb-tt
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
for other,_ in pairs(dictItemsInCellRect) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,ow,oh = self:getRect(other)
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _,row in pairs(self.rows) do
for _,_ in pairs(row) do
count = count + 1
end
end
return count
end
function World:hasItem(item)
return not not self.rects[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.rects) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.rects) do len = len + 1 end
return len
end
function World:getRect(item)
local rect = self.rects[item]
if not rect then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
end
return rect.x, rect.y, rect.w, rect.h
end
function World:toWorld(cx, cy)
return grid_toWorld(self.cellSize, cx, cy)
end
function World:toCell(x,y)
return grid_toCell(self.cellSize, x, y)
end
--- Query methods
function World:queryRect(x,y,w,h, filter)
assertIsRect(x,y,w,h)
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y, filter)
local cx,cy = self:toCell(x,y)
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local items = {}
for i=1, len do
items[i] = itemInfo[i].item
end
return items, len
end
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local dx, dy = x2-x1, y2-y1
local info, ti1, ti2
for i=1, len do
info = itemInfo[i]
ti1 = info.ti1
ti2 = info.ti2
info.weight = nil
info.x1 = x1 + dx * ti1
info.y1 = y1 + dy * ti1
info.x2 = x1 + dx * ti2
info.y2 = y1 + dy * ti2
end
return itemInfo, len
end
--- Main methods
function World:add(item, x,y,w,h)
local rect = self.rects[item]
if rect then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsRect(x,y,w,h)
self.rects[item] = {x=x,y=y,w=w,h=h}
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
addItemToCell(self, item, cx, cy)
end
end
return item
end
function World:remove(item)
local x,y,w,h = self:getRect(item)
self.rects[item] = nil
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
removeItemFromCell(self, item, cx, cy)
end
end
end
function World:update(item, x2,y2,w2,h2)
local x1,y1,w1,h1 = self:getRect(item)
w2,h2 = w2 or w1, h2 or h1
assertIsRect(x2,y2,w2,h2)
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
local cyOut
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy)
end
end
end
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy)
end
end
end
end
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
end
end
function World:move(item, goalX, goalY, filter)
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
self:update(item, actualX, actualY)
return actualX, actualY, cols, len
end
function World:check(item, goalX, goalY, filter)
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then return false end
return filter(itm, other)
end
local cols, len = {}, 0
local x,y,w,h = self:getRect(item)
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, projected_cols, projected_len = response(
self,
col,
x, y, w, h,
goalX, goalY,
visitedFilter
)
end
return goalX, goalY, cols, len
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
rects = {},
rows = {},
nonEmptyCells = {},
responses = {}
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.rect = {
getNearestCorner = rect_getNearestCorner,
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
getDiff = rect_getDiff,
containsPoint = rect_containsPoint,
isIntersecting = rect_isIntersecting,
getSquareDistance = rect_getSquareDistance,
detectCollision = rect_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

20
thirdparty/inspect.lua/MIT-LICENSE.txt vendored Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2013 Enrique García Cota
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.

266
thirdparty/inspect.lua/README.md vendored Normal file
View file

@ -0,0 +1,266 @@
inspect.lua
===========
This library transforms any Lua value into a human-readable representation. It is especially useful for debugging errors in tables.
The objective here is human understanding (i.e. for debugging), not serialization or compactness.
Examples of use
===============
`inspect` has the following declaration: `local str = inspect(value, <options>)`.
`value` can be any Lua value.
`inspect` transforms simple types (like strings or numbers) into strings.
```lua
assert(inspect(1) == "1")
assert(inspect("Hello") == '"Hello"')
```
Tables, on the other hand, are rendered in a way a human can read easily.
"Array-like" tables are rendered horizontally:
```lua
assert(inspect({1,2,3,4}) == "{ 1, 2, 3, 4 }")
```
"Dictionary-like" tables are rendered with one element per line:
```lua
assert(inspect({a=1,b=2}) == [[{
a = 1,
b = 2
}]])
```
The keys will be sorted alphanumerically when possible.
"Hybrid" tables will have the array part on the first line, and the dictionary part just below them:
```lua
assert(inspect({1,2,3,b=2,a=1}) == [[{ 1, 2, 3,
a = 1,
b = 2
}]])
```
Subtables are indented with two spaces per level.
```lua
assert(inspect({a={b=2}}) == [[{
a = {
b = 2
}
}]])
```
Functions, userdata and any other custom types from Luajit are simply as `<function x>`, `<userdata x>`, etc.:
```lua
assert(inspect({ f = print, u = some_user_data, thread = a_thread} ) == [[{
f = <function 1>,
u = <userdata 1>,
thread = <thread 1>
}]])
```
If the table has a metatable, inspect will include it at the end, in a special field called `<metatable>`:
```lua
assert(inspect(setmetatable({a=1}, {b=2}) == [[{
a = 1
<metatable> = {
b = 2
}
}]]))
```
`inspect` can handle tables with loops inside them. It will print `<id>` right before the table is printed out the first time, and replace the whole table with `<table id>` from then on, preventing infinite loops.
```lua
local a = {1, 2}
local b = {3, 4, a}
a[3] = b -- a references b, and b references a
assert(inspect(a) == "<1>{ 1, 2, { 3, 4, <table 1> } }")
```
Notice that since both `a` appears more than once in the expression, it is prefixed by `<1>` and replaced by `<table 1>` every time it appears later on.
### options
`inspect` has a second parameter, called `options`. It is not mandatory, but when it is provided, it must be a table.
#### options.depth
`options.depth` sets the maximum depth that will be printed out.
When the max depth is reached, `inspect` will stop parsing tables and just return `{...}`:
```lua
local t5 = {a = {b = {c = {d = {e = 5}}}}}
assert(inspect(t5, {depth = 4}) == [[{
a = {
b = {
c = {
d = {...}
}
}
}
}]])
assert(inspect(t5, {depth = 2}) == [[{
a = {
b = {...}
}
}]])
```
`options.depth` defaults to infinite (`math.huge`).
#### options.newline & options.indent
These are the strings used by `inspect` to respectively add a newline and indent each level of a table.
By default, `options.newline` is `"\n"` and `options.indent` is `" "` (two spaces).
``` lua
local t = {a={b=1}}
assert(inspect(t) == [[{
a = {
b = 1
}
}]])
assert(inspect(t, {newline='@', indent="++"}), "{@++a = {@++++b = 1@++}@}"
```
#### options.process
`options.process` is a function which allow altering the passed object before transforming it into a string.
A typical way to use it would be to remove certain values so that they don't appear at all.
`options.process` has the following signature:
``` lua
local processed_item = function(item, path)
```
* `item` is either a key or a value on the table, or any of its subtables
* `path` is an array-like table built with all the keys that have been used to reach `item`, from the root.
* For values, it is just a regular list of keys. For example, to reach the 1 in `{a = {b = 1}}`, the `path`
will be `{'a', 'b'}`
* For keys, the special value `inspect.KEY` is inserted. For example, to reach the `c` in `{a = {b = {c = 1}}}`,
the path will be `{'a', 'b', 'c', inspect.KEY }`
* For metatables, the special value `inspect.METATABLE` is inserted. For `{a = {b = 1}}}`, the path
`{'a', {b = 1}, inspect.METATABLE}` means "the metatable of the table `{b = 1}`".
* `processed_item` is the value returned by `options.process`. If it is equal to `item`, then the inspected
table will look unchanged. If it is different, then the table will look different; most notably, if it's `nil`,
the item will dissapear on the inspected table.
#### Examples
Remove a particular metatable from the result:
``` lua
local t = {1,2,3}
local mt = {b = 2}
setmetatable(t, mt)
local remove_mt = function(item)
if item ~= mt then return item end
end
-- mt does not appear
assert(inspect(t, {process = remove_mt}) == "{ 1, 2, 3 }")
```
The previous example only works for a particular metatable. If you want to make *all* metatables, you can use the `path` parameter to check
wether the last element is `inspect.METATABLE`, and return `nil` instead of the item:
``` lua
local t, mt = ... -- (defined as before)
local remove_all_metatables = function(item, path)
if path[#path] ~= inspect.METATABLE then return item end
end
assert(inspect(t, {process = remove_all_metatables}) == "{ 1, 2, 3 }")
```
Filter a value:
```lua
local anonymize_password = function(item, path)
if path[#path] == 'password' then return "XXXX" end
return item
end
local info = {user = 'peter', password = 'secret'}
assert(inspect(info, {process = anonymize_password}) == [[{
password = "XXXX",
user = "peter"
}]])
```
Gotchas / Warnings
==================
This method is *not* appropriate for saving/restoring tables. It is meant to be used by the programmer mainly while debugging a program.
Installation
============
If you are using luarocks, just run
luarocks install inspect
Otherwise, you can just copy the inspect.lua file somewhere in your projects (maybe inside a /lib/ folder) and require it accordingly.
Remember to store the value returned by require somewhere! (I suggest a local variable named inspect, although others might like table.inspect)
local inspect = require 'inspect'
-- or --
local inspect = require 'lib.inspect'
Also, make sure to read the license; the text of that license file must appear somewhere in your projects' files. For your convenience, it's included at the begining of inspect.lua.
Contributing
============
This project uses [Teal](https://github.com/teal-language/tl), a typed dialect of Lua (which generates plain lua files too)
If you want to send a pull request to this project, first of all, thank you! You will need to install the dependencies. You can install all of them by running:
```
make dev
```
When writing your PR, please make your modifications on the `inspect.tl` file and then generate the `inspect.lua` file from it. You will probably want to make sure that the tests are still
working (github should run them from you, but they should run very fast). You can do both things in one go by just invoking
```
make
```
This will generate `inspect.lua`, check it with [luacheck](https://github.com/lunarmodules/luacheck) and then launch [busted](http://olivinelabs.com/busted/) to run the specs.
If you are sending a pull request, you might want to add some specs in the `specs` folder.
Change log
==========
Read it on the CHANGELOG.md file

377
thirdparty/inspect.lua/inspect.lua vendored Normal file
View file

@ -0,0 +1,377 @@
local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local type = type
local inspect = { Options = {} }
inspect._VERSION = 'inspect.lua 3.1.0'
inspect._URL = 'http://github.com/kikito/inspect.lua'
inspect._DESCRIPTION = 'human-readable representations of tables'
inspect._LICENSE = [[
MIT LICENSE
Copyright (c) 2022 Enrique García Cota
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.
]]
inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end })
inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end })
local tostring = tostring
local rep = string.rep
local match = string.match
local char = string.char
local gsub = string.gsub
local fmt = string.format
local sbavailable, stringbuffer = pcall(require, "string.buffer")
local buffnew
local puts
local render
if sbavailable then
buffnew = stringbuffer.new
puts = function(buf, str)
buf:put(str)
end
render = function(buf)
return buf:get()
end
else
buffnew = function()
return { n = 0 }
end
puts = function(buf, str)
buf.n = buf.n + 1
buf[buf.n] = str
end
render = function(buf)
return table.concat(buf)
end
end
local _rawget
if rawget then
_rawget = rawget
else
_rawget = function(t, k) return t[k] end
end
local function rawpairs(t)
return next, t, nil
end
local function smartQuote(str)
if match(str, '"') and not match(str, "'") then
return "'" .. str .. "'"
end
return '"' .. gsub(str, '"', '\\"') .. '"'
end
local shortControlCharEscapes = {
["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127",
}
local longControlCharEscapes = { ["\127"] = "\127" }
for i = 0, 31 do
local ch = char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = "\\" .. i
longControlCharEscapes[ch] = fmt("\\%03d", i)
end
end
local function escape(str)
return (gsub(gsub(gsub(str, "\\", "\\\\"),
"(%c)%f[0-9]", longControlCharEscapes),
"%c", shortControlCharEscapes))
end
local luaKeywords = {}
for k in ([[ and break do else elseif end false for function goto if
in local nil not or repeat return then true until while
]]):gmatch('%w+') do
luaKeywords[k] = true
end
local function isIdentifier(str)
return type(str) == "string" and
not not str:match("^[_%a][_%a%d]*$") and
not luaKeywords[str]
end
local flr = math.floor
local function isSequenceKey(k, sequenceLength)
return type(k) == "number" and
flr(k) == k and
1 <= (k) and
k <= sequenceLength
end
local defaultTypeOrders = {
['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
['function'] = 5, ['userdata'] = 6, ['thread'] = 7,
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
if ta == tb and (ta == 'string' or ta == 'number') then
return (a) < (b)
end
local dta = defaultTypeOrders[ta] or 100
local dtb = defaultTypeOrders[tb] or 100
return dta == dtb and ta < tb or dta < dtb
end
local function getKeys(t)
local seqLen = 1
while _rawget(t, seqLen) ~= nil do
seqLen = seqLen + 1
end
seqLen = seqLen - 1
local keys, keysLen = {}, 0
for k in rawpairs(t) do
if not isSequenceKey(k, seqLen) then
keysLen = keysLen + 1
keys[keysLen] = k
end
end
table.sort(keys, sortKeys)
return keys, keysLen, seqLen
end
local function countCycles(x, cycles, depth)
if type(x) == "table" then
if cycles[x] then
cycles[x] = cycles[x] + 1
else
cycles[x] = 1
if depth > 0 then
for k, v in rawpairs(x) do
countCycles(k, cycles, depth - 1)
countCycles(v, cycles, depth - 1)
end
countCycles(getmetatable(x), cycles, depth - 1)
end
end
end
end
local function makePath(path, a, b)
local newPath = {}
local len = #path
for i = 1, len do newPath[i] = path[i] end
newPath[len + 1] = a
newPath[len + 2] = b
return newPath
end
local function processRecursive(process,
item,
path,
visited)
if item == nil then return nil end
if visited[item] then return visited[item] end
local processed = process(item, path)
if type(processed) == "table" then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k, v in rawpairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= 'table' then mt = nil end
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
local Inspector = {}
local Inspector_mt = { __index = Inspector }
local function tabify(inspector)
puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
end
function Inspector:getId(v)
local id = self.ids[v]
local ids = self.ids
if not id then
local tv = type(v)
id = (ids[tv] or 0) + 1
ids[v], ids[tv] = id, id
end
return tostring(id)
end
function Inspector:putValue(v)
local buf = self.buf
local tv = type(v)
if tv == 'string' then
puts(buf, smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
tv == 'cdata' or tv == 'ctype' then
puts(buf, tostring(v))
elseif tv == 'table' and not self.ids[v] then
local t = v
if t == inspect.KEY or t == inspect.METATABLE then
puts(buf, tostring(t))
elseif self.level >= self.depth then
puts(buf, '{...}')
else
if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end
local keys, keysLen, seqLen = getKeys(t)
puts(buf, '{')
self.level = self.level + 1
for i = 1, seqLen + keysLen do
if i > 1 then puts(buf, ',') end
if i <= seqLen then
puts(buf, ' ')
self:putValue(t[i])
else
local k = keys[i - seqLen]
tabify(self)
if isIdentifier(k) then
puts(buf, k)
else
puts(buf, "[")
self:putValue(k)
puts(buf, "]")
end
puts(buf, ' = ')
self:putValue(t[k])
end
end
local mt = getmetatable(t)
if type(mt) == 'table' then
if seqLen + keysLen > 0 then puts(buf, ',') end
tabify(self)
puts(buf, '<metatable> = ')
self:putValue(mt)
end
self.level = self.level - 1
if keysLen > 0 or type(mt) == 'table' then
tabify(self)
elseif seqLen > 0 then
puts(buf, ' ')
end
puts(buf, '}')
end
else
puts(buf, fmt('<%s %d>', tv, self:getId(v)))
end
end
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or (math.huge)
local newline = options.newline or '\n'
local indent = options.indent or ' '
local process = options.process
if process then
root = processRecursive(process, root, {}, {})
end
local cycles = {}
countCycles(root, cycles, depth)
local inspector = setmetatable({
buf = buffnew(),
ids = {},
cycles = cycles,
depth = depth,
level = 0,
newline = newline,
indent = indent,
}, Inspector_mt)
inspector:putValue(root)
return render(inspector.buf)
end
setmetatable(inspect, {
__call = function(_, root, options)
return inspect.inspect(root, options)
end,
})
return inspect

20
thirdparty/lume/LICENSE vendored Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2020 rxi
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.

495
thirdparty/lume/README.md vendored Normal file
View file

@ -0,0 +1,495 @@
# Lume
A collection of functions for Lua, geared towards game development.
## Installation
The [lume.lua](lume.lua?raw=1) file should be dropped into an existing project
and required by it:
```lua
lume = require "lume"
```
## Function Reference
#### lume.clamp(x, min, max)
Returns the number `x` clamped between the numbers `min` and `max`
#### lume.round(x [, increment])
Rounds `x` to the nearest integer; rounds away from zero if we're midway
between two integers. If `increment` is set then the number is rounded to the
nearest increment.
```lua
lume.round(2.3) -- Returns 2
lume.round(123.4567, .1) -- Returns 123.5
```
#### lume.sign(x)
Returns `1` if `x` is 0 or above, returns `-1` when `x` is negative.
#### lume.lerp(a, b, amount)
Returns the linearly interpolated number between `a` and `b`, `amount` should
be in the range of 0 - 1; if `amount` is outside of this range it is clamped.
```lua
lume.lerp(100, 200, .5) -- Returns 150
```
#### lume.smooth(a, b, amount)
Similar to `lume.lerp()` but uses cubic interpolation instead of linear
interpolation.
#### lume.pingpong(x)
Ping-pongs the number `x` between 0 and 1.
#### lume.distance(x1, y1, x2, y2 [, squared])
Returns the distance between the two points. If `squared` is true then the
squared distance is returned -- this is faster to calculate and can still be
used when comparing distances.
#### lume.angle(x1, y1, x2, y2)
Returns the angle between the two points.
#### lume.vector(angle, magnitude)
Given an `angle` and `magnitude`, returns a vector.
```lua
local x, y = lume.vector(0, 10) -- Returns 10, 0
```
#### lume.random([a [, b]])
Returns a random number between `a` and `b`. If only `a` is supplied a number
between `0` and `a` is returned. If no arguments are supplied a random number
between `0` and `1` is returned.
#### lume.randomchoice(t)
Returns a random value from array `t`. If the array is empty an error is
raised.
```lua
lume.randomchoice({true, false}) -- Returns either true or false
```
#### lume.weightedchoice(t)
Takes the argument table `t` where the keys are the possible choices and the
value is the choice's weight. A weight should be 0 or above, the larger the
number the higher the probability of that choice being picked. If the table is
empty, a weight is below zero or all the weights are 0 then an error is raised.
```lua
lume.weightedchoice({ ["cat"] = 10, ["dog"] = 5, ["frog"] = 0 })
-- Returns either "cat" or "dog" with "cat" being twice as likely to be chosen.
```
#### lume.isarray(x)
Returns `true` if `x` is an array -- the value is assumed to be an array if it
is a table which contains a value at the index `1`. This function is used
internally and can be overridden if you wish to use a different method to detect
arrays.
#### lume.push(t, ...)
Pushes all the given values to the end of the table `t` and returns the pushed
values. Nil values are ignored.
```lua
local t = { 1, 2, 3 }
lume.push(t, 4, 5) -- `t` becomes { 1, 2, 3, 4, 5 }
```
#### lume.remove(t, x)
Removes the first instance of the value `x` if it exists in the table `t`.
Returns `x`.
```lua
local t = { 1, 2, 3 }
lume.remove(t, 2) -- `t` becomes { 1, 3 }
```
#### lume.clear(t)
Nils all the values in the table `t`, this renders the table empty. Returns
`t`.
```lua
local t = { 1, 2, 3 }
lume.clear(t) -- `t` becomes {}
```
#### lume.extend(t, ...)
Copies all the fields from the source tables to the table `t` and returns `t`.
If a key exists in multiple tables the right-most table's value is used.
```lua
local t = { a = 1, b = 2 }
lume.extend(t, { b = 4, c = 6 }) -- `t` becomes { a = 1, b = 4, c = 6 }
```
#### lume.shuffle(t)
Returns a shuffled copy of the array `t`.
#### lume.sort(t [, comp])
Returns a copy of the array `t` with all its items sorted. If `comp` is a
function it will be used to compare the items when sorting. If `comp` is a
string it will be used as the key to sort the items by.
```lua
lume.sort({ 1, 4, 3, 2, 5 }) -- Returns { 1, 2, 3, 4, 5 }
lume.sort({ {z=2}, {z=3}, {z=1} }, "z") -- Returns { {z=1}, {z=2}, {z=3} }
lume.sort({ 1, 3, 2 }, function(a, b) return a > b end) -- Returns { 3, 2, 1 }
```
#### lume.array(...)
Iterates the supplied iterator and returns an array filled with the values.
```lua
lume.array(string.gmatch("Hello world", "%a+")) -- Returns {"Hello", "world"}
```
#### lume.each(t, fn, ...)
Iterates the table `t` and calls the function `fn` on each value followed by
the supplied additional arguments; if `fn` is a string the method of that name
is called for each value. The function returns `t` unmodified.
```lua
lume.each({1, 2, 3}, print) -- Prints "1", "2", "3" on separate lines
lume.each({a, b, c}, "move", 10, 20) -- Does x:move(10, 20) on each value
```
#### lume.map(t, fn)
Applies the function `fn` to each value in table `t` and returns a new table
with the resulting values.
```lua
lume.map({1, 2, 3}, function(x) return x * 2 end) -- Returns {2, 4, 6}
```
#### lume.all(t [, fn])
Returns true if all the values in `t` table are true. If a `fn` function is
supplied it is called on each value, true is returned if all of the calls to
`fn` return true.
```lua
lume.all({1, 2, 1}, function(x) return x == 1 end) -- Returns false
```
#### lume.any(t [, fn])
Returns true if any of the values in `t` table are true. If a `fn` function is
supplied it is called on each value, true is returned if any of the calls to
`fn` return true.
```lua
lume.any({1, 2, 1}, function(x) return x == 1 end) -- Returns true
```
#### lume.reduce(t, fn [, first])
Applies `fn` on two arguments cumulative to the items of the array `t`, from
left to right, so as to reduce the array to a single value. If a `first` value
is specified the accumulator is initialised to this, otherwise the first value
in the array is used. If the array is empty and no `first` value is specified
an error is raised.
```lua
lume.reduce({1, 2, 3}, function(a, b) return a + b end) -- Returns 6
```
#### lume.unique(t)
Returns a copy of the `t` array with all the duplicate values removed.
```lua
lume.unique({2, 1, 2, "cat", "cat"}) -- Returns {1, 2, "cat"}
```
#### lume.filter(t, fn [, retainkeys])
Calls `fn` on each value of `t` table. Returns a new table with only the values
where `fn` returned true. If `retainkeys` is true the table is not treated as
an array and retains its original keys.
```lua
lume.filter({1, 2, 3, 4}, function(x) return x % 2 == 0 end) -- Returns {2, 4}
```
#### lume.reject(t, fn [, retainkeys])
The opposite of `lume.filter()`: Calls `fn` on each value of `t` table; returns
a new table with only the values where `fn` returned false. If `retainkeys` is
true the table is not treated as an array and retains its original keys.
```lua
lume.reject({1, 2, 3, 4}, function(x) return x % 2 == 0 end) -- Returns {1, 3}
```
#### lume.merge(...)
Returns a new table with all the given tables merged together. If a key exists
in multiple tables the right-most table's value is used.
```lua
lume.merge({a=1, b=2, c=3}, {c=8, d=9}) -- Returns {a=1, b=2, c=8, d=9}
```
#### lume.concat(...)
Returns a new array consisting of all the given arrays concatenated into one.
```lua
lume.concat({1, 2}, {3, 4}, {5, 6}) -- Returns {1, 2, 3, 4, 5, 6}
```
#### lume.find(t, value)
Returns the index/key of `value` in `t`. Returns `nil` if that value does not
exist in the table.
```lua
lume.find({"a", "b", "c"}, "b") -- Returns 2
```
#### lume.match(t, fn)
Returns the value and key of the value in table `t` which returns true when
`fn` is called on it. Returns `nil` if no such value exists.
```lua
lume.match({1, 5, 8, 7}, function(x) return x % 2 == 0 end) -- Returns 8, 3
```
#### lume.count(t [, fn])
Counts the number of values in the table `t`. If a `fn` function is supplied it
is called on each value, the number of times it returns true is counted.
```lua
lume.count({a = 2, b = 3, c = 4, d = 5}) -- Returns 4
lume.count({1, 2, 4, 6}, function(x) return x % 2 == 0 end) -- Returns 3
```
#### lume.slice(t [, i [, j]])
Mimics the behaviour of Lua's `string.sub`, but operates on an array rather
than a string. Creates and returns a new array of the given slice.
```lua
lume.slice({"a", "b", "c", "d", "e"}, 2, 4) -- Returns {"b", "c", "d"}
```
#### lume.first(t [, n])
Returns the first element of an array or nil if the array is empty. If `n` is
specificed an array of the first `n` elements is returned.
```lua
lume.first({"a", "b", "c"}) -- Returns "a"
```
#### lume.last(t [, n])
Returns the last element of an array or nil if the array is empty. If `n` is
specificed an array of the last `n` elements is returned.
```lua
lume.last({"a", "b", "c"}) -- Returns "c"
```
#### lume.invert(t)
Returns a copy of the table where the keys have become the values and the
values the keys.
```lua
lume.invert({a = "x", b = "y"}) -- returns {x = "a", y = "b"}
```
#### lume.pick(t, ...)
Returns a copy of the table filtered to only contain values for the given keys.
```lua
lume.pick({ a = 1, b = 2, c = 3 }, "a", "c") -- Returns { a = 1, c = 3 }
```
#### lume.keys(t)
Returns an array containing each key of the table.
#### lume.clone(t)
Returns a shallow copy of the table `t`.
#### lume.fn(fn, ...)
Creates a wrapper function around function `fn`, automatically inserting the
arguments into `fn` which will persist every time the wrapper is called. Any
arguments which are passed to the returned function will be inserted after the
already existing arguments passed to `fn`.
```lua
local f = lume.fn(print, "Hello")
f("world") -- Prints "Hello world"
```
#### lume.once(fn, ...)
Returns a wrapper function to `fn` which takes the supplied arguments. The
wrapper function will call `fn` on the first call and do nothing on any
subsequent calls.
```lua
local f = lume.once(print, "Hello")
f() -- Prints "Hello"
f() -- Does nothing
```
#### lume.memoize(fn)
Returns a wrapper function to `fn` where the results for any given set of
arguments are cached. `lume.memoize()` is useful when used on functions with
slow-running computations.
```lua
fib = lume.memoize(function(n) return n < 2 and n or fib(n-1) + fib(n-2) end)
```
#### lume.combine(...)
Creates a wrapper function which calls each supplied argument in the order they
were passed to `lume.combine()`; nil arguments are ignored. The wrapper
function passes its own arguments to each of its wrapped functions when it is
called.
```lua
local f = lume.combine(function(a, b) print(a + b) end,
function(a, b) print(a * b) end)
f(3, 4) -- Prints "7" then "12" on a new line
```
#### lume.call(fn, ...)
Calls the given function with the provided arguments and returns its values. If
`fn` is `nil` then no action is performed and the function returns `nil`.
```lua
lume.call(print, "Hello world") -- Prints "Hello world"
```
#### lume.time(fn, ...)
Inserts the arguments into function `fn` and calls it. Returns the time in
seconds the function `fn` took to execute followed by `fn`'s returned values.
```lua
lume.time(function(x) return x end, "hello") -- Returns 0, "hello"
```
#### lume.lambda(str)
Takes a string lambda and returns a function. `str` should be a list of
comma-separated parameters, followed by `->`, followed by the expression which
will be evaluated and returned.
```lua
local f = lume.lambda "x,y -> 2*x+y"
f(10, 5) -- Returns 25
```
#### lume.serialize(x)
Serializes the argument `x` into a string which can be loaded again using
`lume.deserialize()`. Only booleans, numbers, tables and strings can be
serialized. Circular references will result in an error; all nested tables are
serialized as unique tables.
```lua
lume.serialize({a = "test", b = {1, 2, 3}, false})
-- Returns "{[1]=false,["a"]="test",["b"]={[1]=1,[2]=2,[3]=3,},}"
```
#### lume.deserialize(str)
Deserializes a string created by `lume.serialize()` and returns the resulting
value. This function should not be run on an untrusted string.
```lua
lume.deserialize("{1, 2, 3}") -- Returns {1, 2, 3}
```
#### lume.split(str [, sep])
Returns an array of the words in the string `str`. If `sep` is provided it is
used as the delimiter, consecutive delimiters are not grouped together and will
delimit empty strings.
```lua
lume.split("One two three") -- Returns {"One", "two", "three"}
lume.split("a,b,,c", ",") -- Returns {"a", "b", "", "c"}
```
#### lume.trim(str [, chars])
Trims the whitespace from the start and end of the string `str` and returns the
new string. If a `chars` value is set the characters in `chars` are trimmed
instead of whitespace.
```lua
lume.trim(" Hello ") -- Returns "Hello"
```
#### lume.wordwrap(str [, limit])
Returns `str` wrapped to `limit` number of characters per line, by default
`limit` is `72`. `limit` can also be a function which when passed a string,
returns `true` if it is too long for a single line.
```lua
-- Returns "Hello world\nThis is a\nshort string"
lume.wordwrap("Hello world. This is a short string", 14)
```
#### lume.format(str [, vars])
Returns a formatted string. The values of keys in the table `vars` can be
inserted into the string by using the form `"{key}"` in `str`; numerical keys
can also be used.
```lua
lume.format("{b} hi {a}", {a = "mark", b = "Oh"}) -- Returns "Oh hi mark"
lume.format("Hello {1}!", {"world"}) -- Returns "Hello world!"
```
#### lume.trace(...)
Prints the current filename and line number followed by each argument separated
by a space.
```lua
-- Assuming the file is called "example.lua" and the next line is 12:
lume.trace("hello", 1234) -- Prints "example.lua:12: hello 1234"
```
#### lume.dostring(str)
Executes the lua code inside `str`.
```lua
lume.dostring("print('Hello!')") -- Prints "Hello!"
```
#### lume.uuid()
Generates a random UUID string; version 4 as specified in
[RFC 4122](http://www.ietf.org/rfc/rfc4122.txt).
#### lume.hotswap(modname)
Reloads an already loaded module in place, allowing you to immediately see the
effects of code changes without having to restart the program. `modname` should
be the same string used when loading the module with require(). In the case of
an error the global environment is restored and `nil` plus an error message is
returned.
```lua
lume.hotswap("lume") -- Reloads the lume module
assert(lume.hotswap("inexistant_module")) -- Raises an error
```
#### lume.ripairs(t)
Performs the same function as `ipairs()` but iterates in reverse; this allows
the removal of items from the table during iteration without any items being
skipped.
```lua
-- Prints "3->c", "2->b" and "1->a" on separate lines
for i, v in lume.ripairs({ "a", "b", "c" }) do
print(i .. "->" .. v)
end
```
#### lume.color(str [, mul])
Takes color string `str` and returns 4 values, one for each color channel (`r`,
`g`, `b` and `a`). By default the returned values are between 0 and 1; the
values are multiplied by the number `mul` if it is provided.
```lua
lume.color("#ff0000") -- Returns 1, 0, 0, 1
lume.color("rgba(255, 0, 255, .5)") -- Returns 1, 0, 1, .5
lume.color("#00ffff", 256) -- Returns 0, 256, 256, 256
lume.color("rgb(255, 0, 0)", 256) -- Returns 256, 0, 0, 256
```
#### lume.chain(value)
Returns a wrapped object which allows chaining of lume functions. The function
result() should be called at the end of the chain to return the resulting
value.
```lua
lume.chain({1, 2, 3, 4})
:filter(function(x) return x % 2 == 0 end)
:map(function(x) return -x end)
:result() -- Returns { -2, -4 }
```
The table returned by the `lume` module, when called, acts in the same manner
as calling `lume.chain()`.
```lua
lume({1, 2, 3}):each(print) -- Prints 1, 2 then 3 on separate lines
```
## Iteratee functions
Several lume functions allow a `table`, `string` or `nil` to be used in place
of their iteratee function argument. The functions that provide this behaviour
are: `map()`, `all()`, `any()`, `filter()`, `reject()`, `match()` and
`count()`.
If the argument is `nil` then each value will return itself.
```lua
lume.filter({ true, true, false, true }, nil) -- { true, true, true }
```
If the argument is a `string` then each value will be assumed to be a table,
and will return the value of the key which matches the string.
``` lua
local t = {{ z = "cat" }, { z = "dog" }, { z = "owl" }}
lume.map(t, "z") -- Returns { "cat", "dog", "owl" }
```
If the argument is a `table` then each value will return `true` or `false`,
depending on whether the values at each of the table's keys match the
collection's value's values.
```lua
local t = {
{ age = 10, type = "cat" },
{ age = 8, type = "dog" },
{ age = 10, type = "owl" },
}
lume.count(t, { age = 10 }) -- returns 2
```
## License
This library is free software; you can redistribute it and/or modify it under
the terms of the MIT license. See [LICENSE](LICENSE) for details.

780
thirdparty/lume/lume.lua vendored Normal file
View file

@ -0,0 +1,780 @@
--
-- lume
--
-- Copyright (c) 2020 rxi
--
-- 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.
--
local lume = { _version = "2.3.0" }
local pairs, ipairs = pairs, ipairs
local type, assert, unpack = type, assert, unpack or table.unpack
local tostring, tonumber = tostring, tonumber
local math_floor = math.floor
local math_ceil = math.ceil
local math_atan2 = math.atan2 or math.atan
local math_sqrt = math.sqrt
local math_abs = math.abs
local noop = function()
end
local identity = function(x)
return x
end
local patternescape = function(str)
return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
end
local absindex = function(len, i)
return i < 0 and (len + i + 1) or i
end
local iscallable = function(x)
if type(x) == "function" then return true end
local mt = getmetatable(x)
return mt and mt.__call ~= nil
end
local getiter = function(x)
if lume.isarray(x) then
return ipairs
elseif type(x) == "table" then
return pairs
end
error("expected table", 3)
end
local iteratee = function(x)
if x == nil then return identity end
if iscallable(x) then return x end
if type(x) == "table" then
return function(z)
for k, v in pairs(x) do
if z[k] ~= v then return false end
end
return true
end
end
return function(z) return z[x] end
end
function lume.clamp(x, min, max)
return x < min and min or (x > max and max or x)
end
function lume.round(x, increment)
if increment then return lume.round(x / increment) * increment end
return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
end
function lume.sign(x)
return x < 0 and -1 or 1
end
function lume.lerp(a, b, amount)
return a + (b - a) * lume.clamp(amount, 0, 1)
end
function lume.smooth(a, b, amount)
local t = lume.clamp(amount, 0, 1)
local m = t * t * (3 - 2 * t)
return a + (b - a) * m
end
function lume.pingpong(x)
return 1 - math_abs(1 - x % 2)
end
function lume.distance(x1, y1, x2, y2, squared)
local dx = x1 - x2
local dy = y1 - y2
local s = dx * dx + dy * dy
return squared and s or math_sqrt(s)
end
function lume.angle(x1, y1, x2, y2)
return math_atan2(y2 - y1, x2 - x1)
end
function lume.vector(angle, magnitude)
return math.cos(angle) * magnitude, math.sin(angle) * magnitude
end
function lume.random(a, b)
if not a then a, b = 0, 1 end
if not b then b = 0 end
return a + math.random() * (b - a)
end
function lume.randomchoice(t)
return t[math.random(#t)]
end
function lume.weightedchoice(t)
local sum = 0
for _, v in pairs(t) do
assert(v >= 0, "weight value less than zero")
sum = sum + v
end
assert(sum ~= 0, "all weights are zero")
local rnd = lume.random(sum)
for k, v in pairs(t) do
if rnd < v then return k end
rnd = rnd - v
end
end
function lume.isarray(x)
return type(x) == "table" and x[1] ~= nil
end
function lume.push(t, ...)
local n = select("#", ...)
for i = 1, n do
t[#t + 1] = select(i, ...)
end
return ...
end
function lume.remove(t, x)
local iter = getiter(t)
for i, v in iter(t) do
if v == x then
if lume.isarray(t) then
table.remove(t, i)
break
else
t[i] = nil
break
end
end
end
return x
end
function lume.clear(t)
local iter = getiter(t)
for k in iter(t) do
t[k] = nil
end
return t
end
function lume.extend(t, ...)
for i = 1, select("#", ...) do
local x = select(i, ...)
if x then
for k, v in pairs(x) do
t[k] = v
end
end
end
return t
end
function lume.shuffle(t)
local rtn = {}
for i = 1, #t do
local r = math.random(i)
if r ~= i then
rtn[i] = rtn[r]
end
rtn[r] = t[i]
end
return rtn
end
function lume.sort(t, comp)
local rtn = lume.clone(t)
if comp then
if type(comp) == "string" then
table.sort(rtn, function(a, b) return a[comp] < b[comp] end)
else
table.sort(rtn, comp)
end
else
table.sort(rtn)
end
return rtn
end
function lume.array(...)
local t = {}
for x in ... do t[#t + 1] = x end
return t
end
function lume.each(t, fn, ...)
local iter = getiter(t)
if type(fn) == "string" then
for _, v in iter(t) do v[fn](v, ...) end
else
for _, v in iter(t) do fn(v, ...) end
end
return t
end
function lume.map(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
local rtn = {}
for k, v in iter(t) do rtn[k] = fn(v) end
return rtn
end
function lume.all(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
for _, v in iter(t) do
if not fn(v) then return false end
end
return true
end
function lume.any(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
for _, v in iter(t) do
if fn(v) then return true end
end
return false
end
function lume.reduce(t, fn, first)
local started = first ~= nil
local acc = first
local iter = getiter(t)
for _, v in iter(t) do
if started then
acc = fn(acc, v)
else
acc = v
started = true
end
end
assert(started, "reduce of an empty table with no first value")
return acc
end
function lume.unique(t)
local rtn = {}
for k in pairs(lume.invert(t)) do
rtn[#rtn + 1] = k
end
return rtn
end
function lume.filter(t, fn, retainkeys)
fn = iteratee(fn)
local iter = getiter(t)
local rtn = {}
if retainkeys then
for k, v in iter(t) do
if fn(v) then rtn[k] = v end
end
else
for _, v in iter(t) do
if fn(v) then rtn[#rtn + 1] = v end
end
end
return rtn
end
function lume.reject(t, fn, retainkeys)
fn = iteratee(fn)
local iter = getiter(t)
local rtn = {}
if retainkeys then
for k, v in iter(t) do
if not fn(v) then rtn[k] = v end
end
else
for _, v in iter(t) do
if not fn(v) then rtn[#rtn + 1] = v end
end
end
return rtn
end
function lume.merge(...)
local rtn = {}
for i = 1, select("#", ...) do
local t = select(i, ...)
local iter = getiter(t)
for k, v in iter(t) do
rtn[k] = v
end
end
return rtn
end
function lume.concat(...)
local rtn = {}
for i = 1, select("#", ...) do
local t = select(i, ...)
if t ~= nil then
local iter = getiter(t)
for _, v in iter(t) do
rtn[#rtn + 1] = v
end
end
end
return rtn
end
function lume.find(t, value)
local iter = getiter(t)
for k, v in iter(t) do
if v == value then return k end
end
return nil
end
function lume.match(t, fn)
fn = iteratee(fn)
local iter = getiter(t)
for k, v in iter(t) do
if fn(v) then return v, k end
end
return nil
end
function lume.count(t, fn)
local count = 0
local iter = getiter(t)
if fn then
fn = iteratee(fn)
for _, v in iter(t) do
if fn(v) then count = count + 1 end
end
else
if lume.isarray(t) then
return #t
end
for _ in iter(t) do count = count + 1 end
end
return count
end
function lume.slice(t, i, j)
i = i and absindex(#t, i) or 1
j = j and absindex(#t, j) or #t
local rtn = {}
for x = i < 1 and 1 or i, j > #t and #t or j do
rtn[#rtn + 1] = t[x]
end
return rtn
end
function lume.first(t, n)
if not n then return t[1] end
return lume.slice(t, 1, n)
end
function lume.last(t, n)
if not n then return t[#t] end
return lume.slice(t, -n, -1)
end
function lume.invert(t)
local rtn = {}
for k, v in pairs(t) do rtn[v] = k end
return rtn
end
function lume.pick(t, ...)
local rtn = {}
for i = 1, select("#", ...) do
local k = select(i, ...)
rtn[k] = t[k]
end
return rtn
end
function lume.keys(t)
local rtn = {}
local iter = getiter(t)
for k in iter(t) do rtn[#rtn + 1] = k end
return rtn
end
function lume.clone(t)
local rtn = {}
for k, v in pairs(t) do rtn[k] = v end
return rtn
end
function lume.fn(fn, ...)
assert(iscallable(fn), "expected a function as the first argument")
local args = { ... }
return function(...)
local a = lume.concat(args, { ... })
return fn(unpack(a))
end
end
function lume.once(fn, ...)
local f = lume.fn(fn, ...)
local done = false
return function(...)
if done then return end
done = true
return f(...)
end
end
local memoize_fnkey = {}
local memoize_nil = {}
function lume.memoize(fn)
local cache = {}
return function(...)
local c = cache
for i = 1, select("#", ...) do
local a = select(i, ...) or memoize_nil
c[a] = c[a] or {}
c = c[a]
end
c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
return unpack(c[memoize_fnkey])
end
end
function lume.combine(...)
local n = select('#', ...)
if n == 0 then return noop end
if n == 1 then
local fn = select(1, ...)
if not fn then return noop end
assert(iscallable(fn), "expected a function or nil")
return fn
end
local funcs = {}
for i = 1, n do
local fn = select(i, ...)
if fn ~= nil then
assert(iscallable(fn), "expected a function or nil")
funcs[#funcs + 1] = fn
end
end
return function(...)
for _, f in ipairs(funcs) do f(...) end
end
end
function lume.call(fn, ...)
if fn then
return fn(...)
end
end
function lume.time(fn, ...)
local start = os.clock()
local rtn = {fn(...)}
return (os.clock() - start), unpack(rtn)
end
local lambda_cache = {}
function lume.lambda(str)
if not lambda_cache[str] then
local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
assert(args and body, "bad string lambda")
local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
lambda_cache[str] = lume.dostring(s)
end
return lambda_cache[str]
end
local serialize
local serialize_map = {
[ "boolean" ] = tostring,
[ "nil" ] = tostring,
[ "string" ] = function(v) return string.format("%q", v) end,
[ "number" ] = function(v)
if v ~= v then return "0/0" -- nan
elseif v == 1 / 0 then return "1/0" -- inf
elseif v == -1 / 0 then return "-1/0" end -- -inf
return tostring(v)
end,
[ "table" ] = function(t, stk)
stk = stk or {}
if stk[t] then error("circular reference") end
local rtn = {}
stk[t] = true
for k, v in pairs(t) do
rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk)
end
stk[t] = nil
return "{" .. table.concat(rtn, ",") .. "}"
end
}
setmetatable(serialize_map, {
__index = function(_, k) error("unsupported serialize type: " .. k) end
})
serialize = function(x, stk)
return serialize_map[type(x)](x, stk)
end
function lume.serialize(x)
return serialize(x)
end
function lume.deserialize(str)
return lume.dostring("return " .. str)
end
function lume.split(str, sep)
if not sep then
return lume.array(str:gmatch("([%S]+)"))
else
assert(sep ~= "", "empty separator")
local psep = patternescape(sep)
return lume.array((str..sep):gmatch("(.-)("..psep..")"))
end
end
function lume.trim(str, chars)
if not chars then return str:match("^[%s]*(.-)[%s]*$") end
chars = patternescape(chars)
return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
end
function lume.wordwrap(str, limit)
limit = limit or 72
local check
if type(limit) == "number" then
check = function(s) return #s >= limit end
else
check = limit
end
local rtn = {}
local line = ""
for word, spaces in str:gmatch("(%S+)(%s*)") do
local s = line .. word
if check(s) then
table.insert(rtn, line .. "\n")
line = word
else
line = s
end
for c in spaces:gmatch(".") do
if c == "\n" then
table.insert(rtn, line .. "\n")
line = ""
else
line = line .. c
end
end
end
table.insert(rtn, line)
return table.concat(rtn)
end
function lume.format(str, vars)
if not vars then return str end
local f = function(x)
return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
end
return (str:gsub("{(.-)}", f))
end
function lume.trace(...)
local info = debug.getinfo(2, "Sl")
local t = { info.short_src .. ":" .. info.currentline .. ":" }
for i = 1, select("#", ...) do
local x = select(i, ...)
if type(x) == "number" then
x = string.format("%g", lume.round(x, .01))
end
t[#t + 1] = tostring(x)
end
print(table.concat(t, " "))
end
function lume.dostring(str)
return assert((loadstring or load)(str))()
end
function lume.uuid()
local fn = function(x)
local r = math.random(16) - 1
r = (x == "x") and (r + 1) or (r % 4) + 9
return ("0123456789abcdef"):sub(r, r)
end
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end
function lume.hotswap(modname)
local oldglobal = lume.clone(_G)
local updated = {}
local function update(old, new)
if updated[old] then return end
updated[old] = true
local oldmt, newmt = getmetatable(old), getmetatable(new)
if oldmt and newmt then update(oldmt, newmt) end
for k, v in pairs(new) do
if type(v) == "table" then update(old[k], v) else old[k] = v end
end
end
local err = nil
local function onerror(e)
for k in pairs(_G) do _G[k] = oldglobal[k] end
err = lume.trim(e)
end
local ok, oldmod = pcall(require, modname)
oldmod = ok and oldmod or nil
xpcall(function()
package.loaded[modname] = nil
local newmod = require(modname)
if type(oldmod) == "table" then update(oldmod, newmod) end
for k, v in pairs(oldglobal) do
if v ~= _G[k] and type(v) == "table" then
update(v, _G[k])
_G[k] = v
end
end
end, onerror)
package.loaded[modname] = oldmod
if err then return nil, err end
return oldmod
end
local ripairs_iter = function(t, i)
i = i - 1
local v = t[i]
if v ~= nil then
return i, v
end
end
function lume.ripairs(t)
return ripairs_iter, t, (#t + 1)
end
function lume.color(str, mul)
mul = mul or 1
local r, g, b, a
r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
if r then
r = tonumber(r, 16) / 0xff
g = tonumber(g, 16) / 0xff
b = tonumber(b, 16) / 0xff
a = 1
elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
local f = str:gmatch("[%d.]+")
r = (f() or 0) / 0xff
g = (f() or 0) / 0xff
b = (f() or 0) / 0xff
a = f() or 1
else
error(("bad color string '%s'"):format(str))
end
return r * mul, g * mul, b * mul, a * mul
end
local chain_mt = {}
chain_mt.__index = lume.map(lume.filter(lume, iscallable, true),
function(fn)
return function(self, ...)
self._value = fn(self._value, ...)
return self
end
end)
chain_mt.__index.result = function(x) return x._value end
function lume.chain(value)
return setmetatable({ _value = value }, chain_mt)
end
setmetatable(lume, {
__call = function(_, ...)
return lume.chain(...)
end
})
return lume

20
thirdparty/middleclass/MIT-LICENSE.txt vendored Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2011 Enrique García Cota
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.

80
thirdparty/middleclass/README.md vendored Normal file
View file

@ -0,0 +1,80 @@
middleclass
===========
[![Build Status](https://travis-ci.org/kikito/middleclass.png?branch=master)](https://travis-ci.org/kikito/middleclass)
[![Coverage Status](https://coveralls.io/repos/kikito/middleclass/badge.svg?branch=master&service=github)](https://coveralls.io/github/kikito/middleclass?branch=master)
A simple OOP library for Lua. It has inheritance, metamethods (operators), class variables and weak mixin support.
Quick Look
==========
```lua
local class = require 'middleclass'
local Fruit = class('Fruit') -- 'Fruit' is the class' name
function Fruit:initialize(sweetness)
self.sweetness = sweetness
end
Fruit.static.sweetness_threshold = 5 -- class variable (also admits methods)
function Fruit:isSweet()
return self.sweetness > Fruit.sweetness_threshold
end
local Lemon = class('Lemon', Fruit) -- subclassing
function Lemon:initialize()
Fruit.initialize(self, 1) -- invoking the superclass' initializer
end
local lemon = Lemon:new()
print(lemon:isSweet()) -- false
```
Documentation
=============
See the [github wiki page](https://github.com/kikito/middleclass/wiki) for examples & documentation.
You can read the `CHANGELOG.md` file to see what has changed on each version of this library.
If you need help updating to a new middleclass version, read `UPDATING.md`.
Installation
============
Just copy the middleclass.lua file wherever you want it (for example on a lib/ folder). Then write this in any Lua file where you want to use it:
```lua
local class = require 'middleclass'
```
Specs
=====
This project uses [busted](http://olivinelabs.com/busted/) for its specs. If you want to run the specs, you will have to install it first. Then just execute the following:
```bash
cd /folder/where/the/spec/folder/is
busted
```
Performance tests
=================
Middleclass also comes with a small performance test suite. Just run the following command:
```bash
lua performance/run.lua
```
License
=======
Middleclass is distributed under the MIT license.

193
thirdparty/middleclass/middleclass.lua vendored Normal file
View file

@ -0,0 +1,193 @@
local middleclass = {
_VERSION = 'middleclass v4.1.1',
_DESCRIPTION = 'Object Orientation for Lua',
_URL = 'https://github.com/kikito/middleclass',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2011 Enrique García Cota
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.
]]
}
local function _createIndexWrapper(aClass, f)
if f == nil then
return aClass.__instanceDict
elseif type(f) == "function" then
return function(self, name)
local value = aClass.__instanceDict[name]
if value ~= nil then
return value
else
return (f(self, name))
end
end
else -- if type(f) == "table" then
return function(self, name)
local value = aClass.__instanceDict[name]
if value ~= nil then
return value
else
return f[name]
end
end
end
end
local function _propagateInstanceMethod(aClass, name, f)
f = name == "__index" and _createIndexWrapper(aClass, f) or f
aClass.__instanceDict[name] = f
for subclass in pairs(aClass.subclasses) do
if rawget(subclass.__declaredMethods, name) == nil then
_propagateInstanceMethod(subclass, name, f)
end
end
end
local function _declareInstanceMethod(aClass, name, f)
aClass.__declaredMethods[name] = f
if f == nil and aClass.super then
f = aClass.super.__instanceDict[name]
end
_propagateInstanceMethod(aClass, name, f)
end
local function _tostring(self) return "class " .. self.name end
local function _call(self, ...) return self:new(...) end
local function _createClass(name, super)
local dict = {}
dict.__index = dict
local aClass = { name = name, super = super, static = {},
__instanceDict = dict, __declaredMethods = {},
subclasses = setmetatable({}, {__mode='k'}) }
if super then
setmetatable(aClass.static, {
__index = function(_,k)
local result = rawget(dict,k)
if result == nil then
return super.static[k]
end
return result
end
})
else
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) end })
end
setmetatable(aClass, { __index = aClass.static, __tostring = _tostring,
__call = _call, __newindex = _declareInstanceMethod })
return aClass
end
local function _includeMixin(aClass, mixin)
assert(type(mixin) == 'table', "mixin must be a table")
for name,method in pairs(mixin) do
if name ~= "included" and name ~= "static" then aClass[name] = method end
end
for name,method in pairs(mixin.static or {}) do
aClass.static[name] = method
end
if type(mixin.included)=="function" then mixin:included(aClass) end
return aClass
end
local DefaultMixin = {
__tostring = function(self) return "instance of " .. tostring(self.class) end,
initialize = function(self, ...) end,
isInstanceOf = function(self, aClass)
return type(aClass) == 'table'
and type(self) == 'table'
and (self.class == aClass
or type(self.class) == 'table'
and type(self.class.isSubclassOf) == 'function'
and self.class:isSubclassOf(aClass))
end,
static = {
allocate = function(self)
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict)
end,
new = function(self, ...)
assert(type(self) == 'table', "Make sure that you are using 'Class:new' instead of 'Class.new'")
local instance = self:allocate()
instance:initialize(...)
return instance
end,
subclass = function(self, name)
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
for methodName, f in pairs(self.__instanceDict) do
if not (methodName == "__index" and type(f) == "table") then
_propagateInstanceMethod(subclass, methodName, f)
end
end
subclass.initialize = function(instance, ...) return self.initialize(instance, ...) end
self.subclasses[subclass] = true
self:subclassed(subclass)
return subclass
end,
subclassed = function(self, other) end,
isSubclassOf = function(self, other)
return type(other) == 'table' and
type(self.super) == 'table' and
( self.super == other or self.super:isSubclassOf(other) )
end,
include = function(self, ...)
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end
return self
end
}
}
function middleclass.class(name, super)
assert(type(name) == 'string', "A name (string) is needed for the new class")
return super and super:subclass(name) or _includeMixin(_createClass(name), DefaultMixin)
end
setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end })
return middleclass