Can't access array values in other class -> options?

Discussions about Coding and Scripting
Post Reply
ShaiHulud
Adept
Posts: 459
Joined: Sat Dec 22, 2012 6:37 am

Can't access array values in other class -> options?

Post by ShaiHulud »

Hello

I'm working on a fairly simple little script (my first) to implement a couple of features which we'd like to see available on our BunnyTrack server. It's gone pretty well (thanks again to Sponge for guidance), and I've been able to cull much of what I need from other existing units.

But I have a few problems and questions which I hope more experienced UnrealScripters will be able to help me with. In each case, I'm getting information from the BTPlusPlus class.

1) I needed a way of checking whether, when a flag capture occurs, the player's new record was a) faster then the server record, or b) their first capture on a given map, or c) not their first cap on a given map. I've been able to do this by monitoring player connects, and when a connect is spotted, asking BTPlusPlus to give me their "BestTime" for the current map. Then, when a capture is observed (by listening in on MutatorBroadcastLocalizedMessage), I can check their new "BestTime" against the "BestTime" I stored when they first joined the server. That's fine. Long winded, but it works (a "0" previous BestTime means first cap for the map, a BestTime < Server.BestTime means a new record etc.).
The reason I have to go to the trouble of storing BestTimes when a player first connects, instead of being able to check these things when a flag cap takes places, is that by the time I can ask the BTPlusPlus class about BestTimes (in MutatorBroadcastLocalizedMessage), the old value has been replaced by the new one - a new faster time overwrites the previous one. So I can't use it to make any decisions about whether it was their first cap for the map, and so on.

But, I see an intriguing bit of code in FlagDisposer.uc on line 110

Code: Select all

Controller.SendEvent("btcap", aTPawn.PlayerReplicationInfo.PlayerID, NewTime, TimeStamp);
What is SendEvent? Controller, here, is a BTPlusPlus object. The SendEvent function in BtPlusPlus.uc looks like this:

Code: Select all

//====================================
// SendEvent - Sends custom events to all registered actors
//====================================
function SendEvent(string EventName, optional coerce string Arg1, optional coerce string Arg2, optional coerce string Arg3, optional coerce string Arg4)
{
	local Actor A;
	local int i;
	local string Event;

	if (Level.Game.LocalLog != None)
		Level.Game.LocalLog.LogSpecialEvent(EventName, Arg1, Arg2, Arg3, Arg4);

	Event = EventName;
	if(Arg1 != "")
		Event = Event$chr(9)$Arg1;
	if(Arg2 != "")
		Event = Event$chr(9)$Arg2;
	if(Arg3 != "")
		Event = Event$chr(9)$Arg3;
	if(Arg4 != "")
		Event = Event$chr(9)$Arg4;

	for(i=0;i<EventHandlersCount+1;i++)
	{
		if(EventHandlers[i] != None)
			EventHandlers[i].GetItemName(Event);
	}
}
The reason this piques my interest is that it's called in FlagDisposer.uc *before* the old player "BestTime" is overwriten, AND it supplies the NEW BestTime as a parameter. But I don't really see how this works - if it does what the name suggests. What I'm hoping, is that this is some way of registering a calback with the FlagDisposer object, which would notify me when a player caps - then I'd not need to maintain my own array of players, I could just work with the individual player data supplied with this callback.
But. To me, as a total newbie, this function looks like it's not doing anything more than calling the GetItemName function and not actually doing anything with the result?
I'm hoping I'm wrong about that, and that there is some way of using this as a callback system somehow, so if anyone can talk about that I'd be very happy to listen. It'd simplify things, basically.


2) I want players to be able to check the most recent server records. Adding this as a mutate command was pretty straightforward, I just modified the BLPlusPlus code a bit. But, some of the information I want to supply to any player that uses it is unavailable. The information I want to ask for is stored in the ServerRecords class, which is accessible as an object inside of the BTPlusPlus class as a variable called "SR".

The ServerRecords class has an array with 3000 slots. I've been hitting my head against this "variable is too large" error quite a bit when attempting to access arrays in other classes, but luckily ServerRecords has 3 getter functions called getCaptime, getTimestamp and getPlayerName. But... the other important item of information that I want from that thing is the map name! And there's no function for extracting that. Which seems a bizarre omission.
I realise ServerRecords was only designed to service BTPlusPlus, and BTPlusPlus apparently has no need to be able to look up the map names connected with a server record. But just for the sake of completion! The other 3 functions get the values stored in 3 fields of a struct that only has 4 fields. Why write 1-line getter functions for only 3 out of 4 of them? As the original coder, that kind of thing would drive me crazy, I'd have to do it just to round things off! Honestly, I thought I must have accidentally delete "getMapName" or something, but no, it's really not there.

But anyway. This is a problem. It's no use just telling the player "Ah, here are the most recent records. Here are the times, and the players... but you'll have to guess which maps these are for. Hahah!". I don't really know how I can get around this. I've had a couple of ideas, but I don't like them, so I could really do with some advice.

Bad idea 1: instead of using my own config file, I tell my class to store everything in the BTPlusPlus.ini file. I've no idea how this works at the moment, I just know that other mutators sometimes store their config variables in the UnrealTournament.ini file (for example). So, my thinking was, if I could make my class use the BTPlusPlus.ini file, that will also give me access to the list of server records stored in that file(?). Of course, this idea falls down (assuming it's technically feasible to begin with) because *my* copy of the server records wouldn't get updated when a player beat a server record or set a new one, so I'd have stale data. I suppose, after a cap, I could update my copy with the new values... but the idea of all of this overhead is not appealing.

Bad idea 2: recompile a custom version of BTPlusPlus. Which I'm loathe to do for a number of reasons. Firstly, because if a new version comes out later, I'd have to go through the process of making my own version all over again. Secondly it would mean extra downloads to visits of our server only. And we don't like downloads.

But that's it. That's all I can come up with. All for the want of a missing 1-line getter function. Tsk. But, if anyone else can suggest something better, I'd be really, really super grateful.

Well, this was a lot longer than I'd meant it to be. And probably a bit meandering, I blame sleep deprivation. Thanks for reading!
User avatar
Sp0ngeb0b
Adept
Posts: 376
Joined: Wed Feb 13, 2008 9:16 pm
Location: Cologne
Contact:

Re: Can't access array values in other class -> options?

Post by Sp0ngeb0b »

To 1):
The SendEvent function is indeed supposed to provide other mutators with additional hooks into BTPlusPlus. So yes, you are able to receive this event. You have to register your mutator as a an EventHandler in BTPlusPlus first, which can be done by calling the Touch function of BTPlusPlus. See:

Code: Select all

event Touch(Actor A)
{
	local int i;

	if(EventHandlersCount == 10)
		A.GetItemName("-1,event handlers number exceeded");

	for(i=0;i<EventHandlersCount+1;i++)
	{
		if(EventHandlers[i] == None)
		{
			EventHandlers[i] = A;
			A.GetItemName("0,registration successful");
			EventHandlersCount++;
		}
	}
}
After you registered your mutator, you will be able to receive events from BTPlusPlus using the

Code: Select all

function string GetItemName(string Input)
function.


To 2):
Here it comes again, that infamous Variable is to large error. In general, the only way to avoid it was indeed the getX functions in the desired class. Indeed pretty stupid the function for Mapname wasn't icluded, BTPlusPlus just uses the CheckRecord(string MapName) function, but it doesn't really offer the stuff you want to achieve. I gotta look into this later...
Website, Forum & UTStats

Image
******************************************************************************
Nexgen Server Controller || My plugins & mods on GitHub
******************************************************************************
ShaiHulud
Adept
Posts: 459
Joined: Sat Dec 22, 2012 6:37 am

Re: Can't access array values in other class -> options?

Post by ShaiHulud »

I've finished it. Well, a first pass anyway. I expect the way I'm doing some of these things is a bit unconventional/naive, but it is what it is. Adding it here in the hope that wiser eyes will be able to point out silly (or serious) errors.

Various bits of it are taken from BTPlusPlus, BTCheckPoints, and BDBMapVote. I opted not to use the Actor (Touch) registration system in BTPlusPlus because it wasn't sending me the events I needed most and I kept getting "event handlers number exceeded" errors in the server log.

The first major problem was figuring out when BTPlusPlus had the data available that I wanted - details from the BTRecords.ini and BTPlusPlus.ini files. I tried various strategies for determining this, but it seems you can't ask it about these things until really quite late in the game (figuratively speaking). In the end I decided to wait until a flag grab had been detected, and then went from there.

The second problem - related to this - was getting an existing player "BestTime" for a map so that it could be compared against the time recorded for a capture on the currently running map. In the end I created an array for in-game players and made a note of their "BestTime" when they first connected, and stored this for use later on.

The third problem was finding out how to do map name look-ups from the ServerRecords object stored in side of the BTPlusPlus object. You can't. So what I did instead was get a list of all of the maps on the server (the slow, horribly horribly slow way using GetMapName, or by using an INI fed cached list - which is acceptably quick), and then query these against the ServerRecords object CheckRecord function - to see which returned a valid index (CheckRecord scans through a list of ServerRecords using a map name, and returns the index of a match in that list - OR the index of the next "free" position in the list).
It's pretty (well, very) slow, and I kept getting "runaway loop detected" errors in the server log. To get around that I made my loop iterator global, and added another value which Returns from the function where this is all running every 500 iterations (an arbitrary value, but seems to work OK). Then it waits for Tick() to run again before returning and continuing where it left off. Pretty cludgy, but it beat the loop detector!

Oh, and my comments are a bit inconsistent and might not be accurate in some places, I didn't go back and update them in response to all changes. And yeah, I know the name is kinda cheesy :)

Code: Select all

// BTXtra 0.1 - an inefficient spaghetti nightmare!
//
// Why? This unit is intended to be used on BunnyTrack servers.
// It has a couple of very basic features:
// 1) It modifies the player score (overrides CTF scoring) mainly for the purposes of player rank tracking (GameTracker)
//    It awards a maximum score (default 100 points) for a new server record, or first cap on a map.
//    It awards a much reduced score (default 10 points) for the second cap on a map, and then a single point
//    for each subsequent cap.
//
// 2) It adds a new command, "newrecs", which lists the most recently set server records
//   CAVEAT: only finds records for maps stored on the server. Any records for maps removed/renamed
//   will not be shown.
//
//
class BTXtra extends Mutator config (BTXtra);

struct ServerRecord
{
  var string m; // mapname
  var int c;    // captime
  var int t;    // timestamp
  var string p; // playername
};

// Server Vars 
var BTPlusPlus Controller;
var bool bCheckForNewPlayers;
var bool bConsoleHeader;
var bool bFoundLastMap;
var bool bSetServerRecords;
var int iCurrentID;
var int iLastMapInServerMaps;
var int iLastServerRecord;
var int iMapCount;
var int iServerRecordIterator;
var String ServerMaps[4096];               // This is a simple list of all of the map names available on the server
var ServerRecord ServerRecords[4096];      // This is a list of BTPlusPlus server records, e.g., map names, plus capper name, date, and the cap time

struct PlayerBestTimes
{
  var PlayerPawn Player;
  var int BestTime;
  var int CapsThisGame;
  var bool bSlotTaken;
};
var PlayerBestTimes BT[32];                // This is a list of players IN the server. It stores their last BestTime for the current map

// Server Vars Configurable 
var() config bool bDebugMessages;
var() config bool bEnabled;
var() config int iDefaultNewReCount;
var() config int iMapsCacheCount;
var() config int iMaxCapPoints;
var() config int iFirstRepeatCapPoints;
var() config string sVersion;
var() config string MapCacheList[4096];    // This is the list of maps stored in the class INI file. Reading from this is WAY faster than asking UT to give us a list of maps
var() config bool bUseCache;

//=============================================================================
// Write a message to (e.g.) Server.log
//=============================================================================
function AddLog(String Message)
{
  Log("### BTXtra.uc LOG [" $ Level.Hour $ ":" $ Level.Minute $ ":" $ Level.Second $ "]: " $ Message);
}

//=============================================================================
// Write an initialization header to the log file
//=============================================================================
function AddLogInit(int Stage)
{
  switch(Stage)
  {
    case 0:
      Log("#=====================================#");
      Log("#                                     #");
      Log("#       BTXtra starting up            #");
	  Log("#                                     #");
	  Log("# + mutate newrecs command available  #");
	  Log("#                                     #");
	  Log("# - Score modifications will not be   #");
	  Log("#   active until a player makes a     #");
	  Log("#   flag grab                         #");
      Log("#                                     #");
      Log("#=====================================#");
	  Break;
	  
	case 1:
      Log("#=====================================#");
      Log("#                                     #");
      Log("#  BTXtra" @ sVersion @ "loaded successfully.    #");
	  Log("#                                     #");
      Log("#  + Score modifications are now      #");
	  Log("#    available                        #");
      Log("#=====================================#");
	  Break;
  }
}

//=============================================================================
// Init global variables. No-one else seems to do this in their scripts, so I
// suppose it's probably not necessary, but it's something I'm used to doing
// in other languages, so just for my own peace-of-mind...
// p.s. This is called from PostBeginPlay(), and by adding logging I know that
// it runs before any of the functions which use these variables.
//=============================================================================
function InitVariables()
{
  local int i;

  bCheckForNewPlayers   = False;
  bConsoleHeader        = False;
  bFoundLastMap         = False;
  bSetServerRecords     = False;
  iCurrentID            = 0;
  iLastServerRecord     = 0;
  iMapCount             = 0;
  iServerRecordIterator = 0;
  Controller            = None;
  
  for(i = 0; i < ArrayCount(ServerMaps); i++)
  {
    ServerMaps[i] = "";
  }
  
  for(i = 0; i < ArrayCount(ServerRecords); i++)
  {
    ServerRecords[i].m = "";
	ServerRecords[i].c = 0;
	ServerRecords[i].t = 0;
	ServerRecords[i].p = "";
  }
  
  for(i = 0; i < ArrayCount(BT); i++)
  {
    BT[i].Player       = None;
	BT[i].BestTime     = 0;
	BT[i].CapsThisGame = 0;
	BT[i].bSlotTaken   = False;
  }
}

//=============================================================================
// Quicksort -> from UnrealWiki
//=============================================================================
Function SortArray(Int Low, Int High) 
{
  //  low is the lower index, high is the upper index
  //  of the region of array a that is to be sorted
  local Int i, j;
  local int x;
  Local ServerRecord Temp;
 
  i = Low;
  j = High;
  x = ServerRecords[(Low + High) / 2].t;

  //  partition
  do
  {
    //while (((ServerRecords[i].t < x) || (x == 0)) && (ServerRecords[i].t > 0))
	while (ServerRecords[i].t < x)
      i += 1;
    while ((ServerRecords[j].t > x) && (x > 0))
	  j -= 1;

    if (i <= j)
    {
	  // swap array elements, inlined
	  Temp = ServerRecords[i];
      ServerRecords[i] = ServerRecords[j];
      ServerRecords[j] = Temp;
      i += 1; 
      j -= 1;
    }
  } until (i > j);
 
  //  recursion
  if (low < j)
    SortArray(low, j);
  if (i < high)
    SortArray(i, high);
}

//=============================================================================
// Load the maps for a specified game type (in our case, BT, with the "BT" 
// prefix. How to get this programmatically?
// Note that "GetMapName" is excruciatingly slow, so it's advised (though 
// burdensome because of the maintenance overhead) to use the map cache.
// For ~1600 maps on my 2GHz PC, this takes about 90 seconds to complete
// without using the map cache...
//=============================================================================
function LoadUncachedMapList()
{
  local string FirstMap, NextMap, TestMap, DisplayName, MapPrefix;
  local int i;

  i = 0;
  MapPrefix = "BT"; // Level.Game.MapPrefix
  FirstMap = GetMapName(MapPrefix, "", 0);
  NextMap = FirstMap;
  while (!(FirstMap ~= TestMap))
  {
    // Add the map.
    if(Right(NextMap, 4) ~= ".unr")
      DisplayName = Left(NextMap, Len(NextMap) - 4);
    else
      DisplayName = NextMap;

	ServerMaps[i] = DisplayName;
    i++;

    NextMap = GetMapName(MapPrefix, NextMap, 1);
    TestMap = NextMap;
  }
}

//=============================================================================
// Copy the maps listed in the class INI file to the MapList array
//=============================================================================
function LoadCachededMapList()
{
  local int i;

  for(i = 0; i < ArrayCount(MapCacheList); i++)
    ServerMaps[i] = MapCacheList[i];

  iMapCount = iMapsCacheCount;
}

//=============================================================================
// 1) Load all maps
// 2) Iterate over list of maps
//   - Call CheckRecord with map name
//   - If index retuned >= 0
//     Get TimeStamp, CapTime, PlayerName -> Add to our array
//   - else
//     Continue;
// 3) Sort our array by timestamp
//=============================================================================
function BuildRecordList()
{
  local int i, j, k, l;
  local int c, t;
  local string p;
  local ServerRecord Rec;

  if (!bFoundLastMap)
  {
    // 1
	if (!bUseCache || MapCacheList[0] == "")
      LoadUncachedMapList();
	else
	  LoadCachededMapList();

    // 2 - had to break this up into parts to prevent UT from telling me it was causing
    // "runaway loop detected (over 10000000 iterations)"
    // Part1 - find the last used map slot
    iLastMapInServerMaps = 0;
    for(i = 0; i < ArrayCount(ServerMaps); i++)
    {
      if (ServerMaps[i] == "")
	  {
        iLastMapInServerMaps = i;
	    Break;
	  }
    }

	bFoundLastMap = True;
  }

  k = iLastServerRecord;
  for(iServerRecordIterator = iServerRecordIterator; iServerRecordIterator < iLastMapInServerMaps; iServerRecordIterator++)
  {
    // Prevent runaway
	if (k > 500)
	  Return;

	// 2a
    j = Controller.SR.CheckRecord(ServerMaps[iServerRecordIterator]);
	k++;
	
	if (j >= 0)
	{
      // Check if we've been given an empty record by CheckRecord (it returns the index
	  // of the next free slot if no record is found matching the name supplied to it)
	  if (Controller.SR.getCaptime(j) == 0)
	    Continue;

	  // 2b
	  c = Controller.SR.getCaptime(j);
	  t = Controller.SR.getTimestamp(j);
	  p = Controller.SR.getPlayerName(j);

	  Rec.c = c;
	  Rec.t = t;
	  Rec.p = p;
	  Rec.m = ServerMaps[iServerRecordIterator];
	  ServerRecords[iLastServerRecord] = Rec;

	  iLastServerRecord++;
	}
  }
  
  // 3
  SortArray(0, iLastServerRecord - 1);
  bSetServerRecords = True;
  AddLogInit(0);
}

//=============================================================================
// Startup and initialize.
//=============================================================================
function PostBeginPlay()
{
  //  Zero all global variables
  InitVariables();

  // Create the class .ini if it doesn't already exist.
  SaveConfig();
  
  // Find BTPlusPlus object, and store a reference to it. This means that start-up
  // order is significant, and BTXtra should be placed later in the command-line
  // than BTPlusPlus.
  FindBTPlusPlusObject(Controller);

  // Register as a message mutator, as we'll be using message monitoring
  // to perform some of our code. If not registered, then many message
  // events will not be passed to our mutator.
  Level.Game.RegisterMessageMutator(Self);
  
  Super.PostBeginPlay();

  // Wait 5 seconds between checks for departed players
  if(Level.NetMode == NM_DedicatedServer)
    SetTimer(5.0, True);
}

//=============================================================================
// Tick - Used for new player detection. Inherited from class'Actor'
//=============================================================================
function Tick(float DeltaTime)
{
  Super.Tick(DeltaTime);

  // bCheckForNewPlayers is False until the first time a flag is grabbed on the
  // current map. BTPlusPlus doesn't finish initialising until after PostNetBeginPlay()
  // so we need to wait until then before we can ask it for player best times.
  // But there are no convenient events after PostNetBeginPlay according to
  // http://wiki.beyondunreal.com/What_happens_at_map_startup
  // ...besides SetInitialState(), but this STILL seems to be too early to
  // retrieve myRec times. So, we wait for something else - I chose the first
  // recorded instace of a flag grab
  if (bCheckForNewPlayers)
  {
    CheckForNewPlayer();
	if (!bConsoleHeader)
	{
	  bConsoleHeader = True;
      AddLogInit(1);
	}
  }
	
  // Check if the BTPlusPlus object will return meaningful data yet.
  // WARNING: if there are no server records so far BuildRecordList
  // will never get called!
  if (!bSetServerRecords)
  {
    if ((Controller != None) && (Controller.SR.getCaptime(0) > 0))
      BuildRecordList();
  }
}

//=============================================================================
// FindPlayer - Find a player in the PlayerBestTimes array, by a comparing 
// aginst a passed-in Pawn object
//=============================================================================
function int FindPlayer(Pawn P)
{
  local int i, ID;

  for(i = 0; i < ArrayCount(BT); i++)
  {
    if (BT[i].Player == P)
      Return i;
  }

  Return -1;
  AddLog("FindPlayer -> Failed to locate Pawn in BT array - should never happen!");
}

//=============================================================================
// CheckForNewPlayer - Check for new player
// Triggered in: Tick
//=============================================================================
function CheckForNewPlayer()
{
  local Pawn P;
  
  if (Controller == None)
    Return;

  // At least one new player has joined - sometimes this happens faster than tick
  while (Level.Game.CurrentID > iCurrentID) 
  {
    for (P = Level.PawnList; P != None; P = P.NextPawn)
    {
      if (P.IsA('PlayerPawn') && P.PlayerReplicationInfo != None && P.PlayerReplicationInfo.PlayerID == iCurrentID)
      {
        InitNewPlayer(PlayerPawn(P));
        Break;
      }
    }
    iCurrentID++;
  }
}

//=============================================================================
// InitNewPlayer - Add a newly detected player to our array of players known
// to be in the current game.
// Triggered in: CheckForNewPlayer
//=============================================================================
function InitNewPlayer(PlayerPawn P)
{
  local int i, ID, BTime;

  for (i = 0; i < ArrayCount(BT); ++i)
  {
    if (!BT[i].bSlotTaken)
    {
	  ID = Controller.FindPlayer(P);
      BT[i].Player = P;
	  BT[i].CapsThisGame = 1;
      BT[i].BestTime = Controller.GetBestTimeClient(ID);
      BT[i].bSlotTaken = True;
      Return;
    }
  }

  AddLog("InitNewPlayer -> BT array full - should never happen!");
}

//=============================================================================
// Keep track of players leaving.
// Fires periodically - see PostBeginPlay for the interval
//=============================================================================
function Timer()
{
  local int i;

  for (i = 0; i < ArrayCount(BT); ++i)
  {
    if (BT[i].bSlotTaken && (BT[i].Player == None || BT[i].Player.Player == None || NetConnection(BT[i].Player.Player) == None))
    {
      PlayerLogout(i);
      BT[i].bSlotTaken = False;
      BT[i].Player = None;
      BT[i].BestTime = 0;
	  BT[i].CapsThisGame = 1;
    }
  }
}

//=============================================================================
// Player disconnected.
// Triggered in: Timer() 
//=============================================================================
function PlayerLogout(int i)
{
  if (bDebugMessages)
    AddLog("PlayerLogout:" @ BT[i].Player.PlayerReplicationInfo.PlayerName);
}

//=============================================================================
// Search all actors of class "BTPlusPlus" (uhm, I'm assuming there will only
// ever *be* one of these), and return a reference to it.
// If none is available (there should be if this class was instantiated in the 
// correct order), then should we create one? I don't know what happens if we 
// do - does UT just use our object instead of creating its own? *shrug*
//=============================================================================
function bool FindBTPlusPlusObject(out BTPlusPlus BTPPPbj)
{
  local BTPlusPlus BTPP;

  foreach AllActors(class'BTPlusPlus', BTPP)
  {
    BTPPPbj = BTPP;
	Return True;
  }

  AddLog("FindBTPlusPlusObject -> BTPlusPlus object not found - should never happen!");
}

//=============================================================================
// Tests whether the player has previously capped this map, after a cap event
// has been detected. It determines this by checking the "BestTime" value stored
// in the player BTRecords.ini file. If this was 0 when the game started (or 
// rather, up until the point when someone first grabbed a flag), then this
// must be their first cap. OK, they could go and re-zero the record, but
// this is a reasonable enough check for our purposes.
//=============================================================================
function bool IsFirstCapOnThisMap(PlayerPawn PP)
{
  local int i, ID, BestTime;
  local bool bIsFirstPB;

  for (i = 0; i < ArrayCount(BT); ++i)
  {
    if (BT[i].bSlotTaken && BT[i].Player == PP)
	{
	  if (bDebugMessages)
	    AddLog("IsFirstCapOnThisMap -> " @ BT[i].Player.PlayerReplicationInfo.PlayerName @ "old BestTime" @ BT[i].BestTime);
		
	  bIsFirstPB     = (BT[i].BestTime == 0);
	  ID             = Controller.FindPlayer(PP);
	  BestTime       = Controller.GetBestTimeClient(ID); // on the current map
      BT[i].BestTime = BestTime;
	  
	  if (bDebugMessages)
	    AddLog("IsFirstCapOnThisMap -> " @ BT[i].Player.PlayerReplicationInfo.PlayerName @ "new BestTime" @ BT[i].BestTime);
		
	  Return bIsFirstPB;	  
    }
  }
  
  AddLog("IsFirstCapOnThisMap() didn't find the player who just capped - should never happen!");
}

//=============================================================================
// Update the server record for the curent map in OUR array "ServerRecords"
//=============================================================================
function UpdateServerRecord(string MapName, string PlayerName, int CapTime, int TimeStamp)
{
  local int i;
  local bool bNewRec;
  
  for (i = 0; i < ArrayCount(ServerRecords); ++i)
  {
    bNewRec = ServerRecords[i].c == 0;
    if (bNewRec || ServerRecords[i].m ~= MapName)
	{
	  if (bNewRec)
	  {
        ServerRecords[i].m = MapName;
		if (bDebugMessages)
		  AddLog("Adding new record for map" @ MapName);
	  }
	  else
	  {
	    if (bDebugMessages)
	      AddLog("Updating record for existing map" @ MapName);
	  }
	  ServerRecords[i].c = CapTime;
	  ServerRecords[i].t = TimeStamp;
	  ServerRecords[i].p = PlayerName;
	
      // This was a new record
	  if (bNewRec)
	    iLastServerRecord++;
	  Return;
	}
  }
}

//=============================================================================
// Intercept CTF messages to adjust scores.
//=============================================================================
function bool MutatorBroadcastLocalizedMessage(Actor Sender, Pawn Receiver, out class<LocalMessage> Message, out optional int Switch, out optional PlayerReplicationInfo RelatedPRI_1, out optional PlayerReplicationInfo RelatedPRI_2, out optional Object OptionalObject)
{
  local PlayerPawn PP;
  local CTFFlag Flag;
  local int i, j;
  local string PlayerName;
  local bool bBeatServerRec, bIsFirstMapCap;

  if (bDebugMessages)
  {
    AddLog("MutatorBroadcastLocalizedMessage -> Message.Name" @ Message.Name);
    AddLog("MutatorBroadcastLocalizedMessage -> Sender.Name " @ Sender.Name);
  }

  if (Sender.IsA('CTFGame'))
    Flag = CTFFlag(OptionalObject);
  else if (Sender.IsA('CTFFlag'))
	Flag = CTFFlag(Sender);
  else if (Sender.IsA('FlagDisposer'))
    Flag = FlagDisposer(Sender).Flag;
  else
  {
    if (bDebugMessages)
      AddLog("MutatorBroadcastLocalizedMessage -> Sender class isn't one we're watching for");
	Return Super.MutatorBroadcastLocalizedMessage(Sender, Receiver, Message, Switch, RelatedPRI_1, RelatedPRI_2, OptionalObject);
  }

  if (Flag == None)
  {
    if (bDebugMessages)
      AddLog("MutatorBroadcastLocalizedMessage -> Flag object isn't assigned, although Sender class was one we're watching for");
    Return Super.MutatorBroadcastLocalizedMessage(Sender, Receiver, Message, Switch, RelatedPRI_1, RelatedPRI_2, OptionalObject);
  }

  // Only call this once (not on all players), so check if the Receiver is the capturer.
  if((Switch == 0 || Switch == 6) && Receiver == Pawn(RelatedPRI_1.Owner))
  {
    PP = PlayerPawn(Receiver);

    switch(Switch)
    {
      // CAPTURE
      // Sender: CTFGame, PRI: Scorer.PlayerReplicationInfo, OptObj: TheFlag
      case 0:
	    if (bDebugMessages)
	      AddLog("MutatorBroadcastLocalizedMessage -> Cap detected");
        if(Receiver == Pawn(RelatedPRI_1.Owner))
        {
		  i = FindPlayer(PP);
		  PlayerName = BT[i].Player.PlayerReplicationInfo.PlayerName;
		  // First, reduce score by default CTF amount - can we programmatically retrieve this value from somewhere?
		  PP.PlayerReplicationInfo.Score = PP.PlayerReplicationInfo.Score - 7;

		  // Score calculation: If the cap time is less than the current record time, award the player full points.
          // OR If this is the players first cap of this map, award the player full points.
		  // Otherwise (if they've capped it before), award a much reduced score (10% of full points), and then
		  // 1 point for each subsequent cap
		  bIsFirstMapCap = IsFirstCapOnThisMap(PP);
		  // Grrr. This would be a lot easier if BTPlusPlus *also* let us have a "function int GetLastTimeClient(int ID){ return PI[ID].Config.lastCap; }" function
		  j = Controller.SR.CheckRecord(Controller.GetLevelName());
		  bBeatServerRec = (Controller.SR.getTimestamp(j) == Controller.CurTimestamp() && PlayerName == Controller.MapBestPlayer);
          if (bIsFirstMapCap || bBeatServerRec)
		  {
			  // Check if the player beat the server record
			  if (bBeatServerRec)
			  {
			    if (bDebugMessages)
	              AddLog("MutatorBroadcastLocalizedMessage -> Player broke the server record, awarding max points:" @ PlayerName @ iMaxCapPoints @ "points");
			    PP.PlayerReplicationInfo.Score += iMaxCapPoints;

				// Update our copy of the server records array -> TODO: split records array into 2 - one half stores map name, player, cap time - sorted by MapName
				// The other stores the cap date, and is sorted by date. Keep an index for each item in the first array to the corresponding entry in the secnnd,
				// and an index for each item in the second array to the corresponding entry in the first. Update these indexes if the element order changes.
				// Because doing linear searches each time is...
				
				if (j >= 0)
				  UpdateServerRecord(Controller.GetLevelName(), PlayerName, Controller.SR.getCaptime(j), Controller.SR.getTimestamp(j));
			  }
			  else
			  {
			    if (bDebugMessages)
	              AddLog("MutatorBroadcastLocalizedMessage -> This is the players first cap on this map, awarding max points:" @ PlayerName @ iMaxCapPoints @ "points");
				PP.PlayerReplicationInfo.Score += iMaxCapPoints;
			  }
          }
		  // NOT first cap on this map
		  else
		  {
		    if (bDebugMessages)
	          AddLog("MutatorBroadcastLocalizedMessage -> This is NOT the players first cap on this map");
			  
			if (BT[i].CapsThisGame == 1)
			{
			  PP.PlayerReplicationInfo.Score += iFirstRepeatCapPoints;
			  if (bDebugMessages)
	            AddLog("MutatorBroadcastLocalizedMessage -> This is the players first cap on this map during this game, awarding max reduced score" @ PlayerName @ iFirstRepeatCapPoints @ "points");
			}
			else
			{
			  PP.PlayerReplicationInfo.Score += 1;
			  if (bDebugMessages)
	            AddLog("MutatorBroadcastLocalizedMessage -> This is NOT the players first cap on this map during this game, awarding modified reduced score" @ PlayerName @ "1 point");
			}
			
			// Increment caps-this-game count
		     BT[i].CapsThisGame = BT[i].CapsThisGame + 1;
		  }
        }
        Break;

      // GRAB
      // Sender: CTFFlag, PRI: Holder.PlayerReplicationInfo, OptObj: CTFGame(Level.Game).Teams[Team]
      case 6:
		if (bDebugMessages)
	      AddLog("MutatorBroadcastLocalizedMessage -> Grab detected");
        if (!bCheckForNewPlayers)
		{
		  CheckForNewPlayer();
          bCheckForNewPlayers = True;
		}
		Break;
    } 
  }
 
  Return Super.MutatorBroadcastLocalizedMessage(Sender, Receiver, Message, Switch, RelatedPRI_1, RelatedPRI_2, OptionalObject);
}

