OldUnreal / UnrealTournamentPatches

Other
967 stars 29 forks source link

[469a] Dedicated server not correct player rotation when used WarpZones #103

Open SeriousBuggie opened 3 years ago

SeriousBuggie commented 3 years ago

Reproduce:

  1. Start DM-WarpBug as dedicated multiplayer. DM-WarpBug.zip
  2. Connect with UT to this dedicated server.
  3. Try walk on tube from one end to another.

Expected result: No any problem. Both parts of level look like one room. Actual result: On center room happens strange rotation to 90 degree. Actually you warped to second part of room but your rotation not properly corrected.

v469a: bugged. v436: bugged.

SeriousBuggie commented 3 years ago

Possible issue in code of WarpZoneInfo:

// When an actor enters this warp zone.
simulated function ActorEntered( actor Other )
{
    local vector L;
    local rotator R;
    local Pawn P;

    //if ( Other.Role == ROLE_AutonomousProxy )
    //  return; // don't simulate for client players
    Super.ActorEntered( Other );
    if( !Other.bJustTeleported )
    {
        Generate();
        if( OtherSideActor != None )
        {
            // This needs to also perform a coordinate system transformation,
            // in case the portals aren't directionally aligned. This is easy to
            // do but UnrealScript doesn't provide coordinate system operators yet.
            Other.Disable('Touch');
            Other.Disable('UnTouch');

            L = Other.Location;
            if( Other.IsA('PlayerPawn') )
                R = PlayerPawn(Other).ViewRotation;
            else
                R = Other.Rotation;

            UnWarp( L, Other.Velocity, R );
            OtherSideActor.Warp( L, Other.Velocity, R );

            if( Other.IsA('Pawn') )
            {
                Pawn(Other).bWarping = bNoTelefrag;
                if ( Other.SetLocation(L) )
                {
                    //tell enemies about teleport
                    if ( Role == ROLE_Authority )
                    {
                        P = Level.PawnList;
                        While ( P != None )
                        {
                            if (P.Enemy == Other)
                                P.LastSeenPos = Other.Location; 
                            P = P.nextPawn;
                        }
                    }
                    R.Roll = 0;
                    Pawn(Other).ViewRotation = R;
                    Pawn(Other).ClientSetLocation(L, R );
                    Pawn(Other).MoveTimer = -1.0;
                }
                else
                {
                    // set up to keep trying to teleport
                    GotoState('DelayedWarp');
                }
            }
            else
            {
                Other.SetLocation(L);
                Other.SetRotation( R );
            }
            Other.Enable('Touch');
            Other.Enable('UnTouch');
            // Change rotation according to portal's rotational change.
        }
    }
}

Possible need call ClientSetRotation.

SeriousBuggie commented 3 years ago

Find reason of issue:

// When an actor enters this warp zone.
simulated function ActorEntered( actor Other )
{

look at word simulated. It is mean this call run on clients side too. And player rotation is client side things. So server rotate player on 90 degrees and server do that too. So finally player turn additionally on 90 degrees.

If remove simulated on subclass we can get fixed version of WarpZoneInfo. DM-WarpBugFix.zip But this solution (subclass) have some caveats: Original ActorEntered call Super.ActorEntered. But we can not do that. And can not access Super.Super.ActorEntered. So you need reimplement this code, or simply discard this. Each option can have some drawbacks and pitfalls.

And you can not make own WarpZoneInfo because it need some native code and not just simple UScript class.

Of course this bug can be fixed in 469b, but on more old servers and clients it still happens.

Partially solution - leave rotation to client and not rotate from server. It must be good if used v469b server. With any client version.

CacoFFF commented 3 years ago

The ClientSetLocation call completely ruins the client's position (and rotation) as it doesn't apply the movement reconstruction when the client receives it, causing the bigger positional stutter the higher the player's ping. The fix runs serverside and stutters will unfortuntately still be seen on old servers.

SeriousBuggie commented 3 years ago

Bad. Maybe possible add some flags for enable different fixes depends on server version? Or even do it automatically?

CacoFFF commented 3 years ago

Removing simulation is the exact opposite of creating a lag free experience. The client has to simulate the warp, and the server must not call functions that cause position stutters on the client.

This is what currently happens as ping increases: https://www.youtube.com/watch?v=itIvWaDw_oQ

SeriousBuggie commented 3 years ago

Yep. And what your solution now? old servers with new client - hmm - not our problem. new servers with any client - we fix issue all fine.

my suggestion: old servers with new client - we aware of bug and try reduce issue as possible. new servers with any client - like as above fixed in same way so we fine here.

And now tell me what solution better.

Also look like you not understand problem completely. Try use in your tests, portals which rotate in 90 degree or even in 180 degree.

Your ping issue "little problem" in compare with hell what you see when warp zones not faced in same direction. In fact you not able fast use any warp zones which not faced in same direction. In combat it is near impossible at all.

So I suggest make proper fix when server v469 and above and do not use client hack. And use client hack for servers lower then v469.

SeriousBuggie commented 3 years ago

Check fix for this issue. server v469b + client v469b = fine. server v469b + client v436 = fine. server v436 + client v469b = bugged.

As I describe above I prefer full fix. So maybe you add another case when server ver < 469b?

SeriousBuggie commented 3 years ago

Found way for fix server v436 + client v469b. Need change line https://github.com/Slipyx/UT99/blob/f2ebd703845075a2d667e8e3f2f71a5e7f187610/Engine/WarpZoneInfo.uc#L155

                    Pawn(Other).ViewRotation = R;

to

                    if (Level.ServerMoveVersion >= 2) Pawn(Other).ViewRotation = R; // only if server at least v469

After that if server v469b all smooth as before. If server less v469 then you rotation fix by server. This make some lag in ping amount, but it is definitely better from be faced to wall after this ping.

This is best we can do without heavy changes.

In case we make unusable situation acceptable - win win. Do not see any sense tune this stuff for better results.

One case still fail: server between v469 and v469b (exclude) + client v469b = bugged. But i do not think we pay attention on this case. Server must be updated to v469b or later. Strictly speaking I am not sure what happen exaclly on this case. And this things happen now. suggested patch do not change it.

SeriousBuggie commented 3 years ago

Fix not full. Exists race condition which cause v469b server + v469b client make no turn. It happens not 100%. Possible 10% at local dedicated server. Client warped without turn.

SeriousBuggie commented 3 years ago

Bugged case. debug code:

// When an actor enters this warp zone.
simulated function ActorEntered( actor Other )
{
    local vector L;
    local rotator R;
    local Pawn P;

    //if ( Other.Role == ROLE_AutonomousProxy )
    //  return; // don't simulate for client players
    Super(ZoneInfo).ActorEntered( Other );
if( Other.IsA('Pawn') ) Log(Level.TimeSeconds @ "=" @ self @ Other);
    if ( (Level.NetMode == NM_Client) && !SimulateWarp_(Other) )
        return;

    if( !Other.bJustTeleported )
    {
if( Other.IsA('Pawn') ) Log(Level.TimeSeconds @ ">" @ self @ Other @ Other.Location @ Other.Rotation @ Pawn(Other).ViewRotation @ ">");
        Generate();
        if( OtherSideActor != None )
        {
            // This needs to also perform a coordinate system transformation,
            // in case the portals aren't directionally aligned. This is easy to
            // do but UnrealScript doesn't provide coordinate system operators yet.
            Other.Disable('Touch');
            Other.Disable('UnTouch');

            L = Other.Location;
            if( Other.IsA('PlayerPawn') )
                R = PlayerPawn(Other).ViewRotation;
            else
                R = Other.Rotation;

            UnWarp( L, Other.Velocity, R );
            OtherSideActor.Warp( L, Other.Velocity, R );

            if( Other.IsA('Pawn') )
            {
                Pawn(Other).bWarping = bNoTelefrag;
                if ( Other.SetLocation(L) )
                {
                    //tell enemies about teleport
                    if ( Role == ROLE_Authority )
                    {
                        P = Level.PawnList;
                        While ( P != None )
                        {
                            if (P.Enemy == Other)
                                P.LastSeenPos = Other.Location; 
                            P = P.nextPawn;
                        }
                    }
                    R.Roll = 0;
                    Pawn(Other).ViewRotation = R;
//                  Pawn(Other).ClientSetLocation(L, R );
//                  Pawn(Other).ClientSetRotation(R);
                    Pawn(Other).MoveTimer = -1.0;
                }
                else
                {
                    // set up to keep trying to teleport
                    GotoState('DelayedWarp');
                }
            }
            else
            {
                Other.SetLocation(L);
                Other.SetRotation( R );
            }
            Other.Enable('Touch');
            Other.Enable('UnTouch');
            // Change rotation according to portal's rotational change.
        }
if( Other.IsA('Pawn') ) Log(Level.TimeSeconds @ "<" @ self @ Other @ Other.Location @ Other.Rotation @ Pawn(Other).ViewRotation @ "<");
    }
}
static function bool SimulateWarp_( Actor Other)
{
    if ( Other.Role == ROLE_DumbProxy ) //Location is updated by server
        return false;

    if ( (PlayerPawn(Other) != None) && (Other.Role == ROLE_AutonomousProxy) ) //Local Player (Viewport may be detached during DemoPlay!!)
        return Other.bCanTeleport;

    return Other.Physics != PHYS_None;
}

We try transfer from MyWarpZoneInfo51 to MyWarpZoneInfo50. Server:

ScriptLog: 92.191444 = DM-!!!ripper_warp_bug.MyWarpZoneInfo51 DM-!!!ripper_warp_bug.TFemale10
ScriptLog: 92.191444 > DM-!!!ripper_warp_bug.MyWarpZoneInfo51 DM-!!!ripper_warp_bug.TFemale10 -255.716675,-13.577606,-84.299995 63944,22736,0 63944,22736,0 >
ScriptLog: 92.191444 < DM-!!!ripper_warp_bug.MyWarpZoneInfo51 DM-!!!ripper_warp_bug.TFemale10 141.577606,256.283325,-84.299995 63944,22736,0 63945,39117,0 <

All fine here. Client:

ScriptLog: 78.780945 = DM-!!!ripper_warp_bug.MyWarpZoneInfo51 DM-!!!ripper_warp_bug.TFemale10
ScriptLog: 78.780945 > DM-!!!ripper_warp_bug.MyWarpZoneInfo51 DM-!!!ripper_warp_bug.TFemale10 -255.717087,-13.577516,-84.299995 63946,6355,1 63946,6355,0 >
ScriptLog: 78.780945 < DM-!!!ripper_warp_bug.MyWarpZoneInfo51 DM-!!!ripper_warp_bug.TFemale10 141.577515,256.282898,-84.299995 63946,6355,1 63944,22736,0 <

ScriptLog: 78.840584 = DM-!!!ripper_warp_bug.MyWarpZoneInfo50 DM-!!!ripper_warp_bug.TFemale10
ScriptLog: 78.840584 > DM-!!!ripper_warp_bug.MyWarpZoneInfo50 DM-!!!ripper_warp_bug.TFemale10 143.726532,255.886826,-84.299995 63944,22736,1 63944,22736,0 >
ScriptLog: 78.840584 < DM-!!!ripper_warp_bug.MyWarpZoneInfo50 DM-!!!ripper_warp_bug.TFemale10 -256.113159,-15.726532,-84.299995 63944,22736,1 63944,6352,0 <

Here is problem. 1 part is fine. 2 part must not happen. On 2 part we on next tick trigger destination zone which locally produce turn back so our turn vanished.

So this is not race condition. It is more like edge problem. When actor balancing on edge of warp zone it can trigger destination zone.

SeriousBuggie commented 3 years ago

Also because of #486 Can be additional desync for rotation. Simple example: Actor block destination warpzone, but actor not relevant, so locally not exists. Locally SetLocation is fine. Rotation chnaged. On server setlocation failed, Rotation not changed. Server correct player location to Warp zone buffer. Player see in wrong direction (like must be when pass warp, but he not pass warp).

SeriousBuggie commented 3 years ago

Main problem how handle local work for ROLE_AutonomousProxy. All others is fine.

I see next possible solutions:

  1. with local prediction.

Locally: On ActorEntered we make SetLocation. If result good we correct ViewRotation. ViewRotation blocked for 150 ms for next correction for this Actor + this WarpZoneInfo or destination WarpZoneInfo.

Result: All work fine and smooth, except case when collide occurs and we get delayed warp.

  1. without local prediction but with use buffer zone.

Locally: We not make any prediction for ActorEntered. We just allow actor travel into buffer zone, until server not correct player position. On ActorLeaved we make ViewRotation correction which blocked for next 150 ms for this Actor + this WarpZoneInfo or destination WarpZoneInfo.

Result: All work fine and smooth, even in case when collide occurs and we get delayed warp. Pitfall exists in replay saved move. After we collect rotation, some old replayed moves can be applied. This moves not translate with warp system and use wrong direction. As result possible some Jitter or move in wrong direction. For example if warp turn right, and we move strictly forward into it, we can get some movement to left, after warped.

Possible this can be fixed by translate saved movement.

Also possible exists problems on determine related actors for player which stay near portal. If client locally not know this info, 1 approach very fast goes to fail, because highly related on local prediction and for this must operate full information for get same result as on server.

SeriousBuggie commented 3 years ago

Current attempt resolve this and #486 issue via custom actor:

//=============================================================================
// MyWarpZoneInfo.
//=============================================================================
class MyWarpZoneInfo expands WarpZoneInfo;

// When an actor enters this warp zone.
simulated function ActorEntered( actor Other )
{
    local vector L;
    local rotator R;
    local Pawn P;
    local Effects Temp;

    Super(ZoneInfo).ActorEntered( Other );

    if ( (Level.NetMode == NM_Client) && !SimulateWarp_(Other) )
        return;

    if( !Other.bJustTeleported )
    {
        Generate();
        if( OtherSideActor != None )
        {
            // This needs to also perform a coordinate system transformation,
            // in case the portals aren't directionally aligned. This is easy to
            // do but UnrealScript doesn't provide coordinate system operators yet.
            Other.Disable('Touch');
            Other.Disable('UnTouch');

            L = Other.Location;
            if( Other.IsA('PlayerPawn') )
                R = PlayerPawn(Other).ViewRotation;
            else
                R = Other.Rotation;

            UnWarp( L, Other.Velocity, R );
            OtherSideActor.Warp( L, Other.Velocity, R );

            if( Other.IsA('Pawn') )
            {
                if (Other.Role != ROLE_AutonomousProxy)
                {
                    Pawn(Other).bWarping = bNoTelefrag;
                    if ( Other.SetLocation(L) )
                    {
                        //tell enemies about teleport
                        if ( Role == ROLE_Authority )
                        {
                            P = Level.PawnList;
                            While ( P != None )
                            {
                                if (P.Enemy == Other)
                                    P.LastSeenPos = Other.Location; 
                                P = P.nextPawn;
                            }
                        }
                        R.Roll = 0;
                        Pawn(Other).ViewRotation = R;
                        Pawn(Other).MoveTimer = -1.0;
                    }
                    else
                    {
                        // set up to keep trying to teleport
                        GotoState('DelayedWarp');
                    }
                }
            }
            else
            {
                Other.SetLocation(L);
                Other.SetRotation( R );
            }
            Other.Enable('Touch');
            Other.Enable('UnTouch');
            // Change rotation according to portal's rotational change.
        }
    }
}

simulated event ActorLeaving( actor Other )
{
    local vector L;
    local rotator R;
    local Effects Temp;

    Super(ZoneInfo).ActorLeaving(Other);
    If ( Other.IsA('Pawn') )
    {
        Pawn(Other).bWarping = false;
        // fix rotation for local player
        if (OtherSideActor != None && Other.Role == ROLE_AutonomousProxy)
        {
            L = Other.Location;
            R = PlayerPawn(Other).ViewRotation;

            UnWarp( L, Other.Velocity, R );
            OtherSideActor.Warp( L, Other.Velocity, R );

            R.Roll = 0;
            if (Other.Role == ROLE_AutonomousProxy)
            {
                foreach AllActors(class'Effects', Temp, Other.Name)
                    if ((Temp.Owner == self || Temp.Owner == OtherSideActor) && Level.TimeSeconds - Temp.Mass < 0.15)
                    {
                        Temp.Mass = Level.TimeSeconds;
                        Temp.LifeSpan += 1;
                        R = Pawn(Other).ViewRotation;
                    }
                if (R != Pawn(Other).ViewRotation)
                {
                    Temp = Spawn(class'Effects', self, Other.Name, L);
                    if (Temp != None)
                    {
                        Temp.LifeSpan = 1;
                        Temp.Mass = Level.TimeSeconds;
                        if (Temp.Region.Zone == OtherSideActor || VSize(Temp.Location - Other.Location) < 1)
                            R = Pawn(Other).ViewRotation;
                    }
                }
            }

            Pawn(Other).ViewRotation = R;
        }
    }
}

static function bool SimulateWarp_( Actor Other)
{
    if ( Other.Role == ROLE_DumbProxy ) //Location is updated by server
        return false;

    if ( (PlayerPawn(Other) != None) && (Other.Role == ROLE_AutonomousProxy) ) //Local Player (Viewport may be detached during DemoPlay!!)
        return Other.bCanTeleport;

    return Other.Physics != PHYS_None;
}

defaultproperties
{
    bStatic=False
}

Actor intended use with v436 so used name SimulateWarp_. class'Effects' with Tag = Other.Name used as temp storage for make all stuff in one class. Possible better add field array with near 32 Effects and iterate over it instead of call iterator AllActors, but attempt create this field crash editor: #475

SeriousBuggie commented 3 years ago

Well. As I found now, problem can not be solved fully and properly with modify only one actor.

  1. We cannot emulate all stuff locally just because client not have all necessary information for say warp happen or not.
  2. We can not be sure of even proper sequence ActorEntered/ActorLeaved. In some cases I see 3 pair of client ActorEntered/ActorLeaved for one on server.
SeriousBuggie commented 3 years ago

Also current fix not fix nothing. Even v469b client on v469b server can suffer from wrong rotation. I start v469b server with -pktlag=300, enter with v469b client and v436 client and record video. Tested map: DM-!!!warp_orig.zip Layout of map: img img Video of test:

https://user-images.githubusercontent.com/70026933/126771073-562ab413-80cc-471f-abb5-2d95a4a7932d.mp4

There we can see few problems.

  1. If pending move goes on special direction even v469b+v469b make wrong direction.
  2. If someone push v469b client it warped (sometimes) with wrong direction.
  3. v436 not work at all. total mess. In all cases.
SeriousBuggie commented 3 years ago

Possible fine enough solution via custom actor:

simulated function ActorEntered( actor Other )
{
    local vector L, V;
    local rotator R;
    local Pawn P;
    local Effects Temp;

    Super(ZoneInfo).ActorEntered( Other );

    if ( (Level.NetMode == NM_Client) && !SimulateWarp_(Other) )
        return;

    if( !Other.bJustTeleported ) 
    {
        Generate();
        if( OtherSideActor != None )
        {
            // This needs to also perform a coordinate system transformation,
            // in case the portals aren't directionally aligned. This is easy to
            // do but UnrealScript doesn't provide coordinate system operators yet.
            Other.Disable('Touch');
            Other.Disable('UnTouch');

            L = Other.Location;
            V = Other.Velocity;
            if( Other.IsA('PlayerPawn') )
                R = PlayerPawn(Other).ViewRotation;
            else
                R = Other.Rotation;

            UnWarp( L, V, R );
            OtherSideActor.Warp( L, V, R );

            if( Other.IsA('Pawn') )
            {
                if (Other.Role != ROLE_AutonomousProxy)
                {
                    Pawn(Other).bWarping = bNoTelefrag;
                    if ( Other.SetLocation(L) )
                    {
                        //tell enemies about teleport
                        if ( Role == ROLE_Authority )
                        {
                            P = Level.PawnList;
                            While ( P != None )
                            {
                                if (P.Enemy == Other)
                                    P.LastSeenPos = Other.Location; 
                                P = P.nextPawn;
                            }
                        }
                        R.Roll = 0;
                        if (PlayerPawn(Other) != None && Pawn(Other).ViewRotation != R)
                            PlayerPawn(Other).ClientSetRotation(R);
                        Pawn(Other).ViewRotation = R;
                        Pawn(Other).MoveTimer = -1.0;
                        Other.Velocity = V;                 
                    }
                    else
                    {
                        // set up to keep trying to teleport
                        GotoState('DelayedWarp');
                    }
                }
            }
            else
            {
                Other.SetLocation(L);
                Other.SetRotation( R );
                Other.Velocity = V;
            }
            Other.Enable('Touch');
            Other.Enable('UnTouch');
            // Change rotation according to portal's rotational change.
        }
    }
}

In short we not simulate warp for ROLE_AutonomousProxy (not checked for demo play - possible there be pitfalls.). Later on success warp on server we correct player rotation by ClientSetRotation if this correction need.

But this good only as custom actor, not as fix base (v436 still use old version). And there exists problem of correct player rotation from server, so we lost some player rotation if it can be.

I think this is best which can be done for work only with WarpZone actor.

SeriousBuggie commented 3 years ago

custom actor with helper for correct rotation relative to current local rotation.

simulated function ActorEntered( actor Other )
{
    local vector L, V;
    local rotator R;
    local Pawn P;
    local MyWarpInfo Temp;

    Super(ZoneInfo).ActorEntered( Other );

    if ( (Level.NetMode == NM_Client) && !SimulateWarp_(Other) )
        return;

    if( !Other.bJustTeleported ) 
    {
        Generate();
        if( OtherSideActor != None )
        {
            // This needs to also perform a coordinate system transformation,
            // in case the portals aren't directionally aligned. This is easy to
            // do but UnrealScript doesn't provide coordinate system operators yet.
            Other.Disable('Touch');
            Other.Disable('UnTouch');

            L = Other.Location;
            V = Other.Velocity;
            if( Other.IsA('PlayerPawn') )
                R = PlayerPawn(Other).ViewRotation;
            else
                R = Other.Rotation;

            UnWarp( L, V, R );
            OtherSideActor.Warp( L, V, R );

            if( Other.IsA('Pawn') )
            {
                if (Other.Role != ROLE_AutonomousProxy)
                {
                    Pawn(Other).bWarping = bNoTelefrag;
                    if ( Other.SetLocation(L) )
                    {
                        //tell enemies about teleport
                        if ( Role == ROLE_Authority )
                        {
                            P = Level.PawnList;
                            While ( P != None )
                            {
                                if (P.Enemy == Other)
                                    P.LastSeenPos = Other.Location; 
                                P = P.nextPawn;
                            }
                        }
                        R.Roll = 0;
                        if (Other.RemoteRole == ROLE_AutonomousProxy && PlayerPawn(Other) != None && Pawn(Other).ViewRotation != R)
                        {
                            Temp = Spawn(class'MyWarpInfo', Other, Other.Name, L, R - Pawn(Other).ViewRotation);
                            if (Temp == None)
                                PlayerPawn(Other).ClientSetRotation(R);
                        }
                        Pawn(Other).ViewRotation = R;
                        Pawn(Other).MoveTimer = -1.0;
                        Other.Velocity = V;                 
                    }
                    else
                    {
                        // set up to keep trying to teleport
                        GotoState('DelayedWarp');
                    }
                }
            }
            else
            {
                Other.SetLocation(L);
                Other.SetRotation( R );
                Other.Velocity = V;
            }
            Other.Enable('Touch');
            Other.Enable('UnTouch');
            // Change rotation according to portal's rotational change.
        }
    }
}
class MyWarpInfo expands Info;

simulated function PostNetBeginPlay()
{
    Super.PostNetBeginPlay();

    if (PlayerPawn(Owner) == None)
        return;
    PlayerPawn(Owner).ViewRotation += Rotation;
}

defaultproperties
{
    bAlwaysRelevant=True
    bNetTemporary=True
    LifeSpan=2.000000
    DrawType=DT_Mesh
    NetPriority=100.000000
    NetUpdateFrequency=1000.000000
}

DrawType=DT_Mesh need for initial replicate Rotation. NetPriority can be 4 or more. NetUpdateFrequency can be 101 or more.

Result is very good even at ping 300. I think it is best what can do.

But integrate this stuff in base WarpZoneInfo can be a problem because of compatibility stuff.