Bug fixes and compatability improvements.
This commit is contained in:
parent
3f417b1e24
commit
8fab74e977
16 changed files with 1350 additions and 496 deletions
131
CHANGELOG
131
CHANGELOG
|
|
@ -7,6 +7,16 @@ SINGE 3.00
|
||||||
API Changes
|
API Changes
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
- math.randomseed accepts fractional seeds (floored), so 2.x games that
|
||||||
|
seed with os.clock() products run under Lua 5.4.
|
||||||
|
|
||||||
|
- A script launched by name (or by its directory) runs with its games.dat
|
||||||
|
entry, as the menu would run it: Singe finds the games.dat in the
|
||||||
|
script's directory or up to three above, and applies the entry whose
|
||||||
|
SCRIPT names the script, LEGACY_SPRITE_ARGS included. Options typed on
|
||||||
|
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.
|
||||||
|
|
||||||
- The name now expands to "SINGE Is Not a Game Emulator", in the banner,
|
- The name now expands to "SINGE Is Not a Game Emulator", in the banner,
|
||||||
the manual, INSTALL, and the Windows file description.
|
the manual, INSTALL, and the Windows file description.
|
||||||
|
|
||||||
|
|
@ -351,11 +361,61 @@ API Changes
|
||||||
|
|
||||||
- lightSetCone raises the outer angle to at least the inner angle.
|
- lightSetCone raises the outer angle to at least the inner angle.
|
||||||
|
|
||||||
|
- Packed games: the data directory is always <base>/<database name>/
|
||||||
|
<script directory>/. An archive whose script sat in a subdirectory
|
||||||
|
(Game/Main.singe) wrote to <base>/Game/, shared with every archive of
|
||||||
|
that layout; its saves now belong under <base>/<name>/Game/ and must be
|
||||||
|
moved by hand. Archives whose script sits at the root are unaffected.
|
||||||
|
|
||||||
|
- Packed games: a path naming the .game file itself (Games/DLe.game,
|
||||||
|
with or without a trailing separator) addresses the database root, as
|
||||||
|
"." does from inside: lfs.dir lists it and lfs.attributes reports a
|
||||||
|
directory, while io.open on it returns nil and lfs.mkdir and lfs.rmdir
|
||||||
|
refuse it. Engine-owned names (Singe/ and the data directory) may use
|
||||||
|
".." as long as they stay below that prefix; any other ".." inside a
|
||||||
|
packed game is still refused.
|
||||||
|
|
||||||
|
- lfs.attributes inside a packed game answers as the real lfs does: the
|
||||||
|
whole table (mode, size, modification and the rest) with one argument,
|
||||||
|
one field by name (an unknown name raises "invalid attribute name"),
|
||||||
|
or a table of the caller's filled in; a missing file returns nil, a
|
||||||
|
message and the error number.
|
||||||
|
|
||||||
|
- sceneProject returns false with 0, 0, 0 before the scene has rendered
|
||||||
|
a frame, instead of inf and nan.
|
||||||
|
|
||||||
|
- softNew removes the node's existing soft body before it looks at the
|
||||||
|
mesh, so a call that fails leaves the node with none; softNew on a
|
||||||
|
node carrying a rope fails with "needs a mesh" (the tube was the
|
||||||
|
engine's) instead of reading freed memory.
|
||||||
|
|
||||||
|
- navAgentNew keeps the radius and height at 0.01 or more, as navNew
|
||||||
|
does, so onNavArrived fires for an agent given a radius of zero.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Fixes
|
Fixes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
- A single-frame image saved as a 1-bit PNG (many overlay assets from
|
||||||
|
the 2.x era) loads again; spriteLoad reported "Blit combination not
|
||||||
|
supported" on it. Images with 16 bits per channel or floating point
|
||||||
|
pixels (16-bit PNGs among them) load as sprites, animation frames and
|
||||||
|
particle textures; they ended the program.
|
||||||
|
|
||||||
|
- lfs.dir(".") and other names for the root work inside a packed game;
|
||||||
|
they were refused as if the directory did not exist.
|
||||||
|
|
||||||
|
- --sindengun and --scalefactor are listed in --help and the manual;
|
||||||
|
they were hidden because mouse positions ignored the Sinden border and
|
||||||
|
the overscan shrink. Positions now map into the picture actually
|
||||||
|
drawn, so a light gun lands where it points inside the border. The
|
||||||
|
border is drawn the right way round (black ring outside, white ring
|
||||||
|
inside, the game in the middle); one too wide for the video ends the
|
||||||
|
program with a message instead of a zero-sized game; and light guns
|
||||||
|
and tablets reporting absolute positions through ManyMouse are mapped
|
||||||
|
into the picture like the mouse.
|
||||||
|
|
||||||
- The manual's 3D Scenes, Physics and Particles chapters are rewritten as
|
- The manual's 3D Scenes, Physics and Particles chapters are rewritten as
|
||||||
sections in a learning order with worked examples, every reference
|
sections in a learning order with worked examples, every reference
|
||||||
entry now shows each form the call accepts with a realistic example,
|
entry now shows each form the call accepts with a realistic example,
|
||||||
|
|
@ -483,18 +543,51 @@ Fixes
|
||||||
- playerSetEnabled(node, false) removes the player's body from the
|
- playerSetEnabled(node, false) removes the player's body from the
|
||||||
world and reports it leaving every trigger it stood in; enabling puts
|
world and reports it leaving every trigger it stood in; enabling puts
|
||||||
it back. navPath ends a partial path at the last reachable point.
|
it back. navPath ends a partial path at the last reachable point.
|
||||||
navLoad rejects data that is not a Detour mesh. The world holds at
|
navLoad rejects data that is not a Detour mesh, or a file shorter
|
||||||
most 4096 bodies; bodyNew raises an error when it is full.
|
than the mesh its header describes ("the navigation mesh file is
|
||||||
|
truncated or damaged"). The world holds at most 4096 bodies; bodyNew
|
||||||
|
raises an error when it is full.
|
||||||
|
|
||||||
|
- A body, player, ragdoll, soft body or vehicle whose node was deleted
|
||||||
|
is released at the start of the next frame, disabled, inactive, static
|
||||||
|
or paused or not; a disabled player or an inactive ragdoll on a deleted
|
||||||
|
node kept its slot (16 players, 8 ragdolls) forever. playerExists,
|
||||||
|
softExists, ragdollExists, vehicleExists and every setter answer false
|
||||||
|
for a node handle that was deleted and reused instead of driving the
|
||||||
|
previous owner's record. bodySetTrigger(node, false) no longer
|
||||||
|
swallows the leave of a trigger the node is standing in.
|
||||||
|
physicsSetDebug's mask is cleared by a reload.
|
||||||
|
|
||||||
|
- Sprite nodes are no longer culled early at the edge of the view, and
|
||||||
|
shadow fitting sees their real size. Soft particles fade correctly in
|
||||||
|
a scene with no meshes instead of against stale depth. Point-light
|
||||||
|
shadows update when a caster's mesh is rewritten (soft bodies), given
|
||||||
|
morph targets, or swapped with nodeSetMesh at an unchanged transform;
|
||||||
|
the six cached faces could freeze in the old shape. Cascaded sun
|
||||||
|
shadows appear in render-to-texture views (viewNew) instead of
|
||||||
|
vanishing; a view takes the finest cascade that covers each point.
|
||||||
|
Debug lines no longer pile up while the scene layer is enabled but has
|
||||||
|
no target.
|
||||||
|
|
||||||
- modelLoad refuses a skin with more than 128 joints or a skinned mesh
|
- modelLoad refuses a skin with more than 128 joints or a skinned mesh
|
||||||
naming a joint its skin lacks (the shader used to read past the
|
naming a joint numbered 128 or more (the shader used to read past the
|
||||||
matrices). A texture that cannot be decoded, or a missing KTX2 image,
|
matrices); a joint beyond the skins that use the mesh is traced and the
|
||||||
is traced and the load goes on without it instead of leaving
|
load goes on, so a mesh shared by skins of different sizes loads. A
|
||||||
modelLastError set. KHR_materials_emissive_strength is applied.
|
texture that cannot be decoded, or a missing KTX2 image, is traced and
|
||||||
Sparse accessors load, so Blender shape keys are no longer silently
|
the load goes on without it instead of leaving modelLastError set.
|
||||||
zero. Alpha MASK materials, textures on TEXCOORD_1 and up, and
|
KHR_materials_emissive_strength is applied. Sparse accessors load,
|
||||||
|
animation keyframes included, so Blender shape keys are no longer
|
||||||
|
silently zero; a sampler that cannot be read is traced and skipped,
|
||||||
|
and a rotation channel with a malformed output no longer produces NaN
|
||||||
|
transforms. Alpha MASK materials, textures on TEXCOORD_1 and up, and
|
||||||
KHR_texture_transform are traced as unsupported.
|
KHR_texture_transform are traced as unsupported.
|
||||||
|
|
||||||
|
- After a script reload, or any stall longer than a tenth of a second,
|
||||||
|
model animations advance by at most a tenth of a second instead of the
|
||||||
|
whole gap; a non-looping clip used to be finished before its first
|
||||||
|
frame. A 3D emitter whose node is deleted is removed even when the
|
||||||
|
handle is reused by a later nodeNew; it used to jump to the new node.
|
||||||
|
|
||||||
- Video index files are named <name>-<hash>.index, the hash from the
|
- Video index files are named <name>-<hash>.index, the hash from the
|
||||||
video's full name, so two videos with one base name in a game stop
|
video's full name, so two videos with one base name in a game stop
|
||||||
rebuilding each other's index. Old <name>.index files are ignored and
|
rebuilding each other's index. Old <name>.index files are ignored and
|
||||||
|
|
@ -514,8 +607,26 @@ Fixes
|
||||||
- A reload (F5, --reload, singeReload) ignores keys and buttons held
|
- A reload (F5, --reload, singeReload) ignores keys and buttons held
|
||||||
across it until they are released, resets the effect channels and their
|
across it until they are released, resets the effect channels and their
|
||||||
pending onSoundCompleted queue, the held switches and the 2D particle
|
pending onSoundCompleted queue, the held switches and the 2D particle
|
||||||
textures, and no longer calls into the closed Lua state when it happens
|
textures, restores the effects volume, mouse mode, mouse enabled state
|
||||||
while paused, so the new script starts exactly as a fresh launch does.
|
and mouse capture, parks the disc on frame 1 paused, and no longer
|
||||||
|
calls into the closed Lua state when it happens while paused, so the
|
||||||
|
new script starts exactly as a fresh launch does. Under --reload a
|
||||||
|
script that fails to compile stays watched, so saving the fix triggers
|
||||||
|
the reload.
|
||||||
|
|
||||||
|
- Quitting with a stick past its dead zone no longer crashes; the
|
||||||
|
release reaches onInputReleased after onShutdown, before the state
|
||||||
|
closes. Plugging in a gamepad no longer resets the positions
|
||||||
|
mouseGetPosition reports, and a controls.cfg mapping that lists the
|
||||||
|
same code twice fires its switch once.
|
||||||
|
|
||||||
|
- materialSetVideo(material) on a framefile disc keeps showing the disc
|
||||||
|
after a segment change.
|
||||||
|
|
||||||
|
- In a packed game the controls.cfg at the root of the data directory
|
||||||
|
loads again, io.open(".") returns nil with a message instead of a
|
||||||
|
handle whose first read fails, and a directory listing where a loose
|
||||||
|
file and a packed one differ only in case shows the loose spelling.
|
||||||
|
|
||||||
- Error messages: an invalid view, navigation mesh or navigation agent
|
- Error messages: an invalid view, navigation mesh or navigation agent
|
||||||
handle reports "No <thing> N." like every other handle; navAgentNew,
|
handle reports "No <thing> N." like every other handle; navAgentNew,
|
||||||
|
|
|
||||||
103
docs/Manual.adoc
103
docs/Manual.adoc
|
|
@ -108,7 +108,10 @@ Singe [OPTIONS] gameName
|
||||||
The game name is the only required argument. It may be a `.singe` script, a
|
The game name is the only required argument. It may be a `.singe` script, a
|
||||||
directory containing a script of the same name (`ActionMax` finds
|
directory containing a script of the same name (`ActionMax` finds
|
||||||
`ActionMax/ActionMax.singe`), or a packed game (`DLe.game` runs the first
|
`ActionMax/ActionMax.singe`), or a packed game (`DLe.game` runs the first
|
||||||
entry of the `games.dat` inside it, `--entry=N` another). For a laserdisc game (`--disc`) with no
|
entry of the `games.dat` inside it, `--entry=N` another). A script or directory
|
||||||
|
launched this way also runs with its `games.dat` entry when one names it (see
|
||||||
|
<<gamesdat,games.dat>>), so a game plays the same from the command line as from
|
||||||
|
the menu. For a laserdisc game (`--disc`) with no
|
||||||
`--framefile`, Singe looks for a video next to the script with the same base
|
`--framefile`, Singe looks for a video next to the script with the same base
|
||||||
name and any extension FFmpeg can demux, then for a `.txt` framefile.
|
name and any extension FFmpeg can demux, then for a `.txt` framefile.
|
||||||
|
|
||||||
|
|
@ -117,13 +120,15 @@ name and any extension FFmpeg can demux, then for a `.txt` framefile.
|
||||||
| Option | Purpose
|
| Option | Purpose
|
||||||
| `-A`, `--audiodelay=MS` | Compensate for audio that is heard `MS` milliseconds later than the engine can measure (negative when it is heard early), `-1000` to `1000`. See <<audiosync,Audio Sync>>.
|
| `-A`, `--audiodelay=MS` | Compensate for audio that is heard `MS` milliseconds later than the engine can measure (negative when it is heard early), `-1000` to `1000`. See <<audiosync,Audio Sync>>.
|
||||||
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
|
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
|
||||||
|
| `-b`, `--scalefactor=PERCENT` | Shrink the picture about its centre to `PERCENT` (`50` to `100`) of the window for displays that lose their edges to overscan. Mouse and light gun positions follow the shrunken picture. Cannot be combined with `--sindengun`.
|
||||||
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
|
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
|
||||||
| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc.
|
| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc.
|
||||||
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
|
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
|
||||||
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory.
|
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it (for a packed game, one named for the database and then for the script's directory inside it). Defaults to the game's own directory.
|
||||||
| `-E`, `--entry=N` | Run the Nth entry of the `games.dat` inside a `.game` file (default 1). See <<singlefile,Single-File Games>>.
|
| `-E`, `--entry=N` | Run the Nth entry of the `games.dat` inside a `.game` file (default 1). See <<singlefile,Single-File Games>>.
|
||||||
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
|
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
|
||||||
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
|
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
|
||||||
|
| `-g`, `--sindengun='PARAMS'` | Draw the border a Sinden light gun tracks and map mouse and light gun positions into the picture inside it: the black ring, when one is given, runs along the edge of the video, the white ring sits inside it, and the game is drawn in the middle. `PARAMS` is one quoted list of numbers: `WW` (white border width in video pixels); `WW WB` (white width, then a black border outside it); `RW GW BW WW` (a custom border color and width); `RW GW BW WW WB` (the same plus a black width); or `RW GW BW WW RB GB BB WB` (custom colors and widths for both). A border that leaves no room for the game (twice the white and black widths together reaching the smaller side of the video), or a negative width, ends the program with a message. Cannot be combined with `--scalefactor`.
|
||||||
| `-H`, `--softwarevideo` | Decode video in software even when the platform offers a hardware decoder (VA-API or VDPAU on Linux, D3D11VA on Windows, VideoToolbox on macOS). Use it to rule the hardware path in or out when a video misbehaves; the program trace says which decoder is in use.
|
| `-H`, `--softwarevideo` | Decode video in software even when the platform offers a hardware decoder (VA-API or VDPAU on Linux, D3D11VA on Windows, VideoToolbox on macOS). Use it to rule the hardware path in or out when a video misbehaves; the program trace says which decoder is in use.
|
||||||
| `-h`, `--help` | Show the option summary and exit.
|
| `-h`, `--help` | Show the option summary and exit.
|
||||||
| `-k`, `--nologos` | Skip the splash screens.
|
| `-k`, `--nologos` | Skip the splash screens.
|
||||||
|
|
@ -136,7 +141,7 @@ name and any extension FFmpeg can demux, then for a `.txt` framefile.
|
||||||
| `-s`, `--nosound` | Mute all audio.
|
| `-s`, `--nosound` | Mute all audio.
|
||||||
| `-T`, `--patch=GAME.game` | Replace files in the packed game from the directory or patch database named after the options, then exit.
|
| `-T`, `--patch=GAME.game` | Replace files in the packed game from the directory or patch database named after the options, then exit.
|
||||||
| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`.
|
| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`.
|
||||||
| `-R`, `--reload` | Watch the game's loose script files and run the game again from scratch when one changes; `F5` does the same on demand. For working on a game; see Reloading While You Work.
|
| `-R`, `--reload` | Watch the game's loose script files and run the game again from scratch when one changes; `F5` does the same on demand. A script that fails to compile stays watched, so saving the fix reloads it. For working on a game; see Reloading While You Work.
|
||||||
| `-U`, `--unpack=GAME.game` | Write the packed game's files into the directory named after the options, then exit. A database whose stored names would land outside that directory is refused.
|
| `-U`, `--unpack=GAME.game` | Write the packed game's files into the directory named after the options, then exit. A database whose stored names would land outside that directory is refused.
|
||||||
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
|
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
|
||||||
| `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script.
|
| `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script.
|
||||||
|
|
@ -278,14 +283,16 @@ Run the game with `--reload` (`-R`) and Singe watches every loose script file
|
||||||
the game has loaded. Save a change to one of them and the game starts again
|
the game has loaded. Save a change to one of them and the game starts again
|
||||||
from its script a moment later: everything the script made goes (sprites,
|
from its script a moment later: everything the script made goes (sprites,
|
||||||
sounds, fonts, videos, the 3D scene, physics, emitters, the overlay back to
|
sounds, fonts, videos, the 3D scene, physics, emitters, the overlay back to
|
||||||
its default size), the engine stays (the window, the GPU, the disc keeps
|
its default size, the effects volume and the mouse back to their startup
|
||||||
playing from where it was, the controllers), and the script runs afresh, so a
|
state), the engine stays (the window, the GPU, the controllers), the disc is
|
||||||
52 MB model is back on screen in the time it takes to load it rather than the
|
parked on its first frame again as at startup, and the script runs afresh,
|
||||||
time it takes to start Singe. A key or button held through the reload is
|
so a 52 MB model is back on screen in the time it takes to load it rather
|
||||||
|
than the time it takes to start Singe. A key or button held through the reload is
|
||||||
ignored until it is released, as at a fresh start. `F5` reloads on demand,
|
ignored until it is released, as at a fresh start. `F5` reloads on demand,
|
||||||
and a script can ask for it with `singeReload()`. Packed games have nothing to watch; the option is for
|
and a script can ask for it with `singeReload()`. Packed games have nothing to watch; the option is for
|
||||||
a game in a directory. A script error on reload is printed and the game sits
|
a game in a directory. A script error on reload is printed and the game sits
|
||||||
empty until the next reload fixes it. Because the reload runs the same
|
empty until the next reload fixes it; the file stays watched, so saving the
|
||||||
|
fix is enough. Because the reload runs the same
|
||||||
teardown that quitting does, anything a script leaks shows up here first.
|
teardown that quitting does, anything a script leaks shows up here first.
|
||||||
|
|
||||||
=== Game Directory Layout
|
=== Game Directory Layout
|
||||||
|
|
@ -365,14 +372,24 @@ this order, and the first hit wins:
|
||||||
Inside a database, names are matched without regard to case, with either
|
Inside a database, names are matched without regard to case, with either
|
||||||
path separator, and relative to the game root. A leading component equal to
|
path separator, and relative to the game root. A leading component equal to
|
||||||
the game's own directory name is ignored, so `DLe/Cfg/game.cfg`,
|
the game's own directory name is ignored, so `DLe/Cfg/game.cfg`,
|
||||||
`Cfg/game.cfg`, and `DIR .. "Cfg/game.cfg"` all find the same file. Names
|
`Cfg/game.cfg`, and `DIR .. "Cfg/game.cfg"` all find the same file. The
|
||||||
|
name of the database itself (`DLe.game`, with or without a trailing
|
||||||
|
separator) is the game root, as `.` is from inside. Names
|
||||||
starting with `Singe/` or with the data directory stay on the filesystem;
|
starting with `Singe/` or with the data directory stay on the filesystem;
|
||||||
`Singe/Framework.singe` and `singeGetDataPath()` work as always. A relative
|
`Singe/Framework.singe` and `singeGetDataPath()` work as always, and such a
|
||||||
|
name may use `..` as long as it stays below that prefix (the `controls.cfg`
|
||||||
|
at the root of the data directory is found that way). Any other relative
|
||||||
name with a `..` component resolves nowhere inside a packed game: `io.open`
|
name with a `..` component resolves nowhere inside a packed game: `io.open`
|
||||||
returns `nil` with a message saying the name reaches outside the game,
|
returns `nil` with a message saying the name reaches outside the game,
|
||||||
`lfs.attributes` returns `nil`, and `spriteLoad` and its kind report the
|
`lfs.attributes` returns `nil`, and `spriteLoad` and its kind report the
|
||||||
file as missing. `lfs.dir` on a packed directory lists the union of the
|
file as missing. The game root is a directory, not a file: `io.open` on it
|
||||||
three places, sorted without regard to case.
|
returns `nil`, and `lfs.mkdir` and `lfs.rmdir` refuse it. `lfs.dir` on a
|
||||||
|
packed directory lists the union of the three places, sorted without regard
|
||||||
|
to case; when a loose file and a packed one differ only in case, the loose
|
||||||
|
spelling is listed. `lfs.attributes` on a packed file answers as the real
|
||||||
|
`lfs` does: the whole table with one argument, one field by name (an
|
||||||
|
unknown name raises an error), or a table of yours filled in; a missing
|
||||||
|
file returns `nil`, a message and the error number.
|
||||||
`require("name")` finds `name.lua`, `name/init.lua`, or `name.singe` under the
|
`require("name")` finds `name.lua`, `name/init.lua`, or `name.singe` under the
|
||||||
script's directory, then under the game root.
|
script's directory, then under the game root.
|
||||||
|
|
||||||
|
|
@ -401,9 +418,17 @@ change is one transaction: the game is intact if it is interrupted. A patch
|
||||||
may also carry a `removed(path)` table naming files to delete. A `.patch`
|
may also carry a `removed(path)` table naming files to delete. A `.patch`
|
||||||
can be unpacked like a game, but it cannot be run and the menu ignores it.
|
can be unpacked like a game, but it cannot be run and the menu ignores it.
|
||||||
|
|
||||||
A packed game keeps its data directory exactly where the loose game would
|
A packed game's data directory is named for the database and then for the
|
||||||
have it (`DLe/` for `DLe/DLe.singe`, or `data/DLe/` under `--datadir=data`), so saves and settings are shared
|
script's directory inside it: `DLe/` for a `DLe.game` whose script sits at
|
||||||
between a loose install and a packed one. The video index goes there too.
|
its root (or `data/DLe/` under `--datadir=data`), which is where the loose
|
||||||
|
`DLe/DLe.singe` has it, so saves and settings are shared between a loose
|
||||||
|
install and a packed one; a script packed in a subdirectory
|
||||||
|
(`Game/Main.singe`) gets `DLe/Game/`. The video index goes there too.
|
||||||
|
|
||||||
|
NOTE: Earlier 3.00 builds gave a packed game whose script sat in a
|
||||||
|
subdirectory the data directory `Game/` under the base, which every archive
|
||||||
|
with that layout shared. Such a game's saves now belong under `DLe/Game/`;
|
||||||
|
move them by hand.
|
||||||
When Singe runs a game from a database the menu passes `CONTAINER` in the
|
When Singe runs a game from a database the menu passes `CONTAINER` in the
|
||||||
`games.dat` entry it launches; the field is set by the menu, never by hand.
|
`games.dat` entry it launches; the field is set by the menu, never by hand.
|
||||||
|
|
||||||
|
|
@ -642,6 +667,7 @@ the engine's switches do nothing there (a `p` typed into a high score entry
|
||||||
must stay a `p`). Gamepad and mouse buttons mapped to those switches keep
|
must stay a `p`). Gamepad and mouse buttons mapped to those switches keep
|
||||||
working in either mode, since they cannot be typed.
|
working in either mode, since they cannot be typed.
|
||||||
|
|
||||||
|
[#gamesdat]
|
||||||
=== games.dat
|
=== games.dat
|
||||||
|
|
||||||
The `games.dat` file allows Singe to automatically locate new games when they
|
The `games.dat` file allows Singe to automatically locate new games when they
|
||||||
|
|
@ -691,6 +717,17 @@ without a disc and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see
|
||||||
with the old sprite argument order (see <<migrating,Migrating from Singe
|
with the old sprite argument order (see <<migrating,Migrating from Singe
|
||||||
2.10>>). The remaining keys are read by the menu for display.
|
2.10>>). The remaining keys are read by the menu for display.
|
||||||
|
|
||||||
|
A script launched directly, by name or by its directory, gets the same
|
||||||
|
treatment: Singe looks for a `games.dat` in the script's directory and up to
|
||||||
|
three directories above it, and if an entry's `SCRIPT` names the launched
|
||||||
|
script (entry paths count from the directory above the `games.dat`, so
|
||||||
|
`ActionMax/games.dat` names `ActionMax/38AmbushAlley.singe`), that entry's
|
||||||
|
keys apply. The one difference from a menu launch is precedence: an option
|
||||||
|
typed on the command line (`--framefile`, `--disc`, `--stretch`, `--nomouse`,
|
||||||
|
`--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`,
|
||||||
|
`--canvas`) keeps its value, since the person typing it meant it. A
|
||||||
|
`games.dat` that fails to run is reported and ignored for a direct launch.
|
||||||
|
|
||||||
[#withoutadisc]
|
[#withoutadisc]
|
||||||
=== Games Without a Disc
|
=== Games Without a Disc
|
||||||
|
|
||||||
|
|
@ -976,6 +1013,9 @@ A directional light's shadow is split into cascades along the view
|
||||||
of what the camera sees, so shadows stay sharp close up in a large level and
|
of what the camera sees, so shadows stay sharp close up in a large level and
|
||||||
still reach as far as `sceneSetShadowDistance`. One cascade fits a single
|
still reach as far as `sceneSetShadowDistance`. One cascade fits a single
|
||||||
map to the whole scene, which suits small scenes and orthographic cameras.
|
map to the whole scene, which suits small scenes and orthographic cameras.
|
||||||
|
A view (`viewNew`) draws with the same maps, taking for each point the
|
||||||
|
finest cascade that covers it, so a monitor shows the shadows the window
|
||||||
|
does.
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
|
|
@ -1394,7 +1434,12 @@ animations have posed their nodes and before the scene renders, so a
|
||||||
kinematic body driven by an animation pushes with the right velocity.
|
kinematic body driven by an animation pushes with the right velocity.
|
||||||
Pausing the game pauses physics; `physicsSetEnabled(false)` pauses it on
|
Pausing the game pauses physics; `physicsSetEnabled(false)` pauses it on
|
||||||
its own. `bodyIsResting` says when a body has settled, which is when a
|
its own. `bodyIsResting` says when a body has settled, which is when a
|
||||||
thrown thing has stopped and a score can be counted.
|
thrown thing has stopped and a score can be counted. A body, player,
|
||||||
|
ragdoll, soft body or vehicle whose node has been deleted is released at
|
||||||
|
the start of the next frame, enabled or not and paused or not, so its slot
|
||||||
|
is free again; a node handle that `nodeNew` later hands out afresh carries
|
||||||
|
none of the old node's physics, and `playerExists` and its kind answer
|
||||||
|
`false` for it until it gets its own.
|
||||||
|
|
||||||
==== Events and Queries
|
==== Events and Queries
|
||||||
|
|
||||||
|
|
@ -1917,6 +1962,9 @@ changed shape, but a few behaviors did:
|
||||||
* Errors inside callbacks now end the game with a traceback instead of being
|
* Errors inside callbacks now end the game with a traceback instead of being
|
||||||
printed and ignored, matching how argument errors have always behaved.
|
printed and ignored, matching how argument errors have always behaved.
|
||||||
* Held keys no longer repeat in `MODE_NORMAL`.
|
* Held keys no longer repeat in `MODE_NORMAL`.
|
||||||
|
* `math.randomseed` accepts a fractional seed, as it did under 2.10's Lua;
|
||||||
|
it is floored before Lua 5.4's generator sees it. `os.clock` is wall time
|
||||||
|
since the engine started, not processor time.
|
||||||
* The disc is parked on frame 1 by the engine before your script runs;
|
* The disc is parked on frame 1 by the engine before your script runs;
|
||||||
`Framework.singe` no longer seeks when it is loaded.
|
`Framework.singe` no longer seeks when it is loaded.
|
||||||
* `onMouseMoved` in `MANY_MOUSE` mode passes real movement in its third and
|
* `onMouseMoved` in `MANY_MOUSE` mode passes real movement in its third and
|
||||||
|
|
@ -2122,7 +2170,8 @@ numbered from `1` in the order `modelGetAnimations` lists them, or named as
|
||||||
the file names them. A clip plays on the base layer, and up to four more can
|
the file names them. A clip plays on the base layer, and up to four more can
|
||||||
play on layers `1` to `4` above it; the time, pause, resume and seek calls
|
play on layers `1` to `4` above it; the time, pause, resume and seek calls
|
||||||
address the base layer only. Time is in seconds, advances with the wall
|
address the base layer only. Time is in seconds, advances with the wall
|
||||||
clock, and holds while the game is paused. Node animation, skinning and
|
clock by at most a tenth of a second a frame, and holds while the game is
|
||||||
|
paused. Node animation, skinning and
|
||||||
morph target weights are all driven. See the Animation part of
|
morph target weights are all driven. See the Animation part of
|
||||||
<<scenes3d,3D Scenes>>.
|
<<scenes3d,3D Scenes>>.
|
||||||
|
|
||||||
|
|
@ -2831,7 +2880,7 @@ bodySetMass(anvil, 200)
|
||||||
bodySetTrigger(node, trigger)
|
bodySetTrigger(node, trigger)
|
||||||
----
|
----
|
||||||
|
|
||||||
Makes the body a trigger with `true`: it pushes nothing and stops nothing, but `onTrigger` reports each body or player that enters and leaves it. `false` makes it solid again. A trigger is usually a static box or sphere placed over a doorway, a finish line or a danger zone. Rays from `physicsRaycast` hit triggers like any other body.
|
Makes the body a trigger with `true`: it pushes nothing and stops nothing, but `onTrigger` reports each body or player that enters and leaves it. `false` makes it solid again: whatever was inside it is forgotten without a leave, while a trigger the body itself stands in goes on reporting it. A trigger is usually a static box or sphere placed over a doorway, a finish line or a danger zone. Rays from `physicsRaycast` hit triggers like any other body.
|
||||||
|
|
||||||
*Notes:* A player is reported inside a trigger when their bounding boxes overlap, so a round trigger is a square one for players.
|
*Notes:* A player is reported inside a trigger when their bounding boxes overlap, so a round trigger is a square one for players.
|
||||||
|
|
||||||
|
|
@ -6393,7 +6442,7 @@ root node scales the whole instance. See <<scenes3d,3D Scenes>>.
|
||||||
model = modelLoad(name)
|
model = modelLoad(name)
|
||||||
----
|
----
|
||||||
|
|
||||||
Reads a `.glb` through the same lookup as every other asset (game directory, data directory, packed database), uploads its meshes and materials to the GPU once, and keeps its node tree, skins and animations for instancing. Call it once at startup and instance the handle as many times as needed; loading the same file twice makes two copies on the GPU. It needs the 3D scene, so on a machine without a suitable GPU it fails like any other 3D call. A bad file ends the script with a message naming the problem: the file could not be read, is not glTF, refers to an external buffer or image, fails validation, holds a primitive that could not be uploaded, has a skin with more than `128` joints, or has a skinned mesh naming a joint its skin does not have. A texture that cannot be decoded, or a missing KTX2 image, does not fail the load; the material goes without that map and the program trace (`--program`) says which one.
|
Reads a `.glb` through the same lookup as every other asset (game directory, data directory, packed database), uploads its meshes and materials to the GPU once, and keeps its node tree, skins and animations for instancing. Call it once at startup and instance the handle as many times as needed; loading the same file twice makes two copies on the GPU. It needs the 3D scene, so on a machine without a suitable GPU it fails like any other 3D call. A bad file ends the script with a message naming the problem: the file could not be read, is not glTF, refers to an external buffer or image, fails validation, holds a primitive that could not be uploaded, has a skin with more than `128` joints, or has a skinned mesh naming a joint numbered `128` or more. A skinned mesh naming a joint beyond the skins that use it loads, as does an animation whose keyframes cannot be read (that sampler is skipped); either is noted in the program trace. A texture that cannot be decoded, or a missing KTX2 image, does not fail the load; the material goes without that map and the program trace (`--program`) says which one.
|
||||||
|
|
||||||
*Returns:* The model handle, an integer.
|
*Returns:* The model handle, an integer.
|
||||||
|
|
||||||
|
|
@ -6873,7 +6922,7 @@ end
|
||||||
agent = navAgentNew(nav, node, radius, height, speed)
|
agent = navAgentNew(nav, node, radius, height, speed)
|
||||||
----
|
----
|
||||||
|
|
||||||
Puts an agent of that size on the mesh at the point nearest the node's world position (the raw position when nothing is within 2 units sideways and 4 up or down), walking at `speed` units a second and accelerating at 8 units a second squared. From then on the engine moves the node along the mesh every frame, in its parent's frame, and turns it about Y to face the way it is going whenever it moves faster than `0.05` units a second; nodes look down -Z, as models do. The crowd steers agents round each other, anticipates turns and keeps them apart. To drive a player controller on the node instead of placing the node, see `navAgentSetPlayer`. Raises an error when the mesh is not built or 128 agents are already on it.
|
Puts an agent of that size on the mesh at the point nearest the node's world position (the raw position when nothing is within 2 units sideways and 4 up or down), walking at `speed` units a second and accelerating at 8 units a second squared. The radius and height are kept at `0.01` or more, as `navNew` keeps its own. From then on the engine moves the node along the mesh every frame, in its parent's frame, and turns it about Y to face the way it is going whenever it moves faster than `0.05` units a second; nodes look down -Z, as models do. The crowd steers agents round each other, anticipates turns and keeps them apart. To drive a player controller on the node instead of placing the node, see `navAgentSetPlayer`. Raises an error when the mesh is not built or 128 agents are already on it.
|
||||||
|
|
||||||
*Parameters:*
|
*Parameters:*
|
||||||
|
|
||||||
|
|
@ -7058,7 +7107,7 @@ end
|
||||||
nav = navLoad(name, agentRadius, agentHeight)
|
nav = navLoad(name, agentRadius, agentHeight)
|
||||||
----
|
----
|
||||||
|
|
||||||
Loads a mesh `navSave` wrote, looked for among the game's files first (so a baked mesh can ship with the game, packed or not) and then in the game's data folder. The mesh comes back built, ready for agents and queries, and takes no more `navAddNode`. The slope and step it was baked with are in the file; the agent radius and height are what the crowd steers with, so give the values it was baked for. Raises an error when the file cannot be read, is not a navigation mesh, or no mesh slot is free.
|
Loads a mesh `navSave` wrote, looked for among the game's files first (so a baked mesh can ship with the game, packed or not) and then in the game's data folder. The mesh comes back built, ready for agents and queries, and takes no more `navAddNode`. The slope and step it was baked with are in the file; the agent radius and height are what the crowd steers with, so give the values it was baked for. Raises an error when the file cannot be read, is not a navigation mesh, is shorter than the mesh its header describes (truncated, or edited by hand), or no mesh slot is free.
|
||||||
|
|
||||||
*Parameters:*
|
*Parameters:*
|
||||||
|
|
||||||
|
|
@ -9655,7 +9704,7 @@ end
|
||||||
x, y, depth, inFront = sceneProject(wx, wy, wz)
|
x, y, depth, inFront = sceneProject(wx, wy, wz)
|
||||||
----
|
----
|
||||||
|
|
||||||
Where a world point falls in overlay coordinates, using the camera of the last rendered frame. `depth` is `0` at the near plane and `1` at the far plane, for sorting labels or fading them with distance. `inFront` is `false` when the point is behind the camera, and `x` and `y` are then meaningless. Points in front but outside the view come back with coordinates off the overlay's edges, so check them against `sceneGetSize` before drawing. The usual use is a name tag or health bar drawn with `fontPrint` or `spriteDraw` above a 3D object.
|
Where a world point falls in overlay coordinates, using the camera of the last rendered frame. `depth` is `0` at the near plane and `1` at the far plane, for sorting labels or fading them with distance. `inFront` is `false` when the point is behind the camera, and `x` and `y` are then meaningless; before the first frame has rendered there is nothing to project against, and every value comes back `0` with `inFront` `false`. Points in front but outside the view come back with coordinates off the overlay's edges, so check them against `sceneGetSize` before drawing. The usual use is a name tag or health bar drawn with `fontPrint` or `spriteDraw` above a 3D object.
|
||||||
|
|
||||||
*Parameters:*
|
*Parameters:*
|
||||||
|
|
||||||
|
|
@ -10305,7 +10354,7 @@ end
|
||||||
path = singeGetDataPath()
|
path = singeGetDataPath()
|
||||||
----
|
----
|
||||||
|
|
||||||
Returns the directory Singe writes to for this game, with a trailing path separator so a file name can be appended directly. With `--datadir`, or for a packed game, it is a subdirectory of that base named for the game; otherwise it is the game's own directory. Screenshots, video indexes and `trace.txt` go there, and so should save games, high scores and settings.
|
Returns the directory Singe writes to for this game, with a trailing path separator so a file name can be appended directly. With `--datadir`, or for a packed game, it is a subdirectory of that base named for the game (for a packed game, the database's name and then the script's directory inside it); otherwise it is the game's own directory. Screenshots, video indexes and `trace.txt` go there, and so should save games, high scores and settings.
|
||||||
|
|
||||||
*Returns:* string path ending in a separator.
|
*Returns:* string path ending in a separator.
|
||||||
|
|
||||||
|
|
@ -10492,7 +10541,7 @@ end
|
||||||
singeReload()
|
singeReload()
|
||||||
----
|
----
|
||||||
|
|
||||||
Runs the game again from its script at the end of this frame, as `F5` and a changed file do under `--reload`; this call works without the option. Every sound stops, the Lua state is discarded without calling `onShutdown`, everything the script loaded is freed, the scene, physics, particles and navigation are reset, the overlay returns to its default resolution, and the keyboard mode, pause key, sound listener and pause flag return to their defaults. Keys and buttons held through the reload are ignored until released, as at a fresh start. The disc and the engine itself stay as they are. It suits a debug menu's restart item or a level editor reloading what it just saved.
|
Runs the game again from its script at the end of this frame, as `F5` and a changed file do under `--reload`; this call works without the option. Every sound stops, the Lua state is discarded without calling `onShutdown`, everything the script loaded is freed, the scene, physics (its debug drawing included), particles and navigation are reset, the overlay returns to its default resolution, and the sound effect volume, mouse mode, mouse capture, keyboard mode, pause key, sound listener and pause flag return to their defaults. Keys and buttons held through the reload are ignored until released, as at a fresh start, and the disc is parked on its first frame, paused, with the default audio track selected, as at startup. The engine itself (the window, the controllers, the mice) stays as it is. It suits a debug menu's restart item or a level editor reloading what it just saved.
|
||||||
|
|
||||||
*Since:* 3.00.
|
*Since:* 3.00.
|
||||||
*See also:* <<singequit,singeQuit>>
|
*See also:* <<singequit,singeQuit>>
|
||||||
|
|
@ -10755,7 +10804,7 @@ softNew(node, SOFT_BODY)
|
||||||
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
|
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
|
||||||
----
|
----
|
||||||
|
|
||||||
For `SOFT_CLOTH` and `SOFT_BODY` the node's mesh, of three vertices or more, becomes the particles: vertices at the same position (within `0.0001` units) are welded into one, placed where the node's transform puts them in the world, and the triangles give the stretch, shear and bend constraints. Cloth collides on both faces and is what an open sheet wants; a body is a closed mesh that `softSetPressure` can inflate. For `SOFT_ROPE` the node needs no mesh: `segments + 1` particles run in a line from the node's world position to the world point, the node is given a new six-sided tube mesh of that radius round them (keeping whatever material it had), and the particles collide as spheres of that radius, where cloth and body particles are spheres of radius `0.03`. Every kind starts with stretch `0.9`, bend `0.2`, mass `1` kilogram, damping `0.1` and pressure `0`, and a soft body already on the node is replaced. Raises an error when physics is unavailable, the kind is unknown, the argument count does not fit the kind, the mesh is missing, a rope has no segments or no radius, or 16 soft bodies already exist.
|
For `SOFT_CLOTH` and `SOFT_BODY` the node's mesh, of three vertices or more, becomes the particles: vertices at the same position (within `0.0001` units) are welded into one, placed where the node's transform puts them in the world, and the triangles give the stretch, shear and bend constraints. Cloth collides on both faces and is what an open sheet wants; a body is a closed mesh that `softSetPressure` can inflate. For `SOFT_ROPE` the node needs no mesh: `segments + 1` particles run in a line from the node's world position to the world point, the node is given a new six-sided tube mesh of that radius round them (keeping whatever material it had), and the particles collide as spheres of that radius, where cloth and body particles are spheres of radius `0.03`. Every kind starts with stretch `0.9`, bend `0.2`, mass `1` kilogram, damping `0.1` and pressure `0`. A soft body already on the node is removed before the new one is made, so a call that fails leaves the node with none; a rope's tube mesh goes with it, which is why a rope's node cannot become cloth or a body (the call fails for want of a mesh). Raises an error when physics is unavailable, the kind is unknown, the argument count does not fit the kind, the mesh is missing, a rope has no segments or no radius, or 16 soft bodies already exist.
|
||||||
|
|
||||||
*Parameters:*
|
*Parameters:*
|
||||||
|
|
||||||
|
|
@ -11611,7 +11660,7 @@ end
|
||||||
result = spriteLoad(filename)
|
result = spriteLoad(filename)
|
||||||
----
|
----
|
||||||
|
|
||||||
Loads an image and returns an integer handle for the other `sprite*` calls. The name goes through the engine's virtual file system, so it may live loose on disk or inside a packed game; it is resolved relative to the directory Singe was started in, so prepend `DIR` for files shipped with your game. An animated GIF or WEBP with two or more frames loads as an animation, parked on frame `0`, not playing and not looping; every other file, including a single-frame GIF, loads as a still image. Pixels with a raw value of `0` in the file's own pixel format become transparent. Loading is synchronous, and a file that cannot be opened or decoded ends the script with the loader's error message rather than returning `nil`.
|
Loads an image and returns an integer handle for the other `sprite*` calls. The name goes through the engine's virtual file system, so it may live loose on disk or inside a packed game; it is resolved relative to the directory Singe was started in, so prepend `DIR` for files shipped with your game. An animated GIF or WEBP with two or more frames loads as an animation, parked on frame `0`, not playing and not looping; every other file, including a single-frame GIF, loads as a still image. An image with more than 8 bits per channel, or floating point pixels, is converted to 8-bit RGBA on load and treated as one from then on. Pixels with a raw value of `0` in the file's own pixel format become transparent. Loading is synchronous, and a file that cannot be opened or decoded ends the script with the loader's error message rather than returning `nil`.
|
||||||
|
|
||||||
*Returns:* integer sprite handle.
|
*Returns:* integer sprite handle.
|
||||||
|
|
||||||
|
|
@ -12542,7 +12591,7 @@ end
|
||||||
[#view]
|
[#view]
|
||||||
=== View
|
=== View
|
||||||
|
|
||||||
A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows and no bloom, so keep them few and small. Every view turns billboards (`nodeSetBillboard`) to its own camera and sorts blended draws back to front from it, so a view whose camera looks from elsewhere still sees billboards face on and transparent objects in the right order. A bad handle raises an error. See <<scenes3d,3D Scenes>>.
|
A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows (a directional light's cascades included: each point takes the finest cascade that covers it) and no bloom, so keep them few and small. Every view turns billboards (`nodeSetBillboard`) to its own camera and sorts blended draws back to front from it, so a view whose camera looks from elsewhere still sees billboards face on and transparent objects in the right order. A bad handle raises an error. See <<scenes3d,3D Scenes>>.
|
||||||
|
|
||||||
[#viewdelete]
|
[#viewdelete]
|
||||||
==== viewDelete
|
==== viewDelete
|
||||||
|
|
|
||||||
40
src/main.c
40
src/main.c
|
|
@ -109,11 +109,10 @@ typedef struct EmbeddedFileS {
|
||||||
static QueueT *_scriptQueue = NULL;
|
static QueueT *_scriptQueue = NULL;
|
||||||
|
|
||||||
// Single source of truth for the command line: feeds both the parser and the usage text.
|
// Single source of truth for the command line: feeds both the parser and the usage text.
|
||||||
// The overscan and Sinden options work but are hidden until the Sinden border scales mouse input.
|
|
||||||
static const OptionT _options[] = {
|
static const OptionT _options[] = {
|
||||||
{ 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false },
|
{ 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false },
|
||||||
{ 'A', "audiodelay", ap_yes, "MS", "compensate for audio heard MS milliseconds late (negative if early)", false },
|
{ 'A', "audiodelay", ap_yes, "MS", "compensate for audio heard MS milliseconds late (negative if early)", false },
|
||||||
{ 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", true },
|
{ 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", false },
|
||||||
{ 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false },
|
{ 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false },
|
||||||
{ 'C', "canvas", ap_yes, "WxH", "world size for games without a disc (default 720x480)", false },
|
{ 'C', "canvas", ap_yes, "WxH", "world size for games without a disc (default 720x480)", false },
|
||||||
{ 'D', "disc", ap_no, NULL, "play a laserdisc video (implied by --framefile)", false },
|
{ 'D', "disc", ap_no, NULL, "play a laserdisc video (implied by --framefile)", false },
|
||||||
|
|
@ -121,7 +120,7 @@ static const OptionT _options[] = {
|
||||||
{ 'E', "entry", ap_yes, "N", "run the Nth games.dat entry of a .game file (default 1)", false },
|
{ 'E', "entry", ap_yes, "N", "run the Nth games.dat entry of a .game file (default 1)", false },
|
||||||
{ 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false },
|
{ 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false },
|
||||||
{ 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false },
|
{ 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false },
|
||||||
{ 'g', "sindengun", ap_yes, "'PARAMS'", "enable Sinden Light Gun support", true },
|
{ 'g', "sindengun", ap_yes, "'PARAMS'", "enable Sinden Light Gun support", false },
|
||||||
{ 'H', "softwarevideo", ap_no, NULL, "decode video in software even when a hardware decoder exists", false },
|
{ 'H', "softwarevideo", ap_no, NULL, "decode video in software even when a hardware decoder exists", false },
|
||||||
{ 'h', "help", ap_no, NULL, "this display", false },
|
{ 'h', "help", ap_no, NULL, "this display", false },
|
||||||
{ 'k', "nologos", ap_no, NULL, "kill the splash screens", false },
|
{ 'k', "nologos", ap_no, NULL, "kill the splash screens", false },
|
||||||
|
|
@ -570,12 +569,14 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
|
|
||||||
// Canvas size
|
// Canvas size
|
||||||
case 'C':
|
case 'C':
|
||||||
|
conf->given |= GIVEN_CANVAS;
|
||||||
free(canvasString);
|
free(canvasString);
|
||||||
canvasString = strdup(arg);
|
canvasString = strdup(arg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Laserdisc
|
// Laserdisc
|
||||||
case 'D':
|
case 'D':
|
||||||
|
conf->given |= GIVEN_VIDEO;
|
||||||
conf->disc = true;
|
conf->disc = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -597,11 +598,13 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
|
|
||||||
// Audio Delay
|
// Audio Delay
|
||||||
case 'A':
|
case 'A':
|
||||||
|
conf->given |= GIVEN_AUDIO_DELAY;
|
||||||
target = &conf->audioDelayMs;
|
target = &conf->audioDelayMs;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Sinden Light Gun
|
// Sinden Light Gun
|
||||||
case 'g':
|
case 'g':
|
||||||
|
conf->given |= GIVEN_SINDEN;
|
||||||
free(sindenString);
|
free(sindenString);
|
||||||
sindenString = strdup(arg);
|
sindenString = strdup(arg);
|
||||||
break;
|
break;
|
||||||
|
|
@ -644,6 +647,7 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
|
|
||||||
// No Mouse
|
// No Mouse
|
||||||
case 'm':
|
case 'm':
|
||||||
|
conf->given |= GIVEN_NO_MOUSE;
|
||||||
conf->noMouse = true;
|
conf->noMouse = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -654,6 +658,7 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
|
|
||||||
// Audio Track Output
|
// Audio Track Output
|
||||||
case 'o':
|
case 'o':
|
||||||
|
conf->given |= GIVEN_AUDIO_TRACK;
|
||||||
target = &conf->audioOutputTrack;
|
target = &conf->audioOutputTrack;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -678,11 +683,13 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
|
|
||||||
// Ugly Stretched Video
|
// Ugly Stretched Video
|
||||||
case 'u':
|
case 'u':
|
||||||
|
conf->given |= GIVEN_STRETCH;
|
||||||
conf->stretchVideo = true;
|
conf->stretchVideo = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Video File - a video means a disc.
|
// Video File - a video means a disc.
|
||||||
case 'v':
|
case 'v':
|
||||||
|
conf->given |= GIVEN_VIDEO;
|
||||||
free(conf->videoFile);
|
free(conf->videoFile);
|
||||||
conf->videoFile = strdup(arg);
|
conf->videoFile = strdup(arg);
|
||||||
conf->disc = true;
|
conf->disc = true;
|
||||||
|
|
@ -695,12 +702,14 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
|
||||||
|
|
||||||
// X Resolution
|
// X Resolution
|
||||||
case 'x':
|
case 'x':
|
||||||
|
conf->given |= GIVEN_RESOLUTION;
|
||||||
target = &conf->xResolution;
|
target = &conf->xResolution;
|
||||||
conf->resolutionWasCalculated = false;
|
conf->resolutionWasCalculated = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Y Resolution
|
// Y Resolution
|
||||||
case 'y':
|
case 'y':
|
||||||
|
conf->given |= GIVEN_RESOLUTION;
|
||||||
target = &conf->yResolution;
|
target = &conf->yResolution;
|
||||||
conf->resolutionWasCalculated = false;
|
conf->resolutionWasCalculated = false;
|
||||||
break;
|
break;
|
||||||
|
|
@ -820,6 +829,8 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) {
|
||||||
size_t length = 0;
|
size_t length = 0;
|
||||||
const char *extension = NULL;
|
const char *extension = NULL;
|
||||||
char *temp = NULL;
|
char *temp = NULL;
|
||||||
|
ConfigT *replacement = NULL;
|
||||||
|
ConfigT swapped;
|
||||||
|
|
||||||
// Exists? A packed game answers through its database.
|
// Exists? A packed game answers through its database.
|
||||||
vfsInit(conf->container, conf->dataDirBase, conf->dataDir);
|
vfsInit(conf->container, conf->dataDirBase, conf->dataDir);
|
||||||
|
|
@ -842,6 +853,17 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) {
|
||||||
_showUsage(exeName, "Unable to locate the game.");
|
_showUsage(exeName, "Unable to locate the game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A loose script runs with the settings of its games.dat entry, as the menu would run it.
|
||||||
|
if (conf->container == NULL) {
|
||||||
|
replacement = confFromGamesDat(conf);
|
||||||
|
if (replacement != NULL) {
|
||||||
|
swapped = *conf;
|
||||||
|
*conf = *replacement;
|
||||||
|
*replacement = swapped;
|
||||||
|
destroyConf(&replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Do we need to generate a video name?
|
// Do we need to generate a video name?
|
||||||
if (conf->videoFile) {
|
if (conf->videoFile) {
|
||||||
utilFixPathSeparators(&conf->videoFile, false);
|
utilFixPathSeparators(&conf->videoFile, false);
|
||||||
|
|
@ -1111,22 +1133,24 @@ char *createDataDir(const char *dataDirBase, const char *filename) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The data directory follows the script's directory, or the database's name when the script sits at its root.
|
// The data directory follows the script's directory; inside a database it sits under the database's
|
||||||
|
// name as well, so two archives holding the same script path never share one. vfs.c's overlay for
|
||||||
|
// a packed game hangs off this directory, so the two cannot disagree.
|
||||||
char *createDataDirFor(const ConfigT *conf) {
|
char *createDataDirFor(const ConfigT *conf) {
|
||||||
char *stem = NULL;
|
char *stem = NULL;
|
||||||
char *name = NULL;
|
char *name = NULL;
|
||||||
char *path = NULL;
|
char *path = NULL;
|
||||||
|
|
||||||
if ((conf->container != NULL) && (strchr(conf->scriptFile, '/') == NULL) && (strchr(conf->scriptFile, '\\') == NULL)) {
|
if (conf->container == NULL) {
|
||||||
|
return createDataDir(conf->dataDirBase, conf->scriptFile);
|
||||||
|
}
|
||||||
stem = vfsDatabaseStem(utilGetLastPathComponent(conf->container));
|
stem = vfsDatabaseStem(utilGetLastPathComponent(conf->container));
|
||||||
name = utilCreateString("%s%c%s", stem, utilGetPathSeparator(), conf->scriptFile);
|
name = utilCreateString("%s%c%s", stem, utilGetPathSeparator(), conf->scriptFile);
|
||||||
path = createDataDir(conf->dataDirBase, name);
|
path = createDataDir(conf->dataDirBase, name);
|
||||||
free(name);
|
free(name);
|
||||||
free(stem);
|
free(stem);
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createDataDir(conf->dataDirBase, conf->scriptFile);
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
288
src/model.c
288
src/model.c
|
|
@ -52,12 +52,16 @@
|
||||||
#define POSE_MORPH 8
|
#define POSE_MORPH 8
|
||||||
#define BASE_LAYER 0
|
#define BASE_LAYER 0
|
||||||
#define CUBIC_SPLINE_STRIDE 3 // Output elements per keyframe: in-tangent, value, out-tangent
|
#define CUBIC_SPLINE_STRIDE 3 // Output elements per keyframe: in-tangent, value, out-tangent
|
||||||
#define CUBIC_SPLINE_VALUE 1 // Which of the three is the value
|
#define CUBIC_SPLINE_IN 0 // Which of the three is which
|
||||||
|
#define CUBIC_SPLINE_VALUE 1
|
||||||
|
#define CUBIC_SPLINE_OUT 2
|
||||||
|
#define QUATERNION_W 3 // Index of w in a four-float rotation
|
||||||
#define MIN_TRIANGLE_INDICES 3
|
#define MIN_TRIANGLE_INDICES 3
|
||||||
#define BASE64_QUAD_CHARS 4 // Four base64 characters ...
|
#define BASE64_QUAD_CHARS 4 // Four base64 characters ...
|
||||||
#define BASE64_QUAD_BYTES 3 // ... hold three bytes
|
#define BASE64_QUAD_BYTES 3 // ... hold three bytes
|
||||||
#define DATA_URI_PREFIX "data:"
|
#define DATA_URI_PREFIX "data:"
|
||||||
#define SLOTS_MIN 8 // Models or instances allocated at once
|
#define SLOTS_MIN 8 // Models or instances allocated at once
|
||||||
|
#define MAX_STEP_SECONDS 0.1 // The most one update advances a clip; a stall does not skip it ahead
|
||||||
|
|
||||||
|
|
||||||
typedef struct PrimitiveS {
|
typedef struct PrimitiveS {
|
||||||
|
|
@ -70,13 +74,29 @@ typedef struct ModelMeshS {
|
||||||
int32_t count;
|
int32_t count;
|
||||||
} ModelMeshT;
|
} ModelMeshT;
|
||||||
|
|
||||||
|
// One animation sampler's keyframes, unpacked to plain floats once at load (which applies sparse
|
||||||
|
// accessors, something cgltf's per-element reader refuses), so sampling never touches cgltf.
|
||||||
|
// Empty (keyCount 0) when the file's accessors could not be read.
|
||||||
|
typedef struct SamplerS {
|
||||||
|
float *times; // keyCount seconds, ascending
|
||||||
|
float *values; // valueCount elements of components floats each
|
||||||
|
int32_t keyCount;
|
||||||
|
int32_t valueCount;
|
||||||
|
int32_t components;
|
||||||
|
} SamplerT;
|
||||||
|
|
||||||
|
typedef struct AnimationS {
|
||||||
|
SamplerT *samplers; // Per glTF sampler
|
||||||
|
double length; // The last keyframe time over every channel
|
||||||
|
} AnimationT;
|
||||||
|
|
||||||
// A model template; used comes first so _allocSlot can find free slots.
|
// A model template; used comes first so _allocSlot can find free slots.
|
||||||
typedef struct ModelS {
|
typedef struct ModelS {
|
||||||
bool used;
|
bool used;
|
||||||
cgltf_data *data;
|
cgltf_data *data;
|
||||||
int32_t *materials; // Scene material per glTF material, NO_HANDLE when it failed
|
int32_t *materials; // Scene material per glTF material, NO_HANDLE when it failed
|
||||||
ModelMeshT *meshes; // Per glTF mesh
|
ModelMeshT *meshes; // Per glTF mesh
|
||||||
double *animationLengths; // Per animation: its last keyframe time
|
AnimationT *animations; // Per glTF animation
|
||||||
int32_t morphStride; // The most morph targets any of its primitives has
|
int32_t morphStride; // The most morph targets any of its primitives has
|
||||||
} ModelT;
|
} ModelT;
|
||||||
|
|
||||||
|
|
@ -149,7 +169,6 @@ typedef struct ImageCacheS {
|
||||||
static void _advanceClip(const InstanceT *instance, ClipT *clip, double delta);
|
static void _advanceClip(const InstanceT *instance, ClipT *clip, double delta);
|
||||||
static PoseT *_allocPose(const InstanceT *instance);
|
static PoseT *_allocPose(const InstanceT *instance);
|
||||||
static int32_t _allocSlot(void **items, int32_t *count, int32_t *capacity, size_t size);
|
static int32_t _allocSlot(void **items, int32_t *count, int32_t *capacity, size_t size);
|
||||||
static double _animationLength(const cgltf_animation *animation);
|
|
||||||
static void _applyTexture(int32_t handle, ImageCacheT *cache, const cgltf_texture_view *view, MaterialMapE map, float strength);
|
static void _applyTexture(int32_t handle, ImageCacheT *cache, const cgltf_texture_view *view, MaterialMapE map, float strength);
|
||||||
static void _attachSkins(InstanceT *instance);
|
static void _attachSkins(InstanceT *instance);
|
||||||
static void _blendPose(const InstanceT *instance, PoseT *dst, const PoseT *src, float t, int32_t maskNode);
|
static void _blendPose(const InstanceT *instance, PoseT *dst, const PoseT *src, float t, int32_t maskNode);
|
||||||
|
|
@ -160,7 +179,7 @@ static void _copyPose(const InstanceT *instance, PoseT *dst, const PoseT
|
||||||
static void _evaluate(InstanceT *instance);
|
static void _evaluate(InstanceT *instance);
|
||||||
static void _fail(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
static void _fail(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
static InstanceT *_findInstance(int32_t root);
|
static InstanceT *_findInstance(int32_t root);
|
||||||
static void _findKeys(const cgltf_accessor *input, double time, int32_t *key, int32_t *next, float *t, float *span);
|
static void _findKeys(const SamplerT *sampler, double time, int32_t *key, int32_t *next, float *t, float *span);
|
||||||
static void _freeImageCache(ImageCacheT *cache, int32_t count);
|
static void _freeImageCache(ImageCacheT *cache, int32_t count);
|
||||||
static void _freeInstance(InstanceT *instance);
|
static void _freeInstance(InstanceT *instance);
|
||||||
static void _freeModel(ModelT *model);
|
static void _freeModel(ModelT *model);
|
||||||
|
|
@ -169,18 +188,20 @@ static bool _imageBytes(const cgltf_image *image, const uint8_t **bytes,
|
||||||
static bool _inMask(const cgltf_data *data, int32_t index, int32_t maskNode);
|
static bool _inMask(const cgltf_data *data, int32_t index, int32_t maskNode);
|
||||||
static int32_t _instanceNode(const InstanceT *instance, int32_t index);
|
static int32_t _instanceNode(const InstanceT *instance, int32_t index);
|
||||||
static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out);
|
static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out);
|
||||||
|
static void _loadAnimations(ModelT *model);
|
||||||
static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material);
|
static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material);
|
||||||
static bool _loadMeshes(ModelT *model);
|
static bool _loadMeshes(ModelT *model);
|
||||||
static void _loadMorphs(int32_t mesh, const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t vertexCount);
|
static void _loadMorphs(int32_t mesh, const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t vertexCount);
|
||||||
static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t jointLimit);
|
static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t jointCount);
|
||||||
static int32_t _meshNode(const InstanceT *instance, int32_t index, int32_t which);
|
static int32_t _meshNode(const InstanceT *instance, int32_t index, int32_t which);
|
||||||
static int32_t _meshNodeCount(const InstanceT *instance, int32_t index);
|
static int32_t _meshNodeCount(const InstanceT *instance, int32_t index);
|
||||||
static int32_t _nodeIndex(const InstanceT *instance, int32_t node);
|
static int32_t _nodeIndex(const InstanceT *instance, int32_t node);
|
||||||
static float *_poseMorph(const InstanceT *instance, const PoseT *pose, int32_t index);
|
static float *_poseMorph(const InstanceT *instance, const PoseT *pose, int32_t index);
|
||||||
static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count);
|
static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count);
|
||||||
static void _sampleChannel(const cgltf_animation_channel *channel, double time, float *out, int32_t components);
|
static void _sampleChannel(const SamplerT *sampler, cgltf_interpolation_type interpolation, double time, float *out, int32_t components);
|
||||||
static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pose);
|
static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pose);
|
||||||
static void _sampleWeights(const cgltf_animation_channel *channel, double time, float *out, int32_t count);
|
static void _samplerValue(const SamplerT *sampler, int32_t element, float *out, int32_t components);
|
||||||
|
static void _sampleWeights(const SamplerT *sampler, cgltf_interpolation_type interpolation, double time, float *out, int32_t count);
|
||||||
static void _snapshotPose(const InstanceT *instance, PoseT *pose);
|
static void _snapshotPose(const InstanceT *instance, PoseT *pose);
|
||||||
static bool _startClip(InstanceT *instance, LayerT *layer, int32_t index, bool loop, float speed, float fade);
|
static bool _startClip(InstanceT *instance, LayerT *layer, int32_t index, bool loop, float speed, float fade);
|
||||||
static void _warn(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
static void _warn(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
|
|
@ -206,7 +227,7 @@ static void _advanceClip(const InstanceT *instance, ClipT *clip, double delta) {
|
||||||
if ((clip->animation == NO_HANDLE) || !clip->playing) {
|
if ((clip->animation == NO_HANDLE) || !clip->playing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
length = _models[instance->model].animationLengths[clip->animation];
|
length = _models[instance->model].animations[clip->animation].length;
|
||||||
clip->time += delta * clip->speed;
|
clip->time += delta * clip->speed;
|
||||||
if (clip->time > length) {
|
if (clip->time > length) {
|
||||||
if (clip->loop && (length > 0.0)) {
|
if (clip->loop && (length > 0.0)) {
|
||||||
|
|
@ -258,23 +279,6 @@ static int32_t _allocSlot(void **items, int32_t *count, int32_t *capacity, size_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The last keyframe time over every channel.
|
|
||||||
static double _animationLength(const cgltf_animation *animation) {
|
|
||||||
double length = 0.0;
|
|
||||||
int32_t x;
|
|
||||||
|
|
||||||
for (x = 0; x < (int32_t)animation->channels_count; x++) {
|
|
||||||
const cgltf_accessor *input = animation->channels[x].sampler->input;
|
|
||||||
float last = 0.0f;
|
|
||||||
|
|
||||||
if ((input->count > 0) && cgltf_accessor_read_float(input, input->count - 1, &last, 1) && (last > length)) {
|
|
||||||
length = last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// A material's texture from a glTF texture view: a KTX2 (KHR_texture_basisu) image transcoded to
|
// A material's texture from a glTF texture view: a KTX2 (KHR_texture_basisu) image transcoded to
|
||||||
// the GPU's block format, or an ordinary image decoded to a surface.
|
// the GPU's block format, or an ordinary image decoded to a surface.
|
||||||
static void _applyTexture(int32_t handle, ImageCacheT *cache, const cgltf_texture_view *view, MaterialMapE map, float strength) {
|
static void _applyTexture(int32_t handle, ImageCacheT *cache, const cgltf_texture_view *view, MaterialMapE map, float strength) {
|
||||||
|
|
@ -318,12 +322,12 @@ static void _attachSkins(InstanceT *instance) {
|
||||||
|
|
||||||
for (x = 0; x < instance->nodeCount; x++) {
|
for (x = 0; x < instance->nodeCount; x++) {
|
||||||
const cgltf_skin *skin = data->nodes[x].skin;
|
const cgltf_skin *skin = data->nodes[x].skin;
|
||||||
|
int32_t meshNodes = _meshNodeCount(instance, x);
|
||||||
int32_t *joints;
|
int32_t *joints;
|
||||||
Mat4T *inverseBind;
|
Mat4T *inverseBind;
|
||||||
int32_t count;
|
int32_t count;
|
||||||
int32_t meshNodes = _meshNodeCount(instance, x);
|
|
||||||
|
|
||||||
if ((skin == NULL) || (meshNodes == 0)) {
|
if ((skin == NULL) || (meshNodes == 0) || (_instanceNode(instance, x) == NO_HANDLE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
count = (int32_t)skin->joints_count; // At most MAX_JOINTS; modelLoad refuses more
|
count = (int32_t)skin->joints_count; // At most MAX_JOINTS; modelLoad refuses more
|
||||||
|
|
@ -333,7 +337,7 @@ static void _attachSkins(InstanceT *instance) {
|
||||||
utilDie("Out of memory attaching a skin.");
|
utilDie("Out of memory attaching a skin.");
|
||||||
}
|
}
|
||||||
for (y = 0; y < count; y++) {
|
for (y = 0; y < count; y++) {
|
||||||
joints[y] = instance->nodes[skin->joints[y] - data->nodes];
|
joints[y] = _instanceNode(instance, (int32_t)(skin->joints[y] - data->nodes));
|
||||||
inverseBind[y] = mat4Identity();
|
inverseBind[y] = mat4Identity();
|
||||||
if (skin->inverse_bind_matrices != NULL) {
|
if (skin->inverse_bind_matrices != NULL) {
|
||||||
cgltf_accessor_read_float(skin->inverse_bind_matrices, (cgltf_size)y, inverseBind[y].m, 16);
|
cgltf_accessor_read_float(skin->inverse_bind_matrices, (cgltf_size)y, inverseBind[y].m, 16);
|
||||||
|
|
@ -571,30 +575,28 @@ static InstanceT *_findInstance(int32_t root) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The keyframe pair around a time, by binary search over the sampler's input: the last key at or
|
// The keyframe pair around a time, by binary search over the sampler's times: the last key at or
|
||||||
// before it (the first when the time is before them all), the key after (the same one at the end),
|
// before it (the first when the time is before them all), the key after (the same one at the end),
|
||||||
// how far between them (0..1) and the seconds between them. The input must have at least one key.
|
// how far between them (0..1) and the seconds between them. The sampler must have at least one key.
|
||||||
static void _findKeys(const cgltf_accessor *input, double time, int32_t *key, int32_t *next, float *t, float *span) {
|
static void _findKeys(const SamplerT *sampler, double time, int32_t *key, int32_t *next, float *t, float *span) {
|
||||||
int32_t count = (int32_t)input->count;
|
|
||||||
int32_t low = 0;
|
int32_t low = 0;
|
||||||
int32_t high = count - 1;
|
int32_t high = sampler->keyCount - 1;
|
||||||
float t0;
|
float t0;
|
||||||
float t1;
|
float t1;
|
||||||
|
|
||||||
while (low < high) {
|
while (low < high) {
|
||||||
int32_t mid = (low + high + 1) / 2;
|
int32_t mid = (low + high + 1) / 2;
|
||||||
|
|
||||||
cgltf_accessor_read_float(input, (cgltf_size)mid, &t1, 1);
|
if (time < sampler->times[mid]) {
|
||||||
if (time < t1) {
|
|
||||||
high = mid - 1;
|
high = mid - 1;
|
||||||
} else {
|
} else {
|
||||||
low = mid;
|
low = mid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*key = low;
|
*key = low;
|
||||||
*next = SDL_min(low + 1, count - 1);
|
*next = SDL_min(low + 1, sampler->keyCount - 1);
|
||||||
cgltf_accessor_read_float(input, (cgltf_size)low, &t0, 1);
|
t0 = sampler->times[low];
|
||||||
cgltf_accessor_read_float(input, (cgltf_size)*next, &t1, 1);
|
t1 = sampler->times[*next];
|
||||||
*span = t1 - t0;
|
*span = t1 - t0;
|
||||||
*t = (t1 > t0) ? (float)((time - t0) / (t1 - t0)) : 0.0f;
|
*t = (t1 > t0) ? (float)((time - t0) / (t1 - t0)) : 0.0f;
|
||||||
*t = SDL_clamp(*t, 0.0f, 1.0f);
|
*t = SDL_clamp(*t, 0.0f, 1.0f);
|
||||||
|
|
@ -635,6 +637,13 @@ static void _freeModel(ModelT *model) {
|
||||||
int32_t y;
|
int32_t y;
|
||||||
|
|
||||||
if (model->data != NULL) {
|
if (model->data != NULL) {
|
||||||
|
for (x = 0; (model->animations != NULL) && (x < (int32_t)model->data->animations_count); x++) {
|
||||||
|
for (y = 0; y < (int32_t)model->data->animations[x].samplers_count; y++) {
|
||||||
|
SDL_free(model->animations[x].samplers[y].times);
|
||||||
|
SDL_free(model->animations[x].samplers[y].values);
|
||||||
|
}
|
||||||
|
SDL_free(model->animations[x].samplers);
|
||||||
|
}
|
||||||
for (x = 0; x < (int32_t)model->data->materials_count; x++) {
|
for (x = 0; x < (int32_t)model->data->materials_count; x++) {
|
||||||
if ((model->materials != NULL) && (model->materials[x] != NO_HANDLE)) {
|
if ((model->materials != NULL) && (model->materials[x] != NO_HANDLE)) {
|
||||||
materialDelete(model->materials[x]);
|
materialDelete(model->materials[x]);
|
||||||
|
|
@ -650,7 +659,7 @@ static void _freeModel(ModelT *model) {
|
||||||
}
|
}
|
||||||
SDL_free(model->materials);
|
SDL_free(model->materials);
|
||||||
SDL_free(model->meshes);
|
SDL_free(model->meshes);
|
||||||
SDL_free(model->animationLengths);
|
SDL_free(model->animations);
|
||||||
memset(model, 0, sizeof(*model));
|
memset(model, 0, sizeof(*model));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -760,6 +769,70 @@ static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Every animation's samplers, unpacked to floats, and its length. A sampler that cannot be read,
|
||||||
|
// or whose output does not fit what its channel drives, is noted; the channel then holds the rest
|
||||||
|
// pose (or, for a short output, whatever _samplerValue fills in).
|
||||||
|
static void _loadAnimations(ModelT *model) {
|
||||||
|
cgltf_data *data = model->data;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
|
||||||
|
model->animations = SDL_calloc(SDL_max(data->animations_count, 1), sizeof(AnimationT));
|
||||||
|
if (model->animations == NULL) {
|
||||||
|
utilDie("Out of memory loading a model.");
|
||||||
|
}
|
||||||
|
for (x = 0; x < (int32_t)data->animations_count; x++) {
|
||||||
|
const cgltf_animation *source = &data->animations[x];
|
||||||
|
AnimationT *animation = &model->animations[x];
|
||||||
|
const char *name = source->name ? source->name : "(unnamed)";
|
||||||
|
|
||||||
|
animation->samplers = SDL_calloc(SDL_max(source->samplers_count, 1), sizeof(SamplerT));
|
||||||
|
if (animation->samplers == NULL) {
|
||||||
|
utilDie("Out of memory loading a model.");
|
||||||
|
}
|
||||||
|
for (y = 0; y < (int32_t)source->samplers_count; y++) {
|
||||||
|
const cgltf_accessor *input = source->samplers[y].input;
|
||||||
|
const cgltf_accessor *output = source->samplers[y].output;
|
||||||
|
SamplerT *sampler = &animation->samplers[y];
|
||||||
|
int32_t components = (int32_t)cgltf_num_components(output->type);
|
||||||
|
|
||||||
|
if ((input->count == 0) || (output->count == 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sampler->times = SDL_malloc(sizeof(float) * input->count);
|
||||||
|
sampler->values = SDL_malloc(sizeof(float) * output->count * (size_t)components);
|
||||||
|
if ((sampler->times == NULL) || (sampler->values == NULL)) {
|
||||||
|
utilDie("Out of memory loading a model.");
|
||||||
|
}
|
||||||
|
if (!_readAttribute(input, sampler->times, 1, (int32_t)input->count) || !_readAttribute(output, sampler->values, components, (int32_t)output->count)) {
|
||||||
|
_warn("Animation %s sampler %d is unreadable; its channels are ignored.", name, y);
|
||||||
|
SDL_free(sampler->times);
|
||||||
|
SDL_free(sampler->values);
|
||||||
|
sampler->times = NULL;
|
||||||
|
sampler->values = NULL;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sampler->keyCount = (int32_t)input->count;
|
||||||
|
sampler->valueCount = (int32_t)output->count;
|
||||||
|
sampler->components = components;
|
||||||
|
}
|
||||||
|
for (y = 0; y < (int32_t)source->channels_count; y++) {
|
||||||
|
const cgltf_animation_channel *channel = &source->channels[y];
|
||||||
|
const SamplerT *sampler = &animation->samplers[channel->sampler - source->samplers];
|
||||||
|
int32_t needed = (channel->target_path == cgltf_animation_path_type_rotation) ? 4 : ((channel->target_path == cgltf_animation_path_type_weights) ? 1 : 3);
|
||||||
|
|
||||||
|
if (sampler->keyCount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sampler->components != needed) {
|
||||||
|
_warn("Animation %s channel %d has %d components per value; %d expected.", name, y, sampler->components, needed);
|
||||||
|
}
|
||||||
|
animation->length = SDL_max(animation->length, (double)sampler->times[sampler->keyCount - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// One scene material from a glTF material; returns NO_HANDLE on failure.
|
// One scene material from a glTF material; returns NO_HANDLE on failure.
|
||||||
static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material) {
|
static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material) {
|
||||||
int32_t handle = materialNew();
|
int32_t handle = materialNew();
|
||||||
|
|
@ -810,12 +883,12 @@ static bool _loadMeshes(ModelT *model) {
|
||||||
}
|
}
|
||||||
_freeImageCache(&cache, (int32_t)data->images_count);
|
_freeImageCache(&cache, (int32_t)data->images_count);
|
||||||
for (x = 0; x < (int32_t)data->meshes_count; x++) {
|
for (x = 0; x < (int32_t)data->meshes_count; x++) {
|
||||||
int32_t jointLimit = MAX_JOINTS;
|
int32_t jointCount = 0;
|
||||||
|
|
||||||
// The smallest skin driving this mesh bounds its vertices' joint ids.
|
// The largest skin driving this mesh, so a vertex naming a joint no skin has can be noted.
|
||||||
for (y = 0; y < (int32_t)data->nodes_count; y++) {
|
for (y = 0; y < (int32_t)data->nodes_count; y++) {
|
||||||
if ((data->nodes[y].mesh == &data->meshes[x]) && (data->nodes[y].skin != NULL)) {
|
if ((data->nodes[y].mesh == &data->meshes[x]) && (data->nodes[y].skin != NULL)) {
|
||||||
jointLimit = SDL_min(jointLimit, (int32_t)data->nodes[y].skin->joints_count);
|
jointCount = SDL_max(jointCount, (int32_t)data->nodes[y].skin->joints_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model->meshes[x].primitives = SDL_calloc(SDL_max(data->meshes[x].primitives_count, 1), sizeof(PrimitiveT));
|
model->meshes[x].primitives = SDL_calloc(SDL_max(data->meshes[x].primitives_count, 1), sizeof(PrimitiveT));
|
||||||
|
|
@ -830,7 +903,7 @@ static bool _loadMeshes(ModelT *model) {
|
||||||
_warn("Mesh %s primitive %d is not triangles; skipped.", data->meshes[x].name ? data->meshes[x].name : "(unnamed)", y);
|
_warn("Mesh %s primitive %d is not triangles; skipped.", data->meshes[x].name ? data->meshes[x].name : "(unnamed)", y);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mesh = _loadPrimitive(&data->meshes[x], primitive, jointLimit);
|
mesh = _loadPrimitive(&data->meshes[x], primitive, jointCount);
|
||||||
if (mesh == NO_HANDLE) {
|
if (mesh == NO_HANDLE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -895,9 +968,9 @@ static void _loadMorphs(int32_t mesh, const cgltf_mesh *source, const cgltf_prim
|
||||||
|
|
||||||
|
|
||||||
// One primitive as a scene mesh: positions, normals (computed when absent), texture
|
// One primitive as a scene mesh: positions, normals (computed when absent), texture
|
||||||
// coordinates, joints (each below jointLimit) and weights when skinned, and indices (generated
|
// coordinates, joints (each below MAX_JOINTS; jointCount is how many the largest skin using the
|
||||||
// when absent).
|
// mesh has) and weights when skinned, and indices (generated when absent).
|
||||||
static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t jointLimit) {
|
static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t jointCount) {
|
||||||
const cgltf_accessor *positions = NULL;
|
const cgltf_accessor *positions = NULL;
|
||||||
const cgltf_accessor *normals = NULL;
|
const cgltf_accessor *normals = NULL;
|
||||||
const cgltf_accessor *uvs = NULL;
|
const cgltf_accessor *uvs = NULL;
|
||||||
|
|
@ -913,6 +986,7 @@ static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *p
|
||||||
int32_t x;
|
int32_t x;
|
||||||
int32_t c;
|
int32_t c;
|
||||||
int32_t mesh;
|
int32_t mesh;
|
||||||
|
bool warned = false;
|
||||||
|
|
||||||
for (x = 0; x < (int32_t)primitive->attributes_count; x++) {
|
for (x = 0; x < (int32_t)primitive->attributes_count; x++) {
|
||||||
const cgltf_attribute *attribute = &primitive->attributes[x];
|
const cgltf_attribute *attribute = &primitive->attributes[x];
|
||||||
|
|
@ -985,20 +1059,25 @@ static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *p
|
||||||
vertices[x].tangent[3] = (values[x * 4 + 3] < 0.0f) ? -1.0f : 1.0f;
|
vertices[x].tangent[3] = (values[x * 4 + 3] < 0.0f) ? -1.0f : 1.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Joint ids are integers read as floats; each must name a joint the skin has, or the shader
|
// Joint ids are integers read as floats. The vertex field and the shader's matrices allow
|
||||||
// reads past its matrices.
|
// MAX_JOINTS; the shader pads a skin's matrices to that with identity, so an id past the skin's
|
||||||
|
// joints is harmless, but noted once as the file is wrong.
|
||||||
if ((joints != NULL) && (weights != NULL) && _readAttribute(joints, values, 4, vertexCount)) {
|
if ((joints != NULL) && (weights != NULL) && _readAttribute(joints, values, 4, vertexCount)) {
|
||||||
for (x = 0; x < vertexCount; x++) {
|
for (x = 0; x < vertexCount; x++) {
|
||||||
for (c = 0; c < 4; c++) {
|
for (c = 0; c < 4; c++) {
|
||||||
int32_t id = (int32_t)values[x * 4 + c];
|
int32_t id = (int32_t)values[x * 4 + c];
|
||||||
|
|
||||||
if ((id < 0) || (id >= jointLimit)) {
|
if ((id < 0) || (id >= MAX_JOINTS)) {
|
||||||
_fail("Mesh %s names joint %d of a skin with %d.", name, id, jointLimit);
|
_fail("Mesh %s names joint %d; at most %d are supported.", name, id, MAX_JOINTS);
|
||||||
SDL_free(values);
|
SDL_free(values);
|
||||||
SDL_free(indices);
|
SDL_free(indices);
|
||||||
SDL_free(vertices);
|
SDL_free(vertices);
|
||||||
return NO_HANDLE;
|
return NO_HANDLE;
|
||||||
}
|
}
|
||||||
|
if ((id >= jointCount) && !warned) {
|
||||||
|
_warn("Mesh %s names joint %d, but the skins using it have %d.", name, id, jointCount);
|
||||||
|
warned = true;
|
||||||
|
}
|
||||||
vertices[x].joints[c] = (uint8_t)id;
|
vertices[x].joints[c] = (uint8_t)id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1083,12 +1162,13 @@ static float *_poseMorph(const InstanceT *instance, const PoseT *pose, int32_t i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Reads count elements of the accessor as floats, components per element; sparse accessors (how
|
// Reads the accessor's count elements as floats, components per element; sparse accessors (how
|
||||||
// Blender writes shape keys), strides and normalised integers are all applied.
|
// Blender writes shape keys), strides and normalised integers are all applied. The accessor must
|
||||||
|
// hold exactly count elements: cgltf's sparse pass writes every element the accessor has.
|
||||||
static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count) {
|
static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count) {
|
||||||
cgltf_size floats = (cgltf_size)components * (cgltf_size)count;
|
cgltf_size floats = (cgltf_size)components * (cgltf_size)count;
|
||||||
|
|
||||||
if (((int32_t)accessor->count < count) || ((int32_t)cgltf_num_components(accessor->type) != components)) {
|
if (((int32_t)accessor->count != count) || ((int32_t)cgltf_num_components(accessor->type) != components)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return cgltf_accessor_unpack_floats(accessor, out, floats) == floats;
|
return cgltf_accessor_unpack_floats(accessor, out, floats) == floats;
|
||||||
|
|
@ -1096,10 +1176,8 @@ static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t c
|
||||||
|
|
||||||
|
|
||||||
// Evaluates one channel at a time (seconds): step, linear (slerp for rotations) or cubic
|
// Evaluates one channel at a time (seconds): step, linear (slerp for rotations) or cubic
|
||||||
// spline, clamped to the first and last keyframes.
|
// spline, clamped to the first and last keyframes. The sampler must have at least one key.
|
||||||
static void _sampleChannel(const cgltf_animation_channel *channel, double time, float *out, int32_t components) {
|
static void _sampleChannel(const SamplerT *sampler, cgltf_interpolation_type interpolation, double time, float *out, int32_t components) {
|
||||||
const cgltf_accessor *input = channel->sampler->input;
|
|
||||||
const cgltf_accessor *output = channel->sampler->output;
|
|
||||||
int32_t k;
|
int32_t k;
|
||||||
int32_t next;
|
int32_t next;
|
||||||
float t;
|
float t;
|
||||||
|
|
@ -1108,11 +1186,8 @@ static void _sampleChannel(const cgltf_animation_channel *channel, double time,
|
||||||
float b[4];
|
float b[4];
|
||||||
int32_t c;
|
int32_t c;
|
||||||
|
|
||||||
if (input->count == 0) {
|
_findKeys(sampler, time, &k, &next, &t, &dt);
|
||||||
return;
|
if (interpolation == cgltf_interpolation_type_cubic_spline) {
|
||||||
}
|
|
||||||
_findKeys(input, time, &k, &next, &t, &dt);
|
|
||||||
if (channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline) {
|
|
||||||
// Three vectors per keyframe: in-tangent, value, out-tangent. Hermite basis.
|
// Three vectors per keyframe: in-tangent, value, out-tangent. Hermite basis.
|
||||||
float p0[4];
|
float p0[4];
|
||||||
float m0[4];
|
float m0[4];
|
||||||
|
|
@ -1121,10 +1196,10 @@ static void _sampleChannel(const cgltf_animation_channel *channel, double time,
|
||||||
float t2 = t * t;
|
float t2 = t * t;
|
||||||
float t3 = t2 * t;
|
float t3 = t2 * t;
|
||||||
|
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)(k * CUBIC_SPLINE_STRIDE + 1), p0, (cgltf_size)components);
|
_samplerValue(sampler, k * CUBIC_SPLINE_STRIDE + CUBIC_SPLINE_VALUE, p0, components);
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)(k * CUBIC_SPLINE_STRIDE + 2), m0, (cgltf_size)components);
|
_samplerValue(sampler, k * CUBIC_SPLINE_STRIDE + CUBIC_SPLINE_OUT, m0, components);
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)(next * CUBIC_SPLINE_STRIDE), m1, (cgltf_size)components);
|
_samplerValue(sampler, next * CUBIC_SPLINE_STRIDE + CUBIC_SPLINE_IN, m1, components);
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)(next * CUBIC_SPLINE_STRIDE + 1), p1, (cgltf_size)components);
|
_samplerValue(sampler, next * CUBIC_SPLINE_STRIDE + CUBIC_SPLINE_VALUE, p1, components);
|
||||||
for (c = 0; c < components; c++) {
|
for (c = 0; c < components; c++) {
|
||||||
out[c] = (2.0f * t3 - 3.0f * t2 + 1.0f) * p0[c] + dt * (t3 - 2.0f * t2 + t) * m0[c] + (-2.0f * t3 + 3.0f * t2) * p1[c] + dt * (t3 - t2) * m1[c];
|
out[c] = (2.0f * t3 - 3.0f * t2 + 1.0f) * p0[c] + dt * (t3 - 2.0f * t2 + t) * m0[c] + (-2.0f * t3 + 3.0f * t2) * p1[c] + dt * (t3 - t2) * m1[c];
|
||||||
}
|
}
|
||||||
|
|
@ -1139,12 +1214,12 @@ static void _sampleChannel(const cgltf_animation_channel *channel, double time,
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)k, a, (cgltf_size)components);
|
_samplerValue(sampler, k, a, components);
|
||||||
if ((channel->sampler->interpolation == cgltf_interpolation_type_step) || (next == k)) {
|
if ((interpolation == cgltf_interpolation_type_step) || (next == k)) {
|
||||||
memcpy(out, a, sizeof(float) * (size_t)components);
|
memcpy(out, a, sizeof(float) * (size_t)components);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)next, b, (cgltf_size)components);
|
_samplerValue(sampler, next, b, components);
|
||||||
if (components == 4) {
|
if (components == 4) {
|
||||||
QuatT qa = { a[0], a[1], a[2], a[3] };
|
QuatT qa = { a[0], a[1], a[2], a[3] };
|
||||||
QuatT qb = { b[0], b[1], b[2], b[3] };
|
QuatT qb = { b[0], b[1], b[2], b[3] };
|
||||||
|
|
@ -1164,8 +1239,10 @@ static void _sampleChannel(const cgltf_animation_channel *channel, double time,
|
||||||
|
|
||||||
// The clip's pose at its time over the rest pose, marking what it drives.
|
// The clip's pose at its time over the rest pose, marking what it drives.
|
||||||
static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pose) {
|
static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pose) {
|
||||||
const cgltf_data *data = _models[instance->model].data;
|
const ModelT *model = &_models[instance->model];
|
||||||
|
const cgltf_data *data = model->data;
|
||||||
const cgltf_animation *animation;
|
const cgltf_animation *animation;
|
||||||
|
const AnimationT *cached;
|
||||||
int32_t x;
|
int32_t x;
|
||||||
float values[4];
|
float values[4];
|
||||||
|
|
||||||
|
|
@ -1174,12 +1251,15 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
animation = &data->animations[clip->animation];
|
animation = &data->animations[clip->animation];
|
||||||
|
cached = &model->animations[clip->animation];
|
||||||
for (x = 0; x < (int32_t)animation->channels_count; x++) {
|
for (x = 0; x < (int32_t)animation->channels_count; x++) {
|
||||||
const cgltf_animation_channel *channel = &animation->channels[x];
|
const cgltf_animation_channel *channel = &animation->channels[x];
|
||||||
|
const SamplerT *sampler = &cached->samplers[channel->sampler - animation->samplers];
|
||||||
|
cgltf_interpolation_type interpolation = channel->sampler->interpolation;
|
||||||
int32_t index;
|
int32_t index;
|
||||||
PoseT *p;
|
PoseT *p;
|
||||||
|
|
||||||
if (channel->target_node == NULL) {
|
if ((channel->target_node == NULL) || (sampler->keyCount == 0)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
index = (int32_t)(channel->target_node - data->nodes);
|
index = (int32_t)(channel->target_node - data->nodes);
|
||||||
|
|
@ -1188,36 +1268,31 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos
|
||||||
}
|
}
|
||||||
p = &pose[index];
|
p = &pose[index];
|
||||||
if (channel->target_path == cgltf_animation_path_type_translation) {
|
if (channel->target_path == cgltf_animation_path_type_translation) {
|
||||||
_sampleChannel(channel, clip->time, values, 3);
|
_sampleChannel(sampler, interpolation, clip->time, values, 3);
|
||||||
p->translation = vec3(values[0], values[1], values[2]);
|
p->translation = vec3(values[0], values[1], values[2]);
|
||||||
p->animated |= POSE_TRANSLATION;
|
p->animated |= POSE_TRANSLATION;
|
||||||
} else if (channel->target_path == cgltf_animation_path_type_rotation) {
|
} else if (channel->target_path == cgltf_animation_path_type_rotation) {
|
||||||
_sampleChannel(channel, clip->time, values, 4);
|
_sampleChannel(sampler, interpolation, clip->time, values, 4);
|
||||||
p->rotation.x = values[0];
|
p->rotation.x = values[0];
|
||||||
p->rotation.y = values[1];
|
p->rotation.y = values[1];
|
||||||
p->rotation.z = values[2];
|
p->rotation.z = values[2];
|
||||||
p->rotation.w = values[3];
|
p->rotation.w = values[QUATERNION_W];
|
||||||
p->animated |= POSE_ROTATION;
|
p->animated |= POSE_ROTATION;
|
||||||
} else if (channel->target_path == cgltf_animation_path_type_scale) {
|
} else if (channel->target_path == cgltf_animation_path_type_scale) {
|
||||||
_sampleChannel(channel, clip->time, values, 3);
|
_sampleChannel(sampler, interpolation, clip->time, values, 3);
|
||||||
p->scale = vec3(values[0], values[1], values[2]);
|
p->scale = vec3(values[0], values[1], values[2]);
|
||||||
p->animated |= POSE_SCALE;
|
p->animated |= POSE_SCALE;
|
||||||
} else if (channel->target_path == cgltf_animation_path_type_weights) {
|
} else if (channel->target_path == cgltf_animation_path_type_weights) {
|
||||||
// One weight per target per keyframe (three per target for a cubic spline).
|
// One weight per target per keyframe (three per target for a cubic spline).
|
||||||
const cgltf_animation_sampler *sampler = channel->sampler;
|
int32_t count = sampler->valueCount / sampler->keyCount;
|
||||||
int32_t count;
|
|
||||||
|
|
||||||
if (sampler->input->count == 0) {
|
if (interpolation == cgltf_interpolation_type_cubic_spline) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
count = (int32_t)(sampler->output->count / sampler->input->count);
|
|
||||||
if (sampler->interpolation == cgltf_interpolation_type_cubic_spline) {
|
|
||||||
count /= CUBIC_SPLINE_STRIDE;
|
count /= CUBIC_SPLINE_STRIDE;
|
||||||
}
|
}
|
||||||
if ((count <= 0) || (count > instance->morphStride)) {
|
if ((count <= 0) || (count > instance->morphStride)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_sampleWeights(channel, clip->time, _poseMorph(instance, pose, index), count);
|
_sampleWeights(sampler, interpolation, clip->time, _poseMorph(instance, pose, index), count);
|
||||||
p->morphCount = count;
|
p->morphCount = count;
|
||||||
p->animated |= POSE_MORPH;
|
p->animated |= POSE_MORPH;
|
||||||
}
|
}
|
||||||
|
|
@ -1225,12 +1300,26 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// One element of a sampler's output as components floats. What the output lacks (a short
|
||||||
|
// element, or one past the end) is filled so the caller sees zeros or an identity rotation
|
||||||
|
// rather than whatever was on the stack.
|
||||||
|
static void _samplerValue(const SamplerT *sampler, int32_t element, float *out, int32_t components) {
|
||||||
|
int32_t have = (element < sampler->valueCount) ? SDL_min(components, sampler->components) : 0;
|
||||||
|
int32_t c;
|
||||||
|
|
||||||
|
for (c = 0; c < have; c++) {
|
||||||
|
out[c] = sampler->values[(size_t)element * (size_t)sampler->components + (size_t)c];
|
||||||
|
}
|
||||||
|
for (; c < components; c++) {
|
||||||
|
out[c] = (c == QUATERNION_W) ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// A weights channel at a time: count scalars per keyframe, linear or step (cubic spline treated
|
// A weights channel at a time: count scalars per keyframe, linear or step (cubic spline treated
|
||||||
// as linear between its values), clamped at both ends.
|
// as linear between its values), clamped at both ends. The sampler must have at least one key.
|
||||||
static void _sampleWeights(const cgltf_animation_channel *channel, double time, float *out, int32_t count) {
|
static void _sampleWeights(const SamplerT *sampler, cgltf_interpolation_type interpolation, double time, float *out, int32_t count) {
|
||||||
const cgltf_accessor *input = channel->sampler->input;
|
bool cubic = interpolation == cgltf_interpolation_type_cubic_spline;
|
||||||
const cgltf_accessor *output = channel->sampler->output;
|
|
||||||
bool cubic = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline;
|
|
||||||
int32_t stride = cubic ? CUBIC_SPLINE_STRIDE : 1;
|
int32_t stride = cubic ? CUBIC_SPLINE_STRIDE : 1;
|
||||||
int32_t value = cubic ? CUBIC_SPLINE_VALUE : 0;
|
int32_t value = cubic ? CUBIC_SPLINE_VALUE : 0;
|
||||||
int32_t k;
|
int32_t k;
|
||||||
|
|
@ -1241,16 +1330,13 @@ static void _sampleWeights(const cgltf_animation_channel *channel, double time,
|
||||||
float b;
|
float b;
|
||||||
int32_t w;
|
int32_t w;
|
||||||
|
|
||||||
if (input->count == 0) {
|
_findKeys(sampler, time, &k, &next, &t, &dt);
|
||||||
return;
|
if (interpolation == cgltf_interpolation_type_step) {
|
||||||
}
|
|
||||||
_findKeys(input, time, &k, &next, &t, &dt);
|
|
||||||
if (channel->sampler->interpolation == cgltf_interpolation_type_step) {
|
|
||||||
t = 0.0f;
|
t = 0.0f;
|
||||||
}
|
}
|
||||||
for (w = 0; w < count; w++) {
|
for (w = 0; w < count; w++) {
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)((k * stride + value) * count + w), &a, 1);
|
_samplerValue(sampler, (k * stride + value) * count + w, &a, 1);
|
||||||
cgltf_accessor_read_float(output, (cgltf_size)((next * stride + value) * count + w), &b, 1);
|
_samplerValue(sampler, (next * stride + value) * count + w, &b, 1);
|
||||||
out[w] = a + (b - a) * t;
|
out[w] = a + (b - a) * t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1691,13 +1777,7 @@ int32_t modelLoad(const char *name) {
|
||||||
return NO_HANDLE;
|
return NO_HANDLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_models[handle].animationLengths = SDL_malloc(sizeof(double) * SDL_max(data->animations_count, 1));
|
_loadAnimations(&_models[handle]);
|
||||||
if (_models[handle].animationLengths == NULL) {
|
|
||||||
utilDie("Out of memory loading a model.");
|
|
||||||
}
|
|
||||||
for (x = 0; x < (int32_t)data->animations_count; x++) {
|
|
||||||
_models[handle].animationLengths[x] = _animationLength(&data->animations[x]);
|
|
||||||
}
|
|
||||||
if (!_loadMeshes(&_models[handle])) {
|
if (!_loadMeshes(&_models[handle])) {
|
||||||
_freeModel(&_models[handle]);
|
_freeModel(&_models[handle]);
|
||||||
return NO_HANDLE;
|
return NO_HANDLE;
|
||||||
|
|
@ -1728,6 +1808,7 @@ void modelQuit(void) {
|
||||||
_modelCapacity = 0;
|
_modelCapacity = 0;
|
||||||
_instanceCount = 0;
|
_instanceCount = 0;
|
||||||
_instanceCapacity = 0;
|
_instanceCapacity = 0;
|
||||||
|
_lastTick = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1740,13 +1821,14 @@ int32_t modelRootOf(int32_t root) {
|
||||||
|
|
||||||
|
|
||||||
// Once per frame: advances every playing animation by the wall clock (not at all while the game
|
// Once per frame: advances every playing animation by the wall clock (not at all while the game
|
||||||
// is paused) and poses the nodes.
|
// is paused, and by at most MAX_STEP_SECONDS after a stall) and poses the nodes.
|
||||||
void modelUpdate(bool advance) {
|
void modelUpdate(bool advance) {
|
||||||
uint64_t now = SDL_GetTicksNS();
|
uint64_t now = SDL_GetTicksNS();
|
||||||
double delta = (_lastTick != 0) ? (double)(now - _lastTick) / 1e9 : 0.0;
|
double delta = (_lastTick != 0) ? (double)(now - _lastTick) / 1e9 : 0.0;
|
||||||
int32_t x;
|
int32_t x;
|
||||||
|
|
||||||
_lastTick = now;
|
_lastTick = now;
|
||||||
|
delta = SDL_min(delta, MAX_STEP_SECONDS);
|
||||||
_freeStaleInstances();
|
_freeStaleInstances();
|
||||||
for (x = 0; x < _instanceCount; x++) {
|
for (x = 0; x < _instanceCount; x++) {
|
||||||
InstanceT *instance = &_instances[x];
|
InstanceT *instance = &_instances[x];
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ extern "C" {
|
||||||
#define NEAREST_EXTENT_XZ 2.0f // Half-size of the box a nearest-polygon search covers
|
#define NEAREST_EXTENT_XZ 2.0f // Half-size of the box a nearest-polygon search covers
|
||||||
#define NEAREST_EXTENT_Y 4.0f
|
#define NEAREST_EXTENT_Y 4.0f
|
||||||
#define MAX_CELLS (4096 * 4096) // Heightfield cells a bake may cover
|
#define MAX_CELLS (4096 * 4096) // Heightfield cells a bake may cover
|
||||||
|
#define TILE_ALIGN 4 // Detour rounds each block of a tile's data up to this many bytes
|
||||||
|
|
||||||
static_assert(AVOIDANCE_PRESETS <= DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS, "more avoidance presets than the crowd holds");
|
static_assert(AVOIDANCE_PRESETS <= DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS, "more avoidance presets than the crowd holds");
|
||||||
static_assert(sizeof(Vec3T) == 3 * sizeof(float), "Vec3T must be three packed floats for Detour to write into");
|
static_assert(sizeof(Vec3T) == 3 * sizeof(float), "Vec3T must be three packed floats for Detour to write into");
|
||||||
|
|
@ -108,6 +109,7 @@ typedef struct AgentRecordS {
|
||||||
|
|
||||||
|
|
||||||
static AgentRecordT *_agentOf(int32_t agent);
|
static AgentRecordT *_agentOf(int32_t agent);
|
||||||
|
static int64_t _alignTile(int64_t bytes);
|
||||||
static bool _buildTile(NavRecordT *nav);
|
static bool _buildTile(NavRecordT *nav);
|
||||||
static void _freeNav(NavRecordT *nav);
|
static void _freeNav(NavRecordT *nav);
|
||||||
static bool _gather(NavRecordT *nav, int32_t node);
|
static bool _gather(NavRecordT *nav, int32_t node);
|
||||||
|
|
@ -117,6 +119,7 @@ static NavRecordT *_navRecord(int32_t nav);
|
||||||
static dtPolyRef _nearestPoly(NavRecordT *nav, Vec3T point, float *out);
|
static dtPolyRef _nearestPoly(NavRecordT *nav, Vec3T point, float *out);
|
||||||
static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent);
|
static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent);
|
||||||
static void _releaseDetour(NavRecordT *nav);
|
static void _releaseDetour(NavRecordT *nav);
|
||||||
|
static int64_t _tileBytes(const dtMeshHeader *header);
|
||||||
|
|
||||||
|
|
||||||
static NavRecordT _navs[MAX_NAVS];
|
static NavRecordT _navs[MAX_NAVS];
|
||||||
|
|
@ -138,6 +141,11 @@ static AgentRecordT *_agentOf(int32_t agent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int64_t _alignTile(int64_t bytes) {
|
||||||
|
return (bytes + TILE_ALIGN - 1) & ~(int64_t)(TILE_ALIGN - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// The single-tile bake: voxelise, filter, regions, contours, polygons, detail, then Detour data.
|
// The single-tile bake: voxelise, filter, regions, contours, polygons, detail, then Detour data.
|
||||||
static bool _buildTile(NavRecordT *nav) {
|
static bool _buildTile(NavRecordT *nav) {
|
||||||
rcContext context(false);
|
rcContext context(false);
|
||||||
|
|
@ -430,6 +438,24 @@ static void _releaseDetour(NavRecordT *nav) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The bytes a tile with this header's counts occupies, laid out as Detour's addTile reads it (each
|
||||||
|
// block rounded up to TILE_ALIGN), or -1 for a count no tile can have.
|
||||||
|
static int64_t _tileBytes(const dtMeshHeader *header) {
|
||||||
|
const int32_t counts[] = { header->vertCount, header->polyCount, header->maxLinkCount, header->detailMeshCount, header->detailVertCount, header->detailTriCount, header->bvNodeCount, header->offMeshConCount };
|
||||||
|
const size_t sizes[] = { sizeof(float) * 3, sizeof(dtPoly), sizeof(dtLink), sizeof(dtPolyDetail), sizeof(float) * 3, sizeof(unsigned char) * 4, sizeof(dtBVNode), sizeof(dtOffMeshConnection) };
|
||||||
|
int64_t total = _alignTile((int64_t)sizeof(dtMeshHeader));
|
||||||
|
size_t x;
|
||||||
|
|
||||||
|
for (x = 0; x < SDL_arraysize(counts); x++) {
|
||||||
|
if (counts[x] < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
total += _alignTile((int64_t)sizes[x] * counts[x]);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== Public =====
|
// ===== Public =====
|
||||||
|
|
||||||
bool navAddNode(int32_t nav, int32_t node) {
|
bool navAddNode(int32_t nav, int32_t node) {
|
||||||
|
|
@ -535,6 +561,8 @@ int32_t navAgentNew(int32_t nav, int32_t node, float radius, float height, float
|
||||||
if (x == MAX_AGENTS) {
|
if (x == MAX_AGENTS) {
|
||||||
return NAV_NO_HANDLE;
|
return NAV_NO_HANDLE;
|
||||||
}
|
}
|
||||||
|
radius = SDL_max(radius, MIN_AGENT_SIZE);
|
||||||
|
height = SDL_max(height, MIN_AGENT_SIZE);
|
||||||
position = nodeGetWorldPosition(node);
|
position = nodeGetWorldPosition(node);
|
||||||
if (_nearestPoly(record, position, start) == 0) {
|
if (_nearestPoly(record, position, start) == 0) {
|
||||||
start[0] = position.x;
|
start[0] = position.x;
|
||||||
|
|
@ -684,11 +712,14 @@ void navInit(void) {
|
||||||
|
|
||||||
|
|
||||||
// A baked mesh from navSave's bytes (copied). Detour reads the tile header before it checks
|
// A baked mesh from navSave's bytes (copied). Detour reads the tile header before it checks
|
||||||
// anything, so the bytes are checked for a whole header with its magic and version first.
|
// anything and then lays the tile out by the header's counts without looking at the size, so the
|
||||||
|
// bytes are checked for a whole header with its magic and version, and then for the whole tile
|
||||||
|
// those counts describe, before Detour sees them.
|
||||||
int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHeight) {
|
int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHeight) {
|
||||||
int32_t x;
|
int32_t x;
|
||||||
unsigned char *copy;
|
unsigned char *copy;
|
||||||
dtMeshHeader header;
|
dtMeshHeader header;
|
||||||
|
int64_t expected;
|
||||||
|
|
||||||
for (x = 0; x < MAX_NAVS; x++) {
|
for (x = 0; x < MAX_NAVS; x++) {
|
||||||
if (!_navs[x].used) {
|
if (!_navs[x].used) {
|
||||||
|
|
@ -703,6 +734,11 @@ int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHei
|
||||||
utilTrace("Nav: not a navigation mesh file.");
|
utilTrace("Nav: not a navigation mesh file.");
|
||||||
return NAV_NO_HANDLE;
|
return NAV_NO_HANDLE;
|
||||||
}
|
}
|
||||||
|
expected = _tileBytes(&header);
|
||||||
|
if ((expected < 0) || (size > INT32_MAX) || ((int64_t)size < expected)) {
|
||||||
|
utilTrace("Nav: the navigation mesh file is truncated or damaged.");
|
||||||
|
return NAV_NO_HANDLE;
|
||||||
|
}
|
||||||
copy = (unsigned char *)dtAlloc(size, DT_ALLOC_PERM);
|
copy = (unsigned char *)dtAlloc(size, DT_ALLOC_PERM);
|
||||||
if (copy == nullptr) {
|
if (copy == nullptr) {
|
||||||
return NAV_NO_HANDLE;
|
return NAV_NO_HANDLE;
|
||||||
|
|
|
||||||
16
src/pack.c
16
src/pack.c
|
|
@ -61,6 +61,7 @@ typedef struct WriterS {
|
||||||
sqlite3_stmt *assetStmt;
|
sqlite3_stmt *assetStmt;
|
||||||
sqlite3_stmt *chunkStmt;
|
sqlite3_stmt *chunkStmt;
|
||||||
sqlite3_stmt *deleteChunkStmt;
|
sqlite3_stmt *deleteChunkStmt;
|
||||||
|
uint8_t *buffer; // One chunk, reused for every file
|
||||||
int64_t chunkBytes;
|
int64_t chunkBytes;
|
||||||
int64_t files;
|
int64_t files;
|
||||||
int64_t bytes;
|
int64_t bytes;
|
||||||
|
|
@ -282,7 +283,7 @@ static bool _exec(sqlite3 *db, const char *sql) {
|
||||||
// recorded is the size stored, so a file that changed under the packer is refused, not misdescribed.
|
// recorded is the size stored, so a file that changed under the packer is refused, not misdescribed.
|
||||||
static bool _insertFile(WriterT *writer, const EntryT *entry) {
|
static bool _insertFile(WriterT *writer, const EntryT *entry) {
|
||||||
FILE *file = NULL;
|
FILE *file = NULL;
|
||||||
uint8_t *buffer = NULL;
|
uint8_t *buffer = writer->buffer;
|
||||||
char *key = _keyFor(entry->relative);
|
char *key = _keyFor(entry->relative);
|
||||||
size_t got = 0;
|
size_t got = 0;
|
||||||
int64_t stored = 0;
|
int64_t stored = 0;
|
||||||
|
|
@ -295,10 +296,6 @@ static bool _insertFile(WriterT *writer, const EntryT *entry) {
|
||||||
free(key);
|
free(key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buffer = (uint8_t *)malloc((size_t)writer->chunkBytes);
|
|
||||||
if (buffer == NULL) {
|
|
||||||
utilDie("Out of memory packing %s.", entry->full);
|
|
||||||
}
|
|
||||||
sqlite3_reset(writer->deleteChunkStmt);
|
sqlite3_reset(writer->deleteChunkStmt);
|
||||||
sqlite3_bind_text(writer->deleteChunkStmt, 1, key, -1, SQLITE_STATIC);
|
sqlite3_bind_text(writer->deleteChunkStmt, 1, key, -1, SQLITE_STATIC);
|
||||||
sqlite3_step(writer->deleteChunkStmt);
|
sqlite3_step(writer->deleteChunkStmt);
|
||||||
|
|
@ -336,7 +333,6 @@ static bool _insertFile(WriterT *writer, const EntryT *entry) {
|
||||||
writer->files++;
|
writer->files++;
|
||||||
writer->bytes += entry->size;
|
writer->bytes += entry->size;
|
||||||
fclose(file);
|
fclose(file);
|
||||||
free(buffer);
|
|
||||||
free(key);
|
free(key);
|
||||||
|
|
||||||
return ok;
|
return ok;
|
||||||
|
|
@ -592,6 +588,7 @@ static bool _writerClose(WriterT *writer, bool commit) {
|
||||||
sqlite3_finalize(writer->chunkStmt);
|
sqlite3_finalize(writer->chunkStmt);
|
||||||
sqlite3_finalize(writer->deleteChunkStmt);
|
sqlite3_finalize(writer->deleteChunkStmt);
|
||||||
sqlite3_close(writer->db);
|
sqlite3_close(writer->db);
|
||||||
|
free(writer->buffer);
|
||||||
memset(writer, 0, sizeof(*writer));
|
memset(writer, 0, sizeof(*writer));
|
||||||
|
|
||||||
return ok;
|
return ok;
|
||||||
|
|
@ -650,9 +647,14 @@ static bool _writerOpen(WriterT *writer, const char *database, bool create) {
|
||||||
sqlite3_finalize(writer->deleteChunkStmt);
|
sqlite3_finalize(writer->deleteChunkStmt);
|
||||||
sqlite3_close(writer->db);
|
sqlite3_close(writer->db);
|
||||||
memset(writer, 0, sizeof(*writer));
|
memset(writer, 0, sizeof(*writer));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
writer->buffer = (uint8_t *)malloc((size_t)writer->chunkBytes);
|
||||||
|
if (writer->buffer == NULL) {
|
||||||
|
utilDie("Out of memory packing %s.", database);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
typedef struct EmitterS {
|
typedef struct EmitterS {
|
||||||
int32_t id;
|
int32_t id;
|
||||||
int32_t node; // -1: 2D
|
int32_t node; // -1: 2D
|
||||||
|
uint32_t nodeGeneration; // The node's when the emitter was made; a reused handle is not ours
|
||||||
// The recipe
|
// The recipe
|
||||||
float rate;
|
float rate;
|
||||||
float lifeMin;
|
float lifeMin;
|
||||||
|
|
@ -113,6 +114,7 @@ static void _freeFrames(EmitterT *emitter);
|
||||||
static void _freePools(EmitterT *emitter);
|
static void _freePools(EmitterT *emitter);
|
||||||
static void _freeTrails(EmitterT *emitter);
|
static void _freeTrails(EmitterT *emitter);
|
||||||
static void _kill(EmitterT *emitter, int32_t index);
|
static void _kill(EmitterT *emitter, int32_t index);
|
||||||
|
static bool _nodeAlive(const EmitterT *emitter);
|
||||||
static Vec3T _origin(EmitterT *emitter);
|
static Vec3T _origin(EmitterT *emitter);
|
||||||
static float _random(float min, float max);
|
static float _random(float min, float max);
|
||||||
static Vec3T _randomDirection(EmitterT *emitter);
|
static Vec3T _randomDirection(EmitterT *emitter);
|
||||||
|
|
@ -323,13 +325,18 @@ static void _kill(EmitterT *emitter, int32_t index) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Whether a 3D emitter's node is still the one it was made on (a handle the scene reused since
|
||||||
|
// belongs to somebody else).
|
||||||
|
static bool _nodeAlive(const EmitterT *emitter) {
|
||||||
|
return nodeValid(emitter->node) && (nodeGetGeneration(emitter->node) == emitter->nodeGeneration);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Where particles are born: the 2D position, or the node's world position.
|
// Where particles are born: the 2D position, or the node's world position.
|
||||||
static Vec3T _origin(EmitterT *emitter) {
|
static Vec3T _origin(EmitterT *emitter) {
|
||||||
if (emitter->node >= 0) {
|
if ((emitter->node >= 0) && _nodeAlive(emitter)) {
|
||||||
if (nodeValid(emitter->node)) {
|
|
||||||
emitter->position = nodeGetWorldPosition(emitter->node);
|
emitter->position = nodeGetWorldPosition(emitter->node);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return emitter->position;
|
return emitter->position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -591,6 +598,7 @@ int32_t emitterNew(int32_t node) {
|
||||||
}
|
}
|
||||||
emitter->id = _nextId++;
|
emitter->id = _nextId++;
|
||||||
emitter->node = node;
|
emitter->node = node;
|
||||||
|
emitter->nodeGeneration = nodeGetGeneration(node);
|
||||||
emitter->rate = 50.0f;
|
emitter->rate = 50.0f;
|
||||||
emitter->lifeMin = 1.0f;
|
emitter->lifeMin = 1.0f;
|
||||||
emitter->lifeMax = 2.0f;
|
emitter->lifeMax = 2.0f;
|
||||||
|
|
@ -970,7 +978,7 @@ void particlesUpdate(bool advance) {
|
||||||
dt = MAX_STEP_SECONDS;
|
dt = MAX_STEP_SECONDS;
|
||||||
}
|
}
|
||||||
HASH_ITER(hh, _emitters, emitter, next) {
|
HASH_ITER(hh, _emitters, emitter, next) {
|
||||||
if ((emitter->node >= 0) && !nodeValid(emitter->node)) {
|
if ((emitter->node >= 0) && !_nodeAlive(emitter)) {
|
||||||
emitterDelete(emitter->id);
|
emitterDelete(emitter->id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ namespace {
|
||||||
|
|
||||||
void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override;
|
void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override;
|
||||||
void OnContactRemoved(const JPH::SubShapeIDPair &pair) override;
|
void OnContactRemoved(const JPH::SubShapeIDPair &pair) override;
|
||||||
void forget(int32_t node); // Drops the overlap counts a node is party to
|
void forget(int32_t node, bool everyRole); // Drops the overlap counts where node is the trigger (or, everyRole, either party)
|
||||||
void overlap(int32_t trigger, int32_t node, bool entered); // One sub-shape pair started or stopped touching
|
void overlap(int32_t trigger, int32_t node, bool entered); // One sub-shape pair started or stopped touching
|
||||||
void push(const PhysicsEventT &event); // Queues an event, taking the lock
|
void push(const PhysicsEventT &event); // Queues an event, taking the lock
|
||||||
void pushLocked(const PhysicsEventT &event); // Queues an event under a lock already held
|
void pushLocked(const PhysicsEventT &event); // Queues an event under a lock already held
|
||||||
|
|
@ -488,6 +488,7 @@ namespace {
|
||||||
BodyRecordT *_find(int32_t node);
|
BodyRecordT *_find(int32_t node);
|
||||||
PlayerRecordT *_findPlayer(int32_t node);
|
PlayerRecordT *_findPlayer(int32_t node);
|
||||||
RagdollRecordT *_findRagdoll(int32_t node);
|
RagdollRecordT *_findRagdoll(int32_t node);
|
||||||
|
template <typename RecordT> RecordT *_findRecord(std::vector<int32_t> &index, RecordT *table, int32_t node, void (*release)(RecordT *));
|
||||||
int32_t _findSkinned(int32_t node);
|
int32_t _findSkinned(int32_t node);
|
||||||
SoftRecordT *_findSoft(int32_t node);
|
SoftRecordT *_findSoft(int32_t node);
|
||||||
VehicleRecordT *_findVehicle(int32_t node);
|
VehicleRecordT *_findVehicle(int32_t node);
|
||||||
|
|
@ -495,7 +496,9 @@ namespace {
|
||||||
JPH::Vec3 _fromVec3(Vec3T v);
|
JPH::Vec3 _fromVec3(Vec3T v);
|
||||||
int32_t _indexGet(const std::vector<int32_t> &index, int32_t node);
|
int32_t _indexGet(const std::vector<int32_t> &index, int32_t node);
|
||||||
void _indexSet(std::vector<int32_t> &index, int32_t node, int32_t slot);
|
void _indexSet(std::vector<int32_t> &index, int32_t node, int32_t slot);
|
||||||
|
bool _isPlayer(int32_t node);
|
||||||
int32_t _nearestSoftVertex(const SoftRecordT *record, Vec3T point);
|
int32_t _nearestSoftVertex(const SoftRecordT *record, Vec3T point);
|
||||||
|
bool _nodeStale(int32_t node, uint32_t generation);
|
||||||
void _pinSoft(void);
|
void _pinSoft(void);
|
||||||
void _playerInside(PlayerRecordT *record, const int32_t *now, int32_t count);
|
void _playerInside(PlayerRecordT *record, const int32_t *now, int32_t count);
|
||||||
void _playerTriggers(PlayerRecordT *record);
|
void _playerTriggers(PlayerRecordT *record);
|
||||||
|
|
@ -503,11 +506,13 @@ namespace {
|
||||||
void _poseWheels(void);
|
void _poseWheels(void);
|
||||||
int32_t _ragdollPartOf(RagdollRecordT *record, int32_t joint);
|
int32_t _ragdollPartOf(RagdollRecordT *record, int32_t joint);
|
||||||
void _release(BodyRecordT *record);
|
void _release(BodyRecordT *record);
|
||||||
|
void _releaseDead(void);
|
||||||
void _releasePlayer(PlayerRecordT *record);
|
void _releasePlayer(PlayerRecordT *record);
|
||||||
void _releaseRagdoll(RagdollRecordT *record);
|
void _releaseRagdoll(RagdollRecordT *record);
|
||||||
void _releaseRagdollBodies(RagdollRecordT *record);
|
void _releaseRagdollBodies(RagdollRecordT *record);
|
||||||
void _releaseSoft(SoftRecordT *record);
|
void _releaseSoft(SoftRecordT *record);
|
||||||
void _releaseSoftBody(SoftRecordT *record);
|
void _releaseSoftBody(SoftRecordT *record);
|
||||||
|
template <typename RecordT> void _releaseStale(RecordT *table, int32_t count, void (*release)(RecordT *));
|
||||||
void _releaseVehicle(VehicleRecordT *record);
|
void _releaseVehicle(VehicleRecordT *record);
|
||||||
void _resetRagdoll(RagdollRecordT *record);
|
void _resetRagdoll(RagdollRecordT *record);
|
||||||
void _resetSoft(SoftRecordT *record);
|
void _resetSoft(SoftRecordT *record);
|
||||||
|
|
@ -544,7 +549,7 @@ namespace {
|
||||||
int32_t node = (int32_t)(uint32_t)other.GetUserData();
|
int32_t node = (int32_t)(uint32_t)other.GetUserData();
|
||||||
|
|
||||||
// A player's inner body: the engine reports players entering triggers itself.
|
// A player's inner body: the engine reports players entering triggers itself.
|
||||||
if (_findPlayer(node) == nullptr) {
|
if (!_isPlayer(node)) {
|
||||||
overlap((int32_t)(uint32_t)trigger.GetUserData(), node, true);
|
overlap((int32_t)(uint32_t)trigger.GetUserData(), node, true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -582,18 +587,20 @@ namespace {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
node = (int32_t)(uint32_t)other->GetUserData();
|
node = (int32_t)(uint32_t)other->GetUserData();
|
||||||
if (_findPlayer(node) == nullptr) {
|
if (!_isPlayer(node)) {
|
||||||
overlap((int32_t)(uint32_t)trigger->GetUserData(), node, false);
|
overlap((int32_t)(uint32_t)trigger->GetUserData(), node, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ContactListenerT::forget(int32_t node) {
|
// A body that stops being a trigger keeps the overlaps it is inside as a body; a body that is
|
||||||
|
// gone drops both.
|
||||||
|
void ContactListenerT::forget(int32_t node, bool everyRole) {
|
||||||
std::lock_guard<std::mutex> guard(lock);
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
std::unordered_map<uint64_t, int32_t>::iterator it = overlaps.begin();
|
std::unordered_map<uint64_t, int32_t>::iterator it = overlaps.begin();
|
||||||
|
|
||||||
while (it != overlaps.end()) {
|
while (it != overlaps.end()) {
|
||||||
if (((int32_t)(uint32_t)(it->first >> 32) == node) || ((int32_t)(uint32_t)it->first == node)) {
|
if (((int32_t)(uint32_t)(it->first >> 32) == node) || (everyRole && ((int32_t)(uint32_t)it->first == node))) {
|
||||||
it = overlaps.erase(it);
|
it = overlaps.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
|
|
@ -742,10 +749,6 @@ namespace {
|
||||||
if (!record->used || (record->type != BODY_KINEMATIC) || !record->enabled) {
|
if (!record->used || (record->type != BODY_KINEMATIC) || !record->enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
||||||
_release(record);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
||||||
bodies.MoveKinematic(record->id, JPH::RVec3(position.x, position.y, position.z), _fromQuat(rotation), dt);
|
bodies.MoveKinematic(record->id, JPH::RVec3(position.x, position.y, position.z), _fromQuat(rotation), dt);
|
||||||
}
|
}
|
||||||
|
|
@ -772,10 +775,6 @@ namespace {
|
||||||
if (!record->used || (record->type != BODY_DYNAMIC) || !record->enabled) {
|
if (!record->used || (record->type != BODY_DYNAMIC) || !record->enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
||||||
_release(record);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bodies.GetPositionAndRotation(record->id, where, how);
|
bodies.GetPositionAndRotation(record->id, where, how);
|
||||||
nodeSetWorldTransform(record->node, vec3((float)where.GetX(), (float)where.GetY(), (float)where.GetZ()), _toQuat(how));
|
nodeSetWorldTransform(record->node, vec3((float)where.GetX(), (float)where.GetY(), (float)where.GetZ()), _toQuat(how));
|
||||||
}
|
}
|
||||||
|
|
@ -932,22 +931,29 @@ namespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The record for a node's body, or NULL. A record whose node was deleted (or reused) is
|
// The record for a node's body, or NULL.
|
||||||
// released on the way.
|
|
||||||
BodyRecordT *_find(int32_t node) {
|
BodyRecordT *_find(int32_t node) {
|
||||||
BodyRecordT *record;
|
return (_world == nullptr) ? nullptr : _findRecord(_world->bodyOfNode, _world->bodies, node, _release);
|
||||||
int32_t slot;
|
|
||||||
|
|
||||||
if (_world == nullptr) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
slot = _indexGet(_world->bodyOfNode, node);
|
|
||||||
|
|
||||||
|
// The record a node maps to in one of the tables, or NULL. A record whose node was deleted (or
|
||||||
|
// whose handle was reused) is released on the way, so a stale handle never reaches its previous
|
||||||
|
// owner's record; a mapping that outlived its record is dropped.
|
||||||
|
template <typename RecordT> RecordT *_findRecord(std::vector<int32_t> &index, RecordT *table, int32_t node, void (*release)(RecordT *)) {
|
||||||
|
int32_t slot = _indexGet(index, node);
|
||||||
|
RecordT *record;
|
||||||
|
|
||||||
if (slot == NO_HANDLE) {
|
if (slot == NO_HANDLE) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
record = &_world->bodies[slot];
|
record = &table[slot];
|
||||||
if (!nodeValid(node) || (nodeGetGeneration(node) != record->generation)) {
|
if (!record->used) {
|
||||||
_release(record);
|
_indexSet(index, node, NO_HANDLE);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (_nodeStale(node, record->generation)) {
|
||||||
|
release(record);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return record;
|
return record;
|
||||||
|
|
@ -988,6 +994,20 @@ namespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Whether a node carries a player, from the index alone: Jolt's job threads ask this mid-step,
|
||||||
|
// where the scene must not be read and nothing may be released (the sweep before the step
|
||||||
|
// guarantees no record is stale in that window anyway).
|
||||||
|
bool _isPlayer(int32_t node) {
|
||||||
|
return _indexGet(_world->playerOfNode, node) != NO_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Whether a record's node was deleted (or its handle reused) since the record took it.
|
||||||
|
bool _nodeStale(int32_t node, uint32_t generation) {
|
||||||
|
return !nodeValid(node) || (nodeGetGeneration(node) != generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Takes the body out of the world (its vehicle and joints first) and frees its slot.
|
// Takes the body out of the world (its vehicle and joints first) and frees its slot.
|
||||||
void _release(BodyRecordT *record) {
|
void _release(BodyRecordT *record) {
|
||||||
VehicleRecordT *vehicle = _findVehicle(record->node);
|
VehicleRecordT *vehicle = _findVehicle(record->node);
|
||||||
|
|
@ -1001,10 +1021,35 @@ namespace {
|
||||||
jointDelete(x);
|
jointDelete(x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_world->contacts->forget(record->node);
|
_world->contacts->forget(record->node, true);
|
||||||
_indexSet(_world->bodyOfNode, record->node, NO_HANDLE);
|
_indexSet(_world->bodyOfNode, record->node, NO_HANDLE);
|
||||||
_destroyBody(record->id);
|
_destroyBody(record->id);
|
||||||
memset(record, 0, sizeof(*record));
|
memset(record, 0, sizeof(*record));
|
||||||
|
record->node = NO_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Releases every record whose node is gone, whatever its state: the step loops skip disabled
|
||||||
|
// bodies and players and inactive ragdolls, so this is where those are found. Bodies go first,
|
||||||
|
// since a body's release takes its vehicle and joints with it.
|
||||||
|
void _releaseDead(void) {
|
||||||
|
_releaseStale(_world->bodies, _world->bodyCount, _release);
|
||||||
|
_releaseStale(_world->players, _world->playerCount, _releasePlayer);
|
||||||
|
_releaseStale(_world->ragdolls, _world->ragdollCount, _releaseRagdoll);
|
||||||
|
_releaseStale(_world->softs, _world->softCount, _releaseSoft);
|
||||||
|
_releaseStale(_world->vehicles, _world->vehicleCount, _releaseVehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The dead-node sweep over one table.
|
||||||
|
template <typename RecordT> void _releaseStale(RecordT *table, int32_t count, void (*release)(RecordT *)) {
|
||||||
|
int32_t x;
|
||||||
|
|
||||||
|
for (x = 0; x < count; x++) {
|
||||||
|
if (table[x].used && _nodeStale(table[x].node, table[x].generation)) {
|
||||||
|
release(&table[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1069,9 +1114,7 @@ namespace {
|
||||||
|
|
||||||
|
|
||||||
PlayerRecordT *_findPlayer(int32_t node) {
|
PlayerRecordT *_findPlayer(int32_t node) {
|
||||||
int32_t slot = (_world != nullptr) ? _indexGet(_world->playerOfNode, node) : NO_HANDLE;
|
return (_world == nullptr) ? nullptr : _findRecord(_world->playerOfNode, _world->players, node, _releasePlayer);
|
||||||
|
|
||||||
return (slot == NO_HANDLE) ? nullptr : &_world->players[slot];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1091,7 +1134,7 @@ namespace {
|
||||||
_indexSet(_world->playerOfNode, record->node, NO_HANDLE);
|
_indexSet(_world->playerOfNode, record->node, NO_HANDLE);
|
||||||
}
|
}
|
||||||
record->character = nullptr;
|
record->character = nullptr;
|
||||||
record->node = 0;
|
record->node = NO_HANDLE;
|
||||||
record->generation = 0;
|
record->generation = 0;
|
||||||
record->intent = vec3(0.0f, 0.0f, 0.0f);
|
record->intent = vec3(0.0f, 0.0f, 0.0f);
|
||||||
record->jumpSpeed = 0.0f;
|
record->jumpSpeed = 0.0f;
|
||||||
|
|
@ -1126,10 +1169,6 @@ namespace {
|
||||||
if (!record->used || !record->enabled) {
|
if (!record->used || !record->enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
||||||
_releasePlayer(record);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
scaledGravity = gravity * record->gravityScale;
|
scaledGravity = gravity * record->gravityScale;
|
||||||
record->character->SetUp(up);
|
record->character->SetUp(up);
|
||||||
velocity = record->character->GetLinearVelocity();
|
velocity = record->character->GetLinearVelocity();
|
||||||
|
|
@ -1180,9 +1219,7 @@ namespace {
|
||||||
|
|
||||||
|
|
||||||
SoftRecordT *_findSoft(int32_t node) {
|
SoftRecordT *_findSoft(int32_t node) {
|
||||||
int32_t slot = (_world != nullptr) ? _indexGet(_world->softOfNode, node) : NO_HANDLE;
|
return (_world == nullptr) ? nullptr : _findRecord(_world->softOfNode, _world->softs, node, _releaseSoft);
|
||||||
|
|
||||||
return (slot == NO_HANDLE) ? nullptr : &_world->softs[slot];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1213,7 +1250,7 @@ namespace {
|
||||||
}
|
}
|
||||||
record->shared = nullptr;
|
record->shared = nullptr;
|
||||||
record->body = JPH::BodyID();
|
record->body = JPH::BodyID();
|
||||||
record->node = 0;
|
record->node = NO_HANDLE;
|
||||||
record->generation = 0;
|
record->generation = 0;
|
||||||
record->kind = SOFT_CLOTH;
|
record->kind = SOFT_CLOTH;
|
||||||
record->mesh = -1;
|
record->mesh = -1;
|
||||||
|
|
@ -1445,7 +1482,7 @@ namespace {
|
||||||
if (!record->used) {
|
if (!record->used) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation) || (nodeGetMesh(record->node) != record->mesh)) {
|
if (nodeGetMesh(record->node) != record->mesh) {
|
||||||
_releaseSoft(record);
|
_releaseSoft(record);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1489,9 +1526,7 @@ namespace {
|
||||||
|
|
||||||
|
|
||||||
RagdollRecordT *_findRagdoll(int32_t node) {
|
RagdollRecordT *_findRagdoll(int32_t node) {
|
||||||
int32_t slot = (_world != nullptr) ? _indexGet(_world->ragdollOfNode, node) : NO_HANDLE;
|
return (_world == nullptr) ? nullptr : _findRecord(_world->ragdollOfNode, _world->ragdolls, node, _releaseRagdoll);
|
||||||
|
|
||||||
return (slot == NO_HANDLE) ? nullptr : &_world->ragdolls[slot];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1564,7 +1599,7 @@ namespace {
|
||||||
record->parts[x].body = JPH::BodyID();
|
record->parts[x].body = JPH::BodyID();
|
||||||
}
|
}
|
||||||
record->filter = nullptr;
|
record->filter = nullptr;
|
||||||
record->node = 0;
|
record->node = NO_HANDLE;
|
||||||
record->generation = 0;
|
record->generation = 0;
|
||||||
record->skinned = -1;
|
record->skinned = -1;
|
||||||
record->partCount = 0;
|
record->partCount = 0;
|
||||||
|
|
@ -1627,10 +1662,6 @@ namespace {
|
||||||
if (!record->used || !record->active) {
|
if (!record->used || !record->active) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
||||||
_releaseRagdoll(record);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (p = 0; p < record->partCount; p++) {
|
for (p = 0; p < record->partCount; p++) {
|
||||||
RagdollPartT *part = &record->parts[p];
|
RagdollPartT *part = &record->parts[p];
|
||||||
JPH::RVec3 position;
|
JPH::RVec3 position;
|
||||||
|
|
@ -1679,9 +1710,7 @@ namespace {
|
||||||
|
|
||||||
|
|
||||||
VehicleRecordT *_findVehicle(int32_t node) {
|
VehicleRecordT *_findVehicle(int32_t node) {
|
||||||
int32_t slot = (_world != nullptr) ? _indexGet(_world->vehicleOfNode, node) : NO_HANDLE;
|
return (_world == nullptr) ? nullptr : _findRecord(_world->vehicleOfNode, _world->vehicles, node, _releaseVehicle);
|
||||||
|
|
||||||
return (slot == NO_HANDLE) ? nullptr : &_world->vehicles[slot];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1700,7 +1729,7 @@ namespace {
|
||||||
if (record->used) {
|
if (record->used) {
|
||||||
_indexSet(_world->vehicleOfNode, record->node, NO_HANDLE);
|
_indexSet(_world->vehicleOfNode, record->node, NO_HANDLE);
|
||||||
}
|
}
|
||||||
record->node = 0;
|
record->node = NO_HANDLE;
|
||||||
record->generation = 0;
|
record->generation = 0;
|
||||||
record->kind = VEHICLE_CAR;
|
record->kind = VEHICLE_CAR;
|
||||||
record->wheelCount = 0;
|
record->wheelCount = 0;
|
||||||
|
|
@ -2101,7 +2130,7 @@ namespace {
|
||||||
JPH::Quat rotation;
|
JPH::Quat rotation;
|
||||||
JPH::RVec3 position;
|
JPH::RVec3 position;
|
||||||
|
|
||||||
if (!nodeValid(record->wheels[w].node) || (nodeGetGeneration(record->wheels[w].node) != record->wheels[w].generation)) {
|
if (_nodeStale(record->wheels[w].node, record->wheels[w].generation)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
transform = record->constraint->GetWheelWorldTransform((JPH::uint)w, JPH::Vec3::sAxisX(), JPH::Vec3::sAxisY());
|
transform = record->constraint->GetWheelWorldTransform((JPH::uint)w, JPH::Vec3::sAxisX(), JPH::Vec3::sAxisY());
|
||||||
|
|
@ -2212,10 +2241,6 @@ namespace {
|
||||||
if (!record->used || !record->enabled) {
|
if (!record->used || !record->enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
|
|
||||||
_releasePlayer(record);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_playerTriggers(record);
|
_playerTriggers(record);
|
||||||
where = record->character->GetPosition();
|
where = record->character->GetPosition();
|
||||||
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
|
||||||
|
|
@ -2502,7 +2527,7 @@ bool bodySetTrigger(int32_t node, bool trigger) {
|
||||||
lock.GetBody().SetIsSensor(trigger);
|
lock.GetBody().SetIsSensor(trigger);
|
||||||
}
|
}
|
||||||
if (!trigger) {
|
if (!trigger) {
|
||||||
_world->contacts->forget(node);
|
_world->contacts->forget(node, false);
|
||||||
}
|
}
|
||||||
record->trigger = trigger;
|
record->trigger = trigger;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -2740,6 +2765,7 @@ bool physicsInit(void) {
|
||||||
_resetRagdoll(&_world->ragdolls[x]);
|
_resetRagdoll(&_world->ragdolls[x]);
|
||||||
}
|
}
|
||||||
_world->enabled = true;
|
_world->enabled = true;
|
||||||
|
_debugMask = DEBUG_NONE;
|
||||||
utilTrace("Physics: Jolt %d.%d.%d ready, %d job thread%s", JPH_VERSION_MAJOR, JPH_VERSION_MINOR, JPH_VERSION_PATCH, threads, (threads == 1) ? "" : "s");
|
utilTrace("Physics: Jolt %d.%d.%d ready, %d job thread%s", JPH_VERSION_MAJOR, JPH_VERSION_MINOR, JPH_VERSION_PATCH, threads, (threads == 1) ? "" : "s");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -2868,15 +2894,17 @@ void physicsSetGravity(Vec3T gravity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Once per frame, after animation: kinematic bodies are moved to their nodes, the world steps at
|
// Once per frame, after animation: records whose nodes are gone are released, kinematic bodies
|
||||||
// a fixed rate for the time that has passed (at most a few steps, and none while the game is
|
// are moved to their nodes, the world steps at a fixed rate for the time that has passed (at most
|
||||||
// paused or physics is disabled), and dynamic bodies drive their nodes.
|
// a few steps, and none while the game is paused or physics is disabled), and dynamic bodies
|
||||||
|
// drive their nodes.
|
||||||
void physicsUpdate(bool advance) {
|
void physicsUpdate(bool advance) {
|
||||||
uint64_t now;
|
uint64_t now;
|
||||||
|
|
||||||
if (_world == nullptr) {
|
if (_world == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_releaseDead();
|
||||||
now = SDL_GetTicksNS();
|
now = SDL_GetTicksNS();
|
||||||
if (advance && _world->enabled && (_world->lastTick != 0)) {
|
if (advance && _world->enabled && (_world->lastTick != 0)) {
|
||||||
_world->accumulator += (double)(now - _world->lastTick) / 1e9;
|
_world->accumulator += (double)(now - _world->lastTick) / 1e9;
|
||||||
|
|
@ -3190,7 +3218,7 @@ bool playerSetVelocity(int32_t node, Vec3T velocity) {
|
||||||
// Physics takes over the skeleton from where the animation left it.
|
// Physics takes over the skeleton from where the animation left it.
|
||||||
bool ragdollActivate(int32_t node) {
|
bool ragdollActivate(int32_t node) {
|
||||||
RagdollRecordT *record = _findRagdoll(node);
|
RagdollRecordT *record = _findRagdoll(node);
|
||||||
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
JPH::BodyInterface *bodies;
|
||||||
const int32_t *joints;
|
const int32_t *joints;
|
||||||
int32_t jointCount;
|
int32_t jointCount;
|
||||||
int32_t p;
|
int32_t p;
|
||||||
|
|
@ -3201,6 +3229,7 @@ bool ragdollActivate(int32_t node) {
|
||||||
if (record->active) {
|
if (record->active) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
bodies = &_world->system->GetBodyInterface();
|
||||||
animationStop(record->node, ANIMATION_ALL_LAYERS);
|
animationStop(record->node, ANIMATION_ALL_LAYERS);
|
||||||
sceneUpdateTransforms();
|
sceneUpdateTransforms();
|
||||||
jointCount = nodeGetSkinJoints(record->skinned, &joints);
|
jointCount = nodeGetSkinJoints(record->skinned, &joints);
|
||||||
|
|
@ -3264,7 +3293,7 @@ bool ragdollActivate(int32_t node) {
|
||||||
settings.mLinearDamping = RAGDOLL_LINEAR_DAMPING;
|
settings.mLinearDamping = RAGDOLL_LINEAR_DAMPING;
|
||||||
settings.mAngularDamping = RAGDOLL_ANGULAR_DAMPING;
|
settings.mAngularDamping = RAGDOLL_ANGULAR_DAMPING;
|
||||||
settings.mCollisionGroup = JPH::CollisionGroup(record->filter, _world->ragdollGroups, (JPH::CollisionGroup::SubGroupID)p);
|
settings.mCollisionGroup = JPH::CollisionGroup(record->filter, _world->ragdollGroups, (JPH::CollisionGroup::SubGroupID)p);
|
||||||
part->body = bodies.CreateAndAddBody(settings, JPH::EActivation::Activate);
|
part->body = bodies->CreateAndAddBody(settings, JPH::EActivation::Activate);
|
||||||
}
|
}
|
||||||
if (part->body.IsInvalid()) {
|
if (part->body.IsInvalid()) {
|
||||||
_releaseRagdollBodies(record);
|
_releaseRagdollBodies(record);
|
||||||
|
|
@ -3291,8 +3320,8 @@ bool ragdollActivate(int32_t node) {
|
||||||
}
|
}
|
||||||
parent = &record->parts[part->parent];
|
parent = &record->parts[part->parent];
|
||||||
record->filter->DisableCollision((JPH::CollisionGroup::SubGroupID)p, (JPH::CollisionGroup::SubGroupID)part->parent);
|
record->filter->DisableCollision((JPH::CollisionGroup::SubGroupID)p, (JPH::CollisionGroup::SubGroupID)part->parent);
|
||||||
bodies.GetPositionAndRotation(part->body, pivot, rotation);
|
bodies->GetPositionAndRotation(part->body, pivot, rotation);
|
||||||
parentRotation = bodies.GetRotation(parent->body);
|
parentRotation = bodies->GetRotation(parent->body);
|
||||||
pivot = pivot + rotation * part->offsetPosition;
|
pivot = pivot + rotation * part->offsetPosition;
|
||||||
twist = rotation * JPH::Vec3::sAxisY();
|
twist = rotation * JPH::Vec3::sAxisY();
|
||||||
plane = twist.GetNormalizedPerpendicular();
|
plane = twist.GetNormalizedPerpendicular();
|
||||||
|
|
@ -3542,12 +3571,14 @@ bool softNew(int32_t node, SoftKindE kind) {
|
||||||
if ((_world == nullptr) || !nodeValid(node) || (kind == SOFT_ROPE)) {
|
if ((_world == nullptr) || !nodeValid(node) || (kind == SOFT_ROPE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Any soft body already here goes first: a rope's tube mesh goes with it, so the geometry is
|
||||||
|
// read only afterwards.
|
||||||
|
softDelete(node);
|
||||||
mesh = nodeGetMesh(node);
|
mesh = nodeGetMesh(node);
|
||||||
if (!meshGetGeometry(mesh, &positions, &vertexCount, &indices, &indexCount) || (vertexCount < 3)) {
|
if (!meshGetGeometry(mesh, &positions, &vertexCount, &indices, &indexCount) || (vertexCount < 3)) {
|
||||||
utilTrace("Physics: node %d needs a mesh to become a soft body.", node);
|
utilTrace("Physics: node %d needs a mesh to become a soft body.", node);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
softDelete(node);
|
|
||||||
for (x = 0; x < _world->softCount; x++) {
|
for (x = 0; x < _world->softCount; x++) {
|
||||||
if (!_world->softs[x].used) {
|
if (!_world->softs[x].used) {
|
||||||
record = &_world->softs[x];
|
record = &_world->softs[x];
|
||||||
|
|
|
||||||
114
src/scene.c
114
src/scene.c
|
|
@ -112,6 +112,9 @@
|
||||||
#define CASCADE_NEAR 0.01f
|
#define CASCADE_NEAR 0.01f
|
||||||
#define SKIN_BOUNDS_GROW 1.5f // A skinned mesh moves beyond its bind pose
|
#define SKIN_BOUNDS_GROW 1.5f // A skinned mesh moves beyond its bind pose
|
||||||
#define MORPH_FLOATS 8 // Per target per vertex: position delta xyz + pad, normal delta xyz + pad
|
#define MORPH_FLOATS 8 // Per target per vertex: position delta xyz + pad, normal delta xyz + pad
|
||||||
|
#define MORPH_INPUT_FLOATS 6 // Per target per vertex as meshSetMorphTargets takes them: position delta xyz, normal delta xyz
|
||||||
|
#define FNV_OFFSET 1469598103934665603ULL // FNV-1a, for the shadow cache fingerprints
|
||||||
|
#define FNV_PRIME 1099511628211ULL
|
||||||
#define BILLBOARD_LANE 15 // The normal matrix's spare lane (the shader's _m33) carrying a draw's BILLBOARD_MODE_*
|
#define BILLBOARD_LANE 15 // The normal matrix's spare lane (the shader's _m33) carrying a draw's BILLBOARD_MODE_*
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,7 +146,7 @@ typedef struct LightUniformS {
|
||||||
// What every draw in a pass shares.
|
// What every draw in a pass shares.
|
||||||
typedef struct FragmentUniformsS {
|
typedef struct FragmentUniformsS {
|
||||||
float cameraPosition[4];
|
float cameraPosition[4];
|
||||||
float cameraForward[4];
|
float cameraForward[4]; // xyz; w = 1 when the cascades were fitted to this camera (the window's)
|
||||||
float ambient[4];
|
float ambient[4];
|
||||||
float counts[4];
|
float counts[4];
|
||||||
float shadowParams[4];
|
float shadowParams[4];
|
||||||
|
|
@ -202,6 +205,7 @@ typedef struct MeshS {
|
||||||
float sizeX;
|
float sizeX;
|
||||||
float sizeY;
|
float sizeY;
|
||||||
float sizeZ;
|
float sizeZ;
|
||||||
|
uint32_t version; // Stamped from the scene's mesh counter whenever the geometry changes (_cubeHash)
|
||||||
bool skinned;
|
bool skinned;
|
||||||
bool used;
|
bool used;
|
||||||
} MeshT;
|
} MeshT;
|
||||||
|
|
@ -530,6 +534,7 @@ typedef struct SceneS {
|
||||||
SDL_GPUGraphicsPipeline *shadowPipelines[SHADOW_PIPELINES];
|
SDL_GPUGraphicsPipeline *shadowPipelines[SHADOW_PIPELINES];
|
||||||
ShadowCacheT shadowCache[MAX_SHADOWS];
|
ShadowCacheT shadowCache[MAX_SHADOWS];
|
||||||
uint32_t shadowMapsVersion; // Bumped whenever the map array is (re)made
|
uint32_t shadowMapsVersion; // Bumped whenever the map array is (re)made
|
||||||
|
uint32_t meshVersion; // The last stamp given to a mesh's contents (MeshT.version)
|
||||||
SDL_GPUSampler *sampler;
|
SDL_GPUSampler *sampler;
|
||||||
SDL_GPUSampler *shadowSampler;
|
SDL_GPUSampler *shadowSampler;
|
||||||
SDL_GPUSampler *nearestSampler; // For FILTER_NEAREST materials
|
SDL_GPUSampler *nearestSampler; // For FILTER_NEAREST materials
|
||||||
|
|
@ -640,6 +645,7 @@ static void _materialDefaults(MaterialT *material);
|
||||||
static void _materialPlace(MaterialT *material, MaterialMapE map, SDL_GPUTexture *texture, float strength);
|
static void _materialPlace(MaterialT *material, MaterialMapE map, SDL_GPUTexture *texture, float strength);
|
||||||
static SDL_GPUTexture *_materialTexture(const MaterialT *material);
|
static SDL_GPUTexture *_materialTexture(const MaterialT *material);
|
||||||
static uint32_t _mipLevels(int32_t width, int32_t height);
|
static uint32_t _mipLevels(int32_t width, int32_t height);
|
||||||
|
static Mat4T _modelOf(const NodeT *node);
|
||||||
static void _orderBlended(Vec3T eye, int32_t drawCount);
|
static void _orderBlended(Vec3T eye, int32_t drawCount);
|
||||||
static ParticleTexturesT *_particleTextures(const EmitterViewT *view);
|
static ParticleTexturesT *_particleTextures(const EmitterViewT *view);
|
||||||
static SDL_GPUTextureFormat _pickDepthFormat(const SDL_GPUTextureFormat *wanted, int32_t count, SDL_GPUTextureUsageFlags usage);
|
static SDL_GPUTextureFormat _pickDepthFormat(const SDL_GPUTextureFormat *wanted, int32_t count, SDL_GPUTextureUsageFlags usage);
|
||||||
|
|
@ -663,6 +669,7 @@ static bool _stageBegin(StagingT *staging, uint32_t bytes);
|
||||||
static bool _stageCopy(StagingT *staging);
|
static bool _stageCopy(StagingT *staging);
|
||||||
static void _stageEnd(StagingT *staging, SDL_GPUTexture *mipmaps);
|
static void _stageEnd(StagingT *staging, SDL_GPUTexture *mipmaps);
|
||||||
static void _stageTexture(const StagingT *staging, uint32_t offset, SDL_GPUTexture *texture, uint32_t level, uint32_t layer, uint32_t width, uint32_t height);
|
static void _stageTexture(const StagingT *staging, uint32_t offset, SDL_GPUTexture *texture, uint32_t level, uint32_t layer, uint32_t width, uint32_t height);
|
||||||
|
static void _stampMesh(MeshT *mesh);
|
||||||
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible);
|
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible);
|
||||||
static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size);
|
static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size);
|
||||||
static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb);
|
static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb);
|
||||||
|
|
@ -757,6 +764,7 @@ static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const
|
||||||
mesh->boundsMin = vec3Subtract(centre, half);
|
mesh->boundsMin = vec3Subtract(centre, half);
|
||||||
mesh->boundsMax = vec3Add(centre, half);
|
mesh->boundsMax = vec3Add(centre, half);
|
||||||
}
|
}
|
||||||
|
_stampMesh(mesh);
|
||||||
return (int32_t)(mesh - _scene.meshes);
|
return (int32_t)(mesh - _scene.meshes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -889,13 +897,14 @@ static void _boundDraws(int32_t drawCount) {
|
||||||
for (x = 0; x < drawCount; x++) {
|
for (x = 0; x < drawCount; x++) {
|
||||||
NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
||||||
MeshT *mesh = &_scene.meshes[node->mesh];
|
MeshT *mesh = &_scene.meshes[node->mesh];
|
||||||
|
Mat4T model = _modelOf(node);
|
||||||
Vec3T drawMin = vec3(0.0f, 0.0f, 0.0f);
|
Vec3T drawMin = vec3(0.0f, 0.0f, 0.0f);
|
||||||
Vec3T drawMax = vec3(0.0f, 0.0f, 0.0f);
|
Vec3T drawMax = vec3(0.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
for (c = 0; c < 8; c++) {
|
for (c = 0; c < 8; c++) {
|
||||||
Vec3T corner = vec3((c & 1) ? mesh->boundsMax.x : mesh->boundsMin.x, (c & 2) ? mesh->boundsMax.y : mesh->boundsMin.y, (c & 4) ? mesh->boundsMax.z : mesh->boundsMin.z);
|
Vec3T corner = vec3((c & 1) ? mesh->boundsMax.x : mesh->boundsMin.x, (c & 2) ? mesh->boundsMax.y : mesh->boundsMin.y, (c & 4) ? mesh->boundsMax.z : mesh->boundsMin.z);
|
||||||
|
|
||||||
corner = mat4TransformPoint(node->world, corner);
|
corner = mat4TransformPoint(model, corner);
|
||||||
if (c == 0) {
|
if (c == 0) {
|
||||||
drawMin = corner;
|
drawMin = corner;
|
||||||
drawMax = corner;
|
drawMax = corner;
|
||||||
|
|
@ -1416,20 +1425,20 @@ static bool _createSkyPipeline(int32_t sampleSet) {
|
||||||
|
|
||||||
|
|
||||||
// A fingerprint of everything a point light's faces depend on: the light, its range, and every
|
// A fingerprint of everything a point light's faces depend on: the light, its range, and every
|
||||||
// caster's transform; skinned and morphing casters count as always changed.
|
// caster's transform, mesh and mesh contents; skinned and morphing casters count as always changed.
|
||||||
static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) {
|
static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) {
|
||||||
uint64_t hash = 1469598103934665603ULL;
|
uint64_t hash = FNV_OFFSET;
|
||||||
int32_t x;
|
int32_t x;
|
||||||
int32_t b;
|
int32_t b;
|
||||||
const uint8_t *bytes;
|
const uint8_t *bytes;
|
||||||
|
|
||||||
bytes = (const uint8_t *)&_scene.nodes[shadow->node].world;
|
bytes = (const uint8_t *)&_scene.nodes[shadow->node].world;
|
||||||
for (b = 0; b < (int32_t)sizeof(Mat4T); b++) {
|
for (b = 0; b < (int32_t)sizeof(Mat4T); b++) {
|
||||||
hash = (hash ^ bytes[b]) * 1099511628211ULL;
|
hash = (hash ^ bytes[b]) * FNV_PRIME;
|
||||||
}
|
}
|
||||||
bytes = (const uint8_t *)&shadow->far;
|
bytes = (const uint8_t *)&shadow->far;
|
||||||
for (b = 0; b < (int32_t)sizeof(float); b++) {
|
for (b = 0; b < (int32_t)sizeof(float); b++) {
|
||||||
hash = (hash ^ bytes[b]) * 1099511628211ULL;
|
hash = (hash ^ bytes[b]) * FNV_PRIME;
|
||||||
}
|
}
|
||||||
for (x = 0; x < drawCount; x++) {
|
for (x = 0; x < drawCount; x++) {
|
||||||
NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
||||||
|
|
@ -1441,10 +1450,12 @@ static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) {
|
||||||
if (_isSkinned(node, mesh) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) {
|
if (_isSkinned(node, mesh) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
hash = (hash ^ (uint64_t)(uint32_t)_scene.draws[x].node) * 1099511628211ULL;
|
hash = (hash ^ (uint64_t)(uint32_t)_scene.draws[x].node) * FNV_PRIME;
|
||||||
|
hash = (hash ^ (uint64_t)(uint32_t)node->mesh) * FNV_PRIME;
|
||||||
|
hash = (hash ^ (uint64_t)mesh->version) * FNV_PRIME;
|
||||||
bytes = (const uint8_t *)&node->world;
|
bytes = (const uint8_t *)&node->world;
|
||||||
for (b = 0; b < (int32_t)sizeof(Mat4T); b++) {
|
for (b = 0; b < (int32_t)sizeof(Mat4T); b++) {
|
||||||
hash = (hash ^ bytes[b]) * 1099511628211ULL;
|
hash = (hash ^ bytes[b]) * FNV_PRIME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (hash == 0) ? 1 : hash;
|
return (hash == 0) ? 1 : hash;
|
||||||
|
|
@ -2086,14 +2097,8 @@ static void _fillInstances(int32_t drawCount) {
|
||||||
}
|
}
|
||||||
for (x = 0; x < drawCount; x++) {
|
for (x = 0; x < drawCount; x++) {
|
||||||
const NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
const NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
||||||
Mat4T model = node->world;
|
Mat4T model = _modelOf(node);
|
||||||
|
|
||||||
// A sprite's quad is a unit square: its size goes in here so the node's own scale stays free.
|
|
||||||
if (node->spriteSlot != NO_HANDLE) {
|
|
||||||
const SpriteNodeT *sprite = &_scene.spriteNodes[node->spriteSlot];
|
|
||||||
|
|
||||||
model = mat4Multiply(model, mat4Compose(vec3(0.0f, 0.0f, 0.0f), quatIdentity(), vec3(sprite->width, sprite->height, 1.0f)));
|
|
||||||
}
|
|
||||||
// The shader makes a billboard's normal matrix from the axes it turns to.
|
// The shader makes a billboard's normal matrix from the axes it turns to.
|
||||||
_scene.instances[x].model = model;
|
_scene.instances[x].model = model;
|
||||||
_scene.instances[x].normal = (node->billboard == BILLBOARD_NONE) ? mat4NormalMatrix(model) : mat4Identity();
|
_scene.instances[x].normal = (node->billboard == BILLBOARD_NONE) ? mat4NormalMatrix(model) : mat4Identity();
|
||||||
|
|
@ -2479,9 +2484,6 @@ static void _gatherParticles(Vec3T eye, Vec3T forward) {
|
||||||
order[e].depth = vec3Dot(vec3Subtract(origin, eye), forward);
|
order[e].depth = vec3Dot(vec3Subtract(origin, eye), forward);
|
||||||
order[e].index = e;
|
order[e].index = e;
|
||||||
total += views[e].count * (1 + SDL_max(views[e].trailLength - 1, 0));
|
total += views[e].count * (1 + SDL_max(views[e].trailLength - 1, 0));
|
||||||
if (views[e].softness > 0.0f) {
|
|
||||||
_scene.particleSoft = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (total == 0) {
|
if (total == 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2550,6 +2552,9 @@ static void _gatherParticles(Vec3T eye, Vec3T forward) {
|
||||||
_scene.particleRuns[_scene.particleRunCount].texture = textures->textures[frame];
|
_scene.particleRuns[_scene.particleRunCount].texture = textures->textures[frame];
|
||||||
_scene.particleRuns[_scene.particleRunCount].blend = view->blend;
|
_scene.particleRuns[_scene.particleRunCount].blend = view->blend;
|
||||||
_scene.particleRunCount++;
|
_scene.particleRunCount++;
|
||||||
|
if (view->softness > 0.0f) {
|
||||||
|
_scene.particleSoft = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2857,6 +2862,18 @@ static uint32_t _mipLevels(int32_t width, int32_t height) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A draw's model matrix: the node's world, with a sprite's size folded in (its quad is a unit
|
||||||
|
// square, so the node's own scale stays free). Bounds and instances both come from this.
|
||||||
|
static Mat4T _modelOf(const NodeT *node) {
|
||||||
|
if (node->spriteSlot != NO_HANDLE) {
|
||||||
|
const SpriteNodeT *sprite = &_scene.spriteNodes[node->spriteSlot];
|
||||||
|
|
||||||
|
return mat4Multiply(node->world, mat4Compose(vec3(0.0f, 0.0f, 0.0f), quatIdentity(), vec3(sprite->width, sprite->height, 1.0f)));
|
||||||
|
}
|
||||||
|
return node->world;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// The blended draws (those after the opaque ones) back to front from an eye, as the order the
|
// The blended draws (those after the opaque ones) back to front from an eye, as the order the
|
||||||
// camera's pass draws them in; the draws and their instances stay where they are.
|
// camera's pass draws them in; the draws and their instances stay where they are.
|
||||||
static void _orderBlended(Vec3T eye, int32_t drawCount) {
|
static void _orderBlended(Vec3T eye, int32_t drawCount) {
|
||||||
|
|
@ -2876,7 +2893,9 @@ static void _orderBlended(Vec3T eye, int32_t drawCount) {
|
||||||
_scene.blendedOrder[x].index = _scene.opaqueCount + x;
|
_scene.blendedOrder[x].index = _scene.opaqueCount + x;
|
||||||
_scene.blendedOrder[x].depth = vec3Length(vec3Subtract(vec3(m[12], m[13], m[14]), eye));
|
_scene.blendedOrder[x].depth = vec3Length(vec3Subtract(vec3(m[12], m[13], m[14]), eye));
|
||||||
}
|
}
|
||||||
|
if (count > 0) {
|
||||||
qsort(_scene.blendedOrder, (size_t)count, sizeof(DepthOrderT), _compareDepthOrder);
|
qsort(_scene.blendedOrder, (size_t)count, sizeof(DepthOrderT), _compareDepthOrder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3101,6 +3120,7 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr
|
||||||
uniforms->cameraForward[0] = frame->forward.x;
|
uniforms->cameraForward[0] = frame->forward.x;
|
||||||
uniforms->cameraForward[1] = frame->forward.y;
|
uniforms->cameraForward[1] = frame->forward.y;
|
||||||
uniforms->cameraForward[2] = frame->forward.z;
|
uniforms->cameraForward[2] = frame->forward.z;
|
||||||
|
uniforms->cameraForward[3] = frame->main ? 1.0f : 0.0f;
|
||||||
_gatherParticles(frame->eye, frame->forward);
|
_gatherParticles(frame->eye, frame->forward);
|
||||||
_uploadParticles(commands);
|
_uploadParticles(commands);
|
||||||
_cullDraws(&frame->viewProjection, drawCount, culled);
|
_cullDraws(&frame->viewProjection, drawCount, culled);
|
||||||
|
|
@ -3116,8 +3136,9 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Soft particles need the camera's depth before the main pass: a depth-only pass of every
|
// Soft particles need the camera's depth before the main pass: a depth-only pass of every
|
||||||
// opaque mesh into the single-sample copy the particle shader samples.
|
// opaque mesh into the single-sample copy the particle shader samples. With nothing to draw
|
||||||
if (_scene.particleSoft && (frame->softDepth != NULL) && (drawCount > 0)) {
|
// the clear alone matters: the copy must not hold last frame's (or no) depth when sampled.
|
||||||
|
if (_scene.particleSoft && (frame->softDepth != NULL)) {
|
||||||
memset(&depth, 0, sizeof(depth));
|
memset(&depth, 0, sizeof(depth));
|
||||||
depth.texture = frame->softDepth;
|
depth.texture = frame->softDepth;
|
||||||
depth.clear_depth = 1.0f;
|
depth.clear_depth = 1.0f;
|
||||||
|
|
@ -3312,7 +3333,9 @@ static bool *_skipScratch(int32_t drawCount) {
|
||||||
utilDie("Out of memory culling the scene.");
|
utilDie("Out of memory culling the scene.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (drawCount > 0) {
|
||||||
memset(_scene.skip, 0, sizeof(bool) * (size_t)drawCount);
|
memset(_scene.skip, 0, sizeof(bool) * (size_t)drawCount);
|
||||||
|
}
|
||||||
return _scene.skip;
|
return _scene.skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3405,6 +3428,13 @@ static void _stageTexture(const StagingT *staging, uint32_t offset, SDL_GPUTextu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Marks a mesh's contents as changed, so caches keyed on them (_cubeHash) miss.
|
||||||
|
static void _stampMesh(MeshT *mesh) {
|
||||||
|
_scene.meshVersion++;
|
||||||
|
mesh->version = _scene.meshVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// World matrices and inherited visibility, depth first.
|
// World matrices and inherited visibility, depth first.
|
||||||
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) {
|
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) {
|
||||||
NodeT *n = &_scene.nodes[node];
|
NodeT *n = &_scene.nodes[node];
|
||||||
|
|
@ -4330,29 +4360,34 @@ int32_t meshPlane(float width, float depth) {
|
||||||
bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names) {
|
bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names) {
|
||||||
MeshT *m;
|
MeshT *m;
|
||||||
float *packed;
|
float *packed;
|
||||||
int32_t count;
|
size_t count;
|
||||||
|
size_t v;
|
||||||
int32_t x;
|
int32_t x;
|
||||||
|
|
||||||
if (!meshValid(mesh) || (deltas == NULL) || (targetCount <= 0)) {
|
if (!meshValid(mesh) || (deltas == NULL) || (targetCount <= 0)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m = &_scene.meshes[mesh];
|
m = &_scene.meshes[mesh];
|
||||||
count = targetCount * m->vertexCount;
|
count = (size_t)targetCount * (size_t)m->vertexCount;
|
||||||
packed = SDL_calloc((size_t)count * MORPH_FLOATS, sizeof(float));
|
// The GPU buffer is sized in 32 bits.
|
||||||
|
if (count > UINT32_MAX / (MORPH_FLOATS * sizeof(float))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
packed = SDL_calloc(count * MORPH_FLOATS, sizeof(float));
|
||||||
if (packed == NULL) {
|
if (packed == NULL) {
|
||||||
utilDie("Out of memory packing morph targets.");
|
utilDie("Out of memory packing morph targets.");
|
||||||
}
|
}
|
||||||
// float4 pairs for the shader: xyz0 position delta, xyz0 normal delta.
|
// float4 pairs for the shader: xyz0 position delta, xyz0 normal delta.
|
||||||
for (x = 0; x < count; x++) {
|
for (v = 0; v < count; v++) {
|
||||||
packed[x * MORPH_FLOATS] = deltas[x * 6];
|
packed[v * MORPH_FLOATS] = deltas[v * MORPH_INPUT_FLOATS];
|
||||||
packed[x * MORPH_FLOATS + 1] = deltas[x * 6 + 1];
|
packed[v * MORPH_FLOATS + 1] = deltas[v * MORPH_INPUT_FLOATS + 1];
|
||||||
packed[x * MORPH_FLOATS + 2] = deltas[x * 6 + 2];
|
packed[v * MORPH_FLOATS + 2] = deltas[v * MORPH_INPUT_FLOATS + 2];
|
||||||
packed[x * MORPH_FLOATS + 4] = deltas[x * 6 + 3];
|
packed[v * MORPH_FLOATS + 4] = deltas[v * MORPH_INPUT_FLOATS + 3];
|
||||||
packed[x * MORPH_FLOATS + 5] = deltas[x * 6 + 4];
|
packed[v * MORPH_FLOATS + 5] = deltas[v * MORPH_INPUT_FLOATS + 4];
|
||||||
packed[x * MORPH_FLOATS + 6] = deltas[x * 6 + 5];
|
packed[v * MORPH_FLOATS + 6] = deltas[v * MORPH_INPUT_FLOATS + 5];
|
||||||
}
|
}
|
||||||
_freeMorphs(m);
|
_freeMorphs(m);
|
||||||
m->morphBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, packed, (uint32_t)((size_t)count * MORPH_FLOATS * sizeof(float)));
|
m->morphBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, packed, (uint32_t)(count * MORPH_FLOATS * sizeof(float)));
|
||||||
SDL_free(packed);
|
SDL_free(packed);
|
||||||
if (m->morphBuffer == NULL) {
|
if (m->morphBuffer == NULL) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -4365,6 +4400,7 @@ bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount,
|
||||||
m->morphNames[x] = ((names != NULL) && (names[x] != NULL)) ? SDL_strdup(names[x]) : NULL;
|
m->morphNames[x] = ((names != NULL) && (names[x] != NULL)) ? SDL_strdup(names[x]) : NULL;
|
||||||
}
|
}
|
||||||
m->morphCount = targetCount;
|
m->morphCount = targetCount;
|
||||||
|
_stampMesh(m);
|
||||||
for (x = 0; x < _scene.nodeCount; x++) {
|
for (x = 0; x < _scene.nodeCount; x++) {
|
||||||
if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) {
|
if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) {
|
||||||
_matchMorphWeights(&_scene.nodes[x]);
|
_matchMorphWeights(&_scene.nodes[x]);
|
||||||
|
|
@ -4406,6 +4442,7 @@ bool meshSetPositions(int32_t mesh, const float *positions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sceneComputeNormals(m->vertices, m->vertexCount, m->indices, (int32_t)m->indexCount);
|
sceneComputeNormals(m->vertices, m->vertexCount, m->indices, (int32_t)m->indexCount);
|
||||||
|
_stampMesh(m);
|
||||||
if (_scene.device == NULL) {
|
if (_scene.device == NULL) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -4823,6 +4860,9 @@ bool nodeSetMaterial(int32_t node, int32_t material) {
|
||||||
if (!nodeValid(node)) {
|
if (!nodeValid(node)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if ((material != NO_HANDLE) && !materialValid(material)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
_scene.nodes[node].material = material;
|
_scene.nodes[node].material = material;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -5377,11 +5417,18 @@ bool sceneIsEnabled(void) {
|
||||||
|
|
||||||
|
|
||||||
// World point to overlay coordinates using the last rendered frame's camera. Returns false when
|
// World point to overlay coordinates using the last rendered frame's camera. Returns false when
|
||||||
// the point is behind the camera (x and y are still filled in).
|
// the point is behind the camera (x and y are still filled in) or there is no target yet (zeros).
|
||||||
bool sceneProject(Vec3T world, float *x, float *y, float *depth) {
|
bool sceneProject(Vec3T world, float *x, float *y, float *depth) {
|
||||||
float w;
|
float w;
|
||||||
Vec3T clip = mat4Project(_scene.viewProjection, world, &w);
|
Vec3T clip;
|
||||||
|
|
||||||
|
if ((_scene.width <= 0) || (_scene.height <= 0)) {
|
||||||
|
*x = 0.0f;
|
||||||
|
*y = 0.0f;
|
||||||
|
*depth = 0.0f;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
clip = mat4Project(_scene.viewProjection, world, &w);
|
||||||
*x = (clip.x + 1.0f) / 2.0f * (float)_scene.width;
|
*x = (clip.x + 1.0f) / 2.0f * (float)_scene.width;
|
||||||
*y = (1.0f - clip.y) / 2.0f * (float)_scene.height;
|
*y = (1.0f - clip.y) / 2.0f * (float)_scene.height;
|
||||||
*depth = clip.z;
|
*depth = clip.z;
|
||||||
|
|
@ -5553,6 +5600,7 @@ SDL_Texture *sceneRender(void) {
|
||||||
NodeT *node;
|
NodeT *node;
|
||||||
|
|
||||||
if (!_scene.enabled || (_scene.colour == NULL)) {
|
if (!_scene.enabled || (_scene.colour == NULL)) {
|
||||||
|
_scene.lineVertexCount = 0;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
// Transforms, the window's camera and the lights for this frame.
|
// Transforms, the window's camera and the lights for this frame.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
#include "sceneShared.h"
|
#include "sceneShared.h"
|
||||||
|
|
||||||
#define CASCADE_BLEND 0.1 // The fraction of each cascade over which the next blends in
|
#define CASCADE_BLEND 0.1 // The fraction of each cascade over which the next blends in
|
||||||
|
#define CASCADE_EDGE 0.9 // How far across a cascade's map (0 = centre, 1 = edge) a view still uses it before the next
|
||||||
#define NORMAL_OFFSET_TEXELS 8.0 // How far along the normal a shadow lookup steps, in depth bias units
|
#define NORMAL_OFFSET_TEXELS 8.0 // How far along the normal a shadow lookup steps, in depth bias units
|
||||||
#define CONE_EDGE_MIN 0.0001 // The narrowest spot cone edge (in cosine), keeping smoothstep defined
|
#define CONE_EDGE_MIN 0.0001 // The narrowest spot cone edge (in cosine), keeping smoothstep defined
|
||||||
#define PI 3.14159265
|
#define PI 3.14159265
|
||||||
|
|
@ -213,7 +214,7 @@ struct Light {
|
||||||
// What every draw in a pass shares: the camera, the lights and their shadows, the sky and fog.
|
// What every draw in a pass shares: the camera, the lights and their shadows, the sky and fog.
|
||||||
cbuffer FragmentUniforms : register(b0, space3) {
|
cbuffer FragmentUniforms : register(b0, space3) {
|
||||||
float4 cameraPosition;
|
float4 cameraPosition;
|
||||||
float4 cameraForward;
|
float4 cameraForward; // xyz; w = 1 when the cascades were fitted to this camera (the window's)
|
||||||
float4 ambient;
|
float4 ambient;
|
||||||
float4 counts; // x = light count
|
float4 counts; // x = light count
|
||||||
float4 shadowParams; // x = depth bias, y = 1 / map size, z = shadow count
|
float4 shadowParams; // x = depth bias, y = 1 / map size, z = shadow count
|
||||||
|
|
@ -254,6 +255,16 @@ TextureCube<float4> skyCube : register(t6, space2); // The sky, or a b
|
||||||
SamplerState skyCubeSampler : register(s6, space2);
|
SamplerState skyCubeSampler : register(s6, space2);
|
||||||
|
|
||||||
|
|
||||||
|
// Whether a cascade's map holds a point, short of its edge by the margin a view keeps so the
|
||||||
|
// filter's taps stay on the map.
|
||||||
|
bool cascadeHolds(int slot, int cascade, float3 worldPosition) {
|
||||||
|
float4 lightSpace = mul(shadowMatrix[slot * MAX_CASCADES + cascade], float4(worldPosition, 1.0));
|
||||||
|
float2 edge = abs(lightSpace.xy / lightSpace.w);
|
||||||
|
|
||||||
|
return max(edge.x, edge.y) <= CASCADE_EDGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// How lit a point is by a directional or spot light's shadow map: its depth from the light against
|
// How lit a point is by a directional or spot light's shadow map: its depth from the light against
|
||||||
// the map, averaged over a 3x3 block of texels so edges soften. Points outside the map are lit.
|
// the map, averaged over a 3x3 block of texels so edges soften. Points outside the map are lit.
|
||||||
float shadowFactorMap(int slot, int cascade, float3 worldPosition, float3 normal, float3 toLight) {
|
float shadowFactorMap(int slot, int cascade, float3 worldPosition, float3 normal, float3 toLight) {
|
||||||
|
|
@ -535,14 +546,17 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
|
||||||
if (shadowInfo[slot].x == SHADOW_CUBE) {
|
if (shadowInfo[slot].x == SHADOW_CUBE) {
|
||||||
attenuation *= shadowFactorCube(slot, input.worldPosition, normal, lights[x].positionType.xyz);
|
attenuation *= shadowFactorCube(slot, input.worldPosition, normal, lights[x].positionType.xyz);
|
||||||
} else if (shadowInfo[slot].x == SHADOW_CASCADE) {
|
} else if (shadowInfo[slot].x == SHADOW_CASCADE) {
|
||||||
// The cascade by view depth, blending into the next over the end of each.
|
int count = (int)shadowInfo[slot].z;
|
||||||
|
float factor;
|
||||||
|
|
||||||
|
if (cameraForward.w > 0.5) {
|
||||||
|
// The camera the cascades were fitted to: the cascade by view depth, blending
|
||||||
|
// into the next over the end of each.
|
||||||
float viewDepth = dot(input.worldPosition - cameraPosition.xyz, cameraForward.xyz);
|
float viewDepth = dot(input.worldPosition - cameraPosition.xyz, cameraForward.xyz);
|
||||||
float4 splits = cascadeSplits[slot];
|
float4 splits = cascadeSplits[slot];
|
||||||
int count = (int)shadowInfo[slot].z;
|
|
||||||
int cascade = (viewDepth > splits.x ? 1 : 0) + (viewDepth > splits.y ? 1 : 0) + (viewDepth > splits.z ? 1 : 0);
|
int cascade = (viewDepth > splits.x ? 1 : 0) + (viewDepth > splits.y ? 1 : 0) + (viewDepth > splits.z ? 1 : 0);
|
||||||
float splitEnd;
|
float splitEnd;
|
||||||
float blendStart;
|
float blendStart;
|
||||||
float factor;
|
|
||||||
|
|
||||||
cascade = min(cascade, count - 1);
|
cascade = min(cascade, count - 1);
|
||||||
splitEnd = (cascade == 0) ? splits.x : ((cascade == 1) ? splits.y : ((cascade == 2) ? splits.z : splits.w));
|
splitEnd = (cascade == 0) ? splits.x : ((cascade == 1) ? splits.y : ((cascade == 2) ? splits.z : splits.w));
|
||||||
|
|
@ -551,6 +565,23 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
|
||||||
if ((cascade + 1 < count) && (viewDepth > blendStart)) {
|
if ((cascade + 1 < count) && (viewDepth > blendStart)) {
|
||||||
factor = lerp(factor, shadowFactorMap(slot, cascade + 1, input.worldPosition, normal, toLight), saturate((viewDepth - blendStart) / (splitEnd - blendStart)));
|
factor = lerp(factor, shadowFactorMap(slot, cascade + 1, input.worldPosition, normal, toLight), saturate((viewDepth - blendStart) / (splitEnd - blendStart)));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Another camera (a view): its own depth means nothing to those splits, so the
|
||||||
|
// finest cascade whose map holds the point, from the coarsest down. Written out
|
||||||
|
// because DXC crashes on loops inside a loop.
|
||||||
|
int cascade = count - 1;
|
||||||
|
|
||||||
|
if ((cascade > 2) && cascadeHolds(slot, 2, input.worldPosition)) {
|
||||||
|
cascade = 2;
|
||||||
|
}
|
||||||
|
if ((cascade > 1) && cascadeHolds(slot, 1, input.worldPosition)) {
|
||||||
|
cascade = 1;
|
||||||
|
}
|
||||||
|
if ((cascade > 0) && cascadeHolds(slot, 0, input.worldPosition)) {
|
||||||
|
cascade = 0;
|
||||||
|
}
|
||||||
|
factor = shadowFactorMap(slot, cascade, input.worldPosition, normal, toLight);
|
||||||
|
}
|
||||||
attenuation *= factor;
|
attenuation *= factor;
|
||||||
} else {
|
} else {
|
||||||
attenuation *= shadowFactorMap(slot, 0, input.worldPosition, normal, toLight);
|
attenuation *= shadowFactorMap(slot, 0, input.worldPosition, normal, toLight);
|
||||||
|
|
|
||||||
530
src/singe.c
530
src/singe.c
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3_image/SDL_image.h>
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
|
@ -75,15 +76,17 @@ LSEC_API int luaopen_ssl_config(lua_State *L);
|
||||||
|
|
||||||
// soundSetVolume/soundGetVolume use this legacy scale; the mixer uses MIX_MAX_VOLUME.
|
// soundSetVolume/soundGetVolume use this legacy scale; the mixer uses MIX_MAX_VOLUME.
|
||||||
#define AUDIO_MAX_VOLUME 63
|
#define AUDIO_MAX_VOLUME 63
|
||||||
|
#define GAMES_DAT_LEVELS 4 // Directories, the script's and above, searched for its games.dat
|
||||||
#define MAX_MICE 4
|
#define MAX_MICE 4
|
||||||
#define MOUSE_AXIS_COUNT 2
|
#define MOUSE_AXIS_COUNT 2
|
||||||
#define MAX_CONTROLLERS 4
|
#define MAX_CONTROLLERS 4
|
||||||
#define CONTROLLER_AXIS_COUNT 6
|
#define CONTROLLER_AXIS_COUNT 6
|
||||||
#define CONTROLLER_BUTTON_COUNT 15
|
#define CONTROLLER_BUTTON_COUNT 15
|
||||||
#define CONTROLLER_DEAD_ZONE_DEFAULT 15000
|
#define CONTROLLER_DEAD_ZONE_DEFAULT 15000
|
||||||
#define AXIS_COUNT (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + MAX_MICE * MOUSE_AXIS_COUNT)
|
#define AXIS_CONTROLLER_COUNT (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT) // Controller axis slots first, then the mice
|
||||||
|
#define AXIS_COUNT (AXIS_CONTROLLER_COUNT + MAX_MICE * MOUSE_AXIS_COUNT)
|
||||||
#define AXIS_INDEX_CONTROLLER(c, a) ((c) * CONTROLLER_AXIS_COUNT + (a))
|
#define AXIS_INDEX_CONTROLLER(c, a) ((c) * CONTROLLER_AXIS_COUNT + (a))
|
||||||
#define AXIS_INDEX_MOUSE(m, a) (MAX_CONTROLLERS * CONTROLLER_AXIS_COUNT + (m) * MOUSE_AXIS_COUNT + (a))
|
#define AXIS_INDEX_MOUSE(m, a) (AXIS_CONTROLLER_COUNT + (m) * MOUSE_AXIS_COUNT + (a))
|
||||||
|
|
||||||
// Input codes handed to scripts and controls.cfg. Framework.singe builds its tables from these.
|
// Input codes handed to scripts and controls.cfg. Framework.singe builds its tables from these.
|
||||||
#define CODE_GAMEPAD_BASE 500
|
#define CODE_GAMEPAD_BASE 500
|
||||||
|
|
@ -97,8 +100,9 @@ LSEC_API int luaopen_ssl_config(lua_State *L);
|
||||||
#define CODE_MOUSE_BUTTON_COUNT 5 // Left, right, middle, X1, X2
|
#define CODE_MOUSE_BUTTON_COUNT 5 // Left, right, middle, X1, X2
|
||||||
#define CODE_MOUSE_WHEEL_UP (CODE_MOUSE_BUTTON_COUNT)
|
#define CODE_MOUSE_WHEEL_UP (CODE_MOUSE_BUTTON_COUNT)
|
||||||
#define CODE_MOUSE_WHEEL_DOWN (CODE_MOUSE_BUTTON_COUNT + 1)
|
#define CODE_MOUSE_WHEEL_DOWN (CODE_MOUSE_BUTTON_COUNT + 1)
|
||||||
// Codes below the gamepad range are keyboard scancodes, so every key SDL defines must sit under it.
|
#define SCANCODE_DYNAMIC_COUNT 100 // SDL hands SDL_SCANCODE_RESERVED onward to keys it has no scancode for
|
||||||
SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE > SDL_SCANCODE_ENDCALL);
|
// Codes below the gamepad range are keyboard scancodes, so every scancode SDL can hand out must sit under it.
|
||||||
|
SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE >= SDL_SCANCODE_RESERVED + SCANCODE_DYNAMIC_COUNT);
|
||||||
|
|
||||||
#define FRAME_TICK_MS 15 // Minimum time between onOverlayUpdate calls
|
#define FRAME_TICK_MS 15 // Minimum time between onOverlayUpdate calls
|
||||||
#define IDLE_SLEEP_MS 1
|
#define IDLE_SLEEP_MS 1
|
||||||
|
|
@ -144,6 +148,9 @@ SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE > SDL_SCANCODE_ENDCAL
|
||||||
#define TRAIL_POINT_FLOATS 3 // x, y, z per recorded trail point
|
#define TRAIL_POINT_FLOATS 3 // x, y, z per recorded trail point
|
||||||
#define COLOR_KEY_VALUE 0
|
#define COLOR_KEY_VALUE 0
|
||||||
#define BLUE_SCREEN_BLUE 255
|
#define BLUE_SCREEN_BLUE 255
|
||||||
|
#define PLAYER_DISC -2 // materialSetVideo's "the disc": resolved every frame, since a frame file swaps players per segment
|
||||||
|
#define PACKED_PERMISSIONS "r--r--r--" // lfs.attributes on a packed entry: read only
|
||||||
|
#define PACKED_BLOCK_SIZE 512 // ... and the block size it reports
|
||||||
|
|
||||||
#define LOGO_FADE_STEPS 256
|
#define LOGO_FADE_STEPS 256
|
||||||
#define LOGO_MARGIN 40 // Pixels of breathing room around a logo in the logo space
|
#define LOGO_MARGIN 40 // Pixels of breathing room around a logo in the logo space
|
||||||
|
|
@ -394,6 +401,9 @@ typedef struct GlobalS {
|
||||||
int32_t pauseTextureHeight;
|
int32_t pauseTextureHeight;
|
||||||
double overlayScaleX; // Overlay size / video size
|
double overlayScaleX; // Overlay size / video size
|
||||||
double overlayScaleY;
|
double overlayScaleY;
|
||||||
|
SDL_FRect drawTarget; // Where the game is drawn, in video coordinates (Sinden border, overscan)
|
||||||
|
double drawScaleX; // Video size / drawTarget size: mouse positions map through it
|
||||||
|
double drawScaleY;
|
||||||
bool overlayDirty;
|
bool overlayDirty;
|
||||||
bool pauseState; // by RDG2010
|
bool pauseState; // by RDG2010
|
||||||
bool pauseEnabled; // by RDG2010
|
bool pauseEnabled; // by RDG2010
|
||||||
|
|
@ -497,9 +507,11 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base);
|
||||||
static void _callLua(const char *func, const char *sig, ...);
|
static void _callLua(const char *func, const char *sig, ...);
|
||||||
static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2);
|
static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2);
|
||||||
static int32_t _controllerSlot(SDL_JoystickID which);
|
static int32_t _controllerSlot(SDL_JoystickID which);
|
||||||
|
static int32_t _defaultEffectsVolume(void);
|
||||||
static bool _delayAndPump(uint32_t ms);
|
static bool _delayAndPump(uint32_t ms);
|
||||||
static void _deliverKey(bool down, int32_t keysym, int32_t scancode);
|
static void _deliverKey(bool down, int32_t keysym, int32_t scancode);
|
||||||
static int64_t _discGetFrame(void);
|
static int64_t _discGetFrame(void);
|
||||||
|
static void _discPark(void);
|
||||||
static void _discSeek(int64_t frame);
|
static void _discSeek(int64_t frame);
|
||||||
static void _doLogos(void);
|
static void _doLogos(void);
|
||||||
static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel);
|
static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel);
|
||||||
|
|
@ -520,6 +532,7 @@ static void _installFileHooks(lua_State *L);
|
||||||
static int32_t _lfsAttributes(lua_State *L);
|
static int32_t _lfsAttributes(lua_State *L);
|
||||||
static int32_t _lfsDir(lua_State *L);
|
static int32_t _lfsDir(lua_State *L);
|
||||||
static int32_t _lfsDirIterator(lua_State *L);
|
static int32_t _lfsDirIterator(lua_State *L);
|
||||||
|
static void _lfsFillAttributes(lua_State *L, bool directory, int64_t size, int64_t modified);
|
||||||
static int32_t _lfsMkdir(lua_State *L);
|
static int32_t _lfsMkdir(lua_State *L);
|
||||||
static int32_t _lfsRmdir(lua_State *L);
|
static int32_t _lfsRmdir(lua_State *L);
|
||||||
static int32_t _loadAudioCalibration(void);
|
static int32_t _loadAudioCalibration(void);
|
||||||
|
|
@ -567,12 +580,14 @@ static void _reloadScript(void);
|
||||||
static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message);
|
static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message);
|
||||||
static void _resetScriptState(void);
|
static void _resetScriptState(void);
|
||||||
static void _runScript(bool fatal);
|
static void _runScript(bool fatal);
|
||||||
|
static bool _sameLoosePath(const char *a, const char *b);
|
||||||
static void _saveAudioCalibration(int32_t milliseconds);
|
static void _saveAudioCalibration(int32_t milliseconds);
|
||||||
static SDL_Texture *_sceneVideoSource(int32_t player);
|
static SDL_Texture *_sceneVideoSource(int32_t player);
|
||||||
static ConfigT *_scriptConfFromTable(lua_State *L, const char *method);
|
static ConfigT *_scriptConfFromTable(lua_State *L, const char *method);
|
||||||
static void _selectDefaultAudioTrack(int32_t handle);
|
static void _selectDefaultAudioTrack(int32_t handle);
|
||||||
static void _setMouseCaptured(bool captured);
|
static void _setMouseCaptured(bool captured);
|
||||||
static void _setPause(bool paused, bool fromKey);
|
static void _setPause(bool paused, bool fromKey);
|
||||||
|
static const char *_skipDotSlash(const char *path);
|
||||||
static void _soundDestroy(SoundT *sound);
|
static void _soundDestroy(SoundT *sound);
|
||||||
static int32_t _soundQueueDrain(int32_t *finished);
|
static int32_t _soundQueueDrain(int32_t *finished);
|
||||||
static void _spriteDestroy(SpriteT *sprite);
|
static void _spriteDestroy(SpriteT *sprite);
|
||||||
|
|
@ -724,6 +739,7 @@ static int32_t apiMaterialSetTiling(lua_State *L);
|
||||||
static int32_t apiMaterialSetUnlit(lua_State *L);
|
static int32_t apiMaterialSetUnlit(lua_State *L);
|
||||||
static int32_t apiMaterialSetVideo(lua_State *L);
|
static int32_t apiMaterialSetVideo(lua_State *L);
|
||||||
static int32_t apiMaterialSetView(lua_State *L);
|
static int32_t apiMaterialSetView(lua_State *L);
|
||||||
|
static int32_t apiMathRandomseed(lua_State *L);
|
||||||
static int32_t apiMeshBox(lua_State *L);
|
static int32_t apiMeshBox(lua_State *L);
|
||||||
static int32_t apiMeshCone(lua_State *L);
|
static int32_t apiMeshCone(lua_State *L);
|
||||||
static int32_t apiMeshCylinder(lua_State *L);
|
static int32_t apiMeshCylinder(lua_State *L);
|
||||||
|
|
@ -1092,7 +1108,9 @@ static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t
|
||||||
|
|
||||||
// A colour component argument, clamped to 0..255.
|
// A colour component argument, clamped to 0..255.
|
||||||
static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) {
|
static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) {
|
||||||
return (uint8_t)SDL_clamp(_argInteger(L, method, index), 0, COLOR_BYTE_MAX);
|
int32_t value = _argInteger(L, method, index);
|
||||||
|
|
||||||
|
return (uint8_t)SDL_clamp(value, 0, COLOR_BYTE_MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1447,8 +1465,10 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
|
||||||
c->noMouse = valueBoolean;
|
c->noMouse = valueBoolean;
|
||||||
} else if (strcmp(confKey, "RESOLUTION_X") == 0) {
|
} else if (strcmp(confKey, "RESOLUTION_X") == 0) {
|
||||||
c->xResolution = (int32_t)valueNumber;
|
c->xResolution = (int32_t)valueNumber;
|
||||||
|
c->resolutionWasCalculated = false;
|
||||||
} else if (strcmp(confKey, "RESOLUTION_Y") == 0) {
|
} else if (strcmp(confKey, "RESOLUTION_Y") == 0) {
|
||||||
c->yResolution = (int32_t)valueNumber;
|
c->yResolution = (int32_t)valueNumber;
|
||||||
|
c->resolutionWasCalculated = false;
|
||||||
} else if (strcmp(confKey, "SINDEN_GUN") == 0) {
|
} else if (strcmp(confKey, "SINDEN_GUN") == 0) {
|
||||||
if ((valueString == NULL) || !parseSindenString(valueString, c)) {
|
if ((valueString == NULL) || !parseSindenString(valueString, c)) {
|
||||||
c->sindenArgc = 0;
|
c->sindenArgc = 0;
|
||||||
|
|
@ -1490,8 +1510,14 @@ static void _callLua(const char *func, const char *sig, ...) {
|
||||||
int32_t narg = 0;
|
int32_t narg = 0;
|
||||||
int32_t nres = 0;
|
int32_t nres = 0;
|
||||||
int32_t handler = 0;
|
int32_t handler = 0;
|
||||||
|
int32_t top = 0;
|
||||||
double d = 0;
|
double d = 0;
|
||||||
const int32_t top = lua_gettop(_global.luaContext);
|
|
||||||
|
// No state (shutting down) means nothing to call.
|
||||||
|
if (_global.luaContext == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
top = lua_gettop(_global.luaContext);
|
||||||
|
|
||||||
// Get Function
|
// Get Function
|
||||||
lua_getglobal(_global.luaContext, func);
|
lua_getglobal(_global.luaContext, func);
|
||||||
|
|
@ -1647,6 +1673,12 @@ static void _createScriptContext(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The sound effect volume a script starts with, from the non-disc volume setting.
|
||||||
|
static int32_t _defaultEffectsVolume(void) {
|
||||||
|
return AUDIO_MAX_VOLUME * _global.conf->volumeNonVldp / VOLUME_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Sleeps while keeping the window responsive. Returns false if the user asked to quit.
|
// Sleeps while keeping the window responsive. Returns false if the user asked to quit.
|
||||||
static bool _delayAndPump(uint32_t ms) {
|
static bool _delayAndPump(uint32_t ms) {
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
|
|
@ -1686,6 +1718,7 @@ static void _deliverKey(bool down, int32_t keysym, int32_t scancode) {
|
||||||
if (_global.controlMappings[move].input[index] == scancode) {
|
if (_global.controlMappings[move].input[index] == scancode) {
|
||||||
_global.switchHeld[move] = down;
|
_global.switchHeld[move] = down;
|
||||||
_callLua(down ? "onInputPressed" : "onInputReleased", "i", move);
|
_callLua(down ? "onInputPressed" : "onInputReleased", "i", move);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1703,6 +1736,16 @@ static int64_t _discGetFrame(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The disc always starts parked on frame 1, paused, like discSearch(1). Only with a disc.
|
||||||
|
static void _discPark(void) {
|
||||||
|
_progTrace("Parking laserdisc on frame 1");
|
||||||
|
_discSeek(1);
|
||||||
|
videoPause(_global.videoHandle);
|
||||||
|
_global.discStopped = false;
|
||||||
|
_selectDefaultAudioTrack(_global.videoHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Seeks the laserdisc, whichever kind it is, held within its ends (a frame file clamps inside its
|
// Seeks the laserdisc, whichever kind it is, held within its ends (a frame file clamps inside its
|
||||||
// segments itself) rather than wrapping around.
|
// segments itself) rather than wrapping around.
|
||||||
static void _discSeek(int64_t frame) {
|
static void _discSeek(int64_t frame) {
|
||||||
|
|
@ -2222,11 +2265,13 @@ static void _installFileHooks(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// lfs.attributes through the vfs: a packed entry answers with mode, size and modification; a
|
// lfs.attributes through the vfs, with lfs's own forms: attributes(path) makes a table,
|
||||||
// plain filesystem path goes to the original (upvalue 1).
|
// attributes(path, name) answers one attribute (an unknown name raises), attributes(path, table)
|
||||||
|
// fills the table. A packed entry answers as a read only file; a plain filesystem path goes to
|
||||||
|
// the original (upvalue 1) untouched.
|
||||||
static int32_t _lfsAttributes(lua_State *L) {
|
static int32_t _lfsAttributes(lua_State *L) {
|
||||||
const char *name = luaL_checkstring(L, 1);
|
const char *name = luaL_checkstring(L, 1);
|
||||||
const char *attribute = luaL_optstring(L, 2, NULL);
|
const char *attribute = NULL;
|
||||||
int64_t size = 0;
|
int64_t size = 0;
|
||||||
int64_t modified = 0;
|
int64_t modified = 0;
|
||||||
bool directory = false;
|
bool directory = false;
|
||||||
|
|
@ -2237,23 +2282,25 @@ static int32_t _lfsAttributes(lua_State *L) {
|
||||||
directory = vfsIsDirectory(name);
|
directory = vfsIsDirectory(name);
|
||||||
if (!directory && !vfsStat(name, &size, &modified)) {
|
if (!directory && !vfsStat(name, &size, &modified)) {
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
lua_pushfstring(L, "cannot obtain information from file '%s'", name);
|
lua_pushfstring(L, "cannot obtain information from file '%s': %s", name, strerror(ENOENT));
|
||||||
return 2;
|
lua_pushinteger(L, ENOENT);
|
||||||
|
return 3;
|
||||||
}
|
}
|
||||||
|
if (lua_isstring(L, 2)) {
|
||||||
|
attribute = lua_tostring(L, 2);
|
||||||
lua_newtable(L);
|
lua_newtable(L);
|
||||||
lua_pushstring(L, directory ? "directory" : "file");
|
_lfsFillAttributes(L, directory, size, modified);
|
||||||
lua_setfield(L, -2, "mode");
|
|
||||||
lua_pushinteger(L, (lua_Integer)size);
|
|
||||||
lua_setfield(L, -2, "size");
|
|
||||||
lua_pushinteger(L, (lua_Integer)modified);
|
|
||||||
lua_setfield(L, -2, "modification");
|
|
||||||
lua_pushinteger(L, (lua_Integer)modified);
|
|
||||||
lua_setfield(L, -2, "change");
|
|
||||||
lua_pushinteger(L, (lua_Integer)modified);
|
|
||||||
lua_setfield(L, -2, "access");
|
|
||||||
if (attribute != NULL) {
|
|
||||||
lua_getfield(L, -1, attribute);
|
lua_getfield(L, -1, attribute);
|
||||||
|
if (lua_isnil(L, -1)) {
|
||||||
|
return luaL_error(L, "invalid attribute name '%s'", attribute);
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_settop(L, 2);
|
||||||
|
if (!lua_istable(L, 2)) {
|
||||||
|
lua_newtable(L);
|
||||||
|
}
|
||||||
|
_lfsFillAttributes(L, directory, size, modified);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -2300,6 +2347,39 @@ static int32_t _lfsDirIterator(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Every attribute real lfs reports, into the table on top of the stack, for a packed entry.
|
||||||
|
static void _lfsFillAttributes(lua_State *L, bool directory, int64_t size, int64_t modified) {
|
||||||
|
lua_pushstring(L, directory ? "directory" : "file");
|
||||||
|
lua_setfield(L, -2, "mode");
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
lua_setfield(L, -2, "dev");
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
lua_setfield(L, -2, "ino");
|
||||||
|
lua_pushinteger(L, 1);
|
||||||
|
lua_setfield(L, -2, "nlink");
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
lua_setfield(L, -2, "uid");
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
lua_setfield(L, -2, "gid");
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
lua_setfield(L, -2, "rdev");
|
||||||
|
lua_pushinteger(L, (lua_Integer)modified);
|
||||||
|
lua_setfield(L, -2, "access");
|
||||||
|
lua_pushinteger(L, (lua_Integer)modified);
|
||||||
|
lua_setfield(L, -2, "modification");
|
||||||
|
lua_pushinteger(L, (lua_Integer)modified);
|
||||||
|
lua_setfield(L, -2, "change");
|
||||||
|
lua_pushinteger(L, (lua_Integer)size);
|
||||||
|
lua_setfield(L, -2, "size");
|
||||||
|
lua_pushstring(L, PACKED_PERMISSIONS);
|
||||||
|
lua_setfield(L, -2, "permissions");
|
||||||
|
lua_pushinteger(L, (lua_Integer)((size + PACKED_BLOCK_SIZE - 1) / PACKED_BLOCK_SIZE));
|
||||||
|
lua_setfield(L, -2, "blocks");
|
||||||
|
lua_pushinteger(L, PACKED_BLOCK_SIZE);
|
||||||
|
lua_setfield(L, -2, "blksize");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// lfs.mkdir through the vfs: inside a packed game the directory is made where its files will be
|
// lfs.mkdir through the vfs: inside a packed game the directory is made where its files will be
|
||||||
// written, the data overlay; a plain filesystem path goes to the original (upvalue 1).
|
// written, the data overlay; a plain filesystem path goes to the original (upvalue 1).
|
||||||
static int32_t _lfsMkdir(lua_State *L) {
|
static int32_t _lfsMkdir(lua_State *L) {
|
||||||
|
|
@ -2683,13 +2763,14 @@ static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bo
|
||||||
lua_pushfstring(L, "cannot open %s", name);
|
lua_pushfstring(L, "cannot open %s", name);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
// Watched from here so a syntax error can be fixed and picked up.
|
||||||
|
if (watch) {
|
||||||
|
_watchFile(name);
|
||||||
|
}
|
||||||
chunkName = utilCreateString("@%s", name);
|
chunkName = utilCreateString("@%s", name);
|
||||||
status = luaL_loadbufferx(L, data, bytes, chunkName, mode);
|
status = luaL_loadbufferx(L, data, bytes, chunkName, mode);
|
||||||
free(chunkName);
|
free(chunkName);
|
||||||
free(data);
|
free(data);
|
||||||
if ((status == LUA_OK) && watch) {
|
|
||||||
_watchFile(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
@ -3728,6 +3809,7 @@ static void _reloadScript(void) {
|
||||||
_global.reloadRequested = false;
|
_global.reloadRequested = false;
|
||||||
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
|
MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0);
|
||||||
lua_close(_global.luaContext);
|
lua_close(_global.luaContext);
|
||||||
|
_global.luaContext = NULL;
|
||||||
_unloadScriptResources();
|
_unloadScriptResources();
|
||||||
_subsystemsQuit();
|
_subsystemsQuit();
|
||||||
_subsystemsInit();
|
_subsystemsInit();
|
||||||
|
|
@ -3765,6 +3847,7 @@ static SDL_Surface *_renderText(lua_State *L, const char *method, const char *me
|
||||||
if (!surface) {
|
if (!surface) {
|
||||||
_luaDie(L, method, "%s", SDL_GetError());
|
_luaDie(L, method, "%s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
_surfaceUnpack(&surface);
|
||||||
SDL_SetSurfaceColorKey(surface, true, COLOR_KEY_VALUE);
|
SDL_SetSurfaceColorKey(surface, true, COLOR_KEY_VALUE);
|
||||||
|
|
||||||
return surface;
|
return surface;
|
||||||
|
|
@ -3772,10 +3855,11 @@ static SDL_Surface *_renderText(lua_State *L, const char *method, const char *me
|
||||||
|
|
||||||
|
|
||||||
// Everything a script sets about itself, back to what a script may expect at its start: colours,
|
// Everything a script sets about itself, back to what a script may expect at its start: colours,
|
||||||
// font quality, keyboard mode, listener, pause, the effect channels and their completion queue,
|
// font quality, keyboard mode, listener, pause, the effect channels, their volume and their
|
||||||
// what the previous script believed was held, and --reload's file list. Keys physically down now
|
// completion queue, the mouse (mode, enabled, captured), the disc parked on frame 1, what the
|
||||||
// were held over from before this script, so they are ignored until released. Nothing here calls
|
// previous script believed was held, and --reload's file list. Keys physically down now were held
|
||||||
// into Lua, so it is safe between one state closing and the next opening.
|
// over from before this script, so they are ignored until released. Nothing here calls into Lua,
|
||||||
|
// so it is safe between one state closing and the next opening.
|
||||||
static void _resetScriptState(void) {
|
static void _resetScriptState(void) {
|
||||||
int32_t finished[SOUND_QUEUE_SIZE];
|
int32_t finished[SOUND_QUEUE_SIZE];
|
||||||
int32_t x = 0;
|
int32_t x = 0;
|
||||||
|
|
@ -3784,6 +3868,14 @@ static void _resetScriptState(void) {
|
||||||
_effectReset(x);
|
_effectReset(x);
|
||||||
}
|
}
|
||||||
_soundQueueDrain(finished);
|
_soundQueueDrain(finished);
|
||||||
|
_global.effectsVolume = _defaultEffectsVolume();
|
||||||
|
MIX_SetTagGain(videoGetMixer(), EFFECT_TAG, _mixerGain(_global.effectsVolume));
|
||||||
|
_global.mouseMode = MOUSE_SINGLE;
|
||||||
|
_global.mouseEnabled = !_global.conf->noMouse && (_global.mouseCount > 0);
|
||||||
|
_setMouseCaptured(true);
|
||||||
|
if (_global.videoHandle >= 0) {
|
||||||
|
_discPark();
|
||||||
|
}
|
||||||
_global.colorForeground.r = SDL_ALPHA_OPAQUE;
|
_global.colorForeground.r = SDL_ALPHA_OPAQUE;
|
||||||
_global.colorForeground.g = SDL_ALPHA_OPAQUE;
|
_global.colorForeground.g = SDL_ALPHA_OPAQUE;
|
||||||
_global.colorForeground.b = SDL_ALPHA_OPAQUE;
|
_global.colorForeground.b = SDL_ALPHA_OPAQUE;
|
||||||
|
|
@ -3829,6 +3921,22 @@ static void _runScript(bool fatal) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// True when two loose file names name the same file: separators unified, leading "./" ignored.
|
||||||
|
static bool _sameLoosePath(const char *a, const char *b) {
|
||||||
|
char *left = strdup(a);
|
||||||
|
char *right = strdup(b);
|
||||||
|
bool same = false;
|
||||||
|
|
||||||
|
utilFixPathSeparators(&left, false);
|
||||||
|
utilFixPathSeparators(&right, false);
|
||||||
|
same = (utilStricmp(_skipDotSlash(left), _skipDotSlash(right)) == 0);
|
||||||
|
free(left);
|
||||||
|
free(right);
|
||||||
|
|
||||||
|
return same;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _saveAudioCalibration(int32_t milliseconds) {
|
static void _saveAudioCalibration(int32_t milliseconds) {
|
||||||
char *path = utilCreateString("%s%s", _global.conf->dataDirBase, AUDIO_CALIBRATION_FILE);
|
char *path = utilCreateString("%s%s", _global.conf->dataDirBase, AUDIO_CALIBRATION_FILE);
|
||||||
FILE *out = fopen(path, "w");
|
FILE *out = fopen(path, "w");
|
||||||
|
|
@ -3848,8 +3956,8 @@ static void _saveAudioCalibration(int32_t milliseconds) {
|
||||||
static SDL_Texture *_sceneVideoSource(int32_t player) {
|
static SDL_Texture *_sceneVideoSource(int32_t player) {
|
||||||
VideoT *video;
|
VideoT *video;
|
||||||
|
|
||||||
if ((player == _global.videoHandle) && (player >= 0)) {
|
if (player == PLAYER_DISC) {
|
||||||
return _global.videoTexture;
|
return (_global.videoHandle >= 0) ? _global.videoTexture : NULL;
|
||||||
}
|
}
|
||||||
for (video = _global.videoList; video != NULL; video = video->hh.next) {
|
for (video = _global.videoList; video != NULL; video = video->hh.next) {
|
||||||
if (video->handle == player) {
|
if (video->handle == player) {
|
||||||
|
|
@ -3913,6 +4021,15 @@ static void _setPause(bool paused, bool fromKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char *_skipDotSlash(const char *path) {
|
||||||
|
while ((path[0] == '.') && ((path[1] == '/') || (path[1] == '\\'))) {
|
||||||
|
path += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _soundDestroy(SoundT *sound) {
|
static void _soundDestroy(SoundT *sound) {
|
||||||
HASH_DEL(_global.soundList, sound);
|
HASH_DEL(_global.soundList, sound);
|
||||||
MIX_DestroyAudio(sound->audio);
|
MIX_DestroyAudio(sound->audio);
|
||||||
|
|
@ -4016,6 +4133,13 @@ static void _startLuaContext(lua_State *L) {
|
||||||
lua_setfield(L, -2, "clock");
|
lua_setfield(L, -2, "clock");
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
// Games written for 2.x seed the generator with fractional numbers; Lua 5.4 refuses them.
|
||||||
|
lua_getglobal(L, "math");
|
||||||
|
lua_getfield(L, -1, "randomseed");
|
||||||
|
lua_pushcclosure(L, apiMathRandomseed, 1);
|
||||||
|
lua_setfield(L, -2, "randomseed");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
// Every file a script names goes through the vfs.
|
// Every file a script names goes through the vfs.
|
||||||
_installFileHooks(L);
|
_installFileHooks(L);
|
||||||
|
|
||||||
|
|
@ -4046,8 +4170,8 @@ static void _stopControllers(void) {
|
||||||
_global.controllers[x] = NULL;
|
_global.controllers[x] = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Anything held on an axis is gone with the controller.
|
// Anything held on a controller axis is gone with the controller.
|
||||||
for (x = 0; x < AXIS_COUNT; x++) {
|
for (x = 0; x < AXIS_CONTROLLER_COUNT; x++) {
|
||||||
_releaseAxis(x);
|
_releaseAxis(x);
|
||||||
_global.axisCache[x] = 0;
|
_global.axisCache[x] = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -4112,19 +4236,40 @@ static uint32_t _surfaceCornerKey(SDL_Surface *surface) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// SDL3_image keeps 1, 2, and 4 bit indexed PNGs packed, and blits from those are unreliable.
|
// Every surface the engine keeps is 8 bits per channel: SDL3_image keeps 1, 2, and 4 bit indexed
|
||||||
// Expand them to one byte per pixel, which is what SDL2 always produced. Replaces *surface.
|
// PNGs packed, and blits from those are unreliable, so they expand to one byte per pixel as SDL2
|
||||||
|
// always produced; anything else that is not an 8 bit indexed, packed 8888 or byte array format
|
||||||
|
// (16 bit PNGs as RGB48 or RGBA64, float formats, 565 and its kind) becomes RGBA32, since colour
|
||||||
|
// keying and blitting those is undefined or aborts inside SDL. Replaces *surface.
|
||||||
static void _surfaceUnpack(SDL_Surface **surface) {
|
static void _surfaceUnpack(SDL_Surface **surface) {
|
||||||
SDL_Surface *unpacked = NULL;
|
SDL_Surface *unpacked = NULL;
|
||||||
|
SDL_PixelFormat format = SDL_PIXELFORMAT_UNKNOWN;
|
||||||
|
SDL_PixelFormat wanted = SDL_PIXELFORMAT_UNKNOWN;
|
||||||
|
|
||||||
if ((*surface != NULL) && SDL_ISPIXELFORMAT_INDEXED((*surface)->format) && (SDL_BITSPERPIXEL((*surface)->format) < 8)) {
|
if (*surface == NULL) {
|
||||||
unpacked = SDL_ConvertSurface(*surface, SDL_PIXELFORMAT_INDEX8);
|
return;
|
||||||
|
}
|
||||||
|
format = (*surface)->format;
|
||||||
|
if (SDL_ISPIXELFORMAT_INDEXED(format)) {
|
||||||
|
if (SDL_BITSPERPIXEL(format) < 8) {
|
||||||
|
wanted = SDL_PIXELFORMAT_INDEX8;
|
||||||
|
}
|
||||||
|
} else if (SDL_ISPIXELFORMAT_PACKED(format)) {
|
||||||
|
if (SDL_PIXELLAYOUT(format) != SDL_PACKEDLAYOUT_8888) {
|
||||||
|
wanted = SDL_PIXELFORMAT_RGBA32;
|
||||||
|
}
|
||||||
|
} else if (!SDL_ISPIXELFORMAT_ARRAY(format) || (SDL_PIXELTYPE(format) != SDL_PIXELTYPE_ARRAYU8)) {
|
||||||
|
wanted = SDL_PIXELFORMAT_RGBA32;
|
||||||
|
}
|
||||||
|
if (wanted == SDL_PIXELFORMAT_UNKNOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unpacked = SDL_ConvertSurface(*surface, wanted);
|
||||||
if (unpacked == NULL) {
|
if (unpacked == NULL) {
|
||||||
utilDie("%s", SDL_GetError());
|
utilDie("%s", SDL_GetError());
|
||||||
}
|
}
|
||||||
SDL_DestroySurface(*surface);
|
SDL_DestroySurface(*surface);
|
||||||
*surface = unpacked;
|
*surface = unpacked;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -5256,12 +5401,15 @@ static int32_t apiEmitterSetCollide(lua_State *L) {
|
||||||
static int32_t apiEmitterSetColor(lua_State *L) {
|
static int32_t apiEmitterSetColor(lua_State *L) {
|
||||||
float start[COLOR_COMPONENTS];
|
float start[COLOR_COMPONENTS];
|
||||||
float finish[COLOR_COMPONENTS];
|
float finish[COLOR_COMPONENTS];
|
||||||
|
float value = 0.0f;
|
||||||
int32_t c = 0;
|
int32_t c = 0;
|
||||||
|
|
||||||
_argCheck(L, "emitterSetColor", 9, 9);
|
_argCheck(L, "emitterSetColor", 9, 9);
|
||||||
for (c = 0; c < COLOR_COMPONENTS; c++) {
|
for (c = 0; c < COLOR_COMPONENTS; c++) {
|
||||||
start[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 2 + c) / (float)COLOR_BYTE_MAX, 0.0f, 1.0f);
|
value = (float)_argNumber(L, "emitterSetColor", 2 + c) / (float)COLOR_BYTE_MAX;
|
||||||
finish[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 6 + c) / (float)COLOR_BYTE_MAX, 0.0f, 1.0f);
|
start[c] = SDL_clamp(value, 0.0f, 1.0f);
|
||||||
|
value = (float)_argNumber(L, "emitterSetColor", 6 + c) / (float)COLOR_BYTE_MAX;
|
||||||
|
finish[c] = SDL_clamp(value, 0.0f, 1.0f);
|
||||||
}
|
}
|
||||||
emitterSetColor(_argEmitter(L, "emitterSetColor", 1), start, finish);
|
emitterSetColor(_argEmitter(L, "emitterSetColor", 1), start, finish);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -6001,7 +6149,7 @@ static int32_t apiMaterialSetVideo(lua_State *L) {
|
||||||
if (_global.videoHandle < 0) {
|
if (_global.videoHandle < 0) {
|
||||||
_luaDie(L, "materialSetVideo", "This game has no disc.");
|
_luaDie(L, "materialSetVideo", "This game has no disc.");
|
||||||
}
|
}
|
||||||
player = _global.videoHandle;
|
player = PLAYER_DISC;
|
||||||
}
|
}
|
||||||
materialSetVideo(material, player);
|
materialSetVideo(material, player);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -6024,6 +6172,31 @@ static int32_t apiMaterialSetView(lua_State *L) {
|
||||||
|
|
||||||
|
|
||||||
// mesh = meshBox(width, height, depth)
|
// mesh = meshBox(width, height, depth)
|
||||||
|
// math.randomseed(x[, y]) Lua 5.4 wants integer seeds; games written for Singe 2.x pass products of
|
||||||
|
// os.clock() and the like, so fractional seeds are floored before Lua's own function sees them.
|
||||||
|
static int32_t apiMathRandomseed(lua_State *L) {
|
||||||
|
int32_t top = lua_gettop(L);
|
||||||
|
int32_t x = 0;
|
||||||
|
lua_Number n = 0.0;
|
||||||
|
|
||||||
|
for (x = 1; (x <= top) && (x <= 2); x++) {
|
||||||
|
if (lua_isnumber(L, x) && !lua_isinteger(L, x)) {
|
||||||
|
n = floor(lua_tonumber(L, x));
|
||||||
|
if (!isfinite(n)) {
|
||||||
|
n = 0.0;
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, (lua_Integer)n);
|
||||||
|
lua_replace(L, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pushvalue(L, lua_upvalueindex(1));
|
||||||
|
lua_insert(L, 1);
|
||||||
|
lua_call(L, top, LUA_MULTRET);
|
||||||
|
|
||||||
|
return lua_gettop(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static int32_t apiMeshBox(lua_State *L) {
|
static int32_t apiMeshBox(lua_State *L) {
|
||||||
int32_t mesh;
|
int32_t mesh;
|
||||||
|
|
||||||
|
|
@ -8417,7 +8590,8 @@ static int32_t apiSoundSetPan(lua_State *L) {
|
||||||
|
|
||||||
_argCheck(L, "soundSetPan", 2, 2);
|
_argCheck(L, "soundSetPan", 2, 2);
|
||||||
channel = _argChannel(L, "soundSetPan", 1);
|
channel = _argChannel(L, "soundSetPan", 1);
|
||||||
pan = SDL_clamp((float)_argNumber(L, "soundSetPan", 2), -1.0f, 1.0f);
|
pan = (float)_argNumber(L, "soundSetPan", 2);
|
||||||
|
pan = SDL_clamp(pan, -1.0f, 1.0f);
|
||||||
_effects[channel].positioned = false;
|
_effects[channel].positioned = false;
|
||||||
_effects[channel].node = LISTENER_CAMERA;
|
_effects[channel].node = LISTENER_CAMERA;
|
||||||
gains.left = SDL_min(1.0f, 1.0f - pan);
|
gains.left = SDL_min(1.0f, 1.0f - pan);
|
||||||
|
|
@ -8665,11 +8839,9 @@ static int32_t apiSpriteLoad(lua_State *L) {
|
||||||
}
|
}
|
||||||
sprite->animation = IMG_LoadAnimation_IO(io, false);
|
sprite->animation = IMG_LoadAnimation_IO(io, false);
|
||||||
if ((sprite->animation != NULL) && (sprite->animation->count < 2)) {
|
if ((sprite->animation != NULL) && (sprite->animation->count < 2)) {
|
||||||
// Only one frame - keep it as a still image (a pixel copy; a blit would premultiply the alpha).
|
// Only one frame: take it over as the still image (no copy, so packed 1-bit PNGs survive).
|
||||||
sprite->originalSurface = SDL_DuplicateSurface(sprite->animation->frames[0]);
|
sprite->originalSurface = sprite->animation->frames[0];
|
||||||
if (sprite->originalSurface == NULL) {
|
sprite->animation->frames[0] = NULL;
|
||||||
_luaDie(L, "spriteLoad", "%s", SDL_GetError());
|
|
||||||
}
|
|
||||||
_surfaceUnpack(&sprite->originalSurface);
|
_surfaceUnpack(&sprite->originalSurface);
|
||||||
IMG_FreeAnimation(sprite->animation);
|
IMG_FreeAnimation(sprite->animation);
|
||||||
sprite->animation = NULL;
|
sprite->animation = NULL;
|
||||||
|
|
@ -9486,8 +9658,10 @@ static int32_t apiVideoSetVolume(lua_State *L) {
|
||||||
|
|
||||||
_argCheck(L, "videoSetVolume", 3, 3);
|
_argCheck(L, "videoSetVolume", 3, 3);
|
||||||
video = _argVideo(L, "videoSetVolume", 1);
|
video = _argVideo(L, "videoSetVolume", 1);
|
||||||
left = SDL_clamp(_argInteger(L, "videoSetVolume", 2), 0, VIDEO_VOLUME_MAX);
|
left = _argInteger(L, "videoSetVolume", 2);
|
||||||
right = SDL_clamp(_argInteger(L, "videoSetVolume", 3), 0, VIDEO_VOLUME_MAX);
|
right = _argInteger(L, "videoSetVolume", 3);
|
||||||
|
left = SDL_clamp(left, 0, VIDEO_VOLUME_MAX);
|
||||||
|
right = SDL_clamp(right, 0, VIDEO_VOLUME_MAX);
|
||||||
videoSetVolume(video->handle, left, right);
|
videoSetVolume(video->handle, left, right);
|
||||||
_luaTrace(L, "videoSetVolume", "%d %d %d", video->id, left, right);
|
_luaTrace(L, "videoSetVolume", "%d %d %d", video->id, left, right);
|
||||||
|
|
||||||
|
|
@ -9609,6 +9783,150 @@ ConfigT *confFromDatabase(const ConfigT *conf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A loose script launched by name runs with the settings of its games.dat entry, as the menu would
|
||||||
|
// run it: the nearest games.dat in the script's directory or above, whose entry names the script
|
||||||
|
// (entry paths count from the directory above the games.dat). Options typed on the command line
|
||||||
|
// keep their values. NULL when there is no such entry.
|
||||||
|
ConfigT *confFromGamesDat(const ConfigT *conf) {
|
||||||
|
lua_State *L = NULL;
|
||||||
|
ConfigT *result = NULL;
|
||||||
|
char *dir = utilGetUpToLastPathComponent(conf->scriptFile);
|
||||||
|
char *file = NULL;
|
||||||
|
char *base = NULL;
|
||||||
|
char *trimmed = NULL;
|
||||||
|
char *parent = NULL;
|
||||||
|
char *data = NULL;
|
||||||
|
char *resolved = NULL;
|
||||||
|
const char *script = NULL;
|
||||||
|
size_t bytes = 0;
|
||||||
|
size_t length = 0;
|
||||||
|
int32_t level = 0;
|
||||||
|
int32_t entry = 0;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
// Find the games.dat.
|
||||||
|
for (level = 0; (level < GAMES_DAT_LEVELS) && (base == NULL); level++) {
|
||||||
|
file = utilCreateString("%s%s", dir, VFS_GAMES_DAT);
|
||||||
|
if (utilFileExists(file)) {
|
||||||
|
// Entry paths are relative to the directory above the one holding games.dat.
|
||||||
|
trimmed = strdup(dir);
|
||||||
|
length = strlen(trimmed);
|
||||||
|
while ((length > 0) && ((trimmed[length - 1] == '/') || (trimmed[length - 1] == '\\'))) {
|
||||||
|
trimmed[--length] = 0;
|
||||||
|
}
|
||||||
|
base = utilGetUpToLastPathComponent(trimmed);
|
||||||
|
free(trimmed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
free(file);
|
||||||
|
file = NULL;
|
||||||
|
trimmed = strdup(dir);
|
||||||
|
length = strlen(trimmed);
|
||||||
|
while ((length > 0) && ((trimmed[length - 1] == '/') || (trimmed[length - 1] == '\\'))) {
|
||||||
|
trimmed[--length] = 0;
|
||||||
|
}
|
||||||
|
if (length == 0) {
|
||||||
|
free(trimmed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parent = utilGetUpToLastPathComponent(trimmed);
|
||||||
|
free(trimmed);
|
||||||
|
if (strcmp(parent, dir) == 0) {
|
||||||
|
free(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
free(dir);
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
if (base == NULL) {
|
||||||
|
free(dir);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run it and look for the entry naming this script.
|
||||||
|
data = utilReadFile(file, &bytes);
|
||||||
|
L = luaL_newstate();
|
||||||
|
luaL_openlibs(L);
|
||||||
|
if ((data == NULL) || (luaL_loadbuffer(L, data, bytes, file) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
|
||||||
|
utilSay("Note: %s ignored: %s", file, (data == NULL) ? "cannot read it" : lua_tostring(L, -1));
|
||||||
|
} else {
|
||||||
|
lua_getglobal(L, "GAMES");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
for (entry = 1; !found; entry++) {
|
||||||
|
if (lua_rawgeti(L, -1, entry) != LUA_TTABLE) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (lua_getfield(L, -1, "SCRIPT") == LUA_TSTRING) {
|
||||||
|
script = lua_tostring(L, -1);
|
||||||
|
resolved = utilCreateString("%s%s", (strcmp(base, "./") == 0) ? "" : base, script);
|
||||||
|
found = _sameLoosePath(resolved, conf->scriptFile);
|
||||||
|
free(resolved);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
if (found) {
|
||||||
|
lua_replace(L, 1);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
result = _buildConfFromTable(L, conf);
|
||||||
|
result->entry = entry;
|
||||||
|
} else {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_close(L);
|
||||||
|
free(data);
|
||||||
|
|
||||||
|
if (result != NULL) {
|
||||||
|
// The launched file is the entry's script, however either was spelled; the video keeps
|
||||||
|
// the entry's spelling, made relative to where the launch happened.
|
||||||
|
free(result->scriptFile);
|
||||||
|
result->scriptFile = strdup(conf->scriptFile);
|
||||||
|
if (conf->given & GIVEN_VIDEO) {
|
||||||
|
free(result->videoFile);
|
||||||
|
result->videoFile = (conf->videoFile != NULL) ? strdup(conf->videoFile) : NULL;
|
||||||
|
result->disc = conf->disc;
|
||||||
|
result->isFrameFile = conf->isFrameFile;
|
||||||
|
} else if ((result->videoFile != NULL) && (strcmp(base, "./") != 0)) {
|
||||||
|
resolved = utilCreateString("%s%s", base, result->videoFile);
|
||||||
|
free(result->videoFile);
|
||||||
|
result->videoFile = resolved;
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_STRETCH) {
|
||||||
|
result->stretchVideo = conf->stretchVideo;
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_NO_MOUSE) {
|
||||||
|
result->noMouse = conf->noMouse;
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_RESOLUTION) {
|
||||||
|
result->xResolution = conf->xResolution;
|
||||||
|
result->yResolution = conf->yResolution;
|
||||||
|
result->resolutionWasCalculated = conf->resolutionWasCalculated;
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_SINDEN) {
|
||||||
|
result->sindenArgc = conf->sindenArgc;
|
||||||
|
memcpy(result->sindenArgv, conf->sindenArgv, sizeof(result->sindenArgv));
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_AUDIO_TRACK) {
|
||||||
|
result->audioOutputTrack = conf->audioOutputTrack;
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_AUDIO_DELAY) {
|
||||||
|
result->audioDelayMs = conf->audioDelayMs;
|
||||||
|
}
|
||||||
|
if (conf->given & GIVEN_CANVAS) {
|
||||||
|
result->canvasWidth = conf->canvasWidth;
|
||||||
|
result->canvasHeight = conf->canvasHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(file);
|
||||||
|
free(base);
|
||||||
|
free(dir);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf) {
|
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf) {
|
||||||
int32_t x = 0;
|
int32_t x = 0;
|
||||||
int32_t y = 0;
|
int32_t y = 0;
|
||||||
|
|
@ -9630,6 +9948,10 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
SDL_Texture *sceneTexture = NULL;
|
SDL_Texture *sceneTexture = NULL;
|
||||||
SDL_Color sindenWhiteColor = { COLOR_BYTE_MAX, COLOR_BYTE_MAX, COLOR_BYTE_MAX, SDL_ALPHA_OPAQUE };
|
SDL_Color sindenWhiteColor = { COLOR_BYTE_MAX, COLOR_BYTE_MAX, COLOR_BYTE_MAX, SDL_ALPHA_OPAQUE };
|
||||||
SDL_Color sindenBlackColor = { 0, 0, 0, SDL_ALPHA_OPAQUE };
|
SDL_Color sindenBlackColor = { 0, 0, 0, SDL_ALPHA_OPAQUE };
|
||||||
|
int32_t sindenWhiteWidth = 0;
|
||||||
|
int32_t sindenBlackWidth = 0;
|
||||||
|
bool sindenBorder = false;
|
||||||
|
double mouseFraction = 0;
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
ManyMouseEvent mouseEvent;
|
ManyMouseEvent mouseEvent;
|
||||||
MouseT *mouse = NULL;
|
MouseT *mouse = NULL;
|
||||||
|
|
@ -9714,22 +10036,21 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
windowTarget.y = 0;
|
windowTarget.y = 0;
|
||||||
windowTarget.w = videoWidth;
|
windowTarget.w = videoWidth;
|
||||||
windowTarget.h = videoHeight;
|
windowTarget.h = videoHeight;
|
||||||
sindenWhite.x = -1;
|
|
||||||
sindenBlack.x = -1;
|
|
||||||
|
|
||||||
// Sinden Light Gun Border Setup
|
// Sinden Light Gun Border Setup: the black border (if any) is the outermost ring, the white
|
||||||
|
// border sits inside it, and the game is drawn inside both. Widths are in video pixels.
|
||||||
if (_global.conf->sindenArgc > 0) {
|
if (_global.conf->sindenArgc > 0) {
|
||||||
//***TODO*** ADD MOUSE SCALING TO COMPENSATE FOR BORDER
|
sindenBorder = true;
|
||||||
switch (_global.conf->sindenArgc) {
|
switch (_global.conf->sindenArgc) {
|
||||||
// WW - Just the width of the white border
|
// WW - Just the width of the white border
|
||||||
case SINDEN_WHITE:
|
case SINDEN_WHITE:
|
||||||
sindenWhite.x = _global.conf->sindenArgv[0];
|
sindenWhiteWidth = _global.conf->sindenArgv[0];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// WW WB - Width of white border and then black border
|
// WW WB - Width of white border and then black border
|
||||||
case SINDEN_WHITE_BLACK:
|
case SINDEN_WHITE_BLACK:
|
||||||
sindenWhite.x = _global.conf->sindenArgv[0];
|
sindenWhiteWidth = _global.conf->sindenArgv[0];
|
||||||
sindenBlack.x = _global.conf->sindenArgv[1];
|
sindenBlackWidth = _global.conf->sindenArgv[1];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// RW GW BW WW - Custom color "white" border and width
|
// RW GW BW WW - Custom color "white" border and width
|
||||||
|
|
@ -9737,7 +10058,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
|
sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
|
||||||
sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
|
sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
|
||||||
sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
|
sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
|
||||||
sindenWhite.x = _global.conf->sindenArgv[3];
|
sindenWhiteWidth = _global.conf->sindenArgv[3];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// RW GW BW WW WB - Custom color "white" border and width then width of black border
|
// RW GW BW WW WB - Custom color "white" border and width then width of black border
|
||||||
|
|
@ -9745,8 +10066,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
|
sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
|
||||||
sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
|
sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
|
||||||
sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
|
sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
|
||||||
sindenWhite.x = _global.conf->sindenArgv[3];
|
sindenWhiteWidth = _global.conf->sindenArgv[3];
|
||||||
sindenBlack.x = _global.conf->sindenArgv[4];
|
sindenBlackWidth = _global.conf->sindenArgv[4];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width
|
// RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width
|
||||||
|
|
@ -9754,30 +10075,32 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
|
sindenWhiteColor.r = (uint8_t)_global.conf->sindenArgv[0];
|
||||||
sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
|
sindenWhiteColor.g = (uint8_t)_global.conf->sindenArgv[1];
|
||||||
sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
|
sindenWhiteColor.b = (uint8_t)_global.conf->sindenArgv[2];
|
||||||
sindenWhite.x = _global.conf->sindenArgv[3];
|
sindenWhiteWidth = _global.conf->sindenArgv[3];
|
||||||
sindenBlackColor.r = (uint8_t)_global.conf->sindenArgv[4];
|
sindenBlackColor.r = (uint8_t)_global.conf->sindenArgv[4];
|
||||||
sindenBlackColor.g = (uint8_t)_global.conf->sindenArgv[5];
|
sindenBlackColor.g = (uint8_t)_global.conf->sindenArgv[5];
|
||||||
sindenBlackColor.b = (uint8_t)_global.conf->sindenArgv[6];
|
sindenBlackColor.b = (uint8_t)_global.conf->sindenArgv[6];
|
||||||
sindenBlack.x = _global.conf->sindenArgv[7];
|
sindenBlackWidth = _global.conf->sindenArgv[7];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
utilDie("Bad Sinden argument count: %d", _global.conf->sindenArgc);
|
utilDie("Bad Sinden argument count: %d", _global.conf->sindenArgc);
|
||||||
}
|
}
|
||||||
// The white border is the inner one; the black border (if any) surrounds it.
|
if ((sindenWhiteWidth < 0) || (sindenBlackWidth < 0) || (2 * (sindenWhiteWidth + sindenBlackWidth) >= SDL_min(videoWidth, videoHeight))) {
|
||||||
sindenWhite.y = sindenWhite.x;
|
utilDie("Sinden border of %d + %d pixels does not fit a %dx%d video", sindenWhiteWidth, sindenBlackWidth, videoWidth, videoHeight);
|
||||||
sindenWhite.w = videoWidth - (sindenWhite.x * 2);
|
|
||||||
sindenWhite.h = videoHeight - (sindenWhite.y * 2);
|
|
||||||
if (sindenBlack.x >= 0) {
|
|
||||||
sindenBlack.y = sindenBlack.x;
|
|
||||||
sindenBlack.w = videoWidth - (sindenBlack.x * 2);
|
|
||||||
sindenBlack.h = videoHeight - (sindenBlack.y * 2);
|
|
||||||
sindenWhite.x += sindenBlack.x;
|
|
||||||
sindenWhite.y += sindenBlack.y;
|
|
||||||
sindenWhite.w -= (sindenBlack.x * 2);
|
|
||||||
sindenWhite.h -= (sindenBlack.y * 2);
|
|
||||||
}
|
}
|
||||||
windowTarget = sindenWhite;
|
// The black ring is the whole frame, the white ring everything inside it, the game inside both.
|
||||||
|
sindenBlack.x = 0;
|
||||||
|
sindenBlack.y = 0;
|
||||||
|
sindenBlack.w = videoWidth;
|
||||||
|
sindenBlack.h = videoHeight;
|
||||||
|
sindenWhite.x = sindenBlackWidth;
|
||||||
|
sindenWhite.y = sindenBlackWidth;
|
||||||
|
sindenWhite.w = videoWidth - (sindenBlackWidth * 2);
|
||||||
|
sindenWhite.h = videoHeight - (sindenBlackWidth * 2);
|
||||||
|
windowTarget.x = sindenBlackWidth + sindenWhiteWidth;
|
||||||
|
windowTarget.y = sindenBlackWidth + sindenWhiteWidth;
|
||||||
|
windowTarget.w = videoWidth - (windowTarget.x * 2);
|
||||||
|
windowTarget.h = videoHeight - (windowTarget.y * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overscan compensation shrinks whatever the game is drawn into (the whole window, or the inside
|
// Overscan compensation shrinks whatever the game is drawn into (the whole window, or the inside
|
||||||
|
|
@ -9789,6 +10112,10 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
scaledTarget.y = windowTarget.y + (windowTarget.h - scaledTarget.h) / 2.0f;
|
scaledTarget.y = windowTarget.y + (windowTarget.h - scaledTarget.h) / 2.0f;
|
||||||
windowTarget = scaledTarget;
|
windowTarget = scaledTarget;
|
||||||
}
|
}
|
||||||
|
// Mouse positions arrive in video coordinates and are mapped into the drawn rectangle.
|
||||||
|
_global.drawTarget = windowTarget;
|
||||||
|
_global.drawScaleX = (double)videoWidth / (double)windowTarget.w;
|
||||||
|
_global.drawScaleY = (double)videoHeight / (double)windowTarget.h;
|
||||||
|
|
||||||
// Create overlay surface and its texture
|
// Create overlay surface and its texture
|
||||||
x = (int32_t)(videoWidth * OVERLAY_SCALE_DEFAULT);
|
x = (int32_t)(videoWidth * OVERLAY_SCALE_DEFAULT);
|
||||||
|
|
@ -9819,16 +10146,12 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
_progTrace("Mouse %d: %s", x, _global.mice[x].name);
|
_progTrace("Mouse %d: %s", x, _global.mice[x].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab mouse
|
|
||||||
_progTrace("Grabbing mouse");
|
|
||||||
_setMouseCaptured(true);
|
|
||||||
|
|
||||||
// Controllers are started by the event loop only for the first script in
|
// Controllers are started by the event loop only for the first script in
|
||||||
// the queue - so kick 'em here to be sure they're going.
|
// the queue - so kick 'em here to be sure they're going.
|
||||||
_startControllers();
|
_startControllers();
|
||||||
|
|
||||||
// Sound effect tracks: a fixed pool so scripts keep getting small channel numbers.
|
// Sound effect tracks: a fixed pool so scripts keep getting small channel numbers.
|
||||||
_global.effectsVolume = AUDIO_MAX_VOLUME * _global.conf->volumeNonVldp / VOLUME_MAX;
|
_global.effectsVolume = _defaultEffectsVolume();
|
||||||
_progTrace("Setting up sound effects mixer");
|
_progTrace("Setting up sound effects mixer");
|
||||||
for (x = 0; x < EFFECT_TRACKS; x++) {
|
for (x = 0; x < EFFECT_TRACKS; x++) {
|
||||||
_effectTracks[x] = MIX_CreateTrack(videoGetMixer());
|
_effectTracks[x] = MIX_CreateTrack(videoGetMixer());
|
||||||
|
|
@ -9841,7 +10164,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
MIX_SetTrackStoppedCallback(_effectTracks[x], _effectStopped, (void *)(intptr_t)x);
|
MIX_SetTrackStoppedCallback(_effectTracks[x], _effectStopped, (void *)(intptr_t)x);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The script's own defaults, now that everything they touch exists.
|
// The script's own defaults (the disc parked on frame 1 among them), now that everything they touch exists.
|
||||||
_resetScriptState();
|
_resetScriptState();
|
||||||
|
|
||||||
// Load overlay font
|
// Load overlay font
|
||||||
|
|
@ -9853,15 +10176,6 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
// The glyph background is whatever colour the top left pixel has (magenta in the shipped font).
|
// The glyph background is whatever colour the top left pixel has (magenta in the shipped font).
|
||||||
SDL_SetSurfaceColorKey(_global.consoleFontSurface, true, _surfaceCornerKey(_global.consoleFontSurface));
|
SDL_SetSurfaceColorKey(_global.consoleFontSurface, true, _surfaceCornerKey(_global.consoleFontSurface));
|
||||||
|
|
||||||
// The disc always starts parked on frame 1, paused, like discSearch(1).
|
|
||||||
if (_global.videoHandle >= 0) {
|
|
||||||
_progTrace("Parking laserdisc on frame 1");
|
|
||||||
_discSeek(1);
|
|
||||||
videoPause(_global.videoHandle);
|
|
||||||
_global.discStopped = false;
|
|
||||||
_selectDefaultAudioTrack(_global.videoHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start script
|
// Start script
|
||||||
_runScript(true);
|
_runScript(true);
|
||||||
|
|
||||||
|
|
@ -9958,12 +10272,13 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
|
|
||||||
case SDL_EVENT_MOUSE_MOTION:
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE)) {
|
if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE)) {
|
||||||
// Positions arrive in window pixels; the game works in the video's coordinates.
|
// Positions arrive in window pixels; the game works in the overlay's coordinates,
|
||||||
|
// drawn into whatever the Sinden border or overscan left of the video area.
|
||||||
SDL_ConvertEventToRenderCoordinates(_global.renderer, &event);
|
SDL_ConvertEventToRenderCoordinates(_global.renderer, &event);
|
||||||
x = (int32_t)(event.motion.x * _global.overlayScaleX);
|
x = (int32_t)((event.motion.x - _global.drawTarget.x) * _global.drawScaleX * _global.overlayScaleX);
|
||||||
y = (int32_t)(event.motion.y * _global.overlayScaleY);
|
y = (int32_t)((event.motion.y - _global.drawTarget.y) * _global.drawScaleY * _global.overlayScaleY);
|
||||||
xr = (int32_t)(event.motion.xrel * _global.overlayScaleX);
|
xr = (int32_t)(event.motion.xrel * _global.drawScaleX * _global.overlayScaleX);
|
||||||
yr = (int32_t)(event.motion.yrel * _global.overlayScaleY);
|
yr = (int32_t)(event.motion.yrel * _global.drawScaleY * _global.overlayScaleY);
|
||||||
_fireMouseMoved(0, x, y, xr, yr);
|
_fireMouseMoved(0, x, y, xr, yr);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -10032,12 +10347,14 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MANYMOUSE_EVENT_ABSMOTION:
|
case MANYMOUSE_EVENT_ABSMOTION:
|
||||||
// Absolute devices (tablets, some guns) report a position within a range.
|
// Absolute devices (tablets, some guns) report a position within a range that spans
|
||||||
|
// the video; the game sits in whatever the Sinden border or overscan left of it.
|
||||||
if (mouseEvent.maxval > mouseEvent.minval) {
|
if (mouseEvent.maxval > mouseEvent.minval) {
|
||||||
|
mouseFraction = (double)(mouseEvent.value - mouseEvent.minval) / (double)(mouseEvent.maxval - mouseEvent.minval);
|
||||||
if (mouseEvent.item == 0) {
|
if (mouseEvent.item == 0) {
|
||||||
mouse->x = (int32_t)((int64_t)(mouseEvent.value - mouseEvent.minval) * videoWidth / (mouseEvent.maxval - mouseEvent.minval));
|
mouse->x = (int32_t)((mouseFraction * videoWidth - _global.drawTarget.x) * _global.drawScaleX);
|
||||||
} else {
|
} else {
|
||||||
mouse->y = (int32_t)((int64_t)(mouseEvent.value - mouseEvent.minval) * videoHeight / (mouseEvent.maxval - mouseEvent.minval));
|
mouse->y = (int32_t)((mouseFraction * videoHeight - _global.drawTarget.y) * _global.drawScaleY);
|
||||||
}
|
}
|
||||||
x = (int32_t)(mouse->x * _global.overlayScaleX);
|
x = (int32_t)(mouse->x * _global.overlayScaleX);
|
||||||
y = (int32_t)(mouse->y * _global.overlayScaleY);
|
y = (int32_t)(mouse->y * _global.overlayScaleY);
|
||||||
|
|
@ -10106,9 +10423,9 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
// Clear entire display to black
|
// Clear entire display to black
|
||||||
SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
||||||
SDL_RenderClear(_global.renderer);
|
SDL_RenderClear(_global.renderer);
|
||||||
// Sinden Gun Border
|
// Sinden Gun Border, outer ring first; the game then covers the middle
|
||||||
if (sindenWhite.x >= 0) {
|
if (sindenBorder) {
|
||||||
if (sindenBlack.x >= 0) {
|
if (sindenBlackWidth > 0) {
|
||||||
SDL_SetRenderDrawColor(_global.renderer, sindenBlackColor.r, sindenBlackColor.g, sindenBlackColor.b, sindenBlackColor.a);
|
SDL_SetRenderDrawColor(_global.renderer, sindenBlackColor.r, sindenBlackColor.g, sindenBlackColor.b, sindenBlackColor.a);
|
||||||
SDL_RenderFillRect(_global.renderer, &sindenBlack);
|
SDL_RenderFillRect(_global.renderer, &sindenBlack);
|
||||||
}
|
}
|
||||||
|
|
@ -10176,9 +10493,16 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
_effectTracks[x] = NULL;
|
_effectTracks[x] = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop controllers and release the mouse while the script can still hear the releases
|
||||||
|
_progTrace("Stopping controllers");
|
||||||
|
_stopControllers();
|
||||||
|
_progTrace("Releasing mouse");
|
||||||
|
_setMouseCaptured(false);
|
||||||
|
|
||||||
// Stop Lua
|
// Stop Lua
|
||||||
_progTrace("Stopping Lua");
|
_progTrace("Stopping Lua");
|
||||||
lua_close(_global.luaContext);
|
lua_close(_global.luaContext);
|
||||||
|
_global.luaContext = NULL;
|
||||||
|
|
||||||
// Free overlay & overlay font
|
// Free overlay & overlay font
|
||||||
_progTrace("Destroying overlay");
|
_progTrace("Destroying overlay");
|
||||||
|
|
@ -10203,13 +10527,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop controllers
|
|
||||||
_progTrace("Stopping controllers");
|
|
||||||
_stopControllers();
|
|
||||||
|
|
||||||
// Stop mice
|
// Stop mice
|
||||||
_progTrace("Releasing mouse");
|
|
||||||
_setMouseCaptured(false);
|
|
||||||
_progTrace("Stopping ManyMouse");
|
_progTrace("Stopping ManyMouse");
|
||||||
ManyMouse_Quit();
|
ManyMouse_Quit();
|
||||||
|
|
||||||
|
|
|
||||||
14
src/singe.h
14
src/singe.h
|
|
@ -51,6 +51,18 @@ typedef enum SindenModeE {
|
||||||
} SindenModeE;
|
} SindenModeE;
|
||||||
|
|
||||||
|
|
||||||
|
// Options typed on the command line: a games.dat entry found beside a loose script leaves these alone.
|
||||||
|
typedef enum GivenE {
|
||||||
|
GIVEN_VIDEO = 1 << 0, // -v or -D
|
||||||
|
GIVEN_STRETCH = 1 << 1,
|
||||||
|
GIVEN_NO_MOUSE = 1 << 2,
|
||||||
|
GIVEN_RESOLUTION = 1 << 3, // -x or -y
|
||||||
|
GIVEN_SINDEN = 1 << 4,
|
||||||
|
GIVEN_AUDIO_TRACK = 1 << 5,
|
||||||
|
GIVEN_AUDIO_DELAY = 1 << 6,
|
||||||
|
GIVEN_CANVAS = 1 << 7
|
||||||
|
} GivenE;
|
||||||
|
|
||||||
typedef struct ConfigS {
|
typedef struct ConfigS {
|
||||||
char *videoFile;
|
char *videoFile;
|
||||||
char *scriptFile;
|
char *scriptFile;
|
||||||
|
|
@ -58,6 +70,7 @@ typedef struct ConfigS {
|
||||||
int32_t entry; // Which games.dat entry a database launch runs, from 1
|
int32_t entry; // Which games.dat entry a database launch runs, from 1
|
||||||
char *toolSource; // Argument of --pack, --unpack, or --patch
|
char *toolSource; // Argument of --pack, --unpack, or --patch
|
||||||
ToolModeE toolMode;
|
ToolModeE toolMode;
|
||||||
|
uint32_t given; // GivenE bits
|
||||||
bool dataDirGiven; // -d was on the command line
|
bool dataDirGiven; // -d was on the command line
|
||||||
char *dataDir;
|
char *dataDir;
|
||||||
char *dataDirBase;
|
char *dataDirBase;
|
||||||
|
|
@ -94,6 +107,7 @@ typedef struct ConfigS {
|
||||||
|
|
||||||
|
|
||||||
ConfigT *confFromDatabase(const ConfigT *conf);
|
ConfigT *confFromDatabase(const ConfigT *conf);
|
||||||
|
ConfigT *confFromGamesDat(const ConfigT *conf);
|
||||||
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf);
|
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ static bool _ensureDirectory(const char *path, const mode_t mode) {
|
||||||
struct stat sb;
|
struct stat sb;
|
||||||
|
|
||||||
if (stat(path, &sb) != 0) {
|
if (stat(path, &sb) != 0) {
|
||||||
// Does not exist - create it.
|
// Does not exist - create it. (Windows takes no mode.)
|
||||||
|
(void)mode;
|
||||||
return (ourMkdir(path, mode) >= 0);
|
return (ourMkdir(path, mode) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
167
src/vfs.c
167
src/vfs.c
|
|
@ -75,9 +75,16 @@ typedef struct DatabaseS {
|
||||||
struct DatabaseS *next;
|
struct DatabaseS *next;
|
||||||
} DatabaseT;
|
} DatabaseT;
|
||||||
|
|
||||||
|
// One name of a directory listing being gathered. The filesystem is listed before the database,
|
||||||
|
// so on a case-insensitive tie the lower insertion order is the loose spelling and it wins.
|
||||||
|
typedef struct ListEntryS {
|
||||||
|
char *name;
|
||||||
|
int32_t order;
|
||||||
|
} ListEntryT;
|
||||||
|
|
||||||
// A directory listing being gathered: names are appended freely and sorted and de-duplicated once.
|
// A directory listing being gathered: names are appended freely and sorted and de-duplicated once.
|
||||||
typedef struct ListS {
|
typedef struct ListS {
|
||||||
char **names;
|
ListEntryT *entries;
|
||||||
int32_t count;
|
int32_t count;
|
||||||
int32_t capacity;
|
int32_t capacity;
|
||||||
} ListT;
|
} ListT;
|
||||||
|
|
@ -109,9 +116,11 @@ static bool _assetExists(DatabaseT *db, const char *key);
|
||||||
static bool _assetSize(DatabaseT *db, const char *key, int64_t *size);
|
static bool _assetSize(DatabaseT *db, const char *key, int64_t *size);
|
||||||
static char *_bindListRange(DatabaseT *db, const char *key);
|
static char *_bindListRange(DatabaseT *db, const char *key);
|
||||||
static bool _cacheCurrent(const TargetT *target);
|
static bool _cacheCurrent(const TargetT *target);
|
||||||
|
static bool _climbsAbove(const char *path);
|
||||||
static void _databaseClose(DatabaseT *db);
|
static void _databaseClose(DatabaseT *db);
|
||||||
static DatabaseT *_databaseOpen(const char *path);
|
static DatabaseT *_databaseOpen(const char *path);
|
||||||
static void _databasesClose(void);
|
static void _databasesClose(void);
|
||||||
|
static size_t _engineNameLength(const char *name);
|
||||||
static bool _fileModified(const char *path, int64_t *size, int64_t *modified);
|
static bool _fileModified(const char *path, int64_t *size, int64_t *modified);
|
||||||
static bool _hasDatabaseExtension(const char *path);
|
static bool _hasDatabaseExtension(const char *path);
|
||||||
static bool _hasParentComponent(const char *norm);
|
static bool _hasParentComponent(const char *norm);
|
||||||
|
|
@ -206,6 +215,34 @@ static bool _cacheCurrent(const TargetT *target) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// True when the ".." components of a normalised path climb above where it starts: "a/../b" stays
|
||||||
|
// put, "a/../../b" and "../b" do not.
|
||||||
|
static bool _climbsAbove(const char *path) {
|
||||||
|
const char *p = path;
|
||||||
|
int32_t depth = 0;
|
||||||
|
|
||||||
|
while (*p != 0) {
|
||||||
|
if (*p == '/') {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((p[0] == '.') && (p[1] == '.') && ((p[2] == '/') || (p[2] == 0))) {
|
||||||
|
if (depth == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
depth--;
|
||||||
|
} else {
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
while ((*p != 0) && (*p != '/')) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Releases a database that is not (or no longer) in the open list.
|
// Releases a database that is not (or no longer) in the open list.
|
||||||
static void _databaseClose(DatabaseT *db) {
|
static void _databaseClose(DatabaseT *db) {
|
||||||
sqlite3_finalize(db->assetStmt);
|
sqlite3_finalize(db->assetStmt);
|
||||||
|
|
@ -336,6 +373,28 @@ static void _databasesClose(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The length of the engine-owned prefix of a name, or 0 when the engine does not own it. The
|
||||||
|
// engine owns Singe/ and the data directory, which resolve on the filesystem even inside a packed
|
||||||
|
// game. Only the leading component counts: "x/../Singe/y" is not an engine name.
|
||||||
|
static size_t _engineNameLength(const char *name) {
|
||||||
|
size_t length = strlen(VFS_ENGINE_DIRECTORY);
|
||||||
|
|
||||||
|
if ((strncasecmp(name, VFS_ENGINE_DIRECTORY, length) == 0) && ((name[length] == '/') || (name[length] == 0))) {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
// The data directory base, when it is a real prefix: "data/" or an absolute path. With no
|
||||||
|
// --datadir the base is "./", which normalises to nothing and must not match everything.
|
||||||
|
if ((_dataDirKey != NULL) && (_dataDirKey[0] != 0)) {
|
||||||
|
length = strlen(_dataDirKey);
|
||||||
|
if ((strncmp(name, _dataDirKey, length) == 0) && ((name[length] == '/') || (name[length] == 0))) {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool _fileModified(const char *path, int64_t *size, int64_t *modified) {
|
static bool _fileModified(const char *path, int64_t *size, int64_t *modified) {
|
||||||
struct stat info;
|
struct stat info;
|
||||||
|
|
||||||
|
|
@ -380,44 +439,42 @@ static bool _isAbsolute(const char *name) {
|
||||||
|
|
||||||
// Names the engine owns resolve on the filesystem even inside a packed game: Singe/ and the data directory.
|
// Names the engine owns resolve on the filesystem even inside a packed game: Singe/ and the data directory.
|
||||||
static bool _isEngineName(const char *name) {
|
static bool _isEngineName(const char *name) {
|
||||||
size_t length = strlen(VFS_ENGINE_DIRECTORY);
|
return _engineNameLength(name) > 0;
|
||||||
|
|
||||||
if ((strncasecmp(name, VFS_ENGINE_DIRECTORY, length) == 0) && ((name[length] == '/') || (name[length] == 0))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// The data directory base, when it is a real prefix: "data/" or an absolute path. With no
|
|
||||||
// --datadir the base is "./", which normalises to nothing and must not match everything.
|
|
||||||
if ((_dataDirKey != NULL) && (_dataDirKey[0] != 0)) {
|
|
||||||
length = strlen(_dataDirKey);
|
|
||||||
return (strncmp(name, _dataDirKey, length) == 0) && ((name[length] == '/') || (name[length] == 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Appends a name to a listing. Duplicates are dropped by _listFinish.
|
// Appends a name to a listing. Duplicates are dropped by _listFinish.
|
||||||
static void _listAdd(ListT *list, const char *name) {
|
static void _listAdd(ListT *list, const char *name) {
|
||||||
char **grown = NULL;
|
ListEntryT *grown = NULL;
|
||||||
|
|
||||||
if (list->count == list->capacity) {
|
if (list->count == list->capacity) {
|
||||||
list->capacity = (list->capacity == 0) ? LIST_INITIAL_CAPACITY : list->capacity * 2;
|
list->capacity = (list->capacity == 0) ? LIST_INITIAL_CAPACITY : list->capacity * 2;
|
||||||
grown = (char **)realloc(list->names, (size_t)list->capacity * sizeof(char *));
|
grown = (ListEntryT *)realloc(list->entries, (size_t)list->capacity * sizeof(ListEntryT));
|
||||||
if (grown == NULL) {
|
if (grown == NULL) {
|
||||||
utilDie("Out of memory listing a directory.");
|
utilDie("Out of memory listing a directory.");
|
||||||
}
|
}
|
||||||
list->names = grown;
|
list->entries = grown;
|
||||||
}
|
}
|
||||||
list->names[list->count] = strdup(name);
|
list->entries[list->count].name = strdup(name);
|
||||||
if (list->names[list->count] == NULL) {
|
list->entries[list->count].order = list->count;
|
||||||
|
if (list->entries[list->count].name == NULL) {
|
||||||
utilDie("Out of memory listing a directory.");
|
utilDie("Out of memory listing a directory.");
|
||||||
}
|
}
|
||||||
list->count++;
|
list->count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Case-insensitive by name, then by insertion order, so equal names sort deterministically.
|
||||||
static int _listCompare(const void *a, const void *b) {
|
static int _listCompare(const void *a, const void *b) {
|
||||||
return utilStricmp(*(char *const *)a, *(char *const *)b);
|
const ListEntryT *x = (const ListEntryT *)a;
|
||||||
|
const ListEntryT *y = (const ListEntryT *)b;
|
||||||
|
int result = utilStricmp(x->name, y->name);
|
||||||
|
|
||||||
|
if (result == 0) {
|
||||||
|
result = (x->order > y->order) - (x->order < y->order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -438,24 +495,34 @@ static void _listDirectory(const char *path, ListT *list) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Sorts a listing and drops names that repeat (case-insensitively, like the keys). NULL when empty.
|
// Sorts a listing and drops names that repeat (case-insensitively, like the keys), keeping the
|
||||||
|
// first added. NULL when empty.
|
||||||
static char **_listFinish(ListT *list, int32_t *count) {
|
static char **_listFinish(ListT *list, int32_t *count) {
|
||||||
|
char **names = NULL;
|
||||||
int32_t i = 0;
|
int32_t i = 0;
|
||||||
int32_t kept = 0;
|
int32_t kept = 0;
|
||||||
|
|
||||||
if (list->count > 0) {
|
*count = 0;
|
||||||
qsort(list->names, (size_t)list->count, sizeof(char *), _listCompare);
|
if (list->count == 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
qsort(list->entries, (size_t)list->count, sizeof(ListEntryT), _listCompare);
|
||||||
|
names = (char **)malloc((size_t)list->count * sizeof(char *));
|
||||||
|
if (names == NULL) {
|
||||||
|
utilDie("Out of memory listing a directory.");
|
||||||
}
|
}
|
||||||
for (i = 0; i < list->count; i++) {
|
for (i = 0; i < list->count; i++) {
|
||||||
if ((kept > 0) && (utilStricmp(list->names[kept - 1], list->names[i]) == 0)) {
|
if ((kept > 0) && (utilStricmp(names[kept - 1], list->entries[i].name) == 0)) {
|
||||||
free(list->names[i]);
|
free(list->entries[i].name);
|
||||||
} else {
|
} else {
|
||||||
list->names[kept++] = list->names[i];
|
names[kept++] = list->entries[i].name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
free(list->entries);
|
||||||
|
memset(list, 0, sizeof(*list));
|
||||||
*count = kept;
|
*count = kept;
|
||||||
|
|
||||||
return list->names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -480,10 +547,17 @@ static char *_normalise(const char *name) {
|
||||||
}
|
}
|
||||||
out[o++] = c;
|
out[o++] = c;
|
||||||
}
|
}
|
||||||
|
// A trailing "." is the directory itself ("." and "Overlay/." are the root and "Overlay").
|
||||||
|
if ((o >= 1) && (out[o - 1] == '.') && ((o == 1) || (out[o - 2] == '/'))) {
|
||||||
|
o--;
|
||||||
|
}
|
||||||
// A trailing separator names the same thing ("Overlay/" is "Overlay"), except for the root.
|
// A trailing separator names the same thing ("Overlay/" is "Overlay"), except for the root.
|
||||||
while ((o > 1) && (out[o - 1] == '/')) {
|
while ((o > 1) && (out[o - 1] == '/')) {
|
||||||
o--;
|
o--;
|
||||||
}
|
}
|
||||||
|
if ((o == 1) && (out[0] == '/')) {
|
||||||
|
o = 0;
|
||||||
|
}
|
||||||
out[o] = 0;
|
out[o] = 0;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|
@ -523,8 +597,8 @@ static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool s
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
size = sqlite3_column_int64(db->assetStmt, 0);
|
size = sqlite3_column_int64(db->assetStmt, 0);
|
||||||
if (size < 0) {
|
if ((size < 0) || ((uint64_t)size >= (uint64_t)SIZE_MAX)) {
|
||||||
utilDie("%s in %s has a negative size.", key, db->path);
|
utilDie("%s in %s has an impossible size (%" PRId64 ").", key, db->path, size);
|
||||||
}
|
}
|
||||||
data = sdlMemory ? (uint8_t *)SDL_malloc((size_t)size + 1) : (uint8_t *)malloc((size_t)size + 1);
|
data = sdlMemory ? (uint8_t *)SDL_malloc((size_t)size + 1) : (uint8_t *)malloc((size_t)size + 1);
|
||||||
if (data == NULL) {
|
if (data == NULL) {
|
||||||
|
|
@ -574,32 +648,37 @@ static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Decides where a name lives. Returns false for an empty name, and for a relative name inside a
|
// Decides where a name lives. Returns false for a relative name inside a packed game that climbs
|
||||||
// packed game that climbs out with "..": the packer refused it and so does the lookup.
|
// out with "..": the packer refused it and so does the lookup.
|
||||||
static bool _resolve(const char *name, TargetT *target) {
|
static bool _resolve(const char *name, TargetT *target) {
|
||||||
char *norm = _normalise(name);
|
char *norm = _normalise(name);
|
||||||
char *prefix = NULL;
|
char *prefix = NULL;
|
||||||
const char *inner = NULL;
|
const char *inner = NULL;
|
||||||
|
const char *relative = NULL;
|
||||||
char *p = NULL;
|
char *p = NULL;
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
size_t length = strlen(norm);
|
size_t length = strlen(norm);
|
||||||
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
|
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
|
||||||
|
size_t engineLength = 0;
|
||||||
|
|
||||||
memset(target, 0, sizeof(*target));
|
memset(target, 0, sizeof(*target));
|
||||||
if (length == 0) {
|
// "." (or "") is the current directory of a loose game and the root of a packed one.
|
||||||
|
if ((length == 0) && (_container == NULL)) {
|
||||||
|
target->path = strdup(".");
|
||||||
free(norm);
|
free(norm);
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A path that passes through a database file addresses its contents: "Games/DLe.game/Overlay/x.png".
|
// A path that passes through a database file addresses its contents: "Games/DLe.game/Overlay/x.png".
|
||||||
// Absolute paths included, so a tool can reach the packed games beside it from inside its own.
|
// Absolute paths included, so a tool can reach the packed games beside it from inside its own.
|
||||||
for (i = extLen; i < length; i++) {
|
// The database file itself ("Games/DLe.game", or with a trailing separator or ".") is its root.
|
||||||
if ((norm[i] == '/') && (strncasecmp(norm + i - extLen, VFS_DATABASE_EXTENSION, extLen) == 0)) {
|
for (i = extLen; i <= length; i++) {
|
||||||
|
if (((norm[i] == '/') || (norm[i] == 0)) && (strncasecmp(norm + i - extLen, VFS_DATABASE_EXTENSION, extLen) == 0)) {
|
||||||
prefix = utilStrndup(norm, i);
|
prefix = utilStrndup(norm, i);
|
||||||
if (utilFileExists(prefix)) {
|
if (utilFileExists(prefix)) {
|
||||||
target->db = _databaseOpen(prefix);
|
target->db = _databaseOpen(prefix);
|
||||||
if (target->db != NULL) {
|
if (target->db != NULL) {
|
||||||
inner = 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, OVERLAY_DIRECTORY);
|
||||||
target->db->cache = _overlayFor(_dataDirBase, _dataDir, prefix, false, CACHE_DIRECTORY);
|
target->db->cache = _overlayFor(_dataDirBase, _dataDir, prefix, false, CACHE_DIRECTORY);
|
||||||
|
|
@ -612,14 +691,21 @@ static bool _resolve(const char *name, TargetT *target) {
|
||||||
prefix = NULL;
|
prefix = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The escape rule covers everything inside a database and every relative name of a packed game.
|
||||||
|
// A name the engine owns never reaches a database, so it may use ".." as long as it stays below
|
||||||
|
// the engine prefix ("data/game/script/../controls.cfg" is the documented override); climbing
|
||||||
|
// above the prefix is an escape. Ownership is decided by the leading component alone, so
|
||||||
|
// "x/../Singe/y" is not an engine name and is refused like any other ".." in a packed game.
|
||||||
if ((target->db != NULL) || ((_container != NULL) && !_isAbsolute(norm))) {
|
if ((target->db != NULL) || ((_container != NULL) && !_isAbsolute(norm))) {
|
||||||
if (_hasParentComponent((target->db != NULL) ? inner : norm)) {
|
relative = (target->db != NULL) ? inner : norm;
|
||||||
|
engineLength = _engineNameLength(relative);
|
||||||
|
if ((engineLength > 0) ? _climbsAbove(relative + engineLength) : _hasParentComponent(relative)) {
|
||||||
memset(target, 0, sizeof(*target));
|
memset(target, 0, sizeof(*target));
|
||||||
free(norm);
|
free(norm);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((target->db == NULL) && (_container != NULL) && !_isAbsolute(norm) && !_isEngineName(norm)) {
|
if ((target->db == NULL) && (_container != NULL) && !_isAbsolute(norm) && (engineLength == 0)) {
|
||||||
target->db = _container;
|
target->db = _container;
|
||||||
inner = norm;
|
inner = norm;
|
||||||
}
|
}
|
||||||
|
|
@ -732,6 +818,11 @@ char *vfsFilePath(const char *name, bool forWriting) {
|
||||||
if (!_resolve(name, &target)) {
|
if (!_resolve(name, &target)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
// The root of a packed game is a directory, never a file to open or a place to write.
|
||||||
|
if ((target.db != NULL) && (target.key[0] == 0)) {
|
||||||
|
_targetFree(&target);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
if ((target.db == NULL) || utilFileExists(target.path)) {
|
if ((target.db == NULL) || utilFileExists(target.path)) {
|
||||||
path = target.path;
|
path = target.path;
|
||||||
target.path = NULL;
|
target.path = NULL;
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ screenshot or two and quits by itself; the results are in screenshots/.
|
||||||
| scene24.singe | 24 | 24.1 | Water: a pool and a river; crates float, sink and drift, the Fox swims across, a raft drives |
|
| scene24.singe | 24 | 24.1 | Water: a pool and a river; crates float, sink and drift, the Fox swims across, a raft drives |
|
||||||
| scene25.singe | 25 | 25.1 | Ragdolls: a running Fox crumples at a wall, one falls down stairs, one lies under motor strength |
|
| scene25.singe | 25 | 25.1 | Ragdolls: a running Fox crumples at a wall, one falls down stairs, one lies under motor strength |
|
||||||
| scene26.singe | 26 | 26.1 | Soft bodies: a flag, a sheet catching a crate, a swinging rope, a balloon |
|
| scene26.singe | 26 | 26.1 | Soft bodies: a flag, a sheet catching a crate, a swinging rope, a balloon |
|
||||||
| scene27.singe | 27 | - | The Khronos Sponza atrium (Models/Sponza.glb, 18 MB: textures compressed to KTX2 with util/compressTextures.py --etc1s, then packed with util/packGlb.py; 99 MB of texture memory instead of 393 MB) under a sun with shadows, six braziers with flame, smoke and dust particles, each crackling in place (crackle.wav from util/makeCrackle.py); no disc, -x 1280 -y 720 -C 1280x720 |
|
| scene27.singe | 27 | - | The Khronos Sponza atrium (Models/Sponza.glb, 18 MB: textures compressed to KTX2 with util/compressTextures.py --etc1s, then packed with util/packGlb.py; 99 MB of texture memory instead of 393 MB) under a sun with cascaded shadows, lit from above by a sky (sky.png) in place of a flat ambient, fog down the nave, the ACES tone curve, six braziers with flame, smoke and dust particles, each crackling in place (crackle.wav from util/makeCrackle.py); no disc, -x 1280 -y 720 -C 1280x720 (screenshots/sponza-nave, -atrium, -gallery and -columns.png are the four stops at 2560x1920) |
|
||||||
| scene28.singe | 28 | - | Materials: roughness and metallic sphere rows before a brick wall with normal, emissive and metallic-roughness maps (brick*.png), each stop a different exposure and tone curve; no disc, -x 1280 -y 720 -C 1280x720 |
|
| scene28.singe | 28 | - | Materials: roughness and metallic sphere rows before a brick wall with normal, emissive and metallic-roughness maps (brick*.png), each stop a different exposure and tone curve; no disc, -x 1280 -y 720 -C 1280x720 |
|
||||||
| scene29.singe | 29 | - | Animation blending: a Fox crossfading Walk to Run, one walking with Survey on a layer masked to its neck, one easing out of a ragdoll into Survey; no disc, -x 1280 -y 720 -C 1280x720 |
|
| scene29.singe | 29 | - | Animation blending: a Fox crossfading Walk to Run, one walking with Survey on a layer masked to its neck, one easing out of a ragdoll into Survey; no disc, -x 1280 -y 720 -C 1280x720 |
|
||||||
| scene30.singe | 30 | - | Sky and fog: the dragon on its plate and metal spheres under an equirectangular sky (sky.png from util/makeSky.py) lighting the scene, fog down an avenue of posts; no disc, -x 1280 -y 720 -C 1280x720 |
|
| scene30.singe | 30 | - | Sky and fog: the dragon on its plate and metal spheres under an equirectangular sky (sky.png from util/makeSky.py) lighting the scene, fog down an avenue of posts; no disc, -x 1280 -y 720 -C 1280x720 |
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
-- The Khronos Sponza atrium, packed with util/packGlb.py from the glTF-Sample-Models 2.0 Sponza.
|
-- The Khronos Sponza atrium, packed with util/packGlb.py from the glTF-Sample-Models 2.0 Sponza.
|
||||||
-- A warm sun falls through the roof opening onto the atrium floor with hard shadows, braziers along the nave
|
-- A warm sun falls through the roof opening onto the atrium floor with cascaded shadows, the sky lights
|
||||||
-- add pools of firelight with particle flames and smoke, each crackling from where it stands (the
|
-- the stone from above in place of a flat ambient, a breath of haze softens the far end of the nave,
|
||||||
-- listener is the camera), and dust drifts in the sunbeams; the camera walks the ground floor,
|
-- braziers along the nave add pools of firelight with particle flames and smoke, each crackling from
|
||||||
-- crosses the atrium, climbs to the gallery, then passes the columns.
|
-- where it stands (the listener is the camera), dust drifts in the sunbeams, and the frame is graded
|
||||||
|
-- through the ACES curve; the camera walks the ground floor, crosses the atrium, climbs to the
|
||||||
|
-- gallery, then passes the columns.
|
||||||
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||||
local frames = 0
|
local frames = 0
|
||||||
|
|
||||||
|
|
@ -11,7 +13,13 @@ sceneEnable(true)
|
||||||
sceneSetBackground(120, 150, 200, 255)
|
sceneSetBackground(120, 150, 200, 255)
|
||||||
sceneSetAmbient(120, 126, 150)
|
sceneSetAmbient(120, 126, 150)
|
||||||
sceneSetShadowSize(2048)
|
sceneSetShadowSize(2048)
|
||||||
|
sceneSetShadowCascades(4)
|
||||||
|
sceneSetShadowDistance(40)
|
||||||
sceneSetBloom(1.2, 0.25)
|
sceneSetBloom(1.2, 0.25)
|
||||||
|
sceneSetSky("testScripts/sky.png")
|
||||||
|
sceneSetSkyIntensity(0.35)
|
||||||
|
sceneSetFog(200, 205, 215, 35, 140)
|
||||||
|
sceneSetTonemap(TONEMAP_ACES)
|
||||||
|
|
||||||
local model = modelLoad("testScripts/Models/Sponza.glb")
|
local model = modelLoad("testScripts/Models/Sponza.glb")
|
||||||
local sponza = modelInstance(model)
|
local sponza = modelInstance(model)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue