Mutators & UnrealScript

ServerPerks Interface: News and Rules Tabs, a Scoreboard Clock, Clan Tags and Trader Categories

Four interface tweaks shared on the killingfloor.ru forum by scar (2012) and Flame (2013-2014). All of them change ServerPerks classes, so you need the ServerPerks sources for your version and a rebuild. After changes, rename the package so players with the old version in their cache do not get Package version mismatch.

News and Rules tabs

Two tabs with scrolling text appear in the Esc menu. Create a NewsTab class in the ServerPerks package (and an identical RulesTab with a RulesLine array):

class NewsTab extends MidGamePanel;

var automated GUISectionBackground i_BGSec;
var automated GUIScrollTextBox lb_Text;
var() config array<string> NewsLine;

function InitComponent(GUIController MyController, GUIComponent MyOwner)
{
    local int i;
    Super.InitComponent(MyController, MyOwner);
    lb_Text.OnDraw = OnLabelDraw;
    for (i = NewsLine.Length - 1; i >= 0; i--)
        lb_Text.AddText(NewsLine[i]);
    lb_Text.AddText(" ");
    lb_Text.MyScrollBar.GripPos = 0;
    lb_Text.MyScrollBar.CurPos = 0;
    lb_Text.MyScrollBar.MyList.SetTopItem(0);
    lb_Text.MyScrollBar.AlignThumb();
}

function bool OnLabelDraw(Canvas C)
{
    C.Style = 1;
    C.Font = class'ROHUD'.Static.GetSmallMenuFont(C);
    return false;
}

defaultproperties
{
    Begin Object Class=GUISectionBackground Name=BGSec
        Caption="Server News"
        WinTop=0.018
        WinLeft=0.019
        WinWidth=0.961
        WinHeight=0.799
        bFillClient=True
        OnPreDraw=BGSec.InternalPreDraw
    End Object
    i_BGSec=GUISectionBackground'BGSec'
    Begin Object Class=GUIScrollTextBox Name=InfoText
        CharDelay=0.0025
        WinTop=0.082
        WinLeft=0.052
        WinWidth=0.945
        WinHeight=0.76
        bNoTeletype=True
        bBoundToParent=True
        bScaleToParent=True
        bNeverFocus=True
        OnCreateComponent=InfoText.InternalOnCreateComponent
    End Object
    lb_Text=GUIScrollTextBox'InfoText'
    NewsLine(0)="23.02.2012 Gorefast armour buffed"
    NewsLine(1)="20.02.2012 M79 damage updated"
}

Lines are printed in reverse order (the loop runs from the end of the array), so fill them accordingly. Then hook the tabs up in SRInvasionLoginMenu.InitComponent and its defaultproperties:

Panels[5].ClassName = string(Class'RulesTab');
Panels[6].ClassName = string(Class'NewsTab');
...
Panels(5)=(Caption="Rules",Hint="Server rules")
Panels(6)=(Caption="News",Hint="Server news")

For Cyrillic or other non-Latin text, save NewsTab.uc and RulesTab.uc as Unicode (UCS-2), for example in Notepad++: the game will not show them from an ANSI file. To avoid rebuilding all of ServerPerks for every news item, scar later moved the tabs into a separate NewsRules mutator. The idea comes from a Tripwire forum thread.

The same applies to translating ServerPerks itself. From the "ServerPerks - нужна русификация" (ServerPerks needs a Russian translation) thread on the killingfloor.ru forum: if a translated ServerPerks v6.1 shows garbage characters instead of Cyrillic letters, save the translation files in UCS-2 Little Endian encoding (for example, in Notepad++) and set the game language to Russian in the settings (Freddy, Titanishu).

A local clock in the scoreboard

The time appears above the scoreboard (Tab) header, and each player sees their own local time. Two classes change.

SRPlayerReplicationInfo updates the time on the client once a second:

class SRPlayerReplicationInfo extends KFPlayerReplicationInfo;

var string PlayerLocalTime;

simulated event PostBeginPlay()
{
    SetTimer(1.0, true);
    Super.PostBeginPlay();
}

simulated function Timer()
{
    if (Role < ROLE_Authority)
        PlayerLocalTime = GetLocalTimeString();
    Super.Timer();
}

simulated function string GetLocalTimeString()
{
    local string Hour, Minute, Second;
    Hour = string(Level.Hour);     if (Level.Hour < 10)   Hour = "0" $ Hour;
    Minute = string(Level.Minute); if (Level.Minute < 10) Minute = "0" $ Minute;
    Second = string(Level.Second); if (Level.Second < 10) Second = "0" $ Second;
    return Hour $ ":" $ Minute $ ":" $ Second;
}

If your ServerPerks has no such class, create it and plug it in through ServerPerksMut.CheckReplacement:

if (Controller(Other) != None)
    Controller(Other).PlayerReplicationInfoClass = Class'SRPlayerReplicationInfo';

SRScoreBoard.UpdateScoreBoard: draw the clock in a large font before the header, and move the header line (difficulty, wave, map, time) down two lines (HeaderOffsetY += 2 * YL; instead of HeaderOffsetY = Canvas.ClipY * 0.11;):

S = SRPlayerReplicationInfo(OwnerPRI).PlayerLocalTime;
Canvas.Font = class'ROHud'.static.GetLargeMenuFont(Canvas);
Canvas.TextSize(S, XL, YL);
Canvas.DrawColor = HUDClass.default.RedColor;
Canvas.Style = ERenderStyle.STY_Normal;
HeaderOffsetY = Canvas.ClipY * 0.11;
Canvas.SetPos(0.5 * (Canvas.ClipX - XL), HeaderOffsetY);
Canvas.DrawTextClipped(S);

Flame noted the same can be done without touching ServerPerks: catch the Tab key and draw the clock from your own Interaction.

A clan tag and logo above players

The clan name is drawn above a player's head in their colour, along with the clan logo. The data lives in an ini:

[ServerPerksMut.ClanLogoMut]
PlayerDataList=(Hash="76561198051378449",ClanName="FlameClan",ColorR=255,ColorG=0,ColorB=0,ColorA=0)
ClanDataList=(ClanName="FlameClan",ClanTexture="Asylum_T.sky.Moon",ClanTextureW=40,ClanTextureH=40,clanX=0,clanY=0,logoX=0,logoY=10)

Hash is the player's SteamID64, ClanTexture is the logo texture (package.group.name), and clanX/clanY and logoX/logoY offset the text and the picture.

  1. Structs and replication in SRPlayerReplicationInfo:
struct PlayerData { var string Hash; var string ClanName; var int ColorR, ColorG, ColorB, ColorA; };
struct ClanData
{
    var string ClanName;
    var string ClanTexture;
    var int ClanTextureW, ClanTextureH;
    var int clanX, clanY;
    var int logoX, logoY;
};
var ClanData myClanData;
var string ClanName;
var int ColorR, ColorG, ColorB, ColorA;

replication
{
    reliable if (bNetDirty && Role == ROLE_Authority)
        myClanData, ClanName, ColorR, ColorG, ColorB, ColorA;
}
  1. A ClanLogoMut mutator (best placed inside ServerPerksMut, with settings in ServerPerks.ini). In PostBeginPlay it adds the logo packages to AddToPackageMap so clients download them. In CheckReplacement it queues new players and, 0.1 s later, looks up each player's clan by GetPlayerIDHash() and fills in myClanData, ClanName and the colour.
  2. Drawing in SRHUDKillingFloor.DrawPlayerInfo (declare the class with dependson(SRPlayerReplicationInfo)): the clan name is drawn in the player's colour before the perk icon, the logo after it:
if (SRPRI != none && SRPRI.ClanName != "")
{
    C.SetDrawColor(SRPRI.ColorR, SRPRI.ColorG, SRPRI.ColorB, SRPRI.ColorA);
    C.StrLen(SRPRI.ClanName, sXL, sYL);
    C.SetPos(ScreenLocX - sXL * 0.83 - 10 + cData.clanX, ScreenLocY - sYL * 2.95 + cData.clanY);
    C.DrawTextClipped(SRPRI.ClanName);
}
...
if (SRPRI.ClanName != "" && cData.ClanTexture != "")
{
    TempMaterial = Texture(DynamicLoadObject(cData.ClanTexture, class'Texture'));
    if (TempMaterial != none)
    {
        C.SetPos(ScreenLocX - cData.ClanTextureW - 62 + cData.logoX, ScreenLocY - cData.ClanTextureH / 2 - 34 + cData.logoY);
        C.DrawTile(TempMaterial, cData.ClanTextureW, cData.ClanTextureH, 0, 0, TempMaterial.MaterialUSize(), TempMaterial.MaterialVSize());
    }
}

TWI and ServerPerks trader categories together

Since patch 1054 the stock trader splits weapons into categories, while ServerPerks has its own, broader ones (often per perk). Flame linked both panels so they stay in sync: clicking a perk icon at the top opens the matching category below and vice versa, and the first weapon of a category scrolls to the top of the list. Three classes change:

  • SRGUIBuyMenu creates the top panel (var automated SRBuyMenuFilter newBuyMenuFilter;) and in InitTabs() links the panels: newBuyMenuFilter.downPanel = buyMenu; buyMenu.upPanel = newBuyMenuFilter;;
  • SRKFTab_BuyMenu (the lower category panel) tells the top panel which perk to highlight when a category is chosen;
  • SRBuyMenuFilter (the top perk panel) opens the matching category below when clicked.

The full code of the three classes is long and depends on the ServerPerks version; start from the stock KFTab_BuyMenu and the perk filter of your ServerPerks.

Other ServerPerks edits

A trader weapon that unlocks at a perk level

AkycTuk's method (killingfloor.ru forum, "Блокируем оружие в магазине, по типу DLC" thread): the weapon is visible in the trader but locked with a caption, like a DLC item, until the perk reaches the required level. Two ServerPerks files change.

  1. In SRVeterancyTypes add the variables and, before defaultproperties, the function (here PerkIndex == 3 is Commando, level 6):
var() localized string DisableTag6lvl;
var() localized string DisableDescription6lvl;
static function bool AllowWeaponInTrader6lvl(class<KFWeaponPickup> Pickup, KFPlayerReplicationInfo KFPRI, byte Level)
{
 if(Pickup == class'M41APickup')
 {
 if((KFPRI.ClientVeteranSkill.default.PerkIndex == 3) && KFPRI.ClientVeteranSkillLevel >=6)
 {
 return true;
 }
 else
 {
 return false;
 }
 }
 return true;
}

plus the captions in defaultproperties (the description reads "The weapon becomes available at perk level 6"):

DisableTag6lvl="6 Lvl"
DisableDescription6lvl="Оружие будет доступно, при получении 6-го уровня перка."
  1. In SRBuyMenuSaleList.UpdateForSaleBuyables() declare local class<SRVeterancyTypes> Blocker, Blocker6lvl; and, after the stock Blocker check, insert the same check for your function (the archived thread lost the [i] index after CachePerks to forum markup; the code needs it):
Blocker6lvl = none;
 for( i=0; i<CPRL.CachePerks.Length; ++i )
 if( !CPRL.CachePerks[i].PerkClass.Static.AllowWeaponInTrader6lvl(ForSalePickup,KFPRI,CPRL.CachePerks[i].CurrentLevel) )
 {
 Blocker6lvl = CPRL.CachePerks[i].PerkClass;
 break;
 }
 if( Blocker6lvl!=None && Blocker6lvl.Default.DisableTag6lvl=="" )
 continue;

and below the if( DLCLocked==0 && Blocker!=None ) block, the lock marker:

if((DLCLocked == 0) && Blocker6lvl != none)
 {
 ForSaleBuyable.ItemCategorie = (Blocker6lvl.default.DisableTag6lvl $ ":") $ Blocker6lvl.default.DisableDescription6lvl;
 DLCLocked = 3;
 }

The author's advice: keep the weapons in a separate package compiled above ServerPerks rather than in one package with the perks, or the package may fail to compile.

Dual pistols in the ServerPerks 7.50 trader

Flame's article ("Добавляем двойные пистолеты в магазин. Нюансы покупки и продажи" thread). Without the fix, selling dual pistols sells both at once, the single pistol stays in the trader after buying the duals, and weight and price can be wrong. In SP 7.50 it is enough to edit the DualWeaponsManager class so that defaultproperties holds strings rather than classes (the weapon package may compile after SP) and the functions get the classes through DynamicLoadObject:

class DualWeaponsManager extends Object
 abstract;
struct FDualList
{
 var string Single,Dual;
};
var array<FDualList> DualMap;
static final function bool IsDualWeapon( class<Weapon> W, optional out class<KFWeapon> SingleType )
{
 local int i;
 local class<KFWeapon> SingeClass, DualClass;
 if( W.Default.DemoReplacement!=None )
 {
 SingleType = class<KFWeapon>(W.Default.DemoReplacement);
 return true;
 }
 for( i=(Default.DualMap.Length-1); i>=0; --i )
 {
 DualClass=Class<KFWeapon>(DynamicLoadObject(Default.DualMap[i].Dual,Class'Class'));
 if( W==DualClass )
 {
 SingeClass=Class<KFWeapon>(DynamicLoadObject(Default.DualMap[i].Single,Class'Class'));
 SingleType = SingeClass;
 return true;
 }
 }
 return false;
}
static final function bool HasDualies( class<Weapon> W, Inventory InvList, optional out class<KFWeapon> DualType )
{
 local int i;
 local Inventory In;
 local class<KFWeapon> SingeClass, DualClass;
 for ( In=InvList; In!=None; In=In.Inventory )
 {
 if( Weapon(In)!=None && Weapon(In).DemoReplacement==W )
 {
 DualType = class<KFWeapon>(In.Class);
 return true;
 }
 }
 for( i=(Default.DualMap.Length-1); i>=0; --i )
 {
 SingeClass=Class<KFWeapon>(DynamicLoadObject(Default.DualMap[i].Single,Class'Class'));
 if( W==SingeClass )
 {
 DualClass=Class<KFWeapon>(DynamicLoadObject(Default.DualMap[i].Dual,Class'Class'));
 DualType = DualClass;
 W = DualClass;
 for ( In=InvList; In!=None; In=In.Inventory )
 {
 if( In.Class==W )
 return true;
 }
 return false;
 }
 }
 return false;
}
defaultproperties
{
 DualMap(0)=(Single="KFMod.Single",Dual="KFMod.Dualies")
 DualMap(1)=(Single="KFMod.Magnum44Pistol",Dual="KFMod.Dual44Magnum")
 DualMap(2)=(Single="KFMod.Deagle",Dual="KFMod.DualDeagle")
 DualMap(3)=(Single="KFMod.FlareRevolver",Dual="KFMod.DualFlareRevolver")
 DualMap(4)=(Single="KFMod.MK23Pistol",Dual="KFMod.DualMK23Pistol")
 DualMap(5)=(Single="SteamPunkMagnum.SteamPunkMagnum44Pistol",Dual="SteamPunkMagnum.Dual44SteamPunkMagnum")
}

Add your own pairs as new DualMap(N) lines. For SP 7.10 Flame edited other classes (SRBuyMenuSaleList, another trader class and the player pawn class).

Faster custom skin loading

From Flame's article "Ускорение загрузки Custom скинов" ("[Цикл статей] Server Perks и его редактирование" thread). In the auto state RepSetup of ClientPerkRepLink, the server first sends the client the trader weapon list and only then the custom skin data. Swap the two blocks and skins load much faster:

 // Send client all the custom characters.
 while( ClientAckSkinNum<CustomChars.Length )
 {
 ClientReceiveChar(CustomChars[ClientAckSkinNum],ClientAckSkinNum);
 Sleep(0.15f);
 }
 // Now MAKE SURE client receives the full inventory list.
 while( ClientAccknowledged[0]<ShopInventory.Length || ClientAccknowledged[1]<ShopCategories.Length )
 {
 for( SendIndex=0; SendIndex<ShopInventory.Length; ++SendIndex )
 {
 ClientReceiveWeapon(SendIndex,ShopInventory[SendIndex].PC,ShopInventory[SendIndex].CatNum);
 Sleep(0.1f);
 }
 for( SendIndex=0; SendIndex<ShopCategories.Length; ++SendIndex )
 {
 ClientReceiveCategory(SendIndex,ShopCategories[SendIndex]);
 Sleep(0.1f);
 }
 ClientSendAcknowledge();
 Sleep(1.f);
 }

The downside: when a player spawns, the logs show messages about loading the trader weapons.

Hidden admin

From Flame's article "Скрытый админ" in the same thread:

  • Join as a spectator unnoticed. Copy JoinedAsSpectatorOnly from KFPlayerController into KFPCServ and comment out the broadcast:
function JoinedAsSpectatorOnly()
{
 if ( Pawn != None )
 Pawn.Died(self, class'DamageType', Pawn.Location);
 if ( PlayerReplicationInfo.Team != None )
 PlayerReplicationInfo.Team.RemoveFromTeam(self);
 PlayerReplicationInfo.Team = None;
 ServerSpectate();
 //BroadcastLocalizedMessage(Level.Game.GameMessageClass, 14, PlayerReplicationInfo);
 ClientBecameSpectator();
}

To hide the message only for admins, keep the call but wrap it in an ID check: Hash=GetPlayerIDHash(); and if(Hash!="76561198051378449" && Hash!="76561198051378448") before BroadcastLocalizedMessage(...).

  • No login message. If Admin+ or AdminControl is installed, edit their code; otherwise create your own admin class without the broadcasts and swap it in with Level.Game.AccessControl.AdminClass = class'SRAdmin'; in ServerPerksMut.PostBeginPlay():
class SRAdmin extends Admin;
function DoLogin( string Username, string Password )
{
 if (Level.Game.AccessControl.AdminLogin(Outer, Username, Password))
 bAdmin = true;
}
function DoLogout()
{
 local bool bWasSilent;
 bWasSilent = Outer.PlayerReplicationInfo.bSilentAdmin;
 if (Level.Game.AccessControl.AdminLogout(Outer))
 bAdmin = false;
}

Renaming ServerPerks

Flame recommends renaming the SP packages (for example to newServerPerks, newServerPerksP, newServerPerksMut) to avoid conflicts with a stock ServerPerks on the client and when joining other servers. Besides the folders and package names you must fix every package reference in defaultproperties, for example in KFPCServ: LobbyMenuClassString="newServerPerks.SRLobbyMenu" and PawnClass=Class'newServerPerks.SRHumanPawn', and the menu element references in GUIHTMLTextBox, SRGUIBuyMenu, SRInvasionLoginMenu, SRKFTab_BuyMenu, SRKFTab_Perks, SRLobbyFooter and SRLobbyMenu ("[Цикл статей] Server Perks и его редактирование" thread).

Third-person camera

AkycTuk ("Как поправить камеру при виде от 3 лица [ответ]" thread): the camera position comes from GetShoulderCam in the KFPCServ class of the ServerPerks package. He moved the camera back by changing one value in this line:

CamPos = vect(-160,20,10) >> Rotation;

In newer ServerPerks versions the "enhanced" shoulder view is switched off in ServerPerks.ini with bEnhancedShoulderView=False (warpel). Stein warned that in SP 6.75, with bEnhancedShoulderView=False, third-person recoil becomes "static", and on some servers players exploit this to land headshots easily.

One mutator, two servers

If two servers from one folder need different ServerPerks settings, Flame suggests putting several identical mutator classes with different names into the package, sharing one settings file; each gets its own ini section ("ServerPerks на два сервера" thread):

Class ServerPerksMut_01 extends Mutator Config(ServerPerks);
Class ServerPerksMut_02 extends Mutator Config(ServerPerks);
[ServerPerksMut.ServerPerksMut_01]
...
[ServerPerksMut.ServerPerksMut_02]
...

Your own HUD textures (money, ammo, wave)

From the "Edit" thread on the killingfloor.ru forum. krestps: make a copy of a package such as KillingFloorHUD.utx, draw your textures, pack them back into a .utx under your own name (for example myhud) and hook it up through a mutator, so it plays normally online on your server. LLIePLLIeHb: you can also override HUDKillingFloor (for example in ServerPerksv5) and put the paths to your textures there; ServerPerks already overrides it by default as SRHUDKillingFloor.