newdark: (ughhhh) binding physics states, ambient sounds, doors, triggers, songs (missing things like XXMIX_S), to-do: more I/O, fix trigger bounds being weird, optimize physics bodies because it eats half my frametime | added dir listing for VFS, added queueing pending sounds to seamlessly transition (streamed audio only), probably a lot of other things just to make the former features happen..............

This commit is contained in:
ecker 2026-07-19 02:38:10 -05:00
parent bd6b41a7a7
commit 7bd018dae0
35 changed files with 2187 additions and 286 deletions

View File

@ -356,8 +356,8 @@
"max": 0.1 // 0.2
},
"debug draw": {
"static": true,
"dynamic": true,
"static": false,
"dynamic": false,
"trigger": false,
"contacts": false,
"constraints": false,

View File

@ -1,5 +1,5 @@
{
"assets": ["./scripts/door.lua"],
"assets": ["./scripts/valve/door.lua"],
"behaviors": [
"AudioEmitterBehavior"
],

View File

@ -0,0 +1,86 @@
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local soundMeta = darkMeta["sound"] or {}
local schemaName = soundMeta["schema"] or ""
local flags = soundMeta["flags"] or 0
local baseVolume = tonumber(soundMeta["volume"]) or 1.0
local radius = tonumber(soundMeta["radius"]) or 0.0
local startOn = (math.floor(flags / 1) % 2) ~= 0
local environmental = (math.floor(flags / 2) % 2) ~= 0
local isMusic = (math.floor(flags / 16) % 2) ~= 0
local isPlaying = false
local function playSound()
if isPlaying or schemaName == "" then return end
local wavList = soundMeta["wavs"]
local url = ""
if wavList and #wavList > 0 then
local totalWeight = 0
for _, wavData in ipairs(wavList) do
totalWeight = totalWeight + (tonumber(wavData.weight) or 1)
end
local roll = math.random() * totalWeight
local current = 0
for _, wavData in ipairs(wavList) do
current = current + (tonumber(wavData.weight) or 1)
if roll <= current then
url = wavData.uri
break
end
end
end
if url == "" then return end
isPlaying = true
local resolvedUrl = string.resolveURI(url, metadata["system"]["root"])
local schemaVol = tonumber(soundMeta["schema_volume"]) or 0
local overrideVol = tonumber(soundMeta["volume"]) or 0
local baseVolume = 1.0
if overrideVol ~= 0 then baseVolume = math.pow(10, overrideVol / 2000.0) end
local finalVolume = baseVolume * math.pow(10, schemaVol / 2000.0)
local payload = {
filename = resolvedUrl,
spatial = not environmental,
streamed = soundMeta["stream"] == true,
volume = finalVolume,
unique = true,
loop = not (soundMeta["play_once"] == true)
}
if radius > 0 and not environmental then
payload.maxDistance = radius
payload.rolloffFactor = 1.0
end
ent:callHook("sound:Emit.%UID%", payload)
end
local function stopSound()
if not isPlaying or schemaName == "" then return end
isPlaying = false
ent:callHook("sound:Stop.%UID%", {
})
end
ent:addHook("dark:Message.%UID%", function(payload)
local msg = payload.message
if msg == "TurnOn" then
playSound()
elseif msg == "TurnOff" then
stopSound()
end
end)
if startOn then
playSound()
end

View File

@ -0,0 +1,209 @@
local ent = ent
local scene = entities.currentScene()
local transform = ent:getComponent("Transform")
local physicsBody = ent:getComponent("PhysicsBody")
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local doorMeta = darkMeta["door"] or {}
local schemaDb = doorMeta["schema_db"] or (darkMeta["schema_db"] or {})
local classTags = doorMeta["class_tags"] or (darkMeta["class_tags"] or "")
local state = 0
local isRotating = doorMeta["is_rotating"] == true
local closedPos = tonumber(doorMeta["closed"]) or 0.0
local openPos = tonumber(doorMeta["open"]) or 90.0
local speed = tonumber(doorMeta["speed"]) or 2.0
local axisRaw = tonumber(doorMeta["axis"]) or 2
local localAxis = Vector3f(0, 1, 0)
if axisRaw == 0 then
localAxis = Vector3f(1, 0, 0)
elseif axisRaw == 1 then
localAxis = Vector3f(0, 0, 1)
end
local currentWav = ""
local currentSchemaName = ""
local moveAxis = transform.orientation:rotate( localAxis )
if isRotating then
closedPos = math.rad(closedPos)
openPos = math.rad(openPos)
speed = math.rad(speed)
else
closedPos = closedPos * 0.7
openPos = openPos * 0.7
end
local targetDistance = math.abs(openPos - closedPos)
local currentDistance = 0.0
local status = tonumber(doorMeta["status"]) or 0
if status == 1 then
state = 2
currentDistance = targetDistance
end
local polarity = (openPos > closedPos) and 1 or -1
local function stopCurrentSound()
if currentWav ~= "" then
ent:callHook("sound:Stop.%UID%", { filename = currentWav })
currentWav = ""
end
end
local function playEnvSchema(newState)
local bestMatch = nil
local bestScore = -1
local myDoorType = nil
if classTags and classTags ~= "" then
myDoorType = string.match(classTags, "DoorType%s+([^%s,]+)")
end
for _, schema in ipairs(schemaDb) do
local sTags = schema.tags or ""
local isStateChange = string.find(sTags, "Event StateChange")
local openStateVals = string.match(sTags, "OpenState%s+([^,]+)")
local matchesState = openStateVals and string.find(openStateVals, newState)
if isStateChange and matchesState then
local score = 0
if myDoorType then
if string.find(sTags, myDoorType) then
score = score + 10
elseif string.find(sTags, "DoorType") then
score = score - 10
end
else
if string.find(sTags, "DoorType") then
score = score - 5
end
end
if score > bestScore then
bestScore = score
bestMatch = schema
end
end
end
if bestMatch then
if (newState == "Open" or newState == "Closed") and bestMatch.name == currentSchemaName then
return
end
currentSchemaName = bestMatch.name
if bestMatch.wavs and #bestMatch.wavs > 0 then
local pick = bestMatch.wavs[math.random(#bestMatch.wavs)]
local resolvedUrl = string.resolveURI(pick, metadata["system"]["root"])
stopCurrentSound()
currentWav = resolvedUrl
ent:callHook("sound:Emit.%UID%", {
filename = resolvedUrl,
spatial = true,
volume = 1.0,
maxDistance = 15.0,
rolloffFactor = 1.0,
unique = true
})
end
end
end
local function setDoorState(newState)
if newState == "Open" and (state == 0 or state == 3) then
state = 1
playEnvSchema("Opening")
elseif newState == "Close" and (state == 2 or state == 1) then
state = 3
playEnvSchema("Closing")
end
end
-- tick
ent:bind("tick", function(self)
local deltaMove = 0
local step = time.delta() * speed
if state == 1 then
deltaMove = step
currentDistance = currentDistance + step
if currentDistance >= targetDistance then
deltaMove = deltaMove - (currentDistance - targetDistance)
currentDistance = targetDistance
state = 2
-- Stop the "Opening" sound
ent:callHook("sound:Stop.%UID%", {})
-- Play the "Open" (Latch) sound!
playEnvSchema("Open")
end
elseif state == 3 then
deltaMove = -step
currentDistance = currentDistance - step
if currentDistance <= 0 then
deltaMove = deltaMove - currentDistance
currentDistance = 0
state = 0
-- Stop the "Closing" sound
ent:callHook("sound:Stop.%UID%", {})
-- Play the "Closed" (Latch) sound!
playEnvSchema("Closed")
end
end
if deltaMove ~= 0 then
local finalMove = deltaMove * polarity
if isRotating then
local rot = Quaternion.axisAngle(localAxis, finalMove)
if physicsBody:initialized() then
physicsBody:applyRotation(rot)
else
transform:rotate(rot)
end
else
local vec = moveAxis * finalMove
if physicsBody:initialized() then
physicsBody:setVelocity(moveAxis * (finalMove / time.delta()))
else
transform.position = transform.position + vec
end
end
else
if not isRotating and physicsBody:initialized() then
physicsBody:setVelocity(Vector3f(0, 0, 0))
end
end
end)
ent:addHook("dark:Message.%UID%", function(payload)
local msg = payload.message
if msg == "TurnOn" then
setDoorState("Open")
elseif msg == "TurnOff" then
setDoorState("Close")
elseif msg == "Toggle" then
if state == 0 or state == 3 then
setDoorState("Open")
else
setDoorState("Close")
end
end
end)

View File

@ -0,0 +1,62 @@
local ent = ent
local scene = entities.currentScene()
local metadata = ent:getComponent("Metadata")
local FLAVOR_CONTROL_DEVICE = 1
_G.DarkTargets = _G.DarkTargets or {}
local darkMeta = metadata["dark"] or {}
if darkMeta["id"] then
_G.DarkTargets[darkMeta["id"]] = ent:uid()
end
ent:addHook("dark:Message.%UID%", function(payload)
local msg = payload.message
local caller = payload.caller
if msg == "TurnOn" then
print("Dark I/O [".. ent:name() .. "]: Received TurnOn from " .. tostring(caller))
elseif msg == "TurnOff" then
print("Dark I/O [".. ent:name() .. "]: Received TurnOff from " .. tostring(caller))
end
end)
local connections = metadata["connections"]
if not connections then return end
ent:addHook("dark:Broadcast.%UID%", function(payload)
local msg = payload.message
local validFlavors = payload.flavors or { "ControlDevice", "SwitchLink" }
for i = 1, #connections do
local conn = connections[i]
local isValid = false
for _, flav in ipairs(validFlavors) do
if conn.flavor == flav then isValid = true break end
end
if isValid then
local targetUID = _G.DarkTargets[conn.target_id]
if targetUID then
local targetEnt = entities.get(targetUID)
if targetEnt and targetEnt:uid() then
targetEnt:callHook("dark:Message." .. targetUID, {
message = msg,
caller = ent:uid()
})
end
end
end
end
end)
ent:addHook("entity:Destroy.%UID%", function()
if darkMeta["id"] and _G.DarkTargets[darkMeta["id"]] == ent:uid() then
_G.DarkTargets[darkMeta["id"]] = nil
end
end)
ent:addHook("entity:Use.%UID%", function(payload)
if payload.user == ent:uid() then return end
ent:callHook("dark:Broadcast.%UID%", { message = "TurnOn" })
end)

View File

@ -0,0 +1,197 @@
local scene = entities.currentScene()
local metadata = ent:getComponent("Metadata")
local activeSongId = metadata["dark"]["mission song"] or "song02"
local songs = metadata["dark"]["songs"] or {}
local songData = songs[activeSongId]
local bakedMarkers = metadata["dark"]["music markers"] or {}
local musicMarkers = {}
local activeMarkers = {}
local currentSectionIdx = 0
local currentLoopCount = 0
local initializedPlayer = false
if not songData then
print("[Music Manager] ERROR: Active song '" .. tostring(activeSongId) .. "' not found!")
return
end
print("[Music Manager] Bound Mission Song: " .. activeSongId)
for _, markerData in ipairs(bakedMarkers) do
table.insert(musicMarkers, {
position = Vector3f(markerData.position[1], markerData.position[2], markerData.position[3]),
radius = tonumber(markerData.radius) or 0,
theme = string.lower(markerData.theme or "")
})
end
print("[Music Manager] Pre-caching song assets...")
for _, section in ipairs(songData.sections or {}) do
if section.samples then
for _, sampleName in ipairs(section.samples) do
local url = "SND://songs/" .. string.lower(sampleName)
if not string.match(url, "%.wav$") then url = url .. ".wav" end
ent:callHook("asset:QueueLoad.%UID%", { uri = url })
end
end
end
local function getSampleUrl(section)
if section.samples and #section.samples > 0 then
local url = "SND://songs/" .. string.lower(section.samples[1])
if not string.match(url, "%.wav$") then url = url .. ".wav" end
return url
end
return nil
end
local function predictNextSection(sectionIdx, loopCount)
local section = songData.sections[sectionIdx + 1]
local maxLoops = section.loopCount or 0
if maxLoops > 0 and loopCount < maxLoops then
return sectionIdx, loopCount + 1
end
local eventFound = nil
if section and section.events then
for _, evt in ipairs(section.events) do
if evt.event == "" then eventFound = evt; break end
end
end
if not eventFound and songData.events then
for _, evt in ipairs(songData.events) do
if evt.event == "" then eventFound = evt; break end
end
end
if eventFound and eventFound.gotos and #eventFound.gotos > 0 then
return eventFound.gotos[1].section, 0
end
return nil, 0
end
local function playSection(sectionIdx, isQueue)
if not songData.sections or not songData.sections[sectionIdx + 1] then return end
local section = songData.sections[sectionIdx + 1]
currentSectionIdx = sectionIdx
local url = getSampleUrl(section)
if url then
if isQueue then
print(string.format("[Music Manager] Queuing section: %s", section.id))
ent:callHook("sound:QueueTrack.%UID%", { filename = url, layer = 1 })
else
print(string.format("[Music Manager] Forcing immediate section: %s", section.id))
ent:callHook("sound:PlayTrack.%UID%", { filename = url, layer = 1, loop = false })
-- Immediately predict and queue the next track to prevent gaps!
local nextIdx, nextLoop = predictNextSection(currentSectionIdx, currentLoopCount)
if nextIdx then
local nextSec = songData.sections[nextIdx + 1]
local nextUrl = getSampleUrl(nextSec)
if nextUrl then
print(string.format("[Music Manager] Pre-queuing next section: %s", nextSec.id))
ent:callHook("sound:QueueTrack.%UID%", { filename = nextUrl, layer = 1 })
end
end
end
else
processEvent("")
end
end
local function processEvent(eventName)
local altEventName = "theme " .. eventName
local section = songData.sections[currentSectionIdx + 1]
local eventFound = nil
if section and section.events then
for _, evt in ipairs(section.events) do
if evt.event == eventName or evt.event == altEventName then
eventFound = evt
break
end
end
end
if not eventFound and songData.events then
for _, evt in ipairs(songData.events) do
if evt.event == eventName or evt.event == altEventName then
eventFound = evt
break
end
end
end
if eventFound and eventFound.gotos and #eventFound.gotos > 0 then
local randNum = math.random(0, 99)
local totalProbability = 0
local targetSection = eventFound.gotos[1].section
for _, gotoData in ipairs(eventFound.gotos) do
totalProbability = totalProbability + (gotoData.probability or 100)
if randNum < totalProbability then
targetSection = gotoData.section
break
end
end
print(string.format("[Music Manager] Event '%s' triggered transition to section %d", eventFound.event, targetSection))
if eventName ~= "" then
currentLoopCount = 0
playSection(targetSection, false)
else
playSection(targetSection, true)
end
else
print(string.format("[Music Manager] WARNING: Event '%s' ignored (not valid from section %d)", eventName, currentSectionIdx))
end
end
ent:addHook( "sound:TrackEnded.%UID%", function(self, payload)
local section = songData.sections[currentSectionIdx + 1]
local maxLoops = section.loopCount or 0
if maxLoops == 0 or currentLoopCount >= maxLoops then
currentLoopCount = 0
processEvent("")
else
currentLoopCount = currentLoopCount + 1
playSection(currentSectionIdx, true)
end
end)
local function updateMusicState(playerPos, forceInitialization)
for id, marker in ipairs(musicMarkers) do
local dist = playerPos:distance(marker.position)
local isInside = (dist <= marker.radius)
if isInside and (not activeMarkers[id] or forceInitialization) then
activeMarkers[id] = true
processEvent(marker.theme)
elseif not isInside and activeMarkers[id] then
activeMarkers[id] = nil
end
end
end
ent:bind( "tick", function(self)
local player = scene:findByName("Player")
if not player then return end
local playerPos = player:getComponent("Transform").position
if not initializedPlayer then
initializedPlayer = true
processEvent("begin")
updateMusicState(playerPos, true)
else
updateMusicState(playerPos, false)
end
end)

View File

@ -0,0 +1,26 @@
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local connections = darkMeta["connections"] or {}
ent:addHook("dark:Message.%UID%", function(payload)
local msg = payload.message
if msg == "TurnOn" then
for _, conn in ipairs(connections) do
if conn.flavor == "SoundDescription" and conn.wavs and #conn.wavs > 0 then
local pick = conn.wavs[math.random(#conn.wavs)]
local resolvedUrl = string.resolveURI(pick, metadata["system"]["root"])
ent:callHook("sound:Emit.%UID%", {
filename = resolvedUrl,
spatial = true,
streamed = false,
volume = 1.0,
unique = true,
loop = false
})
break
end
end
end
end)

View File

@ -0,0 +1,38 @@
local ent = ent
local scene = entities.currentScene()
local physicsBody = ent:getComponent("PhysicsBody")
local touching = {}
ent:bind( "tick", function(self)
if not physicsBody:initialized() then return end
local currentCollisions = {}
local collisionEvents = physicsBody:getCollisionEvents()
for i, event in ipairs(collisionEvents) do
local other = nil
if event.a:getObject():uid() == ent:uid() then
other = event.b
elseif event.b:getObject():uid() == ent:uid() then
other = event.a
end
if other then
local uid = other:getObject():uid()
currentCollisions[uid] = true
if not touching[uid] then
touching[uid] = true
ent:queueHook("dark:Broadcast.%UID%", { message = "TurnOn" }, 0)
end
end
end
for uid, _ in pairs(touching) do
if not currentCollisions[uid] then
touching[uid] = nil
ent:queueHook("dark:Broadcast.%UID%", { message = "TurnOff" }, 0)
end
end
end )

View File

@ -20,14 +20,14 @@ local function playSound()
local url = "valve://sound/" .. soundFile
local payload = {
filename = string.resolveURI(url, metadata["system"]["root"]),
spatial = not playEverywhere,
streamed = true,
volume = volume,
unique = true
}
payload["wants loops"] = not isNotLooped
local payload = {
filename = string.resolveURI(url, metadata["system"]["root"]),
spatial = not playEverywhere,
streamed = true,
volume = volume,
unique = true
}
payload["wants loop"] = not isNotLooped
ent:callHook("sound:Emit.%UID%", payload)
end
@ -58,7 +58,7 @@ ent:addHook("io:Input.%UID%", function(payload)
local newVol = tonumber(payload.parameter)
if newVol then
volume = newVol / 10.0
-- to-do: update volume of currently playing sound
-- to-do: update volume of currently playing sound
end
end
end)

View File

@ -73,7 +73,7 @@ ent:bind("tick", function(self)
for i = #pendingOutputs, 1, -1 do
local job = pendingOutputs[i]
if currentTime >= job.fireTime then
if currentTime >= job.fireTime then
local targetUIDs = _G.IOTargets[job.target]
if targetUIDs then
for _, targetUID in ipairs(targetUIDs) do
@ -139,5 +139,5 @@ ent:addHook("entity:Use.%UID%", function(payload)
ent:callHook("io:FireOutput.%UID%", { output = "OnPlayerUse" })
ent:callHook("io:FireOutput.%UID%", { output = "OnUse" })
ent:callHook("io:FireOutput.%UID%", { output = "OnPressed" })
ent:callHook("io:FireOutput.%UID%", { output = "OnIn" })
ent:callHook("io:FireOutput.%UID%", { output = "OnIn" })
end)

View File

@ -521,6 +521,12 @@ void populateSurface( InstanceAddresses addresses, uvec3 indices ) {
#elif BARYCENTRIC
ivec2 size = textureSize(samplerId, 0).xy;
int sampleIdx = 0;
#elif DEFERRED && MULTISAMPLING
ivec2 size = textureSize(samplerUv).xy;
int sampleIdx = msaa.currentID;
#elif DEFERRED
ivec2 size = textureSize(samplerUv, 0).xy;
int sampleIdx = 0;
#else
ivec2 size = imageSize(outImage).xy;
int sampleIdx = 0;

View File

@ -83,7 +83,7 @@
#endif
#if BARYCENTRIC
#ifndef BARYCENTRIC_CALCULATE
#define BARYCENTRIC_CALCULATE 0
#define BARYCENTRIC_CALCULATE 1
#endif
#ifndef BUFFER_REFERENCE
#define BUFFER_REFERENCE 1

View File

@ -5,15 +5,20 @@
#include <uf/engine/graph/graph.h>
namespace impl {
const float darkToMeters = 0.75f;
const float darkToMeters = 0.7f;
typedef uf::Meshlet_T<uf::graph::mesh::Skinned, uint32_t> Meshlet;
// could be dedicated functions in the engine
template<typename T>
inline bool readStruct(const uf::stl::vector<uint8_t>& buffer, uint32_t& offset, T& outValue) {
if (offset + sizeof(T) > buffer.size()) return false;
std::memcpy(&outValue, buffer.data() + offset, sizeof(T));
offset += sizeof(T);
inline bool readStruct(const uf::stl::vector<uint8_t>& buffer, uint32_t& offset, T& outValue, size_t readSize = sizeof(T)) {
if (offset + readSize > buffer.size()) return false;
if ( readSize < sizeof(T) ) std::memset(&outValue, 0, sizeof(T));
size_t copySize = std::min(sizeof(T), readSize);
std::memcpy(&outValue, buffer.data() + offset, copySize);
offset += readSize;
return true;
}

View File

@ -47,6 +47,9 @@ namespace uf {
void UF_API destroy( pod::AudioClip& clip );
void UF_API bind( pod::AudioSource& source, pod::AudioClip* clip );
void UF_API play( pod::AudioSource& source );
void UF_API queue( pod::AudioSource& source, const uf::stl::string& );
void UF_API pause( pod::AudioSource& source );
bool UF_API paused( const pod::AudioSource& source );
void UF_API stop( pod::AudioSource& source );
void UF_API update( pod::AudioSource& source );
void UF_API update( pod::AudioSource& source, const pod::Vector3f& position, const pod::Quaternion<>& orientation );
@ -56,6 +59,7 @@ namespace uf {
void UF_API loop( pod::AudioSource& source, bool state );
void UF_API position( pod::AudioSource& source, const pod::Vector3f& v );
void UF_API orientation( pod::AudioSource& source, const pod::Quaternion<>& q );
float UF_API time( pod::AudioSource& source ); // non-const because apparently it needs to mutate the timer?
float UF_API time( const pod::AudioSource& source );
void UF_API time( pod::AudioSource& source, float v );
float UF_API pitch( const pod::AudioSource& source );
@ -67,6 +71,7 @@ namespace uf {
float UF_API maxDistance( const pod::AudioSource& source );
void UF_API maxDistance( pod::AudioSource& source, float v );
float UF_API distance( const pod::Vector3f& position );
float UF_API occlusion( const pod::Vector3f& position );
void UF_API occlude( pod::AudioSource& source, float factor );
void UF_API acoustics( const pod::Vector3f&, const pod::Quaternion<>&, float&, float&, int& );

View File

@ -62,6 +62,7 @@ namespace pod {
struct {
uf::Timer<> timer;
float elapsed = 0;
uf::stl::vector<uf::stl::string> pending;
} info;
struct {

View File

@ -29,6 +29,7 @@ namespace uf {
uf::stl::string UF_API directory( const uf::stl::string& );
uf::stl::string UF_API normalize( const uf::stl::string& );
size_t UF_API size( const uf::stl::string& );
uf::stl::vector<uf::stl::string> list( const uf::stl::string&, const uf::stl::string& = "" );
bool UF_API readAsString( uf::stl::string&, const uf::stl::string&, const uf::stl::string& = "" );

View File

@ -22,6 +22,7 @@ namespace pod {
std::function<bool(pod::Mount&, const uf::stl::string&, uf::stl::vector<uint8_t>&)> read;
std::function<size_t(pod::Mount&, const uf::stl::string&, const void*, size_t)> write;
std::function<bool(pod::Mount&, const uf::stl::string&)> mkdir;
std::function<uf::stl::vector<uf::stl::string>(pod::Mount&, const uf::stl::string&, const uf::stl::string&, bool)> list;
std::function<bool(pod::Mount&, const uf::stl::string&, size_t, size_t, uf::stl::vector<uint8_t>&)> readRange;
std::function<bool(pod::Mount&, const uf::stl::string&, const uf::stl::vector<pod::Range>&, uf::stl::vector<uint8_t>&)> readRanges;
@ -35,6 +36,7 @@ namespace uf {
extern UF_API uf::stl::vector<pod::Mount> mounts;
struct UF_API Mount {
size_t hash = {};
const pod::Mount* ptr = NULL;
bool temp = false;
~Mount();
};
@ -52,6 +54,7 @@ namespace uf {
size_t UF_API write( const uf::stl::string& path, const void* data, size_t len );
size_t UF_API write( const uf::stl::string& path, uf::stl::vector<uint8_t>& buffer );
bool UF_API mkdir( const uf::stl::string& path );
uf::stl::vector<uf::stl::string> UF_API list( const uf::stl::string& path, const uf::stl::string& extension = "", bool recursive = false );
bool UF_API readRange( const uf::stl::string& path, size_t start, size_t len, uf::stl::vector<uint8_t>& buffer );
bool UF_API readRanges( const uf::stl::string& path, const uf::stl::vector<pod::Range>& ranges, uf::stl::vector<uint8_t>& buffer );

View File

@ -19,9 +19,6 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
UF_BEHAVIOR_METADATA_BIND_SERIALIZER_HOOKS(metadata, metadataJson);
if ( !metadataJson["audio"]["epsilon"].is<float>() )
metadataJson["audio"]["epsilon"] = 0.001f;
this->addHook( "sound:Stop.%UID%", [&](ext::json::Value& json){
uf::stl::string filename = json["filename"].as<uf::stl::string>();
auto& pools = emitter.get();
@ -71,11 +68,12 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
if ( json["gain"].is<double>() ) uf::audio::gain(source, json["gain"].as<float>());
if ( json["rolloffFactor"].is<double>() ) uf::audio::rolloff(source, json["rolloffFactor"].as<float>());
if ( json["maxDistance"].is<double>() ) uf::audio::maxDistance(source, json["maxDistance"].as<float>());
if ( json["spatial"].is<bool>() ) source.settings.spatial = json["spatial"].as<bool>();
if ( json["loop"].is<bool>() ) uf::audio::loop(source, json["loop"].as<bool>());
if ( json["wants loop"].is<bool>() ) {
uf::audio::loop(source, json["wants loop"].as<bool>(true) && clip && clip->info.loop.has);
else if ( json["wants loop"].is<bool>() ) {
auto wants = json["wants loop"].as<bool>(true);
uf::audio::loop(source, wants && clip && clip->info.loop.has);
}
float volume = 1.0f;
@ -95,18 +93,41 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
uf::audio::play(source);
});
this->addHook( "sound:QueueTrack.%UID%", [&](ext::json::Value& payload){
uf::stl::string filename = payload["filename"].as<uf::stl::string>();
int layer = payload["layer"].as<int>(1);
uf::stl::string channelName = "managed_bgm_channel_" + std::to_string(layer);
UF_MSG_DEBUG("filename={}, channelName={}", filename, channelName);
if ( emitter.has( channelName ) ) {
auto& source = emitter.get(channelName);
uf::audio::queue( source, filename );
}
});
this->addHook( "sound:PlayTrack.%UID%", [&](ext::json::Value& payload){
auto filename = payload["filename"].as<uf::stl::string>();
if ( filename == "" && !metadata.tracks.empty() ) {
filename = uf::stl::random_it( metadata.tracks.begin(), metadata.tracks.end() )->first;
}
if ( metadata.tracks.count( filename ) == 0 && filename != "" ) return;
if ( metadata.tracks.count( filename ) == 0 && filename != "" ) {
metadata.tracks[filename] = {};
metadata.tracks[filename].filename = filename;
}
auto& track = metadata.tracks[filename];
if ( track.intro != "" ) filename = track.intro;
track.epsilon = metadataJson["audio"]["epsilon"].as<float>(2.5f);
this->callHook( "asset:QueueLoad.%UID%", this->resolveToPayload( filename ) );
auto pload = this->resolveToPayload( filename );
pload.metadata["layer"] = payload["layer"];
if ( payload["loop"].is<bool>() ) pload.metadata["loop"] = payload["loop"];
if ( payload["notify"].is<bool>() ) pload.metadata["notify"] = payload["notify"];
this->callHook( "asset:QueueLoad.%UID%", pload );
});
this->addHook( "asset:Load.%UID%", [&](pod::payloads::assetLoad& payload){
@ -116,8 +137,14 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
if ( metadata.tracks.count(payload.uri) > 0 || metadata.tracks.count(metadata.current) > 0 ) {
auto& track = metadata.tracks[payload.uri];
pod::AudioClip* clip = &uf::asset::get<pod::AudioClip>( payload.filename );
int layer = payload.metadata["layer"].as<int>(1);
uf::stl::string channelName = "managed_bgm_channel_" + std::to_string(layer);
pod::AudioSource& source = emitter.emit( "managed_bgm_channel", clip, true );
if ( emitter.has( channelName ) ) {
uf::audio::stop( emitter.get( channelName ) );
}
pod::AudioSource& source = emitter.emit( channelName, clip, true );
#if UF_AUDIO_MAPPED_VOLUMES
auto volume = uf::audio::volumes.count("bgm") > 0 ? uf::audio::volumes.at("bgm") : 1.0f;
@ -125,12 +152,15 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
auto volume = uf::audio::volumes::bgm;
#endif
bool shouldLoop = payload.metadata["loop"].as<bool>(payload.uri != track.intro);
uf::audio::gain(source, track.fade.x > 0 ? 0 : volume);
uf::audio::loop(source, payload.uri != track.intro);
uf::audio::loop(source, shouldLoop);
uf::audio::play(source);
metadata.current = payload.uri;
track.active = true;
UF_MSG_DEBUG("Playing: {} (epsilon: {})", metadata.current, track.epsilon);
} else {
ext::json::Value json = metadataJson["audio"];
json["filename"] = payload.filename;
@ -154,34 +184,92 @@ void ext::AudioEmitterBehavior::tick( uf::Object& self ) {
metadata.deserialize(self, metadataJson);
#endif
if ( !emitter.has("managed_bgm_channel") ) return;
pod::AudioSource& source = emitter.get("managed_bgm_channel");
auto& transform = this->getComponent<pod::Transform<>>();
float distance = uf::audio::distance( transform.position );
#if UF_AUDIO_MAPPED_VOLUMES
auto volume = uf::audio::volumes.count("bgm") > 0 ? uf::audio::volumes.at("bgm") : 1.0f;
#else
auto volume = uf::audio::volumes::bgm;
#endif
bool bgmFound = false;
for ( auto& [ name, sources ] : emitter.get() ) {
for ( auto& source : sources ) {
if ( name.starts_with("managed_bgm_channel") ) {
bgmFound = true;
#if UF_AUDIO_MAPPED_VOLUMES
auto volume = uf::audio::volumes.count("bgm") > 0 ? uf::audio::volumes.at("bgm") : 1.0f;
#else
auto volume = uf::audio::volumes::bgm;
#endif
if ( source.clip ) {
float current = uf::audio::time(source);
float end = source.clip->info.duration;
bool fileChanged = (source.clip->filename != metadata.current);
bool timerWrapped = (metadata.tracks.count(metadata.current) && !metadata.tracks[metadata.current].active && current < 1.0f);
if ( fileChanged || timerWrapped ) {
metadata.current = source.clip->filename;
metadata.tracks[metadata.current].active = true;
}
if ( metadata.tracks.count( metadata.current ) > 0 ) {
auto& track = metadata.tracks[metadata.current];
if ( track.active ) {
bool isIntro = metadata.current == track.intro;
float a = volume;
if ( track.fade.x > 0.0f && current < track.fade.x ) {
a *= current / track.fade.x;
} else if ( track.fade.y > 0.0f && !source.settings.loop && end - current < track.fade.y ) {
a *= 1.0f - (end - current) / track.fade.y;
}
uf::audio::gain(source, a);
bool timeReached = (current + track.epsilon >= end);
bool stoppedPlaying = (!source.settings.loop && !source.alSource.playing());
if ( end > 0 && (timeReached || stoppedPlaying) ) {
track.active = false;
if ( isIntro ) {
auto payload = this->resolveToPayload( track.filename );
this->callHook( "asset:QueueLoad.%UID%", payload );
} else if ( !source.settings.loop ) {
ext::json::Value msg;
msg["filename"] = metadata.current;
this->callHook("sound:TrackEnded.%UID%", msg);
}
}
}
}
}
continue;
}
if ( source.settings.spatial ) {
float maxDist = uf::audio::maxDistance(source);
if ( distance > maxDist * 1.1f ) {
if ( source.alSource.playing() ) uf::audio::pause( source );
} else {
if ( uf::audio::paused(source) ) uf::audio::play( source );
}
}
}
}
if ( metadata.tracks.count( metadata.current ) > 0 ) {
auto& track = metadata.tracks[metadata.current];
if ( track.active && source.clip ) {
float current = uf::audio::time(source);
float end = source.clip->info.duration;
if ( track.active && !bgmFound ) {
UF_MSG_DEBUG("BGM source vanished! Firing TrackEnded natively.");
track.active = false;
bool isIntro = metadata.current == track.intro;
float a = volume;
if ( current < track.fade.x ) {
a *= current / track.fade.x;
} else if ( !source.settings.loop && end - current < track.fade.y ) {
a *= 1.0f - (end - current) / track.fade.y;
}
uf::audio::gain(source, a);
if ( isIntro && end > 0 && (current + track.epsilon >= end || !source.alSource.playing()) ) {
if ( isIntro ) {
auto payload = this->resolveToPayload( track.filename );
this->callHook( "asset:QueueLoad.%UID%", payload );
} else {
ext::json::Value msg;
msg["filename"] = metadata.current;
this->callHook("sound:TrackEnded.%UID%", msg);
}
}
}

View File

@ -18,6 +18,7 @@ namespace ext {
uf::stl::string intro;
float epsilon = 0.0001f;
pod::Vector2f fade = {};
float prevTime = 0.0f;
bool active = false;
};

View File

@ -1167,8 +1167,10 @@ void uf::graph::process( pod::Graph& graph ) {
if ( image.viewType == uf::renderer::enums::Image::VIEW_TYPE_CUBE ) {
gpuIndexCube[i] = countCube++;
} else {
} else if ( image.viewType == uf::renderer::enums::Image::VIEW_TYPE_2D ) {
gpuIndex2D[i] = count2D++;
} else {
UF_MSG_DEBUG("Invalid view type 0x{:x} for: {}", image.viewType, key );
}
}
@ -1359,12 +1361,12 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
// bind io connectivity
if ( ext::json::isArray( metadataValve["connections"] ) || metadataValve["targetname"].is<uf::stl::string>() ) {
node.metadata["connections"] = metadataValve["connections"];
loadJson["assets"].emplace_back("ent://scripts/io.lua");
loadJson["assets"].emplace_back("ent://scripts/valve/io.lua");
}
// bind ambient
if ( node.name.starts_with("ambient_") ) {
loadJson["assets"].emplace_back("ent://scripts/ambient_generic.lua");
loadJson["assets"].emplace_back("ent://scripts/valve/ambient_generic.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
// bind door script
} else if ( ext::json::isObject( metadataValve["door"] ) ) {
@ -1404,7 +1406,7 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
// check if trigger
if ( node.name.starts_with("trigger_") ) {
loadJson["assets"].emplace_back("ent://scripts/trigger.lua");
loadJson["assets"].emplace_back("ent://scripts/valve/trigger.lua");
// signal to assign a physics body
if ( ext::json::isNull( node.metadata["physics"] ) ) {
node.metadata["physics"]["type"] = "bounding box";
@ -1418,7 +1420,7 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
auto& materialName = graph.materials[materialID];
// attach trigger script + physics body
if ( materialName == "tools/toolstrigger" ) {
loadJson["assets"].emplace_back("ent://scripts/trigger.lua");
loadJson["assets"].emplace_back("ent://scripts/valve/trigger.lua");
// signal to assign a physics body
if ( ext::json::isNull( node.metadata["physics"] ) ) {
node.metadata["physics"]["type"] = "bounding box";
@ -1431,6 +1433,49 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
}
}
// convert metadata["dark"] into internal values:
auto& metadataDark = node.metadata["dark"];
if ( ext::json::isObject( metadataDark ) ) {
// bind io connectivity
if ( ext::json::isArray( metadataDark["connections"] ) || metadataDark["id"].is<int>() ) {
node.metadata["connections"] = metadataDark["connections"];
loadJson["assets"].emplace_back("ent://scripts/dark/io.lua");
}
// bind door
if ( ext::json::isObject( metadataDark["door"] ) ) {
loadJson["assets"].emplace_back("ent://scripts/dark/door.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
node.metadata["dark"]["schema_db"] = graph.metadata["dark"]["schema_db"];
}
// bind songs
if ( ext::json::isObject( metadataDark["songs"] ) ) {
loadJson["assets"].emplace_back("ent://scripts/dark/music.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
}
// bind ambient
if ( ext::json::isObject( metadataDark["sound"] ) ) {
loadJson["assets"].emplace_back("ent://scripts/dark/ambient.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
}
// bind trap
if ( node.name.find("SoundTrap") != uf::stl::string::npos || node.name.find("Votrap") != uf::stl::string::npos ) {
loadJson["assets"].emplace_back("ent://scripts/dark/trap.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
}
// bind trigger
auto& physMeta = node.metadata["physics"];
if ( ext::json::isObject( physMeta ) && physMeta["category"].as<uf::stl::string>("") == "trigger" ) {
loadJson["assets"].emplace_back("ent://scripts/dark/trigger.lua");
}
}
if ( ext::json::isObject( tag ) ) {
if ( tag["action"].as<uf::stl::string>() == "load" ) {
if ( tag["filename"].is<uf::stl::string>() ) {

View File

@ -219,52 +219,113 @@ void ext::vorbis::update( pod::AudioSource& source ) {
pod::AudioClip* clip = source.clip;
if ( !clip || !clip->streamed || !source.streamState.handle ) return;
if ( source.settings.loopMode == 1 ) source.alSource.set(AL_LOOPING, AL_FALSE);
OggVorbis_File* vorbisFile = (OggVorbis_File*) source.streamState.handle;
VorbisVfsContext* ctx = (VorbisVfsContext*) source.streamState.context;
ALint state;
ALint state, processed, queued;
source.alSource.get(AL_SOURCE_STATE, state);
if ( state != AL_PLAYING ) {
if ( !source.settings.loop && ctx && ctx->currentOffset >= ctx->totalSize ) return;
source.alSource.play();
}
ALint processed = 0;
source.alSource.get(AL_BUFFERS_PROCESSED, processed);
if ( processed <= 0 ) return;
source.alSource.get(AL_BUFFERS_QUEUED, queued);
ALuint index;
char buffer[uf::audio::bufferSize];
while ( processed-- ) {
memset(buffer, 0, uf::audio::bufferSize);
AL_CHECK_RESULT(alSourceUnqueueBuffers(source.alSource.getIndex(), 1, &index));
bool hasData = (ctx->currentOffset < ctx->totalSize) || !source.info.pending.empty() || source.settings.loop;
#if !NO_FUN // if the engine cannot feed buffers, OpenAL will continue to loop the buffers
source.alSource.set(AL_LOOPING, hasData ? AL_TRUE : AL_FALSE);
#else // consume buffers automatically
if ( source.settings.loopMode == 1 ) {
source.alSource.set(AL_LOOPING, AL_FALSE);
source.settings.loopMode = 0;
}
#endif
auto fillAndQueueBuffer = [&](ALuint index) -> bool {
char buffer[uf::audio::bufferSize];
int totalRead = 0;
while (totalRead < uf::audio::bufferSize) {
while ( totalRead < uf::audio::bufferSize ) {
int result = OV_READ(vorbisFile, buffer + totalRead, uf::audio::bufferSize - totalRead, endian, 2, 1, &source.streamState.bitStream);
if (result <= 0) {
if (result == 0 && source.settings.loop) {
uint32_t seekTarget = clip->info.loop.has ? clip->info.loop.start : 0;
ov_pcm_seek(vorbisFile, seekTarget);
continue;
if ( result <= 0 ) {
if ( result == 0 ) {
if ( !source.info.pending.empty() ) {
uf::stl::string nextFile = source.info.pending.front();
source.info.pending.erase(source.info.pending.begin());
ov_clear(vorbisFile);
VorbisVfsContext* nextCtx = new VorbisVfsContext();
nextCtx->filename = uf::io::resolveURI(nextFile);
nextCtx->currentOffset = 0;
nextCtx->totalSize = uf::vfs::size(nextCtx->filename);
ov_callbacks callbacks = { funs::read, funs::seek, funs::close, funs::tell };
if ( ov_open_callbacks((void*) nextCtx, vorbisFile, NULL, -1, callbacks) < 0 ) {
UF_MSG_ERROR("Gapless Vorbis transition failed! Could not open: {}", nextCtx->filename);
delete nextCtx;
break;
}
clip->filename = nextFile;
clip->info.size = nextCtx->totalSize;
clip->info.duration = ov_time_total(vorbisFile, -1);
source.info.elapsed = 0.0f;
source.info.timer.reset();
source.info.timer.start();
ctx = nextCtx;
source.streamState.context = (void*) ctx;
continue;
}
else if ( source.settings.loop ) {
uint32_t seekTarget = clip->info.loop.has ? clip->info.loop.start : 0;
ov_pcm_seek(vorbisFile, seekTarget);
continue;
}
}
break;
}
totalRead += result;
}
if (totalRead > 0) {
if ( totalRead > 0 ) {
AL_CHECK_RESULT(alBufferData(index, clip->info.format, buffer, totalRead, clip->info.frequency));
AL_CHECK_RESULT(alSourceQueueBuffers(source.alSource.getIndex(), 1, &index));
return true;
}
return false;
};
if ( queued == 0 ) {
if ( hasData ) {
for ( int i = 0; i < source.settings.buffers; ++i ) {
if ( !fillAndQueueBuffer(source.streamBuffers.getIndex(i)) ) break;
}
}
} else {
while ( processed > 0 ) {
ALuint index;
AL_CHECK_RESULT(alSourceUnqueueBuffers(source.alSource.getIndex(), 1, &index));
processed--;
bool hasData = (ctx->currentOffset < ctx->totalSize) || !source.info.pending.empty() || source.settings.loop;
if ( hasData ) {
fillAndQueueBuffer(index);
} else {
// ensure AL_LOOPING is off
}
}
}
if ( source.settings.loopMode == 1 ) source.alSource.set(AL_LOOPING, AL_TRUE);
}
source.alSource.get(AL_SOURCE_STATE, state);
source.alSource.get(AL_BUFFERS_QUEUED, queued);
if ( state != AL_PLAYING && queued > 0 ) {
source.alSource.play();
}
}
void ext::vorbis::close( pod::AudioClip& clip ) {
// ...
}

View File

@ -59,13 +59,12 @@ namespace {
return 1;
}
// default to 16-bit audio
inline bool format( pod::AudioClip& clip, int channels, int bitDepth ) {
if (channels == 1 && bitDepth == 8) clip.info.format = AL_FORMAT_MONO8;
else if (channels == 1 && bitDepth == 16) clip.info.format = AL_FORMAT_MONO16;
else if (channels == 2 && bitDepth == 8) clip.info.format = AL_FORMAT_STEREO8;
else if (channels == 2 && bitDepth == 16) clip.info.format = AL_FORMAT_STEREO16;
if (channels == 1) clip.info.format = AL_FORMAT_MONO16;
else if (channels == 2) clip.info.format = AL_FORMAT_STEREO16;
else {
UF_MSG_ERROR("WAV: unrecognized format: {} channels, {} bps", channels, bitDepth);
UF_MSG_ERROR("WAV: unrecognized format: {} channels", channels);
return false;
}
return true;
@ -85,9 +84,9 @@ void ext::wav::load( pod::AudioClip& clip ) {
}
drwav* wav = &ctx->wav;
clip.info.size = wav->totalPCMFrameCount * wav->channels * (wav->bitsPerSample / 8);
clip.info.channels = wav->channels;
clip.info.bitDepth = wav->bitsPerSample;
clip.info.bitDepth = 16; // wav->bitsPerSample;
clip.info.size = wav->totalPCMFrameCount * wav->channels * sizeof(uint16_t); // (wav->bitsPerSample / 8);
clip.info.frequency = wav->sampleRate;
clip.info.duration = (double) wav->totalPCMFrameCount / wav->sampleRate;
clip.info.loop.has = false;
@ -107,16 +106,15 @@ void ext::wav::load( pod::AudioClip& clip ) {
}
}
if ( !format(clip, wav->channels, wav->bitsPerSample) ) {
if ( !format( clip, wav->channels, wav->bitsPerSample ) ) {
drwav_uninit(wav); delete ctx;
return;
}
if (!clip.streamed) {
uf::stl::vector<uint8_t> bytes(clip.info.size);
drwav_read_pcm_frames(wav, wav->totalPCMFrameCount, bytes.data());
clip.alBuffer.buffer(clip.info.format, bytes.data(), (ALsizei)bytes.size(), clip.info.frequency);
uf::stl::vector<int16_t> pcm(wav->totalPCMFrameCount * wav->channels);
drwav_read_pcm_frames_s16(wav, wav->totalPCMFrameCount, pcm.data());
clip.alBuffer.buffer(clip.info.format, pcm.data(), (ALsizei)(pcm.size() * sizeof(int16_t)), clip.info.frequency);
if ( clip.info.loop.has ) {
ALint loopPoints[2] = { (ALint) clip.info.loop.start, (ALint) clip.info.loop.end };
alBufferiv(clip.alBuffer.getIndex(0), 0x2015 /* AL_LOOP_POINTS_SOFT */, loopPoints);
@ -145,19 +143,19 @@ void ext::wav::open( pod::AudioSource& source ) {
source.streamState.handle = (void*) ctx;
drwav* wav = &ctx->wav;
size_t frameSize = wav->channels * (wav->bitsPerSample / 8);
size_t frameSize = wav->channels * sizeof(uint16_t); // (wav->bitsPerSample / 8);
size_t bufferFrames = uf::audio::bufferSize / frameSize;
char buffer[uf::audio::bufferSize];
int16_t buffer[uf::audio::bufferSize / 2];
uint8_t queuedBuffers = 0;
for ( ; queuedBuffers < source.settings.buffers; ++queuedBuffers ) {
drwav_uint64 framesRead = drwav_read_pcm_frames(wav, bufferFrames, buffer);
drwav_uint64 framesRead = drwav_read_pcm_frames_s16(wav, bufferFrames, buffer);
if ( framesRead == 0 ) {
if ( source.settings.loop ) {
drwav_seek_to_pcm_frame(wav, clip->info.loop.has ? clip->info.loop.start : 0);
framesRead = drwav_read_pcm_frames(wav, bufferFrames, buffer);
framesRead = drwav_read_pcm_frames_s16(wav, bufferFrames, buffer);
}
}
if ( framesRead == 0 ) break;
@ -176,45 +174,105 @@ void ext::wav::update( pod::AudioSource& source ) {
pod::AudioClip* clip = source.clip;
if ( !clip || !clip->streamed || !source.streamState.handle ) return;
if ( source.settings.loopMode == 1 ) source.alSource.set(AL_LOOPING, AL_FALSE);
DrWavVfsContext* ctx = (DrWavVfsContext*) source.streamState.handle;
drwav* wav = &ctx->wav;
ALint state;
ALint state, processed, queued;
source.alSource.get(AL_SOURCE_STATE, state);
if ( state != AL_PLAYING ) {
if ( !source.settings.loop && ctx->currentOffset >= ctx->totalSize ) return;
source.alSource.get(AL_BUFFERS_PROCESSED, processed);
source.alSource.get(AL_BUFFERS_QUEUED, queued);
bool hasData = (ctx->currentOffset < ctx->totalSize) || !source.info.pending.empty() || source.settings.loop;
#if 0 && !NO_FUN // if the engine cannot feed buffers, OpenAL will continue to loop the buffers
source.alSource.set(AL_LOOPING, hasData ? AL_TRUE : AL_FALSE);
#else // consume buffers automatically
if ( source.settings.loopMode == 1 ) {
source.alSource.set(AL_LOOPING, AL_FALSE);
source.settings.loopMode = 0;
}
#endif
size_t frameSize = wav->channels * sizeof(uint16_t);
size_t bufferFrames = uf::audio::bufferSize / frameSize;
int16_t buffer[uf::audio::bufferSize / 2];
auto fillAndQueueBuffer = [&](ALuint index) -> bool {
drwav_uint64 totalFramesRead = 0;
int16_t* bufferPtr = buffer;
while ( totalFramesRead < bufferFrames ) {
drwav_uint64 framesToRead = bufferFrames - totalFramesRead;
drwav_uint64 framesRead = drwav_read_pcm_frames_s16(wav, framesToRead, bufferPtr);
totalFramesRead += framesRead;
bufferPtr += (framesRead * wav->channels);
if ( framesRead == 0 ) {
if ( !source.info.pending.empty() ) {
uf::stl::string nextFile = source.info.pending.front();
source.info.pending.erase(source.info.pending.begin());
drwav_uninit(wav);
ctx->filename = uf::io::resolveURI(nextFile);
ctx->currentOffset = 0;
ctx->totalSize = uf::vfs::size(ctx->filename);
if ( !drwav_init(wav, drwav_vfs_read, drwav_vfs_seek, drwav_vfs_tell, ctx, nullptr) ) {
UF_MSG_ERROR("Transition failed! Could not open: {}", ctx->filename);
break;
}
clip->filename = nextFile;
clip->info.size = ctx->totalSize;
clip->info.duration = (double)wav->totalPCMFrameCount / wav->sampleRate;
source.info.elapsed = 0.0f;
source.info.timer.reset();
source.info.timer.start();
} else if ( source.settings.loop ) {
drwav_seek_to_pcm_frame(wav, clip->info.loop.has ? clip->info.loop.start : 0);
} else {
break;
}
}
}
if ( totalFramesRead > 0 ) {
AL_CHECK_RESULT(alBufferData(index, clip->info.format, buffer, (ALsizei)(totalFramesRead * frameSize), clip->info.frequency));
AL_CHECK_RESULT(alSourceQueueBuffers(source.alSource.getIndex(), 1, &index));
return true;
}
return false;
};
if ( queued == 0 ) {
if ( hasData ) {
for ( int i = 0; i < source.settings.buffers; ++i ) {
if ( !fillAndQueueBuffer(source.streamBuffers.getIndex(i)) ) break;
}
}
} else {
while ( processed > 0 ) {
ALuint index;
AL_CHECK_RESULT(alSourceUnqueueBuffers(source.alSource.getIndex(), 1, &index));
processed--;
bool hasData = (ctx->currentOffset < ctx->totalSize) || !source.info.pending.empty() || source.settings.loop;
if ( hasData ) {
fillAndQueueBuffer(index);
} else {
// ensure AL_LOOPING is off
}
}
}
source.alSource.get(AL_SOURCE_STATE, state);
source.alSource.get(AL_BUFFERS_QUEUED, queued);
if ( state != AL_PLAYING && queued > 0 ) {
source.alSource.play();
}
ALint processed = 0;
source.alSource.get(AL_BUFFERS_PROCESSED, processed);
if ( processed <= 0 ) return;
size_t frameSize = wav->channels * (wav->bitsPerSample / 8);
size_t bufferFrames = uf::audio::bufferSize / frameSize;
ALuint index;
char buffer[uf::audio::bufferSize];
while ( processed-- ) {
AL_CHECK_RESULT(alSourceUnqueueBuffers(source.alSource.getIndex(), 1, &index));
drwav_uint64 framesRead = drwav_read_pcm_frames(wav, bufferFrames, buffer);
if ( framesRead == 0 ) {
if ( !source.settings.loop ) break;
drwav_seek_to_pcm_frame(wav, clip->info.loop.has ? clip->info.loop.start : 0);
framesRead = drwav_read_pcm_frames(wav, bufferFrames, buffer);
}
if ( framesRead > 0 ) {
AL_CHECK_RESULT(alBufferData(index, clip->info.format, buffer, (ALsizei)(framesRead * frameSize), clip->info.frequency));
AL_CHECK_RESULT(alSourceQueueBuffers(source.alSource.getIndex(), 1, &index));
}
}
if ( source.settings.loopMode == 1 ) source.alSource.set(AL_LOOPING, AL_TRUE);
}
void ext::wav::close( pod::AudioClip& clip ) {

View File

@ -202,6 +202,9 @@ namespace binds {
// object.destroy();
// delete &object;
};
uf::stl::vector<uf::Entity*> all() {
return uf::scene::getCurrentScene().getGraph();
}
}
namespace string {
uf::stl::string extension( const uf::stl::string& filename ) {
@ -323,6 +326,7 @@ void ext::lua::initialize() {
entities.set("currentScene", UF_LUA_C_FUN(::binds::entities::currentScene));
entities.set("controller", UF_LUA_C_FUN(::binds::entities::controller));
entities.set("destroy", UF_LUA_C_FUN(::binds::entities::destroy));
entities.set("all", UF_LUA_C_FUN(::binds::entities::all));
}
// `string` table
{
@ -331,7 +335,7 @@ void ext::lua::initialize() {
string.set("resolveURI", UF_LUA_C_FUN(::binds::string::resolveURI));
string.set("si", UF_LUA_C_FUN(::binds::string::si));
string.set("match", UF_LUA_C_FUN(uf::string::match));
// string.set("match", UF_LUA_C_FUN(uf::string::match));
string.set("matched", UF_LUA_C_FUN(uf::string::matched));
}
// `io` table

View File

@ -61,7 +61,7 @@ namespace impl {
struct BinPolyHeader {
uint16_t index;
int16_t data;
uint16_t data;
uint8_t type;
uint8_t num_verts;
uint16_t norm_index;
@ -202,10 +202,12 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
auto it = std::find(graph.materials.begin(), graph.materials.end(), matName);
int32_t matIndex = (mainHeader.version == 4) ? i : binMats[i].slot_num;
if ( it != graph.materials.end() ) {
textureToMaterialId[i] = (int32_t)(std::distance(graph.materials.begin(), it));
textureToMaterialId[matIndex] = (int32_t)(std::distance(graph.materials.begin(), it));
} else {
textureToMaterialId[i] = graph.materials.size();
textureToMaterialId[matIndex] = graph.materials.size();
graph.materials.emplace_back(matName);
storage.materials[matName].indexAlbedo = -1;
}
@ -261,23 +263,29 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
offset += 19;
} else if ( nodeType == 2 ) {
uint16_t nf1{}, nf2{};
impl::readStruct( buffer, offset += 17, nf1 );
impl::readStruct( buffer, offset += 4, nf2 );
offset += 17;
impl::readStruct( buffer, offset, nf1 );
offset += 2;
impl::readStruct( buffer, offset, nf2 );
if ( impl::readArray( buffer, offset, nf1 + nf2, faces ) && !subObjectPolys.empty() ) {
subObjectPolys.back().insert( subObjectPolys.back().end(), faces.begin(), faces.end() );
}
} else if (nodeType == 1) {
uint16_t nf1{}, nf2{};
impl::readStruct( buffer, offset += 17, nf1 );
impl::readStruct( buffer, offset += 12, nf2 );
offset += 17;
impl::readStruct( buffer, offset, nf1 );
offset += 10;
impl::readStruct( buffer, offset, nf2 );
if ( impl::readArray( buffer, offset, nf1 + nf2, faces ) && !subObjectPolys.empty() ) {
subObjectPolys.back().insert( subObjectPolys.back().end(), faces.begin(), faces.end() );
}
} else if (nodeType == 0) {
uint16_t nf{};
impl::readStruct( buffer, offset += 17, nf );
offset += 17;
impl::readStruct( buffer, offset, nf );
if ( impl::readArray(buffer, offset, nf, faces) && !subObjectPolys.empty() ) {
subObjectPolys.back().insert(subObjectPolys.back().end(), faces.begin(), faces.end());
}
@ -316,21 +324,27 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
bool hasUVs = ((polyHeader.type & 3) == 3);
if (hasUVs) impl::readArray(buffer, dataOffset, polyHeader.num_verts, uvIndices);
uint32_t localMatID = 0;
if ( mainHeader.version == 4 ) {
uint8_t matByte = 0;
impl::readStruct(buffer, dataOffset, matByte);
localMatID = matByte;
} else {
localMatID = polyHeader.data;
}
int32_t graphMatID = -1;
int32_t localMatID = polyHeader.data > 0 ? polyHeader.data - 1 : 0;
if (localMatID >= 0 && localMatID < textureToMaterialId.size()) {
if ( localMatID < textureToMaterialId.size() ) {
graphMatID = textureToMaterialId[localMatID];
}
if ( graphMatID == -1 ) {
graphMatID = 0;
}
auto& meshlet = meshlets[graphMatID];
meshlet.primitive.instance.materialID = graphMatID;
uint32_t startVertIdx = meshlet.vertices.size();
/*
pod::Vector3f polyNormal = {0.f, 1.f, 0.f};
if (polyHeader.norm_index < normals.size()) polyNormal = normals[polyHeader.norm_index];
polyNormal = impl::convertPos_NewDark(uf::matrix::multiply<float>(transforms[objIdx], polyNormal, 0.0f));
*/
pod::Vector3f polyNormal = {0.f, 1.f, 0.f};
if (polyHeader.norm_index < normals.size()) polyNormal = normals[polyHeader.norm_index];
polyNormal = uf::matrix::multiply<float>(transforms[objIdx], polyNormal, 0.0f);

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ bool ext::ttlg::loadPalette( const uf::stl::string& family, uf::stl::vector<uint
if ( !palette.empty() ) return true;
uf::stl::vector<uint8_t> buffer;
if ( uf::io::readAsBuffer(buffer, "fam://" + family + "/full.pcx")) {
if ( uf::io::readAsBuffer(buffer, "FAM://" + family + "/full.pcx")) {
if ( buffer.size() >= 769 && buffer[buffer.size() - 769] == 0x0C ) {
palette.assign(buffer.end() - 768, buffer.end());
return true;
@ -14,7 +14,7 @@ bool ext::ttlg::loadPalette( const uf::stl::string& family, uf::stl::vector<uint
buffer.clear();
}
if ( !uf::io::readAsBuffer(buffer, "fam://" + family + "/full.gif") )
if ( !uf::io::readAsBuffer(buffer, "FAM://" + family + "/full.gif") )
return false;
if ( buffer.size() < 13 || buffer[0] != 'G' || buffer[1] != 'I' || buffer[2] != 'F')
return false;

View File

@ -114,6 +114,30 @@ namespace {
auto archive = state.get();
return archive && ext::valve::readVpkRange(*archive, uf::string::lowercase( path ), start, len, buffer);
};
uf::stl::vector<uf::stl::string> list( pod::Mount& mount, const uf::stl::string& dir, const uf::stl::string& extension = "", bool recursive = false ) {
uf::stl::vector<uf::stl::string> results;
auto& state = uf::pointeredUserdata::get<VpkMountState>( mount.userdata );
auto archive = state.get();
if ( !archive ) return results;
uf::stl::string searchDir = uf::string::lowercase(dir);
uf::stl::string searchExt = uf::string::lowercase(extension);
for ( const auto& [filePath, entry] : archive->files ) {
if ( !searchDir.empty() && !filePath.starts_with(searchDir) ) continue;
if ( !recursive ) {
uf::stl::string remainder = filePath.substr(searchDir.length());
if ( remainder.find('/') != uf::stl::string::npos ) continue;
}
if ( !searchExt.empty() && !filePath.ends_with(searchExt) ) continue;
results.emplace_back(filePath);
}
return results;
};
}
pod::Mount ext::valve::createVpkMount( const uf::stl::string& uri, int priority ) {
@ -128,6 +152,7 @@ pod::Mount ext::valve::createVpkMount( const uf::stl::string& uri, int priority
mount.userdata = uf::pointeredUserdata::create<VpkMountState>();
mount.exists = ::exists;
mount.size = ::size;
mount.list = ::list;
mount.mtime = ::mtime;
mount.read = ::read;
mount.readRange = ::readRange;

View File

@ -25,7 +25,7 @@
// currently has issues with:
// * skinned meshes because I'm not applying joint transformations
// * this weird texture offsetting from movement despite having fixed this with the original camera buffers
#define BARYCENTRIC_CALCULATE 0
#define BARYCENTRIC_CALCULATE 1
#endif
#endif

View File

@ -351,6 +351,25 @@ namespace {
// ID for lz4
return false;
};
uf::stl::vector<uf::stl::string> vfs_list( pod::Mount& mount, const uf::stl::string& dir, const uf::stl::string& extension = "", bool recursive = false ) {
uf::stl::vector<uf::stl::string> files;
auto& state = uf::pointeredUserdata::get<ZipMountState>( mount.userdata );
for ( const auto& [filepath, entry] : state.entries ) {
if ( !dir.empty() && !filepath.starts_with(dir) ) continue;
if ( !recursive ) {
uf::stl::string remainder = filepath.substr(dir.length());
if ( remainder.find('/') != uf::stl::string::npos ) continue;
}
if ( !extension.empty() && !filepath.ends_with(extension) ) continue;
files.emplace_back(filepath);
}
return files;
}
}
pod::Mount ext::zlib::createZipMount( const uf::stl::string& uri, uf::stl::vector<uint8_t>& buffer, int priority ) {
@ -370,16 +389,15 @@ pod::Mount ext::zlib::createZipMount( const uf::stl::string& uri, uf::stl::vecto
mount.exists = ::vfs_exists;
mount.size = ::vfs_size;
mount.read = ::vfs_read;
mount.list = ::vfs_list;
auto& state = uf::pointeredUserdata::get<ZipMountState>( mount.userdata );
state.buffer = buffer;
ext::zlib::directory( state.buffer, state.entries );
if ( mount.path.empty() ) {
mount.path = ::fmt::format( "{}/{}", uri, (void*) state.buffer.data() );
}
if ( mount.path.empty() ) mount.path = ::fmt::format( "{}/{}", uri, (void*) state.buffer.data() );
// for ( auto& [ k, v ] : state.entries ) UF_MSG_DEBUG("{} => {}/{}", mount.path, uri, k);
// for ( auto& [ k, v ] : state.entries ) UF_MSG_DEBUG("{} => {}{}", mount.path, uri, k);
return mount;
}

View File

@ -66,6 +66,10 @@ bool uf::audio::load( pod::AudioClip& clip, const uf::stl::string& filename, boo
// to-do: PCM audio load
void uf::audio::bind( pod::AudioSource& source, pod::AudioClip* clip ) {
source.info.elapsed = 0.0f;
source.info.timer.stop();
source.info.timer.reset();
source.clip = clip;
if ( !clip ) return;
@ -79,6 +83,10 @@ void uf::audio::bind( pod::AudioSource& source, pod::AudioClip* clip ) {
}
}
void uf::audio::queue( pod::AudioSource& source, const uf::stl::string& filename ) {
source.info.pending.emplace_back( filename );
}
void uf::audio::play( pod::AudioSource& source ) {
if ( !source.alSource.playing() ) {
source.info.elapsed += source.info.timer.elapsed().asDouble();
@ -95,6 +103,17 @@ void uf::audio::stop( pod::AudioSource& source ) {
}
}
void uf::audio::pause( pod::AudioSource& source ) {
AL_CHECK_RESULT(alSourcePause( source.alSource.getIndex() ));
source.info.timer.stop();
}
bool uf::audio::paused( const pod::AudioSource& source ) {
ALint state;
AL_CHECK_RESULT(alGetSourcei( source.alSource.getIndex(), AL_SOURCE_STATE, &state ));
return state == AL_PAUSED;
}
void uf::audio::update( pod::AudioSource& source ) {
if ( !source.clip || !source.clip->streamed ) return;
@ -167,6 +186,9 @@ void uf::audio::orientation( pod::AudioSource& source, const pod::Quaternion<>&
source.transform.orientation = q;
}
float uf::audio::time( pod::AudioSource& source ) {
return source.info.elapsed + source.info.timer.elapsed().asDouble();
}
float uf::audio::time( const pod::AudioSource& source ) {
return source.info.elapsed + source.info.timer.elapsed().asDouble();
}
@ -207,6 +229,10 @@ void uf::audio::maxDistance( pod::AudioSource& source, float v ) {
}
//
float uf::audio::distance( const pod::Vector3f& position ) {
return uf::vector::distance( ::listener.position, position );
}
float uf::audio::occlusion( const pod::Vector3f& position ) {
return uf::physics::occlusion( ::listener.position, position );
}

View File

@ -25,12 +25,42 @@ pod::AudioSource& uf::AudioEmitter::emit( const uf::stl::string& key, pod::Audio
uf::audio::initialize( source );
uf::audio::bind( source, clip );
return source;
/*
if ( unique && !pool.empty() ) {
pod::AudioSource& source = pool.front();
uf::audio::stop( source );
uf::audio::bind( source, clip );
return source;
}
for ( auto& source : pool ) {
if ( !source.alSource.playing() ) {
uf::audio::stop( source );
uf::audio::bind( source, clip );
return source;
}
}
pod::AudioSource& source = pool.emplace_back();
uf::audio::initialize( source );
uf::audio::bind( source, clip );
return source;
*/
}
void uf::AudioEmitter::update() {
for ( auto& pair : this->m_container ) {
for ( auto& source : pair.second ) {
if ( source.alSource.playing() ) uf::audio::update( source );
bool isPlaying = source.alSource.playing();
bool hasPending = !source.info.pending.empty();
ALint queued = 0;
source.alSource.get(AL_BUFFERS_QUEUED, queued);
if ( isPlaying || hasPending || queued > 0 ) {
uf::audio::update( source );
}
}
}
}
@ -59,7 +89,13 @@ void uf::AudioEmitter::update( const pod::Vector3f& position, const pod::Quatern
for ( auto& pair : this->m_container ) {
for ( auto& source : pair.second ) {
if ( !source.alSource.playing() ) continue;
bool isPlaying = source.alSource.playing();
bool hasPending = !source.info.pending.empty();
ALint queued = 0;
source.alSource.get(AL_BUFFERS_QUEUED, queued);
if ( !isPlaying && !hasPending && queued == 0 ) continue;
if ( source.settings.spatial ) {
uf::audio::position( source, position );
@ -119,7 +155,12 @@ void uf::AudioEmitter::cleanup( bool purge ) {
for ( auto vecIt = pool.begin(); vecIt != pool.end(); ) {
auto& source = *vecIt;
if ( purge || (!source.alSource.playing() && !source.settings.loop) ) {
ALint state;
alGetSourcei( source.alSource.getIndex(), AL_SOURCE_STATE, &state );
bool active = (state == AL_PLAYING || state == AL_PAUSED);
if ( purge || (!active && !source.settings.loop) ) {
uf::audio::destroy( source );
vecIt = pool.erase(vecIt);
} else {

View File

@ -58,6 +58,9 @@ uf::stl::string uf::io::extension( const uf::stl::string& str, int32_t count ) {
uf::stl::string uf::io::directory( const uf::stl::string& str ) {
return str.substr( 0, str.find_last_of('/') ) + "/";
}
uf::stl::vector<uf::stl::string> uf::io::list( const uf::stl::string& dir, const uf::stl::string& extension ) {
return uf::vfs::list( uf::io::resolveURI(dir), extension );
}
size_t uf::io::size( const uf::stl::string& filename ) {
return uf::vfs::size( uf::io::resolveURI(filename) );
}

View File

@ -2,6 +2,7 @@
#include <uf/utils/io/file.h>
#include <uf/utils/math/hash.h>
#include <algorithm>
#include <filesystem>
#include <sys/stat.h>
namespace {
@ -54,6 +55,38 @@ namespace {
return size;
}
uf::stl::vector<uf::stl::string> vfs_list( pod::Mount& mount, const uf::stl::string& dir, const uf::stl::string& extension = "", bool recursive = false ) {
uf::stl::vector<uf::stl::string> files;
uf::stl::string path = mount.path + dir;
if ( !std::filesystem::exists(path) ) return files;
if ( recursive ) {
for ( const auto& entry : std::filesystem::recursive_directory_iterator(path) ) {
if ( entry.is_regular_file() ) {
uf::stl::string fname = entry.path().filename().string();
if ( extension.empty() || fname.ends_with(extension) ) {
uf::stl::string relPath = entry.path().string().substr(mount.path.length());
std::replace(relPath.begin(), relPath.end(), '\\', '/');
files.emplace_back(relPath);
}
}
}
} else {
for ( const auto& entry : std::filesystem::directory_iterator(path) ) {
if ( entry.is_regular_file() ) {
uf::stl::string fname = entry.path().filename().string();
if ( extension.empty() || fname.ends_with(extension) ) {
uf::stl::string relPath = entry.path().string().substr(mount.path.length());
std::replace(relPath.begin(), relPath.end(), '\\', '/');
files.emplace_back(relPath);
}
}
}
}
return files;
}
bool vfs_mkdir( pod::Mount& mount, const uf::stl::string& file ) {
uf::stl::string path = mount.path + file;
#if UF_ENV_DREAMCAST || UF_ENV_LINUX
@ -132,9 +165,10 @@ pod::Mount uf::vfs::createDiskMount( const uf::stl::string& uri, int priority) {
.read = ::vfs_read,
.write = ::vfs_write,
.mkdir = ::vfs_mkdir,
.list = ::vfs_list,
.readRange = ::vfs_readRange,
.readRanges = ::vfs_readRanges,
.stream = ::vfs_stream
.stream = ::vfs_stream,
};
}
@ -150,7 +184,7 @@ uf::vfs::Mount uf::vfs::mount( const pod::Mount& mount, bool temp ) {
uf::hash( hash2, m.prefix, m.path );
if ( hash == hash2 ) {
return uf::vfs::Mount{ hash, false }; // do not honor temp request to avoid breaking mounts in the future
return uf::vfs::Mount{ hash, NULL, false }; // do not honor temp request to avoid breaking mounts in the future
}
}
@ -162,7 +196,7 @@ uf::vfs::Mount uf::vfs::mount( const pod::Mount& mount, bool temp ) {
return a.priority > b.priority;
});
return uf::vfs::Mount{ hash, temp };
return uf::vfs::Mount{ hash, &mount, temp };
}
bool uf::vfs::unmount( size_t hash ) {
@ -220,6 +254,26 @@ bool uf::vfs::exists( const uf::stl::string& path ) {
return false;
}
uf::stl::vector<uf::stl::string> uf::vfs::list( const uf::stl::string& path, const uf::stl::string& extension, bool recursive ) {
uf::stl::vector<uf::stl::string> results;
uf::stl::string prefix, relative;
uf::io::splitUri(path, prefix, relative);
if ( !relative.empty() && relative.back() != '/' ) relative += '/';
for ( auto& mount : mounts ) {
if ( prefix.empty() && mount.priority < 0 ) continue;
if ( prefix.empty() || mount.prefix == prefix ) {
if ( mount.list ) {
auto files = mount.list( mount, relative, extension, recursive );
results.insert(results.end(), files.begin(), files.end());
}
}
}
return results;
}
bool uf::vfs::mkdir( const uf::stl::string& path ) {
uf::stl::string prefix, relative;
uf::io::splitUri(path, prefix, relative);
@ -302,7 +356,7 @@ bool uf::vfs::readRange( const uf::stl::string& path, size_t start, size_t len,
uf::stl::vector<uint8_t> fullBuffer;
if ( !mount.read( mount, relative, fullBuffer ) ) continue;
UF_MSG_DEBUG("hitting fallback: {}", path);
//UF_MSG_DEBUG("hitting fallback: {}", path);
if ( start < fullBuffer.size() ) {
size_t actualLen = std::min(len, fullBuffer.size() - start);
@ -328,7 +382,7 @@ bool uf::vfs::readRanges( const uf::stl::string& path, const uf::stl::vector<pod
uf::stl::vector<uint8_t> fullBuffer;
if ( !mount.read( mount, relative, fullBuffer ) ) continue;
UF_MSG_DEBUG("hitting fallback: {}", path);
//UF_MSG_DEBUG("hitting fallback: {}", path);
size_t totalBytes = 0;
for ( const auto& r : ranges ) {
@ -361,7 +415,7 @@ bool uf::vfs::stream( const uf::stl::string& path, size_t chunkSize, std::functi
if ( mount.stream ) return mount.stream( mount, relative, chunkSize, callback );
if ( !mount.read ) continue;
UF_MSG_DEBUG("hitting fallback: {}", path);
//UF_MSG_DEBUG("hitting fallback: {}", path);
uf::stl::vector<uint8_t> fullBuffer;
if ( !mount.read( mount, relative, fullBuffer ) ) continue;