Skip to content

Kapitola 8.4: Přidání chatových příkazů ​


Shrnutí: Tento tutoriál vás provede vytvořením systému chatových příkazů pro DayZ. Napojíte se na chatový vstup, zpracujete prefixy příkazů a argumenty, ověříte oprávnění administrátora, provedete akci na straně serveru a odešlete zpětnou vazbu hráči. Na konci budete mít funkční příkaz /heal, který plně vyléčí postavu administrátora, spolu s frameworkem pro přidávání dalších příkazů.


Obsah ​


Co budeme vytvářet ​

Systém chatových příkazů s:

  • /heal -- Plně vyléčí postavu administrátora (zdraví, krev, šok, hlad, žízeň)
  • /heal JménoHráče -- Vyléčí konkrétního hráče podle jména
  • Znovupoužitelným frameworkem pro přidání /kill, /teleport, /time, /weather a jakéhokoli dalšího příkazu
  • Ověřováním oprávnění administrátora, aby běžní hráči nemohli používat administrátorské příkazy
  • Provedením na straně serveru se zpětnou vazbou do chatu

Předpoklady ​

Struktura modu pro tento tutoriál ​

ChatCommands/
    mod.cpp
    Scripts/
        config.cpp
        3_Game/
            ChatCommands/
                CCmdRPC.c
                CCmdBase.c
                CCmdRegistry.c
        4_World/
            ChatCommands/
                CCmdServerHandler.c
                commands/
                    CCmdHeal.c
        5_Mission/
            ChatCommands/
                CCmdChatHook.c

Přehled architektury ​

Chatové příkazy sledují tento tok:

KLIENT                                  SERVER
------                                  ------

1. Admin napíše "/heal" do chatu
2. Chat hook zachytí zprávu
   (zabrání odeslání jako normální chat)
3. Klient odešle příkaz přes RPC  ---->  4. Server přijme RPC
                                            Ověří oprávnění administrátora
                                            Vyhledá handler příkazu
                                            Provede příkaz
                                        5. Server odešle zpětnou vazbu  ---->  KLIENT
                                            (chatová zpráva RPC)
                                                                     6. Admin vidí
                                                                        zpětnou vazbu v chatu

Proč zpracovávat příkazy na serveru? Protože server má autoritu nad stavem hry. Pouze server může spolehlivě léčit hráče, měnit počasí, teleportovat postavy a upravovat stav světa. Role klienta je omezena na detekci příkazu a jeho přeposlání.


Krok 1: Napojení na chatový vstup ​

Potřebujeme zachytit chatové zprávy před tím, než jsou odeslány jako běžný chat. DayZ pro tento účel poskytuje třídu ChatInputMenu.

Přístup chat hooku ​

Budeme moddovat třídu MissionGameplay pro zachycení událostí chatového vstupu. Když hráč odešle chatovou zprávu začínající /, zachytíme ji, zabráníme odeslání jako normální chat a místo toho ji odešleme jako příkazové RPC na server.

Vytvořte Scripts/5_Mission/ChatCommands/CCmdChatHook.c ​

c
modded class MissionGameplay
{
    // -------------------------------------------------------
    // Zachycení chatových zpráv začínajících /
    // -------------------------------------------------------
    override void OnEvent(EventType eventTypeId, Param params)
    {
        super.OnEvent(eventTypeId, params);

        // ChatMessageEventTypeID se spustí, když hráč odešle chatovou zprávu
        if (eventTypeId == ChatMessageEventTypeID)
        {
            ChatMessageEventParams chatParams;
            if (Class.CastTo(chatParams, params))
            {
                string message = chatParams.param3;

                // Kontrola, zda začíná /
                if (message.Length() > 0 && message.Substring(0, 1) == "/")
                {
                    // Toto je příkaz -- odeslat na server
                    SendChatCommand(message);
                }
            }
        }
    }

    // -------------------------------------------------------
    // Odeslání řetězce příkazu na server přes RPC
    // -------------------------------------------------------
    protected void SendChatCommand(string fullCommand)
    {
        Man player = GetGame().GetPlayer();
        if (!player)
            return;

        Print("[ChatCommands] Sending command to server: " + fullCommand);

        Param1<string> data = new Param1<string>(fullCommand);
        GetGame().RPCSingleParam(player, CCmdRPC.COMMAND_REQUEST, data, true);
    }

    // -------------------------------------------------------
    // Příjem zpětné vazby příkazu ze serveru
    // -------------------------------------------------------
    override void OnRPC(PlayerIdentity sender, Object target, int rpc_type, ParamsReadContext ctx)
    {
        super.OnRPC(sender, target, rpc_type, ctx);

        if (rpc_type == CCmdRPC.COMMAND_FEEDBACK)
        {
            Param2<string, string> data = new Param2<string, string>("", "");
            if (ctx.Read(data))
            {
                string prefix = data.param1;
                string message = data.param2;

                // Zobrazení zpětné vazby jako systémové chatové zprávy
                GetGame().Chat(prefix + " " + message, "colorStatusChannel");

                Print("[ChatCommands] Feedback: " + prefix + " " + message);
            }
        }
    }
};

Jak funguje zachycení chatu ​

Metoda OnEvent na MissionGameplay se volá pro různé herní události. Když je eventTypeId roven ChatMessageEventTypeID, znamená to, že hráč právě odeslal chatovou zprávu. Parametry jsou typu ChatMessageEventParams (což je Param4<int, string, string, string>) a obsahují:

  • param1 -- Kanál (int): chatový kanál (globální, přímý atd.)
  • param2 -- Jméno odesílatele (string)
  • param3 -- Text zprávy (string)
  • param4 -- Konfigurační třída barvy (string)

Kontrolujeme, zda zpráva začíná /. Pokud ano, přepošleme celý řetězec na server přes RPC. Zpráva je stále odeslána i jako normální chat -- v produkčním modu byste ji potlačili (popsáno v poznámkách na konci).


Krok 2: Parsování prefixu příkazu a argumentů ​

Na straně serveru potřebujeme rozdělit řetězec příkazu jako /heal JménoHráče na jeho části: název příkazu (heal) a argumenty (["JménoHráče"]).

Vytvořte Scripts/3_Game/ChatCommands/CCmdRPC.c ​

c
class CCmdRPC
{
    static const int COMMAND_REQUEST  = 79001;
    static const int COMMAND_FEEDBACK = 79002;
};

Vytvořte Scripts/3_Game/ChatCommands/CCmdBase.c ​

c
// -------------------------------------------------------
// Základní třída pro všechny chatové příkazy
// -------------------------------------------------------
class CCmdBase
{
    // Název příkazu bez prefixu / (např. "heal")
    string GetName()
    {
        return "";
    }

    // Krátký popis zobrazený v nápovědě nebo seznamu příkazů
    string GetDescription()
    {
        return "";
    }

    // Syntaxe použití zobrazená při nesprávném použití příkazu
    string GetUsage()
    {
        return "/" + GetName();
    }

    // Zda tento příkaz vyžaduje oprávnění administrátora
    bool RequiresAdmin()
    {
        return true;
    }

    // Provedení příkazu na serveru
    // Vrací true při úspěchu, false při selhání
    bool Execute(PlayerIdentity caller, array<string> args)
    {
        return false;
    }

    // -------------------------------------------------------
    // Pomocník: Odeslání zpětné vazby volajícímu příkazu
    // -------------------------------------------------------
    protected void SendFeedback(PlayerIdentity caller, string prefix, string message)
    {
        if (!caller)
            return;

        // Nalezení objektu hráče volajícího
        ref array<Man> players = new array<Man>;
        GetGame().GetPlayers(players);

        Man callerPlayer = null;
        for (int i = 0; i < players.Count(); i++)
        {
            Man candidate = players.Get(i);
            if (candidate && candidate.GetIdentity())
            {
                if (candidate.GetIdentity().GetId() == caller.GetId())
                {
                    callerPlayer = candidate;
                    break;
                }
            }
        }

        if (callerPlayer)
        {
            Param2<string, string> data = new Param2<string, string>(prefix, message);
            GetGame().RPCSingleParam(callerPlayer, CCmdRPC.COMMAND_FEEDBACK, data, true, caller);
        }
    }

    // -------------------------------------------------------
    // Pomocník: Nalezení hráče podle částečné shody jména
    // -------------------------------------------------------
    protected Man FindPlayerByName(string partialName)
    {
        ref array<Man> players = new array<Man>;
        GetGame().GetPlayers(players);

        string searchLower = partialName;
        searchLower.ToLower();

        for (int i = 0; i < players.Count(); i++)
        {
            Man man = players.Get(i);
            if (man && man.GetIdentity())
            {
                string playerName = man.GetIdentity().GetName();
                string playerNameLower = playerName;
                playerNameLower.ToLower();

                if (playerNameLower.Contains(searchLower))
                    return man;
            }
        }

        return null;
    }
};

Vytvořte Scripts/3_Game/ChatCommands/CCmdRegistry.c ​

c
// -------------------------------------------------------
// Registr obsahující všechny dostupné příkazy
// -------------------------------------------------------
class CCmdRegistry
{
    protected static ref map<string, ref CCmdBase> s_Commands;

    // -------------------------------------------------------
    // Inicializace registru (zavolat jednou při spuštění)
    // -------------------------------------------------------
    static void Init()
    {
        if (!s_Commands)
            s_Commands = new map<string, ref CCmdBase>;
    }

    // -------------------------------------------------------
    // Registrace instance příkazu
    // -------------------------------------------------------
    static void Register(CCmdBase command)
    {
        if (!s_Commands)
            Init();

        if (!command)
            return;

        string name = command.GetName();
        name.ToLower();

        if (s_Commands.Contains(name))
        {
            Print("[ChatCommands] WARNING: Command '" + name + "' already registered, overwriting.");
        }

        s_Commands.Set(name, command);
        Print("[ChatCommands] Registered command: /" + name);
    }

    // -------------------------------------------------------
    // Vyhledání příkazu podle názvu
    // -------------------------------------------------------
    static CCmdBase GetCommand(string name)
    {
        if (!s_Commands)
            return null;

        string nameLower = name;
        nameLower.ToLower();

        CCmdBase cmd;
        if (s_Commands.Find(nameLower, cmd))
            return cmd;

        return null;
    }

    // -------------------------------------------------------
    // Získání všech registrovaných názvů příkazů
    // -------------------------------------------------------
    static array<string> GetCommandNames()
    {
        ref array<string> names = new array<string>;

        if (s_Commands)
        {
            for (int i = 0; i < s_Commands.Count(); i++)
            {
                names.Insert(s_Commands.GetKey(i));
            }
        }

        return names;
    }

    // -------------------------------------------------------
    // Parsování surového řetězce příkazu na název + argumenty
    // Příklad: "/heal JménoHráče" --> name="heal", args=["JménoHráče"]
    // -------------------------------------------------------
    static void ParseCommand(string fullCommand, out string commandName, out array<string> args)
    {
        args = new array<string>;
        commandName = "";

        if (fullCommand.Length() == 0)
            return;

        // Odebrání úvodního /
        string raw = fullCommand;
        if (raw.Substring(0, 1) == "/")
            raw = raw.Substring(1, raw.Length() - 1);

        // Rozdělení podle mezer
        raw.Split(" ", args);

        if (args.Count() > 0)
        {
            commandName = args.Get(0);
            commandName.ToLower();
            args.RemoveOrdered(0);
        }
    }
};

Vysvětlení logiky parsování ​

Pro vstup /heal SomePlayer metoda ParseCommand provede:

  1. Odebere úvodní / a získá "heal SomePlayer"
  2. Rozdělí podle mezer a získá ["heal", "SomePlayer"]
  3. Vezme první prvek jako název příkazu: "heal"
  4. Odebere ho z pole, zbydou argumenty: ["SomePlayer"]

Název příkazu se převede na malá písmena, takže /Heal, /HEAL a /heal všechny fungují.


Krok 3: Ověření oprávnění administrátora ​

Ověření oprávnění administrátora zabraňuje běžným hráčům provádět administrátorské příkazy. DayZ nemá vestavěný systém oprávnění administrátora ve skriptech, takže kontrolujeme proti jednoduchému seznamu administrátorů.

Kontrola administrátora v handleru serveru ​

Nejjednodušší přístup je kontrola Steam64 ID hráče proti seznamu známých ID administrátorů. V produkčním modu byste tento seznam načítali z konfiguračního souboru.

c
// Jednoduchá kontrola administrátora -- v produkci načtěte z JSON konfiguračního souboru
static bool IsAdmin(PlayerIdentity identity)
{
    if (!identity)
        return false;

    // Kontrola plain ID hráče (Steam64 ID)
    string playerId = identity.GetPlainId();

    // Hardkódovaný seznam administrátorů -- v produkci nahraďte načítáním konfiguračního souboru
    ref array<string> adminIds = new array<string>;
    adminIds.Insert("76561198000000001");    // Nahraďte skutečnými Steam64 ID
    adminIds.Insert("76561198000000002");

    return (adminIds.Find(playerId) != -1);
}

Kde najít Steam64 ID ​

  • Otevřete svůj Steam profil v prohlížeči
  • URL obsahuje vaše Steam64 ID: https://steamcommunity.com/profiles/76561198XXXXXXXXX
  • Nebo použijte nástroj jako https://steamid.io pro vyhledání jakéhokoli hráče

Oprávnění produkční úrovně ​

Ve skutečném modu byste:

  1. Uložili ID administrátorů do JSON souboru ($profile:ChatCommands/admins.json)
  2. Načetli soubor při startu serveru
  3. Podporovali úrovně oprávnění (moderátor, administrátor, superadministrátor)
  4. Použili framework jako systém MyPermissions z MyMod Core pro hierarchická oprávnění

Krok 4: Provedení akce na straně serveru ​

Nyní vytvoříme skutečný příkaz /heal a handler serveru, který zpracovává příchozí příkazová RPC.

Vytvořte Scripts/4_World/ChatCommands/commands/CCmdHeal.c ​

c
class CCmdHeal extends CCmdBase
{
    override string GetName()
    {
        return "heal";
    }

    override string GetDescription()
    {
        return "Fully heals a player (health, blood, shock, hunger, thirst)";
    }

    override string GetUsage()
    {
        return "/heal [PlayerName]";
    }

    override bool RequiresAdmin()
    {
        return true;
    }

    // -------------------------------------------------------
    // Provedení příkazu heal
    // /heal         --> vyléčí volajícího
    // /heal Name    --> vyléčí pojmenovaného hráče
    // -------------------------------------------------------
    override bool Execute(PlayerIdentity caller, array<string> args)
    {
        if (!caller)
            return false;

        Man targetMan = null;
        string targetName = "";

        // Určení cílového hráče
        if (args.Count() > 0)
        {
            // Vyléčení konkrétního hráče podle jména
            string searchName = args.Get(0);
            targetMan = FindPlayerByName(searchName);

            if (!targetMan)
            {
                SendFeedback(caller, "[Heal]", "Player '" + searchName + "' not found.");
                return false;
            }

            targetName = targetMan.GetIdentity().GetName();
        }
        else
        {
            // Vyléčení sebe sama
            ref array<Man> allPlayers = new array<Man>;
            GetGame().GetPlayers(allPlayers);

            for (int i = 0; i < allPlayers.Count(); i++)
            {
                Man candidate = allPlayers.Get(i);
                if (candidate && candidate.GetIdentity())
                {
                    if (candidate.GetIdentity().GetId() == caller.GetId())
                    {
                        targetMan = candidate;
                        break;
                    }
                }
            }

            if (!targetMan)
            {
                SendFeedback(caller, "[Heal]", "Could not find your player object.");
                return false;
            }

            targetName = "yourself";
        }

        // Provedení léčení
        PlayerBase targetPlayer;
        if (!Class.CastTo(targetPlayer, targetMan))
        {
            SendFeedback(caller, "[Heal]", "Target is not a valid player.");
            return false;
        }

        HealPlayer(targetPlayer);

        // Logování a odeslání zpětné vazby
        Print("[ChatCommands] " + caller.GetName() + " healed " + targetName);
        SendFeedback(caller, "[Heal]", "Successfully healed " + targetName + ".");

        return true;
    }

    // -------------------------------------------------------
    // Aplikace plného léčení na hráče
    // -------------------------------------------------------
    protected void HealPlayer(PlayerBase player)
    {
        if (!player)
            return;

        // Obnovení zdraví na maximum
        player.SetHealth("GlobalHealth", "Health", player.GetMaxHealth("GlobalHealth", "Health"));

        // Obnovení krve na maximum
        player.SetHealth("GlobalHealth", "Blood", player.GetMaxHealth("GlobalHealth", "Blood"));

        // Odstranění poškození šokem
        player.SetHealth("GlobalHealth", "Shock", player.GetMaxHealth("GlobalHealth", "Shock"));

        // Nastavení hladu na plno (hodnota energie)
        // PlayerBase má systém statistik -- nastavení statistiky energie
        player.GetStatEnergy().Set(player.GetStatEnergy().GetMax());

        // Nastavení žízně na plno (hodnota vody)
        player.GetStatWater().Set(player.GetStatWater().GetMax());

        // Vyčištění všech zdrojů krvácení
        player.GetBleedingManagerServer().RemoveAllSources();

        Print("[ChatCommands] Healed player: " + player.GetIdentity().GetName());
    }
};

Proč 4_World? ​

Příkaz heal odkazuje na PlayerBase, který je definován ve vrstvě 4_World. Také používá metody statistik hráče (GetStatEnergy, GetStatWater, GetBleedingManagerServer), které jsou dostupné pouze na světových entitách. Příkaz musí žít ve 4_World nebo výše.

Základní třída CCmdBase žije v 3_Game, protože neodkazuje na žádné světové typy. Konkrétní třídy příkazů, které pracují se světovými entitami, žijí ve 4_World.


Krok 5: Odeslání zpětné vazby administrátorovi ​

Zpětná vazba je zpracována metodou SendFeedback() v CCmdBase. Sledujme kompletní cestu zpětné vazby:

Server odesílá zpětnou vazbu ​

c
// Uvnitř CCmdBase.SendFeedback()
Param2<string, string> data = new Param2<string, string>(prefix, message);
GetGame().RPCSingleParam(callerPlayer, CCmdRPC.COMMAND_FEEDBACK, data, true, caller);

Server odesílá RPC COMMAND_FEEDBACK konkrétnímu klientovi, který vydal příkaz. Data obsahují prefix (jako "[Heal]") a text zprávy.

Klient přijímá a zobrazuje zpětnou vazbu ​

Zpět v CCmdChatHook.c (Krok 1), handler OnRPC toto zachytí:

c
if (rpc_type == CCmdRPC.COMMAND_FEEDBACK)
{
    // Deserializace zprávy
    Param2<string, string> data = new Param2<string, string>("", "");
    if (ctx.Read(data))
    {
        string prefix = data.param1;
        string message = data.param2;

        // Zobrazení v chatovém okně
        GetGame().Chat(prefix + " " + message, "colorStatusChannel");
    }
}

GetGame().Chat() zobrazí zprávu v chatovém okně hráče. Druhý parametr je barevný kanál:

KanálBarvaTypické použití
"colorStatusChannel"ModráSystémové zprávy
"colorAction"ŽlutáZpětná vazba akce
"colorFriendly"ZelenáPozitivní zpětná vazba
"colorImportant"ČervenáVarování/chyby

Krok 6: Registrace příkazů ​

Handler serveru přijímá příkazová RPC, vyhledá příkaz v registru a provede ho.

Vytvořte Scripts/4_World/ChatCommands/CCmdServerHandler.c ​

c
modded class MissionServer
{
    // -------------------------------------------------------
    // Registrace všech příkazů při startu serveru
    // -------------------------------------------------------
    override void OnInit()
    {
        super.OnInit();

        CCmdRegistry.Init();

        // Registrace všech příkazů zde
        CCmdRegistry.Register(new CCmdHeal());

        // Přidání dalších příkazů:
        // CCmdRegistry.Register(new CCmdKill());
        // CCmdRegistry.Register(new CCmdTeleport());
        // CCmdRegistry.Register(new CCmdTime());

        Print("[ChatCommands] Server initialized. Commands registered.");
    }
};

// -------------------------------------------------------
// RPC handler na straně serveru pro příchozí příkazy
// -------------------------------------------------------
modded class PlayerBase
{
    override void OnRPC(PlayerIdentity sender, int rpc_type, ParamsReadContext ctx)
    {
        super.OnRPC(sender, rpc_type, ctx);

        if (!GetGame().IsServer())
            return;

        if (rpc_type == CCmdRPC.COMMAND_REQUEST)
        {
            HandleCommandRPC(sender, ctx);
        }
    }

    protected void HandleCommandRPC(PlayerIdentity sender, ParamsReadContext ctx)
    {
        if (!sender)
            return;

        // Čtení řetězce příkazu
        Param1<string> data = new Param1<string>("");
        if (!ctx.Read(data))
        {
            Print("[ChatCommands] ERROR: Failed to read command RPC data.");
            return;
        }

        string fullCommand = data.param1;
        Print("[ChatCommands] Received command from " + sender.GetName() + ": " + fullCommand);

        // Parsování příkazu
        string commandName;
        ref array<string> args;
        CCmdRegistry.ParseCommand(fullCommand, commandName, args);

        if (commandName == "")
            return;

        // Vyhledání příkazu
        CCmdBase command = CCmdRegistry.GetCommand(commandName);
        if (!command)
        {
            SendCommandFeedback(sender, "[Error]", "Unknown command: /" + commandName);
            return;
        }

        // Ověření oprávnění administrátora
        if (command.RequiresAdmin() && !IsCommandAdmin(sender))
        {
            Print("[ChatCommands] Non-admin " + sender.GetName() + " tried to use /" + commandName);
            SendCommandFeedback(sender, "[Error]", "You do not have permission to use this command.");
            return;
        }

        // Provedení příkazu
        bool success = command.Execute(sender, args);

        if (success)
            Print("[ChatCommands] Command /" + commandName + " executed successfully by " + sender.GetName());
        else
            Print("[ChatCommands] Command /" + commandName + " failed for " + sender.GetName());
    }

    // -------------------------------------------------------
    // Ověření, zda je hráč administrátor
    // -------------------------------------------------------
    protected bool IsCommandAdmin(PlayerIdentity identity)
    {
        if (!identity)
            return false;

        string playerId = identity.GetPlainId();

        // ----------------------------------------------------------
        // DŮLEŽITÉ: Nahraďte tyto vašimi skutečnými Steam64 ID administrátorů
        // V produkci místo toho načtěte z JSON konfiguračního souboru
        // ----------------------------------------------------------
        ref array<string> adminIds = new array<string>;
        adminIds.Insert("76561198000000001");
        adminIds.Insert("76561198000000002");

        return (adminIds.Find(playerId) != -1);
    }

    // -------------------------------------------------------
    // Odeslání zpětné vazby konkrétnímu hráči
    // -------------------------------------------------------
    protected void SendCommandFeedback(PlayerIdentity target, string prefix, string message)
    {
        if (!target)
            return;

        ref array<Man> players = new array<Man>;
        GetGame().GetPlayers(players);

        for (int i = 0; i < players.Count(); i++)
        {
            Man candidate = players.Get(i);
            if (candidate && candidate.GetIdentity())
            {
                if (candidate.GetIdentity().GetId() == target.GetId())
                {
                    Param2<string, string> data = new Param2<string, string>(prefix, message);
                    GetGame().RPCSingleParam(candidate, CCmdRPC.COMMAND_FEEDBACK, data, true, target);
                    return;
                }
            }
        }
    }
};

Vzor registrace ​

Příkazy se registrují v MissionServer.OnInit():

c
CCmdRegistry.Init();
CCmdRegistry.Register(new CCmdHeal());

Každé volání Register() vytvoří instanci třídy příkazu a uloží ji do mapy klíčované názvem příkazu. Když dorazí příkazové RPC, handler vyhledá název v registru a zavolá Execute() na odpovídajícím objektu příkazu.

Tento vzor usnadňuje přidávání nových příkazů -- vytvořte novou třídu rozšiřující CCmdBase, implementujte Execute() a přidejte jeden řádek Register().


Krok 7: Přidání do seznamu příkazů administrátorského panelu ​

Pokud máte administrátorský panel (z Kapitoly 8.3), můžete zobrazit seznam dostupných příkazů v UI.

Vyžádání seznamu příkazů ze serveru ​

Přidejte nové RPC ID v CCmdRPC.c:

c
class CCmdRPC
{
    static const int COMMAND_REQUEST   = 79001;
    static const int COMMAND_FEEDBACK  = 79002;
    static const int COMMAND_LIST_REQ  = 79003;
    static const int COMMAND_LIST_RESP = 79004;
};

Strana serveru: Odeslání seznamu příkazů ​

Přidejte tento handler do vašeho kódu na straně serveru:

c
// V handleru serveru přidejte case pro COMMAND_LIST_REQ
if (rpc_type == CCmdRPC.COMMAND_LIST_REQ)
{
    HandleCommandListRequest(sender);
}

protected void HandleCommandListRequest(PlayerIdentity requestor)
{
    if (!requestor)
        return;

    // Sestavení formátovaného řetězce všech příkazů
    array<string> names = CCmdRegistry.GetCommandNames();
    string commandList = "Available Commands:\n";

    for (int i = 0; i < names.Count(); i++)
    {
        CCmdBase cmd = CCmdRegistry.GetCommand(names.Get(i));
        if (cmd)
        {
            commandList = commandList + cmd.GetUsage() + " - " + cmd.GetDescription() + "\n";
        }
    }

    // Odeslání zpět klientovi
    ref array<Man> players = new array<Man>;
    GetGame().GetPlayers(players);

    for (int j = 0; j < players.Count(); j++)
    {
        Man candidate = players.Get(j);
        if (candidate && candidate.GetIdentity() && candidate.GetIdentity().GetId() == requestor.GetId())
        {
            Param1<string> data = new Param1<string>(commandList);
            GetGame().RPCSingleParam(candidate, CCmdRPC.COMMAND_LIST_RESP, data, true, requestor);
            return;
        }
    }
}

Strana klienta: Zobrazení v panelu ​

Na klientu zachyťte odpověď a zobrazte ji v textovém widgetu:

c
if (rpc_type == CCmdRPC.COMMAND_LIST_RESP)
{
    Param1<string> data = new Param1<string>("");
    if (ctx.Read(data))
    {
        string commandList = data.param1;
        // Zobrazení v textovém widgetu administrátorského panelu
        // m_CommandListText.SetText(commandList);
        Print("[ChatCommands] Command list received:\n" + commandList);
    }
}

Kompletní funkční kód: příkaz /heal ​

Zde je každý soubor potřebný pro kompletní funkční systém. Vytvořte tyto soubory a váš mod bude mít funkční příkaz /heal.

Nastavení config.cpp ​

cpp
class CfgPatches
{
    class ChatCommands_Scripts
    {
        units[] = {};
        weapons[] = {};
        requiredVersion = 0.1;
        requiredAddons[] =
        {
            "DZ_Data",
            "DZ_Scripts"
        };
    };
};

class CfgMods
{
    class ChatCommands
    {
        dir = "ChatCommands";
        name = "Chat Commands";
        author = "YourName";
        type = "mod";

        dependencies[] = { "Game", "World", "Mission" };

        class defs
        {
            class gameScriptModule
            {
                value = "";
                files[] = { "ChatCommands/Scripts/3_Game" };
            };
            class worldScriptModule
            {
                value = "";
                files[] = { "ChatCommands/Scripts/4_World" };
            };
            class missionScriptModule
            {
                value = "";
                files[] = { "ChatCommands/Scripts/5_Mission" };
            };
        };
    };
};

3_Game/ChatCommands/CCmdRPC.c ​

c
class CCmdRPC
{
    static const int COMMAND_REQUEST  = 79001;
    static const int COMMAND_FEEDBACK = 79002;
};

3_Game/ChatCommands/CCmdBase.c ​

c
class CCmdBase
{
    string GetName()
    {
        return "";
    }

    string GetDescription()
    {
        return "";
    }

    string GetUsage()
    {
        return "/" + GetName();
    }

    bool RequiresAdmin()
    {
        return true;
    }

    bool Execute(PlayerIdentity caller, array<string> args)
    {
        return false;
    }

    protected void SendFeedback(PlayerIdentity caller, string prefix, string message)
    {
        if (!caller)
            return;

        ref array<Man> players = new array<Man>;
        GetGame().GetPlayers(players);

        Man callerPlayer = null;
        for (int i = 0; i < players.Count(); i++)
        {
            Man candidate = players.Get(i);
            if (candidate && candidate.GetIdentity())
            {
                if (candidate.GetIdentity().GetId() == caller.GetId())
                {
                    callerPlayer = candidate;
                    break;
                }
            }
        }

        if (callerPlayer)
        {
            Param2<string, string> data = new Param2<string, string>(prefix, message);
            GetGame().RPCSingleParam(callerPlayer, CCmdRPC.COMMAND_FEEDBACK, data, true, caller);
        }
    }

    protected Man FindPlayerByName(string partialName)
    {
        ref array<Man> players = new array<Man>;
        GetGame().GetPlayers(players);

        string searchLower = partialName;
        searchLower.ToLower();

        for (int i = 0; i < players.Count(); i++)
        {
            Man man = players.Get(i);
            if (man && man.GetIdentity())
            {
                string playerName = man.GetIdentity().GetName();
                string playerNameLower = playerName;
                playerNameLower.ToLower();

                if (playerNameLower.Contains(searchLower))
                    return man;
            }
        }

        return null;
    }
};

3_Game/ChatCommands/CCmdRegistry.c ​

c
class CCmdRegistry
{
    protected static ref map<string, ref CCmdBase> s_Commands;

    static void Init()
    {
        if (!s_Commands)
            s_Commands = new map<string, ref CCmdBase>;
    }

    static void Register(CCmdBase command)
    {
        if (!s_Commands)
            Init();

        if (!command)
            return;

        string name = command.GetName();
        name.ToLower();

        s_Commands.Set(name, command);
        Print("[ChatCommands] Registered command: /" + name);
    }

    static CCmdBase GetCommand(string name)
    {
        if (!s_Commands)
            return null;

        string nameLower = name;
        nameLower.ToLower();

        CCmdBase cmd;
        if (s_Commands.Find(nameLower, cmd))
            return cmd;

        return null;
    }

    static array<string> GetCommandNames()
    {
        ref array<string> names = new array<string>;

        if (s_Commands)
        {
            for (int i = 0; i < s_Commands.Count(); i++)
            {
                names.Insert(s_Commands.GetKey(i));
            }
        }

        return names;
    }

    static void ParseCommand(string fullCommand, out string commandName, out array<string> args)
    {
        args = new array<string>;
        commandName = "";

        if (fullCommand.Length() == 0)
            return;

        string raw = fullCommand;
        if (raw.Substring(0, 1) == "/")
            raw = raw.Substring(1, raw.Length() - 1);

        raw.Split(" ", args);

        if (args.Count() > 0)
        {
            commandName = args.Get(0);
            commandName.ToLower();
            args.RemoveOrdered(0);
        }
    }
};

4_World/ChatCommands/commands/CCmdHeal.c ​

c
class CCmdHeal extends CCmdBase
{
    override string GetName()
    {
        return "heal";
    }

    override string GetDescription()
    {
        return "Fully heals a player (health, blood, shock, hunger, thirst)";
    }

    override string GetUsage()
    {
        return "/heal [PlayerName]";
    }

    override bool RequiresAdmin()
    {
        return true;
    }

    override bool Execute(PlayerIdentity caller, array<string> args)
    {
        if (!caller)
            return false;

        Man targetMan = null;
        string targetName = "";

        if (args.Count() > 0)
        {
            string searchName = args.Get(0);
            targetMan = FindPlayerByName(searchName);

            if (!targetMan)
            {
                SendFeedback(caller, "[Heal]", "Player '" + searchName + "' not found.");
                return false;
            }

            targetName = targetMan.GetIdentity().GetName();
        }
        else
        {
            ref array<Man> allPlayers = new array<Man>;
            GetGame().GetPlayers(allPlayers);

            for (int i = 0; i < allPlayers.Count(); i++)
            {
                Man candidate = allPlayers.Get(i);
                if (candidate && candidate.GetIdentity())
                {
                    if (candidate.GetIdentity().GetId() == caller.GetId())
                    {
                        targetMan = candidate;
                        break;
                    }
                }
            }

            if (!targetMan)
            {
                SendFeedback(caller, "[Heal]", "Could not find your player object.");
                return false;
            }

            targetName = "yourself";
        }

        PlayerBase targetPlayer;
        if (!Class.CastTo(targetPlayer, targetMan))
        {
            SendFeedback(caller, "[Heal]", "Target is not a valid player.");
            return false;
        }

        HealPlayer(targetPlayer);

        Print("[ChatCommands] " + caller.GetName() + " healed " + targetName);
        SendFeedback(caller, "[Heal]", "Successfully healed " + targetName + ".");

        return true;
    }

    protected void HealPlayer(PlayerBase player)
    {
        if (!player)
            return;

        player.SetHealth("GlobalHealth", "Health", player.GetMaxHealth("GlobalHealth", "Health"));
        player.SetHealth("GlobalHealth", "Blood", player.GetMaxHealth("GlobalHealth", "Blood"));
        player.SetHealth("GlobalHealth", "Shock", player.GetMaxHealth("GlobalHealth", "Shock"));

        player.GetStatEnergy().Set(player.GetStatEnergy().GetMax());
        player.GetStatWater().Set(player.GetStatWater().GetMax());

        player.GetBleedingManagerServer().RemoveAllSources();
    }
};

4_World/ChatCommands/CCmdServerHandler.c ​

c
modded class MissionServer
{
    override void OnInit()
    {
        super.OnInit();

        CCmdRegistry.Init();
        CCmdRegistry.Register(new CCmdHeal());

        Print("[ChatCommands] Server initialized. Commands registered.");
    }
};

modded class PlayerBase
{
    override void OnRPC(PlayerIdentity sender, int rpc_type, ParamsReadContext ctx)
    {
        super.OnRPC(sender, rpc_type, ctx);

        if (!GetGame().IsServer())
            return;

        if (rpc_type == CCmdRPC.COMMAND_REQUEST)
        {
            HandleCommandRPC(sender, ctx);
        }
    }

    protected void HandleCommandRPC(PlayerIdentity sender, ParamsReadContext ctx)
    {
        if (!sender)
            return;

        Param1<string> data = new Param1<string>("");
        if (!ctx.Read(data))
        {
            Print("[ChatCommands] ERROR: Failed to read command RPC data.");
            return;
        }

        string fullCommand = data.param1;
        Print("[ChatCommands] Received command from " + sender.GetName() + ": " + fullCommand);

        string commandName;
        ref array<string> args;
        CCmdRegistry.ParseCommand(fullCommand, commandName, args);

        if (commandName == "")
            return;

        CCmdBase command = CCmdRegistry.GetCommand(commandName);
        if (!command)
        {
            SendCommandFeedback(sender, "[Error]", "Unknown command: /" + commandName);
            return;
        }

        if (command.RequiresAdmin() && !IsCommandAdmin(sender))
        {
            Print("[ChatCommands] Non-admin " + sender.GetName() + " tried to use /" + commandName);
            SendCommandFeedback(sender, "[Error]", "You do not have permission to use this command.");
            return;
        }

        command.Execute(sender, args);
    }

    protected bool IsCommandAdmin(PlayerIdentity identity)
    {
        if (!identity)
            return false;

        string playerId = identity.GetPlainId();

        // NAHRAĎTE TYTO VAŠIMI SKUTEČNÝMI STEAM64 ID ADMINISTRÁTORŮ
        ref array<string> adminIds = new array<string>;
        adminIds.Insert("76561198000000001");
        adminIds.Insert("76561198000000002");

        return (adminIds.Find(playerId) != -1);
    }

    protected void SendCommandFeedback(PlayerIdentity target, string prefix, string message)
    {
        if (!target)
            return;

        ref array<Man> players = new array<Man>;
        GetGame().GetPlayers(players);

        for (int i = 0; i < players.Count(); i++)
        {
            Man candidate = players.Get(i);
            if (candidate && candidate.GetIdentity() && candidate.GetIdentity().GetId() == target.GetId())
            {
                Param2<string, string> data = new Param2<string, string>(prefix, message);
                GetGame().RPCSingleParam(candidate, CCmdRPC.COMMAND_FEEDBACK, data, true, target);
                return;
            }
        }
    }
};

5_Mission/ChatCommands/CCmdChatHook.c ​

c
modded class MissionGameplay
{
    override void OnEvent(EventType eventTypeId, Param params)
    {
        super.OnEvent(eventTypeId, params);

        if (eventTypeId == ChatMessageEventTypeID)
        {
            ChatMessageEventParams chatParams;
            if (Class.CastTo(chatParams, params))
            {
                string message = chatParams.param3;

                if (message.Length() > 0 && message.Substring(0, 1) == "/")
                {
                    SendChatCommand(message);
                }
            }
        }
    }

    protected void SendChatCommand(string fullCommand)
    {
        Man player = GetGame().GetPlayer();
        if (!player)
            return;

        Print("[ChatCommands] Sending command to server: " + fullCommand);

        Param1<string> data = new Param1<string>(fullCommand);
        GetGame().RPCSingleParam(player, CCmdRPC.COMMAND_REQUEST, data, true);
    }

    override void OnRPC(PlayerIdentity sender, Object target, int rpc_type, ParamsReadContext ctx)
    {
        super.OnRPC(sender, target, rpc_type, ctx);

        if (rpc_type == CCmdRPC.COMMAND_FEEDBACK)
        {
            Param2<string, string> data = new Param2<string, string>("", "");
            if (ctx.Read(data))
            {
                string prefix = data.param1;
                string message = data.param2;

                GetGame().Chat(prefix + " " + message, "colorStatusChannel");
                Print("[ChatCommands] Feedback: " + prefix + " " + message);
            }
        }
    }
};

Přidávání dalších příkazů ​

Vzor registru činí přidávání nových příkazů přímočarým. Zde jsou příklady:

Příkaz /kill ​

c
class CCmdKill extends CCmdBase
{
    override string GetName()        { return "kill"; }
    override string GetDescription() { return "Kills a player"; }
    override string GetUsage()       { return "/kill [PlayerName]"; }

    override bool Execute(PlayerIdentity caller, array<string> args)
    {
        Man targetMan = null;

        if (args.Count() > 0)
            targetMan = FindPlayerByName(args.Get(0));
        else
        {
            ref array<Man> players = new array<Man>;
            GetGame().GetPlayers(players);
            for (int i = 0; i < players.Count(); i++)
            {
                if (players.Get(i).GetIdentity() && players.Get(i).GetIdentity().GetId() == caller.GetId())
                {
                    targetMan = players.Get(i);
                    break;
                }
            }
        }

        if (!targetMan)
        {
            SendFeedback(caller, "[Kill]", "Player not found.");
            return false;
        }

        PlayerBase targetPlayer;
        if (Class.CastTo(targetPlayer, targetMan))
        {
            targetPlayer.SetHealth("GlobalHealth", "Health", 0);
            SendFeedback(caller, "[Kill]", "Killed " + targetMan.GetIdentity().GetName() + ".");
            return true;
        }

        return false;
    }
};

Příkaz /time ​

c
class CCmdTime extends CCmdBase
{
    override string GetName()        { return "time"; }
    override string GetDescription() { return "Sets the server time (0-23)"; }
    override string GetUsage()       { return "/time <hour>"; }

    override bool Execute(PlayerIdentity caller, array<string> args)
    {
        if (args.Count() < 1)
        {
            SendFeedback(caller, "[Time]", "Usage: " + GetUsage());
            return false;
        }

        int hour = args.Get(0).ToInt();
        if (hour < 0 || hour > 23)
        {
            SendFeedback(caller, "[Time]", "Hour must be between 0 and 23.");
            return false;
        }

        GetGame().GetWorld().SetDate(2024, 6, 15, hour, 0);
        SendFeedback(caller, "[Time]", "Server time set to " + hour.ToString() + ":00.");
        return true;
    }
};

Registrace nových příkazů ​

Přidejte jeden řádek na příkaz v MissionServer.OnInit():

c
CCmdRegistry.Register(new CCmdHeal());
CCmdRegistry.Register(new CCmdKill());
CCmdRegistry.Register(new CCmdTime());

Řešení problémů ​

Příkaz není rozpoznán ("Unknown command") ​

  • Chybí registrace: Ujistěte se, že CCmdRegistry.Register(new CCmdVášPříkaz()) je voláno v MissionServer.OnInit().
  • Překlep v GetName(): Řetězec vrácený GetName() musí odpovídat tomu, co hráč píše (bez /).
  • Nesoulad velkých písmen: Registr převádí názvy na malá písmena. /Heal, /HEAL a /heal by měly všechny fungovat.

Oprávnění zamítnuto pro administrátory ​

  • Špatné Steam64 ID: Dvakrát zkontrolujte ID administrátorů v IsCommandAdmin(). Musí to být přesná Steam64 ID (17-místná čísla začínající 7656).
  • GetPlainId() vs GetId(): GetPlainId() vrací plaintextové Steam64 ID. GetId() vrací stabilní hashované unikátní ID (bezpečné pro databáze a logy), nikoli ID relace -- ID specifické pro relaci, které se po odpojení hráče znovu použije, je GetPlayerId() (typu int). Pro kontroly administrátora používejte GetPlainId().

Zpětná vazba se nezobrazuje v chatu ​

  • RPC nedorazí ke klientovi: Přidejte příkazy Print() na serveru pro potvrzení, že RPC zpětné vazby je odesíláno.
  • Klient OnRPC to nezachytí: Ověřte, že RPC ID odpovídá (CCmdRPC.COMMAND_FEEDBACK).
  • GetGame().Chat() nefunguje: Tato funkce vyžaduje, aby hra byla ve stavu, kdy je chat dostupný. Na obrazovce načítání nemusí fungovat.

/heal ve skutečnosti neléčí ​

  • Provádění pouze na serveru: SetHealth() a změny statistik musí běžet na serveru. Ověřte, že GetGame().IsServer() je true, když se Execute() spouští.
  • Cast na PlayerBase selže: Pokud Class.CastTo(targetPlayer, targetMan) vrátí false, cíl není platný PlayerBase. To se může stát s AI nebo nehráčskými entitami.
  • Gettery statistik vrací null: GetStatEnergy() a GetStatWater() mohou vrátit null, pokud je hráč mrtvý nebo není plně inicializován. V produkčním kódu přidejte kontroly null.

Příkaz se objeví v chatu jako běžná zpráva ​

  • Hook OnEvent zachytí zprávu, ale nepotlačí ji z odeslání jako chat. Pro potlačení v produkčním modu byste potřebovali moddovat třídu ChatInputMenu pro filtrování zpráv s / před jejich odesláním. Ve vanilce ChatInputMenu odesílá text chatu ze svého handleru OnChange(), kde volá g_Game.ChatPlayer(text). Můžete přepsat OnChange() a přeskočit odeslání, když text začíná /:
c
modded class ChatInputMenu
{
    override bool OnChange(Widget w, int x, int y, bool finished)
    {
        // Získání aktuálního textu z edit widgetu (m_edit_box.GetText())
        // Pokud začíná /, NEVOLEJTE super (který volá g_Game.ChatPlayer)
        // Místo toho zpracujte jako příkaz

        // Tento přístup se liší podle verze DayZ -- zkontrolujte vanilkové zdrojáky
        return super.OnChange(w, x, y, finished);
    }
};

Přesná implementace závisí na verzi DayZ a na tom, jak ChatInputMenu zpřístupňuje text. Přístup OnEvent v tomto tutoriálu je jednodušší a funguje pro vývoj, s kompromisem, že text příkazu se také objeví jako chatová zpráva.


Další kroky ​

  1. Načtěte administrátory z konfiguračního souboru -- Použijte JsonFileLoader pro načtení ID administrátorů z JSON souboru místo hardkódování.
  2. Přidejte příkaz /help -- Vypište všechny dostupné příkazy s jejich popisy a použitím.
  3. Přidejte logování -- Zapisujte použití příkazů do logovacího souboru pro auditní účely.
  4. Integrujte s frameworkem -- MyMod Core poskytuje MyPermissions pro hierarchická oprávnění a MyRPC pro string-směrovaná RPC, která zabraňují kolizím celočíselných ID.
  5. Přidejte cooldowny -- Zabraňte spamování příkazů sledováním času posledního provedení na hráče.
  6. Sestavte UI paletu příkazů -- Vytvořte administrátorský panel, který zobrazí všechny příkazy s klikatelnými tlačítky (kombinace tohoto tutoriálu s Kapitolou 8.3).

Doporučené postupy ​

  • Vždy ověřte oprávnění před provedením administrátorských příkazů. Chybějící kontrola oprávnění znamená, že jakýkoli hráč může /heal nebo /kill kohokoli. Ověřte Steam64 ID volajícího (přes GetPlainId()) na serveru před zpracováním.
  • Odesílejte zpětnou vazbu administrátorovi i pro selhané příkazy. Tichá selhání znemožňují ladění. Vždy odešlete chatovou zprávu vysvětlující, co se pokazilo ("Player not found", "Permission denied").
  • Používejte GetPlainId() pro kontroly administrátora, nikoli GetId(). GetId() vrací stabilní hashované unikátní ID určené pro databáze a logy (ID specifické pro relaci, které se po odpojení hráče znovu použije, je GetPlayerId()). GetPlainId() vrací plaintextové Steam64 ID.
  • Ukládejte ID administrátorů v JSON konfiguračním souboru, nikoli v kódu. Hardkódovaná ID vyžadují přestavbu PBO pro změnu. JSON soubor $profile: může být upraven administrátory serveru bez znalosti moddingu.
  • Převádějte názvy příkazů na malá písmena před porovnáváním. Hráči mohou psát /Heal, /HEAL nebo /heal. Normalizace na malá písmena zabraňuje frustrujícím chybám "unknown command".

Teorie vs praxe ​

KonceptTeorieRealita
Chat hook přes OnEventZachytit zprávu a zpracovat ji jako příkazZpráva se stále zobrazí v chatu všem hráčům. Její potlačení vyžaduje moddování ChatInputMenu, což se liší podle verze DayZ.
GetGame().Chat()Zobrazí zprávu v chatovém okně hráčeFunguje pouze tehdy, když je chatové UI aktivní. Na obrazovce načítání nebo v určitých stavech menu je zpráva tiše zahozena.
Vzor registru příkazůČistá architektura s jednou třídou na příkazKaždý soubor třídy příkazu musí být ve správné skriptové vrstvě. CCmdBase v 3_Game, konkrétní příkazy odkazující na PlayerBase ve 4_World. Špatné umístění vrstvy způsobí "Undefined type" při načítání.
Vyhledávání hráče podle jménaFindPlayerByName porovnává částečná jménaČástečné porovnávání může cílit na špatného hráče na serveru s podobnými jmény. V produkci preferujte cílení pomocí Steam64 ID nebo přidejte krok potvrzení.

Co jste se naučili ​

V tomto tutoriálu jste se naučili:

  • Jak se napojit na chatový vstup pomocí MissionGameplay.OnEvent s ChatMessageEventTypeID
  • Jak parsovat prefixy příkazů a argumenty z textu chatu
  • Jak ověřovat oprávnění administrátora na serveru pomocí Steam64 ID
  • Jak odesílat zpětnou vazbu příkazů zpět hráči přes RPC a GetGame().Chat()
  • Jak sestavit znovupoužitelný vzor registru příkazů pro přidávání nových příkazů

Další: Kapitola 8.6: Ladění a testování vašeho modu


Předchozí: Kapitola 8.3: Tvorba modulu administrátorského panelu

Released under CC BY-SA 4.0 | Code examples under MIT License