newdark: code cleanup (with uf::stl::reader), squashed bugs with ambience (to-do: fix attenuation), more I/O (buttons actually work, most things sans keycard readers emit the right sound, elevator lifts mostly work), added mask filtering for raycast to avoid hitting triggers, to-do: add a crosshair and draw a square around entity looking at, fix SIMD normalize being dreadfully slow

This commit is contained in:
ecker 2026-07-20 00:06:17 -05:00
parent 7bd018dae0
commit 988a61de7a
25 changed files with 1385 additions and 614 deletions

View File

@ -1,3 +1,4 @@
local ent = ent
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local soundMeta = darkMeta["sound"] or {}
@ -7,10 +8,16 @@ 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 environmental = (math.floor(flags / 1) % 2) ~= 0
local isTurnedOff = (math.floor(flags / 4) % 2) ~= 0
local isMusic = (math.floor(flags / 16) % 2) ~= 0
if isMusic then return end
if schemaName == "" then return end
local isTurnedOff = (math.floor(flags / 4) % 2) ~= 0
local startOn = not isTurnedOff
local isPlaying = false
local function playSound()
@ -42,7 +49,7 @@ local function playSound()
local schemaVol = tonumber(soundMeta["schema_volume"]) or 0
local overrideVol = tonumber(soundMeta["volume"]) or 0
local baseVolume = 1.0
local baseVolume = 0.2
if overrideVol ~= 0 then baseVolume = math.pow(10, overrideVol / 2000.0) end
local finalVolume = baseVolume * math.pow(10, schemaVol / 2000.0)
@ -57,6 +64,7 @@ local function playSound()
if radius > 0 and not environmental then
payload.maxDistance = radius
payload.referenceDistance = 1.0
payload.rolloffFactor = 1.0
end
@ -71,7 +79,7 @@ local function stopSound()
})
end
ent:addHook("dark:Message.%UID%", function(payload)
ent:addHook("link:Message.%UID%", function(payload)
local msg = payload.message
if msg == "TurnOn" then

View File

@ -6,7 +6,7 @@ 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 schemaDb = darkMeta["schema_db"] or {}
local classTags = doorMeta["class_tags"] or (darkMeta["class_tags"] or "")
local state = 0
@ -192,7 +192,7 @@ ent:bind("tick", function(self)
end
end)
ent:addHook("dark:Message.%UID%", function(payload)
ent:addHook("link:Message.%UID%", function(payload)
local msg = payload.message
if msg == "TurnOn" then

View File

@ -0,0 +1,107 @@
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 speed = 2.0
local state = 0
local currentTargetPos = nil
local activeWaypointDarkID = nil
local function findInitialWaypoint()
local conns = darkMeta["connections"] or {}
for _, conn in ipairs(conns) do
if conn.flavor == "TPathInit" or conn.flavor == "TPathNext" or conn.flavor == "TPath" then
return conn.target_id
end
end
return nil
end
local currentWaypointID = findInitialWaypoint()
local function getWaypointPos(darkID)
if not darkID then return nil end
local targetUID = _G.DarkTargets[darkID]
if targetUID then
local wpEnt = entities.get(targetUID)
if wpEnt then
return wpEnt:getComponent("Transform").position, targetUID
end
end
return nil
end
local function advanceWaypoint(darkID)
local targetUID = _G.DarkTargets[darkID]
if targetUID then
local wpEnt = entities.get(targetUID)
if wpEnt then
local wpMeta = wpEnt:getComponent("Metadata")["dark"] or {}
local conns = wpMeta["connections"] or {}
for _, conn in ipairs(conns) do
if conn.flavor == "TPathNext" or conn.flavor == "TPath" then
return conn.target_id
end
end
end
end
return nil
end
ent:bind("tick", function(self)
if state == 1 and currentTargetPos then
local myPos = transform.position
local dir = currentTargetPos - myPos
local dist = dir:norm()
local step = time.delta() * speed
if dist <= step then
if physicsBody:initialized() then
physicsBody:setVelocity(Vector3f(0, 0, 0))
end
transform.position = currentTargetPos
state = 0
local nextWp = advanceWaypoint(activeWaypointDarkID)
if nextWp then
currentWaypointID = nextWp
end
else
local vel = dir:normalize() * speed
if physicsBody:initialized() then
physicsBody:setVelocity(vel)
else
transform.position = myPos + (vel * time.delta())
end
end
end
end)
ent:addHook("link:Message.%UID%", function(payload)
local msg = payload.message
if (msg == "TurnOn" or msg == "TurnOff") and state == 0 then
local targetPos, tUid = getWaypointPos(currentWaypointID)
if targetPos and (transform.position - targetPos):norm() < 0.1 then
local nextWp = advanceWaypoint(currentWaypointID)
if nextWp then
currentWaypointID = nextWp
targetPos, tUid = getWaypointPos(currentWaypointID)
end
end
if targetPos then
currentTargetPos = targetPos
activeWaypointDarkID = currentWaypointID
state = 1
end
end
end)

View File

@ -1,33 +1,165 @@
local ent = ent
local scene = entities.currentScene()
local metadata = ent:getComponent("Metadata")
local FLAVOR_CONTROL_DEVICE = 1
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local schemaDb = darkMeta["schema_db"] or {}
local classTags = darkMeta["class_tags"] or ""
_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)
_G.DarkScripts = _G.DarkScripts or {}
_G.DarkScripts["RelayTrap"] = function(entity, payload, dMeta)
local msg = payload.message
local eName = entity or entity:uid()
--print(string.format("%s (RelayTrap) is forwarding %s down its links...", eName, msg))
entity:callHook("link:Broadcast.%UID%", {
message = msg,
flavors = { "ControlDevice", "SwitchLink" },
caller = entity:uid(),
callerDarkID = payload.callerDarkID
})
end
_G.DarkScripts["ElevatorButton"] = _G.DarkScripts["RelayTrap"]
_G.DarkScripts["RequireAllTrap"] = function(entity, payload, dMeta)
local msg = payload.message
local callerDarkID = payload.callerDarkID
_G.RAT_States = _G.RAT_States or {}
local uid = entity:uid()
_G.RAT_States[uid] = _G.RAT_States[uid] or { inputs = {}, wasOn = false }
local state = _G.RAT_States[uid]
if callerDarkID then
state.inputs[callerDarkID] = (msg == "TurnOn")
end
local allOn = true
local incoming = dMeta["incoming_connections"] or {}
for i = 1, #incoming do
local conn = incoming[i]
if conn.flavor == "ControlDevice" or conn.flavor == "SwitchLink" then
if not state.inputs[conn.source_id] then
allOn = false
break
end
end
end
if allOn and not state.wasOn then
state.wasOn = true
--print(entity .. " (RequireAllTrap) conditions met! Broadcasting TurnOn.")
entity:callHook("link:Broadcast.%UID%", { message = "TurnOn", flavors = { "ControlDevice", "SwitchLink" }, callerDarkID = dMeta["id"], caller = uid })
elseif not allOn and state.wasOn then
state.wasOn = false
--print(entity .. " (RequireAllTrap) condition lost! Broadcasting TurnOff.")
entity:callHook("link:Broadcast.%UID%", { message = "TurnOff", flavors = { "ControlDevice", "SwitchLink" }, callerDarkID = dMeta["id"], caller = uid })
end
end
--[[
_G.DarkScripts["BaseElevator"] = function(entity, payload, dMeta)
local msg = payload.message
local eName = entity or entity:uid()
print(string.format("%s (Elevator) received command: %s", eName, msg))
if msg == "TurnOn" or msg == "TurnOff" then
print(string.format("--> Commanding Lift '%s' to move!", eName))
end
end
_G.DarkScripts["Elevator"] = _G.DarkScripts["BaseElevator"]
]]
ent:addHook("link:Message.%UID%", function(payload)
local msg = payload.message
local caller = payload.caller
local eName = ent:name() or ent:uid()
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))
--print(string.format("Entity %s (Dark ID: %s) received message: %s from UID: %s", eName, darkMeta["id"] or "Unknown", msg, tostring(caller)))
local scripts = darkMeta["scripts"] or {}
local scriptHandled = false
for _, script in ipairs(scripts) do
if _G.DarkScripts[script] then
_G.DarkScripts[script](ent, payload, darkMeta)
scriptHandled = true
end
end
if not scriptHandled then
if eName:lower():match("filter") or eName:lower():match("trigger") then
_G.DarkScripts["RelayTrap"](ent, payload, darkMeta)
end
end
end)
local connections = metadata["connections"]
if not connections then return end
local function playButtonSound()
if not classTags or classTags == "" then return end
local myDeviceType = string.match(classTags, "DeviceType%s+([^%s,]+)")
ent:addHook("dark:Broadcast.%UID%", function(payload)
local bestMatch = nil
local bestScore = -1
for _, schema in ipairs(schemaDb) do
local sTags = schema.tags or ""
local score = 0
if myDeviceType then
local paddedTags = sTags .. ","
if string.find(paddedTags, "DeviceType%s+" .. myDeviceType .. "[,%s]") then
score = score + 10
elseif string.find(paddedTags, "DeviceType%s+Gen[,%s]") then
score = score + 5
elseif string.find(sTags, "DeviceType") then
score = score - 10
end
if score > 0 then
if string.find(sTags, "Reject") then
score = score + 5
elseif string.find(sTags, "StateChange") or string.find(sTags, "Activate") then
score = score + 5
end
end
end
if score > bestScore then
bestScore = score
bestMatch = schema
end
end
print("Button sound resolution:", ent, classTags, myDeviceType, bestMatch and bestMatch.name or "None", bestScore)
if bestMatch and bestScore > 0 and bestMatch.wavs and #bestMatch.wavs > 0 then
local pick = bestMatch.wavs[math.random(#bestMatch.wavs)]
local resolvedUrl = string.resolveURI(pick, metadata["system"]["root"])
ent:callHook("sound:Emit.%UID%", {
filename = resolvedUrl, spatial = true, volume = 1.0, maxDistance = 15.0, rolloffFactor = 1.0, unique = true
})
end
end
local connections = darkMeta["connections"]
ent:addHook("link:Broadcast.%UID%", function(payload)
local msg = payload.message
local validFlavors = payload.flavors or { "ControlDevice", "SwitchLink" }
if not connections then
return
end
for i = 1, #connections do
local conn = connections[i]
local isValid = false
@ -37,12 +169,15 @@ ent:addHook("dark:Broadcast.%UID%", function(payload)
if isValid then
local targetUID = _G.DarkTargets[conn.target_id]
--print(string.format("Broadcasting %s to Dark ID %s (UID: %s) via %s", msg, conn.target_id, tostring(targetUID), conn.flavor))
if targetUID then
local targetEnt = entities.get(targetUID)
if targetEnt and targetEnt:uid() then
targetEnt:callHook("dark:Message." .. targetUID, {
targetEnt:callHook("link:Message." .. targetUID, {
message = msg,
caller = ent:uid()
caller = ent:uid(),
callerDarkID = darkMeta["id"] or payload.callerDarkID
})
end
end
@ -50,13 +185,39 @@ ent:addHook("dark:Broadcast.%UID%", function(payload)
end
end)
if darkMeta["frob"] ~= nil then
ent:addHook("entity:Use.%UID%", function(payload)
local frobWorld = darkMeta["frob"]["world"] or 0
local kFrobMove = 1 -- 1 << 0
local kFrobScript = 2 -- 1 << 1
if bit.band(frobWorld, kFrobScript) ~= 0 then
print(ent, json.encode( darkMeta ))
playButtonSound()
ent:callHook("link:Broadcast.%UID%", {
message = "TurnOn",
flavors = { "ControlDevice", "SwitchLink" },
caller = payload.user,
callerDarkID = darkMeta["id"]
})
end
if bit.band(frobWorld, kFrobMove) ~= 0 then
print("Item picked up!")
-- ent:callHook("inventory:Pickup.%UID%", { user = payload.user })
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" })
if _G.RAT_States and _G.RAT_States[ent:uid()] then
_G.RAT_States[ent:uid()] = nil
end
end)

View File

@ -1,3 +1,4 @@
local ent = ent
local scene = entities.currentScene()
local metadata = ent:getComponent("Metadata")
local activeSongId = metadata["dark"]["mission song"] or "song02"
@ -13,12 +14,9 @@ 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]),
@ -27,7 +25,6 @@ for _, markerData in ipairs(bakedMarkers) do
})
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
@ -83,19 +80,15 @@ local function playSection(sectionIdx, isQueue)
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
@ -141,16 +134,12 @@ local function processEvent(eventName)
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

View File

@ -1,26 +1,56 @@
local ent = ent
local scene = entities.currentScene()
local metadata = ent:getComponent("Metadata")
local darkMeta = metadata["dark"] or {}
local soundMeta = darkMeta["sound"] or {}
local schemaDb = darkMeta["schema_db"] or {}
local connections = darkMeta["connections"] or {}
ent:addHook("dark:Message.%UID%", function(payload)
local isPlaying = false
local nativeSchema = soundMeta["schema"] or ""
ent:addHook("link:Message.%UID%", function(payload)
local msg = payload.message
if msg == "TurnOn" then
if msg == "TurnOn" and not isPlaying then
local urlToPlay = ""
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
})
urlToPlay = conn.wavs[math.random(#conn.wavs)]
break
end
end
if urlToPlay == "" and nativeSchema ~= "" then
for _, entry in ipairs(schemaDb) do
if entry.name:lower() == nativeSchema:lower() then
if entry.wavs and #entry.wavs > 0 then
urlToPlay = entry.wavs[math.random(#entry.wavs)]
end
break
end
end
end
if urlToPlay ~= "" then
local resolvedUrl = string.resolveURI(urlToPlay, metadata["system"]["root"])
isPlaying = true
ent:callHook("sound:Emit.%UID%", {
filename = resolvedUrl,
spatial = false,
streamed = true,
volume = 1.0,
unique = true,
loop = false
})
end
elseif msg == "TurnOff" and isPlaying then
isPlaying = false
ent:callHook("sound:Stop.%UID%", {})
end
end)

View File

@ -24,7 +24,7 @@ ent:bind( "tick", function(self)
if not touching[uid] then
touching[uid] = true
ent:queueHook("dark:Broadcast.%UID%", { message = "TurnOn" }, 0)
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOn" }, 0)
end
end
end
@ -32,7 +32,7 @@ ent:bind( "tick", function(self)
for uid, _ in pairs(touching) do
if not currentCollisions[uid] then
touching[uid] = nil
ent:queueHook("dark:Broadcast.%UID%", { message = "TurnOff" }, 0)
ent:queueHook("link:Broadcast.%UID%", { message = "TurnOff" }, 0)
end
end
end )

View File

@ -236,6 +236,8 @@ end
ent:addHook( "entity:Use.%UID%", function( payload )
if payload.user ~= ent:uid() then return end
print( "Use:", entities.get( payload.uid ) )
local validUse = false
if heldObject.uid == 0 and payload.depth > 0 then
local prop = entities.get( payload.uid )

View File

@ -8,30 +8,6 @@ namespace impl {
const float darkToMeters = 0.7f;
typedef uf::Meshlet_T<uf::graph::mesh::Skinned, uint32_t> Meshlet;
// could be dedicated functions in the engine
template<typename T>
inline bool readStruct(const uf::stl::vector<uint8_t>& buffer, uint32_t& offset, T& outValue, 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;
}
template<typename T>
inline bool readArray(const uf::stl::vector<uint8_t>& buffer, uint32_t& offset, size_t count, uf::stl::vector<T>& outArray) {
size_t bytes = count * sizeof(T);
if (offset + bytes > buffer.size()) return false;
outArray.resize(count);
std::memcpy(outArray.data(), buffer.data() + offset, bytes);
offset += bytes;
return true;
}
inline float findWrap( float x ) {
return -64.0f * std::floor(x / 64.0f);
};

View File

@ -70,6 +70,8 @@ namespace uf {
void UF_API rolloff( pod::AudioSource& source, float v );
float UF_API maxDistance( const pod::AudioSource& source );
void UF_API maxDistance( pod::AudioSource& source, float v );
float UF_API referenceDistance( const pod::AudioSource& source );
void UF_API referenceDistance( pod::AudioSource& source, float v );
float UF_API distance( const pod::Vector3f& position );
float UF_API occlusion( const pod::Vector3f& position );

View File

@ -19,9 +19,9 @@ namespace impl {
namespace uf {
namespace physics {
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::PhysicsBody&, float = FLT_MAX );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::World&, float = FLT_MAX );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::World&, const pod::PhysicsBody*, float = FLT_MAX );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::PhysicsBody&, float = FLT_MAX, uint32_t mask = pod::Collider::MASK_PHYSICAL );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::World&, float = FLT_MAX, uint32_t mask = pod::Collider::MASK_PHYSICAL );
pod::RayQuery UF_API rayCast( const pod::Ray&, const pod::World&, const pod::PhysicsBody*, float = FLT_MAX, uint32_t mask = pod::Collider::MASK_PHYSICAL );
float UF_API occlusion( const pod::Vector3f& to, const pod::Vector3f& from );
uf::stl::string UF_API getRayMaterialName( const pod::RayQuery& query );

View File

@ -318,24 +318,27 @@ namespace pod {
CATEGORY_NONE = 0,
CATEGORY_STATIC = 1 << 0,
CATEGORY_DYNAMIC = 1 << 1,
CATEGORY_PLAYER = 1 << 2,
CATEGORY_PLAYER = 1 << 2,
CATEGORY_NPC = 1 << 3,
CATEGORY_TRIGGER = 1 << 4,
CATEGORY_PROJECTILE = 1 << 5,
CATEGORY_INTERACTABLE = 1 << 6,
CATEGORY_CHARACTER = CATEGORY_PLAYER | CATEGORY_NPC,
CATEGORY_ALL = 0xFFFFFFFF
};
// what it collides with
enum CollisionMask : uint32_t {
MASK_NONE = 0,
MASK_NONE = 0,
MASK_STATIC = CATEGORY_DYNAMIC | CATEGORY_PLAYER | CATEGORY_NPC | CATEGORY_PROJECTILE,
MASK_DYNAMIC = CATEGORY_STATIC | CATEGORY_DYNAMIC | CATEGORY_PLAYER | CATEGORY_NPC | CATEGORY_TRIGGER,
MASK_PLAYER = CATEGORY_STATIC | CATEGORY_DYNAMIC | CATEGORY_NPC | CATEGORY_PROJECTILE | CATEGORY_TRIGGER,
MASK_DYNAMIC = CATEGORY_STATIC | CATEGORY_DYNAMIC | CATEGORY_PLAYER | CATEGORY_NPC | CATEGORY_TRIGGER | CATEGORY_INTERACTABLE,
MASK_PLAYER = CATEGORY_STATIC | CATEGORY_DYNAMIC | CATEGORY_NPC | CATEGORY_PROJECTILE | CATEGORY_TRIGGER | CATEGORY_INTERACTABLE,
MASK_NPC = CATEGORY_STATIC | CATEGORY_DYNAMIC | CATEGORY_PLAYER | CATEGORY_PROJECTILE | CATEGORY_TRIGGER,
MASK_TRIGGER = CATEGORY_PLAYER | CATEGORY_NPC,
MASK_PROJECTILE = CATEGORY_STATIC | CATEGORY_DYNAMIC | CATEGORY_PLAYER | CATEGORY_NPC,
MASK_CHARACTER = MASK_PLAYER | MASK_NPC,
MASK_ALL = 0xFFFFFFFF
MASK_ALL = 0xFFFFFFFF,
MASK_PHYSICAL = MASK_ALL & ~CATEGORY_TRIGGER,
};
pod::ShapeType type;

View File

@ -104,6 +104,7 @@ namespace uf {
template<typename T, size_t N> /*FORCE_INLINE*/ pod::Vector<T, N> /*UF_API*/ copy( const pod::Vector<T, N>& = {}); // creates a copy of a vector (for whatever reason)
template<typename T, size_t N> /*FORCE_INLINE*/ pod::Vector<T, N> /*UF_API*/ copy( const T* ); // creates a copy of a vector (for whatever reason)
template<typename T, size_t N, typename U> /*FORCE_INLINE*/ pod::Vector<T, N> /*UF_API*/ cast( const U& from ); // casts one vector of one type to another (of the same size)
template<typename T, size_t N> /*FORCE_INLINE*/ pod::Vector<T, N> /*UF_API*/ fill( const T ); //
// Equality checking
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ equals( const T& left, const T& right ); // equality check between two vectors (==)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ notEquals( const T& left, const T& right ); // equality check between two vectors (==)
@ -112,6 +113,14 @@ namespace uf {
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ greater( const T& left, const T& right ); // equality check between two vectors (>)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ greaterEquals( const T& left, const T& right ); // equality check between two vectors (>=)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ equals( const T& left, const typename T::type_t right ); // equality check between a vector's components (==)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ notEquals( const T& left, const typename T::type_t right ); // equality check between a vector's components (==)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ less( const T& left, const typename T::type_t right ); // equality check between a vector's components (<)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ lessEquals( const T& left, const typename T::type_t right ); // equality check between a vector's components (<=)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ greater( const T& left, const typename T::type_t right ); // equality check between a vector's components (>)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ greaterEquals( const T& left, const typename T::type_t right ); // equality check between a vector's components (>=)
template<typename T> /*FORCE_INLINE*/ bool /*UF_API*/ isValid( const T& v ); // checks if all components are valid (non NaN, inf, etc.)
// Basic arithmetic
template<typename T> /*FORCE_INLINE*/ T /*UF_API*/ add( const T& left, const T& right ); // adds two vectors of same type and size together
@ -220,6 +229,12 @@ namespace uf {
FORCE_INLINE bool operator<=(const Vector<T,N>& rhs) const { return uf::vector::lessEquals(*this, rhs); } \
FORCE_INLINE bool operator>(const Vector<T,N>& rhs) const { return uf::vector::greater(*this, rhs); } \
FORCE_INLINE bool operator>=(const Vector<T,N>& rhs) const { return uf::vector::greaterEquals(*this, rhs); } \
FORCE_INLINE bool operator==(const T rhs) const { return uf::vector::equals(*this, rhs); } \
FORCE_INLINE bool operator!=(const T rhs) const { return uf::vector::notEquals(*this, rhs); } \
FORCE_INLINE bool operator<(const T rhs) const { return uf::vector::less(*this, rhs); } \
FORCE_INLINE bool operator<=(const T rhs) const { return uf::vector::lessEquals(*this, rhs); } \
FORCE_INLINE bool operator>(const T rhs) const { return uf::vector::greater(*this, rhs); } \
FORCE_INLINE bool operator>=(const T rhs) const { return uf::vector::greaterEquals(*this, rhs); } \
FORCE_INLINE explicit operator bool() const { return uf::vector::notEquals(*this, Vector<T,N>{}); } \
template<typename U, size_t M> FORCE_INLINE operator Vector<U,M>() const { return uf::vector::cast<U,M>(*this); }

View File

@ -56,6 +56,14 @@ pod::Vector<T, N> uf::vector::cast( const U& from ) {
to[i] = from[i];
return to;
}
template<typename T, size_t N>
pod::Vector<T, N> uf::vector::fill( const T v ) {
pod::Vector<T, N> res;
FOR_EACH(T::size, {
res[i] = v;
});
return res;
}
template<typename T>
bool uf::vector::equals( const T& left, const T& right ) {
#if UF_USE_SIMD
@ -134,6 +142,86 @@ bool uf::vector::greaterEquals( const T& left, const T& right ) {
});
return result;
}
//
template<typename T>
bool uf::vector::equals( const T& left, const typename T::type_t right ) {
#if UF_USE_SIMD
if constexpr ( simd_able_v<typename T::type_t> && T::size == 4 ) {
return uf::simd::all( uf::simd::equals( left, right ) );
}
#endif
bool result = true;
FOR_EACH(T::size, {
if ( !(left[i] == right) ) result = false;
});
return result;
}
template<typename T>
bool uf::vector::notEquals( const T& left, const typename T::type_t right ) {
#if UF_USE_SIMD
if constexpr ( simd_able_v<typename T::type_t> && T::size == 4 ) {
return uf::simd::all( uf::simd::notEquals( left, right ) );
}
#endif
bool result = true;
FOR_EACH(T::size, {
if ( !(left[i] != right) ) result = false;
});
return result;
}
template<typename T>
bool uf::vector::less( const T& left, const typename T::type_t right ) {
#if UF_USE_SIMD
if constexpr ( simd_able_v<typename T::type_t> && T::size == 4 ) {
return uf::simd::all( uf::simd::less( left, right ) );
}
#endif
bool result = true;
FOR_EACH(T::size, {
if ( !(left[i] < right) ) result = false;
});
return result;
}
template<typename T>
bool uf::vector::lessEquals( const T& left, const typename T::type_t right ) {
#if UF_USE_SIMD
if constexpr ( simd_able_v<typename T::type_t> && T::size == 4 ) {
return uf::simd::all( uf::simd::lessEquals( left, right ) );
}
#endif
bool result = true;
FOR_EACH(T::size, {
if ( !(left[i] <= right) ) result = false;
});
return result;
}
template<typename T>
bool uf::vector::greater( const T& left, const typename T::type_t right ) {
#if UF_USE_SIMD
if constexpr ( simd_able_v<typename T::type_t> && T::size == 4 ) {
return uf::simd::all( uf::simd::greater( left, right ) );
}
#endif
bool result = true;
FOR_EACH(T::size, {
if ( !(left[i] > right) ) result = false;
});
return result;
}
template<typename T>
bool uf::vector::greaterEquals( const T& left, const typename T::type_t right ) {
#if UF_USE_SIMD
if constexpr ( simd_able_v<typename T::type_t> && T::size == 4 ) {
return uf::simd::all( uf::simd::greaterEquals( left, right ) );
}
#endif
bool result = true;
FOR_EACH(T::size, {
if ( !(left[i] >= right) ) result = false;
});
return result;
}
//
template<typename T>
bool uf::vector::isValid( const T& v ) {
return uf::vector::equals( v, v );
@ -274,7 +362,7 @@ T uf::vector::divide( const T& vector, typename T::type_t scalar ) {
}
#endif
T res;
float recip = static_cast<typename T::type_t>(1) / scalar;
float recip = 1.0f / static_cast<float>(scalar);
FOR_EACH(T::size, {
res[i] = vector[i] * recip;
});
@ -505,10 +593,10 @@ T uf::vector::min( const T& left, const T& right ) {
return res;
}
template<typename T>
T uf::vector::min( const T& vector, typename T::type_t min ) {
T uf::vector::min( const T& vector, typename T::type_t right ) {
T res = vector;
FOR_EACH(T::size, {
res[i] = std::min( res[i], min );
res[i] = std::min( res[i], right );
});
return res;
}
@ -534,10 +622,10 @@ T uf::vector::max( const T& left, const T& right ) {
return res;
}
template<typename T>
T uf::vector::max( const T& vector, typename T::type_t max ) {
T uf::vector::max( const T& vector, typename T::type_t right ) {
T res = vector;
FOR_EACH(T::size, {
res[i] = std::max( res[i], max );
res[i] = std::max( res[i], right );
});
return res;
}
@ -728,8 +816,8 @@ typename T::type_t uf::vector::norm( const T& vector ) {
}
template<typename T>
T uf::vector::normalize( const T& vector ) {
#if UF_USE_SIMD
if constexpr ( std::is_same_v<T,float> ) {
#if 0 && UF_USE_SIMD // causes massive frame drops
if constexpr ( std::is_same_v<typename T::type_t, float> ) {
return uf::simd::normalize( vector );
}
#endif
@ -745,13 +833,12 @@ T uf::vector::normalize( const T& vector ) {
template<typename T>
T uf::vector::clampMagnitude( const T& v, float maxMag ) {
T res = v;
float mag = uf::vector::magnitude( res );
if ( mag > maxMag ) {
res /= (maxMag / sqrt(mag));
}
return res;
T res = v;
float magSq = uf::vector::magnitude( res );
if ( magSq > (maxMag * maxMag) ) {
res *= (maxMag / sqrt(magSq));
}
return res;
}
template<typename T>

View File

@ -0,0 +1,81 @@
#pragma once
#include "vector.h"
namespace uf {
namespace stl {
// technically NOT a templated class, but it works with templates
class UF_API reader {
private:
const uf::stl::vector<uint8_t>& m_buffer;
uint32_t m_offset;
uint32_t m_endOffset;
bool m_zeroCopy; // if true, returns a pointer to the original buffer in memory
public:
reader( const uf::stl::vector<uint8_t>& buffer, uint32_t offset, uint32_t length, bool zeroCopy = true );
inline bool eof() const { return m_offset >= m_endOffset; }
inline uint32_t offset() const { return m_offset; }
inline uint32_t remaining() const { return m_endOffset - m_offset; }
inline void skip( size_t bytes ) { m_offset += bytes; }
template<typename T>
const T* read( size_t readSize = sizeof(T) ) {
if ( m_offset + readSize > m_endOffset ) return nullptr;
if ( !m_zeroCopy ) {
static thread_local T copy;
memset(&copy, 0, sizeof(T));
size_t copySize = std::min(sizeof(T), readSize);
memcpy(&copy, m_buffer.data() + m_offset, copySize);
m_offset += readSize;
return &copy;
}
const T* ptr = (const T*)(m_buffer.data() + m_offset);
m_offset += readSize;
return ptr;
}
template<typename T>
bool read( size_t count, uf::stl::vector<T>& outArray ) {
size_t bytes = count * sizeof(T);
if ( m_offset + bytes > m_endOffset ) {
bytes = m_endOffset - m_offset;
count = bytes / sizeof(T);
if (count == 0) return false;
bytes = count * sizeof(T);
}
if ( !m_zeroCopy ) {
outArray.resize(count);
memcpy(outArray.data(), m_buffer.data() + m_offset, bytes);
} else {
outArray.assign(
(const T*)(m_buffer.data() + m_offset),
(const T*)(m_buffer.data() + m_offset + bytes)
);
}
m_offset += bytes;
return true;
}
// to-do: #if C++ >= 20 or something
template<typename T>
std::span<const T> view( size_t count ) {
size_t bytes = count * sizeof(T);
if (m_offset + bytes > m_endOffset) return {};
std::span<const T> view((const T*)(m_buffer.data() + m_offset), count);
m_offset += bytes;
return view;
}
template<typename T>
const T* peek() const {
if ( m_offset + sizeof(T) > m_endOffset ) return nullptr;
return (const T*)(m_buffer.data() + m_offset);
}
};
}
}
template<> const char* uf::stl::reader::read<char>( size_t readSize );

View File

@ -44,6 +44,7 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
if ( ext::json::isNull(json["gain"]) ) json["gain"] = metadataJson["audio"]["gain"];
if ( ext::json::isNull(json["rolloffFactor"]) ) json["rolloffFactor"] = metadataJson["audio"]["rolloffFactor"];
if ( ext::json::isNull(json["maxDistance"]) ) json["maxDistance"] = metadataJson["audio"]["maxDistance"];
if ( ext::json::isNull(json["referenceDistance"]) ) json["referenceDistance"] = metadataJson["audio"]["referenceDistance"];
if ( ext::json::isNull(json["epsilon"]) ) json["epsilon"] = metadataJson["audio"]["epsilon"];
if ( ext::json::isNull(json["loop"]) ) json["loop"] = metadataJson["audio"]["loop"];
if ( ext::json::isNull(json["streamed"]) ) json["streamed"] = metadataJson["audio"]["streamed"];
@ -68,6 +69,7 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
if ( json["gain"].is<double>() ) uf::audio::gain(source, json["gain"].as<float>());
if ( json["rolloffFactor"].is<double>() ) uf::audio::rolloff(source, json["rolloffFactor"].as<float>());
if ( json["maxDistance"].is<double>() ) uf::audio::maxDistance(source, json["maxDistance"].as<float>());
if ( json["referenceDistance"].is<double>() ) uf::audio::referenceDistance(source, json["referenceDistance"].as<float>());
if ( json["spatial"].is<bool>() ) source.settings.spatial = json["spatial"].as<bool>();
if ( json["loop"].is<bool>() ) uf::audio::loop(source, json["loop"].as<bool>());
@ -97,7 +99,6 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
uf::stl::string filename = payload["filename"].as<uf::stl::string>();
int layer = payload["layer"].as<int>(1);
uf::stl::string channelName = "managed_bgm_channel_" + std::to_string(layer);
UF_MSG_DEBUG("filename={}, channelName={}", filename, channelName);
if ( emitter.has( channelName ) ) {
auto& source = emitter.get(channelName);
@ -159,8 +160,6 @@ void ext::AudioEmitterBehavior::initialize( uf::Object& self ) {
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;
@ -259,7 +258,6 @@ void ext::AudioEmitterBehavior::tick( uf::Object& self ) {
if ( metadata.tracks.count( metadata.current ) > 0 ) {
auto& track = metadata.tracks[metadata.current];
if ( track.active && !bgmFound ) {
UF_MSG_DEBUG("BGM source vanished! Firing TrackEnded natively.");
track.active = false;
bool isIntro = metadata.current == track.intro;

View File

@ -823,6 +823,7 @@ void uf::graph::process( pod::Graph& graph ) {
auto& sceneMetadataJson = scene.getComponent<uf::Serializer>();
auto& graphMetadataJson = graph.metadata;
auto& graphMetadataValve = graphMetadataJson["valve"];
auto& graphMetadataDark = graphMetadataJson["dark"];
auto& storage = uf::graph::getStorage( graph );
std::lock_guard<std::mutex> lock(*storage.mutex);
@ -1436,6 +1437,22 @@ 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 ) ) {
bool emitsAudio = false;
// bind elevator
bool isElevator = false;
if ( ext::json::isArray( metadataDark["scripts"] ) ) {
ext::json::forEach( metadataDark["scripts"], [&]( ext::json::Value& value ){
auto script = value.as<uf::stl::string>();
if ( script == "BaseElevator" || script == "Elevator" ) isElevator = true;
});
}
if ( metadataDark["class_tags"].is<uf::stl::string>() ) {
emitsAudio = true;
} else if ( ext::json::isObject( metadataDark["sound"] ) ) {
emitsAudio = true;
}
// bind io connectivity
if ( ext::json::isArray( metadataDark["connections"] ) || metadataDark["id"].is<int>() ) {
@ -1446,27 +1463,28 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
// bind door
if ( ext::json::isObject( metadataDark["door"] ) ) {
loadJson["assets"].emplace_back("ent://scripts/dark/door.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
emitsAudio = true;
}
node.metadata["dark"]["schema_db"] = graph.metadata["dark"]["schema_db"];
if ( isElevator ) {
loadJson["assets"].emplace_back("ent://scripts/dark/elevator.lua");
emitsAudio = true;
}
// 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");
emitsAudio = true;
}
// bind trap
if ( node.name.find("SoundTrap") != uf::stl::string::npos || node.name.find("Votrap") != uf::stl::string::npos ) {
if ( node.name == "Sound Trap" || node.name == "VO Trap" ) {
loadJson["assets"].emplace_back("ent://scripts/dark/trap.lua");
loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
emitsAudio = true;
// bind ambient
} else if ( ext::json::isObject( metadataDark["sound"] ) ) {
loadJson["assets"].emplace_back("ent://scripts/dark/ambient.lua");
emitsAudio = true;
}
// bind trigger
@ -1474,6 +1492,55 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
if ( ext::json::isObject( physMeta ) && physMeta["category"].as<uf::stl::string>("") == "trigger" ) {
loadJson["assets"].emplace_back("ent://scripts/dark/trigger.lua");
}
// bind schema based on what we need
uf::stl::string classTags = metadataDark["class_tags"].as<uf::stl::string>("");
uf::stl::string explicitSchema = "";
if ( metadataDark["sound"].isObject() ) {
explicitSchema = metadataDark["sound"]["schema"].as<uf::stl::string>("");
}
if ( !classTags.empty() || !explicitSchema.empty() ) {
auto& localDb = node.metadata["dark"]["schema_db"];
uf::stl::vector<uf::stl::string> searchTerms;
auto extractTerm = [&](const uf::stl::string& prefix) {
size_t p = classTags.find(prefix);
if ( p != uf::stl::string::npos ) {
p += prefix.length();
size_t end = classTags.find_first_of(" ,", p);
if ( end == uf::stl::string::npos ) end = classTags.length();
uf::stl::string term = classTags.substr(p, end - p);
if ( !term.empty() ) searchTerms.push_back(term);
}
};
extractTerm("DeviceType ");
extractTerm("DoorType ");
ext::json::forEach( graph.metadata["dark"]["schema_db"], [&]( ext::json::Value& sch ){
uf::stl::string sName = sch["name"].as<uf::stl::string>("");
uf::stl::string sTags = sch["tags"].as<uf::stl::string>("");
bool keep = false;
if ( !explicitSchema.empty() && sName == explicitSchema ) {
keep = true;
} else {
for ( const auto& term : searchTerms ) {
if ( sTags.find(term) != uf::stl::string::npos ) {
keep = true;
break;
}
}
}
if ( keep ) {
localDb.emplace_back(sch);
}
});
}
if ( emitsAudio ) loadJson["behaviors"].emplace_back("AudioEmitterBehavior");
}
if ( ext::json::isObject( tag ) ) {
@ -1579,7 +1646,9 @@ void uf::graph::process( pod::Graph& graph, int32_t index, uf::Object& parent )
auto& transform = entity.getComponent<pod::Transform<>>();
{
transform = node.transform;
transform.reference = &parent.getComponent<pod::Transform<>>();
if ( node.metadata["debug"]["parent node transforms"].as<bool>(true) ) {
transform.reference = &parent.getComponent<pod::Transform<>>();
}
// override transform
if ( tag["transform"]["offset"].as<bool>() ) {
auto parsed = uf::transform::decode( tag["transform"], pod::Transform<>{} );

View File

@ -282,11 +282,18 @@ namespace binds {
void ext::lua::initialize() {
if ( !ext::lua::enabled ) return;
state.open_libraries(
sol::lib::base
,sol::lib::package
,sol::lib::table
,sol::lib::math
,sol::lib::string
,sol::lib::bit32
#if UF_USE_LUAJIT
state.open_libraries(sol::lib::base, sol::lib::package, sol::lib::table, sol::lib::math, sol::lib::string, sol::lib::ffi, sol::lib::jit);
#else
state.open_libraries(sol::lib::base, sol::lib::package, sol::lib::table, sol::lib::math, sol::lib::string);
,sol::lib::ffi
,sol::lib::jit
#endif
);
// load modules
for ( auto pair : modules ) {

View File

@ -44,8 +44,8 @@ namespace binds {
return sol::as_table( events );
}
std::tuple<uf::Object*, float> rayCast( pod::PhysicsBody& self, const pod::Vector3f& center, const pod::Vector3f& direction ) {
pod::RayQuery query = uf::physics::rayCast( pod::Ray{center, uf::vector::normalize( direction )}, self, uf::vector::norm( direction ) );
std::tuple<uf::Object*, float> rayCast( pod::PhysicsBody& self, const pod::Vector3f& center, const pod::Vector3f& direction, sol::optional<uint32_t> mask ) {
pod::RayQuery query = uf::physics::rayCast( pod::Ray{center, uf::vector::normalize( direction )}, self, uf::vector::norm( direction ), mask.value_or(pod::Collider::MASK_PHYSICAL) );
uf::Object* object = query.hit ? query.body->object : NULL;
float depth = query.hit ? query.contact.penetration : -1;
return std::make_tuple( object, depth );

View File

@ -2,7 +2,7 @@
#include <uf/ext/ttlg/common.h>
#include <uf/ext/valve/common.h>
#include <uf/ext/zlib/zlib.h>
#include <cstring>
#include <uf/utils/memory/reader.h>
namespace impl {
#pragma pack(push, 1)
@ -137,13 +137,14 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
return false;
}
uint32_t offset = 0;
uf::stl::reader reader(buffer, 0, buffer.size());
impl::BinMainHeader mainHeader;
if ( !impl::readStruct( buffer, offset, mainHeader ) ) {
const auto* pMainHeader = reader.read<impl::BinMainHeader>();
if ( !pMainHeader ) {
UF_MSG_ERROR("Failed to read BIN header: {}", filename);
return false;
}
impl::BinMainHeader mainHeader = *pMainHeader;
if ( strncmp(mainHeader.magic, "LGMM", 4) == 0 ) {
UF_MSG_ERROR("Attempting to read LGMM file as an LMGD: {}", filename);
@ -155,14 +156,15 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
return false;
}
impl::BinHeader header;
if ( !impl::readStruct(buffer, offset, header ) ) {
const auto* pHeader = reader.read<impl::BinHeader>();
if ( !pHeader ) {
UF_MSG_ERROR("Failed to parse BIN header: {}", filename);
return false;
}
impl::BinHeader header = *pHeader;
// skip additional information (to-do: parse)
if ( mainHeader.version == 4 ) offset += 12;
if ( mainHeader.version == 4 ) reader.skip(12);
if ( header.offset_poly_list == 0 || header.offset_poly_list >= buffer.size() ) {
UF_MSG_ERROR("Invalid BIN file (poly list offset out of bounds): {}", filename);
@ -184,10 +186,10 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
// read materials
if ( header.num_mats > 0 && header.offset_mats > 0 && header.offset_mats < buffer.size() ) {
uint32_t matOffset = header.offset_mats;
uf::stl::reader matReader(buffer, header.offset_mats, buffer.size() - header.offset_mats);
uf::stl::vector<impl::BinMaterial> binMats;
if ( impl::readArray(buffer, matOffset, header.num_mats, binMats ) ) {
if ( matReader.read(header.num_mats, binMats ) ) {
for ( uint32_t i = 0; i < header.num_mats; ++i ) {
uf::stl::string matName = uf::stl::string(binMats[i].name);
if ( matName.empty() ) {
@ -200,7 +202,6 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
if ( dotPos != uf::stl::string::npos ) matName = matName.substr(0, dotPos);
}
auto it = std::find(graph.materials.begin(), graph.materials.end(), matName);
int32_t matIndex = (mainHeader.version == 4) ? i : binMats[i].slot_num;
@ -217,29 +218,29 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
// read vertices
if ( header.num_verts > 0 && header.offset_verts > 0 && header.offset_verts < buffer.size() ) {
uint32_t offset = header.offset_verts;
impl::readArray( buffer, offset, header.num_verts, vertices );
uf::stl::reader vertReader(buffer, header.offset_verts, buffer.size() - header.offset_verts);
vertReader.read(header.num_verts, vertices);
}
// read UVs
if ( header.offset_uv > 0 && header.offset_uv < buffer.size() ) {
numUVs = (buffer.size() - header.offset_uv) / sizeof(impl::BinUV);
}
if ( numUVs > 0 ) {
uint32_t offset = header.offset_uv;
impl::readArray( buffer, offset, numUVs, uvs );
uf::stl::reader uvReader(buffer, header.offset_uv, buffer.size() - header.offset_uv);
uvReader.read(numUVs, uvs);
}
// read normals
if ( header.offset_norms > 0 && header.offset_norms < buffer.size() ) {
numNormals = (buffer.size() - header.offset_norms) / sizeof(pod::Vector3f);
}
if ( numNormals > 0 ) {
uint32_t offset = header.offset_norms;
impl::readArray( buffer, offset, numNormals, normals );
uf::stl::reader normReader(buffer, header.offset_norms, buffer.size() - header.offset_norms);
normReader.read(numNormals, normals);
}
// read subobject information
if ( header.num_objs > 0 && header.offset_objs > 0 && header.offset_objs < buffer.size() ) {
uint32_t offset = header.offset_objs;
impl::readArray( buffer, offset, header.num_objs, subObjects );
uf::stl::reader objReader(buffer, header.offset_objs, buffer.size() - header.offset_objs);
objReader.read(header.num_objs, subObjects);
}
uf::stl::vector<pod::Matrix4f> transforms( subObjects.size(), uf::matrix::identity() );
@ -250,43 +251,59 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
// read subobject faces
if ( header.offset_nodes > 0 && header.offset_nodes < buffer.size() ) {
uint32_t offset = header.offset_nodes;
uf::stl::reader nodeReader(buffer, header.offset_nodes, buffer.size() - header.offset_nodes);
uf::stl::vector<uint16_t> faces;
while ( offset < buffer.size() ) {
while ( !nodeReader.eof() ) {
faces.clear();
uint8_t nodeType = buffer[offset];
const auto* pNodeType = nodeReader.peek<uint8_t>();
if (!pNodeType) break;
uint8_t nodeType = *pNodeType;
if ( nodeType == 4 ) {
subObjectPolys.emplace_back();
offset += 3;
nodeReader.skip(3);
} else if ( nodeType == 3 ) {
offset += 19;
nodeReader.skip(19);
} else if ( nodeType == 2 ) {
uint16_t nf1{}, nf2{};
offset += 17;
impl::readStruct( buffer, offset, nf1 );
offset += 2;
impl::readStruct( buffer, offset, nf2 );
nodeReader.skip(17);
if ( impl::readArray( buffer, offset, nf1 + nf2, faces ) && !subObjectPolys.empty() ) {
const auto* pNf1 = nodeReader.read<uint16_t>();
if (!pNf1) break;
uint16_t nf1 = *pNf1;
nodeReader.skip(2);
const auto* pNf2 = nodeReader.read<uint16_t>();
if (!pNf2) break;
uint16_t nf2 = *pNf2;
if ( nodeReader.read(nf1 + nf2, faces) && !subObjectPolys.empty() ) {
subObjectPolys.back().insert( subObjectPolys.back().end(), faces.begin(), faces.end() );
}
} else if (nodeType == 1) {
uint16_t nf1{}, nf2{};
offset += 17;
impl::readStruct( buffer, offset, nf1 );
offset += 10;
impl::readStruct( buffer, offset, nf2 );
nodeReader.skip(17);
if ( impl::readArray( buffer, offset, nf1 + nf2, faces ) && !subObjectPolys.empty() ) {
const auto* pNf1 = nodeReader.read<uint16_t>();
if (!pNf1) break;
uint16_t nf1 = *pNf1;
nodeReader.skip(10);
const auto* pNf2 = nodeReader.read<uint16_t>();
if (!pNf2) break;
uint16_t nf2 = *pNf2;
if ( nodeReader.read(nf1 + nf2, faces) && !subObjectPolys.empty() ) {
subObjectPolys.back().insert( subObjectPolys.back().end(), faces.begin(), faces.end() );
}
} else if (nodeType == 0) {
uint16_t nf{};
offset += 17;
impl::readStruct( buffer, offset, nf );
nodeReader.skip(17);
if ( impl::readArray(buffer, offset, nf, faces) && !subObjectPolys.empty() ) {
const auto* pNf = nodeReader.read<uint16_t>();
if (!pNf) break;
uint16_t nf = *pNf;
if ( nodeReader.read(nf, faces) && !subObjectPolys.empty() ) {
subObjectPolys.back().insert(subObjectPolys.back().end(), faces.begin(), faces.end());
}
} else {
@ -308,27 +325,26 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
uint32_t absolutePolyOffset = header.offset_poly_list + polyOffset;
if (absolutePolyOffset >= buffer.size()) continue;
impl::BinPolyHeader polyHeader;
uint32_t readOffset = absolutePolyOffset;
if (!impl::readStruct(buffer, readOffset, polyHeader)) continue;
uf::stl::reader polyReader(buffer, absolutePolyOffset, buffer.size() - absolutePolyOffset);
uint32_t dataOffset = absolutePolyOffset + 12;
const auto* pPolyHeader = polyReader.read<impl::BinPolyHeader>();
if (!pPolyHeader) continue;
impl::BinPolyHeader polyHeader = *pPolyHeader;
uf::stl::vector<uint16_t> vertIndices;
impl::readArray(buffer, dataOffset, polyHeader.num_verts, vertIndices);
polyReader.read(polyHeader.num_verts, vertIndices);
uf::stl::vector<uint16_t> normIndices;
impl::readArray(buffer, dataOffset, polyHeader.num_verts, normIndices);
polyReader.read(polyHeader.num_verts, normIndices);
uf::stl::vector<uint16_t> uvIndices;
bool hasUVs = ((polyHeader.type & 3) == 3);
if (hasUVs) impl::readArray(buffer, dataOffset, polyHeader.num_verts, uvIndices);
if (hasUVs) polyReader.read(polyHeader.num_verts, uvIndices);
uint32_t localMatID = 0;
if ( mainHeader.version == 4 ) {
uint8_t matByte = 0;
impl::readStruct(buffer, dataOffset, matByte);
localMatID = matByte;
const auto* matByte = polyReader.read<uint8_t>();
if (matByte) localMatID = *matByte;
} else {
localMatID = polyHeader.data;
}
@ -351,7 +367,9 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
for (uint8_t v = 0; v < polyHeader.num_verts; ++v) {
auto& vert = meshlet.vertices.emplace_back();
uint16_t vIdx = vertIndices[v];
uint16_t vIdx = 0;
if (v < vertIndices.size()) vIdx = vertIndices[v]; // Bounds safety
if ( vIdx < vertices.size() ) {
vert.position = impl::convertPos_NewDark( uf::matrix::multiply<float>(transforms[objIdx], vertices[vIdx].position, 1.0f) + offsets[objIdx] );
@ -366,10 +384,12 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
}
// triangle fan => triangles
for ( uint8_t v = 1; v < polyHeader.num_verts - 1; ++v ) {
meshlet.indices.emplace_back(startVertIdx);
meshlet.indices.emplace_back(startVertIdx + v);
meshlet.indices.emplace_back(startVertIdx + v + 1);
if (polyHeader.num_verts >= 3) {
for ( uint8_t v = 1; v < polyHeader.num_verts - 1; ++v ) {
meshlet.indices.emplace_back(startVertIdx);
meshlet.indices.emplace_back(startVertIdx + v);
meshlet.indices.emplace_back(startVertIdx + v + 1);
}
}
}
}
@ -377,12 +397,6 @@ bool ext::ttlg::loadBin( pod::Graph& graph, const uf::stl::string& filename ) {
if ( meshlets.empty() ) {
if ( header.num_vhots > 0 || header.num_objs > 0 || header.num_polys == 0 ) {
UF_MSG_DEBUG("BIN file acts as a dummy node (no polygons, but has VHOTs/Objs): {}", filename);
/*
uf::stl::string meshName = filename;
graph.meshes.emplace_back(meshName);
graph.primitives.emplace_back(meshName);
return true;
*/
} else {
UF_MSG_WARNING("BIN file contained no valid polygons: {}", filename);
}

File diff suppressed because it is too large Load Diff

View File

@ -228,6 +228,15 @@ void uf::audio::maxDistance( pod::AudioSource& source, float v ) {
source.alSource.set( AL_MAX_DISTANCE, v );
}
float uf::audio::referenceDistance( const pod::AudioSource& source ) {
float v;
source.alSource.get( AL_REFERENCE_DISTANCE, v );
return v;
}
void uf::audio::referenceDistance( pod::AudioSource& source, float v ) {
source.alSource.set( AL_REFERENCE_DISTANCE, v );
}
//
float uf::audio::distance( const pod::Vector3f& position ) {
return uf::vector::distance( ::listener.position, position );

View File

@ -323,6 +323,7 @@ void uf::physics::setColliderCategory( pod::PhysicsBody& body, const uf::stl::st
if ( c == "NPC" ) return uf::physics::setColliderCategory( body, pod::Collider::CATEGORY_NPC );
if ( c == "TRIGGER" ) return uf::physics::setColliderCategory( body, pod::Collider::CATEGORY_TRIGGER );
if ( c == "PROJECTILE" ) return uf::physics::setColliderCategory( body, pod::Collider::CATEGORY_PROJECTILE );
if ( c == "INTERACTABLE" ) return uf::physics::setColliderCategory( body, pod::Collider::CATEGORY_INTERACTABLE );
if ( c == "CHARACTER" ) return uf::physics::setColliderCategory( body, pod::Collider::CATEGORY_CHARACTER );
if ( c == "ALL" ) return uf::physics::setColliderCategory( body, pod::Collider::CATEGORY_ALL );
}
@ -339,6 +340,7 @@ void uf::physics::setColliderMask( pod::PhysicsBody& body, const uf::stl::string
if ( m == "TRIGGER" ) return uf::physics::setColliderMask( body, pod::Collider::MASK_TRIGGER );
if ( m == "PROJECTILE" ) return uf::physics::setColliderMask( body, pod::Collider::MASK_PROJECTILE );
if ( m == "CHARACTER" ) return uf::physics::setColliderMask( body, pod::Collider::MASK_CHARACTER );
if ( m == "PHYSICAL" ) return uf::physics::setColliderMask( body, pod::Collider::MASK_PHYSICAL );
if ( m == "ALL" ) return uf::physics::setColliderMask( body, pod::Collider::MASK_ALL );
}
void uf::physics::setGravity( pod::PhysicsBody& body, const pod::Vector3f& gravity ) {

View File

@ -309,13 +309,13 @@ void impl::drawRay( const pod::Ray& ray, const pod::RayQuery& query ) {
uf::debug::addLine( start, end, query.hit ? pod::Vector4f{ 0, 1, 0, 1 } : pod::Vector4f{ 1, 0, 0, 1 } );
}
pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::PhysicsBody& body, float maxDistance ) {
pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::PhysicsBody& body, float maxDistance, uint32_t mask ) {
return rayCast( ray, body.world ? *body.world : uf::physics::getWorld(), &body, maxDistance );
}
pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::World& world, float maxDistance ) {
pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::World& world, float maxDistance, uint32_t mask ) {
return rayCast( ray, world, NULL, maxDistance );
}
pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::World& world, const pod::PhysicsBody* body, float maxDistance ) {
pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::World& world, const pod::PhysicsBody* body, float maxDistance, uint32_t mask ) {
pod::RayQuery rayHit;
rayHit.invoker = body;
rayHit.contact.penetration = maxDistance;
@ -334,6 +334,8 @@ pod::RayQuery uf::physics::rayCast( const pod::Ray& ray, const pod::World& world
if ( body == b ) continue;
if ( !(b->collider.category & mask) ) continue;
switch ( b->collider.type ) {
case pod::ShapeType::AABB: impl::rayAabb( ray, *b, rayHit ); break;
case pod::ShapeType::OBB: impl::rayObb( ray, *b, rayHit ); break;
@ -361,7 +363,7 @@ float uf::physics::occlusion( const pod::Vector3f& to, const pod::Vector3f& from
ray.origin = from;
ray.direction = dir;
pod::RayQuery hit = uf::physics::rayCast( ray, uf::physics::getWorld(), dist );
pod::RayQuery hit = uf::physics::rayCast( ray, uf::physics::getWorld(), dist, pod::Collider::MASK_PHYSICAL );
if ( hit.contact.penetration >= dist ) return 1.0f;
@ -379,7 +381,7 @@ pod::AcousticBounce uf::physics::acousticReflection( const pod::Vector3f& source
primaryRay.origin = sourcePos;
primaryRay.direction = rayDirection;
pod::RayQuery firstHit = uf::physics::rayCast( primaryRay, uf::physics::getWorld(), maxDistance );
pod::RayQuery firstHit = uf::physics::rayCast( primaryRay, uf::physics::getWorld(), maxDistance, pod::Collider::MASK_PHYSICAL );
if ( firstHit.contact.penetration >= maxDistance ) return result;
@ -403,7 +405,7 @@ pod::AcousticBounce uf::physics::acousticReflection( const pod::Vector3f& source
secondaryRay.origin = bounceOrigin;
secondaryRay.direction = toListener / distToListener;
pod::RayQuery secondHit = uf::physics::rayCast( secondaryRay, uf::physics::getWorld(), distToListener );
pod::RayQuery secondHit = uf::physics::rayCast( secondaryRay, uf::physics::getWorld(), distToListener, pod::Collider::MASK_PHYSICAL );
if ( secondHit.contact.penetration >= distToListener ) {
result.valid = true;

View File

@ -0,0 +1,14 @@
#include <uf/utils/memory/reader.h>
uf::stl::reader::reader( const uf::stl::vector<uint8_t>& buffer, uint32_t offset, uint32_t length, bool zeroCopy ) : m_buffer(buffer), m_offset(offset), m_endOffset(offset + length), m_zeroCopy( zeroCopy ) {
}
template<>
const char* uf::stl::reader::read<char>( size_t readSize ) {
if ( m_offset + readSize > m_endOffset ) return nullptr;
const char* ptr = (const char*)(m_buffer.data() + m_offset);
m_offset += readSize;
return ptr;
}