Мутаторы и UnrealScript

Интерфейс ServerPerks: вкладки «Новости» и «Правила», часы в таблице, клановая надпись и категории магазина

Четыре доработки интерфейса, которые на форуме killingfloor.ru выкладывали scar (2012) и Flame (2013-2014). Все они правят классы ServerPerks, поэтому нужны исходники вашей версии ServerPerks и пересборка пакетов. После правок меняйте имя пакета, чтобы у игроков со старой версией в кэше не было Package version mismatch.

Вкладки «Новости» и «Правила»

В меню по Esc добавляются две вкладки с прокручиваемым текстом. Создайте в пакете ServerPerks класс NewsTab (и такой же RulesTab с массивом RulesLine):

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 Усилили броню горфастам"
    NewsLine(1)="20.02.2012 Обновили урон M79"
}

Строки выводятся в обратном порядке (цикл идёт с конца массива), учитывайте это при заполнении. Затем подключите вкладки в SRInvasionLoginMenu.InitComponent и 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")

Для кириллицы сохраните NewsTab.uc и RulesTab.uc в кодировке Unicode (UCS-2), например через Notepad++: в ANSI игра русские буквы не покажет. Чтобы не пересобирать весь ServerPerks при каждой новости, scar позже вынес вкладки в отдельный мутатор NewsRules. Идея основана на теме с форума Tripwire.

То же относится к переводу самого ServerPerks. Из темы «ServerPerks - нужна русификация» (форум killingfloor.ru): если после перевода ServerPerks v6.1 вместо русских букв видны «иероглифы», сохраните файлы с переводами в кодировке UCS-2 Little Endian (например, в Notepad++) и выставьте в настройках игры русский язык (Freddy, Titanishu).

Локальные часы в таблице игроков

Над заголовком таблицы (Tab) показывается время, и у каждого игрока своё, локальное. Правятся два класса.

SRPlayerReplicationInfo считает время на клиенте раз в секунду:

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;
}

Если такого класса в вашем ServerPerks нет, создайте его и подключите в ServerPerksMut.CheckReplacement:

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

SRScoreBoard.UpdateScoreBoard: перед выводом заголовка нарисуйте часы крупным шрифтом, а строку заголовка (сложность, волна, карта, время) сдвиньте ниже на две строки (HeaderOffsetY += 2 * YL; вместо 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 отмечал, что то же можно сделать без правки ServerPerks: ловить нажатие Tab и рисовать часы в собственном Interaction.

Клановая надпись и логотип над игроком

Над головой игрока рисуется название клана его цветом и картинка-логотип клана. Данные хранятся в 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 — SteamID64 игрока, ClanTexture — текстура логотипа (пакет.группа.имя), clanX/clanY и logoX/logoY — сдвиги надписи и картинки.

  1. Структуры и репликация в 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. Мутатор ClanLogoMut (удобнее положить в ServerPerksMut и хранить настройки в ServerPerks.ini). В PostBeginPlay он добавляет пакеты логотипов в AddToPackageMap, чтобы клиенты их скачали. В CheckReplacement ставит новых игроков в очередь и через 0.1 с ищет по GetPlayerIDHash() клан игрока, заполняет его myClanData, ClanName и цвет.
  2. Отрисовка в SRHUDKillingFloor.DrawPlayerInfo (класс объявляется с dependson(SRPlayerReplicationInfo)): перед иконкой перка выводится название клана цветом игрока, а после — логотип:
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 и ServerPerks вместе

С патча 1054 стандартный магазин делит оружие на категории, а у ServerPerks свои, более общие (часто по перкам). Flame связал обе панели, чтобы они синхронизировались: клик по значку перка вверху раскрывает нужную категорию внизу, и наоборот, а первое оружие категории прокручивается в начало списка. Правятся три класса:

  • SRGUIBuyMenu создаёт верхнюю панель (var automated SRBuyMenuFilter newBuyMenuFilter;) и в InitTabs() «знакомит» панели: newBuyMenuFilter.downPanel = buyMenu; buyMenu.upPanel = newBuyMenuFilter;;
  • SRKFTab_BuyMenu (нижняя панель с категориями) при выборе категории сообщает верхней, какой перк подсветить;
  • SRBuyMenuFilter (верхняя панель с перками) при клике раскрывает соответствующую категорию внизу.

Код всех трёх классов длинный и зависит от версии ServerPerks; за основу берите стандартные KFTab_BuyMenu и фильтр перков из вашего ServerPerks.

Другие правки ServerPerks

Оружие в магазине открывается с уровня перка

Способ AkycTuk (форум killingfloor.ru, тема «Блокируем оружие в магазине, по типу DLC»): оружие видно в магазине, но заблокировано с подписью, как DLC, пока перк не достигнет нужного уровня. Правятся два файла ServerPerks.

  1. В SRVeterancyTypes добавьте переменные и перед defaultproperties функцию (здесь PerkIndex == 3 — командос, уровень 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;
}

и подписи в defaultproperties:

DisableTag6lvl="6 Lvl"
DisableDescription6lvl="Оружие будет доступно, при получении 6-го уровня перка."
  1. В SRBuyMenuSaleList.UpdateForSaleBuyables() объявите local class<SRVeterancyTypes> Blocker, Blocker6lvl;, после стандартной проверки Blocker вставьте такую же для своей функции (в архиве темы индекс [i] после CachePerks пропал из-за форумной разметки, в коде он нужен):
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;

а ниже блока if( DLCLocked==0 && Blocker!=None ) — пометку блокировки:

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

Совет автора: оружие держите в отдельном пакете, который компилируется выше ServerPerks, а не в одном пакете с перками, иначе пакет может не собраться.

Двойные пистолеты в магазине ServerPerks 7.50

Статья Flame (тема «Добавляем двойные пистолеты в магазин. Нюансы покупки и продажи»). Без правки при продаже двойных пистолетов продаются оба сразу, при покупке двойных одинарный остаётся в магазине, бывают ошибки с весом и ценой. В SP 7.50 достаточно поправить класс DualWeaponsManager так, чтобы в defaultproperties были строки, а не классы (пакет с оружием может компилироваться после SP), а функции получали классы через 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")
}

Свои пары добавляйте новыми строками DualMap(N). Для SP 7.10 Flame правил другие классы (SRBuyMenuSaleList, ещё один класс магазина и класс тела игрока).

Кастомные скины загружаются быстрее

Из статьи Flame «Ускорение загрузки Custom скинов» (тема «[Цикл статей] Server Perks и его редактирование»). В автоматическом состоянии RepSetup класса ClientPerkRepLink сервер сначала пересылает клиенту список оружия магазина и только потом данные о кастомных скинах. Поменяйте блоки местами, и скины будут загружаться значительно быстрее:

 // 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);
 }

Минус: при появлении игрока в логах появляются сообщения о подгрузке оружия магазина.

Скрытый админ

Из статьи Flame «Скрытый админ» в той же теме:

  • Незаметный вход зрителем. Скопируйте JoinedAsSpectatorOnly из KFPlayerController в KFPCServ и закомментируйте рассылку сообщения:
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();
}

Чтобы сообщение пропадало только для админов, оставьте вызов, но обёрните его проверкой ID: Hash=GetPlayerIDHash(); и if(Hash!="76561198051378449" && Hash!="76561198051378448") перед BroadcastLocalizedMessage(...).

  • Без сообщения о логине. Если стоят Admin+ или AdminControl, правьте их код; иначе создайте свой класс админа без рассылки сообщений и подмените стандартный в ServerPerksMut.PostBeginPlay() строкой Level.Game.AccessControl.AdminClass = class'SRAdmin';:
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;
}

Переименование ServerPerks

Flame советует переименовывать пакеты SP (например, в newServerPerks, newServerPerksP, newServerPerksMut), чтобы не было конфликтов со стандартным ServerPerks у клиента и при заходе на другие серверы. Кроме папок и имён пакетов придётся поправить все ссылки на пакет в defaultproperties, например в KFPCServ: LobbyMenuClassString="newServerPerks.SRLobbyMenu" и PawnClass=Class'newServerPerks.SRHumanPawn', а также ссылки на элементы меню в классах GUIHTMLTextBox, SRGUIBuyMenu, SRInvasionLoginMenu, SRKFTab_BuyMenu, SRKFTab_Perks, SRLobbyFooter, SRLobbyMenu (тема «[Цикл статей] Server Perks и его редактирование»).

Камера от третьего лица

AkycTuk (тема «Как поправить камеру при виде от 3 лица [ответ]»): за положение камеры отвечает функция GetShoulderCam в классе KFPCServ пакета ServerPerks. Он отодвинул камеру, поменяв одно значение в строке:

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

В новых версиях ServerPerks «улучшенный» вид через плечо отключается в ServerPerks.ini параметром bEnhancedShoulderView=False (warpel). Stein предупредил, что в SP 6.75 при bEnhancedShoulderView=False отдача от третьего лица становится «статичной», и на некоторых серверах этим пользуются, чтобы легко попадать в голову.

Одни настройки — два сервера

Если два сервера из одной папки должны иметь разные настройки ServerPerks, Flame предлагает положить в пакет несколько одинаковых классов мутатора с разными именами и общим файлом настроек; у каждого будет своя секция в ini (тема «ServerPerks на два сервера»):

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

Свои текстуры HUD (деньги, патроны, волна)

Из темы «Edit» (форум killingfloor.ru). krestps: сделайте копию пакета, например KillingFloorHUD.utx, нарисуйте свои текстуры, упакуйте их обратно в .utx под своим именем (например, myhud) и подключайте мутатором - так сервер нормально играется по сети. LLIePLLIeHb: можно также переопределить HUDKillingFloor (например, в ServerPerksv5) и прописать там пути к своим текстурам; в ServerPerks он уже переопределён по умолчанию и называется SRHUDKillingFloor.