Compare commits

...

4 Commits

Author SHA1 Message Date
David Markowitz
edda646856 feat: Add Restart Behavior
Resets the model to the default state at the end of the models frame.  Will see if in the future designers want this to be more strict on the resetting timing.
2025-06-28 21:18:44 -07:00
David Markowitz
55d181ea4b fix: lxfml normalization (#1835) 2025-06-27 22:31:48 -07:00
David Markowitz
ecbb465020 fix: get entity from manager vs from character (#1833)
fixes a possible crash due to null entity
2025-06-26 07:04:05 -04:00
David Markowitz
ec9927acbb fix: multiplied speeds with Speed Behaviors (#1832) 2025-06-26 07:03:25 -04:00
14 changed files with 115 additions and 29 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

@@ -167,6 +167,7 @@ Entity::~Entity() {
if (m_Character) { if (m_Character) {
m_Character->SaveXMLToDatabase(); m_Character->SaveXMLToDatabase();
m_Character->SetEntity(nullptr);
} }
CancelAllTimers(); CancelAllTimers();

View File

@@ -526,7 +526,7 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, uint32_t playerID
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
if (character) { if (character) {
auto* entity = character->GetEntity(); auto* entity = Game::entityManager->GetEntity(character->GetObjectID());
if (entity) { if (entity) {
auto* characterComponent = entity->GetComponent<CharacterComponent>(); auto* characterComponent = entity->GetComponent<CharacterComponent>();
if (characterComponent) { if (characterComponent) {

View File

@@ -63,6 +63,12 @@ void ModelComponent::Update(float deltaTime) {
for (auto& behavior : m_Behaviors) { for (auto& behavior : m_Behaviors) {
behavior.Update(deltaTime, *this); behavior.Update(deltaTime, *this);
} }
if (!m_RestartAtEndOfFrame) return;
GameMessages::ResetModelToDefaults reset{};
OnResetModelToDefaults(reset);
m_RestartAtEndOfFrame = false;
} }
void ModelComponent::LoadBehaviors() { void ModelComponent::LoadBehaviors() {
@@ -265,7 +271,7 @@ bool ModelComponent::TrySetVelocity(const NiPoint3& velocity) const {
// If we're currently moving on an axis, prevent the move so only 1 behavior can have control over an axis // If we're currently moving on an axis, prevent the move so only 1 behavior can have control over an axis
if (velocity != NiPoint3Constant::ZERO) { if (velocity != NiPoint3Constant::ZERO) {
const auto [x, y, z] = velocity; const auto [x, y, z] = velocity * m_Speed;
if (x != 0.0f) { if (x != 0.0f) {
if (currentVelocity.x != 0.0f) return false; if (currentVelocity.x != 0.0f) return false;
currentVelocity.x = x; currentVelocity.x = x;
@@ -280,7 +286,6 @@ bool ModelComponent::TrySetVelocity(const NiPoint3& velocity) const {
currentVelocity = velocity; currentVelocity = velocity;
} }
currentVelocity *= m_Speed;
m_Parent->SetVelocity(currentVelocity); m_Parent->SetVelocity(currentVelocity);
return true; return true;
} }

View File

@@ -146,7 +146,11 @@ public:
void OnChatMessageReceived(const std::string& sMessage); void OnChatMessageReceived(const std::string& sMessage);
// Sets the speed of the model
void SetSpeed(const float newSpeed) { m_Speed = newSpeed; } void SetSpeed(const float newSpeed) { m_Speed = newSpeed; }
// Whether or not to restart at the end of the frame
void RestartAtEndOfFrame() { m_RestartAtEndOfFrame = true; }
private: private:
// Loads a behavior from the database. // Loads a behavior from the database.
@@ -190,4 +194,7 @@ private:
// The speed at which this model moves // The speed at which this model moves
float m_Speed{ 3.0f }; float m_Speed{ 3.0f };
// Whether or not to restart at the end of the frame.
bool m_RestartAtEndOfFrame{ false };
}; };

View File

@@ -221,6 +221,8 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
sound.target = modelComponent.GetParent()->GetObjectID(); sound.target = modelComponent.GetParent()->GetObjectID();
sound.soundID = numberAsInt; sound.soundID = numberAsInt;
sound.Send(UNASSIGNED_SYSTEM_ADDRESS); sound.Send(UNASSIGNED_SYSTEM_ADDRESS);
} else if (nextActionType == "Restart") {
modelComponent.RestartAtEndOfFrame();
} }
/* END Action */ /* END Action */
/* BEGIN Gameplay */ /* BEGIN Gameplay */

View File

@@ -65,6 +65,8 @@ private:
// The position of the parent model on the previous frame // The position of the parent model on the previous frame
NiPoint3 m_PreviousFramePosition{}; NiPoint3 m_PreviousFramePosition{};
NiPoint3 m_SavedVelocity{};
}; };
#endif //!__STRIP__H__ #endif //!__STRIP__H__

View File

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

View File

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