fix: lxfml normalization (#1835)

This commit is contained in:
David Markowitz 2025-06-27 22:31:48 -07:00 committed by GitHub
parent ecbb465020
commit 55d181ea4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 95 additions and 26 deletions

View File

@ -3,6 +3,7 @@
// C++ // C++
#include <charconv> #include <charconv>
#include <cstdint> #include <cstdint>
#include <cmath>
#include <ctime> #include <ctime>
#include <functional> #include <functional>
#include <optional> #include <optional>
@ -145,7 +146,7 @@ namespace GeneralUtils {
template <typename... Bases> template <typename... Bases>
struct overload : Bases... { struct overload : Bases... {
using is_transparent = void; using is_transparent = void;
using Bases::operator() ... ; using Bases::operator() ...;
}; };
struct char_pointer_hash { struct char_pointer_hash {
@ -202,7 +203,7 @@ namespace GeneralUtils {
} }
template<typename T> template<typename T>
requires(!Numeric<T>) requires(!Numeric<T>)
[[nodiscard]] std::optional<T> TryParse(std::string_view str); [[nodiscard]] std::optional<T> TryParse(std::string_view str);
#if !(__GNUC__ >= 11 || _MSC_VER >= 1924) #if !(__GNUC__ >= 11 || _MSC_VER >= 1924)
@ -221,7 +222,7 @@ namespace GeneralUtils {
*/ */
template <std::floating_point T> template <std::floating_point T>
[[nodiscard]] std::optional<T> TryParse(std::string_view str) noexcept [[nodiscard]] std::optional<T> TryParse(std::string_view str) noexcept
try { try {
while (!str.empty() && std::isspace(str.front())) str.remove_prefix(1); while (!str.empty() && std::isspace(str.front())) str.remove_prefix(1);
size_t parseNum; size_t parseNum;
@ -323,4 +324,28 @@ namespace GeneralUtils {
return GenerateRandomNumber<T>(std::numeric_limits<T>::min(), std::numeric_limits<T>::max()); return GenerateRandomNumber<T>(std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
} }
// https://www.quora.com/How-do-you-round-to-specific-increments-like-0-5-in-C
// Rounds to the nearest floating point value specified.
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T RountToNearestEven(const T value, const T modulus) {
const auto modulo = std::fmod(value, modulus);
const auto abs_modulo_2 = std::abs(modulo * 2);
const auto abs_modulus = std::abs(modulus);
bool round_away_from_zero = false;
if (abs_modulo_2 > abs_modulus) {
round_away_from_zero = true;
} else if (abs_modulo_2 == abs_modulus) {
const auto trunc_quot = std::floor(std::abs(value / modulus));
const auto odd = std::fmod(trunc_quot, T{ 2 }) != 0;
round_away_from_zero = odd;
}
if (round_away_from_zero) {
return value + (std::copysign(modulus, value) - modulo);
} else {
return value - modulo;
}
}
} }

View File

@ -6,7 +6,7 @@
#include <ranges> #include <ranges>
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) { Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
Result toReturn; Result toReturn;
tinyxml2::XMLDocument doc; tinyxml2::XMLDocument doc;
const auto err = doc.Parse(data.data()); const auto err = doc.Parse(data.data());
@ -44,29 +44,42 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) {
NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f }; NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f };
NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f }; NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f };
// Calculate the lowest and highest points on the entire model NiPoint3 delta = NiPoint3Constant::ZERO;
for (const auto& transformation : transformations | std::views::values) { if (curPosition == NiPoint3Constant::ZERO) {
auto split = GeneralUtils::SplitString(transformation, ','); // Calculate the lowest and highest points on the entire model
if (split.size() < 12) { for (const auto& transformation : transformations | std::views::values) {
LOG("Not enough in the split?"); auto split = GeneralUtils::SplitString(transformation, ',');
continue; if (split.size() < 12) {
LOG("Not enough in the split?");
continue;
}
auto x = GeneralUtils::TryParse<float>(split[9]).value();
auto y = GeneralUtils::TryParse<float>(split[10]).value();
auto z = GeneralUtils::TryParse<float>(split[11]).value();
if (x < lowest.x) lowest.x = x;
if (y < lowest.y) lowest.y = y;
if (z < lowest.z) lowest.z = z;
if (highest.x < x) highest.x = x;
if (highest.y < y) highest.y = y;
if (highest.z < z) highest.z = z;
} }
auto x = GeneralUtils::TryParse<float>(split[9]).value(); delta = (highest - lowest) / 2.0f;
auto y = GeneralUtils::TryParse<float>(split[10]).value(); } else {
auto z = GeneralUtils::TryParse<float>(split[11]).value(); lowest = curPosition;
if (x < lowest.x) lowest.x = x; highest = curPosition;
if (y < lowest.y) lowest.y = y; delta = NiPoint3Constant::ZERO;
if (z < lowest.z) lowest.z = z;
if (highest.x < x) highest.x = x;
if (highest.y < y) highest.y = y;
if (highest.z < z) highest.z = z;
} }
auto delta = (highest - lowest) / 2.0f;
auto newRootPos = lowest + delta; auto newRootPos = lowest + delta;
// Need to snap this chosen position to the nearest valid spot
// on the LEGO grid
newRootPos.x = GeneralUtils::RountToNearestEven(newRootPos.x, 0.8f);
newRootPos.z = GeneralUtils::RountToNearestEven(newRootPos.z, 0.8f);
// Clamp the Y to the lowest point on the model // Clamp the Y to the lowest point on the model
newRootPos.y = lowest.y; newRootPos.y = lowest.y;
@ -78,9 +91,9 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) {
continue; continue;
} }
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x; auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y; auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z; auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
std::stringstream stream; std::stringstream stream;
for (int i = 0; i < 9; i++) { for (int i = 0; i < 9; i++) {
stream << split[i]; stream << split[i];

View File

@ -17,7 +17,7 @@ namespace Lxfml {
// Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0. // Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0.
// Returns a struct of its new center and the updated LXFML containing these edits. // Returns a struct of its new center and the updated LXFML containing these edits.
[[nodiscard]] Result NormalizePosition(const std::string_view data); [[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
// these are only for the migrations due to a bug in one of the implementations. // these are only for the migrations due to a bug in one of the implementations.
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data); [[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);

View File

@ -48,6 +48,7 @@ void MigrationRunner::RunMigrations() {
bool runSd0Migrations = false; bool runSd0Migrations = false;
bool runNormalizeMigrations = false; bool runNormalizeMigrations = false;
bool runNormalizeAfterFirstPartMigrations = false; bool runNormalizeAfterFirstPartMigrations = false;
bool runBrickBuildsNotOnGrid = false;
for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) {
auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry);
@ -64,6 +65,8 @@ void MigrationRunner::RunMigrations() {
runNormalizeMigrations = true; runNormalizeMigrations = true;
} else if (migration.name.ends_with("_normalize_model_positions_after_first_part.sql")) { } else if (migration.name.ends_with("_normalize_model_positions_after_first_part.sql")) {
runNormalizeAfterFirstPartMigrations = true; runNormalizeAfterFirstPartMigrations = true;
} else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) {
runBrickBuildsNotOnGrid = true;
} else { } else {
finalSQL.append(migration.data.c_str()); finalSQL.append(migration.data.c_str());
} }
@ -71,7 +74,7 @@ void MigrationRunner::RunMigrations() {
Database::Get()->InsertMigration(migration.name); Database::Get()->InsertMigration(migration.name);
} }
if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations && !runNormalizeAfterFirstPartMigrations) { if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations && !runNormalizeAfterFirstPartMigrations && !runBrickBuildsNotOnGrid) {
LOG("Server database is up to date."); LOG("Server database is up to date.");
return; return;
} }
@ -103,6 +106,10 @@ void MigrationRunner::RunMigrations() {
if (runNormalizeAfterFirstPartMigrations) { if (runNormalizeAfterFirstPartMigrations) {
ModelNormalizeMigration::RunAfterFirstPart(); ModelNormalizeMigration::RunAfterFirstPart();
} }
if (runBrickBuildsNotOnGrid) {
ModelNormalizeMigration::RunBrickBuildGrid();
}
} }
void MigrationRunner::RunSQLiteMigrations() { void MigrationRunner::RunSQLiteMigrations() {

View File

@ -48,3 +48,24 @@ void ModelNormalizeMigration::RunAfterFirstPart() {
} }
Database::Get()->SetAutoCommit(oldCommit); Database::Get()->SetAutoCommit(oldCommit);
} }
void ModelNormalizeMigration::RunBrickBuildGrid() {
const auto oldCommit = Database::Get()->GetAutoCommit();
Database::Get()->SetAutoCommit(false);
for (auto& [lxfmlData, id, modelID] : Database::Get()->GetAllUgcModels()) {
const auto model = Database::Get()->GetModel(modelID);
// only BBB models (lot 14) need to have their position fixed from the above blunder
if (model.lot != 14) continue;
Sd0 sd0(lxfmlData);
const auto asStr = sd0.GetAsStringUncompressed();
const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr, model.position);
sd0.FromData(reinterpret_cast<const uint8_t*>(newLxfml.data()), newLxfml.size());
LOG("Updated model %llu to have a center of %f %f %f", modelID, newCenter.x, newCenter.y, newCenter.z);
auto asStream = sd0.GetAsStream();
Database::Get()->UpdateModel(model.id, newCenter, model.rotation, model.behaviors);
Database::Get()->UpdateUgcModelData(id, asStream);
}
Database::Get()->SetAutoCommit(oldCommit);
}

View File

@ -7,6 +7,7 @@
namespace ModelNormalizeMigration { namespace ModelNormalizeMigration {
void Run(); void Run();
void RunAfterFirstPart(); void RunAfterFirstPart();
void RunBrickBuildGrid();
}; };
#endif //!MODELNORMALIZEMIGRATION_H #endif //!MODELNORMALIZEMIGRATION_H

View File

@ -0,0 +1 @@
/* See ModelNormalizeMigration.cpp for details */

View File

@ -0,0 +1 @@
/* See ModelNormalizeMigration.cpp for details */