mirror of https://github.com/AxioDL/metaforce.git
Add CMayaSpline update zeus
This commit is contained in:
parent
5e3f70eb77
commit
070d34d281
|
@ -118,6 +118,7 @@ set(RUNTIME_SOURCES_B
|
|||
TCastTo.hpp TCastTo.cpp
|
||||
GCNTypes.hpp
|
||||
CTextureCache.hpp CTextureCache.cpp
|
||||
CMayaSpline.hpp CMayaSpline.cpp
|
||||
${PLAT_SRCS})
|
||||
|
||||
function(add_runtime_common_library name)
|
||||
|
|
|
@ -0,0 +1,435 @@
|
|||
#include "CMayaSpline.hpp"
|
||||
|
||||
namespace rstl {} // namespace rstl
|
||||
|
||||
namespace metaforce {
|
||||
void ValidateTangent(zeus::CVector2f& tangent) {
|
||||
if (tangent.x() < 0.f) {
|
||||
tangent.x() = 0.f;
|
||||
}
|
||||
|
||||
const float mag = tangent.magnitude();
|
||||
if (mag != 0.f) {
|
||||
tangent /= mag;
|
||||
}
|
||||
|
||||
if (tangent.x() == 0.f && tangent.y() != 0.f) {
|
||||
const float mul = tangent.y() >= 0.f ? 1.f : -1.f;
|
||||
tangent.x() = 0.0001f;
|
||||
tangent.y() = 5729578.0f * tangent.x() * mul;
|
||||
}
|
||||
}
|
||||
|
||||
CMayaSplineKnot::CMayaSplineKnot(CInputStream& in) {
|
||||
x0_time = in.readFloatBig();
|
||||
x4_amplitude = in.readFloatBig();
|
||||
x8_ = in.readByte();
|
||||
x9_ = in.readByte();
|
||||
if (x8_ == 5) {
|
||||
float x = in.readFloatBig();
|
||||
float y = in.readFloatBig();
|
||||
xc_cachedTangentA = {x, y};
|
||||
}
|
||||
|
||||
if (x9_ == 5) {
|
||||
float x = in.readFloatBig();
|
||||
float y = in.readFloatBig();
|
||||
x14_cachedTangentB = {x, y};
|
||||
}
|
||||
}
|
||||
|
||||
void CMayaSplineKnot::GetTangents(CMayaSplineKnot* prev, CMayaSplineKnot* next, zeus::CVector2f& tangentA,
|
||||
zeus::CVector2f& tangentB) {
|
||||
if (xa_24_dirty) {
|
||||
CalculateTangents(prev, next);
|
||||
}
|
||||
|
||||
tangentA = xc_cachedTangentA;
|
||||
tangentB = x14_cachedTangentB;
|
||||
}
|
||||
|
||||
void CMayaSplineKnot::CalculateTangents(CMayaSplineKnot* prev, CMayaSplineKnot* next) {
|
||||
xa_24_dirty = false;
|
||||
bool calculateTangents = false;
|
||||
if (x8_ == 4 && prev != nullptr) {
|
||||
float fVar2 = std::abs(prev->GetAmplitude() - GetAmplitude());
|
||||
float fVar3 = fVar2;
|
||||
if (next != nullptr) {
|
||||
fVar3 = std::abs(next->GetAmplitude() - GetAmplitude());
|
||||
}
|
||||
if (fVar3 <= 0.05f || fVar2 <= 0.05f) {
|
||||
x8_ = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (x8_ == 0) {
|
||||
if (prev == nullptr) {
|
||||
xc_cachedTangentA = {1.f, 0.f};
|
||||
} else {
|
||||
xc_cachedTangentA = {GetTime() - prev->GetTime(), GetAmplitude() - prev->GetAmplitude()};
|
||||
}
|
||||
} else if (x8_ == 1) {
|
||||
float fVar1 = 0.f;
|
||||
if (prev != nullptr) {
|
||||
fVar1 = GetTime() - prev->GetTime();
|
||||
} else if (next != nullptr) {
|
||||
fVar1 = next->GetTime() - GetTime();
|
||||
}
|
||||
xc_cachedTangentA = {fVar1, 0.f};
|
||||
} else if (x8_ == 2) {
|
||||
calculateTangents = true;
|
||||
} else if (x8_ == 3) {
|
||||
xc_cachedTangentA = zeus::skOne2f;
|
||||
} else if (x8_ == 4) {
|
||||
x8_ = 2;
|
||||
calculateTangents = true;
|
||||
}
|
||||
|
||||
if (x9_ == 0) {
|
||||
if (next == nullptr) {
|
||||
x14_cachedTangentB = {1.f, 0.f};
|
||||
} else {
|
||||
x14_cachedTangentB = {next->GetTime() - GetTime(), next->GetAmplitude() - GetAmplitude()};
|
||||
}
|
||||
} else if (x9_ == 1) {
|
||||
float fVar1 = 0.f;
|
||||
if (next != nullptr) {
|
||||
fVar1 = next->GetTime() - GetTime();
|
||||
} else if (prev != nullptr) {
|
||||
fVar1 = GetTime() - prev->GetTime();
|
||||
}
|
||||
|
||||
x14_cachedTangentB = {fVar1, 0.f};
|
||||
} else if (x9_ == 2) {
|
||||
calculateTangents = true;
|
||||
} else if (x9_ == 3) {
|
||||
x14_cachedTangentB = {1.f, 0.f};
|
||||
} else if (x9_ == 4 && next != nullptr) {
|
||||
float fVar1 = next->GetAmplitude() - GetAmplitude();
|
||||
float fVar2 = fVar1;
|
||||
if (prev != nullptr) {
|
||||
fVar2 = prev->GetAmplitude() - GetAmplitude();
|
||||
}
|
||||
|
||||
if (fVar1 <= 0.05f || fVar2 <= 0.05f) {
|
||||
x9_ = 1;
|
||||
}
|
||||
calculateTangents = true;
|
||||
}
|
||||
|
||||
if (calculateTangents) {
|
||||
zeus::CVector2f tangentA;
|
||||
zeus::CVector2f tangentB;
|
||||
|
||||
if (prev == nullptr && next != nullptr) {
|
||||
tangentA = tangentB = {next->GetTime() - GetTime(), next->GetAmplitude() - GetAmplitude()};
|
||||
} else if (prev != nullptr && next == nullptr) {
|
||||
tangentA = tangentB = {GetTime() - prev->GetTime(), GetAmplitude() - prev->GetAmplitude()};
|
||||
} else if (prev != nullptr && next != nullptr) {
|
||||
float timeDiff = next->GetTime() - prev->GetTime();
|
||||
float ampDiff = next->GetAmplitude() - prev->GetAmplitude();
|
||||
float amp = timeDiff >= 0.0001f ? ampDiff / timeDiff : (ampDiff <= 0.f ? -5729578.0f : 5729578.0f);
|
||||
float nextTimeDiff = next->GetTime() - GetTime();
|
||||
float prevTimeDiff = GetTime() - prev->GetTime();
|
||||
float ampA = 0.f;
|
||||
float ampB = 0.;
|
||||
float timeA = 0.;
|
||||
float timeB = 0.;
|
||||
if (nextTimeDiff >= 0.f) {
|
||||
ampA = nextTimeDiff * amp;
|
||||
} else {
|
||||
timeA = 0.f;
|
||||
ampA = amp;
|
||||
}
|
||||
|
||||
if (prevTimeDiff >= 0.f) {
|
||||
ampB = prevTimeDiff * amp;
|
||||
} else {
|
||||
timeB = 0.f;
|
||||
}
|
||||
tangentB = {timeB, ampB};
|
||||
tangentA = {timeA, ampA};
|
||||
|
||||
} else {
|
||||
tangentA.zeroOut();
|
||||
tangentB.zeroOut();
|
||||
}
|
||||
|
||||
if (x8_ == 2) {
|
||||
xc_cachedTangentA = tangentA;
|
||||
}
|
||||
if (x9_ == 2) {
|
||||
x14_cachedTangentB = tangentB;
|
||||
}
|
||||
}
|
||||
ValidateTangent(xc_cachedTangentA);
|
||||
ValidateTangent(x14_cachedTangentB);
|
||||
}
|
||||
|
||||
CMayaSpline::CMayaSpline(CInputStream& in, s32 count) {
|
||||
x0_preInfinity = in.readByte();
|
||||
x4_postInfinity = in.readByte();
|
||||
u32 knotCount = in.readUint32Big();
|
||||
x8_knots.reserve(knotCount);
|
||||
for (size_t i = 0; i < knotCount; ++i) {
|
||||
x8_knots.emplace_back(in);
|
||||
}
|
||||
x18_clampMode = in.readByte();
|
||||
x1c_minAmplitudeTime = in.readFloatBig();
|
||||
x20_maxAmplitudeTime = in.readFloatBig();
|
||||
}
|
||||
|
||||
float CMayaSpline::GetMinTime() const { return x8_knots.empty() ? 0.f : x8_knots[0].GetTime(); }
|
||||
float CMayaSpline::GetMaxTime() const { return x8_knots.empty() ? 0.f : x8_knots[GetKnotCount() - 1].GetTime(); }
|
||||
float CMayaSpline::GetDuration() const { return x8_knots.empty() ? 0.f : GetMaxTime() - GetMinTime(); }
|
||||
|
||||
float CMayaSpline::EvaluateAt(float time) {
|
||||
float amplitude = EvaluateAtUnclamped(time);
|
||||
if (x18_clampMode == 1) {
|
||||
if (x1c_minAmplitudeTime > amplitude) {
|
||||
return x1c_minAmplitudeTime;
|
||||
}
|
||||
if (x20_maxAmplitudeTime < amplitude) {
|
||||
return x20_maxAmplitudeTime;
|
||||
}
|
||||
return amplitude;
|
||||
} else if (x18_clampMode == 2) {
|
||||
float center = x20_maxAmplitudeTime - x1c_minAmplitudeTime;
|
||||
|
||||
if (center > 0.f) {
|
||||
if (amplitude <= FLT_EPSILON + x20_maxAmplitudeTime) {
|
||||
return amplitude - (center * static_cast<float>(s32((amplitude - x20_maxAmplitudeTime) / center) + 1));
|
||||
}
|
||||
if (amplitude < x1c_minAmplitudeTime - FLT_EPSILON) {
|
||||
return amplitude + (center * static_cast<float>(std::abs(s32((amplitude - x1c_minAmplitudeTime) / center))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return amplitude;
|
||||
}
|
||||
|
||||
float CMayaSpline::EvaluateAtUnclamped(float time) {
|
||||
if (x8_knots.empty()) {
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
u32 lastIdx = x8_knots.size() - 1;
|
||||
bool bVar2 = false;
|
||||
float retVal;
|
||||
if (time < x8_knots[0].GetTime()) {
|
||||
if (x0_preInfinity == 0) {
|
||||
return x8_knots[0].GetAmplitude();
|
||||
}
|
||||
return EvaluateInfinities(time, true);
|
||||
} else if (x8_knots[lastIdx].GetTime() >= time) {
|
||||
bool bVar2 = false;
|
||||
s32 local_68 = -1;
|
||||
s32 iVar1 = x24_chachedKnotIndex;
|
||||
if (iVar1 != -1) {
|
||||
if (lastIdx <= iVar1 || x8_knots[lastIdx].GetTime() >= time) {
|
||||
if (iVar1 > 0 && x8_knots[iVar1].GetTime() > time) {
|
||||
s32 iVar3 = iVar1 - 1;
|
||||
bVar2 = x8_knots[iVar3].GetTime() < time;
|
||||
if (bVar2) {
|
||||
local_68 = iVar1;
|
||||
}
|
||||
if (x8_knots[iVar3].GetTime() == time) {
|
||||
x24_chachedKnotIndex = iVar3;
|
||||
return x8_knots[x24_chachedKnotIndex].GetAmplitude();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
retVal = x8_knots[iVar1 + 1].GetTime();
|
||||
if (retVal == time) {
|
||||
x24_chachedKnotIndex = lastIdx;
|
||||
return x8_knots[x24_chachedKnotIndex].GetAmplitude();
|
||||
}
|
||||
|
||||
if (retVal > time) {
|
||||
bVar2 = true;
|
||||
local_68 = iVar1 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bVar2 && (FindKnot(time, local_68))) {
|
||||
if (local_68 == 0) {
|
||||
x24_chachedKnotIndex = 0;
|
||||
return x8_knots[0].GetAmplitude();
|
||||
}
|
||||
if (local_68 == x8_knots.size()) {
|
||||
x24_chachedKnotIndex = 0;
|
||||
return x8_knots[lastIdx].GetAmplitude();
|
||||
}
|
||||
}
|
||||
|
||||
lastIdx = local_68 - 1;
|
||||
if (x28_ != lastIdx) {
|
||||
x24_chachedKnotIndex = lastIdx;
|
||||
x28_ = lastIdx;
|
||||
if (x8_knots[x24_chachedKnotIndex].GetX9() == 3) {
|
||||
x2c_24_dirty = true;
|
||||
} else {
|
||||
x2c_24_dirty = false;
|
||||
rstl::reserved_vector<zeus::CVector2f, 4> points;
|
||||
FindControlPoints(x24_chachedKnotIndex, points);
|
||||
CalculateHermiteCoefficients(points, x34_cachedHermitCoefs);
|
||||
x30_cachedMinTime = points[0].x();
|
||||
}
|
||||
}
|
||||
|
||||
if (x2c_24_dirty) {
|
||||
return x8_knots[x24_chachedKnotIndex].GetTime();
|
||||
} else {
|
||||
return EvaluateHermite(time);
|
||||
}
|
||||
}
|
||||
|
||||
if (x4_postInfinity == 0) {
|
||||
return x8_knots[lastIdx].GetAmplitude();
|
||||
}
|
||||
|
||||
return EvaluateInfinities(time, false);
|
||||
}
|
||||
|
||||
float CMayaSpline::EvaluateInfinities(float time, bool pre) {
|
||||
if (x8_knots.empty()) {
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
s32 lastIdx = x8_knots.size() - 1;
|
||||
CMayaSplineKnot* curKnot = &x8_knots[0];
|
||||
const float startTime = x8_knots[0].GetTime();
|
||||
const float endTime = x8_knots[lastIdx].GetAmplitude();
|
||||
float center = endTime - startTime;
|
||||
if (zeus::close_enough(center, 0)) {
|
||||
return curKnot->GetAmplitude();
|
||||
}
|
||||
|
||||
double tmp = 0.f;
|
||||
float divTime =
|
||||
(time <= endTime) ? std::modf((time - startTime) / center, &tmp) : std::modf((time - endTime) / center, &tmp);
|
||||
|
||||
center = center * std::abs(divTime);
|
||||
tmp = 1.f + std::abs(tmp);
|
||||
|
||||
if (!pre) {
|
||||
if (x4_postInfinity == 4) {
|
||||
divTime = std::fmod(tmp, 2.f);
|
||||
if (zeus::close_enough(divTime, 0.f)) {
|
||||
center = startTime + center;
|
||||
} else {
|
||||
center = endTime - center;
|
||||
}
|
||||
} else if (x4_postInfinity == 2 || x4_postInfinity == 3) {
|
||||
center = startTime + center;
|
||||
} else if (x4_postInfinity == 1) {
|
||||
center = time - endTime;
|
||||
zeus::CVector2f tangentA;
|
||||
zeus::CVector2f tangentB;
|
||||
x8_knots[0].GetTangents((lastIdx < 1) ? nullptr : &x8_knots[lastIdx - 2], nullptr, tangentA, tangentB);
|
||||
if (!zeus::close_enough(tangentB.x(), 0.f)) {
|
||||
return x8_knots[lastIdx].GetAmplitude() + (center * tangentB.y() / tangentB.x());
|
||||
}
|
||||
return x8_knots[lastIdx].GetAmplitude();
|
||||
}
|
||||
} else if (x0_preInfinity == 4) {
|
||||
divTime = std::fmod(tmp, 2.f);
|
||||
if (zeus::close_enough(divTime, 0.f)) {
|
||||
center = endTime - center;
|
||||
} else {
|
||||
center = startTime + center;
|
||||
}
|
||||
} else if (x0_preInfinity == 2 || x0_preInfinity == 3) {
|
||||
center = endTime - center;
|
||||
} else if (x0_preInfinity == 1) {
|
||||
center = (startTime - time);
|
||||
zeus::CVector2f tangentA;
|
||||
zeus::CVector2f tangentB;
|
||||
x8_knots[0].GetTangents(nullptr, &x8_knots[1], tangentA, tangentB);
|
||||
if (!zeus::close_enough(tangentA.x(), 0)) {
|
||||
return (x8_knots[0].GetAmplitude() - (center * tangentA.y() / tangentA.x()));
|
||||
}
|
||||
return x8_knots[0].GetAmplitude();
|
||||
}
|
||||
|
||||
float eval = EvaluateAt(center);
|
||||
if (pre && x0_preInfinity == 3) {
|
||||
return eval - (tmp * x8_knots[lastIdx].GetAmplitude() - x8_knots[0].GetAmplitude());
|
||||
}
|
||||
|
||||
if (!pre && x4_postInfinity == 3) {
|
||||
return eval + (tmp * x8_knots[lastIdx].GetAmplitude() - x8_knots[0].GetAmplitude());
|
||||
}
|
||||
return eval;
|
||||
}
|
||||
|
||||
float CMayaSpline::EvaluateHermite(float time) {
|
||||
const float timeDiff = time - x30_cachedMinTime;
|
||||
return x34_cachedHermitCoefs[0] + (timeDiff * x34_cachedHermitCoefs[1]) + (timeDiff * x34_cachedHermitCoefs[2]) +
|
||||
(timeDiff * x34_cachedHermitCoefs[3]);
|
||||
}
|
||||
|
||||
bool CMayaSpline::FindKnot(float time, s32& knotIndex) {
|
||||
if (x8_knots.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
u32 lower = 0;
|
||||
u32 upper = x8_knots.size();
|
||||
while (lower < upper) {
|
||||
u32 index = (lower + upper) / 2;
|
||||
const auto& knot = x8_knots[index];
|
||||
if (knot.GetTime() > time) {
|
||||
upper = index - 1;
|
||||
} else if (time > knot.GetTime()) {
|
||||
lower = index + 1;
|
||||
} else {
|
||||
knotIndex = index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
knotIndex = lower;
|
||||
return false;
|
||||
}
|
||||
|
||||
void CMayaSpline::FindControlPoints(s32 knotIndex, rstl::reserved_vector<zeus::CVector2f, 4>& controlPoints) {
|
||||
CMayaSplineKnot* knot = &x8_knots[knotIndex];
|
||||
controlPoints.emplace_back(knot->GetTime(), knot->GetAmplitude());
|
||||
|
||||
zeus::CVector2f tangentA;
|
||||
zeus::CVector2f tangentB;
|
||||
CMayaSplineKnot* next = (knotIndex + 1 < x8_knots.size()) ? &x8_knots[knotIndex + 1] : nullptr;
|
||||
CMayaSplineKnot* prev = (knotIndex - 1 >= 0) ? &x8_knots[knotIndex - 1] : nullptr;
|
||||
knot->GetTangents(prev, next, tangentA, tangentB);
|
||||
|
||||
knot = &x8_knots[knotIndex + 1];
|
||||
controlPoints.emplace_back(controlPoints[0] + (tangentB * zeus::CVector2f{1.f / 3.f}));
|
||||
next = (knotIndex + 2 < x8_knots.size()) ? &x8_knots[knotIndex + 2] : nullptr;
|
||||
prev = (knotIndex - 2 >= 0) ? &x8_knots[knotIndex - 2] : nullptr;
|
||||
knot->GetTangents(prev, next, tangentA, tangentB);
|
||||
zeus::CVector2f knotV = {knot->GetTime(), knot->GetAmplitude()};
|
||||
controlPoints.emplace_back(knotV - (tangentA * zeus::CVector2f{1.f / 3.f}));
|
||||
controlPoints.emplace_back(knotV);
|
||||
}
|
||||
|
||||
void CMayaSpline::CalculateHermiteCoefficients(const rstl::reserved_vector<zeus::CVector2f, 4>& controlPoints,
|
||||
float* coefs) {
|
||||
const zeus::CVector2f point1 = controlPoints[3] - controlPoints[0];
|
||||
const zeus::CVector2f point2 = controlPoints[1] - controlPoints[0];
|
||||
const zeus::CVector2f point3 = controlPoints[3] - controlPoints[2];
|
||||
float dVar7 = point2.x() != 0.f ? point2.y() / point2.x() : 5729578.0f;
|
||||
float dVar6 = point3.x() != 0.f ? point3.y() / point3.x() : 5729578.0f;
|
||||
const float point1XSq = point1.x() * point1.x();
|
||||
coefs[0] =
|
||||
((1.f / (point1XSq)) * (((dVar7 * point1.x()) + (dVar6 * point1.x()) - point1.y()) - point1.y())) / point1.x();
|
||||
coefs[1] = ((1.f / (point1XSq)) *
|
||||
((((point1.y() + (point1.y() + point1.y())) - (dVar7 * point1.x())) - (dVar7 * point1.x())) -
|
||||
(dVar6 * point1.x())));
|
||||
coefs[2] = dVar7;
|
||||
coefs[3] = controlPoints[0].y();
|
||||
}
|
||||
|
||||
} // namespace metaforce
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "RetroTypes.hpp"
|
||||
|
||||
#include <zeus/CVector2f.hpp>
|
||||
#include <rstl.hpp>
|
||||
#include <cmath>
|
||||
#include <cfloat>
|
||||
|
||||
|
||||
namespace metaforce {
|
||||
class CMayaSplineKnot {
|
||||
float x0_time;
|
||||
float x4_amplitude;
|
||||
u8 x8_;
|
||||
u8 x9_;
|
||||
bool xa_24_dirty : 1 = true;
|
||||
u8 xb_;
|
||||
zeus::CVector2f xc_cachedTangentA;
|
||||
zeus::CVector2f x14_cachedTangentB;
|
||||
|
||||
public:
|
||||
CMayaSplineKnot(CInputStream& in);
|
||||
|
||||
float GetTime() const { return x0_time; }
|
||||
float GetAmplitude() const { return x4_amplitude; }
|
||||
u8 GetX8() const { return x8_; }
|
||||
u8 GetX9() const { return x9_; }
|
||||
void GetTangents(CMayaSplineKnot* prev, CMayaSplineKnot* next, zeus::CVector2f& tangentA, zeus::CVector2f& tangentB);
|
||||
void CalculateTangents(CMayaSplineKnot* prev, CMayaSplineKnot* next);
|
||||
};
|
||||
|
||||
class CMayaSpline {
|
||||
u32 x0_preInfinity;
|
||||
u32 x4_postInfinity;
|
||||
std::vector<CMayaSplineKnot> x8_knots;
|
||||
u32 x18_clampMode;
|
||||
float x1c_minAmplitudeTime;
|
||||
float x20_maxAmplitudeTime;
|
||||
s32 x24_chachedKnotIndex = -1;
|
||||
s32 x28_ = -1;
|
||||
bool x2c_24_dirty = false;
|
||||
float x30_cachedMinTime;
|
||||
float x34_cachedHermitCoefs[4];
|
||||
|
||||
public:
|
||||
CMayaSpline(CInputStream& in, s32 count);
|
||||
u32 GetKnotCount() const { return x8_knots.size(); }
|
||||
const std::vector<CMayaSplineKnot>& GetKnots() const { return x8_knots; }
|
||||
float GetMinTime() const;
|
||||
float GetMaxTime() const;
|
||||
float GetDuration() const;
|
||||
|
||||
float EvaluateAt(float time);
|
||||
float EvaluateAtUnclamped(float time);
|
||||
float EvaluateInfinities(float time, bool Pre);
|
||||
float EvaluateHermite(float time);
|
||||
bool FindKnot(float time, int& knotIndex);
|
||||
void FindControlPoints(s32 knotIndex, rstl::reserved_vector<zeus::CVector2f, 4>& controlPoints);
|
||||
void CalculateHermiteCoefficients(const rstl::reserved_vector<zeus::CVector2f, 4>& controlPoits, float* coefs);
|
||||
};
|
||||
} // namespace metaforce
|
|
@ -1 +1 @@
|
|||
Subproject commit 9130bf977e26bbee87c7364ff16fe621f56f1d3f
|
||||
Subproject commit bb9b4c83af12647df1db7978347bd297dda3277b
|
Loading…
Reference in New Issue