[Source 2] Скриптинг?

xtance

Участник
Сообщения
513
Реакции
743
Внимание! Спасибо за внимание.

Данная тема была создана в марте, когда вышла бета CS2.
С тех пор многое изменилось, в частности игра больше не поддерживает Lua, о которой идёт речь в этом посте.
Весь мой текст ниже практически потерял актуальность, но люди продолжают общаться в этой теме по привычке, и постить разные полезные находки.

Например:
А этот пост остался, можно сказать, для истории.
Ну я предупредил.


Вступление
Тема создана для совместного поиска способов продолжать писать плагины под контру.
И сразу же спойлер: адекватных рабочих решений пока нет. Иначе бы я не создал эту тему.
Далее вас ждёт несвязный поток информации и ссылок, которая была найдена в разных уголках интернета.

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

Ну а дальше я пошёл искать способы выполнять ну хоть какой-то код на сервере.
И обнаружилось то, что чего я не ожидал. Хотя кто знает.
Те кто делал карты под ксго, наверно сталкивались с VScript. Эта штука позволяла писать скрипты на Squirrel и взаимодействовать с объектами на карте.
В новом движке вальв выбрала другой язык скриптинга, а именно Lua, в отличие от Squirrel он хоть где-то используется.

Краткий гайд
Создать скрипт можно в csgo/scripts/vscripts , например script.lua
Данной папки у меня не было по умолчанию, её, как и скрипт, можно создать самому. По крайней мере на "пиратке"
Мы также можем поглазеть на скрипты, написанные разработчиками. Но в отличие от ксго они не лежат готовыми в папке, а запиханны в vpk
Скачиваем GCFScape и открываем pak01_dir.vpk (он лежит в папке csgo в самом конце), всё лежит в папке с говорящим названием scripts
Интересный намёк - скрипт с названием test_surfmode.lua (всё что он делает - отправляет переменные акселерации в консоль)
1679787692710.png
Допустим мы создали скрипт и вписали туда print("Hello world")
Чтобы выполнить его, напишите в консоль кс clear; script_reload_code script.lua
Теперь вы программист на Lua.

Хорошие новости
Теперь мы можем писать плагины.
Изменять свойства игрока, создавать команды, отслеживать события и так далее.
Пример кода:
Пример команды:
Convars:RegisterCommand('xtance', function(commandName, arg1, arg2, etc)

    local player1 = Convars:GetCommandClient()
    local id = player1:GetEntityIndex()
    print("[ID]", id)

    local player2 = PlayerInstanceFromIndex(id)
    print("[Player]", player2)

    print("[Same]", player1 == player2)

    local all = Entities:FindAllByClassname("player")
    for key, player in pairs(all) do
        print(key, player)
        local hp = player:GetHealth()
        print("HP", hp)
        player:SetHealth(555)
    end

end, '', 0)

1679788157701.png

Плохие новости
Собственно, недостаток всего того к чему мы привыкли. А именно
  • луа запускается в сильно урезанной виртуалке. нет способов взаимодействия с внешним миром
  • нет возможности подключения C/C++ библиотек, выполнения http запросов или какого-нибудь там sql
  • из работы с файлами можно разве что прочитать соседний скрипт лежащий в этой же папке
Ну и в целом неудобно и непривычно писать.

Мои костыли
А именно попытка прикрутить к этому всему JS
Есть интересный способ коммуникации с сервером, о котором не все знают
Запускаем игру через повершел с такими параметрами .\cs2.exe -console -developer -condebug -netconport 1337
В принципе только последний здесь принципиально важен, это открывает возможность связываться с консолью через telnet
После запуска игры жмём win+r и пишем telnet localhost 1337 (на моей винде его не было по умолчанию, как включить гуглится)
Результат:
1679789075865.png
Поскольку это известный протокол, нам не составит труда подключиться к нему из js (да хоть из чёрта лысого, я просто разучился писать на чём-то кроме js).
JavaScript:
const socket = net.connect('127.0.0.1', 1337);
socket.on('ready', () => {
    console.log('connected!')
 
    // хаваем строчки с консоли
    const reader = readline.createInterface({ input: socket, crlfDelay: Infinity });
    reader.on('line', input => {
 
        // обрезает строчку, не спрашивайте зачем, это все равно копипаст
        const line = input.replace(/\s{2,}/g, ' ').trim();
 
        console.log(line);
    })
 
    // отправляем команды в консоль
    socket.write(';echo hello hlmod\n'); // вот это \n в конце важно
})

Ну и по сути изначальная идея (сильно не смейтесь) заключалась в том, что:
  • для каждой функции, которая доступна lua (их много), создаём консольную команду
  • эта команда принимает аргументы, парсит их и отправляет движку
  • чтобы отправить результат обратно в js, мы просто отправляем текст в консоль
  • чтобы понять, какой текст в консоли какой функции js принадлежит, можно посылать из js в lua условный параметр (например uuid), хранить объект типа ({ uuid: callback }), потом парсить ответ, получать по uuid наш callback, вызывать функцию
  • на удивление на lua нашлась готовая библиотека для json, так что с парсингом строк из консоли не всё так плохо
Снова плохие новости
Хотя наверно вы и так уже догадались как хреново это работает в плане скорости.
Ведь на каждый пук нужно бегать туда-сюда и выполнять эту тучу операций.
Поэтому я забил на эту идею, а жаль, так как получался весьма приятный синтаксис.
Даже успел накодить обёртку для класса игрока, но вовремя решил измерить скорость. И так, представляю:

1679789745170.png
VS
1679789758336.png


В общем, мои костыли отработали за 300 мс, в то время как sourcepawn за 0.3 мс, разница примерно в 1000 раз.

На этом моменте я впал в тильт (нет серьёзно, стоит пойти поспать).
Но возможно у вас есть идеи, что с этим делать или же выбросить это все нафиг.

А версию на чистом Lua я не затестил, поскольку даже os.clock мне там не доступен.
Но если у вас есть желание вы можете писать простенькие плагины на нём (ней?) прямо сейчас.

Для тех кто решился
Вот апи по функциям доты2 - API | ModDota
Многие из них подходят и в кс2. Ну как минимум отсюда я понял как поставить игроку хп.
Также в Lua есть рефлексия (можно пройтись по глобальной таблице, оттуда получить все доступные и пройтись по ним тоже).
То есть можно посмотреть что там вам доступно. Пример кода по ссылке, глобальная таблица называется _G

Что дальше
Ну я по сути сижу на читерских форумах (uc) и смотрю как они там реверсят движок.
Вообще они там копают примерно в том же направлении, только в основном волнуются за клиент а не сервер, но там тоже можно подцепить инфы
И щас моя надежда остается на то что я научусь работать с памятью и буду напрямую вызывать функции и читать значения, как это делают крутые хакеры
А в идеале чтобы кто-нибудь сделал аналог сорсмода

Всем спасибо за чтение и спокойной ночки. С вами как обычно был xtance
Сообщения автоматически склеены:

Также забыл сказать, если кому-то понадобятся игровые события, их можно посмотреть через тот же GFScape в csgo/resource
1679791032814.png(это просто текстовые файлы)
А слушать их можно через луашку
NGINX:
function TestEvent(event)
    print("[Jump]", event["userid"])
end

local id = ListenToGameEvent("player_jump", TestEvent, self)
 
Последнее редактирование:

Palonez

бб братки
Сообщения
3,035
Реакции
1,837
в конце скобка
C-подобный:
Convars:RegisterCommand('xtance', function(commandName, arg1, arg2, etc)
 

Palonez

бб братки
Сообщения
3,035
Реакции
1,837
Вроде бы всё нормально, она открывается после RegisterCommand, а закрывается в самом конце, или это ошибка?
C-подобный:
RegisterCommand(name: string, callback: (name: string, ...args: [string]) → nil, helpString: string, flags: ConVarFlags): nil
видимо так и надо
 

Hejter

xor ebx, ebx
Сообщения
1,759
Реакции
393
из гмода вытащил
Ruby:
function isnumber(a) return type(a) == 'number' end
function istable(a) return type(a) == 'table' end

function table.GetKeys( tab )

    local keys = {}
    local id = 1

    for k, v in pairs( tab ) do
        keys[ id ] = k
        id = id + 1
    end

    return keys

end

-- print на Msg
function PrintTable( t, indent, done )

    done = done or {}
    indent = indent or 0
    local keys = table.GetKeys( t )

    table.sort( keys, function( a, b )
        if ( isnumber( a ) and isnumber( b ) ) then return a < b end
        return tostring( a ) < tostring( b )
    end )

    done[ t ] = true

    for i = 1, #keys do
        local key = keys[ i ]
        local value = t[ key ]
        print( string.rep( "\t", indent ) )

        if  ( istable( value ) and not done[ value ] ) then

            done[ value ] = true
            print( key, ":\n" )
            PrintTable ( value, indent + 2, done )
            done[ value ] = nil

        else

            print( key, "\t=\t", value, "\n" )

        end

    end

end

Пример #1:
PrintTable({1,2,3})

Второй пример с мета-таблицей #2
PrintTable(debug.getmetatable(Convars))
 

Palonez

бб братки
Сообщения
3,035
Реакции
1,837
результат
C-подобный:
Convars:RegisterCommand('dump', function(commandName, arg1, arg2, etc)
    -- globals.lua
    -- show all global variables

    local seen={}

    function dump(t,i)
        seen[t]=true
        local s={}
        local n=0
        for k in pairs(t) do
            n=n+1 s[n]=k
        end
        table.sort(s)
        for k,v in ipairs(s) do
            print(i,v)
            v=t[v]
            if type(v)=="table" and not seen[v] then
                dump(v,i.."\t")
            end
        end
    end

    dump(_G,"")
end, '', 0)
 

Вложения

  • dump.txt
    53.5 КБ · Просмотры: 59

xtance

Участник
Сообщения
513
Реакции
743
Ещё способ вызывать функции - ent_fire player runscriptcode "тут код"
1679845770110.png

(На сервере играет 10 игроков, точнее ботов, код выполнился 10 раз)
Может пригодится.
 

SAZONISCHE

Участник
Сообщения
405
Реакции
232
Писать на js было бы шикарно, плюсом там из коробки v8 реализован(хз как к нему подрубиться) и ts используется в панораме.
 
  • Мне нравится
Реакции: Toil

Palonez

бб братки
Сообщения
3,035
Реакции
1,837

Hejter

xor ebx, ebx
Сообщения
1,759
Реакции
393
Я когда-то фреймворк писал для DOTA2 API - LUA, хотел гмодовскую реализацию сделать. Учитывая что они не поменяли API на cs2, то она вполне рабочая.


Пример консольной команды
Из реализованного:
  • Таймер
  • Таблицы (table.IsEmpty, table.GetKeys)
  • Строки (string.ToTable, string.Explode, string.Split, string.EndsWith)
  • Глобальные методы (istable, isstring, isbool, isnumber, isfunction, PrintTable, CurTime)
  • Кастомные события (hook.Add, hook.Run)
 
Последнее редактирование:

Palonez

бб братки
Сообщения
3,035
Реакции
1,837
Пример фейк событий
C-подобный:
FireGameEvent("cs_win_panel_round", {["final_event"] = 10, ["funfact_token"] = "HLMOD"}) -- показать cs_win_panel_round
FireGameEvent("round_start", {}) -- обычным способом я не нашел как отключить, поэтому по старинке так, либо ждем реального ивента
final_event = результат раунда(https://github.com/VSES/SourceEngine2007/blob/master/se2007/game/shared/cstrike/cs_gamerules.h)
funfact_token = сообщение снизу
1680033415489.png

C-подобный:
FireGameEvent("show_survival_respawn_status", {["loc_token"] = "<font color=\"red\">HLMOD</font> <font color=\"green\">HELLO</font> <font color=\"blue\">HLMOD</font> HELLO HLMOD HELLO HLMOD HELLO HLMOD HELLO", ["duration"] = 5, ["userid"] = 2})
loc_token - сообщение события
duration - продолжительность отображения события
userid - кому отображать событие
1680057718171.png

C-подобный:
FireGameEvent("show_deathpanel", {["victim"] = 2}) -- включить
FireGameEvent("hide_deathpanel", {}) -- выключить
victim - кому отобразить
1680060582493.png

C-подобный:
FireGameEvent("cs_win_panel_match", {}) -- включить
FireGameEvent("cs_match_end_restart", {}) -- выключить
1680060854866.png

Установить случайный цвет модели
C-подобный:
player:SetRenderColor(RandomInt(0, 255), RandomInt(0, 255), RandomInt(0, 255)) -- установить случайный цвет модели
vec = player:GetRenderColor() -- получить текущий цвет модели (Vector)
1680063076637.png

Изменение размера модели
C-подобный:
player:SetModelScale(0.3) -- установить размер модели
var1 = player:GetModelScale() -- получить размер модели (float)
1680063217447.png

Невидимость сущности + 0.3 ее размер
C-подобный:
player:SetRenderAlpha(0)
player:SetModelScale(0.3)
1680533205180.png

Удалить все оружие у игрока/получить индекс сущности оружия/наименование класса сущности
C-подобный:
    local weapons = player:GetEquippedWeapons() -- получаем массив(таблицу) экипированного оружия
    for key, wps in pairs(weapons) do -- проходим по всему оружию в инвентаре
        wps:Kill() -- удалить оружие
        wps:GetEntityIndex() -- получить индекс сущности оружия (int)
        wps:GetClassname() -- получаем наименование класса предмета (string)
    end
1680064661240.png

Закрыть игру игроку
C-подобный:
player:RemoveSelf() -- RemoveSelf() удаляет энтити

Получение/установка значений переменных
C-подобный:
Convars:GetInt("mp_freezetime") -- 15
Convars:GetFloat("mp_roundtime") -- 1.92
Convars:GetStr("sv_skyname") -- "cs_baggage_skybox_"
Convars:GetBool("sv_cheats") -- true
Convars:GetCommandClient() -- 0x00137298

Convars:SetInt("mp_freezetime", 0) -- mp_freezetime = 0
Convars:SetFloat("mp_roundtime", 25.5) -- mp_roundtime = 25.5
Convars:SetStr("sv_skyname", "HLMOD") -- mp_skyname = "HLMOD"
Convars:SetBool("sv_cheats", false) -- sv_cheats = false
Convars:RegisterConvar("mp_respawn", "myvalue", "description", FCVAR_CHEAT) -- регистрация собственной консольной переменной
1680066934313.png

Проиграть звук для сущности
C-подобный:
player:EmitSoundParams("путь к файлу", SNDPITCH_NORMAL, 1.0, 10.0)
-- EmitSoundParams( string soundname, int nPitch, float flVolume, float flDelay )

Сообщения
C-подобный:
Msg("Msg")
PrintLinkedConsoleMessage("PrintLinkedConsoleMessage", "aaa")
ScriptPrintMessageCenterAll("ScriptPrintMessageCenterAll")
ScriptPrintMessageChatAll("ScriptPrintMessageChatAll")
Warning("Warning")
1680067910450.png

Вывод времени
C-подобный:
print(string.format("Время сервера %s:%s:%s", LocalTime().Hours, LocalTime().Minutes, LocalTime().Seconds))
-- результат
-- Время сервера 5:56:40

Тряска экрана
C-подобный:
ScreenShake(player:GetCenter(), 5.0, 1.0, 1.0, 5.0, 0, true)

Нанесение урона
Используя зарегистрированную команду, получаем клиента через Convars:GetCommandClient()
C-подобный:
Convars:GetCommandClient():TakeDamage(CreateDamageInfo(Convars:GetCommandClient(), Convars:GetCommandClient(), Vector(0.0, 0.0, 0.0), Vector(0.0, 0.0, 0.0), 50, 4))
первые 2 аргумента функции CreateDamageInfo показывают кто кому наносит урон(в данном случае сам себе)

Некоторые ранние плагины

Стандартная работа с цветами в сообщениях
C-подобный:
ScriptPrintMessageChatAll("\x01x01 \x02x02 \x03x03 \x04x04 \x05x05 \x06x06 \x07x07 \x08x08 \x09x09 \x10x10 \x0Ax0A \x0Bx0B \x0Cx0C \x0Dx0D \x0Ex0E \x0Fx0F")
ScriptPrintMessageChatAll("\x10Hello \x01H\x02L\x03M\x04O\x05D")
1680107252518.png

Автозапуск скриптов
На данный момент, чтобы скрипты функционировали, они должны быть загружены вручную, либо прописаны в автозапуск конфигов сервера.
Но есть временное решение, позволяющее работать более менее автономно всему этому.
  • Установка:
    • Переместить csgo в C:\...\Counter-Strike 2\
    • В C:\...\Counter-Strike 2\csgo\cfg\server.cfg прописываем exec lualoader
    • Настраиваем C:\...\Counter-Strike 2\csgo\scripts\configs\lualoader.ini
    • Заполняем конфиг, ориентируясь на комментарии с пояснениями
C-подобный:
"LuaLoader"
{
    "Configs"
    {
        "autoload" "1" // Автозагрузка скриптов при стартке
    }

    "Scripts"
    {
        // Список скриптов ниже, например:
        //
        // "имя_скрипта"         "кастомное_имя"
        //  или
        // "имя_скрипта"         ""

        "example"      ""
        "script1"      "SuperScript"
        "my_script"    "1Script"
    }
}
  • Запуск: скрипт автоматически будет загружаться при каждой инициализации server.cfg
  • Команды:
    • lua_loadscript SCRIPT_NAME - загрузить скрипт из scripts\vscripts по этому имени
    • lua_listscripts - cписок всех скриптов загруженных через LuaLoader
    • lua_reloadscript SCRIPT_ID - перезагрузить скрипт по его ID взятый из lua_listscripts

RAGDOLL(Устарело)
Теперь, как известно, с рэгдолами можно активно взаимодействовать они полностью физические.
Пример хоть какого-то взаимодействия с ними можно посмотреть в 3 плагине пункта "некоторые ранние плагины"
  • Установка: кидаем vscripts в директорию игры: C:\...\Counter-Strike 2\csgo\scripts\
  • Запуск:
    • В консоль сервера вводим script_reload_code spawn_ragdoll_script
    • Берем в руки нож и спавним при помощи нажатия ЛКМ
1680109274598.png

KEYVALUES
Насколько мне известно, работать со структурами в Lua можно, но только считывая их. Это означает, что изменить значения или структуру файла не получится обычным способом.
Для работы со структурами существуют 2 функции:
C-подобный:
LoadKeyValues(string sPath)
LoadKeyValuesFromString(string sPath)

Сама структура находится по пути C:\...\Counter-Strike 2\csgo\scripts\configs\testconfig.ini и выглядит так:
1680169730937.png

В LoadKeyValues() указывается путь до файла относительно папки scripts
SQL:
local kv = LoadKeyValues("scripts/configs/testconfig.ini") -- создаем переменную, в которую считываем структуру из файла по указанному пути в виде таблицы(table)
if kv ~= nil then -- проверяем валидна ли таблица
    kv["key1"]["subkey1"]["subsubkey1"]["param2"] -- получаем значение 'x'
end
1680170044349.png

Если необходимо получить все содержимое ключа(key2)
1680170938051.png
можем сделать так:
C-подобный:
for k, v in pairs(kv["key2"]) do -- в каждой итерации k соответствует значению param, а v значению value
    print(k, v)
end

-- результат:
-- 1       value1
-- 2       value2
-- 3       value3
-- 4       value4

Если мы хотим получить названия секций, то можем использовать проверку типа данных для первой переменной k
C-подобный:
for k, v in pairs(kv) do    -- цикл по всему первому уровню структуры(по первому массиву)
    if type(v) == "table" then -- проверяем значение ключа k, а конкретнее узнаем тип данных у значения v
        print("section_name", k) -- если оно равно table, то выводим название ключа
    end
end

-- результат:
-- section_name    key1
-- section_name    key2
-- section_name    key3

Поскольку наша структура представлена в виде таблице(table), которая в свою очередь представляет из себя множество массивов вложенных друг в друга, то нет особого смысла создавать какие-то массивы для работы с ней, можно просто обращаться к уже существующим массивам.

cvarlist - команда для вывода всех актуальных и зарегистрированных конваров в консоль
entitysummary - общее число существующих сущностей на данный момент

Список нетпропов, которые я нашел https://github.com/Quake1011/dumps/blob/main/netprops[CS2].txt
Список классов, которые я нашел dumps/Classes[CS2].txt at main · Quake1011/dumps

Со временем можно будет поискать полезные ресурсы, которых может не быть на этом форуме, тут - Build software better, together

Больше информации о Lua, его синтаксисе и возможностях - можно найти тут Lua 5.3 Руководство
 
Последнее редактирование:

0-BuTaJIuK-0

Участник
Сообщения
235
Реакции
57
Кто-нибудь знает, как на lua (в cs2) можно взаимодействовать с db или http(json)?
 
Сверху Снизу