Some bug fixes in recent work.
This commit is contained in:
parent
9bf4d9beb2
commit
3f69551719
38 changed files with 6017 additions and 1492 deletions
70
CHANGELOG
70
CHANGELOG
|
|
@ -26,8 +26,8 @@ API Changes
|
||||||
the command line keep their values. Every game in a library therefore
|
the command line keep their values. Every game in a library therefore
|
||||||
behaves the same whether it was started from the menu or from a shell.
|
behaves the same whether it was started from the menu or from a shell.
|
||||||
|
|
||||||
- A game can be described rather than written. Singe/Author.singe and
|
- A game can be described rather than written. Forge/Author.singe and
|
||||||
Singe/AuthorCompile.singe take a table of layers, entities, behaviours
|
Forge/AuthorCompile.singe take a table of layers, entities, behaviours
|
||||||
and rules and compile it into an ordinary Singe game: the rules become
|
and rules and compile it into an ordinary Singe game: the rules become
|
||||||
real Lua, so nothing walks a table every frame and the result can be
|
real Lua, so nothing walks a table every frame and the result can be
|
||||||
read and edited by hand. There is no notion of genre in it -- a game
|
read and edited by hand. There is no notion of genre in it -- a game
|
||||||
|
|
@ -38,6 +38,19 @@ API Changes
|
||||||
releases, and the "lua" action is the deliberate way out when a rule
|
releases, and the "lua" action is the deliberate way out when a rule
|
||||||
needs something the vocabulary cannot say. See the manual.
|
needs something the vocabulary cannot say. See the manual.
|
||||||
|
|
||||||
|
- Forge runs as a game rather than only as a library: started from the
|
||||||
|
menu it opens on a chooser (its own descriptions, a new game from a
|
||||||
|
starter, a copy of anything dropped into its directory), the keys, the
|
||||||
|
mouse and the pad reach the editor, P saves, builds and plays what is on
|
||||||
|
screen and comes back to it, and ESC closes or leaves. Entities can be
|
||||||
|
added, duplicated, deleted and typed field by field -- position, look,
|
||||||
|
behaviours and their parameters, all from the manifest -- and renaming
|
||||||
|
one renames it in every rule. Conditions and actions are picked from
|
||||||
|
the vocabulary with their help beside them, rules reorder, a rule's
|
||||||
|
note is editable, and U undoes forty steps. The compiler loads the
|
||||||
|
runtime from Forge/, where it lives, so it no longer fails looking for
|
||||||
|
a Singe/Author.singe that never shipped.
|
||||||
|
|
||||||
- An editor for those descriptions, Forge, which is itself
|
- An editor for those descriptions, Forge, which is itself
|
||||||
a Singe game: the canvas is the same overlay at the same coordinates the
|
a Singe game: the canvas is the same overlay at the same coordinates the
|
||||||
game will be played in, so what is placed is what is seen. Entity list
|
game will be played in, so what is placed is what is seen. Entity list
|
||||||
|
|
@ -1461,6 +1474,59 @@ Fixes
|
||||||
materialNew, sceneGetSize and sceneGetStats no longer reject extra
|
materialNew, sceneGetSize and sceneGetStats no longer reject extra
|
||||||
arguments.
|
arguments.
|
||||||
|
|
||||||
|
- A review of the week's work (the io patch, the scheduler, Forge, the
|
||||||
|
menu, the GLES backend, saves and the GUI) fixed what it found:
|
||||||
|
|
||||||
|
Lua's io on a packed asset: f:lines and io.lines with more than one
|
||||||
|
format handed back only the last value read a step; a later format
|
||||||
|
that comes up empty now returns what came before it with nil in its
|
||||||
|
place, as Lua does, and only an empty first read ends the loop.
|
||||||
|
read("a") reads in blocks rather than a byte at a time, read(0) is the
|
||||||
|
end-of-file test it is in Lua, and a NUL byte is no longer taken for
|
||||||
|
part of a number. testScripts/packedIo covers all of it.
|
||||||
|
|
||||||
|
Timer and tween handles carry a generation: a handle kept past its
|
||||||
|
timer's end no longer cancels, or answers for, whatever timer was
|
||||||
|
given the same slot afterwards.
|
||||||
|
|
||||||
|
Forge: a rule's keyHeld condition never fired from a real keyboard
|
||||||
|
(it compared a key name against the scancodes the engine delivers);
|
||||||
|
an entity whose look kind is unknown is drawn as a box with a message
|
||||||
|
instead of ending the game; and the compiler loads the runtime from
|
||||||
|
Forge/, where it lives, rather than a Singe/Author.singe that was never
|
||||||
|
shipped. The games.dat Forge writes no longer carries a DATA key the
|
||||||
|
engine does not read, and the manual's example loses it too.
|
||||||
|
|
||||||
|
The menu: a games.dat entry with no ATTRACT clip no longer stops the
|
||||||
|
engine on either renderer, quitting with no games listed no longer
|
||||||
|
fails in onShutdown, and a directory entry lfs cannot describe is
|
||||||
|
skipped rather than indexed.
|
||||||
|
|
||||||
|
The GLES backend freed nothing when its framebuffer cache filled and
|
||||||
|
was reset; the framebuffer objects go now. A save is written with a
|
||||||
|
single atomic rename everywhere but Windows, so the old save stays
|
||||||
|
whole until the new one is in place. A GUI texture decoded with a
|
||||||
|
padded pitch is packed row by row instead of uploaded with its padding
|
||||||
|
read as pixels. The menu's backdrop stops rewriting its grid colours
|
||||||
|
every frame once the grid is lit.
|
||||||
|
|
||||||
|
Net.singe verified a connection against Linux's CA list by path, so
|
||||||
|
without a pin every HTTPS request failed on Windows, macOS and the
|
||||||
|
handhelds. The engine now extracts Mozilla's CA list as
|
||||||
|
Singe/cacerts.pem with the other support files and Net.singe uses it
|
||||||
|
wherever the platform's own list is missing. A chunked response --
|
||||||
|
what an HTTP/1.1 server sends when it does not know the length -- is
|
||||||
|
unwrapped as it arrives rather than handed to the caller with the chunk
|
||||||
|
sizes still in it. And a mistyped value in a table given to scriptPush
|
||||||
|
or scriptExecute is reported like any other bad API argument -- the
|
||||||
|
script line and the call named -- rather than as a bare exit message.
|
||||||
|
|
||||||
|
- Forge has its own manual, docs/Forge.adoc, built to Forge.html and
|
||||||
|
Forge.pdf beside the Singe manual; the engine's manual keeps a pointer.
|
||||||
|
The build packs Forge.game itself, with the manual inside, and Forge's
|
||||||
|
first run copies Forge.pdf out to its data directory and says where.
|
||||||
|
R redoes what U undid, forty steps deep.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SINGE 2.10
|
SINGE 2.10
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,7 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h
|
||||||
# its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime
|
# its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime
|
||||||
# a game it builds loads -- belongs inside Singe.
|
# a game it builds loads -- belongs inside Singe.
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/cacerts.pem ${GENERATED_DIR}/cacerts_pem.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Master.singe ${GENERATED_DIR}/Master_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Master.singe ${GENERATED_DIR}/Master_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Backdrop.singe ${GENERATED_DIR}/Backdrop_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Backdrop.singe ${GENERATED_DIR}/Backdrop_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuDocument.singe ${GENERATED_DIR}/MenuDocument_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuDocument.singe ${GENERATED_DIR}/MenuDocument_singe.h "")
|
||||||
|
|
@ -295,6 +296,18 @@ singeEmbedLua(thirdparty/lume/lume.lua "")
|
||||||
singeEmbedLua(thirdparty/inspect.lua/inspect.lua "")
|
singeEmbedLua(thirdparty/inspect.lua/inspect.lua "")
|
||||||
singeEmbedLua(thirdparty/bump.lua/bump.lua "")
|
singeEmbedLua(thirdparty/bump.lua/bump.lua "")
|
||||||
|
|
||||||
|
# Forge's manual, rendered the same way. It is not embedded: the superbuild packs it into
|
||||||
|
# Forge.game (cmake/Superbuild.cmake), which is the one thing that ships Forge.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${MANUAL_DIR}/Forge.pdf
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
|
||||||
|
COMMAND ${ASCIIDOCTOR_PDF} -a revnumber=${PROJECT_VERSION} ${CMAKE_SOURCE_DIR}/docs/Forge.adoc -o ${MANUAL_DIR}/Forge.pdf
|
||||||
|
DEPENDS docs/Forge.adoc
|
||||||
|
COMMENT "Rendering Forge.pdf"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(forgeManual ALL DEPENDS ${MANUAL_DIR}/Forge.pdf)
|
||||||
|
|
||||||
# Optional HTML manual for browsing: cmake --build . --target docs
|
# Optional HTML manual for browsing: cmake --build . --target docs
|
||||||
if(ASCIIDOCTOR)
|
if(ASCIIDOCTOR)
|
||||||
add_custom_target(docs
|
add_custom_target(docs
|
||||||
|
|
@ -819,7 +832,6 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR}/gen
|
||||||
|
|
||||||
# Rockchip's hardware decoders are reached by name (videoPlayer.c), and only exist when FFmpeg was
|
# Rockchip's hardware decoders are reached by name (videoPlayer.c), and only exist when FFmpeg was
|
||||||
# built against the Rockchip MPP library. Asking FFmpeg itself is what decides it.
|
# built against the Rockchip MPP library. Asking FFmpeg itself is what decides it.
|
||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E echo "" OUTPUT_QUIET)
|
|
||||||
if(EXISTS ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc)
|
if(EXISTS ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc)
|
||||||
file(READ ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc avcodecPc)
|
file(READ ${BUILD_DIR}/lib/pkgconfig/libavcodec.pc avcodecPc)
|
||||||
if(avcodecPc MATCHES "rockchip_mpp")
|
if(avcodecPc MATCHES "rockchip_mpp")
|
||||||
|
|
@ -857,9 +869,9 @@ endif()
|
||||||
# System libraries follow the static ones so their symbols resolve for everything above.
|
# System libraries follow the static ones so their symbols resolve for everything above.
|
||||||
# Apple's linker has no --start-group, so the list is repeated there instead.
|
# Apple's linker has no --start-group, so the list is repeated there instead.
|
||||||
if(KANGAROO_OS STREQUAL "macos")
|
if(KANGAROO_OS STREQUAL "macos")
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Debugger RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
|
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
|
||||||
else()
|
else()
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Debugger RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
|
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group Jolt::Jolt RecastNavigation::DetourCrowd RecastNavigation::Detour RecastNavigation::Recast RmlUi::Lua RmlUi::Core ${SYSTEM_LIBS} -pthread -lm)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
6
LICENSES
6
LICENSES
|
|
@ -67,3 +67,9 @@ Fonts
|
||||||
-----
|
-----
|
||||||
|
|
||||||
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
|
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
|
||||||
|
|
||||||
|
|
||||||
|
Data
|
||||||
|
----
|
||||||
|
|
||||||
|
Mozilla CA list 20260601 (Debian ca-certificates) MPL-2.0 https://wiki.mozilla.org/CA (assets/cacerts.pem)
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ local backdropStarted = 0 -- singeGetTicks() when the backdrop's clock began
|
||||||
local backdropTagX = 0
|
local backdropTagX = 0
|
||||||
local backdropTagY = 0
|
local backdropTagY = 0
|
||||||
local blown = false -- Whether the charge has gone off.
|
local blown = false -- Whether the charge has gone off.
|
||||||
|
local gridLit = false -- Whether the grid has finished coming up and its colours are final.
|
||||||
local backdropFont = nil
|
local backdropFont = nil
|
||||||
|
|
||||||
-- ===== The backdrop ==========================================================================
|
-- ===== The backdrop ==========================================================================
|
||||||
|
|
@ -473,7 +474,6 @@ function backdropFrame()
|
||||||
emitterBurst(core, 110)
|
emitterBurst(core, 110)
|
||||||
emitterBurst(flash, 5)
|
emitterBurst(flash, 5)
|
||||||
emitterBurst(sparks, 130)
|
emitterBurst(sparks, 130)
|
||||||
lightSetIntensity(fireLight, 400)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The fire's light dies over the first second and a half.
|
-- The fire's light dies over the first second and a half.
|
||||||
|
|
@ -483,13 +483,15 @@ function backdropFrame()
|
||||||
-- there is no moment where one thing has ended and the next has not begun.
|
-- there is no moment where one thing has ended and the next has not begun.
|
||||||
local lit = span(t, GRID_UP, GRID_LIT)
|
local lit = span(t, GRID_UP, GRID_LIT)
|
||||||
|
|
||||||
if (lit > 0) and (lit < 1.0001) then
|
if (lit > 0) and not gridLit then
|
||||||
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
|
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
|
||||||
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
|
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
|
||||||
materialSetColor(sunMat2, 255 * lit, 130 * lit, 55 * lit)
|
materialSetColor(sunMat2, 255 * lit, 130 * lit, 55 * lit)
|
||||||
-- The sky stops being black at the same time, or the grid would rise into a void.
|
-- The sky stops being black at the same time, or the grid would rise into a void.
|
||||||
sceneSetBackground(FOG_R * lit, FOG_G * lit, FOG_B * lit, 255)
|
sceneSetBackground(FOG_R * lit, FOG_G * lit, FOG_B * lit, 255)
|
||||||
sceneSetFog(FOG_R, FOG_G, FOG_B, FOG_NEAR, FOG_FAR)
|
sceneSetFog(FOG_R, FOG_G, FOG_B, FOG_NEAR, FOG_FAR)
|
||||||
|
-- Once fully lit the colours are final, so they are not written again every frame.
|
||||||
|
gridLit = (lit >= 1)
|
||||||
end
|
end
|
||||||
if lit > 0 then
|
if lit > 0 then
|
||||||
-- Scrolling from the moment it is visible, so it is already alive when it arrives.
|
-- Scrolling from the moment it is visible, so it is already alive when it arrives.
|
||||||
|
|
@ -579,6 +581,7 @@ function backdropBegin(showIntro)
|
||||||
|
|
||||||
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
|
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
|
||||||
blown = not showIntro
|
blown = not showIntro
|
||||||
|
gridLit = false
|
||||||
if not showIntro then
|
if not showIntro then
|
||||||
nodeSetVisible(logo, false)
|
nodeSetVisible(logo, false)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
-- The runtime an authored game is compiled against (PLAN section 58).
|
-- The runtime an authored game is compiled against (PLAN section 58).
|
||||||
--
|
--
|
||||||
-- A game made with the authoring tools is not interpreted: Singe/AuthorCompile.singe turns its
|
-- A game made with the authoring tools is not interpreted: Forge/AuthorCompile.singe turns its
|
||||||
-- description into ordinary Lua, and this file is the library that Lua calls. Rules become real
|
-- description into ordinary Lua, and this file is the library that Lua calls. Rules become real
|
||||||
-- `if` statements, so nothing walks a table every frame and the result can be opened in ZeroBrane
|
-- `if` statements, so nothing walks a table every frame and the result can be opened in ZeroBrane
|
||||||
-- and edited by hand like any other game.
|
-- and edited by hand like any other game.
|
||||||
|
|
@ -163,7 +163,9 @@ AUTHOR.behaviours.solid = {
|
||||||
help = "Immovable ground or a wall.",
|
help = "Immovable ground or a wall.",
|
||||||
params = {},
|
params = {},
|
||||||
attach = function(entity)
|
attach = function(entity)
|
||||||
bodyNew(entity.node, BODY_STATIC, SHAPE_BOX, entity.look.w / 2, entity.look.h / 2, entity.look.w / 2)
|
local w, h = authorSize(entity)
|
||||||
|
|
||||||
|
bodyNew(entity.node, BODY_STATIC, SHAPE_BOX, w / 2, h / 2, w / 2)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,9 +173,11 @@ AUTHOR.behaviours.platformer = {
|
||||||
help = "Runs, falls and jumps: the engine's character controller in 2D.",
|
help = "Runs, falls and jumps: the engine's character controller in 2D.",
|
||||||
params = { speed = "number", jump = "number" },
|
params = { speed = "number", jump = "number" },
|
||||||
attach = function(entity, params)
|
attach = function(entity, params)
|
||||||
|
local w, h = authorSize(entity)
|
||||||
|
|
||||||
entity.speed = params.speed or 200
|
entity.speed = params.speed or 200
|
||||||
entity.jump = params.jump or 500
|
entity.jump = params.jump or 500
|
||||||
playerNew(entity.node, SHAPE_CAPSULE, entity.look.w * PLAYER_RADIUS, entity.look.h)
|
playerNew(entity.node, SHAPE_CAPSULE, w * PLAYER_RADIUS, h)
|
||||||
end,
|
end,
|
||||||
step = function(entity)
|
step = function(entity)
|
||||||
-- The rules say which way; this clears it each frame so releasing a key stops the run.
|
-- The rules say which way; this clears it each frame so releasing a key stops the run.
|
||||||
|
|
@ -205,7 +209,7 @@ AUTHOR.behaviours.drift = {
|
||||||
AUTHOR.conditions.keyHeld = {
|
AUTHOR.conditions.keyHeld = {
|
||||||
help = "A key is down.",
|
help = "A key is down.",
|
||||||
params = { key = "scancode" },
|
params = { key = "scancode" },
|
||||||
emit = function(p) return string.format("authorKeyHeld(SCANCODE.%s)", p.key) end
|
emit = function(p) return string.format("authorKeyHeld(SCANCODE.%s.value)", p.key) end
|
||||||
}
|
}
|
||||||
|
|
||||||
AUTHOR.conditions.switchHeld = {
|
AUTHOR.conditions.switchHeld = {
|
||||||
|
|
@ -368,9 +372,23 @@ function authorTouching(idA, idB)
|
||||||
local b = AUTHOR_WORLD[idB]
|
local b = AUTHOR_WORLD[idB]
|
||||||
local ax, ay = authorPosition(a)
|
local ax, ay = authorPosition(a)
|
||||||
local bx, by = authorPosition(b)
|
local bx, by = authorPosition(b)
|
||||||
|
local aw, ah = authorSize(a)
|
||||||
|
local bw, bh = authorSize(b)
|
||||||
|
|
||||||
return collideRects(ax - a.look.w / 2, ay - a.look.h / 2, a.look.w, a.look.h,
|
return collideRects(ax - aw / 2, ay - ah / 2, aw, ah, bx - bw / 2, by - bh / 2, bw, bh)
|
||||||
bx - b.look.w / 2, by - b.look.h / 2, b.look.w, b.look.h)
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- How big an entity is, for touching and for the body a behaviour gives it. A box says; a sprite
|
||||||
|
-- is its image; a text look has no size of its own and counts as a point unless it was given one.
|
||||||
|
function authorSize(entity)
|
||||||
|
local look = entity.look
|
||||||
|
|
||||||
|
if entity.sprite ~= nil then
|
||||||
|
return spriteGetWidth(entity.sprite), spriteGetHeight(entity.sprite)
|
||||||
|
end
|
||||||
|
|
||||||
|
return look.w or 0, look.h or 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -431,9 +449,17 @@ function authorMake(description)
|
||||||
node = nodeNew()
|
node = nodeNew()
|
||||||
}
|
}
|
||||||
local look = AUTHOR.looks[entity.look.kind]
|
local look = AUTHOR.looks[entity.look.kind]
|
||||||
local b
|
|
||||||
|
|
||||||
nodeSetPosition(entity.node, description.x or 0, description.y or 0, 0)
|
nodeSetPosition(entity.node, description.x or 0, description.y or 0, 0)
|
||||||
|
if look == nil then
|
||||||
|
-- A look that does not exist would take the game down when it came to be drawn; a box
|
||||||
|
-- is what it wears instead, and the console says which entity it was.
|
||||||
|
debugPrint("Author: no look called '" .. tostring(entity.look.kind) .. "' on " .. tostring(entity.id) .. "; drawing a box")
|
||||||
|
entity.look.kind = "box"
|
||||||
|
entity.look.w = entity.look.w or 20
|
||||||
|
entity.look.h = entity.look.h or 20
|
||||||
|
look = AUTHOR.looks.box
|
||||||
|
end
|
||||||
if look.load then
|
if look.load then
|
||||||
look.load(entity)
|
look.load(entity)
|
||||||
end
|
end
|
||||||
|
|
@ -458,8 +484,6 @@ end
|
||||||
-- Starts the layers the game declared. Anything a layer needs of the engine is asked for here and
|
-- Starts the layers the game declared. Anything a layer needs of the engine is asked for here and
|
||||||
-- nowhere else, which is what keeps the rest of this file free of genre.
|
-- nowhere else, which is what keeps the rest of this file free of genre.
|
||||||
function authorBegin(layers)
|
function authorBegin(layers)
|
||||||
local layer
|
|
||||||
|
|
||||||
-- A font, because the text look draws with fontPrint and fontPrint ends the game when none is
|
-- A font, because the text look draws with fontPrint and fontPrint ends the game when none is
|
||||||
-- selected. Every test had a scene select one first; a released game has nobody to do that,
|
-- selected. Every test had a scene select one first; a released game has nobody to do that,
|
||||||
-- and died on its first text entity. A game that wants its own calls fontSelect afterwards.
|
-- and died on its first text entity. A game that wants its own calls fontSelect afterwards.
|
||||||
|
|
@ -489,12 +513,9 @@ end
|
||||||
function authorFrame(rules)
|
function authorFrame(rules)
|
||||||
local now = authorTime()
|
local now = authorTime()
|
||||||
local dt = now - lastTime
|
local dt = now - lastTime
|
||||||
local entity
|
|
||||||
|
|
||||||
lastTime = now
|
lastTime = now
|
||||||
for _, entity in ipairs(AUTHOR_ORDER) do
|
for _, entity in ipairs(AUTHOR_ORDER) do
|
||||||
local b
|
|
||||||
|
|
||||||
for _, b in ipairs(entity.behaviours) do
|
for _, b in ipairs(entity.behaviours) do
|
||||||
local kind = AUTHOR.behaviours[b.kind]
|
local kind = AUTHOR.behaviours[b.kind]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,14 @@
|
||||||
-- This is written in Lua rather than in util/ as a Python script because the editor is itself a
|
-- This is written in Lua rather than in util/ as a Python script because the editor is itself a
|
||||||
-- Singe game: it has to be able to compile what it is editing without leaving the engine. It
|
-- Singe game: it has to be able to compile what it is editing without leaving the engine. It
|
||||||
-- lives with Forge rather than in Singe/ because only building needs it -- a finished game is
|
-- lives with Forge rather than in Singe/ because only building needs it -- a finished game is
|
||||||
-- already Lua. Singe/Author.singe, the runtime that game loads, has to stay an engine support
|
-- already Lua. Author.singe, the runtime that game loads, travels with the game: a copy is
|
||||||
-- file: a game handed to someone who has never installed Forge still has to run.
|
-- written beside it when it is built, so a game handed to someone who has never installed Forge
|
||||||
|
-- still runs. The compiler loads it too, for the vocabulary it emits against.
|
||||||
|
|
||||||
dofile("Singe/Author.singe")
|
-- Where the runtime is taken from when a game is built. Forge carries it; a game gets a copy.
|
||||||
|
AUTHOR_RUNTIME = "Forge/Author.singe"
|
||||||
|
|
||||||
|
dofile(AUTHOR_RUNTIME)
|
||||||
|
|
||||||
|
|
||||||
local INDENT = "\t"
|
local INDENT = "\t"
|
||||||
|
|
@ -58,7 +62,6 @@ end
|
||||||
local function tableSource(t)
|
local function tableSource(t)
|
||||||
local parts = {}
|
local parts = {}
|
||||||
local keys = {}
|
local keys = {}
|
||||||
local key
|
|
||||||
|
|
||||||
for key in pairs(t) do
|
for key in pairs(t) do
|
||||||
keys[#keys + 1] = key
|
keys[#keys + 1] = key
|
||||||
|
|
@ -88,7 +91,6 @@ end
|
||||||
|
|
||||||
local function emitConditions(rule)
|
local function emitConditions(rule)
|
||||||
local tests = {}
|
local tests = {}
|
||||||
local item
|
|
||||||
|
|
||||||
for _, item in ipairs(rule.when or {}) do
|
for _, item in ipairs(rule.when or {}) do
|
||||||
local test = emitOne(AUTHOR.conditions, item, "condition")
|
local test = emitOne(AUTHOR.conditions, item, "condition")
|
||||||
|
|
@ -108,8 +110,6 @@ end
|
||||||
|
|
||||||
|
|
||||||
local function emitRule(out, rule, index)
|
local function emitRule(out, rule, index)
|
||||||
local item
|
|
||||||
|
|
||||||
out[#out + 1] = string.format("%s-- %s", INDENT, rule.note or ("rule " .. index))
|
out[#out + 1] = string.format("%s-- %s", INDENT, rule.note or ("rule " .. index))
|
||||||
out[#out + 1] = string.format("%sif %s then", INDENT, emitConditions(rule))
|
out[#out + 1] = string.format("%sif %s then", INDENT, emitConditions(rule))
|
||||||
for _, item in ipairs(rule.act or {}) do
|
for _, item in ipairs(rule.act or {}) do
|
||||||
|
|
@ -131,11 +131,8 @@ end
|
||||||
-- the engine's, as it is for every game ever written for Singe.
|
-- the engine's, as it is for every game ever written for Singe.
|
||||||
function authorCompile(game)
|
function authorCompile(game)
|
||||||
local out = {}
|
local out = {}
|
||||||
local entity
|
|
||||||
local layer
|
|
||||||
local rule
|
|
||||||
|
|
||||||
out[#out + 1] = "-- Generated by Singe/AuthorCompile.singe from " .. (game.source or "a game description")
|
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
|
||||||
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
||||||
out[#out + 1] = "--"
|
out[#out + 1] = "--"
|
||||||
out[#out + 1] = "-- This is an ordinary Singe game and can be edited by hand. Doing so and then"
|
out[#out + 1] = "-- This is an ordinary Singe game and can be edited by hand. Doing so and then"
|
||||||
|
|
@ -160,7 +157,6 @@ function authorCompile(game)
|
||||||
|
|
||||||
for _, entity in ipairs(game.entities or {}) do
|
for _, entity in ipairs(game.entities or {}) do
|
||||||
local parts = {}
|
local parts = {}
|
||||||
local b
|
|
||||||
|
|
||||||
parts[#parts + 1] = string.format("id = %q", entity.id)
|
parts[#parts + 1] = string.format("id = %q", entity.id)
|
||||||
parts[#parts + 1] = string.format("x = %s, y = %s", entity.x or 0, entity.y or 0)
|
parts[#parts + 1] = string.format("x = %s, y = %s", entity.x or 0, entity.y or 0)
|
||||||
|
|
@ -226,10 +222,6 @@ function authorCopy(fromPath, toPath)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- Where the runtime is taken from when a game is built. Forge carries it; a game gets a copy.
|
|
||||||
AUTHOR_RUNTIME = "Forge/Author.singe"
|
|
||||||
|
|
||||||
|
|
||||||
-- The directory part of a path, with its separator, or "" for a bare name.
|
-- The directory part of a path, with its separator, or "" for a bare name.
|
||||||
local function directoryOf(path)
|
local function directoryOf(path)
|
||||||
return (string.match(path, "^(.*[/\\])") or "")
|
return (string.match(path, "^(.*[/\\])") or "")
|
||||||
|
|
@ -267,9 +259,6 @@ end
|
||||||
-- Whether a table holds only leaves, in which case it is written on one line. A rule reads far
|
-- Whether a table holds only leaves, in which case it is written on one line. A rule reads far
|
||||||
-- better as { "keyHeld", key = "LEFT" } than as six lines, and it is what an author typed.
|
-- better as { "keyHeld", key = "LEFT" } than as six lines, and it is what an author typed.
|
||||||
local function isLeaf(t)
|
local function isLeaf(t)
|
||||||
local key
|
|
||||||
local value
|
|
||||||
|
|
||||||
for key, value in pairs(t) do
|
for key, value in pairs(t) do
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
return false
|
return false
|
||||||
|
|
@ -288,8 +277,6 @@ local function valueSource(value, depth)
|
||||||
local parts = {}
|
local parts = {}
|
||||||
local keys = {}
|
local keys = {}
|
||||||
local count = 0
|
local count = 0
|
||||||
local key
|
|
||||||
local item
|
|
||||||
local sep
|
local sep
|
||||||
local open
|
local open
|
||||||
local close
|
local close
|
||||||
|
|
@ -340,7 +327,6 @@ end
|
||||||
function authorSave(game, path)
|
function authorSave(game, path)
|
||||||
local file = assert(io.open(path, "w"))
|
local file = assert(io.open(path, "w"))
|
||||||
local copy = {}
|
local copy = {}
|
||||||
local key
|
|
||||||
|
|
||||||
-- source says where a description came from and is set by the loader, so writing it back out
|
-- source says where a description came from and is set by the loader, so writing it back out
|
||||||
-- would make a description that had been through the editor differ from one that had not.
|
-- would make a description that had been through the editor differ from one that had not.
|
||||||
|
|
@ -349,7 +335,7 @@ function authorSave(game, path)
|
||||||
copy[key] = game[key]
|
copy[key] = game[key]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
file:write("-- Written by Singe/AuthorCompile.singe. Edit by hand or in the editor; either is fine.\n")
|
file:write("-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.\n")
|
||||||
file:write("return " .. valueSource(copy, 0) .. "\n")
|
file:write("return " .. valueSource(copy, 0) .. "\n")
|
||||||
file:close()
|
file:close()
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,6 @@ GAMES = {
|
||||||
SCRIPT = "Forge/Forge.singe",
|
SCRIPT = "Forge/Forge.singe",
|
||||||
LEGACY_SPRITE_ARGS = false,
|
LEGACY_SPRITE_ARGS = false,
|
||||||
VIDEO = "Singe/menuBackground.mkv",
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
DATA = "Forge",
|
|
||||||
STRETCH = false,
|
STRETCH = false,
|
||||||
NO_MOUSE = false,
|
NO_MOUSE = false,
|
||||||
RESOLUTION_X = 720,
|
RESOLUTION_X = 720,
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,18 @@ end
|
||||||
function loadGameAssets()
|
function loadGameAssets()
|
||||||
local game = GAME_LIST[GAME_SELECTED]
|
local game = GAME_LIST[GAME_SELECTED]
|
||||||
|
|
||||||
|
-- An entry without an attract clip, or naming one that is not there, shows its art and no
|
||||||
|
-- video rather than taking the whole menu down with a videoLoad that cannot open it.
|
||||||
|
if menuFileExists(game.ATTRACT) then
|
||||||
VIDEO_ATTRACT = videoLoad(game.ATTRACT)
|
VIDEO_ATTRACT = videoLoad(game.ATTRACT)
|
||||||
if game.AUDIO_TRACK then
|
if game.AUDIO_TRACK then
|
||||||
videoSetAudioTrack(VIDEO_ATTRACT, game.AUDIO_TRACK)
|
videoSetAudioTrack(VIDEO_ATTRACT, game.AUDIO_TRACK)
|
||||||
end
|
end
|
||||||
videoPlay(VIDEO_ATTRACT)
|
videoPlay(VIDEO_ATTRACT)
|
||||||
videoSeek(VIDEO_ATTRACT, game.ATTRACT_START)
|
videoSeek(VIDEO_ATTRACT, game.ATTRACT_START or 0)
|
||||||
|
else
|
||||||
|
debugPrint("Menu: " .. tostring(game.TITLE) .. " has no attract clip to play.")
|
||||||
|
end
|
||||||
MENU_RENDER.showGame(game)
|
MENU_RENDER.showGame(game)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -96,18 +102,26 @@ end
|
||||||
MENU_MISSING_ART = "Singe/missing.png"
|
MENU_MISSING_ART = "Singe/missing.png"
|
||||||
|
|
||||||
|
|
||||||
-- A readable art path: the game's own when it can be opened, the missing-art picture when not.
|
-- Whether a name is a file the engine can open. Asked of the index rather than of the file:
|
||||||
function menuArtPath(file)
|
-- io.open, which this used to use, has to hand Lua a real file it can read with the C library, so
|
||||||
|
-- every picture it was asked about was unpacked out of the .game into data/<game>/cache first --
|
||||||
|
-- and the menu asks about two of them for every game in the library, none of which it then opens
|
||||||
|
-- that way: the sprite loader and the document's texture loader both read straight out of the
|
||||||
|
-- database. lfs.attributes goes through the same virtual filesystem and answers from the index,
|
||||||
|
-- opening nothing.
|
||||||
|
function menuFileExists(file)
|
||||||
local path = tostring(file or "")
|
local path = tostring(file or "")
|
||||||
local found = (path ~= "") and lfs.attributes(path) or nil
|
local found = (path ~= "") and lfs.attributes(path) or nil
|
||||||
|
|
||||||
-- Asked of the index rather than of the file. io.open, which this used to use, has to hand
|
return (found ~= nil) and (found.mode ~= "directory")
|
||||||
-- Lua a real file it can read with the C library, so every picture it was asked about was
|
end
|
||||||
-- unpacked out of the .game into data/<game>/cache first -- and the menu asks about two of
|
|
||||||
-- them for every game in the library, none of which it then opens that way: the sprite loader
|
|
||||||
-- and the document's texture loader both read straight out of the database. lfs.attributes
|
-- A readable art path: the game's own when it can be opened, the missing-art picture when not.
|
||||||
-- goes through the same virtual filesystem and answers from the index, opening nothing.
|
function menuArtPath(file)
|
||||||
if (found == nil) or (found.mode == "directory") then
|
local path = tostring(file or "")
|
||||||
|
|
||||||
|
if not menuFileExists(path) then
|
||||||
if path ~= "" then
|
if path ~= "" then
|
||||||
debugPrint("Menu: cannot read " .. path .. "; showing the missing-art picture.")
|
debugPrint("Menu: cannot read " .. path .. "; showing the missing-art picture.")
|
||||||
end
|
end
|
||||||
|
|
@ -209,8 +223,10 @@ function onOverlayUpdate()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The attract clip runs between the two frames the game's entry names.
|
-- The attract clip runs between the two frames the game's entry names.
|
||||||
|
if VIDEO_ATTRACT and GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
||||||
if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
||||||
videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START)
|
videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START or 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
MENU_RENDER.frame()
|
MENU_RENDER.frame()
|
||||||
end
|
end
|
||||||
|
|
@ -221,6 +237,10 @@ end
|
||||||
|
|
||||||
|
|
||||||
function onShutdown()
|
function onShutdown()
|
||||||
|
-- With no games found there is no renderer and nothing was loaded; quitting is all there is.
|
||||||
|
if MENU_RENDER == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
unloadGameAssets()
|
unloadGameAssets()
|
||||||
saveConfig(not SHUTDOWN_FROM_PUSH)
|
saveConfig(not SHUTDOWN_FROM_PUSH)
|
||||||
MENU_RENDER.finish()
|
MENU_RENDER.finish()
|
||||||
|
|
@ -306,7 +326,7 @@ for dir in lfs.dir(".") do
|
||||||
local dirattr = lfs.attributes(dir)
|
local dirattr = lfs.attributes(dir)
|
||||||
if dir:sub(-5):lower() == ".game" then
|
if dir:sub(-5):lower() == ".game" then
|
||||||
loadGamesDat(dir .. "/games.dat", dir)
|
loadGamesDat(dir .. "/games.dat", dir)
|
||||||
elseif dirattr.mode == "directory" then
|
elseif dirattr and (dirattr.mode == "directory") then
|
||||||
for file in lfs.dir(dir .. "/.") do
|
for file in lfs.dir(dir .. "/.") do
|
||||||
if file == "games.dat" then
|
if file == "games.dat" then
|
||||||
loadGamesDat(dir .. "/games.dat", nil)
|
loadGamesDat(dir .. "/games.dat", nil)
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,6 @@ local detailClock = 0
|
||||||
local SCROLL_HOLD = 3.0 -- Seconds it rests at each end before moving, as in the overlay
|
local SCROLL_HOLD = 3.0 -- Seconds it rests at each end before moving, as in the overlay
|
||||||
local SCROLL_STEP = 1.1 -- renderer, and seconds a step while it is moving.
|
local SCROLL_STEP = 1.1 -- renderer, and seconds a step while it is moving.
|
||||||
local SCROLL_LINE = 16 -- How far a step goes: a line of the description.
|
local SCROLL_LINE = 16 -- How far a step goes: a line of the description.
|
||||||
local backdropRoot = nil
|
|
||||||
local backdropGrid = nil
|
|
||||||
|
|
||||||
|
|
||||||
function menuElement(id)
|
function menuElement(id)
|
||||||
|
|
@ -358,7 +356,9 @@ end
|
||||||
|
|
||||||
MENU_RENDER.frame = function()
|
MENU_RENDER.frame = function()
|
||||||
-- Attract Mode Video, under the hole the document leaves for it
|
-- Attract Mode Video, under the hole the document leaves for it
|
||||||
|
if VIDEO_ATTRACT then
|
||||||
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
||||||
|
end
|
||||||
if scrollPending then
|
if scrollPending then
|
||||||
menuScrollToSelection()
|
menuScrollToSelection()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,9 @@ MENU_RENDER.frame = function()
|
||||||
spriteDraw(spriteMarquee, x, y)
|
spriteDraw(spriteMarquee, x, y)
|
||||||
|
|
||||||
-- Attract mode video
|
-- Attract mode video
|
||||||
|
if VIDEO_ATTRACT then
|
||||||
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
||||||
|
end
|
||||||
|
|
||||||
-- Which game this is, since there is no list to look at. It heads the details rather than
|
-- Which game this is, since there is no list to look at. It heads the details rather than
|
||||||
-- sitting over the cabinet artwork, where it was unreadable against whatever the picture was.
|
-- sitting over the cabinet artwork, where it was unreadable against whatever the picture was.
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,11 @@
|
||||||
--
|
--
|
||||||
-- The pin is of the *public key*, not the certificate, so an ordinary renewal that keeps the key
|
-- The pin is of the *public key*, not the certificate, so an ordinary renewal that keeps the key
|
||||||
-- does not lock out every cabinet; two pins are carried so a key can actually be rotated.
|
-- does not lock out every cabinet; two pins are carried so a key can actually be rotated.
|
||||||
|
--
|
||||||
|
-- Without a pin the chain is verified against a CA list. Linux has one; Windows, macOS and the
|
||||||
|
-- handhelds have nothing LuaSec can read, so the engine extracts Mozilla's list as
|
||||||
|
-- Singe/cacerts.pem beside the other support files and that is used wherever the platform's own
|
||||||
|
-- is missing.
|
||||||
|
|
||||||
|
|
||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
|
|
@ -53,6 +58,9 @@ NET_REDIRECT_MAX = 5
|
||||||
-- host's own trust store is used instead, which is what a self-hosted server without a pin gets.
|
-- host's own trust store is used instead, which is what a self-hosted server without a pin gets.
|
||||||
NET_PINS = {}
|
NET_PINS = {}
|
||||||
|
|
||||||
|
NET_CA_SYSTEM = "/etc/ssl/certs/ca-certificates.crt" -- Linux's list, when it is there,
|
||||||
|
NET_CA_SHIPPED = "Singe/cacerts.pem" -- and the one Singe carries otherwise.
|
||||||
|
|
||||||
local active = {}
|
local active = {}
|
||||||
local nextId = 1
|
local nextId = 1
|
||||||
|
|
||||||
|
|
@ -149,17 +157,28 @@ local function buildRequest(request)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The CA list a connection without a pin is verified against.
|
||||||
|
local function caFile()
|
||||||
|
local lfs = require("lfs")
|
||||||
|
|
||||||
|
if lfs.attributes(NET_CA_SYSTEM, "mode") == "file" then
|
||||||
|
return NET_CA_SYSTEM
|
||||||
|
end
|
||||||
|
|
||||||
|
return NET_CA_SHIPPED
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
local function startTls(request)
|
local function startTls(request)
|
||||||
local params = {
|
local params = {
|
||||||
mode = "client",
|
mode = "client",
|
||||||
protocol = "any",
|
protocol = "any",
|
||||||
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1" },
|
options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1" },
|
||||||
-- With a pin, the chain is checked against the key rather than a CA list, and the pin is
|
-- With a pin, the chain is checked against the key rather than a CA list, and the pin is
|
||||||
-- what decides. Without one, the host's own store is asked -- and if it has nothing to
|
-- what decides. Without one, a CA list is asked -- and if it has nothing to say, the
|
||||||
-- say, the request fails rather than proceeding unverified.
|
-- request fails rather than proceeding unverified.
|
||||||
verify = (#NET_PINS > 0) and "none" or "peer",
|
verify = (#NET_PINS > 0) and "none" or "peer",
|
||||||
capath = "/etc/ssl/certs",
|
cafile = caFile(),
|
||||||
cafile = "/etc/ssl/certs/ca-certificates.crt",
|
|
||||||
}
|
}
|
||||||
local wrapped, err = ssl.wrap(request.socket, params)
|
local wrapped, err = ssl.wrap(request.socket, params)
|
||||||
|
|
||||||
|
|
@ -238,6 +257,12 @@ local function splitHead(request)
|
||||||
end
|
end
|
||||||
request.length = tonumber(request.responseHeaders["content-length"])
|
request.length = tonumber(request.responseHeaders["content-length"])
|
||||||
request.got = 0
|
request.got = 0
|
||||||
|
-- HTTP/1.1 may send the body in sized pieces instead of announcing a length; those are
|
||||||
|
-- unwrapped as they arrive and the last, empty, piece is the end of the body.
|
||||||
|
request.chunked = (request.responseHeaders["transfer-encoding"] or ""):lower():find("chunked", 1, true) ~= nil
|
||||||
|
request.chunkBuffer = ""
|
||||||
|
request.chunkLeft = nil
|
||||||
|
request.chunkDone = false
|
||||||
if request.toFile then
|
if request.toFile then
|
||||||
local file, err = io.open(request.toFile, "wb")
|
local file, err = io.open(request.toFile, "wb")
|
||||||
if not file then
|
if not file then
|
||||||
|
|
@ -263,6 +288,46 @@ local function consume(request, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Body bytes arriving under chunked transfer encoding: a hexadecimal size on a line, that many
|
||||||
|
-- bytes, a blank line, again until a size of zero. Whatever is left over waits for the next read.
|
||||||
|
local function consumeChunked(request, data)
|
||||||
|
request.chunkBuffer = request.chunkBuffer .. data
|
||||||
|
while not request.chunkDone do
|
||||||
|
if request.chunkLeft == nil then
|
||||||
|
local at = request.chunkBuffer:find("\r\n", 1, true)
|
||||||
|
if not at then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local size = tonumber(request.chunkBuffer:sub(1, at - 1):match("^%x+"), 16)
|
||||||
|
request.chunkBuffer = request.chunkBuffer:sub(at + 2)
|
||||||
|
if size == nil then
|
||||||
|
return fail(request, "the server sent a chunk with no size")
|
||||||
|
end
|
||||||
|
if size == 0 then
|
||||||
|
request.chunkDone = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
request.chunkLeft = size
|
||||||
|
elseif request.chunkLeft > 0 then
|
||||||
|
local take = request.chunkBuffer:sub(1, request.chunkLeft)
|
||||||
|
if #take == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
request.chunkBuffer = request.chunkBuffer:sub(#take + 1)
|
||||||
|
request.chunkLeft = request.chunkLeft - #take
|
||||||
|
consume(request, take)
|
||||||
|
else
|
||||||
|
-- The blank line after a chunk's bytes.
|
||||||
|
if #request.chunkBuffer < 2 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
request.chunkBuffer = request.chunkBuffer:sub(3)
|
||||||
|
request.chunkLeft = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
local function complete(request)
|
local function complete(request)
|
||||||
local body = request.file and "" or table.concat(request.parts)
|
local body = request.file and "" or table.concat(request.parts)
|
||||||
|
|
||||||
|
|
@ -277,6 +342,7 @@ local function complete(request)
|
||||||
local _, _, port, path = parseUrl(location)
|
local _, _, port, path = parseUrl(location)
|
||||||
request.port, request.path = port, path
|
request.port, request.path = port, path
|
||||||
request.buffer, request.parts, request.outgoing, request.sent = "", {}, nil, 0
|
request.buffer, request.parts, request.outgoing, request.sent = "", {}, nil, 0
|
||||||
|
request.chunked, request.chunkBuffer, request.chunkLeft, request.chunkDone = false, "", nil, false
|
||||||
if request.socket then pcall(function() request.socket:close() end) end
|
if request.socket then pcall(function() request.socket:close() end) end
|
||||||
request.socket = nil
|
request.socket = nil
|
||||||
return beginConnect(request)
|
return beginConnect(request)
|
||||||
|
|
@ -298,13 +364,22 @@ local function pumpReading(request)
|
||||||
if splitHead(request) and #request.buffer > 0 then
|
if splitHead(request) and #request.buffer > 0 then
|
||||||
local rest = request.buffer
|
local rest = request.buffer
|
||||||
request.buffer = ""
|
request.buffer = ""
|
||||||
|
if request.chunked then
|
||||||
|
consumeChunked(request, rest)
|
||||||
|
else
|
||||||
consume(request, rest)
|
consume(request, rest)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
elseif request.chunked then
|
||||||
|
consumeChunked(request, got)
|
||||||
else
|
else
|
||||||
consume(request, got)
|
consume(request, got)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if request.responseHeaders and request.length and request.got >= request.length then
|
if not active[request.id] then
|
||||||
|
return -- A bad chunk failed the request from inside consumeChunked.
|
||||||
|
end
|
||||||
|
if request.responseHeaders and ((request.length and request.got >= request.length) or request.chunkDone) then
|
||||||
return complete(request)
|
return complete(request)
|
||||||
end
|
end
|
||||||
if err == "closed" then
|
if err == "closed" then
|
||||||
|
|
|
||||||
|
|
@ -1214,7 +1214,7 @@ end
|
||||||
-- A .game installed here that the catalogue no longer lists. Without this it would vanish from
|
-- A .game installed here that the catalogue no longer lists. Without this it would vanish from
|
||||||
-- the page the moment it was withdrawn, leaving a player with a game they can play, cannot update,
|
-- the page the moment it was withdrawn, leaving a player with a game they can play, cannot update,
|
||||||
-- and cannot remove from anywhere but a file manager.
|
-- and cannot remove from anywhere but a file manager.
|
||||||
function shopAddWithdrawn()
|
local function shopAddWithdrawn()
|
||||||
local listed = {}
|
local listed = {}
|
||||||
|
|
||||||
for _, game in ipairs(shopGames) do
|
for _, game in ipairs(shopGames) do
|
||||||
|
|
|
||||||
3004
assets/cacerts.pem
Normal file
3004
assets/cacerts.pem
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,15 +1,19 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build the Singe manual from docs/Manual.adoc.
|
# Build the Singe manual from docs/Manual.adoc and the Forge manual from docs/Forge.adoc.
|
||||||
# Produces .builddir/Manual.html and .builddir/Manual.pdf. The version comes from CMakeLists.txt.
|
# Produces .builddir/Manual.html, Manual.pdf, Forge.html and Forge.pdf. The version comes from
|
||||||
|
# CMakeLists.txt.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
here=$(cd "$(dirname "$0")" && pwd)
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
src=$here/docs/Manual.adoc
|
src=$here/docs/Manual.adoc
|
||||||
|
forge=$here/docs/Forge.adoc
|
||||||
|
|
||||||
if [[ ! -f $src ]]; then
|
for doc in "$src" "$forge"; do
|
||||||
echo "error: $src not found" >&2
|
if [[ ! -f $doc ]]; then
|
||||||
|
echo "error: $doc not found" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
for tool in asciidoctor asciidoctor-pdf; do
|
for tool in asciidoctor asciidoctor-pdf; do
|
||||||
if ! command -v "$tool" > /dev/null; then
|
if ! command -v "$tool" > /dev/null; then
|
||||||
echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
|
echo "error: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
|
||||||
|
|
@ -23,6 +27,8 @@ out=$here/.builddir
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
asciidoctor -a revnumber="$version" "$src" -o "$out/Manual.html"
|
asciidoctor -a revnumber="$version" "$src" -o "$out/Manual.html"
|
||||||
asciidoctor-pdf -a revnumber="$version" "$src" -o "$out/Manual.pdf"
|
asciidoctor-pdf -a revnumber="$version" "$src" -o "$out/Manual.pdf"
|
||||||
|
asciidoctor -a revnumber="$version" "$forge" -o "$out/Forge.html"
|
||||||
|
asciidoctor-pdf -a revnumber="$version" "$forge" -o "$out/Forge.pdf"
|
||||||
|
|
||||||
echo "built:"
|
echo "built:"
|
||||||
ls -la "$out/Manual.html" "$out/Manual.pdf"
|
ls -la "$out/Manual.html" "$out/Manual.pdf" "$out/Forge.html" "$out/Forge.pdf"
|
||||||
|
|
|
||||||
|
|
@ -457,18 +457,23 @@ ExternalProject_Add(singe
|
||||||
# Forge, packed into .builddir beside the binary so it can be copied out and tested. It is NOT
|
# Forge, packed into .builddir beside the binary so it can be copied out and tested. It is NOT
|
||||||
# part of the engine and nothing of it is embedded; this only puts the distributable where the
|
# part of the engine and nothing of it is embedded; this only puts the distributable where the
|
||||||
# binaries already are. Only a build that produces a runnable binary can pack it, since packing is
|
# binaries already are. Only a build that produces a runnable binary can pack it, since packing is
|
||||||
# the engine's own --pack, so cross builds skip it and the host build does the work.
|
# the engine's own --pack, so cross builds skip it and the host build does the work. The manual,
|
||||||
|
# rendered by the engine's own build into .builddir, goes in beside the scripts: Forge copies it
|
||||||
|
# out to its data directory on its first run.
|
||||||
if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux"))
|
if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux"))
|
||||||
file(GLOB forgeSources ${CMAKE_SOURCE_DIR}/assets/Forge/*)
|
file(GLOB forgeSources ${CMAKE_SOURCE_DIR}/assets/Forge/*)
|
||||||
set(forgePack ${CMAKE_SOURCE_DIR}/.builddir/Forge.game)
|
set(forgePack ${CMAKE_SOURCE_DIR}/.builddir/Forge.game)
|
||||||
|
set(forgeManual ${CMAKE_SOURCE_DIR}/.builddir/Forge.pdf)
|
||||||
|
set(forgeStage ${SB_PREFIX}/forgepack)
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${forgePack}
|
OUTPUT ${forgePack}
|
||||||
# Packing refuses to overwrite, and a stale pack is worse than none.
|
# Packing refuses to overwrite, and a stale pack is worse than none.
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -f ${forgePack}
|
COMMAND ${CMAKE_COMMAND} -E rm -rf ${forgePack} ${forgeStage}
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${SB_PREFIX}/forgepack
|
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets/Forge ${forgeStage}/Forge
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
|
||||||
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
|
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
|
||||||
COMMAND ${CMAKE_COMMAND} -E chdir ${SB_PREFIX}/forgepack ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${CMAKE_SOURCE_DIR}/assets/Forge ${forgePack}
|
COMMAND ${CMAKE_COMMAND} -E chdir ${forgeStage} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${forgeStage}/Forge ${forgePack}
|
||||||
DEPENDS singe ${forgeSources}
|
DEPENDS singe ${forgeSources} ${forgeManual}
|
||||||
COMMENT "Packing Forge into .builddir/Forge.game"
|
COMMENT "Packing Forge into .builddir/Forge.game"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
|
||||||
253
docs/Forge.adoc
Normal file
253
docs/Forge.adoc
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
= Forge
|
||||||
|
Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
:revnumber: 3.00
|
||||||
|
:revdate: 2026
|
||||||
|
:doctype: book
|
||||||
|
:toc: left
|
||||||
|
:toclevels: 3
|
||||||
|
:sectnums:
|
||||||
|
:sectnumlevels: 3
|
||||||
|
:source-highlighter: rouge
|
||||||
|
:icons: font
|
||||||
|
:experimental:
|
||||||
|
|
||||||
|
[preface]
|
||||||
|
== About Forge
|
||||||
|
|
||||||
|
Forge is Singe's authoring tool: a way to make a game by describing it --
|
||||||
|
placing things, attaching behaviours, writing rules -- and having ordinary
|
||||||
|
Singe Lua written for you. It is itself a Singe game and is distributed on
|
||||||
|
its own, beside the engine rather than inside it. This document covers the
|
||||||
|
description format, the vocabulary, the compiler and the editor. The engine
|
||||||
|
calls the generated code makes (`playerNew`, `collidePointRect`,
|
||||||
|
`onKeyPressed`, `scriptPush` and the rest) are documented in the Singe Manual.
|
||||||
|
|
||||||
|
== Describing a Game Instead of Writing One
|
||||||
|
|
||||||
|
A game can be written as a *description* -- a table of layers, entities and
|
||||||
|
rules -- and compiled into an ordinary Singe game.
|
||||||
|
`Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime
|
||||||
|
the result calls. **No part of Forge ships with Singe** -- not the editor, not
|
||||||
|
the compiler, not the runtime -- so a game built with it carries its own copy of
|
||||||
|
that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules
|
||||||
|
become real Lua `if` statements, so a description costs nothing per frame on a
|
||||||
|
Raspberry Pi, and the game it produces can be opened, read and edited by hand
|
||||||
|
like any other.
|
||||||
|
|
||||||
|
There is no notion of genre anywhere in it. A game declares which of the
|
||||||
|
engine's own layers it uses, and that is the only difference between a light
|
||||||
|
gun game, a platformer and a quick-time event over video.
|
||||||
|
|
||||||
|
=== The three nouns
|
||||||
|
|
||||||
|
*Layers* are what the game draws through: `world2d` (physics in the XY plane,
|
||||||
|
drawn into the overlay), `overlay` (flat drawing over everything) and `disc`
|
||||||
|
(the video the game is played over). A game lists the ones it wants.
|
||||||
|
|
||||||
|
*Entities* are things on a layer. Each has a position, a `look` (`box`,
|
||||||
|
`sprite` or `text`) and any number of behaviours. Every entity is a node,
|
||||||
|
whether or not the 3D scene is drawing, which is what lets a 2D game built this
|
||||||
|
way run on a machine with no GPU.
|
||||||
|
|
||||||
|
*Behaviours* are bundles over engine calls that already exist: `platformer` is
|
||||||
|
the character controller (`playerNew` and friends), `solid` is a
|
||||||
|
static body, `drift` moves at a constant velocity. Attaching one is a line in
|
||||||
|
the description rather than code.
|
||||||
|
|
||||||
|
*Rules* are conditions and actions. Every rule is tested every frame, in the
|
||||||
|
order written, and all of a rule's conditions must hold for its actions to run.
|
||||||
|
|
||||||
|
.A description, in full
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
return {
|
||||||
|
title = "One rule",
|
||||||
|
layers = { { kind = "world2d", gravity = 1500 } },
|
||||||
|
entities = {
|
||||||
|
{ id = "ground", x = 360, y = 440,
|
||||||
|
look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
|
||||||
|
behaviours = { { kind = "solid" } } },
|
||||||
|
{ id = "hero", x = 120, y = 380,
|
||||||
|
look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
|
||||||
|
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
|
||||||
|
},
|
||||||
|
rules = {
|
||||||
|
{ note = "Run right",
|
||||||
|
when = { { "keyHeld", key = "RIGHT" } },
|
||||||
|
act = { { "run", entity = "hero", direction = 1 } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Compile it and run what comes out:
|
||||||
|
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
dofile("Forge/AuthorCompile.singe")
|
||||||
|
dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
|
||||||
|
----
|
||||||
|
|
||||||
|
`testScripts/author/platformer.game` and `testScripts/author/qte.game` are
|
||||||
|
worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile
|
||||||
|
and play them.
|
||||||
|
|
||||||
|
=== The vocabulary, and adding to it
|
||||||
|
|
||||||
|
Conditions and actions are not built into the compiler. Each is an entry in
|
||||||
|
the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind
|
||||||
|
of game is a set of entries rather than a new release. The conditions today
|
||||||
|
are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`,
|
||||||
|
`touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`,
|
||||||
|
`moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`.
|
||||||
|
|
||||||
|
`once` deserves a word. Rules run every frame, so anything that should happen
|
||||||
|
a single time -- a door opening, a score awarded -- needs it:
|
||||||
|
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
{ when = { { "touching", entity = "hero", other = "prize" },
|
||||||
|
{ "once", tag = "prize" } },
|
||||||
|
act = { { "addScore", amount = 100 },
|
||||||
|
{ "show", entity = "prize", visible = false } } }
|
||||||
|
----
|
||||||
|
|
||||||
|
=== The way out
|
||||||
|
|
||||||
|
The `lua` action takes a line of Lua and emits it as it stands. It is there on
|
||||||
|
purpose: when a rule needs something the vocabulary cannot say, that rule drops
|
||||||
|
to Lua and the rest of the game is unaffected. A description is a convenience,
|
||||||
|
not a cage, and the compiled output is a normal game you can stop describing
|
||||||
|
and start editing whenever it suits you.
|
||||||
|
|
||||||
|
=== The editor
|
||||||
|
|
||||||
|
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
||||||
|
its own directory beside the games, appears in the menu like one, and packs to
|
||||||
|
`Forge.game` with `--pack`; the build does that itself (the `forge` target),
|
||||||
|
with this manual inside.
|
||||||
|
Nothing in it is a preview: the canvas is the same overlay at the same
|
||||||
|
coordinates the game will be played in, so what is placed is what is seen.
|
||||||
|
|
||||||
|
Started from the menu it opens on a chooser: the descriptions in its data
|
||||||
|
directory, any dropped into the `Forge` directory itself (those are opened as a
|
||||||
|
copy, since inside a `.game` they are read only), and *New game*, which writes
|
||||||
|
a starter -- ground, a hero that runs and jumps, a score readout -- and opens
|
||||||
|
it. The first run copies this manual, `Forge.pdf`, out of `Forge.game` into
|
||||||
|
that data directory, and the chooser says where it is. `Esc` in the editor closes the description (twice, when it has unsaved
|
||||||
|
changes) and `Esc` on the chooser leaves Forge. `P` plays: the description is
|
||||||
|
saved, compiled beside a copy of the runtime, and handed to the engine with
|
||||||
|
`scriptPush`; when the game ends Forge comes back on the same file.
|
||||||
|
|
||||||
|
A script can drive the editor instead, which is how the test scenes do it:
|
||||||
|
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
FORGE_LIBRARY = true
|
||||||
|
dofile("Forge/Forge.singe")
|
||||||
|
forgeBegin("mygame.game")
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
local x, y = mouseGetPosition(0)
|
||||||
|
|
||||||
|
forgeDraw(x, y)
|
||||||
|
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
The entity list and the details are an RmlUi document; the canvas beside them is
|
||||||
|
drawn into the overlay and picked with `collidePointRect`.
|
||||||
|
The two compose because the engine offers a button to the GUI first and passes
|
||||||
|
on what it did not use, while pointer motion is never consumed at all -- so
|
||||||
|
point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse
|
||||||
|
callbacks and clicks on the panels will not reach the canvas.
|
||||||
|
|
||||||
|
`forgeSave()` writes the description back, `forgeBuild(path)`
|
||||||
|
compiles what is on screen (and puts the runtime beside it, so the result
|
||||||
|
plays), and `forgeMove(index, x, y)` moves an entity without a pointer, which
|
||||||
|
is how `testScripts/scene54.singe` drives it.
|
||||||
|
|
||||||
|
=== Editing the entities
|
||||||
|
|
||||||
|
`A` adds a box in the middle of the canvas, `D` duplicates the selected entity
|
||||||
|
a little to one side, and `Delete` removes it. `Enter` walks the entity's
|
||||||
|
fields the same way it walks a rule's parameters: `id`, `x`, `y`, the look's
|
||||||
|
`kind`, then whatever that look takes, then `behaviours` -- typed as a list of
|
||||||
|
kinds, `platformer, solid` -- and then each behaviour's own parameters as
|
||||||
|
`platformer.speed` and so on. The fields come from the `AUTHOR` manifest, so a
|
||||||
|
new look or behaviour is editable the moment it is declared. Renaming an
|
||||||
|
entity renames it in every rule that talks about it. A `sprite` look is drawn
|
||||||
|
with its image once the file name is right, and as a box until then.
|
||||||
|
|
||||||
|
From a script, `forgeEntityAdd(x, y)`, `forgeEntityDuplicate()`,
|
||||||
|
`forgeEntityDelete()` and `forgeEntitySet(entity, field, value)` do the same.
|
||||||
|
|
||||||
|
=== Editing the rules
|
||||||
|
|
||||||
|
The panel shows either the entities or the event sheet; `Tab` swaps them. In
|
||||||
|
the rules, the selected rule opens in place and its conditions and actions are
|
||||||
|
listed under it, because a rule only means anything whole -- a `when` without a
|
||||||
|
`then` tells you nothing.
|
||||||
|
|
||||||
|
Point `onKeyPressed` at `forgeKey` and the whole editor
|
||||||
|
works without a pointer, which is how the bundled menu has always been driven
|
||||||
|
and what a cabinet wants:
|
||||||
|
|
||||||
|
[cols="1,4"]
|
||||||
|
|===
|
||||||
|
| `Tab` | entities or rules
|
||||||
|
| Up, Down | move through the list, and through the parts of the open rule
|
||||||
|
| Left, Right | slide the panel
|
||||||
|
| `Enter` | type a value for whatever is selected; again for its next value
|
||||||
|
| `Esc` | put the value back
|
||||||
|
| `A`, `D`, `Delete` | add, duplicate, delete the selected entity
|
||||||
|
| `N` | a new rule
|
||||||
|
| `C`, `T` | add a condition, an action -- picked from the vocabulary, with its help beside it
|
||||||
|
| `[`, `]` | move the rule up or down the sheet
|
||||||
|
| `Delete` | delete the selected rule, or the selected condition or action
|
||||||
|
| `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history
|
||||||
|
| `S`, `B`, `P` | save, build, play
|
||||||
|
|===
|
||||||
|
|
||||||
|
With the rule itself selected rather than one of its parts, `Enter` edits its
|
||||||
|
note.
|
||||||
|
|
||||||
|
A typed number comes back a number rather than a string, because `100` and
|
||||||
|
`"100"` compile to different source and a description that changed shape when a
|
||||||
|
value was retyped would stop round-tripping.
|
||||||
|
|
||||||
|
From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act",
|
||||||
|
name)`, `forgePartSet(key, value)`, `forgePartDelete()`, `forgeRuleMove(by)`,
|
||||||
|
`forgeUndo()` and `forgeRedo()` do the same work. Adding a condition selects it, so its
|
||||||
|
parameters can be set at once.
|
||||||
|
|
||||||
|
The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs
|
||||||
|
changing when a condition or an action is added: it offers whatever is
|
||||||
|
declared. A new parameter gets a sensible starting value for its type, so a
|
||||||
|
rule compiles the moment it is made rather than only once every field is
|
||||||
|
filled in.
|
||||||
|
|
||||||
|
A description survives the round trip: loading one, saving it and loading it
|
||||||
|
again compiles to the same game, byte for byte. The editor depends on that and
|
||||||
|
the test asserts it.
|
||||||
|
|
||||||
|
=== Releasing a game
|
||||||
|
|
||||||
|
`forgeExport(folder, name)` writes everything a finished game needs into a
|
||||||
|
directory of its own: the compiled script, a `games.dat` so the menu lists it,
|
||||||
|
the description it was built from so it can be opened again, and **a copy of
|
||||||
|
the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
|
||||||
|
like any other, and it runs on a machine that has never had Forge on it.
|
||||||
|
|
||||||
|
Every built game finds its own directory to load that runtime from, using
|
||||||
|
`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
|
||||||
|
engine was *launched* with, so a game reached by `dofile` -- a test, a
|
||||||
|
launcher, a preview -- would otherwise look beside the caller.
|
||||||
|
|
||||||
|
The panel slides. Drag the tab on its outer edge and it moves across the
|
||||||
|
window, so an entity that lives underneath it -- a score readout at `12, 12`
|
||||||
|
does -- is never permanently out of reach. `forgePanelTo(x)` moves it from
|
||||||
|
a script, and `forgeOverPanel(x)` says whether a point is currently
|
||||||
|
covered.
|
||||||
|
|
||||||
|
[#migrating]
|
||||||
192
docs/Manual.adoc
192
docs/Manual.adoc
|
|
@ -1421,7 +1421,6 @@ GAMES = {
|
||||||
TITLE = ".38 Ambush Alley",
|
TITLE = ".38 Ambush Alley",
|
||||||
SCRIPT = "ActionMax/38AmbushAlley.singe",
|
SCRIPT = "ActionMax/38AmbushAlley.singe",
|
||||||
VIDEO = "ActionMax/frame_38AmbushAlley.txt",
|
VIDEO = "ActionMax/frame_38AmbushAlley.txt",
|
||||||
DATA = "ActionMax",
|
|
||||||
STRETCH = false,
|
STRETCH = false,
|
||||||
NO_MOUSE = false,
|
NO_MOUSE = false,
|
||||||
RESOLUTION_X = 720,
|
RESOLUTION_X = 720,
|
||||||
|
|
@ -3192,195 +3191,12 @@ die.
|
||||||
=== Describing a Game Instead of Writing One
|
=== Describing a Game Instead of Writing One
|
||||||
|
|
||||||
A game can be written as a *description* -- a table of layers, entities and
|
A game can be written as a *description* -- a table of layers, entities and
|
||||||
rules -- and compiled into an ordinary Singe game.
|
rules -- and compiled into an ordinary Singe game by Forge, the authoring
|
||||||
`Forge/AuthorCompile.singe` does the compiling and `Author.singe` is the runtime
|
tool. Forge is distributed on its own, beside the engine, and has its own
|
||||||
the result calls. **No part of Forge ships with Singe** -- not the editor, not
|
manual: `docs/Forge.adoc` in the source tree, built to `Forge.html` and
|
||||||
the compiler, not the runtime -- so a game built with it carries its own copy of
|
`Forge.pdf` beside this one.
|
||||||
that runtime and stands entirely on its own. Nothing is interpreted at run time: the rules
|
|
||||||
become real Lua `if` statements, so a description costs nothing per frame on a
|
|
||||||
Raspberry Pi, and the game it produces can be opened, read and edited by hand
|
|
||||||
like any other.
|
|
||||||
|
|
||||||
There is no notion of genre anywhere in it. A game declares which of the
|
|
||||||
engine's own layers it uses, and that is the only difference between a light
|
|
||||||
gun game, a platformer and a quick-time event over video.
|
|
||||||
|
|
||||||
==== The three nouns
|
|
||||||
|
|
||||||
*Layers* are what the game draws through: `world2d` (physics in the XY plane,
|
|
||||||
drawn into the overlay), `overlay` (flat drawing over everything) and `disc`
|
|
||||||
(the video the game is played over). A game lists the ones it wants.
|
|
||||||
|
|
||||||
*Entities* are things on a layer. Each has a position, a `look` (`box`,
|
|
||||||
`sprite` or `text`) and any number of behaviours. Every entity is a node,
|
|
||||||
whether or not the 3D scene is drawing, which is what lets a 2D game built this
|
|
||||||
way run on a machine with no GPU.
|
|
||||||
|
|
||||||
*Behaviours* are bundles over engine calls that already exist: `platformer` is
|
|
||||||
the character controller (<<playernew,playerNew>> and friends), `solid` is a
|
|
||||||
static body, `drift` moves at a constant velocity. Attaching one is a line in
|
|
||||||
the description rather than code.
|
|
||||||
|
|
||||||
*Rules* are conditions and actions. Every rule is tested every frame, in the
|
|
||||||
order written, and all of a rule's conditions must hold for its actions to run.
|
|
||||||
|
|
||||||
.A description, in full
|
|
||||||
[source,lua]
|
|
||||||
----
|
|
||||||
return {
|
|
||||||
title = "One rule",
|
|
||||||
layers = { { kind = "world2d", gravity = 1500 } },
|
|
||||||
entities = {
|
|
||||||
{ id = "ground", x = 360, y = 440,
|
|
||||||
look = { kind = "box", w = 720, h = 40, r = 60, g = 70, b = 90 },
|
|
||||||
behaviours = { { kind = "solid" } } },
|
|
||||||
{ id = "hero", x = 120, y = 380,
|
|
||||||
look = { kind = "box", w = 24, h = 44, r = 230, g = 90, b = 170 },
|
|
||||||
behaviours = { { kind = "platformer", speed = 210, jump = 620 } } }
|
|
||||||
},
|
|
||||||
rules = {
|
|
||||||
{ note = "Run right",
|
|
||||||
when = { { "keyHeld", key = "RIGHT" } },
|
|
||||||
act = { { "run", entity = "hero", direction = 1 } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
Compile it and run what comes out:
|
|
||||||
|
|
||||||
[source,lua]
|
|
||||||
----
|
|
||||||
dofile("Forge/AuthorCompile.singe")
|
|
||||||
dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
|
|
||||||
----
|
|
||||||
|
|
||||||
`testScripts/author/platformer.game` and `testScripts/author/qte.game` are
|
|
||||||
worked examples, and `testScripts/scene52.singe` and `scene53.singe` compile
|
|
||||||
and play them.
|
|
||||||
|
|
||||||
==== The vocabulary, and adding to it
|
|
||||||
|
|
||||||
Conditions and actions are not built into the compiler. Each is an entry in
|
|
||||||
the `AUTHOR` table declaring its parameters and the Lua it emits, so a new kind
|
|
||||||
of game is a set of entries rather than a new release. The conditions today
|
|
||||||
are `keyHeld`, `switchHeld`, `timeBetween`, `discBetween`, `onGround`,
|
|
||||||
`touching`, `below`, `flagSet` and `once`; the actions are `run`, `jump`,
|
|
||||||
`moveTo`, `setText`, `show`, `addScore`, `setFlag`, `discTo` and `lua`.
|
|
||||||
|
|
||||||
`once` deserves a word. Rules run every frame, so anything that should happen
|
|
||||||
a single time -- a door opening, a score awarded -- needs it:
|
|
||||||
|
|
||||||
[source,lua]
|
|
||||||
----
|
|
||||||
{ when = { { "touching", entity = "hero", other = "prize" },
|
|
||||||
{ "once", tag = "prize" } },
|
|
||||||
act = { { "addScore", amount = 100 },
|
|
||||||
{ "show", entity = "prize", visible = false } } }
|
|
||||||
----
|
|
||||||
|
|
||||||
==== The way out
|
|
||||||
|
|
||||||
The `lua` action takes a line of Lua and emits it as it stands. It is there on
|
|
||||||
purpose: when a rule needs something the vocabulary cannot say, that rule drops
|
|
||||||
to Lua and the rest of the game is unaffected. A description is a convenience,
|
|
||||||
not a cage, and the compiled output is a normal game you can stop describing
|
|
||||||
and start editing whenever it suits you.
|
|
||||||
|
|
||||||
==== The editor
|
|
||||||
|
|
||||||
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
|
||||||
its own directory beside the games, appears in the menu like one, and packs to
|
|
||||||
`Forge.game` with `--pack`.
|
|
||||||
Nothing in it is a preview: the canvas is the same overlay at the same
|
|
||||||
coordinates the game will be played in, so what is placed is what is seen.
|
|
||||||
|
|
||||||
[source,lua]
|
|
||||||
----
|
|
||||||
FORGE_LIBRARY = true
|
|
||||||
dofile("Forge/Forge.singe")
|
|
||||||
forgeBegin("mygame.game")
|
|
||||||
|
|
||||||
function onOverlayUpdate()
|
|
||||||
local x, y = mouseGetPosition(0)
|
|
||||||
|
|
||||||
forgeDraw(x, y)
|
|
||||||
|
|
||||||
return OVERLAY_UPDATED
|
|
||||||
end
|
|
||||||
----
|
|
||||||
|
|
||||||
The entity list and the details are an RmlUi document; the canvas beside them is
|
|
||||||
drawn into the overlay and picked with <<collidepointrect,collidePointRect>>.
|
|
||||||
The two compose because the engine offers a button to the GUI first and passes
|
|
||||||
on what it did not use, while pointer motion is never consumed at all -- so
|
|
||||||
point `forgePress`, `forgeDrag` and `forgeRelease` at the mouse
|
|
||||||
callbacks and clicks on the panels will not reach the canvas.
|
|
||||||
|
|
||||||
`forgeSave()` writes the description back, `forgeBuild(path)`
|
|
||||||
compiles what is on screen, and `forgeMove(index, x, y)` moves an entity
|
|
||||||
without a pointer, which is how `testScripts/scene54.singe` drives it.
|
|
||||||
|
|
||||||
==== Editing the rules
|
|
||||||
|
|
||||||
The panel shows either the entities or the event sheet; `Tab` swaps them. In
|
|
||||||
the rules, the selected rule opens in place and its conditions and actions are
|
|
||||||
listed under it, because a rule only means anything whole -- a `when` without a
|
|
||||||
`then` tells you nothing.
|
|
||||||
|
|
||||||
Point <<onkeypressed,onKeyPressed>> at `forgeKey` and the whole editor
|
|
||||||
works without a pointer, which is how the bundled menu has always been driven
|
|
||||||
and what a cabinet wants:
|
|
||||||
|
|
||||||
[cols="1,4"]
|
|
||||||
|===
|
|
||||||
| `Tab` | entities or rules
|
|
||||||
| Up, Down | move through the list, and through the parts of the open rule
|
|
||||||
| Left, Right | slide the panel
|
|
||||||
| `Enter` | type a value for the selected condition or action; again for its next value
|
|
||||||
| `Esc` | put the value back
|
|
||||||
| `N` | a new rule
|
|
||||||
| `Delete` | delete the selected rule, or the selected condition or action
|
|
||||||
| `S`, `B` | save, build
|
|
||||||
|===
|
|
||||||
|
|
||||||
A typed number comes back a number rather than a string, because `100` and
|
|
||||||
`"100"` compile to different source and a description that changed shape when a
|
|
||||||
value was retyped would stop round-tripping.
|
|
||||||
|
|
||||||
From a script, `forgeRuleNew(note)`, `forgeRuleAdd("when"|"act",
|
|
||||||
name)`, `forgePartSet(key, value)` and `forgePartDelete()` do the
|
|
||||||
same work. Adding a condition selects it, so its parameters can be set at once.
|
|
||||||
|
|
||||||
The vocabulary comes from the `AUTHOR` manifest, so the rule editor never needs
|
|
||||||
changing when a condition or an action is added: it offers whatever is
|
|
||||||
declared. A new parameter gets a sensible starting value for its type, so a
|
|
||||||
rule compiles the moment it is made rather than only once every field is
|
|
||||||
filled in.
|
|
||||||
|
|
||||||
A description survives the round trip: loading one, saving it and loading it
|
|
||||||
again compiles to the same game, byte for byte. The editor depends on that and
|
|
||||||
the test asserts it.
|
|
||||||
|
|
||||||
==== Releasing a game
|
|
||||||
|
|
||||||
`forgeExport(folder, name)` writes everything a finished game needs into a
|
|
||||||
directory of its own: the compiled script, a `games.dat` so the menu lists it,
|
|
||||||
the description it was built from so it can be opened again, and **a copy of
|
|
||||||
the runtime**, taken out of Forge. `--pack` turns that directory into a `.game`
|
|
||||||
like any other, and it runs on a machine that has never had Forge on it.
|
|
||||||
|
|
||||||
Every built game finds its own directory to load that runtime from, using
|
|
||||||
`debug.getinfo` rather than `DIR`: `DIR` is the directory of the script the
|
|
||||||
engine was *launched* with, so a game reached by `dofile` -- a test, a
|
|
||||||
launcher, a preview -- would otherwise look beside the caller.
|
|
||||||
|
|
||||||
The panel slides. Drag the tab on its outer edge and it moves across the
|
|
||||||
window, so an entity that lives underneath it -- a score readout at `12, 12`
|
|
||||||
does -- is never permanently out of reach. `forgePanelTo(x)` moves it from
|
|
||||||
a script, and `forgeOverPanel(x)` says whether a point is currently
|
|
||||||
covered.
|
|
||||||
|
|
||||||
[#migrating]
|
|
||||||
=== Migrating from Singe 2.10
|
=== Migrating from Singe 2.10
|
||||||
|
|
||||||
Singe 3.00 moved the sprite handle to the first argument of `spriteDraw`,
|
Singe 3.00 moved the sprite handle to the first argument of `spriteDraw`,
|
||||||
|
|
|
||||||
49
src/decode.c
49
src/decode.c
|
|
@ -477,30 +477,6 @@ SDL_Surface *decodeImage(const void *bytes, size_t size) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
SDL_Surface *decodeImageIO(SDL_IOStream *io, bool closeio) {
|
|
||||||
SDL_Surface *surface = IMG_Load_IO(io, false);
|
|
||||||
void *bytes = NULL;
|
|
||||||
size_t size = 0;
|
|
||||||
|
|
||||||
if (surface == NULL) {
|
|
||||||
// SDL_image could not read it. It may still be a TIFF, an OpenEXR, a JPEG 2000, an AVIF or
|
|
||||||
// any of the other pictures the bundled FFmpeg decodes for video.
|
|
||||||
if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) >= 0) {
|
|
||||||
bytes = SDL_LoadFile_IO(io, &size, false);
|
|
||||||
if (bytes != NULL) {
|
|
||||||
surface = decodeImage(bytes, size);
|
|
||||||
SDL_free(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (closeio) {
|
|
||||||
SDL_CloseIO(io);
|
|
||||||
}
|
|
||||||
|
|
||||||
return surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
float *decodeImageFloat(const void *bytes, size_t size, int32_t *width, int32_t *height) {
|
float *decodeImageFloat(const void *bytes, size_t size, int32_t *width, int32_t *height) {
|
||||||
const AVPixFmtDescriptor *description = NULL;
|
const AVPixFmtDescriptor *description = NULL;
|
||||||
SourceT source;
|
SourceT source;
|
||||||
|
|
@ -576,3 +552,28 @@ float *decodeImageFloat(const void *bytes, size_t size, int32_t *width, int32_t
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SDL_Surface *decodeImageIO(SDL_IOStream *io, bool closeio) {
|
||||||
|
SDL_Surface *surface = IMG_Load_IO(io, false);
|
||||||
|
void *bytes = NULL;
|
||||||
|
size_t size = 0;
|
||||||
|
|
||||||
|
if (surface == NULL) {
|
||||||
|
// SDL_image could not read it. It may still be a TIFF, an OpenEXR, a JPEG 2000, an AVIF or
|
||||||
|
// any of the other pictures the bundled FFmpeg decodes for video.
|
||||||
|
if (SDL_SeekIO(io, 0, SDL_IO_SEEK_SET) >= 0) {
|
||||||
|
bytes = SDL_LoadFile_IO(io, &size, false);
|
||||||
|
if (bytes != NULL) {
|
||||||
|
surface = decodeImage(bytes, size);
|
||||||
|
SDL_free(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closeio) {
|
||||||
|
SDL_CloseIO(io);
|
||||||
|
}
|
||||||
|
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
#include "generated/Menu_singe.h"
|
#include "generated/Menu_singe.h"
|
||||||
#include "generated/Tools_singe.h"
|
#include "generated/Tools_singe.h"
|
||||||
#include "generated/Net_singe.h"
|
#include "generated/Net_singe.h"
|
||||||
|
#include "generated/cacerts_pem.h"
|
||||||
#include "generated/Master_singe.h"
|
#include "generated/Master_singe.h"
|
||||||
#include "generated/Backdrop_singe.h"
|
#include "generated/Backdrop_singe.h"
|
||||||
#include "generated/MenuDocument_singe.h"
|
#include "generated/MenuDocument_singe.h"
|
||||||
|
|
|
||||||
|
|
@ -819,9 +819,11 @@ Rml::TextureHandle GuiRenderT::LoadTexture(Rml::Vector2i &textureDimensions, con
|
||||||
SDL_Surface *surface = nullptr;
|
SDL_Surface *surface = nullptr;
|
||||||
SDL_Surface *converted = nullptr;
|
SDL_Surface *converted = nullptr;
|
||||||
Rml::byte *pixels = nullptr;
|
Rml::byte *pixels = nullptr;
|
||||||
|
Rml::byte *row = nullptr;
|
||||||
size_t fileSize = 0;
|
size_t fileSize = 0;
|
||||||
size_t pixelSize = 0;
|
size_t rowSize = 0;
|
||||||
size_t i = 0;
|
size_t x = 0;
|
||||||
|
size_t y = 0;
|
||||||
size_t j = 0;
|
size_t j = 0;
|
||||||
size_t dot = source.rfind('.');
|
size_t dot = source.rfind('.');
|
||||||
Rml::String extension = (dot == Rml::String::npos) ? Rml::String() : source.substr(dot + 1);
|
Rml::String extension = (dot == Rml::String::npos) ? Rml::String() : source.substr(dot + 1);
|
||||||
|
|
@ -865,16 +867,23 @@ Rml::TextureHandle GuiRenderT::LoadTexture(Rml::Vector2i &textureDimensions, con
|
||||||
}
|
}
|
||||||
surface = converted;
|
surface = converted;
|
||||||
}
|
}
|
||||||
// Premultiplied alpha, which compositing needs.
|
// Premultiplied alpha, which compositing needs. Rows are packed as they go, since the surface
|
||||||
pixelSize = (size_t)surface->w * (size_t)surface->h * BYTES_PER_PIXEL;
|
// may pad its pitch and GenerateTexture wants the rows back to back.
|
||||||
|
rowSize = (size_t)surface->w * BYTES_PER_PIXEL;
|
||||||
pixels = static_cast<Rml::byte *>(surface->pixels);
|
pixels = static_cast<Rml::byte *>(surface->pixels);
|
||||||
for (i = 0; i < pixelSize; i += BYTES_PER_PIXEL) {
|
for (y = 0; y < (size_t)surface->h; y++) {
|
||||||
|
row = pixels + (y * (size_t)surface->pitch);
|
||||||
|
for (x = 0; x < rowSize; x += BYTES_PER_PIXEL) {
|
||||||
for (j = 0; j < COLOUR_CHANNELS; j++) {
|
for (j = 0; j < COLOUR_CHANNELS; j++) {
|
||||||
pixels[i + j] = (Rml::byte)((int)pixels[i + j] * (int)pixels[i + COLOUR_CHANNELS] / COLOUR_MAX);
|
row[x + j] = (Rml::byte)((int)row[x + j] * (int)row[x + COLOUR_CHANNELS] / COLOUR_MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row != pixels + (y * rowSize)) {
|
||||||
|
memmove(pixels + (y * rowSize), row, rowSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textureDimensions = { surface->w, surface->h };
|
textureDimensions = { surface->w, surface->h };
|
||||||
handle = GenerateTexture(Rml::Span<const Rml::byte>(pixels, (size_t)surface->pitch * (size_t)surface->h), textureDimensions);
|
handle = GenerateTexture(Rml::Span<const Rml::byte>(pixels, rowSize * (size_t)surface->h), textureDimensions);
|
||||||
SDL_DestroySurface(surface);
|
SDL_DestroySurface(surface);
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
287
src/main.c
287
src/main.c
|
|
@ -287,8 +287,8 @@ static int32_t _optionIndex(int32_t code);
|
||||||
static int32_t _optionNamed(const char *name);
|
static int32_t _optionNamed(const char *name);
|
||||||
static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]);
|
static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]);
|
||||||
static bool _parseBoolean(const char *text, bool *value);
|
static bool _parseBoolean(const char *text, bool *value);
|
||||||
static bool _parseGamepadOrder(const char *text);
|
|
||||||
static bool _parseFloat(const char *text, double *value);
|
static bool _parseFloat(const char *text, double *value);
|
||||||
|
static bool _parseGamepadOrder(const char *text);
|
||||||
static bool _parseInteger(const char *text, int32_t *value);
|
static bool _parseInteger(const char *text, int32_t *value);
|
||||||
static void _requireRange(const char *exeName, const char *source, int32_t value, int32_t min, int32_t max, const char *what, const char *unit);
|
static void _requireRange(const char *exeName, const char *source, int32_t value, int32_t min, int32_t max, const char *what, const char *unit);
|
||||||
static void _requireRangeFloat(const char *exeName, const char *source, double value, double min, double max, const char *what);
|
static void _requireRangeFloat(const char *exeName, const char *source, double value, double min, double max, const char *what);
|
||||||
|
|
@ -950,98 +950,6 @@ static void _crashHandler(int signalNumber) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Which audio formats this build can decode, for a bug report and so a user whose music is silent
|
|
||||||
// can see at a glance whether its format was ever compiled in. The mixer keeps the list.
|
|
||||||
char *mainDescribeAudioDecoders(void) {
|
|
||||||
char *list = strdup("");
|
|
||||||
char *grown = NULL;
|
|
||||||
int32_t count = MIX_GetNumAudioDecoders();
|
|
||||||
int32_t x = 0;
|
|
||||||
|
|
||||||
for (x = 0; x < count; x++) {
|
|
||||||
grown = utilCreateString("%s%s%s", list, (x > 0) ? ", " : "", MIX_GetAudioDecoder(x));
|
|
||||||
free(list);
|
|
||||||
list = grown;
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The processor, for a bug report. Every platform keeps the name somewhere different, and none of
|
|
||||||
// them is worth failing over: the core count is always there as a fallback.
|
|
||||||
char *mainDescribeCpu(void) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
char name[128];
|
|
||||||
DWORD bytes = sizeof(name);
|
|
||||||
|
|
||||||
if (RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, NULL, name, &bytes) == ERROR_SUCCESS) {
|
|
||||||
return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
|
||||||
}
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
char name[128];
|
|
||||||
size_t bytes = sizeof(name);
|
|
||||||
|
|
||||||
if (sysctlbyname("machdep.cpu.brand_string", name, &bytes, NULL, 0) == 0) {
|
|
||||||
return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
char line[256];
|
|
||||||
FILE *info = fopen("/proc/cpuinfo", "r");
|
|
||||||
char *colon = NULL;
|
|
||||||
char *end = NULL;
|
|
||||||
char *found = NULL;
|
|
||||||
|
|
||||||
// /proc/cpuinfo calls it "model name" on x86 and "Model" on a Raspberry Pi. The kernel
|
|
||||||
// reports its size as zero, so it is read a line at a time rather than in one piece.
|
|
||||||
while ((info != NULL) && (found == NULL) && (fgets(line, sizeof(line), info) != NULL)) {
|
|
||||||
if (utilStartsWith(line, "model name") || utilStartsWith(line, "Model")) {
|
|
||||||
colon = strchr(line, ':');
|
|
||||||
if (colon != NULL) {
|
|
||||||
colon++;
|
|
||||||
while (*colon == ' ') {
|
|
||||||
colon++;
|
|
||||||
}
|
|
||||||
for (end = colon + strlen(colon); (end > colon) && ((uint8_t)end[-1] <= ' '); end--) {
|
|
||||||
end[-1] = 0;
|
|
||||||
}
|
|
||||||
found = utilCreateString("%s, %d cores, %d MB", colon, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (info != NULL) {
|
|
||||||
fclose(info);
|
|
||||||
}
|
|
||||||
if (found != NULL) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return utilCreateString("%s, %d cores, %d MB", SDL_GetPlatform(), SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The operating system and its version, for a bug report.
|
|
||||||
char *mainDescribeOs(void) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
char release[64];
|
|
||||||
DWORD bytes = sizeof(release);
|
|
||||||
|
|
||||||
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild", RRF_RT_REG_SZ, NULL, release, &bytes) == ERROR_SUCCESS) {
|
|
||||||
return utilCreateString("%s build %s", SDL_GetPlatform(), release);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
struct utsname system;
|
|
||||||
|
|
||||||
if (uname(&system) == 0) {
|
|
||||||
return utilCreateString("%s %s (%s)", system.sysname, system.release, system.machine);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return strdup(SDL_GetPlatform());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Writes an embedded support file, or rewrites it when the installed copy differs from this build's.
|
// Writes an embedded support file, or rewrites it when the installed copy differs from this build's.
|
||||||
static bool _extractFile(const char *filename, const uint8_t *data, size_t length) {
|
static bool _extractFile(const char *filename, const uint8_t *data, size_t length) {
|
||||||
FILE *out = NULL;
|
FILE *out = NULL;
|
||||||
|
|
@ -1428,33 +1336,6 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --gamepad_reorder is a list of enumeration positions, one per gamepad slot, written as Hypseus
|
|
||||||
// writes it: bare digits (3210) or separated by commas or spaces (3, 2, 1, 0). A repeated position
|
|
||||||
// is the user contradicting themselves and is refused; anything but a digit or a separator is too.
|
|
||||||
static bool _parseGamepadOrder(const char *text) {
|
|
||||||
bool seen[GAMEPAD_ORDER_DIGITS];
|
|
||||||
int32_t digits = 0;
|
|
||||||
int32_t x = 0;
|
|
||||||
|
|
||||||
memset(seen, 0, sizeof(seen));
|
|
||||||
for (x = 0; text[x] != '\0'; x++) {
|
|
||||||
if ((text[x] == ',') || (text[x] == ' ')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((text[x] < '0') || (text[x] > '9')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (seen[text[x] - '0']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
seen[text[x] - '0'] = true;
|
|
||||||
digits++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (digits > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The words a settings file (and --linearscale) may use for a switch.
|
// The words a settings file (and --linearscale) may use for a switch.
|
||||||
static bool _parseBoolean(const char *text, bool *value) {
|
static bool _parseBoolean(const char *text, bool *value) {
|
||||||
static const char *yes[] = { "true", "yes", "on", "1" };
|
static const char *yes[] = { "true", "yes", "on", "1" };
|
||||||
|
|
@ -1494,6 +1375,33 @@ static bool _parseFloat(const char *text, double *value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --gamepad_reorder is a list of enumeration positions, one per gamepad slot, written as Hypseus
|
||||||
|
// writes it: bare digits (3210) or separated by commas or spaces (3, 2, 1, 0). A repeated position
|
||||||
|
// is the user contradicting themselves and is refused; anything but a digit or a separator is too.
|
||||||
|
static bool _parseGamepadOrder(const char *text) {
|
||||||
|
bool seen[GAMEPAD_ORDER_DIGITS];
|
||||||
|
int32_t digits = 0;
|
||||||
|
int32_t x = 0;
|
||||||
|
|
||||||
|
memset(seen, 0, sizeof(seen));
|
||||||
|
for (x = 0; text[x] != '\0'; x++) {
|
||||||
|
if ((text[x] == ',') || (text[x] == ' ')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((text[x] < '0') || (text[x] > '9')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seen[text[x] - '0']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen[text[x] - '0'] = true;
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (digits > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool _parseInteger(const char *text, int32_t *value) {
|
static bool _parseInteger(const char *text, int32_t *value) {
|
||||||
char *end = NULL;
|
char *end = NULL;
|
||||||
long parsed = 0;
|
long parsed = 0;
|
||||||
|
|
@ -1764,6 +1672,29 @@ static void _stopSDL(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Reduces every component that names a packed game to its stem, in place. A game writes to one
|
||||||
|
// data directory whether it was reached as the container the engine opened ("ActionMax.game", which
|
||||||
|
// createDataDirFor already reduces) or by a name that passes through it ("ActionMax.game/video.mkv",
|
||||||
|
// which is how a script names a file inside one). Without this the two disagreed: a packed game's
|
||||||
|
// video index went to data/ActionMax.game while everything else it wrote went to data/ActionMax,
|
||||||
|
// and the library appeared to have two of every game.
|
||||||
|
static void _stripDatabaseNames(char *path) {
|
||||||
|
const size_t extension = strlen(VFS_DATABASE_EXTENSION);
|
||||||
|
size_t i = 0;
|
||||||
|
char next = 0;
|
||||||
|
|
||||||
|
for (i = 0; path[i] != 0; i++) {
|
||||||
|
if (strncasecmp(path + i, VFS_DATABASE_EXTENSION, extension) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next = path[i + extension];
|
||||||
|
if ((next == 0) || (next == '/') || (next == '\\')) {
|
||||||
|
memmove(path + i, path + i + extension, strlen(path + i + extension) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// What a bug report needs, at the top of trace.txt and nowhere else: the build, the command that
|
// What a bug report needs, at the top of trace.txt and nowhere else: the build, the command that
|
||||||
// started it, the machine, and what the engine picked to run on. main.c's own trace lines wait
|
// started it, the machine, and what the engine picked to run on. main.c's own trace lines wait
|
||||||
// behind it so the block is always first and can be pasted whole.
|
// behind it so the block is always first and can be pasted whole.
|
||||||
|
|
@ -1810,6 +1741,7 @@ static void _unpackData(const char *exePath, bool absolute) {
|
||||||
{ "Menu.singe", Menu_singe, Menu_singe_len },
|
{ "Menu.singe", Menu_singe, Menu_singe_len },
|
||||||
{ "Tools.singe", Tools_singe, Tools_singe_len },
|
{ "Tools.singe", Tools_singe, Tools_singe_len },
|
||||||
{ "Net.singe", Net_singe, Net_singe_len },
|
{ "Net.singe", Net_singe, Net_singe_len },
|
||||||
|
{ "cacerts.pem", cacerts_pem, cacerts_pem_len },
|
||||||
{ "Master.singe", Master_singe, Master_singe_len },
|
{ "Master.singe", Master_singe, Master_singe_len },
|
||||||
{ "Backdrop.singe", Backdrop_singe, Backdrop_singe_len },
|
{ "Backdrop.singe", Backdrop_singe, Backdrop_singe_len },
|
||||||
{ "MenuDocument.singe", MenuDocument_singe, MenuDocument_singe_len },
|
{ "MenuDocument.singe", MenuDocument_singe, MenuDocument_singe_len },
|
||||||
|
|
@ -1909,29 +1841,6 @@ ConfigT *cloneConf(const ConfigT *conf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Reduces every component that names a packed game to its stem, in place. A game writes to one
|
|
||||||
// data directory whether it was reached as the container the engine opened ("ActionMax.game", which
|
|
||||||
// createDataDirFor already reduces) or by a name that passes through it ("ActionMax.game/video.mkv",
|
|
||||||
// which is how a script names a file inside one). Without this the two disagreed: a packed game's
|
|
||||||
// video index went to data/ActionMax.game while everything else it wrote went to data/ActionMax,
|
|
||||||
// and the library appeared to have two of every game.
|
|
||||||
static void _stripDatabaseNames(char *path) {
|
|
||||||
const size_t extension = strlen(VFS_DATABASE_EXTENSION);
|
|
||||||
size_t i = 0;
|
|
||||||
char next = 0;
|
|
||||||
|
|
||||||
for (i = 0; path[i] != 0; i++) {
|
|
||||||
if (strncasecmp(path + i, VFS_DATABASE_EXTENSION, extension) != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
next = path[i + extension];
|
|
||||||
if ((next == 0) || (next == '/') || (next == '\\')) {
|
|
||||||
memmove(path + i, path + i + extension, strlen(path + i + extension) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Builds and creates dataDirBase + directory of filename. Returns a new string or NULL on failure.
|
// Builds and creates dataDirBase + directory of filename. Returns a new string or NULL on failure.
|
||||||
char *createDataDir(const char *dataDirBase, const char *filename) {
|
char *createDataDir(const char *dataDirBase, const char *filename) {
|
||||||
const char separator = utilGetPathSeparator();
|
const char separator = utilGetPathSeparator();
|
||||||
|
|
@ -2020,6 +1929,98 @@ bool isFrameFileName(const char *filename) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Which audio formats this build can decode, for a bug report and so a user whose music is silent
|
||||||
|
// can see at a glance whether its format was ever compiled in. The mixer keeps the list.
|
||||||
|
char *mainDescribeAudioDecoders(void) {
|
||||||
|
char *list = strdup("");
|
||||||
|
char *grown = NULL;
|
||||||
|
int32_t count = MIX_GetNumAudioDecoders();
|
||||||
|
int32_t x = 0;
|
||||||
|
|
||||||
|
for (x = 0; x < count; x++) {
|
||||||
|
grown = utilCreateString("%s%s%s", list, (x > 0) ? ", " : "", MIX_GetAudioDecoder(x));
|
||||||
|
free(list);
|
||||||
|
list = grown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The processor, for a bug report. Every platform keeps the name somewhere different, and none of
|
||||||
|
// them is worth failing over: the core count is always there as a fallback.
|
||||||
|
char *mainDescribeCpu(void) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
char name[128];
|
||||||
|
DWORD bytes = sizeof(name);
|
||||||
|
|
||||||
|
if (RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, NULL, name, &bytes) == ERROR_SUCCESS) {
|
||||||
|
return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
||||||
|
}
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
char name[128];
|
||||||
|
size_t bytes = sizeof(name);
|
||||||
|
|
||||||
|
if (sysctlbyname("machdep.cpu.brand_string", name, &bytes, NULL, 0) == 0) {
|
||||||
|
return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
char line[256];
|
||||||
|
FILE *info = fopen("/proc/cpuinfo", "r");
|
||||||
|
char *colon = NULL;
|
||||||
|
char *end = NULL;
|
||||||
|
char *found = NULL;
|
||||||
|
|
||||||
|
// /proc/cpuinfo calls it "model name" on x86 and "Model" on a Raspberry Pi. The kernel
|
||||||
|
// reports its size as zero, so it is read a line at a time rather than in one piece.
|
||||||
|
while ((info != NULL) && (found == NULL) && (fgets(line, sizeof(line), info) != NULL)) {
|
||||||
|
if (utilStartsWith(line, "model name") || utilStartsWith(line, "Model")) {
|
||||||
|
colon = strchr(line, ':');
|
||||||
|
if (colon != NULL) {
|
||||||
|
colon++;
|
||||||
|
while (*colon == ' ') {
|
||||||
|
colon++;
|
||||||
|
}
|
||||||
|
for (end = colon + strlen(colon); (end > colon) && ((uint8_t)end[-1] <= ' '); end--) {
|
||||||
|
end[-1] = 0;
|
||||||
|
}
|
||||||
|
found = utilCreateString("%s, %d cores, %d MB", colon, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info != NULL) {
|
||||||
|
fclose(info);
|
||||||
|
}
|
||||||
|
if (found != NULL) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return utilCreateString("%s, %d cores, %d MB", SDL_GetPlatform(), SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The operating system and its version, for a bug report.
|
||||||
|
char *mainDescribeOs(void) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
char release[64];
|
||||||
|
DWORD bytes = sizeof(release);
|
||||||
|
|
||||||
|
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild", RRF_RT_REG_SZ, NULL, release, &bytes) == ERROR_SUCCESS) {
|
||||||
|
return utilCreateString("%s build %s", SDL_GetPlatform(), release);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
struct utsname system;
|
||||||
|
|
||||||
|
if (uname(&system) == 0) {
|
||||||
|
return utilCreateString("%s %s (%s)", system.sysname, system.release, system.machine);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return strdup(SDL_GetPlatform());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Ok, this thing can have a mess of different arguments:
|
// Ok, this thing can have a mess of different arguments:
|
||||||
// WW - Just the width of the white border
|
// WW - Just the width of the white border
|
||||||
// WW WB - Width of white border and then black border
|
// WW WB - Width of white border and then black border
|
||||||
|
|
|
||||||
20
src/midi.c
20
src/midi.c
|
|
@ -152,6 +152,16 @@ bool midiIs(const void *bytes, size_t size) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void midiQuit(void) {
|
||||||
|
if (_soundfont != NULL) {
|
||||||
|
tsf_close(_soundfont);
|
||||||
|
_soundfont = NULL;
|
||||||
|
}
|
||||||
|
free(_found);
|
||||||
|
_found = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool midiRender(const void *bytes, size_t size, DecodedAudioT *out) {
|
bool midiRender(const void *bytes, size_t size, DecodedAudioT *out) {
|
||||||
tml_message *messages = NULL;
|
tml_message *messages = NULL;
|
||||||
tml_message *message = NULL;
|
tml_message *message = NULL;
|
||||||
|
|
@ -212,16 +222,6 @@ bool midiRender(const void *bytes, size_t size, DecodedAudioT *out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void midiQuit(void) {
|
|
||||||
if (_soundfont != NULL) {
|
|
||||||
tsf_close(_soundfont);
|
|
||||||
_soundfont = NULL;
|
|
||||||
}
|
|
||||||
free(_found);
|
|
||||||
_found = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const char *midiSoundfont(void) {
|
const char *midiSoundfont(void) {
|
||||||
return (_found != NULL) ? _found : "none found (MIDI will not play; name one with --soundfont)";
|
return (_found != NULL) ? _found : "none found (MIDI will not play; name one with --soundfont)";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,12 @@ bool persistWrite(const char *text, size_t length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fclose(file);
|
fclose(file);
|
||||||
// Windows will not rename onto an existing file, so the old one goes first. The window between
|
// Windows will not rename onto an existing file, so the old one goes first there. The window
|
||||||
// the two is the one risk left, and it is smaller than writing in place.
|
// between the two is the one risk left, and it is smaller than writing in place. Everywhere
|
||||||
|
// else the rename is atomic and the old save stays whole until the new one is in place.
|
||||||
|
#ifdef _WIN32
|
||||||
remove(_path);
|
remove(_path);
|
||||||
|
#endif
|
||||||
if (rename(_temporary, _path) != 0) {
|
if (rename(_temporary, _path) != 0) {
|
||||||
remove(_temporary);
|
remove(_temporary);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
16
src/render.c
16
src/render.c
|
|
@ -68,6 +68,14 @@ void renderEnd(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void renderSelect(RenderApiT api, const RenderBackendT *backend, SDL_Renderer *renderer) {
|
||||||
|
_api = backend != NULL ? api : RENDER_NONE;
|
||||||
|
_backend = backend;
|
||||||
|
_renderer = renderer;
|
||||||
|
utilTrace("Render: %s", renderApiName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
SDL_GPUTexture *renderTextureFor(SDL_Texture *texture) {
|
SDL_GPUTexture *renderTextureFor(SDL_Texture *texture) {
|
||||||
if (texture == NULL) {
|
if (texture == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -112,14 +120,6 @@ SDL_Texture *renderWrapTexture(SDL_Renderer *renderer, SDL_GPUTexture *texture,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void renderSelect(RenderApiT api, const RenderBackendT *backend, SDL_Renderer *renderer) {
|
|
||||||
_api = backend != NULL ? api : RENDER_NONE;
|
|
||||||
_backend = backend;
|
|
||||||
_renderer = renderer;
|
|
||||||
utilTrace("Render: %s", renderApiName());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Every run of rgpu* drawing sits between acquiring a command buffer and submitting it, so that is
|
// Every run of rgpu* drawing sits between acquiring a command buffer and submitting it, so that is
|
||||||
// where the context is taken and given back. Bracketing here rather than at the callers means
|
// where the context is taken and given back. Bracketing here rather than at the callers means
|
||||||
|
|
|
||||||
107
src/renderGles.c
107
src/renderGles.c
|
|
@ -159,9 +159,9 @@ typedef struct GlesDeviceS {
|
||||||
|
|
||||||
static GlesDeviceT _device;
|
static GlesDeviceT _device;
|
||||||
|
|
||||||
|
static GLenum _addressMode(SDL_GPUSamplerAddressMode mode);
|
||||||
static void _applyVertexLayout(GlesRenderPassT *pass);
|
static void _applyVertexLayout(GlesRenderPassT *pass);
|
||||||
static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSampler *sampler);
|
static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSampler *sampler);
|
||||||
static GLenum _addressMode(SDL_GPUSamplerAddressMode mode);
|
|
||||||
static GLenum _blendFactor(SDL_GPUBlendFactor factor);
|
static GLenum _blendFactor(SDL_GPUBlendFactor factor);
|
||||||
static GLenum _blendOp(SDL_GPUBlendOp op);
|
static GLenum _blendOp(SDL_GPUBlendOp op);
|
||||||
static GLenum _compare(SDL_GPUCompareOp op);
|
static GLenum _compare(SDL_GPUCompareOp op);
|
||||||
|
|
@ -214,8 +214,8 @@ static void _glesUnmapTransferBuffer(SDL_GPUDevice *device,
|
||||||
static void _glesUploadToBuffer(SDL_GPUCopyPass *copyPass, const SDL_GPUTransferBufferLocation *source, const SDL_GPUBufferRegion *destination, bool cycle);
|
static void _glesUploadToBuffer(SDL_GPUCopyPass *copyPass, const SDL_GPUTransferBufferLocation *source, const SDL_GPUBufferRegion *destination, bool cycle);
|
||||||
static void _glesUploadToTexture(SDL_GPUCopyPass *copyPass, const SDL_GPUTextureTransferInfo *source, const SDL_GPUTextureRegion *destination, bool cycle);
|
static void _glesUploadToTexture(SDL_GPUCopyPass *copyPass, const SDL_GPUTextureTransferInfo *source, const SDL_GPUTextureRegion *destination, bool cycle);
|
||||||
static GLenum _primitive(SDL_GPUPrimitiveType type);
|
static GLenum _primitive(SDL_GPUPrimitiveType type);
|
||||||
static GLuint _scratchFramebuffer(GLenum binding, GlesTextureT *texture, uint32_t level);
|
|
||||||
static void _remapFragmentBlocks(GLuint program, const char *fragmentSource);
|
static void _remapFragmentBlocks(GLuint program, const char *fragmentSource);
|
||||||
|
static GLuint _scratchFramebuffer(GLenum binding, GlesTextureT *texture, uint32_t level);
|
||||||
static void _setPipelineState(GlesPipelineT *pipeline);
|
static void _setPipelineState(GlesPipelineT *pipeline);
|
||||||
static GLenum _stencilOp(SDL_GPUStencilOp op);
|
static GLenum _stencilOp(SDL_GPUStencilOp op);
|
||||||
static void _vertexFormat(SDL_GPUVertexElementFormat format, GLint *size, GLenum *type, GLboolean *normalized);
|
static void _vertexFormat(SDL_GPUVertexElementFormat format, GLint *size, GLenum *type, GLboolean *normalized);
|
||||||
|
|
@ -224,6 +224,56 @@ void renderGlesRestore(void);
|
||||||
bool renderGlesStart(void);
|
bool renderGlesStart(void);
|
||||||
|
|
||||||
|
|
||||||
|
static GLenum _addressMode(SDL_GPUSamplerAddressMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case SDL_GPU_SAMPLERADDRESSMODE_REPEAT: return GL_REPEAT;
|
||||||
|
case SDL_GPU_SAMPLERADDRESSMODE_MIRRORED_REPEAT: return GL_MIRRORED_REPEAT;
|
||||||
|
default: return GL_CLAMP_TO_EDGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void _applyVertexLayout(GlesRenderPassT *pass) {
|
||||||
|
GlesPipelineT *pipeline = pass->pipeline;
|
||||||
|
uint32_t i;
|
||||||
|
uint32_t a;
|
||||||
|
|
||||||
|
if ((pipeline == NULL) || !pass->vertexDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pass->vertexDirty = false;
|
||||||
|
for (i = 0; i < pass->vertexCount; i++) {
|
||||||
|
GlesBufferT *buffer = pass->vertexBuffer[i];
|
||||||
|
uint32_t slot = i;
|
||||||
|
|
||||||
|
if (buffer == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, buffer->name);
|
||||||
|
for (a = 0; a < pipeline->attributeCount; a++) {
|
||||||
|
const SDL_GPUVertexAttribute *attribute = &pipeline->attributes[a];
|
||||||
|
GLboolean normalized;
|
||||||
|
GLenum type;
|
||||||
|
GLint size;
|
||||||
|
|
||||||
|
if (attribute->buffer_slot != slot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_vertexFormat(attribute->format, &size, &type, &normalized);
|
||||||
|
glEnableVertexAttribArray(attribute->location);
|
||||||
|
// An integer attribute must be fed as an integer: the scene's joint indices are uvec4 in
|
||||||
|
// the shader, and handing them over as floats is a link-time type mismatch, not a value
|
||||||
|
// one, so the draw is rejected rather than merely wrong.
|
||||||
|
if (((type == GL_UNSIGNED_BYTE) && (normalized == GL_FALSE)) || (type == GL_INT) || (type == GL_UNSIGNED_INT)) {
|
||||||
|
glVertexAttribIPointer(attribute->location, size, type, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
|
||||||
|
} else {
|
||||||
|
glVertexAttribPointer(attribute->location, size, type, normalized, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSampler *sampler) {
|
static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSampler *sampler) {
|
||||||
GLuint name = (GLuint)(uintptr_t)sampler;
|
GLuint name = (GLuint)(uintptr_t)sampler;
|
||||||
|
|
||||||
|
|
@ -237,15 +287,6 @@ static void _bindTextureUnit(uint32_t unit, GlesTextureT *texture, SDL_GPUSample
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static GLenum _addressMode(SDL_GPUSamplerAddressMode mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case SDL_GPU_SAMPLERADDRESSMODE_REPEAT: return GL_REPEAT;
|
|
||||||
case SDL_GPU_SAMPLERADDRESSMODE_MIRRORED_REPEAT: return GL_MIRRORED_REPEAT;
|
|
||||||
default: return GL_CLAMP_TO_EDGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static GLenum _blendFactor(SDL_GPUBlendFactor factor) {
|
static GLenum _blendFactor(SDL_GPUBlendFactor factor) {
|
||||||
switch (factor) {
|
switch (factor) {
|
||||||
case SDL_GPU_BLENDFACTOR_ZERO: return GL_ZERO;
|
case SDL_GPU_BLENDFACTOR_ZERO: return GL_ZERO;
|
||||||
|
|
@ -398,6 +439,9 @@ static GLuint _framebufferFor(const SDL_GPUColorTargetInfo *colour, uint32_t col
|
||||||
// The cache is a fixed size on purpose; overflowing it means a target set is being rebuilt
|
// The cache is a fixed size on purpose; overflowing it means a target set is being rebuilt
|
||||||
// every frame, which is a bug worth hearing about rather than hiding behind a bigger array.
|
// every frame, which is a bug worth hearing about rather than hiding behind a bigger array.
|
||||||
utilTrace("Gles: framebuffer cache full");
|
utilTrace("Gles: framebuffer cache full");
|
||||||
|
for (index = 0; index < _device.framebufferCount; index++) {
|
||||||
|
glDeleteFramebuffers(1, &_device.framebuffers[index].name);
|
||||||
|
}
|
||||||
_device.framebufferCount = 0;
|
_device.framebufferCount = 0;
|
||||||
}
|
}
|
||||||
slot = &_device.framebuffers[_device.framebufferCount];
|
slot = &_device.framebuffers[_device.framebufferCount];
|
||||||
|
|
@ -586,47 +630,6 @@ static void _glesBindVertexBuffers(SDL_GPURenderPass *renderPass, Uint32 firstSl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _applyVertexLayout(GlesRenderPassT *pass) {
|
|
||||||
GlesPipelineT *pipeline = pass->pipeline;
|
|
||||||
uint32_t i;
|
|
||||||
uint32_t a;
|
|
||||||
|
|
||||||
if ((pipeline == NULL) || !pass->vertexDirty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pass->vertexDirty = false;
|
|
||||||
for (i = 0; i < pass->vertexCount; i++) {
|
|
||||||
GlesBufferT *buffer = pass->vertexBuffer[i];
|
|
||||||
uint32_t slot = i;
|
|
||||||
|
|
||||||
if (buffer == NULL) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, buffer->name);
|
|
||||||
for (a = 0; a < pipeline->attributeCount; a++) {
|
|
||||||
const SDL_GPUVertexAttribute *attribute = &pipeline->attributes[a];
|
|
||||||
GLboolean normalized;
|
|
||||||
GLenum type;
|
|
||||||
GLint size;
|
|
||||||
|
|
||||||
if (attribute->buffer_slot != slot) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_vertexFormat(attribute->format, &size, &type, &normalized);
|
|
||||||
glEnableVertexAttribArray(attribute->location);
|
|
||||||
// An integer attribute must be fed as an integer: the scene's joint indices are uvec4 in
|
|
||||||
// the shader, and handing them over as floats is a link-time type mismatch, not a value
|
|
||||||
// one, so the draw is rejected rather than merely wrong.
|
|
||||||
if (((type == GL_UNSIGNED_BYTE) && (normalized == GL_FALSE)) || (type == GL_INT) || (type == GL_UNSIGNED_INT)) {
|
|
||||||
glVertexAttribIPointer(attribute->location, size, type, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
|
|
||||||
} else {
|
|
||||||
glVertexAttribPointer(attribute->location, size, type, normalized, (GLsizei)pipeline->strides[slot], (const void *)(uintptr_t)(pass->vertexOffset[slot] + attribute->offset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void _glesBindVertexStorageBuffers(SDL_GPURenderPass *renderPass, Uint32 firstSlot, SDL_GPUBuffer *const *storageBuffers, Uint32 numBindings) {
|
static void _glesBindVertexStorageBuffers(SDL_GPURenderPass *renderPass, Uint32 firstSlot, SDL_GPUBuffer *const *storageBuffers, Uint32 numBindings) {
|
||||||
uint32_t i;
|
uint32_t i;
|
||||||
|
|
||||||
|
|
|
||||||
24
src/scene.c
24
src/scene.c
|
|
@ -3978,18 +3978,6 @@ bool materialSetBlend(int32_t material, bool blend) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// glTF's alpha masking: a texel whose base colour alpha falls below the cutoff is discarded, in
|
|
||||||
// the lit pass and in the shadow pass alike. Zero turns masking off, which is the default and what
|
|
||||||
// every material that never asked for it keeps.
|
|
||||||
bool materialSetCutoff(int32_t material, float cutoff) {
|
|
||||||
if (!materialValid(material)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_scene.materials[material].cutoff = SDL_clamp(cutoff, 0.0f, 1.0f);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
||||||
if (!materialValid(material)) {
|
if (!materialValid(material)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -4015,6 +4003,18 @@ bool materialSetColorLinear(int32_t material, float r, float g, float b, float a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// glTF's alpha masking: a texel whose base colour alpha falls below the cutoff is discarded, in
|
||||||
|
// the lit pass and in the shadow pass alike. Zero turns masking off, which is the default and what
|
||||||
|
// every material that never asked for it keeps.
|
||||||
|
bool materialSetCutoff(int32_t material, float cutoff) {
|
||||||
|
if (!materialValid(material)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_scene.materials[material].cutoff = SDL_clamp(cutoff, 0.0f, 1.0f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool materialSetDoubleSided(int32_t material, bool doubleSided) {
|
bool materialSetDoubleSided(int32_t material, bool doubleSided) {
|
||||||
if (!materialValid(material)) {
|
if (!materialValid(material)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
#define SLOTS_FIRST 16 // Grown by doubling from here
|
#define SLOTS_FIRST 16 // Grown by doubling from here
|
||||||
|
#define SLOT_BITS 16 // A handle is the slot in its low bits and a generation above them:
|
||||||
|
#define SLOT_MASK 0xFFFF // a slot that is freed and given out again gets a new handle, so a
|
||||||
|
#define GENERATION_MASK 0x7FFF // stale handle a script kept cannot cancel or ask after the newcomer.
|
||||||
#define BACK_OVERSHOOT 1.70158 // How far past the target the "back" easings go, the usual constant
|
#define BACK_OVERSHOOT 1.70158 // How far past the target the "back" easings go, the usual constant
|
||||||
#define ELASTIC_PERIOD 0.3
|
#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_SCALE 7.5625 // The bounce curve's four arcs, as everyone writes them
|
||||||
#define BOUNCE_SPLIT 2.75
|
#define BOUNCE_SPLIT 2.75
|
||||||
#define HALF 0.5
|
#define HALF 0.5
|
||||||
|
|
@ -47,6 +49,7 @@ typedef struct {
|
||||||
double from;
|
double from;
|
||||||
double to;
|
double to;
|
||||||
SchedulerEaseE easing;
|
SchedulerEaseE easing;
|
||||||
|
int32_t generation; // Stepped every time the slot is given out
|
||||||
bool repeating;
|
bool repeating;
|
||||||
bool used;
|
bool used;
|
||||||
} SlotT;
|
} SlotT;
|
||||||
|
|
@ -60,24 +63,33 @@ static int32_t _capacity = 0;
|
||||||
static int32_t _allocate(void);
|
static int32_t _allocate(void);
|
||||||
static double _ease(SchedulerEaseE easing, double t);
|
static double _ease(SchedulerEaseE easing, double t);
|
||||||
static double _easeBounceOut(double t);
|
static double _easeBounceOut(double t);
|
||||||
|
static int32_t _handleOf(int32_t slot);
|
||||||
|
static SlotT *_slotOf(int32_t handle);
|
||||||
|
|
||||||
|
|
||||||
// ===== Internal helpers =====
|
// ===== Internal helpers =====
|
||||||
|
|
||||||
// The first free slot, growing the table when there is none. Handles are indexes, so a slot never
|
// The first free slot, growing the table when there is none. A slot never moves while anything
|
||||||
// moves while anything might still name it.
|
// might still name it, and each time one is handed out again its generation steps, so the handle
|
||||||
|
// a script was given for the old occupant names nothing once that occupant is gone.
|
||||||
static int32_t _allocate(void) {
|
static int32_t _allocate(void) {
|
||||||
SlotT *grown = NULL;
|
SlotT *grown = NULL;
|
||||||
int32_t want = 0;
|
int32_t want = 0;
|
||||||
int32_t x = 0;
|
int32_t x = 0;
|
||||||
|
int32_t generation = 0;
|
||||||
|
|
||||||
for (x = 0; x < _count; x++) {
|
for (x = 0; x < _count; x++) {
|
||||||
if (!_slots[x].used) {
|
if (!_slots[x].used) {
|
||||||
|
generation = (_slots[x].generation + 1) & GENERATION_MASK;
|
||||||
memset(&_slots[x], 0, sizeof(_slots[x]));
|
memset(&_slots[x], 0, sizeof(_slots[x]));
|
||||||
|
_slots[x].generation = generation;
|
||||||
_slots[x].used = true;
|
_slots[x].used = true;
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_count > SLOT_MASK) {
|
||||||
|
utilDie("Too many timers and tweens are alive at once.");
|
||||||
|
}
|
||||||
if (_count == _capacity) {
|
if (_count == _capacity) {
|
||||||
want = (_capacity == 0) ? SLOTS_FIRST : (_capacity * 2);
|
want = (_capacity == 0) ? SLOTS_FIRST : (_capacity * 2);
|
||||||
grown = (SlotT *)realloc(_slots, sizeof(SlotT) * (size_t)want);
|
grown = (SlotT *)realloc(_slots, sizeof(SlotT) * (size_t)want);
|
||||||
|
|
@ -155,13 +167,33 @@ static double _easeBounceOut(double t) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The handle a script is given for a slot: the slot's index under its generation.
|
||||||
|
static int32_t _handleOf(int32_t slot) {
|
||||||
|
return (_slots[slot].generation << SLOT_BITS) | slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The slot a handle names, or NULL when it names nothing: out of range, free, or an earlier
|
||||||
|
// occupant of a slot that has since been given to something else.
|
||||||
|
static SlotT *_slotOf(int32_t handle) {
|
||||||
|
int32_t slot = handle & SLOT_MASK;
|
||||||
|
|
||||||
|
if ((handle < 0) || (slot >= _count) || !_slots[slot].used || (_handleOf(slot) != handle)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &_slots[slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== Public =====
|
// ===== Public =====
|
||||||
|
|
||||||
void schedulerCancel(int32_t handle) {
|
void schedulerCancel(int32_t handle) {
|
||||||
if ((handle < 0) || (handle >= _count)) {
|
SlotT *slot = _slotOf(handle);
|
||||||
return;
|
|
||||||
|
if (slot != NULL) {
|
||||||
|
slot->used = false;
|
||||||
}
|
}
|
||||||
_slots[handle].used = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -180,11 +212,7 @@ int32_t schedulerCount(void) {
|
||||||
|
|
||||||
|
|
||||||
bool schedulerIsActive(int32_t handle) {
|
bool schedulerIsActive(int32_t handle) {
|
||||||
if ((handle < 0) || (handle >= _count)) {
|
return _slotOf(handle) != NULL;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _slots[handle].used;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -197,8 +225,8 @@ void schedulerReset(void) {
|
||||||
|
|
||||||
|
|
||||||
int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now) {
|
int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now) {
|
||||||
int32_t handle = _allocate();
|
int32_t index = _allocate();
|
||||||
SlotT *slot = &_slots[handle];
|
SlotT *slot = &_slots[index];
|
||||||
|
|
||||||
if (milliseconds < 0) {
|
if (milliseconds < 0) {
|
||||||
milliseconds = 0;
|
milliseconds = 0;
|
||||||
|
|
@ -208,13 +236,13 @@ int32_t schedulerTimer(int64_t milliseconds, bool repeating, uint64_t now) {
|
||||||
slot->due = now + (uint64_t)milliseconds;
|
slot->due = now + (uint64_t)milliseconds;
|
||||||
slot->repeating = repeating;
|
slot->repeating = repeating;
|
||||||
|
|
||||||
return handle;
|
return _handleOf(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEaseE easing, uint64_t now) {
|
int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEaseE easing, uint64_t now) {
|
||||||
int32_t handle = _allocate();
|
int32_t index = _allocate();
|
||||||
SlotT *slot = &_slots[handle];
|
SlotT *slot = &_slots[index];
|
||||||
|
|
||||||
if (milliseconds < 0) {
|
if (milliseconds < 0) {
|
||||||
milliseconds = 0;
|
milliseconds = 0;
|
||||||
|
|
@ -230,7 +258,7 @@ int32_t schedulerTween(double from, double to, int64_t milliseconds, SchedulerEa
|
||||||
slot->to = to;
|
slot->to = to;
|
||||||
slot->easing = easing;
|
slot->easing = easing;
|
||||||
|
|
||||||
return handle;
|
return _handleOf(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -260,7 +288,7 @@ void schedulerUpdate(uint64_t now, SchedulerFireT fire, void *context) {
|
||||||
slot->used = false;
|
slot->used = false;
|
||||||
}
|
}
|
||||||
if (fire != NULL) {
|
if (fire != NULL) {
|
||||||
fire(context, x, SCHEDULER_TIMER, 0, 0, !slot->repeating);
|
fire(context, _handleOf(x), SCHEDULER_TIMER, 0, 0, !slot->repeating);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +302,7 @@ void schedulerUpdate(uint64_t now, SchedulerFireT fire, void *context) {
|
||||||
slot->used = false;
|
slot->used = false;
|
||||||
}
|
}
|
||||||
if (fire != NULL) {
|
if (fire != NULL) {
|
||||||
fire(context, x, SCHEDULER_TWEEN, value, progress, finished);
|
fire(context, _handleOf(x), SCHEDULER_TWEEN, value, progress, finished);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
910
src/singe.c
910
src/singe.c
File diff suppressed because it is too large
Load diff
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
#define HISTORY 120 // Frames kept, about two seconds at sixty
|
#define HISTORY 120 // Frames kept, about two seconds at sixty
|
||||||
#define MARGIN 8
|
#define MARGIN 8
|
||||||
|
#define PADDING 4 // Between the backdrop edge and the text
|
||||||
#define LINE_HEIGHT (SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE + 2)
|
#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 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 LINES_MAX 6 // What statsDraw writes; the backdrop is sized from it
|
||||||
|
|
@ -50,7 +51,7 @@ static void _line(SDL_Renderer *renderer, int32_t index, const char *text);
|
||||||
|
|
||||||
static void _line(SDL_Renderer *renderer, int32_t index, const char *text) {
|
static void _line(SDL_Renderer *renderer, int32_t index, const char *text) {
|
||||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
|
||||||
SDL_RenderDebugText(renderer, (float)(MARGIN + 4), (float)(MARGIN + 4 + (index * LINE_HEIGHT)), text);
|
SDL_RenderDebugText(renderer, (float)(MARGIN + PADDING), (float)(MARGIN + PADDING + (index * LINE_HEIGHT)), text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -67,7 +68,7 @@ void statsDraw(SDL_Renderer *renderer, const StatsT *stats) {
|
||||||
backdrop.x = MARGIN;
|
backdrop.x = MARGIN;
|
||||||
backdrop.y = MARGIN;
|
backdrop.y = MARGIN;
|
||||||
backdrop.w = PANEL_WIDTH;
|
backdrop.w = PANEL_WIDTH;
|
||||||
backdrop.h = (float)((LINES_MAX * LINE_HEIGHT) + 8);
|
backdrop.h = (float)((LINES_MAX * LINE_HEIGHT) + (PADDING * 2));
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, BACKDROP_ALPHA);
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, BACKDROP_ALPHA);
|
||||||
SDL_RenderFillRect(renderer, &backdrop);
|
SDL_RenderFillRect(renderer, &backdrop);
|
||||||
|
|
|
||||||
19
src/vfs.c
19
src/vfs.c
|
|
@ -127,7 +127,7 @@ static int _listCompare(const void *a, const void *b); // qsort callba
|
||||||
static void _listDirectory(const char *path, ListT *list);
|
static void _listDirectory(const char *path, ListT *list);
|
||||||
static char **_listFinish(ListT *list, int32_t *count);
|
static char **_listFinish(ListT *list, int32_t *count);
|
||||||
static char *_normalise(const char *name);
|
static char *_normalise(const char *name);
|
||||||
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory);
|
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer);
|
||||||
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory);
|
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory);
|
||||||
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length);
|
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length);
|
||||||
static bool _resolve(const char *name, TargetT *target);
|
static bool _resolve(const char *name, TargetT *target);
|
||||||
|
|
@ -540,15 +540,15 @@ static char *_normalise(const char *name) {
|
||||||
|
|
||||||
|
|
||||||
// A game's own directory under the data directory, holding its overlay.
|
// A game's own directory under the data directory, holding its overlay.
|
||||||
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory) {
|
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer) {
|
||||||
char *stem = NULL;
|
char *stem = NULL;
|
||||||
char *result = NULL;
|
char *result = NULL;
|
||||||
|
|
||||||
if (isContainer && (dataDir != NULL)) {
|
if (isContainer && (dataDir != NULL)) {
|
||||||
return utilCreateString("%s%s%c", dataDir, directory, utilGetPathSeparator());
|
return utilCreateString("%s%s%c", dataDir, OVERLAY_DIRECTORY, utilGetPathSeparator());
|
||||||
}
|
}
|
||||||
stem = vfsDatabaseStem(utilGetLastPathComponent(databasePath));
|
stem = vfsDatabaseStem(utilGetLastPathComponent(databasePath));
|
||||||
result = utilCreateString("%s%s%c%s%c", dataDirBase ? dataDirBase : "", stem, utilGetPathSeparator(), directory, utilGetPathSeparator());
|
result = utilCreateString("%s%s%c%s%c", dataDirBase ? dataDirBase : "", stem, utilGetPathSeparator(), OVERLAY_DIRECTORY, utilGetPathSeparator());
|
||||||
free(stem);
|
free(stem);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -655,7 +655,7 @@ static bool _resolve(const char *name, TargetT *target) {
|
||||||
if (target->db != NULL) {
|
if (target->db != NULL) {
|
||||||
inner = (norm[i] == 0) ? norm + i : norm + i + 1;
|
inner = (norm[i] == 0) ? norm + i : norm + i + 1;
|
||||||
if (target->db->overlay == NULL) {
|
if (target->db->overlay == NULL) {
|
||||||
target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false, OVERLAY_DIRECTORY);
|
target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false);
|
||||||
}
|
}
|
||||||
free(prefix);
|
free(prefix);
|
||||||
break;
|
break;
|
||||||
|
|
@ -824,7 +824,7 @@ void vfsInit(const char *container, const char *dataDirBase, const char *dataDir
|
||||||
utilDie("%s is not a Singe game database.", container);
|
utilDie("%s is not a Singe game database.", container);
|
||||||
}
|
}
|
||||||
free(_container->overlay);
|
free(_container->overlay);
|
||||||
_container->overlay = _overlayFor(_dataDirBase, _dataDir, container, true, OVERLAY_DIRECTORY);
|
_container->overlay = _overlayFor(_dataDirBase, _dataDir, container, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -872,14 +872,11 @@ bool vfsIsFilesystem(const char *name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The entries directly under a directory name, from the loose directory, the overlay and the
|
|
||||||
// database together, each name once, sorted. Free with vfsListFree.
|
|
||||||
// Whether a name lives only inside a database: no loose file and no overlay copy shadows it. That
|
// Whether a name lives only inside a database: no loose file and no overlay copy shadows it. That
|
||||||
// is the case that has no filesystem path at all: vfsFilePath answers with a name that is not
|
// is the case that has no filesystem path at all: vfsFilePath answers with a name that is not
|
||||||
// there, and a stream is the only way to read it.
|
// there, and a stream is the only way to read it.
|
||||||
bool vfsIsPacked(const char *name) {
|
bool vfsIsPacked(const char *name) {
|
||||||
TargetT target;
|
TargetT target;
|
||||||
int64_t size = 0;
|
|
||||||
bool packed = false;
|
bool packed = false;
|
||||||
|
|
||||||
if (!_resolve(name, &target)) {
|
if (!_resolve(name, &target)) {
|
||||||
|
|
@ -887,13 +884,15 @@ bool vfsIsPacked(const char *name) {
|
||||||
}
|
}
|
||||||
packed = (target.db != NULL) && (target.key[0] != 0) &&
|
packed = (target.db != NULL) && (target.key[0] != 0) &&
|
||||||
!utilFileExists(target.path) && !utilFileExists(target.overlay) &&
|
!utilFileExists(target.path) && !utilFileExists(target.overlay) &&
|
||||||
_assetSize(target.db, target.key, &size);
|
_assetExists(target.db, target.key);
|
||||||
_targetFree(&target);
|
_targetFree(&target);
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The entries directly under a directory name, from the loose directory, the overlay and the
|
||||||
|
// database together, each name once, sorted. Free with vfsListFree.
|
||||||
char **vfsList(const char *name, int32_t *count) {
|
char **vfsList(const char *name, int32_t *count) {
|
||||||
TargetT target;
|
TargetT target;
|
||||||
ListT list;
|
ListT list;
|
||||||
|
|
|
||||||
|
|
@ -287,14 +287,11 @@ static int64_t _avioSeek(void *opaque, int64_t offset, int whence);
|
||||||
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
|
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
|
||||||
static int _compareFrames(const void *a, const void *b); // qsort callback. Not changing int.
|
static int _compareFrames(const void *a, const void *b); // qsort callback. Not changing int.
|
||||||
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame);
|
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame);
|
||||||
static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame);
|
|
||||||
static void _deinterlaceClose(VideoPlayerT *v);
|
|
||||||
static bool _srtAppend(char **text, size_t *used, size_t *room, const char *addition);
|
|
||||||
static char *_srtFromRect(const AVSubtitleRect *rect);
|
|
||||||
static void _srtTime(int64_t milliseconds, char *out, size_t size);
|
|
||||||
static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame);
|
|
||||||
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want);
|
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want);
|
||||||
static void _decodeHere(VideoPlayerT *v);
|
static void _decodeHere(VideoPlayerT *v);
|
||||||
|
static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame);
|
||||||
|
static void _deinterlaceClose(VideoPlayerT *v);
|
||||||
|
static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame);
|
||||||
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
|
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
|
||||||
static void _feedAudio(VideoPlayerT *v);
|
static void _feedAudio(VideoPlayerT *v);
|
||||||
static int64_t _findFrameIndex(VideoPlayerT *v, int64_t pts);
|
static int64_t _findFrameIndex(VideoPlayerT *v, int64_t pts);
|
||||||
|
|
@ -314,13 +311,16 @@ static bool _readIndexCache(VideoPlayerT *v, const char *indexName, co
|
||||||
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
|
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
|
||||||
static void _requestFrame(VideoPlayerT *v);
|
static void _requestFrame(VideoPlayerT *v);
|
||||||
static void _resetClock(VideoPlayerT *v, uint64_t now);
|
static void _resetClock(VideoPlayerT *v, uint64_t now);
|
||||||
|
static const AVCodec *_rkmppDecoder(const AVCodec *decoder);
|
||||||
static void _seekVideo(VideoPlayerT *v, int64_t keyframe);
|
static void _seekVideo(VideoPlayerT *v, int64_t keyframe);
|
||||||
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats); // libavcodec callback.
|
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats); // libavcodec callback.
|
||||||
|
static bool _srtAppend(char **text, size_t *used, size_t *room, const char *addition);
|
||||||
|
static char *_srtFromRect(const AVSubtitleRect *rect);
|
||||||
|
static void _srtTime(int64_t milliseconds, char *out, size_t size);
|
||||||
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase);
|
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase);
|
||||||
static bool _takeDecodedFrame(VideoPlayerT *v);
|
static bool _takeDecodedFrame(VideoPlayerT *v);
|
||||||
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
|
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
|
||||||
static void _uploadFrame(VideoPlayerT *v);
|
static void _uploadFrame(VideoPlayerT *v);
|
||||||
static const AVCodec *_rkmppDecoder(const AVCodec *decoder);
|
|
||||||
static const AVCodec *_v4l2Decoder(const AVCodec *decoder);
|
static const AVCodec *_v4l2Decoder(const AVCodec *decoder);
|
||||||
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header);
|
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header);
|
||||||
|
|
||||||
|
|
@ -835,91 +835,6 @@ static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A frame ready to convert: the deinterlaced one when the picture is interlaced and the option
|
|
||||||
// allows it, and the frame itself otherwise. Laserdisc rips are the reason this exists -- a disc
|
|
||||||
// held interlaced fields, and a rip that kept them combs on every progressive display.
|
|
||||||
static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame) {
|
|
||||||
if ((v->deinterlace == DEINTERLACE_OFF) || (frame->format == AV_PIX_FMT_NONE)) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
// bwdif is told to leave progressive frames alone, so in automatic mode the graph is built only
|
|
||||||
// once something interlaced actually turns up and costs nothing on a progressive disc.
|
|
||||||
if ((v->deinterlace == DEINTERLACE_AUTO) && ((frame->flags & AV_FRAME_FLAG_INTERLACED) == 0) && (v->filterGraph == NULL)) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
if ((v->filterGraph != NULL) && ((frame->format != v->filterFormat) || (frame->width != v->filterWidth) || (frame->height != v->filterHeight))) {
|
|
||||||
_deinterlaceClose(v);
|
|
||||||
}
|
|
||||||
if ((v->filterGraph == NULL) && !_deinterlaceOpen(v, frame)) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
av_frame_unref(v->filterFrame);
|
|
||||||
if (av_buffersrc_add_frame_flags(v->filterSource, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
if (av_buffersink_get_frame(v->filterSink, v->filterFrame) < 0) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
return v->filterFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void _deinterlaceClose(VideoPlayerT *v) {
|
|
||||||
if (v->filterGraph != NULL) {
|
|
||||||
avfilter_graph_free(&v->filterGraph);
|
|
||||||
v->filterSource = NULL;
|
|
||||||
v->filterSink = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// buffer -> bwdif -> buffersink. bwdif runs in send_frame mode, one picture out for one in: a
|
|
||||||
// disc is addressed by frame number, and a filter that turned each frame into two fields would
|
|
||||||
// move every frame in the index.
|
|
||||||
static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame) {
|
|
||||||
char arguments[FILTER_ARGUMENTS_MAX];
|
|
||||||
const AVFilter *source = avfilter_get_by_name("buffer");
|
|
||||||
const AVFilter *sink = avfilter_get_by_name("buffersink");
|
|
||||||
const AVFilter *bwdif = avfilter_get_by_name("bwdif");
|
|
||||||
AVFilterContext *filter = NULL;
|
|
||||||
AVRational ratio = (frame->sample_aspect_ratio.num > 0) ? frame->sample_aspect_ratio : (AVRational){ 1, 1 };
|
|
||||||
|
|
||||||
if ((source == NULL) || (sink == NULL) || (bwdif == NULL)) {
|
|
||||||
utilTrace("Video %d: this build has no deinterlacer.", v->id);
|
|
||||||
v->deinterlace = DEINTERLACE_OFF;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
v->filterGraph = avfilter_graph_alloc();
|
|
||||||
if (v->filterGraph == NULL) {
|
|
||||||
utilDie("Unable to allocate the deinterlacing graph.");
|
|
||||||
}
|
|
||||||
snprintf(arguments, sizeof(arguments), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
|
|
||||||
frame->width, frame->height, frame->format, v->videoTimeBase.num, v->videoTimeBase.den, ratio.num, ratio.den);
|
|
||||||
if ((avfilter_graph_create_filter(&v->filterSource, source, "in", arguments, NULL, v->filterGraph) < 0) ||
|
|
||||||
(avfilter_graph_create_filter(&filter, bwdif, "bwdif", (v->deinterlace == DEINTERLACE_ON) ? "mode=send_frame:parity=auto:deint=all" : "mode=send_frame:parity=auto:deint=interlaced", NULL, v->filterGraph) < 0) ||
|
|
||||||
(avfilter_graph_create_filter(&v->filterSink, sink, "out", NULL, NULL, v->filterGraph) < 0) ||
|
|
||||||
(avfilter_link(v->filterSource, 0, filter, 0) < 0) ||
|
|
||||||
(avfilter_link(filter, 0, v->filterSink, 0) < 0) ||
|
|
||||||
(avfilter_graph_config(v->filterGraph, NULL) < 0)) {
|
|
||||||
utilTrace("Video %d: the deinterlacing graph would not build; leaving the picture alone.", v->id);
|
|
||||||
_deinterlaceClose(v);
|
|
||||||
v->deinterlace = DEINTERLACE_OFF;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
v->filterFormat = (enum AVPixelFormat)frame->format;
|
|
||||||
v->filterWidth = frame->width;
|
|
||||||
v->filterHeight = frame->height;
|
|
||||||
if (!v->filterReported) {
|
|
||||||
v->filterReported = true;
|
|
||||||
utilTrace("Video %d: deinterlacing with bwdif.", v->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Decodes frame "want" into the back buffer. Runs on the decoder thread. On DECODE_ERROR the
|
// Decodes frame "want" into the back buffer. Runs on the decoder thread. On DECODE_ERROR the
|
||||||
// message is in threadErrMsg; the caller raises threadError under the lock, which publishes both.
|
// message is in threadErrMsg; the caller raises threadError under the lock, which publishes both.
|
||||||
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want) {
|
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want) {
|
||||||
|
|
@ -1084,6 +999,91 @@ static int _decoderThread(void *data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A frame ready to convert: the deinterlaced one when the picture is interlaced and the option
|
||||||
|
// allows it, and the frame itself otherwise. Laserdisc rips are the reason this exists -- a disc
|
||||||
|
// held interlaced fields, and a rip that kept them combs on every progressive display.
|
||||||
|
static const AVFrame *_deinterlace(VideoPlayerT *v, AVFrame *frame) {
|
||||||
|
if ((v->deinterlace == DEINTERLACE_OFF) || (frame->format == AV_PIX_FMT_NONE)) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
// bwdif is told to leave progressive frames alone, so in automatic mode the graph is built only
|
||||||
|
// once something interlaced actually turns up and costs nothing on a progressive disc.
|
||||||
|
if ((v->deinterlace == DEINTERLACE_AUTO) && ((frame->flags & AV_FRAME_FLAG_INTERLACED) == 0) && (v->filterGraph == NULL)) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
if ((v->filterGraph != NULL) && ((frame->format != v->filterFormat) || (frame->width != v->filterWidth) || (frame->height != v->filterHeight))) {
|
||||||
|
_deinterlaceClose(v);
|
||||||
|
}
|
||||||
|
if ((v->filterGraph == NULL) && !_deinterlaceOpen(v, frame)) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
av_frame_unref(v->filterFrame);
|
||||||
|
if (av_buffersrc_add_frame_flags(v->filterSource, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
if (av_buffersink_get_frame(v->filterSink, v->filterFrame) < 0) {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
return v->filterFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void _deinterlaceClose(VideoPlayerT *v) {
|
||||||
|
if (v->filterGraph != NULL) {
|
||||||
|
avfilter_graph_free(&v->filterGraph);
|
||||||
|
v->filterSource = NULL;
|
||||||
|
v->filterSink = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// buffer -> bwdif -> buffersink. bwdif runs in send_frame mode, one picture out for one in: a
|
||||||
|
// disc is addressed by frame number, and a filter that turned each frame into two fields would
|
||||||
|
// move every frame in the index.
|
||||||
|
static bool _deinterlaceOpen(VideoPlayerT *v, const AVFrame *frame) {
|
||||||
|
char arguments[FILTER_ARGUMENTS_MAX];
|
||||||
|
const AVFilter *source = avfilter_get_by_name("buffer");
|
||||||
|
const AVFilter *sink = avfilter_get_by_name("buffersink");
|
||||||
|
const AVFilter *bwdif = avfilter_get_by_name("bwdif");
|
||||||
|
AVFilterContext *filter = NULL;
|
||||||
|
AVRational ratio = (frame->sample_aspect_ratio.num > 0) ? frame->sample_aspect_ratio : (AVRational){ 1, 1 };
|
||||||
|
|
||||||
|
if ((source == NULL) || (sink == NULL) || (bwdif == NULL)) {
|
||||||
|
utilTrace("Video %d: this build has no deinterlacer.", v->id);
|
||||||
|
v->deinterlace = DEINTERLACE_OFF;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
v->filterGraph = avfilter_graph_alloc();
|
||||||
|
if (v->filterGraph == NULL) {
|
||||||
|
utilDie("Unable to allocate the deinterlacing graph.");
|
||||||
|
}
|
||||||
|
snprintf(arguments, sizeof(arguments), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
|
||||||
|
frame->width, frame->height, frame->format, v->videoTimeBase.num, v->videoTimeBase.den, ratio.num, ratio.den);
|
||||||
|
if ((avfilter_graph_create_filter(&v->filterSource, source, "in", arguments, NULL, v->filterGraph) < 0) ||
|
||||||
|
(avfilter_graph_create_filter(&filter, bwdif, "bwdif", (v->deinterlace == DEINTERLACE_ON) ? "mode=send_frame:parity=auto:deint=all" : "mode=send_frame:parity=auto:deint=interlaced", NULL, v->filterGraph) < 0) ||
|
||||||
|
(avfilter_graph_create_filter(&v->filterSink, sink, "out", NULL, NULL, v->filterGraph) < 0) ||
|
||||||
|
(avfilter_link(v->filterSource, 0, filter, 0) < 0) ||
|
||||||
|
(avfilter_link(filter, 0, v->filterSink, 0) < 0) ||
|
||||||
|
(avfilter_graph_config(v->filterGraph, NULL) < 0)) {
|
||||||
|
utilTrace("Video %d: the deinterlacing graph would not build; leaving the picture alone.", v->id);
|
||||||
|
_deinterlaceClose(v);
|
||||||
|
v->deinterlace = DEINTERLACE_OFF;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
v->filterFormat = (enum AVPixelFormat)frame->format;
|
||||||
|
v->filterWidth = frame->width;
|
||||||
|
v->filterHeight = frame->height;
|
||||||
|
if (!v->filterReported) {
|
||||||
|
v->filterReported = true;
|
||||||
|
utilTrace("Video %d: deinterlacing with bwdif.", v->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Keeps the mixer track's stream topped up from the current audio track.
|
// Keeps the mixer track's stream topped up from the current audio track.
|
||||||
static void _feedAudio(VideoPlayerT *v) {
|
static void _feedAudio(VideoPlayerT *v) {
|
||||||
int32_t result = 0;
|
int32_t result = 0;
|
||||||
|
|
@ -1537,6 +1537,74 @@ static void _resetClock(VideoPlayerT *v, uint64_t now) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The V4L2 memory-to-memory decoder for the stream's codec on a Pi build, NULL when there is none or
|
||||||
|
// hardware decoding is off. Whether the device exists is found out when the decoder opens.
|
||||||
|
// Rockchip boards decode through the vendor's Media Process Platform, which FFmpeg exposes as named
|
||||||
|
// decoders in the same way as V4L2's. They exist only when FFmpeg was built against that library,
|
||||||
|
// which is why this is compiled out everywhere else. Tried before V4L2: a Rockchip running its
|
||||||
|
// vendor kernel has both, and the MPP path is the one that works there.
|
||||||
|
static const AVCodec *_rkmppDecoder(const AVCodec *decoder) {
|
||||||
|
static const V4l2DecoderT table[] = {
|
||||||
|
{ AV_CODEC_ID_H264, "h264_rkmpp" },
|
||||||
|
{ AV_CODEC_ID_HEVC, "hevc_rkmpp" }
|
||||||
|
};
|
||||||
|
size_t x = 0;
|
||||||
|
|
||||||
|
if (!RKMPP_DECODE || !_hardwareDecoding) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (x = 0; x < SDL_arraysize(table); x++) {
|
||||||
|
if (table[x].id == decoder->id) {
|
||||||
|
return avcodec_find_decoder_by_name(table[x].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void _seekVideo(VideoPlayerT *v, int64_t keyframe) {
|
||||||
|
if (av_seek_frame(v->videoFormat, v->videoStream, v->frames[keyframe].pts, AVSEEK_FLAG_BACKWARD) < 0) {
|
||||||
|
avformat_seek_file(v->videoFormat, v->videoStream, INT64_MIN, 0, INT64_MAX, 0);
|
||||||
|
keyframe = 0;
|
||||||
|
}
|
||||||
|
avcodec_flush_buffers(v->videoCodec);
|
||||||
|
if (v->packetPending) {
|
||||||
|
av_packet_unref(v->videoPacket);
|
||||||
|
v->packetPending = false;
|
||||||
|
}
|
||||||
|
// Streams without timestamps are counted from here; the rest are matched by timestamp.
|
||||||
|
v->nextDecodeFrame = keyframe;
|
||||||
|
v->seekKeyframe = keyframe;
|
||||||
|
v->videoDrained = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// libavcodec asks which of the formats it can produce we want; take the hardware one when offered.
|
||||||
|
// The list is every hardware format the build knows followed by the software one, so the fallback
|
||||||
|
// is the first entry that is not a hardware surface, not the first entry.
|
||||||
|
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats) {
|
||||||
|
VideoPlayerT *v = (VideoPlayerT *)codec->opaque;
|
||||||
|
const AVPixFmtDescriptor *desc = NULL;
|
||||||
|
int32_t x = 0;
|
||||||
|
|
||||||
|
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
|
||||||
|
if (formats[x] == v->hwPixelFormat) {
|
||||||
|
return formats[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utilTrace("Video %d: hardware decoder declined the stream; decoding in software.", v->id);
|
||||||
|
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
|
||||||
|
desc = av_pix_fmt_desc_get(formats[x]);
|
||||||
|
if ((desc != NULL) && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
|
||||||
|
return formats[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Positions the video demuxer at a keyframe and resets the decoder. Only ever called by whichever
|
// Positions the video demuxer at a keyframe and resets the decoder. Only ever called by whichever
|
||||||
// thread is decoding: the decoder thread, or the frame loop in deterministic mode.
|
// thread is decoding: the decoder thread, or the frame loop in deterministic mode.
|
||||||
// Appends to a growing string, doubling it as it goes. False when it will not grow.
|
// Appends to a growing string, doubling it as it goes. False when it will not grow.
|
||||||
|
|
@ -1644,48 +1712,6 @@ static void _srtTime(int64_t milliseconds, char *out, size_t size) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _seekVideo(VideoPlayerT *v, int64_t keyframe) {
|
|
||||||
if (av_seek_frame(v->videoFormat, v->videoStream, v->frames[keyframe].pts, AVSEEK_FLAG_BACKWARD) < 0) {
|
|
||||||
avformat_seek_file(v->videoFormat, v->videoStream, INT64_MIN, 0, INT64_MAX, 0);
|
|
||||||
keyframe = 0;
|
|
||||||
}
|
|
||||||
avcodec_flush_buffers(v->videoCodec);
|
|
||||||
if (v->packetPending) {
|
|
||||||
av_packet_unref(v->videoPacket);
|
|
||||||
v->packetPending = false;
|
|
||||||
}
|
|
||||||
// Streams without timestamps are counted from here; the rest are matched by timestamp.
|
|
||||||
v->nextDecodeFrame = keyframe;
|
|
||||||
v->seekKeyframe = keyframe;
|
|
||||||
v->videoDrained = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// libavcodec asks which of the formats it can produce we want; take the hardware one when offered.
|
|
||||||
// The list is every hardware format the build knows followed by the software one, so the fallback
|
|
||||||
// is the first entry that is not a hardware surface, not the first entry.
|
|
||||||
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats) {
|
|
||||||
VideoPlayerT *v = (VideoPlayerT *)codec->opaque;
|
|
||||||
const AVPixFmtDescriptor *desc = NULL;
|
|
||||||
int32_t x = 0;
|
|
||||||
|
|
||||||
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
|
|
||||||
if (formats[x] == v->hwPixelFormat) {
|
|
||||||
return formats[x];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
utilTrace("Video %d: hardware decoder declined the stream; decoding in software.", v->id);
|
|
||||||
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
|
|
||||||
desc = av_pix_fmt_desc_get(formats[x]);
|
|
||||||
if ((desc != NULL) && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
|
|
||||||
return formats[x];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return formats[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase) {
|
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase) {
|
||||||
return av_rescale(ts, (int64_t)timeBase.num * (int64_t)MS_PER_SECOND, timeBase.den);
|
return av_rescale(ts, (int64_t)timeBase.num * (int64_t)MS_PER_SECOND, timeBase.den);
|
||||||
}
|
}
|
||||||
|
|
@ -1762,32 +1788,6 @@ static void _uploadFrame(VideoPlayerT *v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The V4L2 memory-to-memory decoder for the stream's codec on a Pi build, NULL when there is none or
|
|
||||||
// hardware decoding is off. Whether the device exists is found out when the decoder opens.
|
|
||||||
// Rockchip boards decode through the vendor's Media Process Platform, which FFmpeg exposes as named
|
|
||||||
// decoders in the same way as V4L2's. They exist only when FFmpeg was built against that library,
|
|
||||||
// which is why this is compiled out everywhere else. Tried before V4L2: a Rockchip running its
|
|
||||||
// vendor kernel has both, and the MPP path is the one that works there.
|
|
||||||
static const AVCodec *_rkmppDecoder(const AVCodec *decoder) {
|
|
||||||
static const V4l2DecoderT table[] = {
|
|
||||||
{ AV_CODEC_ID_H264, "h264_rkmpp" },
|
|
||||||
{ AV_CODEC_ID_HEVC, "hevc_rkmpp" }
|
|
||||||
};
|
|
||||||
size_t x = 0;
|
|
||||||
|
|
||||||
if (!RKMPP_DECODE || !_hardwareDecoding) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
for (x = 0; x < SDL_arraysize(table); x++) {
|
|
||||||
if (table[x].id == decoder->id) {
|
|
||||||
return avcodec_find_decoder_by_name(table[x].name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static const AVCodec *_v4l2Decoder(const AVCodec *decoder) {
|
static const AVCodec *_v4l2Decoder(const AVCodec *decoder) {
|
||||||
static const V4l2DecoderT table[] = {
|
static const V4l2DecoderT table[] = {
|
||||||
{ AV_CODEC_ID_H263, "h263_v4l2m2m" },
|
{ AV_CODEC_ID_H263, "h263_v4l2m2m" },
|
||||||
|
|
@ -1881,6 +1881,38 @@ int32_t videoGetAudioTracks(int32_t playerHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// What the decoder will be, for the trace header: the platform's hardware decoders in the order
|
||||||
|
// they are tried, or software when there are none or --softwarevideo ruled them out. Which one a
|
||||||
|
// given video actually got is traced as that video opens, since a codec may offer neither.
|
||||||
|
const char *videoGetDecoderDescription(void) {
|
||||||
|
if (!_hardwareDecoding) {
|
||||||
|
return "software only (--softwarevideo)";
|
||||||
|
}
|
||||||
|
if (V4L2_DECODE) {
|
||||||
|
// An ARM build may carry three: the stateless V4L2 decoders, Rockchip's own, and the
|
||||||
|
// stateful V4L2 ones. Which of them a board actually has is a question for the board.
|
||||||
|
if (RKMPP_DECODE && V4L2_REQUEST_DECODE) {
|
||||||
|
return "hardware v4l2 stateless, rkmpp or v4l2m2m where the board allows, software otherwise";
|
||||||
|
}
|
||||||
|
if (V4L2_REQUEST_DECODE) {
|
||||||
|
return "hardware v4l2 stateless or v4l2m2m where the board allows, software otherwise";
|
||||||
|
}
|
||||||
|
if (RKMPP_DECODE) {
|
||||||
|
return "hardware rkmpp or v4l2m2m where the board allows, software otherwise";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "hardware v4l2m2m where the codec allows, software otherwise";
|
||||||
|
}
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return "hardware d3d11va or dxva2 where the codec allows, software otherwise";
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
return "hardware videotoolbox where the codec allows, software otherwise";
|
||||||
|
#else
|
||||||
|
return "hardware vaapi or vdpau where the codec allows, software otherwise";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Frames per second of the loaded video, which is what a subtitle's timestamps are turned into
|
// Frames per second of the loaded video, which is what a subtitle's timestamps are turned into
|
||||||
// frame numbers with.
|
// frame numbers with.
|
||||||
double videoGetFps(int32_t playerHandle) {
|
double videoGetFps(int32_t playerHandle) {
|
||||||
|
|
@ -1890,178 +1922,6 @@ double videoGetFps(int32_t playerHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// How many subtitle tracks the file holds, bitmap ones included: they are counted so that the
|
|
||||||
// numbering matches what a player or ffprobe shows, and videoReadSubtitles says which have words.
|
|
||||||
int32_t videoGetSubtitleTracks(int32_t playerHandle) {
|
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleTracks");
|
|
||||||
AVFormatContext *format = _formatOpen(v->filename);
|
|
||||||
int32_t count = 0;
|
|
||||||
uint32_t x = 0;
|
|
||||||
|
|
||||||
if (format == NULL) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (avformat_find_stream_info(format, NULL) >= 0) {
|
|
||||||
for (x = 0; x < format->nb_streams; x++) {
|
|
||||||
if (format->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_formatClose(&format);
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The language of a subtitle track as the file labels it, "" when it says nothing. The pointer is
|
|
||||||
// good until the next call.
|
|
||||||
const char *videoGetSubtitleLanguage(int32_t playerHandle, int32_t track) {
|
|
||||||
static char language[LANGUAGE_CODE_BYTES];
|
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleLanguage");
|
|
||||||
AVFormatContext *format = _formatOpen(v->filename);
|
|
||||||
AVDictionaryEntry *entry = NULL;
|
|
||||||
int32_t seen = 0;
|
|
||||||
uint32_t x = 0;
|
|
||||||
|
|
||||||
language[0] = '\0';
|
|
||||||
if (format == NULL) {
|
|
||||||
return language;
|
|
||||||
}
|
|
||||||
if (avformat_find_stream_info(format, NULL) >= 0) {
|
|
||||||
for (x = 0; x < format->nb_streams; x++) {
|
|
||||||
if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (seen == track) {
|
|
||||||
entry = av_dict_get(format->streams[x]->metadata, "language", NULL, 0);
|
|
||||||
if ((entry != NULL) && (entry->value != NULL)) {
|
|
||||||
SDL_strlcpy(language, entry->value, sizeof(language));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
seen++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_formatClose(&format);
|
|
||||||
|
|
||||||
return language;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// One subtitle track read out of the container as SubRip text, which is the form the engine's own
|
|
||||||
// subtitle loader already takes. The whole file is walked once, decoding nothing but the subtitle
|
|
||||||
// packets, so this costs a pass over the file and no picture decoding at all. NULL when the track
|
|
||||||
// does not exist, holds pictures rather than words, or yields nothing; free() what comes back.
|
|
||||||
char *videoReadSubtitles(int32_t playerHandle, int32_t track) {
|
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoReadSubtitles");
|
|
||||||
AVFormatContext *format = _formatOpen(v->filename);
|
|
||||||
AVCodecContext *codec = NULL;
|
|
||||||
const AVCodec *decoder = NULL;
|
|
||||||
AVPacket *packet = NULL;
|
|
||||||
AVSubtitle subtitle;
|
|
||||||
AVRational timeBase;
|
|
||||||
char *text = NULL;
|
|
||||||
char *words = NULL;
|
|
||||||
char line[SRT_CUE_MAX];
|
|
||||||
char from[SRT_TIME_MAX];
|
|
||||||
char to[SRT_TIME_MAX];
|
|
||||||
size_t used = 0;
|
|
||||||
size_t room = 0;
|
|
||||||
int64_t start = 0;
|
|
||||||
int64_t end = 0;
|
|
||||||
int32_t stream = -1;
|
|
||||||
int32_t seen = 0;
|
|
||||||
int32_t cues = 0;
|
|
||||||
int got = 0;
|
|
||||||
uint32_t x = 0;
|
|
||||||
bool ok = true;
|
|
||||||
|
|
||||||
if (format == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
if (avformat_find_stream_info(format, NULL) < 0) {
|
|
||||||
_formatClose(&format);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
for (x = 0; x < format->nb_streams; x++) {
|
|
||||||
if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (seen == track) {
|
|
||||||
stream = (int32_t)x;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
seen++;
|
|
||||||
}
|
|
||||||
if (stream < 0) {
|
|
||||||
_formatClose(&format);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
timeBase = format->streams[stream]->time_base;
|
|
||||||
decoder = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
|
|
||||||
codec = (decoder != NULL) ? avcodec_alloc_context3(decoder) : NULL;
|
|
||||||
packet = av_packet_alloc();
|
|
||||||
if ((codec == NULL) || (packet == NULL) ||
|
|
||||||
(avcodec_parameters_to_context(codec, format->streams[stream]->codecpar) < 0) ||
|
|
||||||
(avcodec_open2(codec, decoder, NULL) < 0)) {
|
|
||||||
utilTrace("Video %d: subtitle track %d has no decoder in this build.", v->id, track);
|
|
||||||
av_packet_free(&packet);
|
|
||||||
avcodec_free_context(&codec);
|
|
||||||
_formatClose(&format);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
while (ok && (av_read_frame(format, packet) >= 0)) {
|
|
||||||
if (packet->stream_index != stream) {
|
|
||||||
av_packet_unref(packet);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
memset(&subtitle, 0, sizeof(subtitle));
|
|
||||||
got = 0;
|
|
||||||
if (avcodec_decode_subtitle2(codec, &subtitle, &got, packet) >= 0) {
|
|
||||||
if (got != 0) {
|
|
||||||
start = (int64_t)((double)packet->pts * av_q2d(timeBase) * MS_PER_SECOND) + (int64_t)subtitle.start_display_time;
|
|
||||||
if (subtitle.end_display_time > subtitle.start_display_time) {
|
|
||||||
end = start + (int64_t)(subtitle.end_display_time - subtitle.start_display_time);
|
|
||||||
} else {
|
|
||||||
end = start + (int64_t)((double)packet->duration * av_q2d(timeBase) * MS_PER_SECOND);
|
|
||||||
}
|
|
||||||
for (x = 0; (x < subtitle.num_rects) && ok; x++) {
|
|
||||||
words = _srtFromRect(subtitle.rects[x]);
|
|
||||||
if (words == NULL) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cues++;
|
|
||||||
_srtTime(start, from, sizeof(from));
|
|
||||||
_srtTime(end, to, sizeof(to));
|
|
||||||
snprintf(line, sizeof(line), "%d\n", cues);
|
|
||||||
ok = _srtAppend(&text, &used, &room, line) &&
|
|
||||||
_srtAppend(&text, &used, &room, from) &&
|
|
||||||
_srtAppend(&text, &used, &room, " --> ") &&
|
|
||||||
_srtAppend(&text, &used, &room, to) &&
|
|
||||||
_srtAppend(&text, &used, &room, "\n") &&
|
|
||||||
_srtAppend(&text, &used, &room, words) &&
|
|
||||||
_srtAppend(&text, &used, &room, "\n\n");
|
|
||||||
free(words);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
avsubtitle_free(&subtitle);
|
|
||||||
}
|
|
||||||
av_packet_unref(packet);
|
|
||||||
}
|
|
||||||
av_packet_free(&packet);
|
|
||||||
avcodec_free_context(&codec);
|
|
||||||
_formatClose(&format);
|
|
||||||
if (!ok || (cues == 0)) {
|
|
||||||
free(text);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
utilTrace("Video %d: subtitle track %d read as %d cues.", v->id, track, cues);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int64_t videoGetFrame(int32_t playerHandle) {
|
int64_t videoGetFrame(int32_t playerHandle) {
|
||||||
return _getPlayer(playerHandle, "videoGetFrame")->frame;
|
return _getPlayer(playerHandle, "videoGetFrame")->frame;
|
||||||
}
|
}
|
||||||
|
|
@ -2114,38 +1974,6 @@ MIX_Mixer *videoGetMixer(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// What the decoder will be, for the trace header: the platform's hardware decoders in the order
|
|
||||||
// they are tried, or software when there are none or --softwarevideo ruled them out. Which one a
|
|
||||||
// given video actually got is traced as that video opens, since a codec may offer neither.
|
|
||||||
const char *videoGetDecoderDescription(void) {
|
|
||||||
if (!_hardwareDecoding) {
|
|
||||||
return "software only (--softwarevideo)";
|
|
||||||
}
|
|
||||||
if (V4L2_DECODE) {
|
|
||||||
// An ARM build may carry three: the stateless V4L2 decoders, Rockchip's own, and the
|
|
||||||
// stateful V4L2 ones. Which of them a board actually has is a question for the board.
|
|
||||||
if (RKMPP_DECODE && V4L2_REQUEST_DECODE) {
|
|
||||||
return "hardware v4l2 stateless, rkmpp or v4l2m2m where the board allows, software otherwise";
|
|
||||||
}
|
|
||||||
if (V4L2_REQUEST_DECODE) {
|
|
||||||
return "hardware v4l2 stateless or v4l2m2m where the board allows, software otherwise";
|
|
||||||
}
|
|
||||||
if (RKMPP_DECODE) {
|
|
||||||
return "hardware rkmpp or v4l2m2m where the board allows, software otherwise";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "hardware v4l2m2m where the codec allows, software otherwise";
|
|
||||||
}
|
|
||||||
#if defined(_WIN32)
|
|
||||||
return "hardware d3d11va or dxva2 where the codec allows, software otherwise";
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
return "hardware videotoolbox where the codec allows, software otherwise";
|
|
||||||
#else
|
|
||||||
return "hardware vaapi or vdpau where the codec allows, software otherwise";
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Reads one pixel of the frame being shown. Returns false if there is no frame yet.
|
// Reads one pixel of the frame being shown. Returns false if there is no frame yet.
|
||||||
bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) {
|
bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) {
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel");
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel");
|
||||||
|
|
@ -2200,6 +2028,65 @@ bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The language of a subtitle track as the file labels it, "" when it says nothing. The pointer is
|
||||||
|
// good until the next call.
|
||||||
|
const char *videoGetSubtitleLanguage(int32_t playerHandle, int32_t track) {
|
||||||
|
static char language[LANGUAGE_CODE_BYTES];
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleLanguage");
|
||||||
|
AVFormatContext *format = _formatOpen(v->filename);
|
||||||
|
AVDictionaryEntry *entry = NULL;
|
||||||
|
int32_t seen = 0;
|
||||||
|
uint32_t x = 0;
|
||||||
|
|
||||||
|
language[0] = '\0';
|
||||||
|
if (format == NULL) {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
if (avformat_find_stream_info(format, NULL) >= 0) {
|
||||||
|
for (x = 0; x < format->nb_streams; x++) {
|
||||||
|
if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seen == track) {
|
||||||
|
entry = av_dict_get(format->streams[x]->metadata, "language", NULL, 0);
|
||||||
|
if ((entry != NULL) && (entry->value != NULL)) {
|
||||||
|
SDL_strlcpy(language, entry->value, sizeof(language));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_formatClose(&format);
|
||||||
|
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// How many subtitle tracks the file holds, bitmap ones included: they are counted so that the
|
||||||
|
// numbering matches what a player or ffprobe shows, and videoReadSubtitles says which have words.
|
||||||
|
int32_t videoGetSubtitleTracks(int32_t playerHandle) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetSubtitleTracks");
|
||||||
|
AVFormatContext *format = _formatOpen(v->filename);
|
||||||
|
int32_t count = 0;
|
||||||
|
uint32_t x = 0;
|
||||||
|
|
||||||
|
if (format == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (avformat_find_stream_info(format, NULL) >= 0) {
|
||||||
|
for (x = 0; x < format->nb_streams; x++) {
|
||||||
|
if (format->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_formatClose(&format);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent) {
|
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent) {
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetVolume");
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetVolume");
|
||||||
|
|
||||||
|
|
@ -2397,6 +2284,119 @@ void videoQuit(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// One subtitle track read out of the container as SubRip text, which is the form the engine's own
|
||||||
|
// subtitle loader already takes. The whole file is walked once, decoding nothing but the subtitle
|
||||||
|
// packets, so this costs a pass over the file and no picture decoding at all. NULL when the track
|
||||||
|
// does not exist, holds pictures rather than words, or yields nothing; free() what comes back.
|
||||||
|
char *videoReadSubtitles(int32_t playerHandle, int32_t track) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoReadSubtitles");
|
||||||
|
AVFormatContext *format = _formatOpen(v->filename);
|
||||||
|
AVCodecContext *codec = NULL;
|
||||||
|
const AVCodec *decoder = NULL;
|
||||||
|
AVPacket *packet = NULL;
|
||||||
|
AVSubtitle subtitle;
|
||||||
|
AVRational timeBase;
|
||||||
|
char *text = NULL;
|
||||||
|
char *words = NULL;
|
||||||
|
char line[SRT_CUE_MAX];
|
||||||
|
char from[SRT_TIME_MAX];
|
||||||
|
char to[SRT_TIME_MAX];
|
||||||
|
size_t used = 0;
|
||||||
|
size_t room = 0;
|
||||||
|
int64_t start = 0;
|
||||||
|
int64_t end = 0;
|
||||||
|
int32_t stream = -1;
|
||||||
|
int32_t seen = 0;
|
||||||
|
int32_t cues = 0;
|
||||||
|
int got = 0;
|
||||||
|
uint32_t x = 0;
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
|
if (format == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (avformat_find_stream_info(format, NULL) < 0) {
|
||||||
|
_formatClose(&format);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (x = 0; x < format->nb_streams; x++) {
|
||||||
|
if (format->streams[x]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seen == track) {
|
||||||
|
stream = (int32_t)x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seen++;
|
||||||
|
}
|
||||||
|
if (stream < 0) {
|
||||||
|
_formatClose(&format);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
timeBase = format->streams[stream]->time_base;
|
||||||
|
decoder = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
|
||||||
|
codec = (decoder != NULL) ? avcodec_alloc_context3(decoder) : NULL;
|
||||||
|
packet = av_packet_alloc();
|
||||||
|
if ((codec == NULL) || (packet == NULL) ||
|
||||||
|
(avcodec_parameters_to_context(codec, format->streams[stream]->codecpar) < 0) ||
|
||||||
|
(avcodec_open2(codec, decoder, NULL) < 0)) {
|
||||||
|
utilTrace("Video %d: subtitle track %d has no decoder in this build.", v->id, track);
|
||||||
|
av_packet_free(&packet);
|
||||||
|
avcodec_free_context(&codec);
|
||||||
|
_formatClose(&format);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
while (ok && (av_read_frame(format, packet) >= 0)) {
|
||||||
|
if (packet->stream_index != stream) {
|
||||||
|
av_packet_unref(packet);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
memset(&subtitle, 0, sizeof(subtitle));
|
||||||
|
got = 0;
|
||||||
|
if (avcodec_decode_subtitle2(codec, &subtitle, &got, packet) >= 0) {
|
||||||
|
if (got != 0) {
|
||||||
|
start = (int64_t)((double)packet->pts * av_q2d(timeBase) * MS_PER_SECOND) + (int64_t)subtitle.start_display_time;
|
||||||
|
if (subtitle.end_display_time > subtitle.start_display_time) {
|
||||||
|
end = start + (int64_t)(subtitle.end_display_time - subtitle.start_display_time);
|
||||||
|
} else {
|
||||||
|
end = start + (int64_t)((double)packet->duration * av_q2d(timeBase) * MS_PER_SECOND);
|
||||||
|
}
|
||||||
|
for (x = 0; (x < subtitle.num_rects) && ok; x++) {
|
||||||
|
words = _srtFromRect(subtitle.rects[x]);
|
||||||
|
if (words == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cues++;
|
||||||
|
_srtTime(start, from, sizeof(from));
|
||||||
|
_srtTime(end, to, sizeof(to));
|
||||||
|
snprintf(line, sizeof(line), "%d\n", cues);
|
||||||
|
ok = _srtAppend(&text, &used, &room, line) &&
|
||||||
|
_srtAppend(&text, &used, &room, from) &&
|
||||||
|
_srtAppend(&text, &used, &room, " --> ") &&
|
||||||
|
_srtAppend(&text, &used, &room, to) &&
|
||||||
|
_srtAppend(&text, &used, &room, "\n") &&
|
||||||
|
_srtAppend(&text, &used, &room, words) &&
|
||||||
|
_srtAppend(&text, &used, &room, "\n\n");
|
||||||
|
free(words);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avsubtitle_free(&subtitle);
|
||||||
|
}
|
||||||
|
av_packet_unref(packet);
|
||||||
|
}
|
||||||
|
av_packet_free(&packet);
|
||||||
|
avcodec_free_context(&codec);
|
||||||
|
_formatClose(&format);
|
||||||
|
if (!ok || (cues == 0)) {
|
||||||
|
free(text);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
utilTrace("Video %d: subtitle track %d read as %d cues.", v->id, track, cues);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Replaces the audio side with another file (NULL: the video's own audio, or silence when it has
|
// Replaces the audio side with another file (NULL: the video's own audio, or silence when it has
|
||||||
// none) and realigns it at the current frame, keeping the volume, the play state and the track
|
// none) and realigns it at the current frame, keeping the volume, the play state and the track
|
||||||
// index when the new file has it. False, with the old audio untouched, when the file cannot be read.
|
// index when the new file has it. False, with the old audio untouched, when the file cannot be read.
|
||||||
|
|
@ -2485,12 +2485,6 @@ void videoSetBlend(int32_t playerHandle, bool enabled) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Chosen before any video loads; existing players keep whatever they opened with.
|
|
||||||
void videoSetHardwareDecoding(bool enabled) {
|
|
||||||
_hardwareDecoding = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Brightens or darkens the picture. The level runs 0 to LUMA_LEVEL_MAX with LUMA_LEVEL_NEUTRAL
|
// Brightens or darkens the picture. The level runs 0 to LUMA_LEVEL_MAX with LUMA_LEVEL_NEUTRAL
|
||||||
// changing nothing, and the distance from neutral is applied to every luma sample in eighths, so 0
|
// changing nothing, and the distance from neutral is applied to every luma sample in eighths, so 0
|
||||||
// halves the brightness and 8 raises it by half. Turning it off, or asking for the neutral level,
|
// halves the brightness and 8 raises it by half. Turning it off, or asking for the neutral level,
|
||||||
|
|
@ -2500,6 +2494,12 @@ void videoSetDeinterlace(DeinterlaceE mode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Chosen before any video loads; existing players keep whatever they opened with.
|
||||||
|
void videoSetHardwareDecoding(bool enabled) {
|
||||||
|
_hardwareDecoding = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void videoSetLuma(int32_t playerHandle, bool enabled, int32_t level) {
|
void videoSetLuma(int32_t playerHandle, bool enabled, int32_t level) {
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetLuma");
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetLuma");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
-- A game description: data, not code. Singe/AuthorCompile.singe turns it into a Singe game.
|
-- A game description: data, not code. Forge/AuthorCompile.singe turns it into a Singe game.
|
||||||
--
|
--
|
||||||
-- Genre one of two. This one uses a single world2d layer and shares nothing with the QTE beside
|
-- Genre one of two. This one uses a single world2d layer and shares nothing with the QTE beside
|
||||||
-- it -- no disc, no branching, no time windows -- which is the point: if the core can express
|
-- it -- no disc, no branching, no time windows -- which is the point: if the core can express
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,26 @@ check("f:lines count", count, 3)
|
||||||
check("f:lines leaves it open", io.type(g), "file")
|
check("f:lines leaves it open", io.type(g), "file")
|
||||||
g:close()
|
g:close()
|
||||||
|
|
||||||
|
-- More than one format a step keeps every value, and a later format coming up empty hands back
|
||||||
|
-- what came before it with nil in its place, as Lua does; only an empty first read ends the loop.
|
||||||
|
local pairs = {}
|
||||||
|
|
||||||
|
for a, b in io.lines("packedIo/numbers.txt", "n", "n") do
|
||||||
|
pairs[#pairs + 1] = { a, b }
|
||||||
|
end
|
||||||
|
check("lines n n steps", #pairs, 2)
|
||||||
|
check("lines n n first", pairs[1][1] + pairs[1][2], 46.5)
|
||||||
|
check("lines n n second", pairs[2][1], 16)
|
||||||
|
check("lines n n short", pairs[2][2], nil)
|
||||||
|
|
||||||
|
-- read(0) is the test for the end of the file.
|
||||||
|
local z = io.open("packedIo/tail.txt")
|
||||||
|
|
||||||
|
check("read 0 with more", z:read(0), "")
|
||||||
|
z:read("a")
|
||||||
|
check("read 0 at end", z:read(0), nil)
|
||||||
|
z:close()
|
||||||
|
|
||||||
-- Numbers, and the default input.
|
-- Numbers, and the default input.
|
||||||
local n = io.open("packedIo/numbers.txt")
|
local n = io.open("packedIo/numbers.txt")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,16 +59,16 @@ function onOverlayUpdate()
|
||||||
|
|
||||||
frames = frames + 1
|
frames = frames + 1
|
||||||
if not held then
|
if not held then
|
||||||
authorKeyDown(0, SCANCODE.RIGHT)
|
authorKeyDown(0, SCANCODE.RIGHT.value)
|
||||||
held = true
|
held = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Jump once, when the hero is under way and has ground beneath it.
|
-- Jump once, when the hero is under way and has ground beneath it.
|
||||||
if (not jumped) and (hx >= JUMP_AT) and playerIsOnGround(hero.node) then
|
if (not jumped) and (hx >= JUMP_AT) and playerIsOnGround(hero.node) then
|
||||||
authorKeyDown(0, SCANCODE.SPACE)
|
authorKeyDown(0, SCANCODE.SPACE.value)
|
||||||
jumped = true
|
jumped = true
|
||||||
elseif jumped then
|
elseif jumped then
|
||||||
authorKeyUp(0, SCANCODE.SPACE)
|
authorKeyUp(0, SCANCODE.SPACE.value)
|
||||||
end
|
end
|
||||||
|
|
||||||
local r = gameUpdate()
|
local r = gameUpdate()
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ local function step()
|
||||||
|
|
||||||
if after == before then
|
if after == before then
|
||||||
debugPrint("RULES FAIL the new rule did not reach the compiled game")
|
debugPrint("RULES FAIL the new rule did not reach the compiled game")
|
||||||
elseif after:find('authorKeyHeld%(SCANCODE%.UP%)') and after:find('authorOnGround%("hero"%)') and after:find('authorJump%("hero"%)') then
|
elseif after:find('authorKeyHeld%(SCANCODE%.UP%.value%)') and after:find('authorOnGround%("hero"%)') and after:find('authorJump%("hero"%)') then
|
||||||
debugPrint("RULES the compiled game carries the new rule")
|
debugPrint("RULES the compiled game carries the new rule")
|
||||||
else
|
else
|
||||||
debugPrint("RULES FAIL the compiled game is missing part of the new rule")
|
debugPrint("RULES FAIL the compiled game is missing part of the new rule")
|
||||||
|
|
@ -89,7 +89,7 @@ local function step()
|
||||||
forgePartDelete()
|
forgePartDelete()
|
||||||
local after = compiled()
|
local after = compiled()
|
||||||
|
|
||||||
if after:find('authorKeyHeld%(SCANCODE%.UP%)') then
|
if after:find('authorKeyHeld%(SCANCODE%.UP%.value%)') then
|
||||||
debugPrint("RULES FAIL the deleted condition is still compiled")
|
debugPrint("RULES FAIL the deleted condition is still compiled")
|
||||||
else
|
else
|
||||||
debugPrint("RULES the deleted condition is gone from the compiled game")
|
debugPrint("RULES the deleted condition is gone from the compiled game")
|
||||||
|
|
|
||||||
236
testScripts/scene57.singe
Normal file
236
testScripts/scene57.singe
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
-- Forge as a game (PLAN section 58): the chooser, the engine callbacks, and the editing that
|
||||||
|
-- needs no pointer -- entities added, renamed and deleted, a rule built from the pickers, undo.
|
||||||
|
--
|
||||||
|
-- Unlike scene54 to 56 this does not set FORGE_LIBRARY: Forge installs its own callbacks, and
|
||||||
|
-- the scene drives them the way the engine would, two integers to onKeyPressed. What is
|
||||||
|
-- asserted is that every key reaches the description and that a rename reaches the rules.
|
||||||
|
dofile("Forge/Forge.singe")
|
||||||
|
|
||||||
|
local forgeUpdate = onOverlayUpdate
|
||||||
|
local frames = 0
|
||||||
|
local stage = 0
|
||||||
|
local opened = nil
|
||||||
|
|
||||||
|
|
||||||
|
local function key(code, character)
|
||||||
|
onKeyPressed(character or 0, code.value)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function typeIn(text)
|
||||||
|
for c in string.gmatch(text, ".") do
|
||||||
|
key(SCANCODE.A, string.byte(c))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function step()
|
||||||
|
if stage == 1 then
|
||||||
|
-- The chooser is up and offers a new game at least.
|
||||||
|
if FORGE.game ~= nil then
|
||||||
|
debugPrint("GAME FAIL the editor is open before anything was chosen")
|
||||||
|
end
|
||||||
|
debugPrint("GAME the chooser offers " .. #forgeFiles() .. " entries")
|
||||||
|
-- The last entry is always a new game.
|
||||||
|
for _ = 1, 20 do
|
||||||
|
key(SCANCODE.DOWN)
|
||||||
|
end
|
||||||
|
key(SCANCODE.RETURN)
|
||||||
|
if FORGE.game == nil then
|
||||||
|
debugPrint("GAME FAIL ENTER on the chooser opened nothing")
|
||||||
|
else
|
||||||
|
opened = FORGE.path
|
||||||
|
debugPrint("GAME opened " .. opened .. " with " .. #FORGE.game.entities .. " entities")
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif stage == 2 then
|
||||||
|
-- Add an entity, name it, and give it a look by typing the fields ENTER walks.
|
||||||
|
local count = #FORGE.game.entities
|
||||||
|
|
||||||
|
key(SCANCODE.A)
|
||||||
|
if #FORGE.game.entities ~= count + 1 then
|
||||||
|
debugPrint("GAME FAIL A did not add an entity")
|
||||||
|
end
|
||||||
|
key(SCANCODE.RETURN) -- id
|
||||||
|
typeIn("coin")
|
||||||
|
key(SCANCODE.RETURN) -- x
|
||||||
|
typeIn("300")
|
||||||
|
key(SCANCODE.RETURN) -- y
|
||||||
|
typeIn("200")
|
||||||
|
key(SCANCODE.RETURN) -- kind
|
||||||
|
key(SCANCODE.ESCAPE) -- untouched
|
||||||
|
local coin = FORGE.game.entities[#FORGE.game.entities]
|
||||||
|
|
||||||
|
if coin.id == "coin" and coin.x == 300 and coin.y == 200 and coin.look.kind == "box" then
|
||||||
|
debugPrint("GAME typed an entity in: " .. coin.id .. " at " .. coin.x .. "," .. coin.y)
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL the typed entity is " .. tostring(coin.id) .. " at " .. tostring(coin.x) .. "," .. tostring(coin.y) .. " " .. tostring(coin.look.kind))
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif stage == 3 then
|
||||||
|
-- Renaming the hero has to reach every rule that names it.
|
||||||
|
key(SCANCODE.UP)
|
||||||
|
key(SCANCODE.UP)
|
||||||
|
key(SCANCODE.UP)
|
||||||
|
key(SCANCODE.UP)
|
||||||
|
key(SCANCODE.DOWN) -- ground, then hero
|
||||||
|
key(SCANCODE.RETURN)
|
||||||
|
typeIn("player")
|
||||||
|
key(SCANCODE.ESCAPE) -- ESC puts it back: the hero keeps its name
|
||||||
|
if FORGE.game.entities[FORGE.selected].id ~= "hero" then
|
||||||
|
debugPrint("GAME FAIL escape did not put the name back")
|
||||||
|
end
|
||||||
|
key(SCANCODE.RETURN)
|
||||||
|
typeIn("player")
|
||||||
|
key(SCANCODE.RETURN) -- commits, moves on to x
|
||||||
|
key(SCANCODE.ESCAPE)
|
||||||
|
local named = 0
|
||||||
|
|
||||||
|
for _, rule in ipairs(FORGE.game.rules) do
|
||||||
|
for _, item in ipairs(rule.act) do
|
||||||
|
if item.entity == "player" then
|
||||||
|
named = named + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if FORGE.game.entities[FORGE.selected].id == "player" and named == 3 then
|
||||||
|
debugPrint("GAME renamed the hero and " .. named .. " actions followed")
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL rename: id " .. tostring(FORGE.game.entities[FORGE.selected].id) .. ", " .. named .. " actions renamed")
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif stage == 4 then
|
||||||
|
-- A rule from the pickers: N, then C picks the first condition, T the first action.
|
||||||
|
local rules = #FORGE.game.rules
|
||||||
|
|
||||||
|
key(SCANCODE.TAB)
|
||||||
|
key(SCANCODE.N)
|
||||||
|
key(SCANCODE.C)
|
||||||
|
if FORGE.picker == nil then
|
||||||
|
debugPrint("GAME FAIL C opened no picker")
|
||||||
|
end
|
||||||
|
key(SCANCODE.DOWN)
|
||||||
|
key(SCANCODE.RETURN)
|
||||||
|
key(SCANCODE.T)
|
||||||
|
key(SCANCODE.RETURN)
|
||||||
|
local rule = FORGE.game.rules[#FORGE.game.rules]
|
||||||
|
|
||||||
|
if #FORGE.game.rules == rules + 1 and #rule.when == 1 and #rule.act == 1 then
|
||||||
|
debugPrint("GAME built a rule from the pickers: when " .. rule.when[1][1] .. " then " .. rule.act[1][1])
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL the picked rule has " .. #(rule.when or {}) .. " conditions and " .. #(rule.act or {}) .. " actions")
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif stage == 5 then
|
||||||
|
-- Undo takes the action back, then the condition, then the rule; redo brings the rule
|
||||||
|
-- back, and a change by hand ends the redo history.
|
||||||
|
local rules = #FORGE.game.rules
|
||||||
|
|
||||||
|
key(SCANCODE.U)
|
||||||
|
key(SCANCODE.U)
|
||||||
|
key(SCANCODE.U)
|
||||||
|
if #FORGE.game.rules == rules - 1 then
|
||||||
|
debugPrint("GAME three undos took the rule back out")
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL after three undos there are " .. #FORGE.game.rules .. " rules, not " .. (rules - 1))
|
||||||
|
end
|
||||||
|
key(SCANCODE.R)
|
||||||
|
if (#FORGE.game.rules == rules) and (#FORGE.game.rules[rules].when == 0) then
|
||||||
|
debugPrint("GAME redo brought the empty rule back")
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL redo left " .. #FORGE.game.rules .. " rules")
|
||||||
|
end
|
||||||
|
key(SCANCODE.U)
|
||||||
|
key(SCANCODE.N)
|
||||||
|
key(SCANCODE.R)
|
||||||
|
if #FORGE.redo == 0 and #FORGE.game.rules == rules then
|
||||||
|
debugPrint("GAME a new change emptied the redo history")
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL redo history survived a change: " .. #FORGE.redo .. " kept")
|
||||||
|
end
|
||||||
|
key(SCANCODE.U)
|
||||||
|
|
||||||
|
elseif stage == 6 then
|
||||||
|
-- Duplicate and delete in the entity list.
|
||||||
|
key(SCANCODE.TAB)
|
||||||
|
local count = #FORGE.game.entities
|
||||||
|
|
||||||
|
key(SCANCODE.D)
|
||||||
|
key(SCANCODE.DELETE)
|
||||||
|
if #FORGE.game.entities == count then
|
||||||
|
debugPrint("GAME duplicate then delete leaves " .. count .. " entities")
|
||||||
|
else
|
||||||
|
debugPrint("GAME FAIL duplicate then delete leaves " .. #FORGE.game.entities)
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif stage == 7 then
|
||||||
|
-- ESC on unsaved work warns, S saves, ESC then leaves; the chooser lists the file.
|
||||||
|
key(SCANCODE.ESCAPE)
|
||||||
|
if FORGE.game == nil then
|
||||||
|
debugPrint("GAME FAIL one ESC closed unsaved work")
|
||||||
|
end
|
||||||
|
key(SCANCODE.S)
|
||||||
|
key(SCANCODE.ESCAPE)
|
||||||
|
if FORGE.game ~= nil then
|
||||||
|
debugPrint("GAME FAIL ESC did not close a saved description")
|
||||||
|
end
|
||||||
|
local listed = false
|
||||||
|
|
||||||
|
for _, entry in ipairs(forgeFiles()) do
|
||||||
|
if entry.path == opened then
|
||||||
|
listed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
debugPrint("GAME closed; the chooser lists what was saved: " .. tostring(listed))
|
||||||
|
|
||||||
|
elseif stage == 8 then
|
||||||
|
-- Open it again from the chooser -- it is listed first -- and the rename is in the file.
|
||||||
|
for _ = 1, 20 do
|
||||||
|
key(SCANCODE.UP)
|
||||||
|
end
|
||||||
|
key(SCANCODE.RETURN)
|
||||||
|
if FORGE.game == nil then
|
||||||
|
debugPrint("GAME FAIL could not reopen the saved description")
|
||||||
|
else
|
||||||
|
local found = false
|
||||||
|
|
||||||
|
for _, entity in ipairs(FORGE.game.entities) do
|
||||||
|
if entity.id == "player" then
|
||||||
|
found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
debugPrint("GAME reopened " .. FORGE.path .. "; the rename was saved: " .. tostring(found))
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif stage == 9 then
|
||||||
|
-- B builds beside the runtime, which is what makes the result playable.
|
||||||
|
key(SCANCODE.B)
|
||||||
|
local runtime = io.open(singeGetDataPath() .. "Author.singe", "r")
|
||||||
|
|
||||||
|
if runtime == nil then
|
||||||
|
debugPrint("GAME FAIL build left no runtime beside the preview")
|
||||||
|
else
|
||||||
|
runtime:close()
|
||||||
|
debugPrint("GAME the build put the runtime beside the preview")
|
||||||
|
end
|
||||||
|
debugPrint("GAME RESULT done")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
if frames % 12 == 0 and stage < 10 then
|
||||||
|
stage = stage + 1
|
||||||
|
step()
|
||||||
|
end
|
||||||
|
forgeUpdate()
|
||||||
|
if frames == 6 or frames == 40 or frames == 60 or frames == 100 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames > 130 then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue