blob: 1dffff6cbefc814fe8f9aafe034e408dfdad89f6 [file]
#include "rive/animation/listener_invocation.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/core/binary_reader.hpp"
#include "rive/input/focus_manager.hpp"
#include "rive/input/gamepad_batch.hpp"
#include "rive/input/gamepad_snapshot.hpp"
#include "rive/input/standard_gamepad.hpp"
#include "rive/scripted/scripted_drawable.hpp"
#include "rive/span.hpp"
namespace rive
{
static void recomputeButtonMaskFromValues(GamepadSnapshot& s)
{
s.buttonMask = 0;
for (size_t i = 0; i < s.buttonValues.size(); i++)
{
if (s.buttonValues[i] >= 0.5f)
{
s.buttonMask |= (uint64_t(1) << i);
}
}
}
static void fillStandardIntent(const GamepadSnapshot& snap,
GamepadEventInvocation& ev)
{
ev.hasStandardButtonIntent = false;
ev.hasStandardAxisIntent = false;
if (snap.mapping != GamepadMappingKind::standard)
{
return;
}
if (ev.change.kind == GamepadInputChangeKind::button)
{
if (ev.change.index <=
static_cast<uint8_t>(StandardGamepadButton::start))
{
ev.hasStandardButtonIntent = true;
ev.standardButton =
static_cast<StandardGamepadButton>(ev.change.index);
}
}
else
{
if (ev.change.index <=
static_cast<uint8_t>(StandardGamepadAxis::rightTrigger))
{
ev.hasStandardAxisIntent = true;
ev.standardAxis = static_cast<StandardGamepadAxis>(ev.change.index);
}
}
}
static bool readConnectedPayload(BinaryReader& reader,
GamepadSnapshot& outSnapshot)
{
outSnapshot = GamepadSnapshot{};
outSnapshot.deviceId = static_cast<int32_t>(reader.readUint32());
const uint8_t mappingByte = reader.readByte();
const uint8_t nButtons = reader.readByte();
const uint8_t nAxes = reader.readByte();
reader.readByte(); // 1-byte padding to align the float arrays.
if (reader.hasError() || nButtons > kGamepadBatchMaxButtons ||
nAxes > kGamepadBatchMaxAxes)
{
return false;
}
outSnapshot.mapping = mappingByte == 0 ? GamepadMappingKind::standard
: GamepadMappingKind::unknown;
outSnapshot.buttonValues.resize(nButtons);
outSnapshot.axes.resize(nAxes);
for (uint8_t b = 0; b < nButtons; b++)
{
outSnapshot.buttonValues[b] = reader.readFloat32();
}
for (uint8_t a = 0; a < nAxes; a++)
{
outSnapshot.axes[a] = reader.readFloat32();
}
if (reader.hasError())
{
return false;
}
// buttonMask is not on the wire — derive it from the per-button values.
recomputeButtonMaskFromValues(outSnapshot);
return true;
}
static bool applyUpdateChange(GamepadSnapshot& snap,
const GamepadInputChange& ch)
{
if (ch.kind == GamepadInputChangeKind::button)
{
if (ch.index >= kGamepadBatchMaxButtons)
{
return false;
}
if (snap.buttonValues.size() <= ch.index)
{
snap.buttonValues.resize(ch.index + 1, 0.f);
}
snap.buttonValues[ch.index] = ch.value;
recomputeButtonMaskFromValues(snap);
}
else
{
if (ch.index >= kGamepadBatchMaxAxes)
{
return false;
}
if (snap.axes.size() <= ch.index)
{
snap.axes.resize(ch.index + 1, 0.f);
}
snap.axes[ch.index] = ch.value;
}
return true;
}
static GamepadEventInvocation makeEventInvocation(
const GamepadSnapshot& afterApply,
const GamepadInputChange& ch)
{
GamepadEventInvocation ev;
ev.fullState = afterApply;
ev.change = ch;
fillStandardIntent(afterApply, ev);
return ev;
}
// Decode a little-endian binary batch of gamepad events produced by the
// embedder (e.g. the JS runtime — see registerGamepadInteractions.ts) and
// dispatch them through the focus manager.
//
// Wire format (all multi-byte values little-endian, fixed-width — no LEB128):
// uint32 version -- must equal kGamepadBatchWireVersion
// then a stream of records, each starting with a 1-byte GamepadRecordType:
//
// connected (0):
// int32 deviceId
// uint8 mapping (0 = standard, else unknown)
// uint8 nButtons (<= kGamepadBatchMaxButtons)
// uint8 nAxes (<= kGamepadBatchMaxAxes)
// uint8 padding (alignment filler)
// float32 buttonValues[nButtons]
// float32 axes[nAxes]
//
// update (1):
// int32 deviceId
// uint8 nChanges
// repeated nChanges times:
// uint8 kind (0 = button, 1 = axis)
// uint8 index
// float32 value
//
// disconnected (2):
// int32 deviceId
//
// Returns false (and stops dispatching) on any malformed record: short read,
// wrong version, unknown device on update, out-of-range button/axis index,
// or unknown record type. A truncated batch leaves any records dispatched so
// far in place.
bool StateMachineInstance::submitGamepadsFromBuffer(const uint8_t* data,
size_t n)
{
if (data == nullptr)
{
return false;
}
BinaryReader reader(Span<const uint8_t>(data, n));
const uint32_t version = reader.readUint32();
if (reader.hasError() || version != kGamepadBatchWireVersion)
{
return false;
}
FocusManager* fm = focusManager();
if (fm == nullptr)
{
return false;
}
// reachedEnd() returns true once the cursor hits end-of-buffer OR the
// reader has flagged an overflow, so any partial-record read inside the
// loop will both `return false` and naturally terminate iteration.
while (!reader.reachedEnd())
{
const uint8_t typeByte = reader.readByte();
if (reader.hasError())
{
return false;
}
const auto rec = static_cast<GamepadRecordType>(typeByte);
switch (rec)
{
case GamepadRecordType::connected:
{
GamepadSnapshot snap;
if (!readConnectedPayload(reader, snap))
{
return false;
}
m_embedderGamepads[snap.deviceId] = snap;
{
auto invocation =
ListenerInvocation::gamepadConnected(snap);
ScriptedDrawable* dispatched = nullptr;
(void)fm->gamepadDispatch(invocation, &dispatched);
broadcastGamepadToScriptedDrawables(invocation, dispatched);
}
break;
}
case GamepadRecordType::update:
{
const int32_t deviceId =
static_cast<int32_t>(reader.readUint32());
const uint8_t nChanges = reader.readByte();
if (reader.hasError())
{
return false;
}
// Updates must target a gamepad we've previously seen
// connected; otherwise we have no snapshot to mutate.
auto it = m_embedderGamepads.find(deviceId);
if (it == m_embedderGamepads.end())
{
return false;
}
std::vector<GamepadInputChange> changes;
changes.reserve(nChanges);
for (uint8_t i = 0; i < nChanges; i++)
{
const uint8_t chKindB = reader.readByte();
const uint8_t chIndex = reader.readByte();
const float fval = reader.readFloat32();
if (reader.hasError())
{
return false;
}
GamepadInputChange ch;
ch.kind = chKindB == 0 ? GamepadInputChangeKind::button
: GamepadInputChangeKind::axis;
ch.index = chIndex;
ch.value = fval;
changes.push_back(ch);
}
// Apply all changes to the stored snapshot first so that every
// dispatched event sees the same fully-updated `fullState`...
GamepadSnapshot& snap = it->second;
for (const auto& ch : changes)
{
if (!applyUpdateChange(snap, ch))
{
return false;
}
}
// ...then dispatch one event per change carrying that final
// state plus the specific change that occurred.
const GamepadSnapshot finalSnap = snap;
for (const auto& ch : changes)
{
GamepadEventInvocation evi =
makeEventInvocation(finalSnap, ch);
auto invocation =
ListenerInvocation::gamepadEvent(std::move(evi));
ScriptedDrawable* dispatched = nullptr;
(void)fm->gamepadDispatch(invocation, &dispatched);
broadcastGamepadToScriptedDrawables(invocation, dispatched);
}
break;
}
case GamepadRecordType::disconnected:
{
const int32_t deviceId =
static_cast<int32_t>(reader.readUint32());
if (reader.hasError())
{
return false;
}
m_embedderGamepads.erase(deviceId);
{
auto invocation =
ListenerInvocation::gamepadDisconnected(deviceId);
ScriptedDrawable* dispatched = nullptr;
(void)fm->gamepadDispatch(invocation, &dispatched);
broadcastGamepadToScriptedDrawables(invocation, dispatched);
}
break;
}
default:
// Unknown record type — wire format mismatch, bail out.
return false;
}
}
return true;
}
HitResult StateMachineInstance::broadcastGamepadToScriptedDrawables(
const ListenerInvocation& invocation,
ScriptedDrawable* alreadyDispatched)
{
bool hitSomething = false;
bool hitOpaque = false;
// Walk m_hitComponents purely for the nested-propagation overrides
// (HitNestedArtboard / HitArtboardComponentList recurse into their
// child SMIs). HitScriptedDrawable's gamepad path is intentionally a
// no-op — direct dispatch happens via m_gamepadScriptedDrawables below
// so scripts without pointer handlers are still reached.
for (const auto& hitShape : m_hitComponents)
{
HitResult hitResult =
hitShape->processGamepadInvocation(invocation, alreadyDispatched);
if (hitResult != HitResult::none)
{
hitSomething = true;
if (hitResult == HitResult::hitOpaque)
{
hitOpaque = true;
}
}
}
#ifdef WITH_RIVE_SCRIPTING
for (ScriptedDrawable* drawable : m_gamepadScriptedDrawables)
{
if (drawable == nullptr || drawable == alreadyDispatched)
{
continue;
}
switch (invocation.kind())
{
case ListenerInvocationKind::gamepadConnected:
if (!drawable->wantsGamePadConnect())
{
continue;
}
break;
case ListenerInvocationKind::gamepadEvent:
if (!drawable->wantsGamePadEvent())
{
continue;
}
break;
case ListenerInvocationKind::gamepadDisconnected:
if (!drawable->wantsGamePadDisconnect())
{
continue;
}
break;
default:
continue;
}
if (drawable->gamepadDispatch(invocation))
{
hitSomething = true;
}
}
#endif
return hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit
: HitResult::none;
}
} // namespace rive