jacob404 / promod-future

GNU General Public License v3.0
7 stars 1 forks source link

Spawn cycle should be static / predictable #223

Open HighCookie opened 8 years ago

HighCookie commented 8 years ago

Just played a game with 13% DK3 tank had 2:2 with no charger as first set, entire set died. on tank, still didn't have charger, sacked 2 more players, didnt get charger. after tank still didnt get charger with the set after tank. That's 10 spawns including 3 while tank was up so the set of possible infected was less and we didn't get a charger. Second team got charger on tank.

The spawning should follow 2 simple rules

  1. Minimum 1 support.
  2. First in first out unless it breaks rule one.
jbzdarkid commented 8 years ago

@Attano claims he knows how the spawn rotations work, via decompile and testing. He should be able to explain how it currently works, and we can redesign from there?

MattNF commented 8 years ago

I've had something similar happen. Back when spit did damage in the hard rain elevators, on map 3, we had a tricap with boomer first hit. Sacced boomer immediately, got another one in the field, sacced it too. Then we got a THIRD boomer, and still had no spitter. During that time if you didn't get an elevator hit the other team basically won the map (and possibly the game). We finally got a spitter after that, but it was pointless by then.

People have said that there's no RNG in the spawns, but I feel like there definitely is a small amount, or perhaps there is some other type of fuckery going on.

Attano commented 8 years ago

@Attano knows everything.

The following is a piece of reversed portion of code taken directly from CTerrorPlayer::Spawn(), the method that is called whenever a survivor or an SI needs to spawn(NOT materialize from ghost state, in SIs case).

You can clearly see that a lot of shit is going on before actually calling CTerrorPlayer::SetClass(), which is what everyone is trying to do and hopes he can get away with it.

if ( player->GetTeamNumber() == TEAM_INFECTED )
{
    if ( player->m_fLastKilledTime == 0.0 )
    {
        timeSinceLastKilled = 0.0;
    }
    else
    {
        ++*(player + 11640); // total number of spawns?
        timeSinceLastKilled = gpGlobals->GetCurrentGameTime() - player->m_fLastKilledTime;

        if ( timeSinceLastKilled < *(player + 11628) ) // another time of death related prop
        {
            *(player + 11628) = timeSinceLastKilled;
        }

        if ( timeSinceLastKilled > *(player + 11632) ) // have another one, just in case
        {
            *(player + 11632) = timeSinceLastKilled;
        }

        if ( player->m_totalDeadDuration != (timeSinceLastKilled + player->m_totalDeadDuration) )
        {
            player->m_totalDeadDuration = timeSinceLastKilled + player->m_totalDeadDuration;
        }
        player->m_fLastKilledTime = 0;
    }

    // g_ZombieClass is a global static variable that holds the last established/spawned zombieclass across all players
    establishedClass = g_ZombieClass;
    if ( CTerrorGameRules::HasPlayerControlledZombies() ) // check if this gamemode has player-controlled SI, i.e. is it versus or a mutation of sorts
    {
        if ( player->IsBot() || *(player + 16385) /* this looks like a L4D1 legacy prop since it isn't changed in many L4D2 functions */ )
        {
            establishedClass = g_ZombieClass;
        }
        else
        {
            if ( g_ZombieClass == ZOMBIECLASS_TANK )
            {
                establishedClass = ZOMBIECLASS_TANK;
            }
            else
            {
                // probably some retarded compiler optimization
                if ( player->m_zombieClass )
                {
                    player->m_zombieClass = 0;
                }

                defaultClass = ZOMBIECLASS_HUNTER; // the very default SI class
                                                   // for those cases when shit hits the fan
                                                   // now u know why the game, in extremely rare cases, gives out 2 hunters
                maxSpecialClasses = 99999;
                v167 = *(TheDirector + 1080);      // this would make sense if it were to equal 2
                currentClassIndex = v167 - 1;
                maxClassIndex = v167 + 5;
                do
                {
                    while ( true )
                    {
                        classArrayIndex = currentClassIndex % 6 + 1;
                        currentClassSpawnCount = player->m_classSpawnCount[classArrayIndex];
                        if ( currentClassSpawnCount < maxSpecialClasses )
                            break;

                        ++currentClassIndex;

                        if ( currentClassIndex == maxClassIndex )
                            goto LABEL_618_AKA_FUCK_THIS_DO_WHILE;
                    }
                    isClassOverlimit = player->IsClassOverLimit(currentClassIndex % 6 + 1); // http://www.l4dnation.com/confogl-and-other-configs/dominators-control-or-l4d1-style-config/ 
                    if ( isClassOverlimit ) 
                    {
                        currentClassSpawnCount = maxSpecialClasses;
                        classArrayIndex = defaultClass;
                    }

                    ++currentClassIndex;
                    maxSpecialClasses = currentClassSpawnCount;
                    defaultClass = classArrayIndex;
                }
                while ( currentClassIndex != maxClassIndex );

                LABEL_618_AKA_FUCK_THIS_DO_WHILE:
                g_ZombieClass = defaultClass;
                establishedClass = TheDirector->CDirectorChallengeMode()->ConvertZombieClass(defaultClass);
                g_ZombieClass = establishedClass;
            }
        }
    }
    player->SetClass(establishedClass);
    v69 = 4;
    v71_nonptr = player->m_classSpawnCount[g_ZombieClass];
    v72_ptr = player->m_classSpawnCount[g_ZombieClass];
    v73 = &v196;
    v74 = v71 == -1;
    v190 = v71 + 1;
    v196 = v71 + 1;
    do
    {
        if ( !v69 )
            break;
        v74 = *v72++ == *v73;
        v73 = (v73 + 1);
        --v69;
    }
    while ( v74 );
    if ( !v74 )
    {
        player->m_classSpawnCount[g_ZombieClass] = v190;
    }
    if ( !player->IsBot() && z_player_zombie_debug )
    {
        DevMsg("Player %s spawned as %s after waiting %3.2f seconds\n", player->GetPlayerName(), player->GetClassName(), *&_mm_cvtps_pd(LODWORD(timeSinceLastKilled)));
    }

    if ( player->IsGhost() )
        player->EmitPrivateSound("PlayerZombie.BecomeGhost");
}
else
{
    player->ResetClassSpawnSystem(player);
    player->m_fLastKilledTime = 0;
}
jbzdarkid commented 8 years ago

I only mentioned this because I'd asked you before :P

Default class hunter makes sense to me too, it's what spawns when you set all the zversus***_limit convars to 0.

As far as I can tell, this code is just iterating through the classes and selecting the first one that isn't at spawn capacity (the while loop), and isn't otherwise limited by dominators (the IsClassOverLimit() call). This is thus clearly not the only code governing spawns, since there's no "sac history" in this, i.e. it's not doing anything responsive with regards to the previous si deaths.

Attano commented 8 years ago

What does zombie death have to do with anything in this scenario? A spawn has been given out -> saved, registered in multiple places, approved.

Try looking at it from a different angle. Yes there is some more when the SI dies, but it doesn't matter in this context. Plus there is also more inside ConvertZombieClass(), which I won't bother reversing.

This is in fact the main logic that proves two things: there is no RNG in the zombieclass decision, and calling setclass() directly will skip updating the history and the global g_ZombieClass.

Attano commented 8 years ago

Also I might have forgotten to mention that this is called for spectators as well. So if you spectate, ResetClassSpawnSystem() is called on you.

MattNF commented 8 years ago

Yeah, I don't think the "last SI who dies won't be in the next spawn" is a rule set in stone (or code in this case) but just a general rule of thumb.

So since there's no RNG in the code, why/how were those scenarios possible for me and high cookie? Were those bugs or just rare edge cases?

jbzdarkid commented 8 years ago

So you're saying that if you sac a boomer with tricap it depends only on the tricap as to whether or not you get another boomer? Seems sketchy.