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.
- 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;
}
- A
ClanLogoMutmutator (best placed insideServerPerksMut, with settings inServerPerks.ini). InPostBeginPlayit adds the logo packages toAddToPackageMapso clients download them. InCheckReplacementit queues new players and, 0.1 s later, looks up each player's clan byGetPlayerIDHash()and fills inmyClanData,ClanNameand the colour. - Drawing in
SRHUDKillingFloor.DrawPlayerInfo(declare the class withdependson(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:
SRGUIBuyMenucreates the top panel (var automated SRBuyMenuFilter newBuyMenuFilter;) and inInitTabs()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.
- In
SRVeterancyTypesadd the variables and, beforedefaultproperties, the function (herePerkIndex == 3is 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-го уровня перка."
- In
SRBuyMenuSaleList.UpdateForSaleBuyables()declarelocal class<SRVeterancyTypes> Blocker, Blocker6lvl;and, after the stockBlockercheck, insert the same check for your function (the archived thread lost the[i]index afterCachePerksto 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
JoinedAsSpectatorOnlyfromKFPlayerControllerintoKFPCServand 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';inServerPerksMut.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.