More handy tools for game developers!
This commit is contained in:
parent
9af9f7e35e
commit
e7b24ea7b3
26 changed files with 6069 additions and 0 deletions
43
CHANGELOG
43
CHANGELOG
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
4
LICENSES
4
LICENSES
|
|
@ -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)
|
||||||
|
|
|
||||||
637
docs/Manual.adoc
637
docs/Manual.adoc
|
|
@ -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
166
src/collide.c
Normal 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
53
src/collide.h
Normal 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
|
||||||
|
|
@ -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
128
src/persist.c
Normal 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
55
src/persist.h
Normal 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
280
src/scheduler.c
Normal 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
95
src/scheduler.h
Normal 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
|
||||||
768
src/singe.c
768
src/singe.c
|
|
@ -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
142
src/stats.c
Normal 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
73
src/stats.h
Normal 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
20
thirdparty/bump.lua/MIT-LICENSE.txt
vendored
Normal 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
565
thirdparty/bump.lua/README.md
vendored
Normal file
|
|
@ -0,0 +1,565 @@
|
||||||
|
# bump.lua
|
||||||
|
|
||||||
|
[](https://travis-ci.org/kikito/bump.lua)
|
||||||
|
[](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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
773
thirdparty/bump.lua/bump.lua
vendored
Normal 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
20
thirdparty/inspect.lua/MIT-LICENSE.txt
vendored
Normal 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
266
thirdparty/inspect.lua/README.md
vendored
Normal 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
377
thirdparty/inspect.lua/inspect.lua
vendored
Normal 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
20
thirdparty/lume/LICENSE
vendored
Normal 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
495
thirdparty/lume/README.md
vendored
Normal 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
780
thirdparty/lume/lume.lua
vendored
Normal 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
20
thirdparty/middleclass/MIT-LICENSE.txt
vendored
Normal 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
80
thirdparty/middleclass/README.md
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
middleclass
|
||||||
|
===========
|
||||||
|
|
||||||
|
[](https://travis-ci.org/kikito/middleclass)
|
||||||
|
[](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
193
thirdparty/middleclass/middleclass.lua
vendored
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue