• Рекомендуем зарегистрироваться для просмотра контента и скачивания файлов!

WebAdmin и кириллица(WebAdminCyrillicMut)

denfil777

Патриарх
Команда форума
Регистрация
12 Янв 2024
Сообщения
74
Реакции
10
Баллы
8
Написал небольшой мутатор, который заменяет пару функций в паре классов у вебадмина
После этого можно и писать и читать в вебадмине под виндой и линуксом.
Хотя в принципе можно было бы не заменять классы, а сделать мутатор использующий тот же BroadcastHandler, как и здесь

У мутатора 1 переменная в ини файле - isWindows
Если сервер работает под виндой - ставим true, иначе false

Важно!
Настраивал мутатор при следующей настройке в UWeb.int

[WebResponse]
CharSet="windows-1251"

Для другой кодировки возможно потребуется другой сдвиг кода символа. Если у кого-то не будет работать мутатор - сделаю версию, где можно будет руками настраивать сдвиг.
Код:
class WebAdminCyrillicMut extends Mutator config(WebAdminCyrillicMut);

var config bool isWindows;

function PostBeginPlay()
{
    SaveConfig();
    class'XWebAdmin.UTServerAdmin'.default.QueryHandlerClasses[0]="WebAdminCyrillicMut.newxWebQueryCurrent";
    class'XWebAdmin.UTServerAdmin'.default.SpectatorType=Class'WebAdminCyrillicMut.newUTServerAdminSpectator';
    class'WebAdminCyrillicMut.newUTServerAdminSpectator'.default.isWindows=isWindows;
}

defaultproperties
{
    isWindows=true
}

Код:
class newUTServerAdminSpectator extends UTServerAdminSpectator
    config;
var bool isWindows;
function string ConvertString(string msg)
{
    local int i;
    local string tmp;
    local string result;
    local int code;
    tmp=msg;
    for(i=0; i<Len(msg); i++)
    {
        code=Asc(tmp);
        if(code>1000) code-=848;
        if(i==0) result=Chr(code);
        else result=result$Chr(code);
        tmp=Mid(tmp,1);
    }
    return result;
}

function string FormatMessage(PlayerReplicationInfo PRI, String Text, name Type)
{
    local string Message;
    local string cPlayerName;
    if(PRI!=none)
        cPlayerName=PRI.PlayerName;
    if(!isWindows)
    {
        Text=ConvertString(Text);
        if(PRI!=none)
            cPlayerName=ConvertString(PRI.PlayerName);
    }
    if (PRI != None)
    {
        if (Type == 'Say' && PRI == PlayerReplicationInfo)
            Message = Text;
        else if (Type == 'Say')
            Message = cPlayerName$": "$Text;
        else if (Type == 'TeamSay')
            Message = "["$cPlayerName$"]: "$Text;
        else
            Message = "("$Type$") "$Text;
    }
    else if (Type == 'Console')
        Message = "WebAdmin:"@Text;
    else
        Message = "("$Type$") "$Text;

    return Message;
}


Код:
class newxWebQueryCurrent extends xWebQueryCurrent
    config;

function string ConvertString(string msg)
{
    local int i;
    local string tmp;
    local string result;
    local int code;
    tmp=msg;
    for(i=0; i<Len(msg); i++)
    {
        code=Asc(tmp);
        if(code>191 && code<400) code+=848;
        if(i==0) result=Chr(code);
        else result=result$Chr(code);
        tmp=Mid(tmp,1);
    }
    return result;
}

function QueryCurrentConsole(WebRequest Request, WebResponse Response)
{
    local String SendStr, OutStr;
    if (CanPerform("Xc"))
    {
        SendStr = Request.GetVariable("SendText", "");
        SendStr=ConvertString(SendStr);//Flame

        if (SendStr != "" && !(Left(SendStr, 6) ~= "debug " || SendStr ~= "debug"))
        {
            if (Left(SendStr, 4) ~= "say ")
                Level.Game.Broadcast(Spectator, Mid(SendStr, 4), 'Say');
            else if (SendStr ~= "pause")
            {
                if (Level.Pauser == None)
                    Level.Pauser = Spectator.PlayerReplicationInfo;
                else Level.Pauser = None;
            }
            else if (SendStr ~= "dump")
                Spectator.Dump();

            else if ((Left(SendStr, 4) ~= "get " || Left(SendStr,4) ~= "set ") &&
            (InStr(Caps(SendStr), "XADMINCONFIG") != -1 || !CanPerform("Ms")) )
            {
                if ( InStr(Caps(SendStr), "XADMINCONFIG") != -1 )
                {
                    StatusError(Response, ConsoleUserlist);
                    ShowMessage(Response, Error, "");
                    log("User attempted to modify or enumerate admin account information illegally using the webadmin console.  User:"$Request.Username$".",'WebAdmin');
                }

                else if ( !CanPerform("Ms") )
                    AccessDenied(Response);
            }
            else
            {
                OutStr = Level.ConsoleCommand(SendStr);
                if (OutStr != "")
                    Spectator.AddMessage(None, OutStr, 'Console');
            }
        }

        Response.Subst("LogURI", CurrentConsoleLogPage);
        Response.Subst("SayURI", CurrentConsoleSendPage);
        ShowPage(Response, CurrentConsolePage);
    }
    else
        AccessDenied(Response);
}

function QueryCurrentPlayers(WebRequest Request, WebResponse Response)
{
    local string Sort, PlayerListSubst, TempStr, TempTag, TempData;
    local string TableHeaders, GameType, Reverse, ColorNames[2], Last;
    local StringArray    PlayerList;
    local Controller P, NextP;
    local int i, Cols, mlength;
    local string IP, ID;
    local bool bCanKick, bCanBan, bCanKickBots;

    Response.Subst("Section", CurrentLinks[1]);
    Response.Subst("PostAction", CurrentPlayersPage);
    ColorNames[0] = class'TeamInfo'.default.ColorNames[0];
    ColorNames[1] = class'TeamInfo'.default.ColorNames[1];
    MLength = int(Eval(Len(ColorNames[0]) > Len(ColorNames[1]), string(Len(ColorNames[0])), string(Len(ColorNames[1]))));

    if (CanPerform("Xp|Kp|Kb|Ko"))
    {
        PlayerList = new(None) class'SortedStringArray';

        Sort = Request.GetVariable("Sort", "Name");
        Last = Request.GetVariable("Last");
        Response.Subst("Sort", Sort);
        Cols = 0;

        bCanKick = CanPerform("Kp");
        bCanBan = CanPerform("Kb");
        bCanKickBots = CanPerform("Ko|Mb");
        if (Last == Sort && Request.GetVariable("ReverseSort") == "")
        {
            PlayerList.ToggleSort();
            Reverse = "?ReverseSort=True";
        }

        else Reverse = "";

        // Count the number of Columns allowed
        if (bCanKick || bCanBan || bCanKickBots)
        {
        // Use 'do-while' to avoid access-none when destroying Controllers within the loop
            P = Level.ControllerList;
            if (P != None)
            {
                do {
                    NextP = P.NextController;
                    if(        PlayerController(P) != None
                        &&    P.PlayerReplicationInfo != None
                        &&    NetConnection(PlayerController(P).Player) != None)
                    {
                        if ( bCanBan && Request.GetVariable("Ban" $ string(P.PlayerReplicationInfo.PlayerID)) != "" )
                            Level.Game.AccessControl.KickBanPlayer(PlayerController(P));

                        //if _RO_
                        else if ( bCanBan && Request.GetVariable("Session" $ string(P.PlayerReplicationInfo.PlayerID)) != "" )
                            Level.Game.AccessControl.BanPlayer(PlayerController(P), true);
                        //end _RO_

                        else if ( bCanKick && Request.GetVariable("Kick" $ string(P.PlayerReplicationInfo.PlayerID)) != "" )
                            Level.Game.AccessControl.KickPlayer(PlayerController(P));
                    }

                    else if ( PlayerController(P) == None && bCanKickBots && P.PlayerReplicationInfo != None &&
                            Request.GetVariable("Kick" $ string(P.PlayerReplicationInfo.PlayerID)) != "")
                    {    // Kick Bots
                        P.Destroy();
                    }
                    P = NextP;
                } until (P == None);
            }

            if (bCanKick || bCanKickBots) Cols += 1;
            if (bCanBan) Cols += 2;

            Response.Subst("KickButton", SubmitButton("Kick", KickButtonText[Cols-1]));

            // Build of valid TableHeaders
            TableHeaders = "";
            if (bCanKick || bCanKickBots)
            {
                Response.Subst("HeadTitle", "Kick");
                TableHeaders $= WebInclude(PlayerListHeader);
            }

            if (bCanBan)
            {
                //if _RO_
                Response.Subst("HeadTitle", "Session");
                TableHeaders $= WebInclude(PlayerListHeader);
                //end _RO_

                Response.Subst("HeadTitle", "Ban");
                TableHeaders $= WebInclude(PlayerListHeader);
            }

            if (Sort ~= "Name") Response.Subst("ReverseSort", Reverse);
            else Response.Subst("ReverseSort", "");
            Response.Subst("HeadTitle", "Name");
            TableHeaders $= WebInclude(PlayerListLinkedHeader);

            if (Level.Game.GameReplicationInfo.bTeamGame)
            {
                if (Sort ~= "Team")    Response.Subst("ReverseSort", Reverse);
                else Response.Subst("ReverseSort", "");
                Response.Subst("HeadTitle", "Team");
                TableHeaders $= WebInclude(PlayerListLinkedHeader);
            }

            if (Sort ~= "Ping")    Response.Subst("ReverseSort", Reverse);
            else Response.Subst("ReverseSort", "");
            Response.Subst("HeadTitle", "Ping");
            TableHeaders $= WebInclude(PlayerListLinkedHeader);

            if (Sort ~= "Score") Response.Subst("ReverseSort", Reverse);
            else Response.Subst("ReverseSort", "");
            Response.Subst("HeadTitle", "Score");
            TableHeaders $= WebInclude(PlayerListLinkedHeader);

            //if _RO_
            Response.Subst("HeadTitle", "Team Kills");
            TableHeaders $= WebInclude(PlayerListHeader);
            //end _RO_

            Response.Subst("HeadTitle", "IP");
            TableHeaders $= WebInclude(PlayerListHeader);

            // evo ---
            if (Level.Game.AccessControl.bBanbyID)
            {
                Response.Subst("HeadTitle", "Global ID");
                TableHeaders $= WebInclude(PlayerListHeader);
            }
            // --- evo

            Response.Subst("TableHeaders", TableHeaders);
        }

        if (CanPerform("Ms"))
        {
            GameType = Level.GetItemName(SetGamePI(GameType));
            if (GamePI != None && GamePI.Settings[GamePI.FindIndex(GameType$".MinPlayers")].SecLevel <= CurAdmin.MaxSecLevel())
            {
                if ((Request.GetVariable("SetMinPlayers", "") != "") && UnrealMPGameInfo(Level.Game) != None)
                {
                    UnrealMPGameInfo(Level.Game).MinPlayers = Min(Max(int(Request.GetVariable("MinPlayers", String(0))), 0), 32);
                    Level.Game.SaveConfig();
                }

                Response.Subst("MinPlayers", string(UnrealMPGameInfo(Level.Game).MinPlayers));
                Response.Subst("MinPlayerPart", WebInclude(PlayerListMinPlayers));
            }

            else
            {
                Response.Subst("MinPlayers", "");
                Response.Subst("MinPlayersPart", "");
            }
        }

        for (P=Level.ControllerList; P!=None; P=P.NextController)
        {
            TempData = "";
            if (!P.bDeleteMe && P.bIsPlayer && P.PlayerReplicationInfo != None)
            {
                Response.Subst("Content", CheckBox("Kick" $ string(P.PlayerReplicationInfo.PlayerID), False));
                if (CanPerform("Kp"))
                    TempData $= WebInclude(CellCenter);

                if (CanPerform("Kb"))
                {
                    //if _RO_
                    if ( PlayerController(P) != None )
                    {
                        Response.Subst("Content", Checkbox("Session" $ string(P.PlayerReplicationInfo.PlayerID), False));
                        TempData $= WebInclude(CellCenter);
                        Response.Subst("Content", Checkbox("Ban" $ string(P.PlayerReplicationInfo.PlayerID), False));
                        TempData $= WebInclude(CellCenter);
                    }
                    else
                    {
                        Response.Subst("Content", "");
                        TempData $= WebInclude(CellCenter)$WebInclude(CellCenter);
                    }
                    //else
                    //if ( PlayerController(P) != None )
                    //    Response.Subst("Content", Checkbox("Ban" $ string(P.PlayerReplicationInfo.PlayerID), False));
                    //else Response.Subst("Content", "");
                    //TempData $= WebInclude(CellCenter);
                    //end _RO_
                }

                TempStr = "";
                if (DeathMatch(Level.Game) != None && DeathMatch(Level.Game).bTournament && P.PlayerReplicationInfo.bReadyToPlay)
                    TempStr = " (Ready) ";

                else if (P.PlayerReplicationInfo.bIsSpectator)
                    TempStr = " (Spectator) ";

                else if (PlayerController(P) == None)
                    TempStr = " (Bot) ";

                if( PlayerController(P) != None )
                {
                    IP = PlayerController(P).GetPlayerNetworkAddress();
                    IP = HtmlEncode(" " $ Left(IP, InStr(IP, ":")));
                    // evo ---
                    ID = HtmlEncode(" " $ Eval(Level.Game.AccessControl.bBanbyID, PlayerController(P).GetPlayerIDHash(), " "));
                    // --- evo
                }

                else
                {
                    IP = HtmlEncode("  ");
                    ID = HtmlEncode("  ");
                }

                Response.Subst("Content", HtmlEncode(ConvertString(P.PlayerReplicationInfo.PlayerName) $ TempStr));
                TempData $= WebInclude(NowrapLeft);

                if (Level.Game.bTeamGame)
                {
                    if (P.PlayerReplicationInfo.Team != None && P.PlayerReplicationInfo.Team.TeamIndex < 4)
                        Response.Subst("Content", "<span style='background-color: "$class'TeamInfo'.default.ColorNames[P.PlayerReplicationInfo.Team.TeamIndex]$"'>"$HtmlEncode("  ")$"</span>"$HtmlEncode(P.PlayerReplicationInfo.Team.GetHumanReadableName()));

                    else if (P.PlayerReplicationInfo.bIsSpectator)
                        Response.Subst("Content", HtmlEncode("  "));

                    TempData $= WebInclude(NowrapCenter);
                }

                Response.Subst("Content", string(P.PlayerReplicationInfo.Ping*4));
                TempData $= WebInclude(CellCenter);

                Response.Subst("Content", string(int(P.PlayerReplicationInfo.Score)));
                TempData $= WebInclude(CellCenter);

                //if _RO_
                Response.Subst("Content", string(P.PlayerReplicationInfo.FFKills));
                TempData $= WebInclude(CellCenter);
                //end _RO_

                Response.Subst("Content", IP);
                TempData $= WebInclude(CellCenter);

                if (Level.Game.AccessControl.bBanbyID)
                {
                    Response.Subst("Content", ID);
                    TempData $= WebInclude(CellCenter);
                }

                switch (Sort)
                {
                    case "Name":
                        TempTag = ConvertString(P.PlayerReplicationInfo.PlayerName); break;
                    case "Team":    // Ordered by Team, then subordered by last selected sort method
                        TempTag = PadRight(class'TeamInfo'.default.ColorNames[P.PlayerReplicationInfo.Team.TeamIndex],MLength,"0");
                        switch (Last)
                        {
                            case "Name":
                                TempTag $= ConvertString(P.PlayerReplicationInfo.PlayerName); break;
                            case "Ping":
                                TempTag $= PadLeft(string(P.PlayerReplicationInfo.Ping*4), 5, "0"); break;
                            default:
                                TempTag $= PadLeft(string(int(P.PlayerReplicationInfo.Score)), 4, "0"); break;
                        }
                        break;
                    case "Ping":
                        TempTag = PadLeft(string(P.PlayerReplicationInfo.Ping*4), 5, "0"); break;
                    default:
                        TempTag = PadLeft(string(int(P.PlayerReplicationInfo.Score)), 4, "0"); break;
                }

                Response.Subst("RowContent", TempData);
                PlayerList.Add( WebInclude(RowLeft), TempTag);
            }
        }

        PlayerListSubst = "";
        if (PlayerList.Count() > 0)
        {
            for ( i=0; i<PlayerList.Count(); i++)
            {
                if (Sort ~= "Score")
                    PlayerListSubst = PlayerList.GetItem(i) $ PlayerListSubst;

                else PlayerListSubst $= PlayerList.GetItem(i);
            }
        }

        else
        {
            Response.Subst("SpanContent", NoPlayersConnected);
            Response.Subst("SpanLength", "6");
            Response.Subst("RowContent", WebInclude(CellColSpan));
            PlayerListSubst = WebInclude(RowCenter);
        }

        Response.Subst("PlayerList", PlayerListSubst);
        Response.Subst("MinPlayers", string(UnrealMPGameInfo(Level.Game).MinPlayers));

        Response.Subst("PageHelp", NotePlayersPage);
        MapTitle(Response);
        ShowPage(Response, CurrentPlayersPage);
    }
    else
        AccessDenied(Response);
}

function QueryCurrentGame(WebRequest Request, WebResponse Response)
{
local StringArray    ExcludeMaps, IncludeMaps, MovedMaps;
local class<GameInfo> GameClass;
local string NewGameType, SwitchButtonName, GameState, NewMap;
local bool bMakeChanges;
local Controller C;
local xPlayer XP;
local TeamPlayerReplicationInfo PRI;
local int MultiKills, Sprees, GameIndex;

    if (CanPerform("Mt|Mm"))
    {
        if (Request.GetVariable("SwitchGameTypeAndMap", "") != "")
        {
            if (CanPerform("Mt"))
                ServerChangeMap(Request, Response, Request.GetVariable("MapSelect"), Request.GetVariable("GameTypeSelect"));

            else AccessDenied(Response);

            return;
        }

        else if (Request.GetVariable("SwitchMap", "") != "")
        {
            if (CanPerform("Mm|Mt"))
            {
                NewMap = Request.GetVariable("MapSelect");
                Level.ServerTravel(NewMap$"?game="$Level.Game.Class$"?mutator="$UsedMutators(), false);
                ShowMessage(Response, WaitTitle, Repl(MapChangingTo, "%MapName%", NewMap));
            }

            else AccessDenied(Response);

            return;
        }

        bMakeChanges = (Request.GetVariable("ApplySettings", "") != "");
        if (CanPerform("Mt") && (bMakeChanges || Request.GetVariable("SwitchGameType", "") != ""))
        {
            NewGameType = Request.GetVariable("GameTypeSelect");
            GameClass = class<GameInfo>(DynamicLoadObject(NewGameType, class'Class'));
        }
        else GameClass = None;

        if (GameClass == None)
        {
            GameClass = Level.Game.Class;
            NewGameType = String(GameClass);
        }

        GameIndex = Level.Game.MaplistHandler.GetGameIndex(NewGameType);
        ExcludeMaps = ReloadExcludeMaps(NewGameType);
        IncludeMaps = ReloadIncludeMaps(ExcludeMaps, GameIndex, Level.Game.MaplistHandler.GetActiveList(GameIndex));

        GameState = "";
        // Show game status if admin has necessary privs
        if (CanPerform("Ma"))
        {
            if (Level.Game.NumPlayers > 0)
            {
                for (C = Level.ControllerList; C != None; C = C.NextController)
                {
                    MultiKills = 0;
                    Sprees = 0;
                    PRI = None;
                    XP = xPlayer(C);
                    if (XP != None && !XP.bDeleteMe)
                    {
                        if (TeamPlayerReplicationInfo(XP.PlayerReplicationInfo) != None)
                            PRI = TeamPlayerReplicationInfo(XP.PlayerReplicationInfo);

                        if (PRI != None)
                        {
                            Response.Subst("PlayerName", HtmlEncode(ConvertString(PRI.PlayerName)));
                            Response.Subst("Kills", string(PRI.Kills));
                            //if _RO_
                            Response.Subst("FFKills", string(PRI.FFKills));
                            //end _RO_
                            Response.Subst("Deaths", string(PRI.Deaths));
                            Response.Subst("Suicides",string(PRI.Suicides));
                            //if _RO_
                            /*
                            //end _RO_
                            for (i = 0; i < 7; i++)
                                MultiKills += PRI.MultiKills[i];
                            Response.Subst("MultiKills", string(MultiKills));
                            for (i = 0; i < 6; i++)
                                Sprees += PRI.Spree[i];
                            Response.Subst("Sprees", string(Sprees));
                            //if _RO_
                            */
                            //end _RO_
                            GameState $= WebInclude(StatTableRow);
                        }
                    }
                }
            }
            else
                //if _RO_
                GameState = "<tr><td colspan=\"5\" align=\"center\">"@NoPlayersConnected@"</td></tr>";
                //else
                //GameState = "<tr><td colspan=\"6\" align=\"center\">"@NoPlayersConnected@"</td></tr>";
                //end _RO_

            Response.Subst("StatRows", GameState);
            Response.Subst("GameState", WebInclude(StatTable));
        }

        if (GameClass == Level.Game.Class)
        {
            SwitchButtonName="SwitchMap";
            MovedMaps = New(None) Class'SortedStringArray';
            MovedMaps.CopyFromId(IncludeMaps, IncludeMaps.FindTagId(Left(string(Level), InStr(string(Level), "."))));
        }
        else SwitchButtonName="SwitchGameTypeAndMap";

        if (CanPerform("Mt"))
        {
            Response.Subst("Content", Select("GameTypeSelect", GenerateGameTypeOptions(NewGameType)));
            Response.Subst("GameTypeButton", SubmitButton("SwitchGameType", SwitchText));
        }
        else Response.Subst("Content", Level.Game.Default.GameName);

        Response.Subst("GameTypeSelect", WebInclude(CellLeft));
        Response.Subst("Content", Select("MapSelect", GenerateMapListSelect(IncludeMaps, MovedMaps)));
        Response.Subst("MapSelect", WebInclude(CellLeft));
        Response.Subst("MapButton", SubmitButton(SwitchButtonName, SwitchText));
        Response.Subst("PostAction", CurrentGamePage);

        Response.Subst("Section", CurrentLinks[0]);
        Response.Subst("PageHelp", NoteGamePage);
        MapTitle(Response);
        ShowPage(Response, CurrentGamePage);
    }
    else AccessDenied(Response);
}

defaultproperties
{
}

Ссылка
Добавлять в виде:WebAdminCyrillicMut.WebAdminCyrillicMut
Замечание: Тем у кого не работает мутатор.
Убедитесь, что у вас нет мутаторов, которые правят

class'XWebAdmin.UTServerAdmin'.default.QueryHandlerClasses[0]

Если хотите использовать мутатор совместно с AdminControlv2 - проверьте, чтобы в AdminControlv2.ini была следующая строка

bSupportWebAdmin=false
Версия с использованием BroadcastHandler:

Код для загрузки мутатора: WebAdminCyrillicMutv2.WebAdminCyrillicMut.
Код:
class WebAdminCyrillicMut extends Mutator config(WebAdminCyrillicMut);

var config bool isWindows;
var WACBroadcastHandler WACBHandler;

simulated function PostBeginPlay()
{
    local KFGameType KFGT;
    local BroadcastHandler BH;
  
    Super.PostBeginPlay();
  
    KFGT = KFGameType(Level.Game);
    BH = KFGT.BroadcastHandler;
  
    while ( BH != none )
    {
        if ( BH.NextBroadcastHandler == none )
        {
            WACBHandler = Spawn(class'WACBroadcastHandler',Self);
            WACBHandler.isWindows = isWindows;
            BH.NextBroadcastHandler = WACBHandler;
            break;
        }
        BH = BH.NextBroadcastHandler;
    }
}

defaultproperties
{
    isWindows=true

     GroupName="KF-WebAdminCyrillicMut"
     FriendlyName="WebAdminCyrillicMut"
     Description="Fixes appearance of russian text sent from and to WebAdmin."
}

Код:
class WACBroadcastHandler extends BroadcastHandler;

var bool isWindows;

function BroadcastText( PlayerReplicationInfo SenderPRI, PlayerController Receiver, coerce string Msg, optional name Type )
{
    if ( SenderPRI != Receiver.PlayerReplicationInfo ) // Не перекодировать сообщения самому себе
    {
        if ( SenderPRI.PlayerName ~= "WebAdmin" && SenderPRI.PlayerID == 0 )
        {
            Msg = ConvertStringOut(Msg);
        }
        else if ( Receiver.PlayerReplicationInfo.PlayerName ~= "WebAdmin" && Receiver.PlayerReplicationInfo.PlayerID == 0 )
        {
            if ( !isWindows )
            {
                Msg = ConvertStringIn(Msg);
            }
        }
    }
  
    if ( NextBroadcastHandler != None )
    {
        NextBroadcastHandler.BroadcastText( SenderPRI, Receiver, Msg, Type );
    }
    else
    {
        Receiver.TeamMessage( SenderPRI, Msg, Type );
    }
}

Ссылка
 
Последнее редактирование:
Сверху Снизу