• Please make sure you are familiar with the forum rules. You can find them here: https://forums.tripwireinteractive.com/index.php?threads/forum-rules.2334636/

PC Custom game mode kicks players on map change

Kavoh

Member
Dec 19, 2013
6
3
*CAN IGNORE MOST OF THIS/SKIP TO 2ND POST*

Category
: Code.

Reproducibility: Always.

Online/Offline: Online for sure, offline unknown.

Summary: If a server is using a custom game mode (GameInfo mod), players will be kicked/dropped from the server when a map change occurs. They have to manually reconnect to the server to rejoin.

Description: I believe I also found the code that causes this bug. In the KFGame\Classes\KFGameInfo.uc class, in the SetGameType function:
Code:
/**
  * Allows overriding of which gameinfo class to use.
  * Called on the DefaultGameType from the ini, or the one specified on the command line (?game=xxx)
  */
static event class<GameInfo> SetGameType(string MapName, string Options, string Portal)
{
    local string ThisMapPrefix;
    local int i;
    local class<GameInfo> NewGameType;
    local string GameOption;

    // if we're in the menu level, use the menu gametype
    if ( class'WorldInfo'.static.IsMenuLevel(MapName) )
    {
        return class'KFGameInfo_Entry';
    }

    // allow commandline to override game type setting
    GameOption = ParseOption( Options, "Game");
    if ( GameOption != "" )
    {
        return Default.class;
    }

    // strip the UEDPIE_ from the filename, if it exists (meaning this is a Play in Editor game)
    MapName = StripPlayOnPrefix( MapName );
    ThisMapPrefix = left(MapName, InStr(MapName,"-"));

    // change game type
    for ( i=0; i < Default.DefaultMapPrefixes.Length; i++ )
    {
        if ( Default.DefaultMapPrefixes[i].Prefix ~= ThisMapPrefix )
        {
            NewGameType = class<GameInfo>(DynamicLoadObject(Default.DefaultMapPrefixes[i].GameType,class'Class'));
            if ( NewGameType != None )
            {
                return NewGameType;
            }
        }
    }
    for ( i = 0; i < Default.CustomMapPrefixes.Length; i++ )
    {
        if ( Default.CustomMapPrefixes[i].Prefix ~= ThisMapPrefix )
        {
            NewGameType = class<GameInfo>(DynamicLoadObject(Default.CustomMapPrefixes[i].GameType,class'Class'));
            if ( NewGameType != None )
            {
                return NewGameType;
            }
        }
    }

    return Default.class;
}

I believe this piece of code causes it:
Code:
// allow commandline to override game type setting
GameOption = ParseOption( Options, "Game");
if ( GameOption != "" )
{
    return Default.class;
}

As you can see it reads the custom game class, but then uses the default class instead of the class on the commandline (unless I'm crazy lol). I'm guessing this mismatch between the client and server causes them to get dropped and have to reconnect. This could be tested and fixed through custom game mode overrides of this function (I think?). I'm working on a large mod and in a few days I plan on testing the recent changes I've made to it. I'll try changing that code too and seeing if it gets fixed when I test my mod in a few days, and reply back. Hopefully a dev could let us know if that is the problem, and fix it in the game ;).

Also, this is the bug mentioned [here] too. Although this is the actual bug (I believe) for that problem. They mentioned a fix but it appears to just work around the actual problem.
 
Last edited:
  • Like
Reactions: Pharrahnox
Ok, so after digging further it turns out this technically isn't a bug. The game is doing exactly what it should be, although the process of adding a new game mode is undocumented and semi-confusing, leading to the disconnect issue. The issue is in the KFGameInfo class, in the PreLogin event/function:

Code:
event PreLogin(string Options, string Address, const UniqueNetId UniqueId, bool bSupportsAuth, out string ErrorMessage)
{
    local bool bSpectator;
    local bool bPerfTesting;
    local string DesiredDifficulty, DesiredWaveLength, DesiredGameMode;

    // Check for an arbitrated match in progress and kick if needed
    if (WorldInfo.NetMode != NM_Standalone && bUsingArbitration && bHasArbitratedHandshakeBegun)
    {
        ErrorMessage = PathName(WorldInfo.Game.GameMessageClass) $ ".ArbitrationMessage";
        return;
    }

    // If this player is banned, reject him
    if (AccessControl != none && AccessControl.IsIDBanned(UniqueId))
    {
        LogInternal(Address@"is banned, rejecting...");
        ErrorMessage = "<Strings:KFGame.KFLocalMessage.BannedFromServerString>";
        return;
    }

    // Check against what is expected from the client in the case of quick join/server browser. The server settings can change from the time the server gets the properties from the backend
    if( WorldInfo.NetMode == NM_DedicatedServer && !HasOption( Options, "bJoinViaInvite" ) )
    {
        DesiredDifficulty = ParseOption( Options, "Difficulty" );
        if( DesiredDifficulty != "" && int(DesiredDifficulty) != GameDifficulty )
        {
            LogInternal("Got bad difficulty"@DesiredDifficulty@"expected"@GameDifficulty);
            ErrorMessage = "<Strings:KFGame.KFLocalMessage.ServerNoLongerAvailableString>";
            return;
        }

        DesiredWaveLength = ParseOption( Options, "GameLength" );
        if( DesiredWaveLength != "" && int(DesiredWaveLength) != GameLength )
        {
            LogInternal("Got bad wave length"@DesiredWaveLength@"expected"@GameLength);
            ErrorMessage = "<Strings:KFGame.KFLocalMessage.ServerNoLongerAvailableString>";
            return;
        }

        DesiredGameMode = ParseOption( Options, "Game" );
        if( DesiredGameMode != "" && !(DesiredGameMode ~= GetFullGameModePath()) )
        {
            LogInternal("Got bad wave length"@DesiredGameMode@"expected"@GetFullGameModePath());
            ErrorMessage = "<Strings:KFGame.KFLocalMessage.ServerNoLongerAvailableString>";
            return;
        }
    }


    bPerfTesting = ( ParseOption( Options, "AutomatedPerfTesting" ) ~= "1" );
    bSpectator = bPerfTesting || ( ParseOption( Options, "SpectatorOnly" ) ~= "1" ) || ( ParseOption( Options, "CauseEvent" ) ~= "FlyThrough" );

    if (AccessControl != None)
    {
        AccessControl.PreLogin(Options, Address, UniqueId, bSupportsAuth, ErrorMessage, bSpectator);
    }
}

The issue is when it checks the desired game mode:

Code:
DesiredGameMode = ParseOption( Options, "Game" );
if( DesiredGameMode != "" && !(DesiredGameMode ~= GetFullGameModePath()) )
{
    LogInternal("Got bad wave length"@DesiredGameMode@"expected"@GetFullGameModePath());
    ErrorMessage = "<Strings:KFGame.KFLocalMessage.ServerNoLongerAvailableString>";
    return;
}

GetFullGameModePath() calls GetGameModeNum(), which calls GetGameModeNumFromClass(PathName(default.class)):

Code:
static function int GetGameModeNumFromClass(string GameModeClassString)
{
    return default.GameModes.Find('ClassNameAndPath', GameModeClassString);
}

When this happens, the game tries to look up the custom game mode in the GameModes array, which most servers probably won't realize to set. Since it isn't set, it can't find the correct game mode. This goes back to the PreLogin after not finding the correct game mode, and kicks the player. So technically it is working as intended (code wise) I guess. This is something a server admin probably wouldn't realize to change without documentation.

On my server I added my custom game mode to the GameModes array, and also changed the DefaultGameType to the custom game mode. I also went ahead and updated the original SetGameType function that I thought was bugged:

Code:
static event class<GameInfo> SetGameType(string MapName, string Options, string Portal)
{
    local string GameOption;
    local class<GameInfo> NewGameType;

    GameOption = ParseOption(Options, "Game");

    if (GameOption != "")
    {
        NewGameType = class<GameInfo>(DynamicLoadObject(GameOption, class'Class'));

        if (NewGameType != none)
        {
            return NewGameType;
        }
    }

    NewGameType = class<GameInfo>(DynamicLoadObject(Default.DefaultGameType, class'Class'));

    if (NewGameType != none)
    {
        return NewGameType;
    }

    return Default.class;
}

The GameModes array and DefaultGameType can be found in the PCServer-KFGame.ini if your custom game mode doesn't set a new config ini. If you set a new config ini for your custom game mode, it will be in that ini instead. It will be under the [YourPackage.YourGameMode] section.

Code:
GameModes=(FriendlyName="MyMode",ClassNameAndPath="YourPackage.YourGameMode",bSoloPlaySupported=true,DifficultyLevels=4,Lengths=3,LocalizeID=0)
DefaultGameType=YourPackage.YourGameMode

After overriding the SetGameType function and updating my server config, the server no longer kicked players during map change, and everything appeared to work as intended.

Credits to notblackout and Pharranox for pointing to PreLogin.
 
Last edited:
  • Like
Reactions: Pharrahnox
Upvote 0