diff --git a/bin/data/config.json b/bin/data/config.json index 95357fac..8edb8bc5 100644 --- a/bin/data/config.json +++ b/bin/data/config.json @@ -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, diff --git a/bin/data/entities/door.json b/bin/data/entities/door.json index ba140830..5a43a3bd 100644 --- a/bin/data/entities/door.json +++ b/bin/data/entities/door.json @@ -1,5 +1,5 @@ { - "assets": ["./scripts/door.lua"], + "assets": ["./scripts/valve/door.lua"], "behaviors": [ "AudioEmitterBehavior" ], diff --git a/bin/data/entities/scripts/dark/ambient.lua b/bin/data/entities/scripts/dark/ambient.lua new file mode 100644 index 00000000..92645bd1 --- /dev/null +++ b/bin/data/entities/scripts/dark/ambient.lua @@ -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 \ No newline at end of file diff --git a/bin/data/entities/scripts/dark/door.lua b/bin/data/entities/scripts/dark/door.lua new file mode 100644 index 00000000..30b80589 --- /dev/null +++ b/bin/data/entities/scripts/dark/door.lua @@ -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) \ No newline at end of file diff --git a/bin/data/entities/scripts/dark/io.lua b/bin/data/entities/scripts/dark/io.lua new file mode 100644 index 00000000..8fbad98e --- /dev/null +++ b/bin/data/entities/scripts/dark/io.lua @@ -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) \ No newline at end of file diff --git a/bin/data/entities/scripts/dark/music.lua b/bin/data/entities/scripts/dark/music.lua new file mode 100644 index 00000000..3dedcdcb --- /dev/null +++ b/bin/data/entities/scripts/dark/music.lua @@ -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) \ No newline at end of file diff --git a/bin/data/entities/scripts/dark/trap.lua b/bin/data/entities/scripts/dark/trap.lua new file mode 100644 index 00000000..01577eb4 --- /dev/null +++ b/bin/data/entities/scripts/dark/trap.lua @@ -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) \ No newline at end of file diff --git a/bin/data/entities/scripts/dark/trigger.lua b/bin/data/entities/scripts/dark/trigger.lua new file mode 100644 index 00000000..2d6d856e --- /dev/null +++ b/bin/data/entities/scripts/dark/trigger.lua @@ -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 ) \ No newline at end of file diff --git a/bin/data/entities/scripts/ambient_generic.lua b/bin/data/entities/scripts/valve/ambient_generic.lua similarity index 77% rename from bin/data/entities/scripts/ambient_generic.lua rename to bin/data/entities/scripts/valve/ambient_generic.lua index 2fac284c..52eecf29 100644 --- a/bin/data/entities/scripts/ambient_generic.lua +++ b/bin/data/entities/scripts/valve/ambient_generic.lua @@ -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) diff --git a/bin/data/entities/scripts/door.lua b/bin/data/entities/scripts/valve/door.lua similarity index 100% rename from bin/data/entities/scripts/door.lua rename to bin/data/entities/scripts/valve/door.lua diff --git a/bin/data/entities/scripts/io.lua b/bin/data/entities/scripts/valve/io.lua similarity index 93% rename from bin/data/entities/scripts/io.lua rename to bin/data/entities/scripts/valve/io.lua index 85e3b65c..0fbd6686 100644 --- a/bin/data/entities/scripts/io.lua +++ b/bin/data/entities/scripts/valve/io.lua @@ -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) \ No newline at end of file diff --git a/bin/data/entities/scripts/trigger.lua b/bin/data/entities/scripts/valve/trigger.lua similarity index 100% rename from bin/data/entities/scripts/trigger.lua rename to bin/data/entities/scripts/valve/trigger.lua diff --git a/bin/data/shaders/common/functions.h b/bin/data/shaders/common/functions.h index bb4baa1b..00f75ddc 100644 --- a/bin/data/shaders/common/functions.h +++ b/bin/data/shaders/common/functions.h @@ -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; diff --git a/bin/data/shaders/common/macros.h b/bin/data/shaders/common/macros.h index 75cd6c02..ea9155ac 100644 --- a/bin/data/shaders/common/macros.h +++ b/bin/data/shaders/common/macros.h @@ -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 diff --git a/engine/inc/uf/ext/ttlg/common.h b/engine/inc/uf/ext/ttlg/common.h index 6b4de884..24da1d36 100644 --- a/engine/inc/uf/ext/ttlg/common.h +++ b/engine/inc/uf/ext/ttlg/common.h @@ -5,15 +5,20 @@ #include namespace impl { - const float darkToMeters = 0.75f; + const float darkToMeters = 0.7f; typedef uf::Meshlet_T Meshlet; // could be dedicated functions in the engine template - inline bool readStruct(const uf::stl::vector& 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& 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; } diff --git a/engine/inc/uf/utils/audio/audio.h b/engine/inc/uf/utils/audio/audio.h index bf214e77..0f433bc0 100644 --- a/engine/inc/uf/utils/audio/audio.h +++ b/engine/inc/uf/utils/audio/audio.h @@ -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& ); diff --git a/engine/inc/uf/utils/audio/metadata.h b/engine/inc/uf/utils/audio/metadata.h index 95948055..da87fda6 100644 --- a/engine/inc/uf/utils/audio/metadata.h +++ b/engine/inc/uf/utils/audio/metadata.h @@ -62,6 +62,7 @@ namespace pod { struct { uf::Timer<> timer; float elapsed = 0; + uf::stl::vector pending; } info; struct { diff --git a/engine/inc/uf/utils/io/file.h b/engine/inc/uf/utils/io/file.h index 37a7e0e1..e4b91b95 100644 --- a/engine/inc/uf/utils/io/file.h +++ b/engine/inc/uf/utils/io/file.h @@ -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 list( const uf::stl::string&, const uf::stl::string& = "" ); bool UF_API readAsString( uf::stl::string&, const uf::stl::string&, const uf::stl::string& = "" ); diff --git a/engine/inc/uf/utils/io/vfs.h b/engine/inc/uf/utils/io/vfs.h index f0f408d6..03475fe0 100644 --- a/engine/inc/uf/utils/io/vfs.h +++ b/engine/inc/uf/utils/io/vfs.h @@ -22,6 +22,7 @@ namespace pod { std::function&)> read; std::function write; std::function mkdir; + std::function(pod::Mount&, const uf::stl::string&, const uf::stl::string&, bool)> list; std::function&)> readRange; std::function&, uf::stl::vector&)> readRanges; @@ -35,6 +36,7 @@ namespace uf { extern UF_API uf::stl::vector 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& buffer ); bool UF_API mkdir( const uf::stl::string& path ); + uf::stl::vector 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& buffer ); bool UF_API readRanges( const uf::stl::string& path, const uf::stl::vector& ranges, uf::stl::vector& buffer ); diff --git a/engine/src/engine/ext/audio/emitter/behavior.cpp b/engine/src/engine/ext/audio/emitter/behavior.cpp index d70c8122..3d43b097 100644 --- a/engine/src/engine/ext/audio/emitter/behavior.cpp +++ b/engine/src/engine/ext/audio/emitter/behavior.cpp @@ -19,9 +19,6 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) { UF_BEHAVIOR_METADATA_BIND_SERIALIZER_HOOKS(metadata, metadataJson); - if ( !metadataJson["audio"]["epsilon"].is() ) - metadataJson["audio"]["epsilon"] = 0.001f; - this->addHook( "sound:Stop.%UID%", [&](ext::json::Value& json){ uf::stl::string filename = json["filename"].as(); auto& pools = emitter.get(); @@ -71,11 +68,12 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) { if ( json["gain"].is() ) uf::audio::gain(source, json["gain"].as()); if ( json["rolloffFactor"].is() ) uf::audio::rolloff(source, json["rolloffFactor"].as()); if ( json["maxDistance"].is() ) uf::audio::maxDistance(source, json["maxDistance"].as()); - if ( json["spatial"].is() ) source.settings.spatial = json["spatial"].as(); + if ( json["loop"].is() ) uf::audio::loop(source, json["loop"].as()); - if ( json["wants loop"].is() ) { - uf::audio::loop(source, json["wants loop"].as(true) && clip && clip->info.loop.has); + else if ( json["wants loop"].is() ) { + auto wants = json["wants loop"].as(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(); + int layer = payload["layer"].as(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(); 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(2.5f); - this->callHook( "asset:QueueLoad.%UID%", this->resolveToPayload( filename ) ); + auto pload = this->resolveToPayload( filename ); + pload.metadata["layer"] = payload["layer"]; + + if ( payload["loop"].is() ) pload.metadata["loop"] = payload["loop"]; + if ( payload["notify"].is() ) 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( payload.filename ); + + int layer = payload.metadata["layer"].as(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(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>(); + 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); } } } diff --git a/engine/src/engine/ext/audio/emitter/behavior.h b/engine/src/engine/ext/audio/emitter/behavior.h index 3ea54d48..b0373053 100644 --- a/engine/src/engine/ext/audio/emitter/behavior.h +++ b/engine/src/engine/ext/audio/emitter/behavior.h @@ -18,6 +18,7 @@ namespace ext { uf::stl::string intro; float epsilon = 0.0001f; pod::Vector2f fade = {}; + float prevTime = 0.0f; bool active = false; }; diff --git a/engine/src/engine/graph/graph.cpp b/engine/src/engine/graph/graph.cpp index fac041b3..3be684eb 100644 --- a/engine/src/engine/graph/graph.cpp +++ b/engine/src/engine/graph/graph.cpp @@ -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() ) { 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() ) { + 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("") == "trigger" ) { + loadJson["assets"].emplace_back("ent://scripts/dark/trigger.lua"); + } + } + if ( ext::json::isObject( tag ) ) { if ( tag["action"].as() == "load" ) { if ( tag["filename"].is() ) { diff --git a/engine/src/ext/audio/vorbis.cpp b/engine/src/ext/audio/vorbis.cpp index e1e8c446..3a78c785 100644 --- a/engine/src/ext/audio/vorbis.cpp +++ b/engine/src/ext/audio/vorbis.cpp @@ -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 ) { // ... } diff --git a/engine/src/ext/audio/wav.cpp b/engine/src/ext/audio/wav.cpp index 7b779789..083535a5 100644 --- a/engine/src/ext/audio/wav.cpp +++ b/engine/src/ext/audio/wav.cpp @@ -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 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 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 ) { diff --git a/engine/src/ext/lua/lua.cpp b/engine/src/ext/lua/lua.cpp index aa067958..5c5ddd35 100644 --- a/engine/src/ext/lua/lua.cpp +++ b/engine/src/ext/lua/lua.cpp @@ -202,6 +202,9 @@ namespace binds { // object.destroy(); // delete &object; }; + uf::stl::vector 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 diff --git a/engine/src/ext/ttlg/bin.cpp b/engine/src/ext/ttlg/bin.cpp index c08a7336..d0fd2b39 100644 --- a/engine/src/ext/ttlg/bin.cpp +++ b/engine/src/ext/ttlg/bin.cpp @@ -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(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(transforms[objIdx], polyNormal, 0.0f); diff --git a/engine/src/ext/ttlg/mis.cpp b/engine/src/ext/ttlg/mis.cpp index b7c5ec2f..9893e218 100644 --- a/engine/src/ext/ttlg/mis.cpp +++ b/engine/src/ext/ttlg/mis.cpp @@ -8,6 +8,9 @@ #include #include + +// to-do: split this into subcomponents + namespace impl { #pragma pack(push, 1) struct DarkDBHeader { @@ -117,7 +120,7 @@ namespace impl { uint32_t size; }; - struct LinkData { + struct DarkLinkData { int32_t sourceId; int32_t destId; uint16_t flavor; @@ -130,13 +133,10 @@ namespace impl { uint16_t flavor; }; - // ordered in the way they're read per OpenDarkEngine struct PropertyPosition { pod::Vector3f position; int32_t cell; - int16_t heading; - int16_t pitch; - int16_t bank; + int16_t facing[3]; // heading, pitch, bank }; struct PropertyLight { @@ -147,11 +147,28 @@ namespace impl { float radius; }; + struct PropertyDoor { + int32_t type; + float closed; + float open; + float base_speed; + int32_t axis; + int32_t status; + int32_t hard_limits; + float sound_blocking; + int32_t vision_blocking; + float push_mass; + // ... + }; + struct PropertyAmbient { + int32_t radius; + int32_t override_volume; + uint16_t flags; + uint16_t pad; char schemaName[16]; - uint32_t flags; - float volume; - float radius; + char auxSchema1[16]; + char auxSchema2[16]; }; // ? @@ -162,10 +179,174 @@ namespace impl { float brightness; float radius; }; + + struct PropertyPhysType { + int32_t type; // 0 = OBB, 1 = Sphere, 2 = SphereHat, 3 = None + int32_t num_submodels; + int32_t remove_on_sleep; + int32_t special; + }; + + struct PropertyPhysAttr { + float gravity; + float mass; + float density; + float elasticity; + float base_friction; + pod::Vector3f cog_offset; + int32_t rot_axes; + int32_t rest_axes; + int32_t climbable_sides; + int32_t edge_trigger; + float pore_size; + }; + + struct PropertyPhysDims { + float radius[2]; + pod::Vector3f offset[2]; + pod::Vector3f size; + int32_t pt_vs_terrain; + int32_t pt_vs_not_special; + }; + + struct PropertyPhysState { + pod::Vector3f location; + pod::Vector3f facing; + pod::Vector3f velocity; + pod::Vector3f rot_velocity; + }; + + constexpr uint32_t SCH_PLAY_RETRIGGER = (1 << 0); + constexpr uint32_t SCH_PAN_POS = (1 << 1); + constexpr uint32_t SCH_PAN_RANGE = (1 << 2); + constexpr uint32_t SCH_NO_REPEAT = (1 << 3); + constexpr uint32_t SCH_NO_CACHE = (1 << 4); + constexpr uint32_t SCH_STREAM = (1 << 5); + constexpr uint32_t SCH_PLAY_ONCE = (1 << 6); + constexpr uint32_t SCH_NO_COMBAT = (1 << 7); + constexpr uint32_t SCH_NET_AMBIENT = (1 << 8); + constexpr uint32_t SCH_LOC_SPATIAL = (1 << 9); + + constexpr uint8_t SCHEMA_LOOP_POLY = 0x01; + constexpr uint8_t SCHEMA_LOOP_COUNT = 0x02; + + struct PropertySchPlayParams { + uint32_t flags; + int32_t volume; + int32_t pan; + int32_t initialDelay; + int32_t fade; + }; + + struct PropertySchLoopParams { + uint8_t flags; + uint8_t maxSamples; + uint16_t count; + uint16_t intervalMin; + uint16_t intervalMax; + }; + + struct PropertySchSamp { + uf::stl::string name; + uint8_t weight; + }; + + struct PropertySongParams { + char songName[32]; + }; + + constexpr size_t kSONG_MaxStringLen = 32; + + struct sSongInfo { + char id[kSONG_MaxStringLen]; + }; + + struct sSongSectionInfo { + char id[kSONG_MaxStringLen]; + int32_t volume; + int32_t loopCount; + }; + + struct sSongSampleInfo { + char name[kSONG_MaxStringLen]; + }; + + struct sSongEventInfo { + char eventString[kSONG_MaxStringLen]; + uint32_t flags; + }; + + struct sSongGotoInfo { + int32_t sectionIndex; + int32_t probability; + }; + + struct sTagDBData { + int32_t objId; + float weight; + }; + + struct cTagDBKey { + uint32_t type; + union { + struct { + int32_t minVal; + int32_t maxVal; + }; + uint8_t aEnum[8]; + }; + }; #pragma pack(pop) + // pseudo-structs + struct DarkSchema { + uf::stl::string name; + uf::stl::vector wavs; + PropertySchPlayParams playParams{}; + PropertySchLoopParams loopParams{}; + + uf::stl::string classTag; + uf::stl::string msg; + uf::stl::string action; + + // can probably deduce without these + bool hasPlayParams = false; + bool hasLoopParams = false; + }; + + struct SongGoto { + int32_t sectionIndex; + int32_t probability; + }; + + struct SongEvent { + uf::stl::string eventString; + uint32_t flags; + uf::stl::vector gotos; + }; + + struct SongSample { + uf::stl::string name; + }; + + struct SongSection { + uf::stl::string id; + int32_t volume; + int32_t loopCount; + uf::stl::vector samples; + uf::stl::vector events; + }; + + struct Song { + uf::stl::string id; + uf::stl::vector globalEvents; + uf::stl::vector sections; + }; + + // do not load these uf::stl::unordered_set modelBlacklist = { + "playbox.bin", "fx_particle.bin", "spark_.bin" }; @@ -185,11 +366,30 @@ namespace impl { struct { uf::stl::unordered_map position; uf::stl::unordered_map light; + uf::stl::unordered_map rotDoor; + uf::stl::unordered_map transDoor; + + uf::stl::unordered_map physType; + uf::stl::unordered_map physAttr; + uf::stl::unordered_map physDims; + uf::stl::unordered_map physState; + uf::stl::unordered_map ambient; + + uf::stl::unordered_map schPlayParams; + uf::stl::unordered_map schLoopParams; + uf::stl::unordered_map> schSamps; + uf::stl::unordered_map classTag; + uf::stl::unordered_map schMsg; + uf::stl::unordered_map schAction; + uf::stl::unordered_map> script; } properties; - uf::stl::vector links; + uf::stl::unordered_map fileDatabase; + + uf::stl::unordered_map schemas; + uf::stl::vector links; uf::stl::unordered_map linkFlavorNames; uf::stl::unordered_map> palettes; @@ -254,7 +454,7 @@ namespace impl { auto& buffer = ctx.buffer; auto& inventory = ctx.inventory; - auto fullName = ::fmt::format( "P${}", propName ); + auto fullName = ::fmt::format( "P${}", propName ).substr(0, 11); if ( inventory.count( fullName ) == 0 ) { return; } @@ -269,53 +469,195 @@ namespace impl { uint32_t next = offset + entry.size; - if ( propName == "Position" && entry.size >= sizeof(PropertyPosition) && entry.objectId >= 0 ) { + // to-do: clean up most of this redundancy + if ( propName == "Position" && entry.objectId >= 0 ) { PropertyPosition position; - if ( impl::readStruct( buffer, offset, position ) ) { + if ( impl::readStruct( buffer, offset, position, entry.size ) ) { ctx.properties.position[entry.objectId] = position; } - } else if ( propName == "Light" && entry.size >= sizeof(PropertyLight) ) { + } else if ( propName == "Light" ) { PropertyLight light; - if ( impl::readStruct( buffer, offset, light ) ) { + if ( impl::readStruct( buffer, offset, light, entry.size ) ) { ctx.properties.light[entry.objectId] = light; } - } else if ( propName == "Ambient" && entry.size >= sizeof(PropertyAmbient) ) { + } else if ( propName == "RotDoor" ) { + PropertyDoor p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.rotDoor[entry.objectId] = p; + } + } else if ( propName == "TransDoor" ) { + PropertyDoor p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.transDoor[entry.objectId] = p; + } + } else if ( propName == "Ambient" || propName == "AmbientHacked" ) { PropertyAmbient sound; - if ( impl::readStruct( buffer, offset, sound ) ) { + if ( impl::readStruct( buffer, offset, sound, entry.size ) ) { ctx.properties.ambient[entry.objectId] = sound; } + } else if ( propName == "PhysType" ) { + PropertyPhysType p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.physType[entry.objectId] = p; + } + } else if ( propName == "PhysType" && entry.size >= 8 ) { + PropertyPhysType p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.physType[entry.objectId] = p; + } + } else if ( propName == "PhysAttr" && entry.size >= 48 ) { + PropertyPhysAttr p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.physAttr[entry.objectId] = p; + } + } else if ( propName == "PhysDims" && entry.size >= 48 ) { + PropertyPhysDims p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.physDims[entry.objectId] = p; + } + } else if ( propName == "PhysState" && entry.size >= 48 ) { + PropertyPhysState p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.physState[entry.objectId] = p; + } + } else if ( propName == "SchPlayParams" && entry.size >= sizeof(PropertySchPlayParams) ) { + PropertySchPlayParams p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.schPlayParams[entry.objectId] = p; + } + } else if ( propName == "SchLoopParams" && entry.size >= sizeof(PropertySchLoopParams) ) { + PropertySchLoopParams p; + if ( impl::readStruct( buffer, offset, p, entry.size ) ) { + ctx.properties.schLoopParams[entry.objectId] = p; + } } else if ( propName == "Scripts" ) { - const char* scriptCursor = (const char*)(buffer.data() + offset); + const char* cursor = (const char*)(buffer.data() + offset); size_t remainingBytes = entry.size; for ( auto s = 0; s < 4; ++s ) { - if ( remainingBytes <= 0 || *scriptCursor == '\0' ) break; + if ( remainingBytes <= 0 || *cursor == '\0' ) break; - auto script = uf::stl::string( scriptCursor ); + auto script = uf::stl::string( cursor ); if ( script.empty() ) break; ctx.properties.script[entry.objectId].emplace_back(script); - size_t step = strnlen(scriptCursor, remainingBytes) + 1; + size_t step = strnlen(cursor, remainingBytes) + 1; if ( step > remainingBytes ) break; - scriptCursor += step; + cursor += step; remainingBytes -= step; } + } else if ( propName == "Class Tag" || propName == "SchMsg" || propName == "SchAction" ) { + if ( entry.size > 4 && entry.objectId != 0 ) { + auto s = impl::getStringFromOffset( buffer, offset + 4, entry.size - 4 ); + if ( propName == "Class Tag" ) ctx.properties.classTag[entry.objectId] = s; + if ( propName == "SchMsg" ) ctx.properties.schMsg[entry.objectId] = s; + if ( propName == "SchAction" ) ctx.properties.schAction[entry.objectId] = s; + } + } /*else if ( propName == "Class Tag" ) { + auto s = impl::getStringFromOffset( buffer, offset, entry.size ); + if ( !s.empty() ) ctx.properties.classTag[entry.objectId] = s; + } else if ( propName == "SchMsg" ) { + auto s = impl::getStringFromOffset( buffer, offset, entry.size ); + if ( !s.empty() ) ctx.properties.schMsg[entry.objectId] = s; + } else if ( propName == "SchAction" ) { + auto s = impl::getStringFromOffset( buffer, offset, entry.size ); + if ( !s.empty() ) ctx.properties.schAction[entry.objectId] = s; + } */ else if ( propName == "ModelName" ) { + auto modelName = impl::getStringFromOffset( buffer, offset, entry.size ); + if ( !modelName.empty() ) ctx.modelNames[entry.objectId] = modelName; } else if ( propName == "SymName" ) { if ( entry.size > 4 ) { auto symName = impl::getStringFromOffset( buffer, offset + 4, entry.size - 4 ); if ( !symName.empty() ) ctx.archetypes[entry.objectId] = symName; } - } else if ( propName == "ModelName" ) { - auto modelName = impl::getStringFromOffset(buffer, offset, entry.size); - if ( !modelName.empty() ) ctx.modelNames[entry.objectId] = modelName; } offset = next; } } + void parseEnvSoundTree( + const uf::stl::vector& buffer, uint32_t& offset, + const uf::stl::unordered_map& tagMap, + const uf::stl::unordered_map& valueMap, + uf::stl::string currentTags, + impl::DarkContext& ctx + ) { + int32_t dataSize; + if (!impl::readStruct(buffer, offset, dataSize)) return; + + for (int i = 0; i < dataSize; ++i) { + sTagDBData data; + if (!impl::readStruct(buffer, offset, data)) return; + ctx.properties.classTag[data.objId] = currentTags; + } + + int32_t branchSize; + if (!impl::readStruct(buffer, offset, branchSize)) return; + + for (int i = 0; i < branchSize; ++i) { + cTagDBKey key; + if (!impl::readStruct(buffer, offset, key)) return; + + uf::stl::string newTags = currentTags; + if (!newTags.empty()) newTags += ", "; + + uf::stl::string tagName = tagMap.count(key.type) ? tagMap.at(key.type) : ::fmt::format("UnknownTag_{}", key.type); + uf::stl::string valueNames = ""; + + bool isEnum = false; + for (int v = 0; v < 8; ++v) { + if (key.aEnum[v] == 255) isEnum = true; + } + + if (isEnum || valueMap.count(key.aEnum[0])) { + for (int v = 0; v < 8; ++v) { + if (key.aEnum[v] != 255 && key.aEnum[v] != 0) { + if (!valueNames.empty()) valueNames += "|"; + valueNames += valueMap.count(key.aEnum[v]) ? valueMap.at(key.aEnum[v]) : std::to_string(key.aEnum[v]); + } + } + newTags += tagName + " " + valueNames; + } else { + newTags += tagName + " [" + std::to_string(key.minVal) + "-" + std::to_string(key.maxVal) + "]"; + } + + parseEnvSoundTree(buffer, offset, tagMap, valueMap, newTags, ctx); + } + } + + void parseSchSamp( impl::DarkContext& ctx, const impl::DarkDBInvItem& item ) { + auto& buffer = ctx.buffer; + uint32_t offset = item.offset + sizeof(impl::DarkDBChunkHeader); + uint32_t endOffset = item.offset + item.length; + + while ( offset < endOffset ) { + int32_t objId; + if ( !impl::readStruct( buffer, offset, objId ) ) break; + + int32_t numSamples; + if ( !impl::readStruct( buffer, offset, numSamples ) ) break; + + for ( int32_t i = 0; i < numSamples; ++i ) { + int32_t strLen; + if ( !impl::readStruct( buffer, offset, strLen ) ) break; + + uf::stl::string sampleName = impl::getStringFromOffset( buffer, offset, strLen ); + offset += strLen; + + uint8_t freq; + if ( !impl::readStruct( buffer, offset, freq ) ) break; + + ctx.properties.schSamps[objId].emplace_back(PropertySchSamp{ + .name = sampleName, + .weight = freq, + }); + } + } + } + void parseObjVec( pod::Graph& graph, impl::DarkContext& ctx, const impl::DarkDBInvItem& item ) { auto& buffer = ctx.buffer; DarkDBObjVec_Header header; @@ -392,92 +734,194 @@ namespace impl { } } + void parseSchemas( impl::DarkContext& ctx ) { + for ( const auto& [objId, wavs] : ctx.properties.schSamps ) { + uf::stl::string schemaName; + + if ( ctx.customNames.count(objId) ) { + schemaName = ctx.customNames.at(objId); + } else if ( ctx.archetypes.count(objId) ) { + schemaName = ctx.archetypes.at(objId); + } else { + continue; + } + + std::transform(schemaName.begin(), schemaName.end(), schemaName.begin(), ::tolower); + + auto& schema = ctx.schemas[objId]; + schema.name = schemaName; + + ctx.findInheritedProperty(objId, ctx.properties.classTag, schema.classTag); + ctx.findInheritedProperty(objId, ctx.properties.schMsg, schema.msg); + ctx.findInheritedProperty(objId, ctx.properties.schAction, schema.action); + + for ( const auto& sample : wavs ) { + uf::stl::string wavLower = sample.name; + std::transform(wavLower.begin(), wavLower.end(), wavLower.begin(), ::tolower); + if (!wavLower.ends_with(".wav")) wavLower += ".wav"; + + uf::stl::string baseName = wavLower; + size_t slashPos = baseName.find_last_of('/'); + if (slashPos != uf::stl::string::npos) { + baseName = baseName.substr(slashPos + 1); + } + + uf::stl::string resolvedPath = "SND://" + baseName; + if ( ctx.fileDatabase.count(baseName) ) { + resolvedPath = ctx.fileDatabase[baseName]; + } + + schema.wavs.emplace_back(PropertySchSamp{ + .name = resolvedPath, + .weight = sample.weight, + }); + } + + schema.hasPlayParams = ctx.findInheritedProperty(objId, ctx.properties.schPlayParams, schema.playParams); + schema.hasLoopParams = ctx.findInheritedProperty(objId, ctx.properties.schLoopParams, schema.loopParams); + } + } + + bool parseSong( const uf::stl::vector& buffer, impl::Song& outSong ) { + uint32_t offset = 0; + + uint32_t fileVersion; + if ( !impl::readStruct( buffer, offset, fileVersion ) ) return false; + if ( fileVersion != 1) { + UF_MSG_WARNING("Song file version mismatch (expected 1, got {})", fileVersion); + } + + impl::sSongInfo songInfo; + if ( !impl::readStruct( buffer, offset, songInfo ) ) return false; + outSong.id = uf::stl::string(songInfo.id, strnlen(songInfo.id, kSONG_MaxStringLen)); + + uint32_t numGlobalEvents; + if ( !impl::readStruct( buffer, offset, numGlobalEvents ) ) return false; + + for ( uint32_t i = 0; i < numGlobalEvents; ++i ) { + impl::sSongEventInfo evtInfo; + if ( !impl::readStruct( buffer, offset, evtInfo ) ) break; + + auto& evt = outSong.globalEvents.emplace_back(); + evt.eventString = uf::stl::string(evtInfo.eventString, strnlen(evtInfo.eventString, kSONG_MaxStringLen)); + evt.flags = evtInfo.flags; + + uint32_t numGotos; + if ( !impl::readStruct( buffer, offset, numGotos ) ) break; + + for ( uint32_t j = 0; j < numGotos; ++j ) { + impl::sSongGotoInfo gotoInfo; + if ( !impl::readStruct( buffer, offset, gotoInfo ) ) break; + evt.gotos.push_back({ gotoInfo.sectionIndex, gotoInfo.probability }); + } + } + + uint32_t numSections; + if ( !impl::readStruct( buffer, offset, numSections ) ) return false; + + for ( uint32_t i = 0; i < numSections; ++i ) { + impl::sSongSectionInfo secInfo; + if ( !impl::readStruct( buffer, offset, secInfo ) ) break; + + auto& sec = outSong.sections.emplace_back(); + sec.id = uf::stl::string(secInfo.id, strnlen(secInfo.id, kSONG_MaxStringLen)); + sec.volume = secInfo.volume; + sec.loopCount = secInfo.loopCount; + + uint32_t numSamples; + if ( !impl::readStruct( buffer, offset, numSamples ) ) break; + + for ( uint32_t j = 0; j < numSamples; ++j ) { + impl::sSongSampleInfo sampInfo; + if ( !impl::readStruct( buffer, offset, sampInfo ) ) break; + + uf::stl::string sampleName = uf::stl::string(sampInfo.name, strnlen(sampInfo.name, kSONG_MaxStringLen)); + sec.samples.push_back({ sampleName }); + } + + uint32_t numSecEvents; + if ( !impl::readStruct( buffer, offset, numSecEvents ) ) break; + + for ( uint32_t j = 0; j < numSecEvents; ++j ) { + impl::sSongEventInfo evtInfo; + if ( !impl::readStruct( buffer, offset, evtInfo ) ) break; + + auto& evt = sec.events.emplace_back(); + evt.eventString = uf::stl::string(evtInfo.eventString, strnlen(evtInfo.eventString, kSONG_MaxStringLen)); + evt.flags = evtInfo.flags; + + uint32_t numGotos; + if ( !impl::readStruct( buffer, offset, numGotos ) ) break; + + for ( uint32_t k = 0; k < numGotos; ++k ) { + impl::sSongGotoInfo gotoInfo; + if ( !impl::readStruct( buffer, offset, gotoInfo ) ) break; + evt.gotos.push_back({ gotoInfo.sectionIndex, gotoInfo.probability }); + } + } + } + + return true; + } + void loadMaterials( pod::Graph& graph, impl::DarkContext& ctx ) { auto& storage = uf::graph::getStorage(graph); - uf::stl::vector extensions = { ".png", ".dds", ".tga", ".pcx", ".gif", ".bmp" }; uf::stl::vector missing_pixels = { 255, 0, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 0, 255, 255 }; - uf::stl::vector buffer; + for ( const auto& matName : graph.materials ) { auto& image = storage.images[matName].data; if ( !image.getPixels().empty() ) continue; // already loaded bool loaded = false; - - // sanitize name and family uf::stl::string family = ""; uf::stl::string texName = matName; - uf::stl::vector searchDirs; + size_t slashPos = matName.find('/'); if ( slashPos != uf::stl::string::npos ) { family = matName.substr(0, slashPos); texName = matName.substr(slashPos + 1); } - // family deduced, add it to search queue - if ( !family.empty() ) { - searchDirs.emplace_back("fam://" + family + "/"); - searchDirs.emplace_back("fam://" + family + "/anim/"); - for ( const auto& fam : ctx.families ) { - if ( fam != family && !fam.empty() ) searchDirs.emplace_back("fam://" + fam + "/"); - if ( fam != family && !fam.empty() ) searchDirs.emplace_back("fam://" + fam + "/anim/"); - } - // no family deduced, most likely an object + uf::stl::string targetPath = ""; + if ( ctx.fileDatabase.count(matName) ) { + targetPath = ctx.fileDatabase[matName]; } else { - searchDirs.emplace_back("obj://txt16/"); - searchDirs.emplace_back("obj://txt/"); - searchDirs.emplace_back("obj://"); for ( const auto& fam : ctx.families ) { - if ( !fam.empty() ) { - searchDirs.emplace_back("fam://" + fam + "/"); - searchDirs.emplace_back("fam://" + fam + "/anim/"); + if ( ctx.fileDatabase.count(fam + "/" + texName) ) { + targetPath = ctx.fileDatabase[fam + "/" + texName]; + family = fam; + break; } } } + if ( targetPath.empty() && ctx.fileDatabase.count(texName) ) { + targetPath = ctx.fileDatabase[texName]; + } - // search for target - for ( const auto& dir : searchDirs ) { - if ( loaded ) break; + if ( !targetPath.empty() && uf::io::readAsBuffer( buffer, targetPath ) ) { + uf::stl::string lowerTarget = targetPath; + std::transform(lowerTarget.begin(), lowerTarget.end(), lowerTarget.begin(), ::tolower); - if ( dir.starts_with("fam://") ) { - size_t fStart = 6; - size_t fEnd = dir.find('/', fStart); - if ( fEnd != uf::stl::string::npos ) family = dir.substr(fStart, fEnd - fStart); - } - - for ( const auto& ext : extensions ) { - uf::stl::string target = dir + texName + ext; - // target doesn't exist - if ( !uf::io::exists( target ) ) continue; - // failed to read target - if ( !uf::io::readAsBuffer( buffer, target ) ) continue; - - // is a PCX - if ( ext == ".pcx" ) { - if ( !family.empty() ) ext::ttlg::loadPalette( family, ctx.palettes[family] ); - const uint8_t* palette = (!family.empty() && !ctx.palettes[family].empty()) ? ctx.palettes[family].data() : nullptr; - if ( !ext::ttlg::loadPcx( image, buffer, palette ) ) { - UF_MSG_ERROR("Failed to load PCX: {}", target); - continue; - } - // not a PCX, load it - } else if ( !uf::image::open(image, buffer, target ) ) { - continue; + if ( lowerTarget.ends_with(".pcx") ) { + if ( !family.empty() ) ext::ttlg::loadPalette( family, ctx.palettes[family] ); + const uint8_t* palette = (!family.empty() && !ctx.palettes[family].empty()) ? ctx.palettes[family].data() : nullptr; + if ( !ext::ttlg::loadPcx( image, buffer, palette ) ) { + UF_MSG_ERROR("Failed to load PCX: {}", targetPath); + } else { + loaded = true; } - - // animated - if ( dir.find("/anim/") != uf::stl::string::npos ) { - UF_MSG_DEBUG("Animated texture found: {}", target); - } - + } else if ( uf::image::open(image, buffer, targetPath ) ) { loaded = true; - break; + } + + if ( loaded && lowerTarget.find("/anim/") != uf::stl::string::npos ) { + UF_MSG_DEBUG("Animated texture found: {}", targetPath); } } - // did not load, fallback to missing_texture if ( !loaded ) { - UF_MSG_DEBUG("Could not load material: {}", texName); + UF_MSG_DEBUG("Could not load material: {}", matName); image.loadFromBuffer( missing_pixels, { 2, 2 }, 8, 4 ); } @@ -501,7 +945,6 @@ namespace impl { material.indexOcclusion = -1; material.indexCubemap = -1; - // to-do: fill out if ( matName.find("glass") != uf::stl::string::npos ) { material.modeAlpha = pod::Material::AlphaMode::BLEND; } @@ -521,7 +964,8 @@ namespace impl { auto& mesh = storage.meshes[meshName]; auto& primitives = storage.primitives[meshName]; - uf::stl::unordered_map meshlets; + //uf::stl::unordered_map, impl::Meshlet> meshlets; + uf::stl::unordered_map meshlets; // cell information struct ParsedCell { @@ -642,11 +1086,14 @@ namespace impl { } } - // combine lightmap into atlas - auto atlasImageID = graph.images.size(); - auto atlasTextureID = graph.textures.size(); + auto atlasImageID = -1; + auto atlasTextureID = -1; + // combine lightmap into atlas if ( !ctx.lightmapAtlas.tiles.empty() ) { + atlasImageID = graph.images.size(); + atlasTextureID = graph.textures.size(); + uf::atlas::generate(ctx.lightmapAtlas, 1); auto& imageKey = graph.images.emplace_back("lightmap_atlas"); auto& textureKey = graph.textures.emplace_back("lightmap_atlas"); @@ -683,9 +1130,13 @@ namespace impl { matName = graph.materials[materialID]; } } + if ( materialID == -1 ) { + materialID = 0; + } // prepare meshlet - auto& meshlet = meshlets[materialID]; + //auto& meshlet = meshlets[std::make_pair(cell.cellIdx, materialID)]; + auto& meshlet = meshlets[(static_cast(cell.cellIdx) << 32) | static_cast(materialID)]; meshlet.primitive.instance.materialID = materialID; meshlet.primitive.instance.lightmapID = atlasTextureID; @@ -774,25 +1225,22 @@ namespace impl { if ( meshlets.empty()) return; size_t primitiveID = 0; - for ( auto& [ matID, meshlet ] : meshlets ) { + for ( auto& [ meshletKey, meshlet ] : meshlets ) { + // auto& [ auxID, matID ] = meshletKey; + uint32_t auxID = static_cast(meshletKey >> 32); + uint32_t matID = static_cast(meshletKey & 0xFFFFFFFF); + meshlet.primitive.drawCommand.indices = meshlet.indices.size(); meshlet.primitive.drawCommand.vertices = meshlet.vertices.size(); meshlet.primitive.instance.materialID = matID; + meshlet.primitive.instance.auxID = auxID; meshlet.primitive.instance.primitiveID = primitiveID++; meshlet.primitive.instance.bounds = uf::mesh::bounds( meshlet.vertices ); uf::mesh::tangents( meshlet.vertices, meshlet.indices ); } - if ( false ) { - uf::meshgrid::Grid grid; - grid.divisions = {8, 1, 8}; - auto mlets = uf::stl::values( meshlets ); - auto partitioned = uf::meshgrid::partition( grid, mlets, EPS, true, true ); - mesh.compile( partitioned, primitives ); - } else { - mesh.compile( meshlets, primitives ); - } + mesh.compile( meshlets, primitives ); auto nodeID = graph.nodes.size(); @@ -828,7 +1276,8 @@ namespace impl { // bind position PropertyPosition position = ctx.properties.position.at(objectID); { // intentionally out of order so the struct can stay in the same order - auto facing = pod::Vector3f{ position.heading, position.bank, position.pitch } * -M_PI / 32768.0f; + // auto facing = pod::Vector3f{ position.heading, position.bank, position.pitch } * -M_PI / 32768.0f; + auto facing = pod::Vector3f{ position.facing[0], position.facing[2], position.facing[1] } * -M_PI / 32768.0f; auto qPitch = uf::quaternion::axisAngle(pod::Vector3f{1.0f, 0.0f, 0.0f}, facing.x); auto qHeading = uf::quaternion::axisAngle(pod::Vector3f{0.0f, 1.0f, 0.0f}, facing.y); @@ -847,7 +1296,7 @@ namespace impl { if ( !model.ends_with(".bin") ) model += ".bin"; - uf::stl::string path = "obj://" + model; + uf::stl::string path = "OBJ://" + model; auto it = std::find( graph.meshes.begin(), graph.meshes.end(), path ); if ( it != graph.meshes.end() ) { node.mesh = (int32_t)std::distance(graph.meshes.begin(), it); @@ -861,29 +1310,74 @@ namespace impl { } } - // bind light - PropertyLight light; - if ( ctx.findInheritedProperty( objectID, ctx.properties.light, light ) ) { - // create new node - auto lightNodeID = graph.nodes.size(); - auto& lightNode = graph.nodes.emplace_back(); - lightNode.name = ::fmt::format("{}_light", node.name); - graph.nodes[nodeID].children.emplace_back(lightNodeID); + // bind door + PropertyDoor doorProp; + bool isDoor = false; - graph.lights[::fmt::format("{}_{}", lightNode.name, lightNodeID)] = { - .range = light.radius, - .color = impl::hsvToRgb(light.hue, light.saturation, 1.0f), - .intensity = light.brightness / M_PI, - }; + if ( ctx.findInheritedProperty( objectID, ctx.properties.rotDoor, doorProp ) ) { + metadata["door"]["is_rotating"] = true; + isDoor = true; + } else if ( ctx.findInheritedProperty( objectID, ctx.properties.transDoor, doorProp ) ) { + metadata["door"]["is_rotating"] = false; + isDoor = true; + } + + if ( isDoor ) { + metadata["door"]["closed"] = doorProp.closed; + metadata["door"]["open"] = doorProp.open; + metadata["door"]["speed"] = doorProp.base_speed * impl::darkToMeters; + metadata["door"]["axis"] = doorProp.axis; + metadata["door"]["status"] = doorProp.status; + } + + // bind class tags + uf::stl::string classTags; + if ( ctx.findInheritedProperty( objectID, ctx.properties.classTag, classTags ) ) { + UF_MSG_DEBUG("objectID={}, name={}, classTags={}", objectID, node.name, classTags); + metadata["class_tags"] = classTags; } // bind ambient sound PropertyAmbient ambient; if ( ctx.findInheritedProperty( objectID, ctx.properties.ambient, ambient ) ) { - metadata["sound"]["schema"] = uf::stl::string(ambient.schemaName); - metadata["sound"]["volume"] = ambient.volume; - metadata["sound"]["radius"] = ambient.radius; + uf::stl::string schemaName(ambient.schemaName, strnlen(ambient.schemaName, 16)); + + metadata["sound"]["schema"] = schemaName; + metadata["sound"]["volume"] = ambient.override_volume; + metadata["sound"]["radius"] = ambient.radius * impl::darkToMeters; metadata["sound"]["flags"] = ambient.flags; + + uf::stl::string targetSchema = schemaName; + std::transform(targetSchema.begin(), targetSchema.end(), targetSchema.begin(), ::tolower); + + int32_t schemaObjId = -1; + for ( const auto& [id, resolved] : ctx.schemas ) { + if ( resolved.name == targetSchema ) { + schemaObjId = id; + break; + } + } + + if ( schemaObjId != -1 ) { + const auto& resolved = ctx.schemas.at(schemaObjId); + + for ( const auto& wav : resolved.wavs ) { + auto& metadataWav = metadata["sound"]["wavs"].emplace_back(); + metadataWav["uri"] = wav.name; + metadataWav["weight"] = wav.weight; + } + + if ( resolved.hasPlayParams ) { + metadata["sound"]["schema_volume"] = resolved.playParams.volume; + metadata["sound"]["play_once"] = (resolved.playParams.flags & impl::SCH_PLAY_ONCE) != 0; + metadata["sound"]["stream"] = (resolved.playParams.flags & impl::SCH_STREAM) != 0; + } + + if ( resolved.hasLoopParams ) { + metadata["sound"]["interval_min"] = resolved.loopParams.intervalMin; + metadata["sound"]["interval_max"] = resolved.loopParams.intervalMax; + } + } } // bind script @@ -892,16 +1386,98 @@ namespace impl { for ( const auto& s : scripts ) metadata["scripts"].emplace_back(s); } + // bind physics + PropertyPhysType physType; + // to-do: optimize this as it cuts my FPS by ~30% with lots of small physics objects + if ( ctx.findInheritedProperty( objectID, ctx.properties.physType, physType ) && physType.type != 3 ) { + auto& physMeta = node.metadata["physics"]; + + if ( physType.type == 0 ) { + physMeta["type"] = "obb"; + } else if ( physType.type == 1 || physType.type == 2 ) { + physMeta["type"] = "sphere"; + physMeta["radius"] = 0.5f; + } else { + physMeta["type"] = "mesh"; + } + + PropertyPhysAttr physAttr; + if ( ctx.findInheritedProperty( objectID, ctx.properties.physAttr, physAttr ) ) { + physMeta["mass"] = physAttr.mass; + physMeta["friction"] = physAttr.base_friction; + physMeta["restitution"] = physAttr.elasticity; + + if ( physAttr.gravity == 0.0f ) { + physMeta["gravity"] = uf::vector::encode(pod::Vector3f{0.0f, 0.0f, 0.0f}); + } else { + physMeta["gravity"] = uf::vector::encode(pod::Vector3f{0.0f, -0.0981f * physAttr.gravity, 0.0f}); + } + + if ( physAttr.edge_trigger != 0 ) { + physMeta["category"] = "trigger"; + physMeta["inertia"] = false; + physMeta["mass"] = 0.0f; + } + if ( physType.type == 0 ) { + physMeta["mass"] = 0.0f; + } + } + + PropertyPhysDims physDims; + if ( ctx.findInheritedProperty( objectID, ctx.properties.physDims, physDims ) ) { + if ( physType.type == 0 ) { + pod::Vector3f extent = impl::convertPos_NewDark(physDims.size) * 0.5f; + if ( extent.x > 0 && extent.y > 0 && extent.z > 0 ) { + physMeta["extent"] = uf::vector::encode(extent); + } + + pod::Vector3f center = impl::convertPos_NewDark(physDims.offset[0]); + if ( center.x != 0.f || center.y != 0.f || center.z != 0.f ) { + physMeta["center"] = uf::vector::encode(center); + } + } else if ( physType.type == 1 || physType.type == 2 ) { + if ( physDims.radius[0] > 0 ) { + physMeta["radius"] = physDims.radius[0] * impl::darkToMeters; + } + + pod::Vector3f offset = impl::convertPos_NewDark(physDims.offset[0]); + if ( offset.x != 0.f || offset.y != 0.f || offset.z != 0.f ) { + physMeta["offset"] = uf::vector::encode(offset); + } + } + } + + PropertyPhysState physState; + if ( ctx.findInheritedProperty( objectID, ctx.properties.physState, physState ) ) { + pod::Vector3f vel = impl::convertPos_NewDark(physState.velocity, 1.0f); + if ( vel.x != 0.f || vel.y != 0.f || vel.z != 0.f ) physMeta["velocity"] = uf::vector::encode(vel); + + pod::Vector3f angVel = { physState.rot_velocity.x, physState.rot_velocity.z, physState.rot_velocity.y }; + if ( angVel.x != 0.f || angVel.y != 0.f || angVel.z != 0.f ) physMeta["angularVelocity"] = uf::vector::encode(angVel); + } + } + // fill out metadata { metadata["id"] = objectID; metadata["cell"] = position.cell; + } - // to-do: deduce whether this should have physics - if ( false ) { - node.metadata["physics"]["type"] = "bounding box"; - node.metadata["physics"]["mass"] = 0; - } + // bind light (at the end because we insert a new node) + PropertyLight light; + if ( ctx.findInheritedProperty( objectID, ctx.properties.light, light ) ) { + // create new node + auto lightNodeName = ::fmt::format("{}_light", node.name); + auto lightNodeID = graph.nodes.size(); + auto& lightNode = graph.nodes.emplace_back(); + lightNode.name = lightNodeName; + graph.nodes[nodeID].children.emplace_back(lightNodeID); + + graph.lights[::fmt::format("{}_{}", lightNode.name, lightNodeID)] = { + .range = light.radius, + .color = impl::hsvToRgb(light.hue, light.saturation, 1.0f), + .intensity = light.brightness / M_PI, + }; } } } @@ -924,12 +1500,139 @@ namespace impl { auto& srcNode = graph.nodes[sourceIdx]; auto& connection = srcNode.metadata["dark"]["connections"].emplace_back(); + uf::stl::string flavor = ""; + if ( ctx.linkFlavorNames.count(link.flavor) ) { + flavor = ctx.linkFlavorNames.at(link.flavor); + } else { + flavor = ::fmt::format("Unknown_{}", link.flavor); + } + + connection["flavor"] = flavor; connection["target_node"] = graph.nodes[destIdx].name; connection["target_id"] = link.destId; - connection["flavor"] = link.flavor; + + // bind sound descriptions + if ( flavor == "SoundDescription" ) { + if ( ctx.schemas.count(link.destId) ) { + const auto& schema = ctx.schemas.at(link.destId); + for (const auto& wav : schema.wavs) { + connection["wavs"].emplace_back(wav.name); + } + } + } } } + void processSongs( pod::Graph& graph, impl::DarkContext& ctx ) { + uf::stl::vector songFiles = uf::vfs::list("SND://song/", ".snc"); + if ( songFiles.empty() ) songFiles = uf::vfs::list("SND://", ".snc"); + + auto nodeID = graph.nodes.size(); + auto& node = graph.nodes.emplace_back(); + + graph.root.children.emplace_back( nodeID ); + node.name = "Song"; + + uf::stl::string missionSong = ""; + if ( ctx.inventory.count("SONGPARAMS") > 0 ) { + const auto& item = ctx.inventory.at("SONGPARAMS"); + uint32_t offset = item.offset + sizeof(impl::DarkDBChunkHeader); + impl::PropertySongParams params; + if ( impl::readStruct( ctx.buffer, offset, params ) ) { + missionSong = uf::stl::string(params.songName, strnlen(params.songName, 32)); + } + } + node.metadata["dark"]["mission song"] = missionSong; + + auto& musicMarkersMeta = node.metadata["dark"]["music markers"]; + for ( auto& node : graph.nodes) { + auto& soundMeta = node.metadata["dark"]["sound"]; + if ( !soundMeta.isObject() ) continue; + int flags = soundMeta["flags"].as(0); + if ((flags & 16) != 0) { + auto& marker = musicMarkersMeta.emplace_back(); + + marker["position"] = uf::vector::encode(node.transform.position); + marker["radius"] = soundMeta["radius"].as(); + marker["theme"] = soundMeta["schema"].as(); + } + } + + auto& songMeta = node.metadata["dark"]["songs"]; + for ( const auto& sncPath : songFiles ) { + uf::stl::vector sncBuf; + impl::Song song; + + if ( !uf::io::readAsBuffer( sncBuf, sncPath ) || !impl::parseSong( sncBuf, song ) ) continue; + + uf::stl::string songId = sncPath; + + size_t slashPos = songId.find_last_of('/'); + if ( slashPos != uf::stl::string::npos ) songId = songId.substr(slashPos + 1); + + size_t dotPos = songId.find_last_of('.'); + if ( dotPos != uf::stl::string::npos ) songId = songId.substr(0, dotPos); + + std::transform(songId.begin(), songId.end(), songId.begin(), ::tolower); + + auto& sm = songMeta[songId]; + + for ( const auto& evt : song.globalEvents ) { + auto& em = sm["events"].emplace_back(); + em["event"] = evt.eventString; + em["flags"] = evt.flags; + for ( const auto& gt : evt.gotos ) { + auto& gm = em["gotos"].emplace_back(); + gm["section"] = gt.sectionIndex; + gm["probability"] = gt.probability; + } + } + + for ( const auto& sec : song.sections ) { + auto& secm = sm["sections"].emplace_back(); + secm["id"] = sec.id; + secm["volume"] = sec.volume; + secm["loop_count"] = sec.loopCount; + + for ( const auto& samp : sec.samples ) { + secm["samples"].emplace_back(samp.name); + } + + for ( const auto& evt : sec.events ) { + auto& em = secm["events"].emplace_back(); + em["event"] = evt.eventString; + em["flags"] = evt.flags; + for ( const auto& gt : evt.gotos ) { + auto& gm = em["gotos"].emplace_back(); + gm["section"] = gt.sectionIndex; + gm["probability"] = gt.probability; + } + } + } + } + } + + uf::stl::unordered_map parseNameMap(const uf::stl::vector& buffer, uint32_t& offset) { + uf::stl::unordered_map map; + int32_t upperBound, lowerBound, size; + + if (!impl::readStruct(buffer, offset, upperBound)) return map; + if (!impl::readStruct(buffer, offset, lowerBound)) return map; + if (!impl::readStruct(buffer, offset, size)) return map; + + for (int i = 0; i < size; ++i) { + char flag; + if (!impl::readStruct(buffer, offset, flag)) break; + + if (flag == '+') { + uf::stl::vector text; + if (!impl::readArray(buffer, offset, 16, text)) break; + map[i + lowerBound] = uf::stl::string(text.data(), strnlen(text.data(), 16)); + } + } + return map; + } + void readInventory( impl::DarkContext& ctx, const impl::DarkDBHeader& header ) { auto& buffer = ctx.buffer; auto& inventory = ctx.inventory; @@ -950,8 +1653,30 @@ namespace impl { // parse properties impl::parseProperty( ctx, "Position" ); impl::parseProperty( ctx, "Light" ); + impl::parseProperty( ctx, "TransDoor" ); + impl::parseProperty( ctx, "RotDoor" ); impl::parseProperty( ctx, "Ambient" ); + impl::parseProperty( ctx, "AmbientHacked" ); impl::parseProperty( ctx, "Scripts" ); + // physics + impl::parseProperty( ctx, "PhysType" ); + impl::parseProperty( ctx, "PhysAttr" ); + impl::parseProperty( ctx, "PhysDims" ); + impl::parseProperty( ctx, "PhysState" ); + // schemas + impl::parseProperty( ctx, "SchPlayParams" ); + impl::parseProperty( ctx, "SchLoopParams" ); + + impl::parseProperty( ctx, "Class Tag" ); + impl::parseProperty( ctx, "SchMsg" ); + impl::parseProperty( ctx, "SchAction" ); + + // parse schsamp + if ( inventory.count("SchSamp") > 0 ) { + impl::parseSchSamp( ctx, inventory["SchSamp"] ); + } + impl::parseSchemas( ctx ); + // parse links if ( inventory.count("Relations") > 0 ) { impl::parseRelations( ctx, inventory["Relations"] ); @@ -962,6 +1687,30 @@ namespace impl { } } + // parse tags + if ( inventory.count("Speech_DB") > 0 ) { + const auto& item = inventory["Speech_DB"]; + uint32_t offset = item.offset + sizeof(impl::DarkDBChunkHeader); + auto& buffer = ctx.buffer; + + auto concepts = parseNameMap(buffer, offset); + auto tags = parseNameMap(buffer, offset); + auto values = parseNameMap(buffer, offset); + + if ( inventory.count("ENV_SOUND") > 0 ) { + const auto& item = inventory["ENV_SOUND"]; + uint32_t offset = item.offset + sizeof(impl::DarkDBChunkHeader); + + int32_t numRequiredTags; + if ( impl::readStruct(buffer, offset, numRequiredTags) ) { + offset += numRequiredTags; + + parseEnvSoundTree(buffer, offset, tags, values, "", ctx); + } + } + } + + // bind hierarchy { uf::stl::unordered_map flavorCounts; @@ -1038,10 +1787,67 @@ void ext::ttlg::loadMis( pod::Graph& graph, const uf::stl::string& filename, con auto& storage = uf::graph::getStorage( graph ); // mount files - auto famMount = uf::vfs::mount(ext::zlib::createZipMount("fam://", "game://Data/res/FAM.CRF", 1000), true); - auto mshMount = uf::vfs::mount(ext::zlib::createZipMount("obj://", "game://Data/res/MESH.CRF", 1000), true); - auto bmpMount = uf::vfs::mount(ext::zlib::createZipMount("obj://", "game://Data/res/BITMAP.CRF", 1000), true); - auto objMount = uf::vfs::mount(ext::zlib::createZipMount("obj://", "game://Data/res/OBJ.CRF", 1000), true); + auto famMount = uf::vfs::mount(ext::zlib::createZipMount("FAM://", "game://Data/res/FAM.CRF", 1000), true); + auto mshMount = uf::vfs::mount(ext::zlib::createZipMount("OBJ://", "game://Data/res/MESH.CRF", 1000), true); + auto bmpMount = uf::vfs::mount(ext::zlib::createZipMount("OBJ://", "game://Data/res/BITMAP.CRF", 1000), true); + auto objMount = uf::vfs::mount(ext::zlib::createZipMount("OBJ://", "game://Data/res/OBJ.CRF", 1000), true); + + auto sndMount = uf::vfs::mount(ext::zlib::createZipMount("SND://", "game://Data/res/SND.CRF", 1000)); // not temp + auto snd2Mount = uf::vfs::mount(ext::zlib::createZipMount("SND://", "game://Data/res/SND2.CRF", 1000)); // not temp + auto songMount = uf::vfs::mount(ext::zlib::createZipMount("SND://", "game://Data/res/SONG.CRF", 1000)); // not temp + + // + { + auto wavs = uf::vfs::list( "SND://", ".wav", true ); + uf::stl::vector imgExts = { ".png", ".dds", ".tga", ".pcx", ".gif", ".bmp" }; + uf::stl::vector texMounts = { "FAM://", "OBJ://" }; + + for ( const auto& path : wavs ) { + uf::stl::string filename = path; + + size_t slashPos = filename.find_last_of('/'); + if (slashPos != uf::stl::string::npos) filename = filename.substr(slashPos + 1); + + std::transform(filename.begin(), filename.end(), filename.begin(), ::tolower); + + uf::stl::string absolutePath = path; + if ( !absolutePath.starts_with("SND://") && !absolutePath.starts_with("snd://") ) { + if ( absolutePath.starts_with("/") ) absolutePath = "SND:/" + absolutePath; + else absolutePath = "SND://" + absolutePath; + } + + ctx.fileDatabase[filename] = absolutePath; + } + + + for ( const auto& mount : texMounts ) { + for ( const auto& ext : imgExts ) { + auto files = uf::vfs::list(mount, ext, true); + for ( const auto& path : files ) { + uf::stl::string lowerPath = path; + std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::tolower); + + uf::stl::string filename = lowerPath; + size_t slashPos = filename.find_last_of('/'); + if (slashPos != uf::stl::string::npos) filename = filename.substr(slashPos + 1); + uf::stl::string baseName = filename.substr(0, filename.find_last_of('.')); + + if (ctx.fileDatabase.count(baseName) == 0) { + ctx.fileDatabase[baseName] = path; + } + + if (lowerPath.starts_with("fam://")) { + size_t famEnd = lowerPath.find('/', 6); + if (famEnd != uf::stl::string::npos) { + uf::stl::string family = lowerPath.substr(6, famEnd - 6); + ctx.fileDatabase[family + "/" + baseName] = path; + } + } + } + } + } + } + // load initial data impl::loadGam( ctx, /*metadata["game"].as*/("game://Data/SHOCK2.GAM") ); // to-do: deduce path from metadata impl::readInventory( ctx, header ); @@ -1059,6 +1865,24 @@ void ext::ttlg::loadMis( pod::Graph& graph, const uf::stl::string& filename, con impl::loadMaterials( graph, ctx ); } impl::processLinks( graph, ctx ); + impl::processSongs( graph, ctx ); + + // needs a home + auto& schemaDbMeta = graph.metadata["dark"]["schema_db"]; + for ( const auto& [id, schema] : ctx.schemas ) { + if ( schema.wavs.empty() ) continue; + + auto& entry = schemaDbMeta.emplace_back(); + entry["name"] = schema.name; + entry["tags"] = schema.classTag; + entry["msg"] = schema.msg; + entry["action"] = schema.action; + + for (const auto& wav : schema.wavs) { + entry["wavs"].emplace_back(wav.name); + } + } + // disable postprocessing flags if ( filename.starts_with("game://") ) graph.metadata["exporter"]["enabled"] = false; // disable exporting if loaded from a VPK graph.metadata["exporter"]["unwrap"] = false; // do not unwrap UVs for baking (we already have those) diff --git a/engine/src/ext/ttlg/pcx.cpp b/engine/src/ext/ttlg/pcx.cpp index 207932a7..afd9e9fd 100644 --- a/engine/src/ext/ttlg/pcx.cpp +++ b/engine/src/ext/ttlg/pcx.cpp @@ -6,7 +6,7 @@ bool ext::ttlg::loadPalette( const uf::stl::string& family, uf::stl::vector 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 list( pod::Mount& mount, const uf::stl::string& dir, const uf::stl::string& extension = "", bool recursive = false ) { + uf::stl::vector results; + auto& state = uf::pointeredUserdata::get( 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(); mount.exists = ::exists; mount.size = ::size; + mount.list = ::list; mount.mtime = ::mtime; mount.read = ::read; mount.readRange = ::readRange; diff --git a/engine/src/ext/vulkan/rendermodes/deferred.cpp b/engine/src/ext/vulkan/rendermodes/deferred.cpp index a6bd9e88..ce0bb22f 100644 --- a/engine/src/ext/vulkan/rendermodes/deferred.cpp +++ b/engine/src/ext/vulkan/rendermodes/deferred.cpp @@ -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 diff --git a/engine/src/ext/zlib/zlib.cpp b/engine/src/ext/zlib/zlib.cpp index e8dfe527..1d451bb4 100644 --- a/engine/src/ext/zlib/zlib.cpp +++ b/engine/src/ext/zlib/zlib.cpp @@ -351,6 +351,25 @@ namespace { // ID for lz4 return false; }; + uf::stl::vector vfs_list( pod::Mount& mount, const uf::stl::string& dir, const uf::stl::string& extension = "", bool recursive = false ) { + uf::stl::vector files; + auto& state = uf::pointeredUserdata::get( 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& 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( 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; } diff --git a/engine/src/utils/audio/audio.cpp b/engine/src/utils/audio/audio.cpp index ddf55c37..013e310f 100644 --- a/engine/src/utils/audio/audio.cpp +++ b/engine/src/utils/audio/audio.cpp @@ -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 ); } diff --git a/engine/src/utils/audio/emitter.cpp b/engine/src/utils/audio/emitter.cpp index 670a5932..76e6637c 100644 --- a/engine/src/utils/audio/emitter.cpp +++ b/engine/src/utils/audio/emitter.cpp @@ -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 { diff --git a/engine/src/utils/io/file.cpp b/engine/src/utils/io/file.cpp index 5140f110..1bc704ad 100644 --- a/engine/src/utils/io/file.cpp +++ b/engine/src/utils/io/file.cpp @@ -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::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) ); } diff --git a/engine/src/utils/io/vfs.cpp b/engine/src/utils/io/vfs.cpp index eb6aab4a..46a37f10 100644 --- a/engine/src/utils/io/vfs.cpp +++ b/engine/src/utils/io/vfs.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace { @@ -54,6 +55,38 @@ namespace { return size; } + uf::stl::vector vfs_list( pod::Mount& mount, const uf::stl::string& dir, const uf::stl::string& extension = "", bool recursive = false ) { + uf::stl::vector 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::vfs::list( const uf::stl::string& path, const uf::stl::string& extension, bool recursive ) { + uf::stl::vector 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 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 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 fullBuffer; if ( !mount.read( mount, relative, fullBuffer ) ) continue;