mirror of
https://github.com/izzy2lost/dolphin.git
synced 2026-06-19 01:16:48 -07:00
Merge pull request #7861 from jordan-woyak/mplus-emu
WiimoteEmu: Emulated MotionPlus and improved emulated swing.
This commit is contained in:
@@ -24,6 +24,18 @@ constexpr T Clamp(const T val, const T& min, const T& max)
|
||||
return std::max(min, std::min(max, val));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto Sign(const T& val) -> decltype((T{} < val) - (val < T{}))
|
||||
{
|
||||
return (T{} < val) - (val < T{});
|
||||
}
|
||||
|
||||
template <typename T, typename F>
|
||||
constexpr auto Lerp(const T& x, const T& y, const F& a) -> decltype(x + (y - x) * a)
|
||||
{
|
||||
return x + (y - x) * a;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr bool IsPow2(T imm)
|
||||
{
|
||||
|
||||
+40
-14
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
|
||||
// Tiny matrix/vector library.
|
||||
@@ -58,6 +59,25 @@ union TVec3
|
||||
|
||||
TVec3 operator-() const { return {-x, -y, -z}; }
|
||||
|
||||
// Apply function to each element and return the result.
|
||||
template <typename F>
|
||||
auto Map(F&& f) const -> TVec3<decltype(f(T{}))>
|
||||
{
|
||||
return {f(x), f(y), f(z)};
|
||||
}
|
||||
|
||||
template <typename F, typename T2>
|
||||
auto Map(F&& f, const TVec3<T2>& t) const -> TVec3<decltype(f(T{}, t.x))>
|
||||
{
|
||||
return {f(x, t.x), f(y, t.y), f(z, t.z)};
|
||||
}
|
||||
|
||||
template <typename F, typename T2>
|
||||
auto Map(F&& f, T2 scalar) const -> TVec3<decltype(f(T{}, scalar))>
|
||||
{
|
||||
return {f(x, scalar), f(y, scalar), f(z, scalar)};
|
||||
}
|
||||
|
||||
std::array<T, 3> data = {};
|
||||
|
||||
struct
|
||||
@@ -69,39 +89,45 @@ union TVec3
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
TVec3<T> operator+(TVec3<T> lhs, const TVec3<T>& rhs)
|
||||
TVec3<bool> operator<(const TVec3<T>& lhs, const TVec3<T>& rhs)
|
||||
{
|
||||
return lhs += rhs;
|
||||
return lhs.Map(std::less<T>{}, rhs);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
TVec3<T> operator-(TVec3<T> lhs, const TVec3<T>& rhs)
|
||||
auto operator+(const TVec3<T>& lhs, const TVec3<T>& rhs) -> TVec3<decltype(lhs.x + rhs.x)>
|
||||
{
|
||||
return lhs -= rhs;
|
||||
return lhs.Map(std::plus<decltype(lhs.x + rhs.x)>{}, rhs);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
TVec3<T> operator*(TVec3<T> lhs, const TVec3<T>& rhs)
|
||||
auto operator-(const TVec3<T>& lhs, const TVec3<T>& rhs) -> TVec3<decltype(lhs.x - rhs.x)>
|
||||
{
|
||||
return lhs *= rhs;
|
||||
return lhs.Map(std::minus<decltype(lhs.x - rhs.x)>{}, rhs);
|
||||
}
|
||||
|
||||
template <typename T1, typename T2>
|
||||
auto operator*(const TVec3<T1>& lhs, const TVec3<T2>& rhs) -> TVec3<decltype(lhs.x * rhs.x)>
|
||||
{
|
||||
return lhs.Map(std::multiplies<decltype(lhs.x * rhs.x)>{}, rhs);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline TVec3<T> operator/(TVec3<T> lhs, const TVec3<T>& rhs)
|
||||
auto operator/(const TVec3<T>& lhs, const TVec3<T>& rhs) -> TVec3<decltype(lhs.x / rhs.x)>
|
||||
{
|
||||
return lhs /= rhs;
|
||||
return lhs.Map(std::divides<decltype(lhs.x / rhs.x)>{}, rhs);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
TVec3<T> operator*(TVec3<T> lhs, std::common_type_t<T> scalar)
|
||||
template <typename T1, typename T2>
|
||||
auto operator*(const TVec3<T1>& lhs, T2 scalar) -> TVec3<decltype(lhs.x * scalar)>
|
||||
{
|
||||
return lhs *= TVec3<T>{scalar, scalar, scalar};
|
||||
return lhs.Map(std::multiplies<decltype(lhs.x * scalar)>{}, scalar);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
TVec3<T> operator/(TVec3<T> lhs, std::common_type_t<T> scalar)
|
||||
template <typename T1, typename T2>
|
||||
auto operator/(const TVec3<T1>& lhs, T2 scalar) -> TVec3<decltype(lhs.x / scalar)>
|
||||
{
|
||||
return lhs /= TVec3<T>{scalar, scalar, scalar};
|
||||
return lhs.Map(std::divides<decltype(lhs.x / scalar)>{}, scalar);
|
||||
}
|
||||
|
||||
using Vec3 = TVec3<float>;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace WiimoteEmu
|
||||
void EmulateShake(PositionalState* state, ControllerEmu::Shake* const shake_group,
|
||||
float time_elapsed)
|
||||
{
|
||||
auto target_position = shake_group->GetState() * shake_group->GetIntensity() / 2;
|
||||
auto target_position = shake_group->GetState() * float(shake_group->GetIntensity() / 2);
|
||||
for (std::size_t i = 0; i != target_position.data.size(); ++i)
|
||||
{
|
||||
if (state->velocity.data[i] * std::copysign(1.f, target_position.data[i]) < 0 ||
|
||||
@@ -90,7 +90,9 @@ void EmulateTilt(RotationalState* state, ControllerEmu::Tilt* const tilt_group,
|
||||
const ControlState roll = target.x * MathUtil::PI;
|
||||
const ControlState pitch = target.y * MathUtil::PI;
|
||||
|
||||
// TODO: expose this setting in UI:
|
||||
// Higher values will be more responsive but will increase rate of M+ "desync".
|
||||
// I'd rather not expose this value in the UI if not needed.
|
||||
// Desync caused by tilt seems not as severe as accelerometer data can estimate pitch/yaw.
|
||||
constexpr auto MAX_ACCEL = float(MathUtil::TAU * 50);
|
||||
|
||||
ApproachAngleWithAccel(state, Common::Vec3(pitch, -roll, 0), MAX_ACCEL, time_elapsed);
|
||||
@@ -98,22 +100,80 @@ void EmulateTilt(RotationalState* state, ControllerEmu::Tilt* const tilt_group,
|
||||
|
||||
void EmulateSwing(MotionState* state, ControllerEmu::Force* swing_group, float time_elapsed)
|
||||
{
|
||||
const auto target = swing_group->GetState();
|
||||
const auto input_state = swing_group->GetState();
|
||||
const float max_distance = swing_group->GetMaxDistance();
|
||||
const float max_angle = swing_group->GetTwistAngle();
|
||||
|
||||
// Note. Y/Z swapped because X/Y axis to the swing_group is X/Z to the wiimote.
|
||||
// Note: Y/Z swapped because X/Y axis to the swing_group is X/Z to the wiimote.
|
||||
// X is negated because Wiimote X+ is to the left.
|
||||
ApproachPositionWithJerk(state, {-target.x, -target.z, target.y},
|
||||
Common::Vec3{1, 1, 1} * swing_group->GetMaxJerk(), time_elapsed);
|
||||
const auto target_position = Common::Vec3{-input_state.x, -input_state.z, input_state.y};
|
||||
|
||||
// Just jump to our target angle scaled by our progress to the target position.
|
||||
// TODO: If we wanted to be less hacky we could use ApproachAngleWithAccel.
|
||||
const auto angle = state->position / swing_group->GetMaxDistance() * swing_group->GetTwistAngle();
|
||||
// Jerk is scaled based on input distance from center.
|
||||
// X and Z scale is connected for sane movement about the circle.
|
||||
const auto xz_target_dist = Common::Vec2{target_position.x, target_position.z}.Length();
|
||||
const auto y_target_dist = std::abs(target_position.y);
|
||||
const auto target_dist = Common::Vec3{xz_target_dist, y_target_dist, xz_target_dist};
|
||||
const auto speed = MathUtil::Lerp(Common::Vec3{1, 1, 1} * float(swing_group->GetReturnSpeed()),
|
||||
Common::Vec3{1, 1, 1} * float(swing_group->GetSpeed()),
|
||||
target_dist / max_distance);
|
||||
|
||||
const auto old_angle = state->angle;
|
||||
state->angle = {-angle.z, 0, angle.x};
|
||||
// Convert our m/s "speed" to the jerk required to reach this speed when traveling 1 meter.
|
||||
const auto max_jerk = speed * speed * speed * 4;
|
||||
|
||||
// Update velocity based on change in angle.
|
||||
state->angular_velocity = state->angle - old_angle;
|
||||
// Rotational acceleration to approximately match the completion time of our swing.
|
||||
const auto max_accel = max_angle * speed.x * speed.x;
|
||||
|
||||
// Apply rotation based on amount of swing.
|
||||
const auto target_angle =
|
||||
Common::Vec3{-target_position.z, 0, target_position.x} / max_distance * max_angle;
|
||||
|
||||
// Angular acceleration * 2 seems to reduce "spurious stabs" in ZSS.
|
||||
// TODO: Fix properly.
|
||||
ApproachAngleWithAccel(state, target_angle, max_accel * 2, time_elapsed);
|
||||
|
||||
// Clamp X and Z rotation.
|
||||
for (const int c : {0, 2})
|
||||
{
|
||||
if (std::abs(state->angle.data[c] / max_angle) > 1 &&
|
||||
MathUtil::Sign(state->angular_velocity.data[c]) == MathUtil::Sign(state->angle.data[c]))
|
||||
{
|
||||
state->angular_velocity.data[c] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust target position backwards based on swing progress and max angle
|
||||
// to simulate a swing with an outstretched arm.
|
||||
const auto backwards_angle = std::max(std::abs(state->angle.x), std::abs(state->angle.z));
|
||||
const auto backwards_movement = (1 - std::cos(backwards_angle)) * max_distance;
|
||||
|
||||
// TODO: Backswing jerk should be based on x/z speed.
|
||||
|
||||
ApproachPositionWithJerk(state, target_position + Common::Vec3{0, backwards_movement, 0},
|
||||
max_jerk, time_elapsed);
|
||||
|
||||
// Clamp Left/Right/Up/Down movement within the configured circle.
|
||||
const auto xz_progress =
|
||||
Common::Vec2{state->position.x, state->position.z}.Length() / max_distance;
|
||||
if (xz_progress > 1)
|
||||
{
|
||||
state->position.x /= xz_progress;
|
||||
state->position.z /= xz_progress;
|
||||
|
||||
state->acceleration.x = state->acceleration.z = 0;
|
||||
state->velocity.x = state->velocity.z = 0;
|
||||
}
|
||||
|
||||
// Clamp Forward/Backward movement within the configured distance.
|
||||
// We allow additional backwards movement for the back swing.
|
||||
const auto y_progress = state->position.y / max_distance;
|
||||
const auto max_y_progress = 2 - std::cos(max_angle);
|
||||
if (y_progress > max_y_progress || y_progress < -1)
|
||||
{
|
||||
state->position.y =
|
||||
MathUtil::Clamp(state->position.y, -1.f * max_distance, max_y_progress * max_distance);
|
||||
state->velocity.y = 0;
|
||||
state->acceleration.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g,
|
||||
@@ -129,10 +189,17 @@ WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3&
|
||||
u16(MathUtil::Clamp(std::lround(scaled_accel.z + zero_g), 0l, MAX_VALUE))};
|
||||
}
|
||||
|
||||
Common::Matrix44 EmulateCursorMovement(ControllerEmu::Cursor* ir_group)
|
||||
void EmulateCursor(MotionState* state, ControllerEmu::Cursor* ir_group, float time_elapsed)
|
||||
{
|
||||
using Common::Matrix33;
|
||||
using Common::Matrix44;
|
||||
const auto cursor = ir_group->GetState(true);
|
||||
|
||||
if (!cursor.IsVisible())
|
||||
{
|
||||
// Move the wiimote a kilometer forward so the sensor bar is always behind it.
|
||||
*state = {};
|
||||
state->position = {0, -1000, 0};
|
||||
return;
|
||||
}
|
||||
|
||||
// Nintendo recommends a distance of 1-3 meters.
|
||||
constexpr float NEUTRAL_DISTANCE = 2.f;
|
||||
@@ -147,12 +214,32 @@ Common::Matrix44 EmulateCursorMovement(ControllerEmu::Cursor* ir_group)
|
||||
const float yaw_scale = ir_group->GetTotalYaw() / 2;
|
||||
const float pitch_scale = ir_group->GetTotalPitch() / 2;
|
||||
|
||||
const auto cursor = ir_group->GetState(true);
|
||||
// TODO: Move state out of ControllerEmu::Cursor
|
||||
// TODO: Use ApproachPositionWithJerk
|
||||
// TODO: Move forward/backward after rotation.
|
||||
const auto new_position =
|
||||
Common::Vec3(0, NEUTRAL_DISTANCE - MOVE_DISTANCE * float(cursor.z), -height);
|
||||
|
||||
return Matrix44::Translate({0, MOVE_DISTANCE * float(cursor.z), 0}) *
|
||||
Matrix44::FromMatrix33(Matrix33::RotateX(pitch_scale * cursor.y) *
|
||||
Matrix33::RotateZ(yaw_scale * cursor.x)) *
|
||||
Matrix44::Translate({0, -NEUTRAL_DISTANCE, height});
|
||||
const auto target_angle = Common::Vec3(pitch_scale * -cursor.y, 0, yaw_scale * -cursor.x);
|
||||
|
||||
// If cursor was hidden, jump to the target position/angle immediately.
|
||||
if (state->position.y < 0)
|
||||
{
|
||||
state->position = new_position;
|
||||
state->angle = target_angle;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state->acceleration = new_position - state->position;
|
||||
state->position = new_position;
|
||||
|
||||
// Higher values will be more responsive but increase rate of M+ "desync".
|
||||
// I'd rather not expose this value in the UI if not needed.
|
||||
// At this value, sync is very good and responsiveness still appears instant.
|
||||
constexpr auto MAX_ACCEL = float(MathUtil::TAU * 8);
|
||||
|
||||
ApproachAngleWithAccel(state, target_angle, MAX_ACCEL, time_elapsed);
|
||||
}
|
||||
|
||||
void ApproachAngleWithAccel(RotationalState* state, const Common::Vec3& angle_target,
|
||||
@@ -165,10 +252,7 @@ void ApproachAngleWithAccel(RotationalState* state, const Common::Vec3& angle_ta
|
||||
|
||||
const auto offset = angle_target - state->angle;
|
||||
const auto stop_offset = offset - stop_distance;
|
||||
|
||||
const Common::Vec3 accel{std::copysign(max_accel, stop_offset.x),
|
||||
std::copysign(max_accel, stop_offset.y),
|
||||
std::copysign(max_accel, stop_offset.z)};
|
||||
const auto accel = MathUtil::Sign(stop_offset) * max_accel;
|
||||
|
||||
state->angular_velocity += accel * time_elapsed;
|
||||
|
||||
@@ -177,11 +261,11 @@ void ApproachAngleWithAccel(RotationalState* state, const Common::Vec3& angle_ta
|
||||
|
||||
for (std::size_t i = 0; i != offset.data.size(); ++i)
|
||||
{
|
||||
// If new velocity will overshoot assume we would have stopped right on target.
|
||||
// TODO: Improve check to see if less accel would have caused undershoot.
|
||||
if ((change_in_angle.data[i] / offset.data[i]) > 1.0)
|
||||
// If new angle will overshoot stop right on target.
|
||||
if (std::abs(offset.data[i]) < 0.0001 || (change_in_angle.data[i] / offset.data[i] > 1.0))
|
||||
{
|
||||
state->angular_velocity.data[i] = 0;
|
||||
state->angular_velocity.data[i] =
|
||||
(angle_target.data[i] - state->angle.data[i]) / time_elapsed;
|
||||
state->angle.data[i] = angle_target.data[i];
|
||||
}
|
||||
else
|
||||
@@ -201,10 +285,7 @@ void ApproachPositionWithJerk(PositionalState* state, const Common::Vec3& positi
|
||||
|
||||
const auto offset = position_target - state->position;
|
||||
const auto stop_offset = offset - stop_distance;
|
||||
|
||||
const Common::Vec3 jerk{std::copysign(max_jerk.x, stop_offset.x),
|
||||
std::copysign(max_jerk.y, stop_offset.y),
|
||||
std::copysign(max_jerk.z, stop_offset.z)};
|
||||
const auto jerk = MathUtil::Sign(stop_offset) * max_jerk;
|
||||
|
||||
state->acceleration += jerk * time_elapsed;
|
||||
|
||||
|
||||
@@ -19,14 +19,19 @@ constexpr double GRAVITY_ACCELERATION = 9.80665;
|
||||
|
||||
struct PositionalState
|
||||
{
|
||||
// meters
|
||||
Common::Vec3 position;
|
||||
// meters/second
|
||||
Common::Vec3 velocity;
|
||||
// meters/second^2
|
||||
Common::Vec3 acceleration;
|
||||
};
|
||||
|
||||
struct RotationalState
|
||||
{
|
||||
// radians
|
||||
Common::Vec3 angle;
|
||||
// radians/second
|
||||
Common::Vec3 angular_velocity;
|
||||
};
|
||||
|
||||
@@ -47,11 +52,10 @@ void ApproachAngleWithAccel(RotationalState* state, const Common::Vec3& target,
|
||||
void EmulateShake(PositionalState* state, ControllerEmu::Shake* shake_group, float time_elapsed);
|
||||
void EmulateTilt(RotationalState* state, ControllerEmu::Tilt* tilt_group, float time_elapsed);
|
||||
void EmulateSwing(MotionState* state, ControllerEmu::Force* swing_group, float time_elapsed);
|
||||
void EmulateCursor(MotionState* state, ControllerEmu::Cursor* ir_group, float time_elapsed);
|
||||
|
||||
// Convert m/s/s acceleration data to the format used by Wiimote/Nunchuk (10-bit unsigned integers).
|
||||
WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g,
|
||||
u16 one_g);
|
||||
|
||||
Common::Matrix44 EmulateCursorMovement(ControllerEmu::Cursor* ir_group);
|
||||
|
||||
} // namespace WiimoteEmu
|
||||
|
||||
@@ -149,11 +149,17 @@ void Wiimote::SendAck(OutputReportID rpt_id, ErrorCode error_code)
|
||||
|
||||
void Wiimote::HandleExtensionSwap()
|
||||
{
|
||||
if (WIIMOTE_BALANCE_BOARD == m_index)
|
||||
{
|
||||
// Prevent M+ or anything else silly from being attached to a balance board.
|
||||
// In the future if we support an emulated balance board we can force the BB "extension" here.
|
||||
return;
|
||||
}
|
||||
|
||||
ExtensionNumber desired_extension_number =
|
||||
static_cast<ExtensionNumber>(m_attachments->GetSelectedAttachment());
|
||||
|
||||
// const bool desired_motion_plus = m_motion_plus_setting->GetValue();
|
||||
const bool desired_motion_plus = false;
|
||||
const bool desired_motion_plus = m_motion_plus_setting.GetValue();
|
||||
|
||||
// FYI: AttachExtension also connects devices to the i2c bus
|
||||
|
||||
@@ -283,7 +289,7 @@ void Wiimote::HandleWriteData(const OutputReportWriteData& wd)
|
||||
if (address >= 0x0FCA && address < 0x12C0)
|
||||
{
|
||||
// TODO: Only write parts of the Mii block.
|
||||
// TODO: Use fifferent files for different wiimote numbers.
|
||||
// TODO: Use different files for different wiimote numbers.
|
||||
std::ofstream file;
|
||||
File::OpenFStream(file, File::GetUserPath(D_SESSION_WIIROOT_IDX) + "/mii.bin",
|
||||
std::ios::binary | std::ios::out);
|
||||
@@ -578,12 +584,16 @@ void Wiimote::DoState(PointerWrap& p)
|
||||
(m_is_motion_plus_attached ? m_motion_plus.GetExtPort() : m_extension_port)
|
||||
.AttachExtension(GetActiveExtension());
|
||||
|
||||
m_motion_plus.DoState(p);
|
||||
GetActiveExtension()->DoState(p);
|
||||
if (m_is_motion_plus_attached)
|
||||
m_motion_plus.DoState(p);
|
||||
|
||||
if (m_active_extension != ExtensionNumber::NONE)
|
||||
GetActiveExtension()->DoState(p);
|
||||
|
||||
// Dynamics
|
||||
p.Do(m_swing_state);
|
||||
p.Do(m_tilt_state);
|
||||
p.Do(m_cursor_state);
|
||||
p.Do(m_shake_state);
|
||||
|
||||
p.DoMarker("Wiimote");
|
||||
|
||||
@@ -91,8 +91,7 @@ void Nunchuk::Update()
|
||||
EmulateTilt(&m_tilt_state, m_tilt, 1.f / ::Wiimote::UPDATE_FREQ);
|
||||
EmulateShake(&m_shake_state, m_shake, 1.f / ::Wiimote::UPDATE_FREQ);
|
||||
|
||||
const auto transformation =
|
||||
GetRotationalMatrix(-m_tilt_state.angle) * GetRotationalMatrix(-m_swing_state.angle);
|
||||
const auto transformation = GetRotationalMatrix(-m_tilt_state.angle - m_swing_state.angle);
|
||||
|
||||
Common::Vec3 accel = transformation * (m_swing_state.acceleration +
|
||||
Common::Vec3(0, 0, float(GRAVITY_ACCELERATION)));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,14 @@
|
||||
#include <array>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Core/HW/WiimoteEmu/Dynamics.h"
|
||||
#include "Core/HW/WiimoteEmu/ExtensionPort.h"
|
||||
#include "Core/HW/WiimoteEmu/I2CBus.h"
|
||||
|
||||
namespace WiimoteEmu
|
||||
{
|
||||
struct AngularVelocity;
|
||||
|
||||
struct MotionPlus : public Extension
|
||||
{
|
||||
public:
|
||||
@@ -23,7 +26,37 @@ public:
|
||||
|
||||
ExtensionPort& GetExtPort();
|
||||
|
||||
// Vec3 is interpreted as radians/s about the x,y,z axes following the "right-hand rule".
|
||||
void PrepareInput(const Common::Vec3& angular_velocity);
|
||||
|
||||
private:
|
||||
enum class ChallengeState : u8
|
||||
{
|
||||
// Note: This is not a value seen on a real M+.
|
||||
// Used to emulate activation state during which the M+ is not responsive.
|
||||
Activating = 0x00,
|
||||
|
||||
PreparingX = 0x02,
|
||||
ParameterXReady = 0x0e,
|
||||
PreparingY = 0x14,
|
||||
ParameterYReady = 0x1a,
|
||||
};
|
||||
|
||||
enum class PassthroughMode : u8
|
||||
{
|
||||
Disabled = 0x04,
|
||||
Nunchuk = 0x05,
|
||||
Classic = 0x07,
|
||||
};
|
||||
|
||||
enum class ActivationStatus
|
||||
{
|
||||
Inactive,
|
||||
Activating,
|
||||
Deactivating,
|
||||
Active,
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct DataFormat
|
||||
{
|
||||
@@ -49,83 +82,95 @@ private:
|
||||
|
||||
struct Register
|
||||
{
|
||||
u8 controller_data[21];
|
||||
std::array<u8, 21> controller_data;
|
||||
u8 unknown_0x15[11];
|
||||
|
||||
// address 0x20
|
||||
u8 calibration_data[0x20];
|
||||
std::array<u8, 0x20> calibration_data;
|
||||
|
||||
u8 unknown_0x40[0x10];
|
||||
// address 0x40
|
||||
// Data is read from the extension on the passthrough port.
|
||||
std::array<u8, 0x10> passthrough_ext_calib;
|
||||
|
||||
// address 0x50
|
||||
u8 cert_data[0x40];
|
||||
std::array<u8, 0x40> challenge_data;
|
||||
|
||||
u8 unknown_0x90[0x60];
|
||||
|
||||
// address 0xF0
|
||||
u8 initialized;
|
||||
// Writes initialize the M+ to it's default (non-activated) state.
|
||||
// Used to deactivate the M+ and activate an attached extension.
|
||||
u8 init_trigger;
|
||||
|
||||
// address 0xF1
|
||||
u8 cert_enable;
|
||||
// Value is either 0 or 1.
|
||||
u8 challenge_type;
|
||||
|
||||
// Conduit 2 writes 1 byte to 0xf2 on calibration screen
|
||||
u8 unknown_0xf2[5];
|
||||
// address 0xF2
|
||||
// Games write 0x00 here to start and stop calibration.
|
||||
u8 calibration_trigger;
|
||||
|
||||
// address 0xf7
|
||||
// Wii Sports Resort reads regularly
|
||||
// Value starts at 0x00 and goes up after activation (not initialization)
|
||||
// Immediately returns 0x02, even still after 15 and 30 seconds
|
||||
// After the first data read the value seems to progress to 0x4,0x8,0xc,0xe
|
||||
// More typical seems to be 2,8,c,e
|
||||
// A value of 0xe triggers the game to read 64 bytes from 0x50
|
||||
// The game claims M+ is disconnected after this read of unsatisfactory data
|
||||
u8 cert_ready;
|
||||
// address 0xF3
|
||||
u8 unknown_0xf3[3];
|
||||
|
||||
u8 unknown_0xf8[2];
|
||||
// address 0xF6
|
||||
// Value is taken from the extension on the passthrough port.
|
||||
u8 passthrough_ext_id_4;
|
||||
|
||||
// address 0xF7
|
||||
// Games read this value to know when the data at 0x50 is ready.
|
||||
// Value is 0x02 upon activation. (via a write to 0xfe)
|
||||
// Real M+ changes this value to 0x4, 0x8, 0xc, and finally 0xe.
|
||||
// Games then trigger a 2nd stage via a write to 0xf1.
|
||||
// Real M+ changes this value to 0x14, 0x18, and finally 0x1a.
|
||||
// Note: We don't progress like this. We jump to the final value as soon as possible.
|
||||
ChallengeState challenge_state;
|
||||
|
||||
// address 0xF8
|
||||
// Values are taken from the extension on the passthrough port.
|
||||
u8 passthrough_ext_id_0;
|
||||
u8 passthrough_ext_id_5;
|
||||
|
||||
// address 0xFA
|
||||
u8 ext_identifier[6];
|
||||
std::array<u8, 6> ext_identifier;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(DataFormat) == 6, "Wrong size");
|
||||
static_assert(0x100 == sizeof(Register));
|
||||
static_assert(0x100 == sizeof(Register), "Wrong size");
|
||||
|
||||
static const u8 INACTIVE_DEVICE_ADDR = 0x53;
|
||||
static const u8 ACTIVE_DEVICE_ADDR = 0x52;
|
||||
static constexpr u8 INACTIVE_DEVICE_ADDR = 0x53;
|
||||
static constexpr u8 ACTIVE_DEVICE_ADDR = 0x52;
|
||||
|
||||
enum class PassthroughMode : u8
|
||||
{
|
||||
Disabled = 0x04,
|
||||
Nunchuk = 0x05,
|
||||
Classic = 0x07,
|
||||
};
|
||||
static constexpr u8 PASSTHROUGH_MODE_OFFSET = 0xfe;
|
||||
|
||||
bool IsActive() const;
|
||||
static constexpr int CALIBRATION_BITS = 16;
|
||||
|
||||
static constexpr u16 CALIBRATION_ZERO = 1 << (CALIBRATION_BITS - 1);
|
||||
// Values are similar to that of a typical real M+.
|
||||
static constexpr u16 CALIBRATION_SCALE_OFFSET = 0x4400;
|
||||
static constexpr u16 CALIBRATION_FAST_SCALE_DEGREES = 0x4b0;
|
||||
static constexpr u16 CALIBRATION_SLOW_SCALE_DEGREES = 0x10e;
|
||||
|
||||
void Activate();
|
||||
void Deactivate();
|
||||
void OnPassthroughModeWrite();
|
||||
|
||||
ActivationStatus GetActivationStatus() const;
|
||||
PassthroughMode GetPassthroughMode() const;
|
||||
|
||||
// TODO: when activated it seems the motion plus reactivates the extension
|
||||
// It sends 0x55 to 0xf0
|
||||
// It also writes 0x00 to slave:0x52 addr:0xfa for some reason
|
||||
// And starts a write to 0xfa but never writes bytes..
|
||||
// It tries to read data at 0x00 for 3 times (failing)
|
||||
// then it reads the 16 bytes of calibration at 0x20 and stops
|
||||
|
||||
// TODO: if an extension is attached after activation, it also does this.
|
||||
|
||||
int BusRead(u8 slave_addr, u8 addr, int count, u8* data_out) override;
|
||||
int BusWrite(u8 slave_addr, u8 addr, int count, const u8* data_in) override;
|
||||
|
||||
bool ReadDeviceDetectPin() const override;
|
||||
bool IsButtonPressed() const override;
|
||||
|
||||
// TODO: rename m_
|
||||
Register m_reg_data = {};
|
||||
|
||||
Register reg_data = {};
|
||||
// Used for timing of activation, deactivation, and preparation of challenge values.
|
||||
u8 m_progress_timer = {};
|
||||
|
||||
// The port on the end of the motion plus:
|
||||
I2CBus i2c_bus;
|
||||
ExtensionPort m_extension_port{&i2c_bus};
|
||||
I2CBus m_i2c_bus;
|
||||
ExtensionPort m_extension_port{&m_i2c_bus};
|
||||
};
|
||||
} // namespace WiimoteEmu
|
||||
|
||||
@@ -93,6 +93,7 @@ void Wiimote::Reset()
|
||||
m_eeprom.accel_calibration_1 = accel_calibration;
|
||||
m_eeprom.accel_calibration_2 = accel_calibration;
|
||||
|
||||
// TODO: Is this needed?
|
||||
// Data of unknown purpose:
|
||||
constexpr std::array<u8, 24> EEPROM_DATA_16D0 = {0x00, 0x00, 0x00, 0xFF, 0x11, 0xEE, 0x00, 0x00,
|
||||
0x33, 0xCC, 0x44, 0xBB, 0x00, 0x00, 0x66, 0x99,
|
||||
@@ -106,29 +107,29 @@ void Wiimote::Reset()
|
||||
m_i2c_bus.AddSlave(&m_speaker_logic);
|
||||
m_i2c_bus.AddSlave(&m_camera_logic);
|
||||
|
||||
// Reset extension connections:
|
||||
// Reset extension connections to NONE:
|
||||
m_is_motion_plus_attached = false;
|
||||
m_active_extension = ExtensionNumber::NONE;
|
||||
m_extension_port.AttachExtension(GetNoneExtension());
|
||||
m_motion_plus.GetExtPort().AttachExtension(GetNoneExtension());
|
||||
|
||||
// Switch to desired M+ status and extension (if any).
|
||||
// M+ and EXT are reset on attachment.
|
||||
HandleExtensionSwap();
|
||||
|
||||
// Reset sub-devices:
|
||||
// Reset sub-devices.
|
||||
m_speaker_logic.Reset();
|
||||
m_camera_logic.Reset();
|
||||
m_motion_plus.Reset();
|
||||
GetActiveExtension()->Reset();
|
||||
|
||||
m_status = {};
|
||||
// TODO: This will suppress a status report on connect when an extension is already attached.
|
||||
// I am not 100% sure if this is proper.
|
||||
// This will suppress a status report on connect when an extension is already attached.
|
||||
// TODO: I am not 100% sure if this is proper.
|
||||
m_status.extension = m_extension_port.IsDeviceConnected();
|
||||
|
||||
// Dynamics:
|
||||
m_swing_state = {};
|
||||
m_tilt_state = {};
|
||||
m_cursor_state = {};
|
||||
m_shake_state = {};
|
||||
}
|
||||
|
||||
@@ -165,6 +166,8 @@ Wiimote::Wiimote(const unsigned int index) : m_index(index)
|
||||
m_attachments->AddAttachment(std::make_unique<WiimoteEmu::Drums>());
|
||||
m_attachments->AddAttachment(std::make_unique<WiimoteEmu::Turntable>());
|
||||
|
||||
m_attachments->AddSetting(&m_motion_plus_setting, {_trans("Attach MotionPlus")}, true);
|
||||
|
||||
// rumble
|
||||
groups.emplace_back(m_rumble = new ControllerEmu::ControlGroup(_trans("Rumble")));
|
||||
m_rumble->controls.emplace_back(
|
||||
@@ -193,8 +196,6 @@ Wiimote::Wiimote(const unsigned int index) : m_index(index)
|
||||
_trans("%")},
|
||||
95, 0, 100);
|
||||
|
||||
// m_options->AddSetting(&m_motion_plus_setting, {_trans("Attach MotionPlus")}, true);
|
||||
|
||||
// Note: "Upright" and "Sideways" options can be enabled at the same time which produces an
|
||||
// orientation where the wiimote points towards the left with the buttons towards you.
|
||||
m_options->AddSetting(&m_upright_setting,
|
||||
@@ -310,6 +311,7 @@ void Wiimote::UpdateButtonsStatus()
|
||||
m_dpad->GetState(&m_status.buttons.hex, IsSideways() ? dpad_sideways_bitmasks : dpad_bitmasks);
|
||||
}
|
||||
|
||||
// This is called every ::Wiimote::UPDATE_FREQ (200hz)
|
||||
void Wiimote::Update()
|
||||
{
|
||||
// Check if connected.
|
||||
@@ -322,6 +324,7 @@ void Wiimote::Update()
|
||||
// Data is later accessed in IsSideways and IsUpright
|
||||
m_hotkeys->GetState();
|
||||
|
||||
// Update our motion simulations.
|
||||
StepDynamics();
|
||||
|
||||
// Update buttons in the status struct which is sent in 99% of input reports.
|
||||
@@ -334,10 +337,22 @@ void Wiimote::Update()
|
||||
// If a new extension is requested in the GUI the change will happen here.
|
||||
HandleExtensionSwap();
|
||||
|
||||
// Allow extension to perform any regular duties it may need.
|
||||
// (e.g. Nunchuk motion simulation step)
|
||||
// Input is prepared here too.
|
||||
// TODO: Separate input preparation from Update.
|
||||
GetActiveExtension()->Update();
|
||||
|
||||
if (m_is_motion_plus_attached)
|
||||
{
|
||||
// M+ has some internal state that must processed.
|
||||
m_motion_plus.Update();
|
||||
}
|
||||
|
||||
// Returns true if a report was sent.
|
||||
if (ProcessExtensionPortEvent())
|
||||
{
|
||||
// Extension port event occured.
|
||||
// Extension port event occurred.
|
||||
// Don't send any other reports.
|
||||
return;
|
||||
}
|
||||
@@ -403,6 +418,8 @@ void Wiimote::SendDataReport()
|
||||
// IR Camera:
|
||||
if (rpt_builder.HasIR())
|
||||
{
|
||||
// Note: Camera logic currently contains no changing state so we can just update it here.
|
||||
// If that changes this should be moved to Wiimote::Update();
|
||||
m_camera_logic.Update(GetTransformation());
|
||||
|
||||
// The real wiimote reads camera data from the i2c bus starting at offset 0x37:
|
||||
@@ -416,9 +433,16 @@ void Wiimote::SendDataReport()
|
||||
// Extension port:
|
||||
if (rpt_builder.HasExt())
|
||||
{
|
||||
// Update extension first as motion-plus may read from it.
|
||||
GetActiveExtension()->Update();
|
||||
m_motion_plus.Update();
|
||||
// Prepare extension input first as motion-plus may read from it.
|
||||
// This currently happens in Wiimote::Update();
|
||||
// TODO: Separate extension input data preparation from Update.
|
||||
// GetActiveExtension()->PrepareInput();
|
||||
|
||||
if (m_is_motion_plus_attached)
|
||||
{
|
||||
// TODO: Make input preparation triggered by bus read.
|
||||
m_motion_plus.PrepareInput(GetAngularVelocity());
|
||||
}
|
||||
|
||||
u8* ext_data = rpt_builder.GetExtDataPtr();
|
||||
const u8 ext_size = rpt_builder.GetExtDataSize();
|
||||
@@ -658,44 +682,59 @@ void Wiimote::StepDynamics()
|
||||
{
|
||||
EmulateSwing(&m_swing_state, m_swing, 1.f / ::Wiimote::UPDATE_FREQ);
|
||||
EmulateTilt(&m_tilt_state, m_tilt, 1.f / ::Wiimote::UPDATE_FREQ);
|
||||
EmulateCursor(&m_cursor_state, m_ir, 1.f / ::Wiimote::UPDATE_FREQ);
|
||||
EmulateShake(&m_shake_state, m_shake, 1.f / ::Wiimote::UPDATE_FREQ);
|
||||
|
||||
// TODO: Move cursor state out of ControllerEmu::Cursor
|
||||
// const auto cursor_mtx = EmulateCursorMovement(m_ir);
|
||||
}
|
||||
|
||||
Common::Vec3 Wiimote::GetAcceleration()
|
||||
{
|
||||
// Includes effects of:
|
||||
// IR, Tilt, Swing, Orientation, Shake
|
||||
|
||||
auto orientation = Common::Matrix33::Identity();
|
||||
|
||||
if (IsSideways())
|
||||
orientation *= Common::Matrix33::RotateZ(float(MathUtil::TAU / -4));
|
||||
|
||||
if (IsUpright())
|
||||
orientation *= Common::Matrix33::RotateX(float(MathUtil::TAU / 4));
|
||||
// TODO: Cursor forward/backward movement should produce acceleration.
|
||||
|
||||
Common::Vec3 accel =
|
||||
orientation *
|
||||
GetOrientation() *
|
||||
GetTransformation().Transform(
|
||||
m_swing_state.acceleration + Common::Vec3(0, 0, float(GRAVITY_ACCELERATION)), 0);
|
||||
|
||||
// Our shake effects have never been affected by orientation. Should they be?
|
||||
accel += m_shake_state.acceleration;
|
||||
|
||||
// Simulate centripetal acceleration caused by an offset of the accelerometer sensor.
|
||||
// Estimate of sensor position based on an image of the wii remote board:
|
||||
constexpr float ACCELEROMETER_Y_OFFSET = 0.1f;
|
||||
|
||||
const auto angular_velocity = GetAngularVelocity();
|
||||
const auto centripetal_accel =
|
||||
// TODO: Is this the proper way to combine the x and z angular velocities?
|
||||
std::pow(std::abs(angular_velocity.x) + std::abs(angular_velocity.z), 2) *
|
||||
ACCELEROMETER_Y_OFFSET;
|
||||
|
||||
accel.y += centripetal_accel;
|
||||
|
||||
return accel;
|
||||
}
|
||||
|
||||
Common::Vec3 Wiimote::GetAngularVelocity()
|
||||
{
|
||||
return GetOrientation() * (m_tilt_state.angular_velocity + m_swing_state.angular_velocity +
|
||||
m_cursor_state.angular_velocity);
|
||||
}
|
||||
|
||||
Common::Matrix44 Wiimote::GetTransformation() const
|
||||
{
|
||||
// Includes positional and rotational effects of:
|
||||
// IR, Swing, Tilt, Shake
|
||||
// Cursor, Swing, Tilt, Shake
|
||||
|
||||
// TODO: think about and clean up matrix order, make nunchuk match.
|
||||
return Common::Matrix44::Translate(-m_shake_state.position) *
|
||||
Common::Matrix44::FromMatrix33(GetRotationalMatrix(-m_tilt_state.angle) *
|
||||
GetRotationalMatrix(-m_swing_state.angle)) *
|
||||
EmulateCursorMovement(m_ir) * Common::Matrix44::Translate(-m_swing_state.position);
|
||||
Common::Matrix44::FromMatrix33(GetRotationalMatrix(
|
||||
-m_tilt_state.angle - m_swing_state.angle - m_cursor_state.angle)) *
|
||||
Common::Matrix44::Translate(-m_swing_state.position - m_cursor_state.position);
|
||||
}
|
||||
|
||||
Common::Matrix33 Wiimote::GetOrientation() const
|
||||
{
|
||||
return Common::Matrix33::RotateZ(float(MathUtil::TAU / -4 * IsSideways())) *
|
||||
Common::Matrix33::RotateX(float(MathUtil::TAU / 4 * IsUpright()));
|
||||
}
|
||||
|
||||
} // namespace WiimoteEmu
|
||||
|
||||
@@ -137,10 +137,20 @@ private:
|
||||
|
||||
void UpdateButtonsStatus();
|
||||
|
||||
// Returns simulated accelerometer data in m/s^2.
|
||||
Common::Vec3 GetAcceleration();
|
||||
// Used for simulating camera data. Does not include orientation transformations.
|
||||
|
||||
// Returns simulated gyroscope data in radians/s.
|
||||
Common::Vec3 GetAngularVelocity();
|
||||
|
||||
// Returns the transformation of the world around the wiimote.
|
||||
// Used for simulating camera data and for rotating acceleration data.
|
||||
// Does not include orientation transformations.
|
||||
Common::Matrix44 GetTransformation() const;
|
||||
|
||||
// Returns the world rotation from the effects of sideways/upright settings.
|
||||
Common::Matrix33 GetOrientation() const;
|
||||
|
||||
void HIDOutputReport(const void* data, u32 size);
|
||||
|
||||
void HandleReportRumble(const WiimoteCommon::OutputReportRumble&);
|
||||
@@ -236,7 +246,7 @@ private:
|
||||
ControllerEmu::SettingValue<bool> m_upright_setting;
|
||||
ControllerEmu::SettingValue<double> m_battery_setting;
|
||||
ControllerEmu::SettingValue<double> m_speaker_pan_setting;
|
||||
// ControllerEmu::SettingValue<bool> m_motion_plus_setting;
|
||||
ControllerEmu::SettingValue<bool> m_motion_plus_setting;
|
||||
|
||||
SpeakerLogic m_speaker_logic;
|
||||
MotionPlus m_motion_plus;
|
||||
@@ -267,6 +277,7 @@ private:
|
||||
// Dynamics:
|
||||
MotionState m_swing_state;
|
||||
RotationalState m_tilt_state;
|
||||
MotionState m_cursor_state;
|
||||
PositionalState m_shake_state;
|
||||
};
|
||||
} // namespace WiimoteEmu
|
||||
|
||||
@@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
|
||||
static std::thread g_save_thread;
|
||||
|
||||
// Don't forget to increase this after doing changes on the savestate system
|
||||
static const u32 STATE_VERSION = 108; // Last changed in PR 7870
|
||||
static const u32 STATE_VERSION = 109; // Last changed in PR 7861
|
||||
|
||||
// Maps savestate versions to Dolphin versions.
|
||||
// Versions after 42 don't need to be added to this list,
|
||||
|
||||
@@ -217,7 +217,7 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
|
||||
QRectF(-scale, raw_coord.z * scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS));
|
||||
|
||||
// Adjusted Z (if not hidden):
|
||||
if (adj_coord.z && adj_coord.x < 10000)
|
||||
if (adj_coord.IsVisible())
|
||||
{
|
||||
p.setBrush(GetAdjustedInputColor());
|
||||
p.drawRect(
|
||||
@@ -250,7 +250,7 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
|
||||
p.drawEllipse(QPointF{raw_coord.x, raw_coord.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
|
||||
|
||||
// Adjusted cursor position (if not hidden):
|
||||
if (adj_coord.x < 10000)
|
||||
if (adj_coord.IsVisible())
|
||||
{
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(GetAdjustedInputColor());
|
||||
@@ -492,11 +492,19 @@ void MappingIndicator::DrawForce(ControllerEmu::Force& force)
|
||||
QRectF(-scale, raw_coord.z * scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS));
|
||||
|
||||
// Adjusted Z:
|
||||
if (adj_coord.y)
|
||||
const auto curve_point =
|
||||
std::max(std::abs(m_motion_state.angle.x), std::abs(m_motion_state.angle.z)) / MathUtil::TAU;
|
||||
if (adj_coord.y || curve_point)
|
||||
{
|
||||
p.setBrush(GetAdjustedInputColor());
|
||||
p.drawRect(
|
||||
QRectF(-scale, adj_coord.y * -scale - INPUT_DOT_RADIUS / 2, scale * 2, INPUT_DOT_RADIUS));
|
||||
// Show off the angle somewhat with a curved line.
|
||||
QPainterPath path;
|
||||
path.moveTo(-scale, (adj_coord.y + curve_point) * -scale);
|
||||
path.quadTo({0, (adj_coord.y - curve_point) * -scale},
|
||||
{scale, (adj_coord.y + curve_point) * -scale});
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS));
|
||||
p.drawPath(path);
|
||||
}
|
||||
|
||||
// Draw "gate" shape.
|
||||
|
||||
@@ -55,7 +55,7 @@ void WiimoteEmuGeneral::CreateMainLayout()
|
||||
|
||||
extension->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
static_cast<QFormLayout*>(extension->layout())->addRow(m_extension_combo);
|
||||
static_cast<QFormLayout*>(extension->layout())->insertRow(0, m_extension_combo);
|
||||
|
||||
layout->addWidget(extension, 0, 3);
|
||||
layout->addWidget(CreateGroupBox(tr("Rumble"), Wiimote::GetWiimoteGroup(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -153,8 +154,7 @@ Cursor::StateData Cursor::GetState(const bool adjusted)
|
||||
// If auto-hide time is up or hide button is held:
|
||||
if (!m_auto_hide_timer || controls[6]->control_ref->State() > BUTTON_THRESHOLD)
|
||||
{
|
||||
// TODO: Use NaN or something:
|
||||
result.x = 10000;
|
||||
result.x = std::numeric_limits<ControlState>::quiet_NaN();
|
||||
result.y = 0;
|
||||
}
|
||||
|
||||
@@ -176,4 +176,9 @@ ControlState Cursor::GetVerticalOffset() const
|
||||
return m_vertical_offset_setting.GetValue() / 100;
|
||||
}
|
||||
|
||||
bool Cursor::StateData::IsVisible() const
|
||||
{
|
||||
return !std::isnan(x);
|
||||
}
|
||||
|
||||
} // namespace ControllerEmu
|
||||
|
||||
@@ -20,6 +20,8 @@ public:
|
||||
ControlState x{};
|
||||
ControlState y{};
|
||||
ControlState z{};
|
||||
|
||||
bool IsVisible() const;
|
||||
};
|
||||
|
||||
explicit Cursor(const std::string& name);
|
||||
|
||||
@@ -31,16 +31,30 @@ Force::Force(const std::string& name_) : ReshapableInput(name_, name_, GroupType
|
||||
_trans("cm"),
|
||||
// i18n: Refering to emulated wii remote swing movement.
|
||||
_trans("Distance of travel from neutral position.")},
|
||||
25, 0, 100);
|
||||
50, 1, 100);
|
||||
|
||||
AddSetting(&m_jerk_setting,
|
||||
// i18n: "Jerk" as it relates to physics. The time derivative of acceleration.
|
||||
{_trans("Jerk"),
|
||||
// i18n: The symbol/abbreviation for meters per second to the 3rd power.
|
||||
_trans("m/s³"),
|
||||
// These speed settings are used to calculate a maximum jerk (change in acceleration).
|
||||
// The calculation uses a travel distance of 1 meter.
|
||||
// The maximum value of 40 m/s is the approximate speed of the head of a golf club.
|
||||
// Games seem to not even properly detect motions at this speed.
|
||||
// Values result in an exponentially increasing jerk.
|
||||
|
||||
AddSetting(&m_speed_setting,
|
||||
{_trans("Speed"),
|
||||
// i18n: The symbol/abbreviation for meters per second.
|
||||
_trans("m/s"),
|
||||
// i18n: Refering to emulated wii remote swing movement.
|
||||
_trans("Maximum change in acceleration.")},
|
||||
500, 1, 1000);
|
||||
_trans("Peak velocity of outward swing movements.")},
|
||||
16, 1, 40);
|
||||
|
||||
// "Return Speed" allows for a "slow return" that won't trigger additional actions.
|
||||
AddSetting(&m_return_speed_setting,
|
||||
{_trans("Return Speed"),
|
||||
// i18n: The symbol/abbreviation for meters per second.
|
||||
_trans("m/s"),
|
||||
// i18n: Refering to emulated wii remote swing movement.
|
||||
_trans("Peak velocity of movements to neutral position.")},
|
||||
2, 1, 40);
|
||||
|
||||
AddSetting(&m_angle_setting,
|
||||
{_trans("Angle"),
|
||||
@@ -48,7 +62,7 @@ Force::Force(const std::string& name_) : ReshapableInput(name_, name_, GroupType
|
||||
_trans("°"),
|
||||
// i18n: Refering to emulated wii remote swing movement.
|
||||
_trans("Rotation applied at extremities of swing.")},
|
||||
45, 0, 180);
|
||||
90, 1, 180);
|
||||
}
|
||||
|
||||
Force::ReshapeData Force::GetReshapableState(bool adjusted)
|
||||
@@ -70,8 +84,8 @@ Force::StateData Force::GetState(bool adjusted)
|
||||
|
||||
if (adjusted)
|
||||
{
|
||||
// Apply deadzone to z.
|
||||
z = ApplyDeadzone(z, GetDeadzonePercentage());
|
||||
// Apply deadzone to z and scale.
|
||||
z = ApplyDeadzone(z, GetDeadzonePercentage()) * GetMaxDistance();
|
||||
}
|
||||
|
||||
return {float(state.x), float(state.y), float(z)};
|
||||
@@ -83,9 +97,14 @@ ControlState Force::GetGateRadiusAtAngle(double) const
|
||||
return GetMaxDistance();
|
||||
}
|
||||
|
||||
ControlState Force::GetMaxJerk() const
|
||||
ControlState Force::GetSpeed() const
|
||||
{
|
||||
return m_jerk_setting.GetValue();
|
||||
return m_speed_setting.GetValue();
|
||||
}
|
||||
|
||||
ControlState Force::GetReturnSpeed() const
|
||||
{
|
||||
return m_return_speed_setting.GetValue();
|
||||
}
|
||||
|
||||
ControlState Force::GetTwistAngle() const
|
||||
|
||||
@@ -26,8 +26,9 @@ public:
|
||||
|
||||
StateData GetState(bool adjusted = true);
|
||||
|
||||
// Return jerk in m/s^3.
|
||||
ControlState GetMaxJerk() const;
|
||||
// Velocities returned in m/s.
|
||||
ControlState GetSpeed() const;
|
||||
ControlState GetReturnSpeed() const;
|
||||
|
||||
// Return twist angle in radians.
|
||||
ControlState GetTwistAngle() const;
|
||||
@@ -37,7 +38,8 @@ public:
|
||||
|
||||
private:
|
||||
SettingValue<double> m_distance_setting;
|
||||
SettingValue<double> m_jerk_setting;
|
||||
SettingValue<double> m_speed_setting;
|
||||
SettingValue<double> m_return_speed_setting;
|
||||
SettingValue<double> m_angle_setting;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user