#1 - quicr module

This commit is contained in:
Martin Slachta
2026-07-22 17:34:44 +02:00
parent a04f0dc262
commit f4174eb0c7
177 changed files with 5309 additions and 2265 deletions
@@ -0,0 +1,122 @@
#include "PlayerReconciler.hpp"
#include "world/JoltPhysicsWorld.hpp"
#include "world/CharacterBody.hpp"
#include "world/Transform.hpp"
#include <spdlog/spdlog.h>
#include <glm/glm.hpp>
#include <glm/gtx/norm.hpp>
namespace tw::net {
PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0)
{
for (auto& record : m_records) {
record.frame = 0;
record.valid = false;
record.input = glm::vec3(0.0f);
record.predicted_position = glm::vec3(0.0f);
}
}
void PlayerReconciler::record_input(uint32_t frame, glm::vec3 input) {
size_t idx = frame % RING_SIZE;
m_records[idx].frame = frame;
m_records[idx].valid = true;
m_records[idx].input = input;
}
void PlayerReconciler::record_prediction(uint32_t frame, glm::vec3 position) {
size_t idx = frame % RING_SIZE;
if (m_records[idx].frame == frame && m_records[idx].valid) {
m_records[idx].predicted_position = position;
}
}
bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
entt::entity player, entt::registry* registry,
uint32_t current_frame)
{
if (ack_frame == 0 || ack_frame <= m_last_reconciled_ack || ack_frame >= current_frame) {
return false;
}
m_last_reconciled_ack = ack_frame;
m_last_ack_frame = ack_frame;
Record& record = m_records[ack_frame % RING_SIZE];
const bool has_prediction = record.valid && record.frame == ack_frame;
// With a prediction to compare against, an answer that already matches costs
// nothing further. This is the case almost every frame.
if (has_prediction) {
float distance = glm::distance(record.predicted_position, authoritative_position);
m_last_correction_distance = distance;
if (distance < kPositionEpsilon) {
return false;
}
}
// Restoring the frame the answer describes keeps everything the simulation
// derived from it, so only the character has to be moved. Without a stored
// frame there is nothing to restore and the answer is taken as it stands.
const bool restored = has_prediction && m_physics->rollback(ack_frame);
place_character(player, registry, authoritative_position, !restored);
replay_from(ack_frame, player, registry, current_frame);
m_last_replayed_frames = current_frame - 1 - ack_frame;
m_rollback_count++;
spdlog::debug("Corrected at frame {}: distance {}, replayed {}, restored {}",
ack_frame, m_last_correction_distance, m_last_replayed_frames, restored);
return true;
}
void PlayerReconciler::place_character(entt::entity player, entt::registry* registry,
glm::vec3 position, bool clear_velocity) {
CharacterBody* body = registry->try_get<CharacterBody>(player);
if (!body) {
return;
}
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
if (clear_velocity) {
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
body->m_desired_velocity = JPH::Vec3::sZero();
}
}
void PlayerReconciler::replay_from(uint32_t from_frame, entt::entity player,
entt::registry* registry, uint32_t current_frame) {
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
Transform* transform = registry->try_get<Transform>(player);
if (!transform) {
continue;
}
// Frames the ring never saw still need an entry, or the answer to them
// arrives with nothing to compare against and forces another correction.
Record& replayed = m_records[f % RING_SIZE];
replayed.frame = f;
replayed.valid = true;
replayed.predicted_position = transform->position();
}
}
void PlayerReconciler::reset_at(uint32_t frame) {
m_last_reconciled_ack = frame;
for (auto& record : m_records) {
record.valid = false;
}
}
}