//=============================================================================
// Give info on 'mutate smartctf' commands.
//=============================================================================
function Mutate(string MutateString, PlayerPawn Sender)
{
  local string Argument;
  local int i, j, RecCount;
  local int CapTime, TimeStamp;
  local string CapTimeString, Player;

  if (NextMutator != None)
    NextMutator.Mutate(MutateString, Sender);

  if (Sender != None)
  {
    if (Left(MutateString, 7) ~= "NEWRECS")
    {
	  // Show latest server records
	  // TODO: add "days" and "maps" qualifiers
	  if (iLastServerRecord <= 0)
	  {
		Sender.ClientMessage("Sorry - found no records.");
		Return;
	  }

	  // No record count specified, so use default
	  if (Len(MutateString) == 7)
	    RecCount = iDefaultNewReCount;
	  else
	  {
	    Argument = Mid(MutateString, 8);
	    RecCount = Int(Argument);
		if (RecCount <= 0)
		  RecCount = iDefaultNewReCount;
	  }

	  if (RecCount > iLastServerRecord)
	    j = iLastServerRecord;
	  else
	    j = RecCount;
	  // Rewind
	  Sender.ClientMessage(j @ "most recent server records:");
	  for (i = (iLastServerRecord - j); i < iLastServerRecord; ++i)
	  {
		CapTime = ServerRecords[i].c;
		TimeStamp = Controller.lastTimestamp - ServerRecords[i].t;
		CapTimeString = Controller.FormatCentiseconds(CapTime, False);
		Player = ServerRecords[i].p;
		Sender.ClientMessage(ServerRecords[i].m @ CapTimeString @ "(" $ TimeStamp/86400 @ "day(s) ago) by" @ Player);
	  }
    }
	
    // simple info queries (spectators too)
    switch(Caps(MutateString))
    {
      case "BTHELP":
	  case "BT++HELP":
	  case "BTPPHELP":
        Sender.ClientMessage("- newrecs (show most recent records), OR");
	    Sender.ClientMessage("- newrecs <number> (replace <number> with number of records to show)");
        break;
    }
  }
}

//----------------------------------------------------------------------------------------------------------------
//------------------------------------------------ CLIENT FUNCTIONS ----------------------------------------------
//----------------------------------------------------------------------------------------------------------------

defaultproperties
{
  bDebugMessages=True
  bEnabled=True
  bUseCache=True
  iDefaultNewReCount=20;
  iMapsCacheCount=0
  iMaxCapPoints=100
  iFirstRepeatCapPoints=10
  sVersion="0.1"
}

Post Reply