diff --git a/dCommon/GeneralUtils.h b/dCommon/GeneralUtils.h index b888ddf5..69c5fc44 100644 --- a/dCommon/GeneralUtils.h +++ b/dCommon/GeneralUtils.h @@ -3,6 +3,7 @@ // C++ #include #include +#include #include #include #include @@ -145,7 +146,7 @@ namespace GeneralUtils { template struct overload : Bases... { using is_transparent = void; - using Bases::operator() ... ; + using Bases::operator() ...; }; struct char_pointer_hash { @@ -202,7 +203,7 @@ namespace GeneralUtils { } template - requires(!Numeric) + requires(!Numeric) [[nodiscard]] std::optional TryParse(std::string_view str); #if !(__GNUC__ >= 11 || _MSC_VER >= 1924) @@ -221,7 +222,7 @@ namespace GeneralUtils { */ template [[nodiscard]] std::optional TryParse(std::string_view str) noexcept - try { + try { while (!str.empty() && std::isspace(str.front())) str.remove_prefix(1); size_t parseNum; @@ -323,4 +324,28 @@ namespace GeneralUtils { return GenerateRandomNumber(std::numeric_limits::min(), std::numeric_limits::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 , 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; + } + } } diff --git a/dCommon/Lxfml.cpp b/dCommon/Lxfml.cpp index a244c3ae..e71b2d8e 100644 --- a/dCommon/Lxfml.cpp +++ b/dCommon/Lxfml.cpp @@ -6,7 +6,7 @@ #include -Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) { +Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) { Result toReturn; tinyxml2::XMLDocument doc; 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 highest{ -10'000.0f, -10'000.0f, -10'000.0f }; - // Calculate the lowest and highest points on the entire model - for (const auto& transformation : transformations | std::views::values) { - auto split = GeneralUtils::SplitString(transformation, ','); - if (split.size() < 12) { - LOG("Not enough in the split?"); - continue; + NiPoint3 delta = NiPoint3Constant::ZERO; + if (curPosition == NiPoint3Constant::ZERO) { + // Calculate the lowest and highest points on the entire model + for (const auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value(); + auto y = GeneralUtils::TryParse(split[10]).value(); + auto z = GeneralUtils::TryParse(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(split[9]).value(); - auto y = GeneralUtils::TryParse(split[10]).value(); - auto z = GeneralUtils::TryParse(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; + delta = (highest - lowest) / 2.0f; + } else { + lowest = curPosition; + highest = curPosition; + delta = NiPoint3Constant::ZERO; } - auto delta = (highest - lowest) / 2.0f; 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 newRootPos.y = lowest.y; @@ -78,9 +91,9 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) { continue; } - auto x = GeneralUtils::TryParse(split[9]).value() - newRootPos.x; - auto y = GeneralUtils::TryParse(split[10]).value() - newRootPos.y; - auto z = GeneralUtils::TryParse(split[11]).value() - newRootPos.z; + auto x = GeneralUtils::TryParse(split[9]).value() - newRootPos.x + curPosition.x; + auto y = GeneralUtils::TryParse(split[10]).value() - newRootPos.y + curPosition.y; + auto z = GeneralUtils::TryParse(split[11]).value() - newRootPos.z + curPosition.z; std::stringstream stream; for (int i = 0; i < 9; i++) { stream << split[i]; diff --git a/dCommon/Lxfml.h b/dCommon/Lxfml.h index 5304d8ec..80710713 100644 --- a/dCommon/Lxfml.h +++ b/dCommon/Lxfml.h @@ -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. // 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. [[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data); diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp index ee767ba0..b3310a4f 100644 --- a/dDatabase/MigrationRunner.cpp +++ b/dDatabase/MigrationRunner.cpp @@ -48,6 +48,7 @@ void MigrationRunner::RunMigrations() { bool runSd0Migrations = false; bool runNormalizeMigrations = false; bool runNormalizeAfterFirstPartMigrations = false; + bool runBrickBuildsNotOnGrid = false; for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); @@ -64,6 +65,8 @@ void MigrationRunner::RunMigrations() { runNormalizeMigrations = true; } else if (migration.name.ends_with("_normalize_model_positions_after_first_part.sql")) { runNormalizeAfterFirstPartMigrations = true; + } else if (migration.name.ends_with("_brickbuilds_not_on_grid.sql")) { + runBrickBuildsNotOnGrid = true; } else { finalSQL.append(migration.data.c_str()); } @@ -71,7 +74,7 @@ void MigrationRunner::RunMigrations() { 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."); return; } @@ -103,6 +106,10 @@ void MigrationRunner::RunMigrations() { if (runNormalizeAfterFirstPartMigrations) { ModelNormalizeMigration::RunAfterFirstPart(); } + + if (runBrickBuildsNotOnGrid) { + ModelNormalizeMigration::RunBrickBuildGrid(); + } } void MigrationRunner::RunSQLiteMigrations() { diff --git a/dDatabase/ModelNormalizeMigration.cpp b/dDatabase/ModelNormalizeMigration.cpp index e33e5e4f..b415ef57 100644 --- a/dDatabase/ModelNormalizeMigration.cpp +++ b/dDatabase/ModelNormalizeMigration.cpp @@ -48,3 +48,24 @@ void ModelNormalizeMigration::RunAfterFirstPart() { } 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(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); +} diff --git a/dDatabase/ModelNormalizeMigration.h b/dDatabase/ModelNormalizeMigration.h index 686ca02f..8cbeb871 100644 --- a/dDatabase/ModelNormalizeMigration.h +++ b/dDatabase/ModelNormalizeMigration.h @@ -7,6 +7,7 @@ namespace ModelNormalizeMigration { void Run(); void RunAfterFirstPart(); + void RunBrickBuildGrid(); }; #endif //!MODELNORMALIZEMIGRATION_H diff --git a/migrations/dlu/mysql/22_brickbuilds_not_on_grid.sql b/migrations/dlu/mysql/22_brickbuilds_not_on_grid.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/mysql/22_brickbuilds_not_on_grid.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */ diff --git a/migrations/dlu/sqlite/5_brickbuilds_not_on_grid.sql b/migrations/dlu/sqlite/5_brickbuilds_not_on_grid.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/sqlite/5_brickbuilds_not_on_grid.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */