Randomizing player choice

Discussions about Coding and Scripting
Post Reply
Higor
Godlike
Posts: 1866
Joined: Sun Mar 04, 2012 6:47 pm

Randomizing player choice

Post by Higor »

Posting some code as I work on a random BotZ creation system, can be perfectly adapted to player choice as one joins a server as well, called from ModifyLogin(...) in mutator.

The code can exclude player classes, skins, and voice packs either by PackageName.Element or by its description in the .int file.
Useful to randomize a player if he's using an illegal class/skin, or a default one (not installed on server).

Code: Select all

var config string ExcludedClasses[24];
var config string ExcludedSkins[24];
var config string ExcludedVoices[24];

function GenerateRandom()
{
	local class<PlayerPawn> theClass;
	local string rSkin, rFace, rVoice;

	theClass = RandomPlayerClass();
	rSkin = RandomPlayerSkin( theClass);
	rFace = RandomPlayerFace( theClass, rSkin);
	rVoice = RandomPlayerVoice( theClass);

	//Do something else with this
}

final function class<PlayerPawn> RandomPlayerClass()
{
	local int NumPlayerClasses, i;
	local string NextPlayer, NextDesc, sCurrent;
	local float Factor;
	
	Factor = 1.0;
	GetNextIntDesc( "BotPack.TournamentPlayer", 0, NextPlayer, NextDesc);
	while( (NextPlayer != "") && (NumPlayerClasses < 64) )
	{
		if ( FRand() * Factor < 1.0 )
		{
			//Faster if exclusion check is done here
			For ( i=0 ; (i<24)&&(ExcludedClasses[i]!="") ; i++ )
				if ( (ExcludedClasses[i] ~= NextPlayer) || (ExcludedClasses[i] ~= NextDesc) )
					Goto EXCLUSION;

			sCurrent = NextPlayer;
		}
		EXCLUSION:
		if ( sCurrent != "")
			Factor += 1.0;
		NumPlayerClasses++;
		GetNextIntDesc("BotPack.TournamentPlayer", NumPlayerClasses, NextPlayer, NextDesc);
	}
	return class<PlayerPawn>( DynamicLoadObject( sCurrent, class'Class') );
}

final function string RandomPlayerSkin( class<PlayerPawn> TheClass )
{
	local string SkinName, SkinDesc, TestName, Temp, ASkin, result;
	local int i;
	local float Factor;

	Factor = 1.0;
	SkinName = "None";
	TestName = "";
	while ( True )
	{
		SKIN_EXCLUSION:
		GetNextSkin( GetItemName(string(TheClass.Default.Mesh)), SkinName, 1, SkinName, SkinDesc);
		if( SkinName == TestName )
			break;
		if( TestName == "" )
			TestName = SkinName;
		// Multiskin format
		if( SkinDesc != "")
		{			
			Temp = GetItemName(SkinName);
			if(Mid(Temp, 5, 64) == "")
			{
				// This is a skin
				if ( FRand() * Factor < 1.0 )
				{
					ASkin = Left(SkinName, Len(SkinName) - Len(Temp)) $ Left(Temp, 4);
					For ( i=0 ; (i<24)&&(ExcludedSkins[i]!="") ; i++ )
						if ( (ExcludedSkins[i] ~= ASkin) || (ExcludedSkins[i] ~= SkinDesc) )
							Goto SKIN_EXCLUSION;

					result = ASkin;
				}
				if ( result != "" )
					Factor += 1.0;
			}
		}
	}
	return result;
}

final function string RandomPlayerFace( class<PlayerPawn> TheClass, string InSkinName)
{
	local string SkinName, SkinDesc, TestName, Temp, result;
	local float Factor;

	Factor = 1.0;
	SkinName = "None";
	TestName = "";
	while ( True )
	{
		GetNextSkin( GetItemName(string(TheClass.Default.Mesh)), SkinName, 1, SkinName, SkinDesc);
		if( SkinName == TestName )
			break;
		if( TestName == "" )
			TestName = SkinName;
		// Multiskin format
		if( SkinDesc != "")
		{			
			Temp = GetItemName(SkinName);
			if(Mid(Temp, 5) != "" && Left(Temp, 4) == GetItemName(InSkinName))
			{	if ( FRand() * Factor < 1.0 )
				{
					result = Left(SkinName, Len(SkinName) - Len(Temp)) $ Mid(Temp, 5);
				}
				if ( result != "" )
					Factor += 1.0;
			}
		}
	}
	return result;
}

final function string RandomPlayerVoice( class<PlayerPawn> TheClass)
{
	local int NumVoices, i;
	local string NextVoice, NextDesc, result;
	local string VoicepackMetaClass;
	local float Factor;

	Factor = 1.0;
	if(ClassIsChildOf(TheClass, class'TournamentPlayer'))
		VoicePackMetaClass = class<TournamentPlayer>(TheClass).default.VoicePackMetaClass;
	else
		VoicePackMetaClass = "Botpack.ChallengeVoicePack";

	// Load the base class into memory to prevent GetNextIntDesc crashing as the class isn't loadded.
	DynamicLoadObject(VoicePackMetaClass, class'Class');

	GetNextIntDesc(VoicePackMetaClass, 0, NextVoice, NextDesc);
	while( (NextVoice != "") && (NumVoices < 64) )
	{

		if ( FRand() * Factor < 1.0 )
		{
			For ( i=0 ; (i<24)&&(ExcludedVoices[i]!="") ; i++ )
				if ( (ExcludedVoices[i] ~= NextVoice) || (ExcludedVoices[i] ~= NextDesc) )
					Goto VOICE_EXCLUSION;

			result = NextVoice;
		}
		if ( result != "" )
			Factor += 1.0;

		VOICE_EXCLUSION:
		NumVoices++;
		GetNextIntDesc(VoicePackMetaClass, NumVoices, NextVoice, NextDesc);
	}
	return result;
}
Higor
Godlike
Posts: 1866
Joined: Sun Mar 04, 2012 6:47 pm

Re: Randomizing player choice

Post by Higor »

Edited first post.
Fix for randomizer, now all possible choices have the same chance.
JackGriffin
Godlike
Posts: 3774
Joined: Fri Jan 14, 2011 1:53 pm
Personal rank: -Retired-

Re: Randomizing player choice

Post by JackGriffin »

Higor, there are two mods that have plagued me for years as unfixable: mapvote and Val Avatar. I got mapvote sorted this week and here you are presenting a way to redo Val Av (in a sense).

How do you plan on replicating the skin back out to the players without addressing PRI?
So long, and thanks for all the fish
Higor
Godlike
Posts: 1866
Joined: Sun Mar 04, 2012 6:47 pm

Re: Randomizing player choice

Post by Higor »

Never used Valhalla Avatar, so I don't exactly understand what the issue is, though I do understand the basics of it.

Same player class for everyone, model, sounds and skins are carefully handled by it right?

I can say this code relies on having the .int file definitions, which is how the character selection screen works.

About addressing PRI, what's the problem with that?
Addressing PRI clientside is totally possible, you use the PlayerID and use it to differ players.
Create a replicated actor with an array of PlayerID's and SkinCodes (something like "Boss;boss;xan")
And let the client enforce the class/skin if the replicated version of the player doesn't match that skincode.


Edit:
Check my AntiPistonCamp mutator for a way of linking PRI's and an internal list by never holding an actual pointer to the PRI or is Owner (it's a server version, but can easily be done clienside)
JackGriffin
Godlike
Posts: 3774
Joined: Fri Jan 14, 2011 1:53 pm
Personal rank: -Retired-

Re: Randomizing player choice

Post by JackGriffin »

OK I see now.

Val Av allows you to send your current skin out to everyone and if they have it installed will see you as what you are playing as. If you don't have it installed you see a default model. This replication requires a new PRI assignment to the player, I just thought maybe you had worked out some way to get through that issue. Since Val Av needs it's own PRI it has heavy conflicts with a lot of stuff (my own notwithstanding). Seeing that your mod depends on an .int defining pretty much excludes me trying it since the client will have an unknown skin string.

I appreciate your reply and advice. It's nice to see someone with new both ideas and zeal to flesh them out.
So long, and thanks for all the fish
Higor
Godlike
Posts: 1866
Joined: Sun Mar 04, 2012 6:47 pm

Re: Randomizing player choice

Post by Higor »

I made two different types of code that interact with the PRI array without even holding a pointer to them.

One with the FV_AntiPistonCamp which is the simple version, and another one with a Team Balancer I still haven't released.

It's a way to link data within 2 structures quickly without rebuilding the mutator's list when the main list (PRI array, Pawn list) changes.
Believe it or not I got the idea after seeing a how asyncronous can data be between social security check system and bank check logs (yeah, lol).

The key is very simple: order.

You could mirror a whole 400 element list with your own list without even lagging the game if you keep the order, and know the size of your array.
All of this using STATIC ARRAYS. (In c++, that number would be greater)


Method:
First check -
Mirror the entire list, for the AntiPistonCamp I created a struct containing a variable named PID.
I went through the gameinfo's PRIArray (PRI could alse be located using allactors).
As the iteration went, the structs' MyList[ i].PID = PRIArray[ i].PlayerID. and once the iteration ends, size=i.

Ok you mirrored the list, you know need to maintain it over the course of the game.
List maintenance -
Be warned, you are going to have to jump inside your maintenance iterator under a very special case: deletion and addition of elements between maintenance iterations.
Ok here goes:
Start iterating through the original list (PRIArray in my case).
-- First things first, add a jump point here, name it 'CHECKAGAIN:', then compare if (i==size). This is mandatory.
The iterator should end at i-1, this means a new element was inserted in the main list. Mirror it on our list, do size++, if there are more new elements, the above (i==size) will be TRUE again in the following iteration.
-- Second check: Compare if MyList[ i].PID == PRIArray[ i].PlayerID (exampes to make this easier to understand).
.Is it equal? Hit continue; (i++ will happen, iteration checks next element)
.Not equal? Why does this happen?
Because a PRI was just deleted from this list, we must remove the mismatching element from our list.
We check if the following element in our list matches the current one in the main one, count up (++j)
If until the next one matches we keep counting (if only one contiguous element was removed, j will be 1, if 2, j will be 2 and so on)
Special case, when counting up, if you find an empty struct, stop counting here. (this means many elements were substracted at the end of the list)
Starting from i+j in our list, we move the elements in our list 'j' places down. Remember to substract 'j' from SIZE once finished moving the upper block.
Now that part of our list and the main list are synched again, now JUMP back to the start of the iterator without having the i++ happen. Use Goto CHECKAGAIN;
That jump prevents an endless loop if between list maintenances the final element was removed, then a new one appeared in its place.

Ok, that's how you keep your struct array and the original array synched over time.
Let's assume the needed array would be something like this:
struct PlayerSkin
{
var int PID;
var string PlayerCharacter;
}
var PlayerSkin PInfos[64]; //(or 32 if you're not using an extended PRIArray

replication
{
if ( Role==ROLE_Authority)
PInfos;
if ( Role < ROLE_Authority) //To allow clients to change their own skin and let server know it.
ClientChangeSkin;
}

Keep both PInfos and the PRIArray synched.
This helps a lot in multiplayer games since no pointers are held, clients can do the checking on the list since PRI's are replicated.

So PlayerCharacter would be something like this: Mesh;multiskin0;multiskin1;multiskin2;multiskin3;talktexture
Or whatever Val does, you know there.
Clients will run periodic checks between the PInfo struct and existing relevant players, if they don't match the PlayerCharacter code, client changes it by himself.

QUICK EDIT:
Structs are replicated in a unitary way, so if you have tons of info to add to the player struct, and are afraid that might cripple networking...
I have an idea that works, but I don't want to write another wall of text...
Simplified here: the struct holds the PID and an actor pointer
Instead of subclassing PRI, make a bogus actor holding the extra variables.
- bAlwaysRelevant on all involved actors to allow replication
- ROLE_SimulatedProxy on the struct holder (subclass of actor, needs full event simulation)
- ROLE_DumbProxy on the bogus actor containing the tons of info (subclass of Info, these don't run events so they are faster in code)
Use the info actor to store and replicate variables, and the struct holder actor as bridge between the main player list and the clientside functions (class change, for example).

It's like setting up a parallel PRI system without interfering with the main one.

EDIT 2:
Depending on the style of the list maintenance, a jump-less version of it can be made.
So far I've thought of three different methods.
There is another choice here, that special class is unitary, meaning all players use it?
Even better, instead of making an array and synching to PRI's, do this:

(in player class)
var PlayerSkinInfo PSInfo;
var bool bSkinUpdated; //This will become false every time player disappears and reappears on netcode.

replication
{
if ( Role==ROLE_Authority)
PSInfo;
if ( Role < ROLE_Authority) //To allow clients to change their own skin and let server know it.
ClientChangeSkin; //This function will modify the skin entries on the server machine, this one will replicate back to other players
}

PSInfo:
- bAlwaysRelevant=true (player may disappear, but not his skin info)
- ROLE_SimulatedProxy (info holder, subclass of actor, needs to run Timer checks)
- Owner (key variable, must point to the player, have that player Spawn() this actor)

PSInfo will have this code:

Code: Select all

event PostNetBeginPlay()
{
         SetTimer(0.2, true);
}

event PostBeginPlay() //Just in case
{
       if ( Level.NetMode == NM_Client )
            SetTimer(0.2, true);
}

event Timer()
{
      if ( Level.NetMode != NM_Client )
             return;
      if ( (SpecialPlayer(Owner) != none) && !SpecialPlayer(Owner).bSkinUpdated) //Your player class
             EnforceSkins(); //Do your magic on the owner pawn
}
Post Reply