Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e43dd45
add interpolation frame check for support ships
notimaginative Mar 20, 2026
9ef38f9
remove dead code for ai modes
notimaginative Mar 20, 2026
37cf972
continue processing ship records when gaps exist
notimaginative Mar 20, 2026
e8c7cbc
fix afterburn hack and document why it's there
notimaginative Mar 20, 2026
7c261e0
make sure to update client info interpolation frame
notimaginative Mar 20, 2026
905995e
fix crash if system_info is null
notimaginative Mar 20, 2026
b478c1b
add safety check for interpolation packet replacement
notimaginative Mar 20, 2026
68a7333
add safety checks and index updates to interpolation packet handling
notimaginative Mar 20, 2026
b88679d
use the correct frame in reinterpolate_previous()
notimaginative Mar 20, 2026
559044f
fix inverted interpolation scale
notimaginative Mar 20, 2026
f02ce5c
initialize _support_comparison_frame
notimaginative Mar 21, 2026
ac329b0
use proper elapsed time for non-homing multi weapons
notimaginative Mar 22, 2026
291dcd9
fix ship whack sync issue in multi
notimaginative Mar 23, 2026
b580ce7
fix log spam erroneously triggered by packet index fixes
notimaginative Mar 24, 2026
d6d8074
comment tweak
notimaginative Mar 24, 2026
1a1271d
try to fix rollback shots
wookieejedi Mar 24, 2026
6c37c51
test target nearest attacker fix
wookieejedi Mar 24, 2026
fe09c7b
more rollback tests
wookieejedi Mar 24, 2026
81c6612
Revert "more rollback tests"
wookieejedi Mar 24, 2026
6fe11f0
Revert "try to fix rollback shots"
wookieejedi Mar 24, 2026
42f88a3
test with no rollback
wookieejedi Mar 24, 2026
7b3b103
Revert "test with no rollback"
wookieejedi Mar 26, 2026
76299d2
try 60 fps
wookieejedi Mar 26, 2026
20eb4aa
try no rollback one more time
wookieejedi Mar 26, 2026
2f8d5df
Revert "try no rollback one more time"
wookieejedi Mar 27, 2026
b1824af
test additive weapon vel
wookieejedi Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions code/network/multi_interpolate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ void interpolation_manager::reassess_packet_index(vec3d* pos, matrix* ori, physi
int current_index = static_cast<int>(_packets.size()) - 2;
int prev_index = static_cast<int>(_packets.size()) - 1;

const bool index_only = ( !pos || !ori || !pip );

// iterate through the packets
for (; current_index > -1; current_index--, prev_index--) {

Expand All @@ -25,7 +27,7 @@ void interpolation_manager::reassess_packet_index(vec3d* pos, matrix* ori, physi
// probably the "hackiest" thing about this. If we were just straight simulating,
// and we now need to go back, pretend that the position we were in *was* our old packet
// and we are now going towards our new packet's physics.
if (_simulation_mode) {
if (_simulation_mode && !index_only) {
replace_packet(prev_index, pos, ori, pip); // TODO, if simulation mode was forced by the collision code, this method regresses a bug where collisions instantly kill
_simulation_mode = false;
}
Expand All @@ -34,9 +36,11 @@ void interpolation_manager::reassess_packet_index(vec3d* pos, matrix* ori, physi
}
}

// if we didn't find indexes then we are overwhelmingly likely to have passed the server somehow
// and we need to make sure that we just straight simulate these ships
_simulation_mode = true;
if ( !index_only ) {
// if we didn't find indexes then we are overwhelmingly likely to have passed the server somehow
// and we need to make sure that we just straight simulate these ships
_simulation_mode = true;
}
}

void interpolate_main_helper(int objnum, vec3d* pos, matrix* ori, physics_info* pip, vec3d* last_pos, matrix* last_orient, vec3d* gravity, bool player_ship)
Expand Down Expand Up @@ -112,7 +116,7 @@ void interpolation_manager::interpolate_main(vec3d* pos, matrix* ori, physics_in
}

// calc what the current timing should be.
float numerator = static_cast<float>(_packets[_upcoming_packet_index].remote_missiontime) - static_cast<float>(Multi_Timing_Info.get_current_time());
float numerator = static_cast<float>(Multi_Timing_Info.get_current_time()) - static_cast<float>(_packets[_prev_packet_index].remote_missiontime);
float denominator = static_cast<float>(_packets[_upcoming_packet_index].remote_missiontime) - static_cast<float>(_packets[_prev_packet_index].remote_missiontime);

// work around for weird situations that might cause NAN (you just never know with multi)
Expand Down Expand Up @@ -162,7 +166,7 @@ void interpolation_manager::interpolate_main(vec3d* pos, matrix* ori, physics_in
void interpolation_manager::reinterpolate_previous(TIMESTAMP stamp, int prev_packet_index, int next_packet_index, vec3d& position, matrix& orientation, vec3d& velocity, vec3d& rotational_velocity)
{
// calc what the timing was previously.
float numerator = static_cast<float>(_packets[next_packet_index].remote_missiontime) - static_cast<float>(stamp.value());
float numerator = static_cast<float>(stamp.value()) - static_cast<float>(_packets[prev_packet_index].remote_missiontime);
float denominator = static_cast<float>(_packets[next_packet_index].remote_missiontime) - static_cast<float>(_packets[prev_packet_index].remote_missiontime);

denominator = (denominator > 0.05f) ? denominator : 0.05f;
Expand All @@ -174,7 +178,7 @@ void interpolation_manager::reinterpolate_previous(TIMESTAMP stamp, int prev_pac

physics_snapshot temp_snapshot;

physics_interpolate_snapshots(temp_snapshot, _packets[_prev_packet_index].snapshot, _packets[_upcoming_packet_index].snapshot, scale);
physics_interpolate_snapshots(temp_snapshot, _packets[prev_packet_index].snapshot, _packets[next_packet_index].snapshot, scale);
physics_apply_snapshot_manual(position, orientation, velocity, rotational_velocity, temp_snapshot);
}

Expand Down Expand Up @@ -213,21 +217,26 @@ void interpolation_manager::add_packet(int objnum, int frame, int packet_timesta
_prev_packet_index = 1;
}

if (static_cast<int>(_packets.size()) > PACKET_INFO_LIMIT) {
if (_packets.size() > PACKET_INFO_LIMIT) {
_packets.pop_back();
}

// whenenver the server gets a player packet, we need to update the ship record, since the old info is now stale
if (Objects[objnum].flags[Object::Object_Flags::Player_ship]){
if ((_upcoming_packet_index >= 0) && (_prev_packet_index >= 0)) {
// update packet indexes (ignoring simulation mode)
// NOTE: indexes must be valid before being reassesed!
reassess_packet_index();

int start_time = Multi_Timing_Info.get_mission_start_time();
// whenenver the server gets a player packet, we need to update the ship record, since the old info is now stale
if (Objects[objnum].flags[Object::Object_Flags::Player_ship]){
int start_time = Multi_Timing_Info.get_mission_start_time();

multi_ship_record_signal_update(objnum, TIMESTAMP(start_time + _packets[_prev_packet_index].remote_missiontime), TIMESTAMP(start_time + _packets[_upcoming_packet_index].remote_missiontime), _prev_packet_index, _upcoming_packet_index);
multi_ship_record_signal_update(objnum, TIMESTAMP(start_time + _packets[_prev_packet_index].remote_missiontime), TIMESTAMP(start_time + _packets[_upcoming_packet_index].remote_missiontime), _prev_packet_index, _upcoming_packet_index);

// if it's not the front packet, we need to update more info past the current packet, as well.
// Should be rare though as it is a contingency for out of order packets.
if (_upcoming_packet_index != 0){
multi_ship_record_signal_update(objnum, TIMESTAMP(start_time + _packets[_upcoming_packet_index].remote_missiontime), TIMESTAMP(start_time + _packets[_upcoming_packet_index - 1].remote_missiontime), _upcoming_packet_index, _upcoming_packet_index - 1);
// if it's not the front packet, we need to update more info past the current packet, as well.
// Should be rare though as it is a contingency for out of order packets.
if (_upcoming_packet_index > 0) {
multi_ship_record_signal_update(objnum, TIMESTAMP(start_time + _packets[_upcoming_packet_index].remote_missiontime), TIMESTAMP(start_time + _packets[_upcoming_packet_index - 1].remote_missiontime), _upcoming_packet_index, _upcoming_packet_index - 1);
}
}
}

Expand All @@ -240,6 +249,11 @@ void interpolation_manager::add_packet(int objnum, int frame, int packet_timesta
// should never replace index 0
void interpolation_manager::replace_packet(int index, vec3d* pos, matrix* orient, physics_info* pip)
{
if (index < 1) {
UNREACHABLE("Invalid replace interpolation packet index! (%d)", index);
return;
}

// the hackiest part of the hack? Setting its frame. Let FSO think that it was basically brand new.
// it needs to handle it this way because otherwise another packet might get placed in front of it,
// and we lose our intended effect of interpolating the simulation error away.
Expand Down
9 changes: 7 additions & 2 deletions code/network/multi_interpolate.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

struct physics_info;

constexpr int PACKET_INFO_LIMIT = 4; // we should never need more than 4 packets to do interpolation. Overwrite the oldest ones if we do.
constexpr size_t PACKET_INFO_LIMIT = 4; // we should never need more than 4 packets to do interpolation. Overwrite the oldest ones if we do.

typedef struct packet_info {

Expand Down Expand Up @@ -42,7 +42,7 @@ class interpolation_manager {
SCP_vector<packet_info> _packets; // all the info from the position/orientation portion of packets that we care to keep
int _source_player_index;

void reassess_packet_index(vec3d* pos, matrix* ori, physics_info* pip); // for finding which packets from within _packets we should use
void reassess_packet_index(vec3d* pos = nullptr, matrix* ori = nullptr, physics_info* pip = nullptr); // for finding which packets from within _packets we should use
void replace_packet(int index, vec3d* pos, matrix* orient, physics_info* pip); // a function that acts as a workaround, when coming out of simulation_mode

// Frame numbers that helps us figure out if we should ignore new information coming from the server because
Expand All @@ -52,6 +52,7 @@ class interpolation_manager {
int _client_info_comparison_frame; // what frame was the last cleint info received?
SCP_vector<std::pair<int,int>> _subsystems_comparison_frame; // what frame was the last subsystem information received? (for each subsystem) First is health, second is animation
int _ai_comparison_frame; // what frame was the last ai information received?
int _support_comparison_frame; // what frame was the last support information received?

public:

Expand Down Expand Up @@ -85,6 +86,7 @@ class interpolation_manager {


int get_ai_comparison_frame() const { return _ai_comparison_frame; }
int get_support_comparison_frame() const { return _support_comparison_frame; }

void set_hull_comparison_frame(int frame) { _hull_comparison_frame = frame; }
void set_shields_comparison_frame(int frame) { _shields_comparison_frame = frame; }
Expand All @@ -105,6 +107,7 @@ class interpolation_manager {
}

void set_ai_comparison_frame(int frame) { _ai_comparison_frame = frame; }
void set_support_comparison_frame(int frame) { _support_comparison_frame = frame; }

void force_interpolation_mode() { _simulation_mode = true; }

Expand Down Expand Up @@ -134,6 +137,7 @@ class interpolation_manager {
}

_ai_comparison_frame = -1;
_support_comparison_frame = -1;
}

void clean_up()
Expand All @@ -155,6 +159,7 @@ class interpolation_manager {
_shields_comparison_frame = -1;
_client_info_comparison_frame = -1;
_ai_comparison_frame = -1;
_support_comparison_frame = -1;
_source_player_index = -1;
}
};
Expand Down
127 changes: 88 additions & 39 deletions code/network/multi_obj.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ struct rollback_unsimulated_shots {
object* shooterp; // pointer to the shooting object (ship)
vec3d pos; // the relative position calculated from the non-homing packet.
matrix orient; // the relative orientation from the non-homing packet.
vec3d velocity; // the shooter's velocity at the time the shot was fired, from the packet itself.
// Used so that additive weapon velocity and converging autoaim lead are computed
// with the exact velocity the client had, not the server's stale OO-recorded value.
bool secondary_shot; // is this a dumbfire missile shot?
int target_objnum; // the shooter's aip->target_objnum at the time the shot packet was received,
// so autoaim can be reactivated correctly during rollback playback.
};

// our main struct for keeping track of all interpolation and oo packet info.
Expand Down Expand Up @@ -139,8 +144,12 @@ struct oo_general_info {

oo_general_info Oo_info;

// flags
bool Afterburn_hack = false; // HACK!!!
// This is part of a fix for AB trails and related model animations not working
// for other ships in multi. The "hack" part is so the fix applies to hosts as well.
// This could possibly be revisited in a future multi bump with a better solution - taylor
// See: https://github.com/scp-fs2open/fs2open.github.com/commit/186bd01
// Or: "mantis bug 895 response" in SCP Internal on HLP forums
static bool Afterburn_hack = false; // HACK!!!

// returns the last frame's index.
int multi_find_prev_frame_idx();
Expand Down Expand Up @@ -606,17 +615,29 @@ void multi_oo_remove_colliders()
}

// This stores the information we got from the client to create later.
void multi_ship_record_add_rollback_shot(object* pobjp, vec3d* pos, matrix* orient, int frame, bool secondary)
void multi_ship_record_add_rollback_shot(object* pobjp, vec3d* pos, matrix* orient, vec3d* velocity, int frame, bool secondary)
{
Oo_info.rollback_mode = true;

rollback_unsimulated_shots new_shot;
new_shot.shooterp = pobjp;
new_shot.pos = *pos;
new_shot.orient = *orient;
new_shot.velocity = *velocity;
new_shot.secondary_shot = secondary;

Oo_info.rollback_shots_to_be_fired[frame].push_back(new_shot);
// Capture the shooter's current target so autoaim can be correctly restored during rollback
// playback. All ship positions are rewound before firing, so the target's position/velocity
// will already be at the historical values; we only need to preserve which object was targeted.
new_shot.target_objnum = -1;
if (pobjp->type == OBJ_SHIP) {
ship* shipp = &Ships[pobjp->instance];
if (shipp->ai_index >= 0) {
new_shot.target_objnum = Ai_info[shipp->ai_index].target_objnum;
}
}

Oo_info.rollback_shots_to_be_fired[frame].push_back(new_shot);
}

// Manage rollback for a frame
Expand All @@ -632,10 +653,9 @@ void multi_ship_record_do_rollback()

// set up all restore points and ship portion of the collision list
for (ship& cur_ship : Ships) {

// once this happens, we've run out of ships.
// skip destroyed ships
if (cur_ship.objnum < 0) {
break;
continue;
}

objp = &Objects[cur_ship.objnum];
Expand Down Expand Up @@ -754,12 +774,41 @@ void multi_oo_fire_rollback_shots(int frame_idx)
rollback_shot.shooterp->pos = rollback_shot.pos;
rollback_shot.shooterp->orient = rollback_shot.orient;

// Override the shooter's velocity with the value from the fire packet. The server's
// frame_info records are updated from OO packets and may be stale by several frames,
// while the packet value is the client's exact velocity at the moment of firing.
// This ensures additive weapon velocity and converging autoaim lead are computed
// identically to what the client did, preventing trajectory divergence.
vec3d saved_velocity = rollback_shot.shooterp->phys_info.vel;
rollback_shot.shooterp->phys_info.vel = rollback_shot.velocity;

// Temporarily restore the target_objnum the shooter had when it fired so that
// autoaim (including converging autoaim) can activate correctly. Ship positions
// have already been rewound by multi_oo_restore_frame, so the target object's
// position and velocity are already at their historical values.
int saved_target_objnum = -1;
ai_info* rollback_aip = nullptr;
if (rollback_shot.target_objnum >= 0 && rollback_shot.shooterp->type == OBJ_SHIP) {
ship* shipp = &Ships[rollback_shot.shooterp->instance];
if (shipp->ai_index >= 0) {
rollback_aip = &Ai_info[shipp->ai_index];
saved_target_objnum = rollback_aip->target_objnum;
rollback_aip->target_objnum = rollback_shot.target_objnum;
}
}

if (rollback_shot.secondary_shot) {
ship_fire_secondary(rollback_shot.shooterp, 1, true);
}
else {
ship_fire_primary(rollback_shot.shooterp, 1, true);
}

if (rollback_aip != nullptr) {
rollback_aip->target_objnum = saved_target_objnum;
}

rollback_shot.shooterp->phys_info.vel = saved_velocity;
}

// add the newly created shots to the collision list.
Expand Down Expand Up @@ -904,10 +953,10 @@ void multi_ship_record_signal_update(int objnum, TIMESTAMP lower_time_limit, TIM
}
}

if (prev_index < 0 || post_index < 0) {
mprintf(("Getting prev_index %d and post_index %d, which is not valid, while trying to update the ship record.\n", prev_index, post_index));
if ((prev_index < 0) || (prev_index == post_index)) {
return;
} else if (prev_index == post_index) {
} else if (post_index < 0) {
mprintf(("Getting prev_index %d and post_index %d, which is not valid, while trying to update the ship record.\n", prev_index, post_index));
return;
}

Expand Down Expand Up @@ -1838,10 +1887,18 @@ int multi_oo_unpack_data(net_player* pl, ubyte* data, int seq_num, int time_delt
// SPECIAL CLIENT INFO
// ---------------------------------------------------------------------------------------------------------------

// make sure the ab hack is reset before we read in new info
Afterburn_hack = false;

// if this is from a player, read his button info
if(MULTIPLAYER_MASTER){
int r0 = multi_oo_unpack_client_data(pl, data + offset, seq_num > Interp_info[objnum].get_client_info_comparison_frame());
offset += r0;

// update comparison frame
if (seq_num > Interp_info[objnum].get_client_info_comparison_frame()) {
Interp_info[objnum].set_client_info_comparison_frame(seq_num);
}
}

// ---------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -2127,15 +2184,6 @@ int multi_oo_unpack_data(net_player* pl, ubyte* data, int seq_num, int time_delt

if( seq_num > Interp_info[objnum].get_ai_comparison_frame() ){
if ( shipp->ai_index >= 0 ){
// make sure to undo the wrap if it occurred during compression for unset ai mode.
if (umode == 255) {
Ai_info[shipp->ai_index].mode = -1;
}
else {
Ai_info[shipp->ai_index].mode = umode;
}
Ai_info[shipp->ai_index].submode = submode;

// set this guy's target objnum, and other info
target_objp = multi_get_network_object( target_signature );

Expand Down Expand Up @@ -2182,29 +2230,30 @@ int multi_oo_unpack_data(net_player* pl, ubyte* data, int seq_num, int time_delt
GET_INT(ai_submode);
GET_USHORT(dock_sig);

// verify that it's a valid ship
if((shipp != nullptr) && (shipp->ai_index >= 0) && (shipp->ai_index < MAX_AI_INFO)){
// bash ai info, this info does not get rebashed, because it is not as vital.
Ai_info[shipp->ai_index].ai_flags.from_u64(ai_flags);
Ai_info[shipp->ai_index].mode = ai_mode;
Ai_info[shipp->ai_index].submode = ai_submode;

object *objp = multi_get_network_object( dock_sig );
if(objp != nullptr){
Ai_info[shipp->ai_index].support_ship_objnum = OBJ_INDEX(objp);
if ((objp->instance > -1) && (objp->type == OBJ_SHIP)) {
Ai_info[shipp->ai_index].goals[0].target_name = Ships[objp->instance].ship_name;
Ai_info[shipp->ai_index].goals[0].target_signature = objp->signature;
} else {
Ai_info[shipp->ai_index].goals[0].target_name = nullptr;
Ai_info[shipp->ai_index].goals[0].target_signature = 0;
if (seq_num > Interp_info[objnum].get_support_comparison_frame()) {
// verify that it's a valid ship
if((shipp != nullptr) && (shipp->ai_index >= 0) && (shipp->ai_index < MAX_AI_INFO)){
// bash ai info, this info does not get rebashed, because it is not as vital.
Ai_info[shipp->ai_index].ai_flags.from_u64(ai_flags);
Ai_info[shipp->ai_index].mode = ai_mode;
Ai_info[shipp->ai_index].submode = ai_submode;

object *objp = multi_get_network_object( dock_sig );
if(objp != nullptr){
Ai_info[shipp->ai_index].support_ship_objnum = OBJ_INDEX(objp);
if ((objp->instance > -1) && (objp->type == OBJ_SHIP)) {
Ai_info[shipp->ai_index].goals[0].target_name = Ships[objp->instance].ship_name;
Ai_info[shipp->ai_index].goals[0].target_signature = objp->signature;
} else {
Ai_info[shipp->ai_index].goals[0].target_name = nullptr;
Ai_info[shipp->ai_index].goals[0].target_signature = 0;
}
}
}
}
}

// make sure the ab hack is reset before we read in new info
Afterburn_hack = false;
Interp_info[objnum].set_support_comparison_frame(seq_num);
}
}

// afterburner info
if ( (oo_flags & OO_AFTERBURNER_NEW) || Afterburn_hack ) {
Expand Down
2 changes: 1 addition & 1 deletion code/network/multi_obj.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ matrix multi_ship_record_lookup_orientation(object* objp, int frame);
int multi_ship_record_find_time_after_frame(int client_frame, int frame, int time_elapsed);

// This stores the information we got from the client to create later.
void multi_ship_record_add_rollback_shot(object* pobjp, vec3d* pos, matrix* orient, int frame, bool secondary);
void multi_ship_record_add_rollback_shot(object* pobjp, vec3d* pos, matrix* orient, vec3d* velocity, int frame, bool secondary);

// Lookup whether rollback mode is on
bool multi_ship_record_get_rollback_wep_mode();
Expand Down
2 changes: 1 addition & 1 deletion code/network/multi_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ typedef struct multi_global_options {
std_voice = 1;
memset(std_passwd, 0, STD_PASSWD_LEN+1);
memset(std_pname, 0, STD_NAME_LEN+1);
std_framecap = 30;
std_framecap = 60;

webapiPort = 8080;
webapiUsername = "admin";
Expand Down
Loading
Loading