Справочник по функциям и классам. Часть I. — S.T.A.L.K.E.R. Inside Wiki

Справочник по функциям и классам. Часть I.

Материал из S.T.A.L.K.E.R. Inside Wiki

Перейти к: навигация, поиск

Что это?

Это вики-версия оригинальной темы на АМК-форуме, несколько более удобная для просмотра, а также анонимного (и не очень) редактирования. Авторы: malandrinus, Monnoroch, Kolmogor, Unnamed Black Wolf, меченый(стрелок), IQDDD, Kirag, Taroz, dan, lekzd, 7.9, Garry_Galler, AKKK1, Bak и другие.


Навигация

Справочник по функциям и классам. Часть I.
Справочник по функциям и классам. Часть II.
Справочник по функциям и классам. Часть III.
Справочник по функциям и классам. Часть IV.
Справочник по функциям и классам. Часть V.

Содержание

Основы

Класс alife_simulator. Базовые операции с серверными объектами

Класс нужен для выполнения самых базовых задач на серверной стороне: создание и удаление объектов, поиск объектов по идентификаторам, перевод в онлайн/оффлайн, проверка инфопорций. Из этого ряда странным образом выбивается получение номера текущего уровня. Объект такого класса всего один и может быть получен глобальной функцией alife(). Используем примерно так:

local sim = alife() -- получаем сам объект класса alife_simulator
local sactor = sim:actor() -- получаем серверный объект для актора

Формальное описание:

class alife_simulator {
    // получить дистанцию переключения онлайн/оффлайн
    float switch_distance();
    // (!) установить дистанцию. Не работает!
    void switch_distance(float dist);
    // номер текущего уровня
    int level_id();
    // возвращает имя уровня по его id
    string level_name(int level_id)
    // создание объектов на уровне
    cse_abstract* create(string <имя секции объекта>, vector* position, int level_vertex_id,
                                                                                int game_vertex_id)
    // создание объектов в инвентаре
    cse_abstract* create(string <имя секции объекта>, vector* position, int level_vertex_id,
                                                                int game_vertex_id, int parent_id)
    // создание пачек с патронами
    cse_abstract* create_ammo(string <имя секции патронов>, vector* position, int level_vertex_id,
                                                    int game_vertex_id, int parent_id, int amount)
    // создание объекта на основе секции в all.spawn
    cse_alife_dynamic_object* create(int <индекс объекта в all.spawn> )
    // индекс секции в all.spawn для объекта с заданным id
    int spawn_id(int id)
    // удаление объекта.
    void release(cse_abstract* obj, bool);
    // получение объекта по id.
    cse_alife_dynamic_object* object(int id, bool no_assert);
    // эквивалентно предыдущей функции с no_assert == true.
    cse_alife_dynamic_object* object(int id);
    // (!) получение объекта по имени. В ЗП нет!
    cse_alife_dynamic_object* object(string name);
    // получения объекта по сюжетному идентификатору
    cse_alife_dynamic_object* story_object(int story_id);
    // получение серверного объекта актора. Можно и просто получить его по id, равному 0
    cse_alife_creature_actor* actor();
    // проверяет, что аргумент не равен 65535 (-1). На редкость бесполезная функция.
    bool valid_object_id(int id);
    // устанавливает флажок flSwitchOnline, который определяет возможность перехода в онлайн
    void set_switch_online(int id, bool v);
    // устанавливает флажок flSwitchOffline, который определяет возможность перехода в оффлайн
    void set_switch_offline(int id, bool v);
    // устанавливает флажок flInteractive
    void set_interactive(int id, bool v);
    // проверяет наличие инфопоршена
    bool has_info(int id, string info_portion);
    // проверяет отсутствие инфопоршена
    bool dont_has_info(int id, string info_portion);
    // установка ограничения на выход
    void add_out_restriction(cse_alife_monster_abstract* obj, int restrictor_id);
    // установка ограничения на вход
    void add_in_restriction(cse_alife_monster_abstract* obj, int restrictor_id);
    // снятие ограничения на выход
    void remove_out_restriction(cse_alife_monster_abstract* obj, int restrictor_id);
    // снятие ограничения на вход
    void remove_in_restriction(cse_alife_monster_abstract* obj, int restrictor_id);
    // снятие всех ограничений либо на вход, либо на выход
    void remove_all_restrictions(int object_id, const enum RestrictionSpace::ERestrictorTypes type);
    // (?) возможно, отвечают за убийство в оффлайне
    void kill_entity(cse_alife_monster_abstract* monster, int game_vertex_id,
                                                                        cse_alife_schedulable* obj);
    // эквивалентно первой функции с obj == 0
    void kill_entity(cse_alife_monster_abstract* monster, int game_vertex_id);
    // эквивалентно второй с game_vertex_id, равным gvid монстра, переданного в первом аргументе
    void kill_entity(cse_alife_monster_abstract* monster);
};

Комментарии к некоторым функциям

void switch_distance(float dist);

Функция для установки онлайн-радиуса. У меня всегда вызывает вылет. Впрочем, этот радиус можно изменить в файле конфигурации alife.ltx в секции [alife] (параметр switch_distance). Там же есть параметр гистерезиса перехода switch_factor.


string level_name(int level_id)

Функция возвращает системное имя уровня, например "L01_Escape" для Кордона. В списке строк для многих уровней есть внятные имена. Их можно получить так:

local level_name = game.translate_string(string.lower(alife():level_name(level_id)))

Функция create с аргументом parent_id позволяет создавать только инвентарные предметы. Аргумент amount функции create_ammo задаёт количество патронов в пачке. Пачка всегда создаётся одна. Номер секции в файле all.spawn для функции create с одним аргументом и spawn_id - это просто порядковый номер расположения объекта в all.spawn. Имеет тенденцию меняться после перепаковки с помощью acdc.

Функции create возвращают созданный объект. Однако он приведён к типу cse_abstract, и с ним возможны не все операции. Лучше после создания объекта получить объект заново, используя одну из функций поиска объектов.


С функцией release требуется осторожность. Некоторые объекты перед удалением надо переводить в оффлайн. Второй булевский аргумент этой функции игнорируется.


Функции set_switch_online, set_switch_offline и set_interactive позволяют менять соответствующие флажки из поля object_flags. Флажки flSwitchOnline и flSwitchOffline периодически проверяются для каждого объекта и определяют возможность перехода в онлайн или оффлайн соответственно. Если они оба true, то объект будет переходить в онлайн/оффлайн автоматически при пересечении онлайн-радиуса. Комбинации true/false и false/true соответственно форсируют одно из состояний вне зависимости от расстояния до актора. Естественно, это всё имеет смысл при нахождении объекта на одном уровне с актором.

Для некоторых объектов проверка осуществляется не этими флажками, а функциями can_switch_online и can_switch_offline, которые надо перегрузить в скриптовом серверном классе и, таким образом, управлять переключением. Собственно для остальных объектов управление также осуществляется этими функциями, просто они по дефолту возвращают значения этих флажков.

С флажком flInteractive я не экспериментировал, только выяснил, что функция interactive возвращает true, если установлены в 1 три флажка: flInteractive, flVisibleForAI и flUsefulForAI. Это имеет смысл для НПС, но какой именно - пока не выяснял.


В функциях для управления ограничениями надо передавать id рестрикторов. У рестрикторов имеется собственное поле типа, но, как мне показалось, в данных функциях собственный тип рестриктора игнорируется.

В функции remove_all_restrictions второй аргумент type определяет, какие именно ограничения надо удалить. Допустимые значения для этой функции: 4 - in, 5 - out.

Подробнее о типах рестрикторов

Их всего шесть, что определяется вот таким перечислением:

enum RestrictionSpace::ERestrictorTypes {
    eDefaultRestrictorTypeNone, // 0
    eDefaultRestrictorTypeOut,  // 1
    eDefaultRestrictorTypeIn,   // 2
    eRestrictorTypeNone,        // 3
    eRestrictorTypeIn,          // 4
    eRestrictorTypeOut,         // 5
};

Можно обратить внимание, что в all.spawn никогда нет рестрикторов с типами 4 и 5. Не бывает также и типа 1.

В целом можно сделать ряд предположений:

  • тип рестриктора 0 или 3 - это как бы "не рестриктор" вообще, и используются в основном для скриптовых зон, где движок ни за что не отвечает, а все проверки выполняются скриптами.
  • только типы рестриктора 1 и 2 имеют смысл для движка и, по всей видимости, приводят к созданию зон "куда все не ходят".
  • зоны "откуда всем ходу нет", очевидно, лишены смысла, поэтому и не встречается в природе тип 1.
  • что касается типов 4 и 5, то это выходит не типы рестрикторов, а специальные константы для функции. Зачем их поместили в это-же перечисление, не знаю. Ещё раз, выглядит так, использовать можно рестрикторы с любым типом (поскольку, как я говорил, тип их не проверяется).

Обратите также внимание, что у класса game_object имеются функции, также отвечающие за управление ограничениями.

Назначение всех вариантов функции kill_entity неясно. Вроде бы отвечают за убийство в оффлайне, но у меня стабильно вызывали вылет.


Пространства имён. Глобальные функции для большого числа задач

Разберём глобальные фунции или иначе функции из пространств имён. Такая функция в любом месте вызывается следующим образом:

 <пространство имён>.<имя функции>(<список аргументов>)

Пространства имён - это всего лишь способ сгруппировать функции по некоторому признаку. Хотя надо заметить, что логика группировки функций в сталкере иной раз не поддаётся объяснению.

Всего есть несколько пространств имён: безымянное (т.е. функции из него вызываются просто по именам как обычные глобальные функции), game, level, relation_registry, actor_stats. В ЧН (и ЗП) появилось пространство имён main_menu, кроме того есть незначительные изменения в остальных: некоторые функции исчезли, некоторые добавлены. Изменения однако незначительны.

Сейчас рассматриваем только три основных пространства имён (безымянное, game, level) и только для ТЧ.

Безымянное пространство имён

 
-- функции, которые не работают или вызывают вылеты или непонятно назначение
function log(string) -- видимо для дебаговой версии
function error_log(string) -- видимо для дебаговой версии
function flush() -- видимо для дебаговой версии
bool app_ready() -- готовность чего-то к чему-то =)
-- пока игра не загружена, у меня возвращает false, после загрузки - true
bool editor() -- под редактором??? Это то, чего нам как ушей не видать???
int script_server_object_version() -- число, знать бы что означает
function verify_if_thread_is_running()
function xrRender_test_r2_hw()
------------- получение некоторых глобальных объектов --------------------------------------
alife_simulator* alife() -- см. описание alife_simulator
render_device* device() -- см. описание render_device
CGameGraph* game_graph() -- получение глобального графа игры
CConsole* get_console() -- получение объекта для управления консолью. Это и так все знают
CUIGameCustom* get_hud() -- получение объекта для управления худом.
-- Можно добавлять к худу свои окошки и элементы управления. Так делают всякие индикаторы.
FS* getFS() -- файловые операции. см. описание FS
string command_line() -- командная строка, включая полный путь к исполняемому файлу
string user_name() -- сетевое имя игрока (вроде бы имя активного пользователя в системе)
cef_storage* ef_storage() -- хз что такое
-------------- работа с файлами конфигурации
ini_file* create_ini_file(string) -- создаёт из строки объект типа ini_file в памяти
ini_file* game_ini() -- открытый файл "config\game.ltx" и все его вложения
ini_file* system_ini() -- открытый файл "config\system.ltx" и все его вложения
------------ побитовые операции --------------------------------------
function bit_and(number, number)
function bit_not(number)
function bit_or(number, number)
function bit_xor(number, number)
function dik_to_bind(number) -- я подозреваю, что преобразует константу
-- из набора DIK_keys в константу из набора key_bindings
DWORD GetARGB(number, number, number, number) -- формирует 32-х разрядное целое из
-- 4-х отдельных байт. для задания цвета. Где используется - точно не известно.
------------ всякий мелкий утиль -----------------
void prefetch(string <имя модуля>) -- вызывает загрузку и прекомпиляцию модуля.
-- Имя модуля указывается без расширения ".script"
int time_global() -- реальное время (в миллисекундах) с начала запуска программы
---------------------------------------------------------------------------
void buy_condition(ini_file*, string <имя секции>) -- прочитать настройки торговца
-- на покупку из файла конфигурации и определённой секции.
-- Только непонятно, для кого читаются эти настройки
void buy_condition(float, float) -- непонятная функция.
void sell_condition(ini_file*, string <имя секции>) -- аналогично на продажу
void sell_condition(float, float) -- непонятная функция
void show_condition(ini_file*, string <имя секции>) -- совсем непонятно
--
-- далее описанные функции позволяют преобразовать ссылку на базовый класс
-- в ссылку на производный для получения доступа к методам производного класса
-- за деталями надо обращаться в описание соответствущих иерархий.
-- надеюсь, руки до этого дойдут
action_planner* cast_action_to_planner(action_base*)
action_base* cast_planner_to_action(action_planner*)
cse_alife_creature_abstract* cast_alife_object_to_creature(cse_alife_object*)
cse_alife_trader_abstract* cast_alife_object_to_trader_abstract(cse_alife_object*)
--------------------- получение встроенных шрифтов ---------------------
CGameFont* GetFontDI()
CGameFont* GetFontGraffiti19Russian()
CGameFont* GetFontGraffiti22Russian()
CGameFont* GetFontGraffiti32Russian()
CGameFont* GetFontGraffiti50Russian()
CGameFont* GetFontLetterica16Russian()
CGameFont* GetFontLetterica18Russian()
CGameFont* GetFontLetterica25()
CGameFont* GetFontMedium()
CGameFont* GetFontSmall()
--------- некая информация о текстурах. В ЗП эти функции убраны
CGameFont* GetTextureInfo(string, string)
CGameFont* GetTextureName(string)
CGameFont* GetTextureRect(string)
 

Пространство имён game

 
CTime* get_game_time() -- возвращает игровое время в виде объекта класса CTime
void start_tutorial(string) -- запускает туториал, зарегистрированный
-- в файле ui\game_tutorials.xml. В частности, на туториалах сделаны сны
bool has_active_tutorial() -- запущен ли туториал
int time() -- игровое время (в игровых миллисекундах) с начала игры (т.е. с начала прохождения игры)
string translate_string(string) -- получает вменяемую строку по её строковому идентификатору
-- из одного из xml файлов, прописанных в файле \config\localization.ltx в секции
-- [string_table] в параметре files
-- если там такой строки нет, то возвращает свой аргумент - исходную строку
 

Пространство имён level

 
-- функции неопределённые или вызывающие непонятные вылеты
//function check_object(game_object*) -- выглядит хитро. Как бы поле level.check_object есть
-- и тип его "function"
-- однако значение этого поля nil и вызов его невозможен. Т.е. такой функции на самом деле нет
-- Видимо была в отладочной версии
function debug_actor() -- аналогично check_object. Нет такой функции
function debug_object(string) -- аналогично check_object. Нет такой функции
function physics_world() -- в ТЧ - вылет
function environment() -- в ТЧ - вылет
---------------- вылета не вызывают, но назначение неясно ---------------------------
int game_id() -- возвращает некое число (у меня 1). Вероятно для мультиплея.
client_spawn_manager* client_spawn_manager() -- возвращает некий объект,
-- с помощью которого можно ставить некие коллбеки на спаун. В игре нигде не используется
---------------- информация об уровне в целом, управление уровнем в целом -----------
bool present() -- наличие уровня. Так можно определять, что игра загружена.
-- Т.е. если мы в главном меню и игра не загружена, то функция возвращает false
string name() -- имя текущего уровня, точнее его идентификатор
Fbox* get_bounding_volume() -- даёт некий параллелепипед, видимо охватывающий уровень
------------------ коллбеки ----------------------------------------
void add_call(const function<boolean>&, const function<void>&)
-- устанавливаются два коллбека:
-- первый. Аргументов нет, вызывается периодически непосредственно после апдейта актора
-- должен возвращать false до тех пор, пока не решит прекратить работу.
-- второй. Аргументов нет, вызывается один раз сразу после последнего вызова
-- первого коллбека (после того, как тот вернёт true)
-- поскольку частота вызовов совпадает с апдейтами актора, то можно сделать вывод,
-- что коллбек ставится на актора
void remove_call(const function<boolean>&, const function<void>&) -- по идее должно убирать коллбеки, но как-то не работает
void add_call(object, const function<boolean>&, const function<void>&) -- аналогично для
-- произвольного объекта, но теперь в качестве первого аргумента колбека при его вызове
-- передаётся тот объект, на который его ставили
void remove_call(object, const function<boolean>&, const function<void>&)
--
void add_call(object, string, string) -- непонятно, как работает. Пытался передать имя функции,
-- но это не сработало.
void remove_call(object, string, string)
--
void remove_calls_for_object(object)
---------------- эффекторы ---------------------------------------------------
float add_cam_effector(
string <имя анимации>, -- имя файла *.anm, адресуемого от папки anims
int <идентификатор>, -- произвольное целое число, которое можно использовать для удаления
boolean <зациклить>, -- проигрывать бесконечно
string <коллбек на окончание>) -- имя функции, которая выполнится по окончании
-- действия эффекта. Функция не принимает аргументов и не возвращает значений.
-- Не вызывается при принудительном завершении эффектора функцией remove_cam_effector.
-- Функция возвращает некое число, для каждого файла анимации своё. Зачем нужно - не знаю.
float add_cam_effector2(string, number, boolean, string) -- в целом тоже самое,
-- что и предыдущая функция. Видимая разница в том, что предыдущая смещала позицию камеры
-- от текущего положения актора, а эта сначала перемещает камеру актора в некую стартовую позицию.
void remove_cam_effector(number <идентификатор>) -- убрать эффектор с ранее заданным номером
--
void add_pp_effector(
string <имя постэффекта>, -- имя файла *.ppe, адресуемого от папки anims
int <произвольный идентификатор>, -- для дальнейшего удаления
boolean <зациклить>) -- проигрывать бесконечно
void set_pp_effector_factor(
int <идентификатор>, -- число, ранее заданное при установке эффекта
float <интенсивность>) -- (0, 1) -- интенсивность эффекта
void set_pp_effector_factor(
int <идентификатор>,
float <интенсивность>,
number <скорость изменения>) -- не до конца ясно, но вроде как скорость перехода
-- от текущего состояния к заданному. По какому принципу считается время перехода - непонятно
void remove_pp_effector(int <идентификатор>) -- убрать эффект
--
-- следующие две функции практически не используются. Задействованы незначительно только в ЗП
void add_complex_effector(
string <секция>, -- имя секции в system.ltx, с описанием эффекта
int <идентификатор>)
void remove_complex_effector(int <идентификатор>) -- отмена эффектора
----------------------- функции времени --------------------------------
int get_time_days() -- день месяца по игровому времени
int get_time_hours() -- час текущего игрового дня
int get_time_minutes() -- минута текущего игрового часа
float get_time_factor() -- возвращает отношение скорости течения игрового времени к скорости реального (game_time_speed / real_time_speed)
void set_time_factor(number) -- устанавливает это отношение. Фактор времени в целом
-- не сказывается на физике мира. Т.е. скорость движения объектов остаётся естественной.
-- Пули летят с той же скоростью, что и раньше. Люди и монстры бегают как и раньше.
-- На чём точно сказывается фактор времени - это на смене дня и ночи. Установив его
-- очень большим можно воочию наблюдать смену дня и ночи в течении пары минут.
------------------ погода ----------------------------------------------------
string get_weather() -- получить идентификатор текущей погоды
void set_weather(string <weather>, boolean <now>) -- устанавливает погоду. Идентификатор погоды
-- должен присутствовать в файле config\weathers\environment.ltx в сеции [weathers] иначе вылет
-- второй параметр отвечает за немедленную смену погоды
-- если true - меняем прямо сейчас
-- если false - то непонятно. У меня также менялось сразу.
void set_weather_fx(string) -- устанавливает погодный эффект,
-- описанный в файле weathers\environment.ltx в секции [weather_effects]. Это всякие молнии,
-- ветер с туманом, затемнения и пр. Так в частности сделан выброс
bool is_wfx_playing() -- соответственно проигрывается ли сейчас погодный эффект
float rain_factor() -- степень дождливости. Зависит от выбранной погоды. Меняется видимо автоматически
-------------------------- HUD и интерфейс ---------------------------------------
function start_stop_menu(CUIDialogWnd*, boolean) -- в ЗП убрана
-- Эти функции практически не используются. Открытие окон делается через объект худа
function add_dialog_to_render(CUIDialogWnd*)
function remove_dialog_to_render(CUIDialogWnd*)
CUIWindow* main_input_receiver() -- Возвращает открытое окно(инвентаря, торговли, ПДА, диалога)
-- С ее помощью к стандартным окнам можно добавить новые элементы. В ЗП отсутствует!
-- (спасибо Колмогору за дополнение)
--
void show_indicators() -- показывает индикаторы и прицел
void hide_indicators() -- прячет индикаторы и прицел
void disable_input() -- блокирует мышь и клавиатуру
void enable_input() -- разблокирует мышь и клавиатуру
float get_snd_volume() -- уровень звука (0,1) с ползунками в опциях не связано
void set_snd_volume(number) -- установить уровень звука (0,1)
ESingleGameDifficulty get_game_difficulty() -- сложность игры, с ползунками в опциях не связано
void set_game_difficulty(enum ESingleGameDifficulty) -- установить сложность игры
-- Перечисление ESingleGameDifficulty экспортировано как класс game_difficulty
-- Его описание
C++ class game_difficulty {
const novice = 0;
const stalker = 1;
const veteran = 2;
const master = 3;
}
-- т.е. вызываем функцию так: level.set_game_difficulty(game_difficulty.veteran)
---------------------- работа с разными объектами на уровне --------------
game_object* object_by_id(number) -- клиентский объект по его id (или nil,
-- если объект не в онлайне или не существует)
void spawn_phantom(const vector&) -- создаёт фантомного монстра в указанной точке (как на радаре)
-- фантомный мостр начинает бежать к актору и добежав - исчезает
function patrol_path_exists(string)
function vertex_in_direction(number, vector, number)
vector vertex_position(number) -- положение левел-вертекса по его номеру
-- на каждом уровне свои вертексы. Нумерация на разных уровнях может
-- (и вообще говоря обязательно будет) пересекаться
function cover_in_direction(number, const vector&)
-------------------- управление картой -----------------------------------
void map_add_object_spot( -- установить метку
int <id>, -- идентификатор объекта
string <тип метки>, -- имя типа метки, зарегистрированное в файле config\ui\map_spots.xml и всех, что в него включены инклюдами
string <надпись>) -- надпись
 
void map_add_object_spot_ser(int <id>, string <тип метки>, string <надпись>) -- разница
-- между этой и предыдущей функцией в том, что вторая ставит метку постоянно,
-- или иными словами на серверный объект (потому и _ser в конце). При вызове вы сначала не заметите разницы,
-- поскольку и та и другая функция поставят метку как на объект на текущем уровне, так и на любом другом.
-- Однако после сохранения и загрузки метки, поставленные второй функцией останутся.
-- Очевидно, каждая из функций полезна в своём случае: постоянные метки нужно ставить на тайники и квесты,
-- а временные можно использовать к примеру для радаров и прочего в этом роде.
 
void map_change_spot_hint(int <id>, string <тип метки>, string <надпись>) -- сменить надпись на метке
bool map_has_object_spot(int <id>, string <тип метки>) -- проверить наличие метки
void map_remove_object_spot(int <id>, string <тип метки>) -- удалить метку
--------------------------------------------------------------------------
function iterate_sounds(string, number, function<void>)
function iterate_sounds(string, number, object, function<void>)
function prefetch_sound(string)
 


Создание своего класса и наследование от экспортированных классов

Базовые сведения об ООП для сталкера. Необходимо прочитать для понимания темы про биндер и некоторых других.

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

Создание своего класса

Свой класс можно добавить так:

class "my_cool_class"

Здесь "my_cool_class" - имя моего нового класса. Записывается в кавычках, т.е. это строка.

Потом можно добавлять к этому классу свои методы:

 
-- это специальный метод - конструктор. Будет вызван при создании класса.
function my_cool_class:__init(num)
    self.my_param = num -- добавили в свой класс переменную
end
 
-- обычный метод
function my_cool_class:cool_method_of_my_class()
    get_console():execute("lua_bind_in_action_"..self.my_param)
end
 
-- деструктор, вызывается при сборке объекта сборщиком мусора. Аргументов не имеет (кроме скрытого self. об этом см. далее)
function my_cool_class:__finalize()
    get_console():execute("good_by_"..self.my_param)
end
 

Потом можно создавать экземпляры своего класса, вызывать их методы и пр.:

 
local obj1 = my_cool_class(1) -- здесь создаётся новый объект. При этом вызывается конструктор и ему передаются аргументы этого вызова (в данном случае число 1).
local obj2 = my_cool_class(4)
obj1:cool_method_of_my_class()
obj2:cool_method_of_my_class()
 

Как только объект становится ненужным (т.е. на него больше нет ссылок) он удаляется сборщиком мусора. При этом вызывается его метод __finalize.

При чём здесь self.

self - это скрытый первый аргумент. Конструкция вида:

 
function class_name:fun_name(<список аргументов>)
end
 

на самом деле эквивалентна конструкции

 
function class_name.fun_name(self, <список аргументов>)
end
 

Обратите внимание на двоеточие в первом случае и точку во втором.

При вызове метода конструкция вида:

 object_name:fun_name(<список аргументов>)

эквивалентна конструкции

 object_name.fun_name(object_name, <список аргументов>)

Это означает, что в качестве self, как правило, передаётся сам же объект. "Как правило" - это потому, что можно в принципе передать что-то другое. Но лучше этого соглашения не нарушать.

Всё это здорово, но какая от этого польза? Ну сделал я свой класс, и что с ним делать?

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

Наследование от экспортированных классов

Итак, кроме создания собственных классов, технология Luabind позволяет создавать класы на основе уже существующих. Это реализовано в виде своеобразного наследования. Делается это с помощью следующей конструкции:

 class "my_derived_class" (base_class)

Здесь:

"my_derived_class" как и раньше - это имя нового класса.

base_class - это уже существующий класс, который будет базовым для вновь созданного. Имя выбрано произвольно, но подразумевается, что такой класс экспортирован и предназначен для наследования.

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

Вопрос, какой глубины может быть наследование, т.е. можно ли наследовать от производного класса, мне пока до конца не ясен. С другой стороны, в этом практически нет необходимости.

Итак, после выполнения указанной конструкции появляется класс my_derived_class, который является копией класса base_class. У него есть в точности те же методы, что и у его базового класса. Пока от этого мало толку. Но теперь можно переопределить методы нашего класса, изменив таким образом его поведение. Делается это так же, как и ранее, но с некоторыми дополнениями.

 
-- конструктор
function my_derived_class:__init(num) super(num)
end
-- обычный метод
function my_cool_class:some_method(<список аргументов>)
    base_class.some_method(self, <список аргументов>)
end
-- деструктор
function my_cool_class:__finalize()
end
 

На что стоит обратить внимание.

Во-первых, конструкция super(num). Это вызов конструктора базового класса.

Во-вторых, для вызова метода базового класса из метода унаследованного используется другая конструкция base_class.some_method(self, ). Здесь надо использовать альтернативную форму вызова метода (с точкой и явным указанием первого аргумента self). Наконец, метод __finalize() не требует вызова этого же метода для базового класса, поскольку тот будет вызван автоматически при удалении объекта сборщиком мусора.

Собственно, всё. Теперь можно пользоваться созданным классом: создавать объекты этого класса, вызывать их методы и пр. Здесь уже всё зависит от конкретных целей и требует знания конкретных классов.


Общие слова об архитектуре и скриптовой модели сталкера

Факт первый. Сингловая игра построена на основе мультиплеерного движка. Мультиплеер подразумевает наличие сервера, к которому подключаются клиенты. Все данные (т.е. все объекты игры) существуют в первую очередь на сервере, а у клиентов имеются копии этих данных. Естественным образом имеется разграничение обязанностей между клиентом и сервером: сервер отвечает в целом за создание, удаление, хранение объектов, а клиент - за всевозможные _игровые_ манипуляции с ними: отображение, проигрывание анимации и звуков, движение, убиение (но не удаление) и т.п.


Следующий факт. Существует такое понятие онлайн/оффлайн. Что это такое? Дело в том, что загрузить в память все объекты игрового мира сразу невозможно. Для решения этой проблемы конкретно здесь сделаны две вещи:

  • Игра разбита на уровни, и в каждый момент времени загружен только один уровень и, соответственно, загружены только объекты уровня (и давайте не будем обсуждать, как это делают в современных движках). Думаю, с этой идеей всё более или менее ясно.
  • Загружать весь уровень целиком также оказалось накладно. Поэтому в память загружаются только те объекты, которые находятся на некотором разумном расстоянии от игрока. Загрузку объекта называют переходом в онлайн, а выгрузку - переходом в оффлайн. Если конкретнее, то расстояние перехода в онлайн управляется двумя параметрами: switch_distance и switch_factor секции alife из файла config\alife.ltx. Их обычные значения:
[alife]
    switch_distance     = 150
    switch_factor       = 0.1

Переход в онлайн произойдёт на расстоянии switch_distance * (1 - switch_factor).

Переход в оффлайн произойдёт на расстоянии switch_distance * (1 + switch_factor).

Теперь вспомним про нашу архитектуру. Нетрудно догадаться, что описанный процесс хорошо ложится на эту архитектуру. А именно: загрузка объекта (переход в онлайн) - это создание его клиентской части, а выгрузка (переход в оффлайн) - это удаление клиентского объекта. Серверный при этом остаётся постоянно. Отсюда и второе название серверного и клиентского объектов - оффлайновый и онлайновый соответственно.


Пошли дальше. Скриптовая модель (т.е. вся совокупность доступных классов) отражает эту архитектуру. Есть классы для серверных объектов, есть классы для клиентских. Есть вспомогательные классы общего назначения. Есть и ещё другие, но не будем забегать вперёд.

Пример серверных классов и функций:

  • Часть функций объявленных в пространстве имён game и в глобальном пространстве имён (например, alife()).
  • Крупная иерархия классов, унаследованных от cse_abstract (который сам по себе ещё унаследован от нескольких классов, но в основном все значимые для скриптования серверные классы унаследованы от него).

Пример клиентских классов и функций:

  • Все или почти все функции из пространства имён level.
  • Иерархия классов, порождённых от CGameObject.
  • Класс game_object (надеюсь, мы ещё разберём этого уродца).

О других пока не будем.

Важно понять, что в сингловой игре это разделение объектов на серверные и клиентские никуда не делось. Есть всё тот же сервер и есть клиент. Клиент, разумеется, всего один, и сервер - это разумеется этот же компьютер. Объекты, которые раньше существовали по разные стороны сети, теперь находятся в одном адресном пространстве. Но их взаимодействие осталось прежним: при создании клиентский объект копирует себе нужные ему данные с серверной стороны, при удалении - сохраняет. В процессе жизни клиентского объекта он синхронизируется с серверным, в основном записывая в него свои данные. Используются ли при этом сетевые технологии, или путь передачи данных несколько оптимизирован для сингла - неизвестно.


Ещё немного лирики.

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

Глядя на это или по каким иным причинам искусственный интеллект игры исходно создавался в расчёте исключительно на сингл. Вполне может быть, что ваяли его в спешке и просто не продумали архитектурно. Поэтому пресловутый A-Life вышел и не серверным и не клиентским. Он как бы размазан между этими частями. Так что совместное сафари за кабанчиками в мультиплее нам не светит.

К чему я всё это? К тому, что часть классов довольно сложно отнести к серверным или клиентским.

Наконец, существует также довольно обширная иерархия оконных классов для создания пользовательского интерфейса: окна, кнопочки, списки и т.п. Это чистые клиентские классы в том смысле, что в контексте сервера они вообще лишены смысла.


Несколько подробнее о клиентских объектах. Для непосредственного управления клиентскими объектами существует два типа классов:

  • классы, унаследованные от CGameObject. С ними есть одна проблема, большинство этих классов обладают весьма бедным интерфейсом и вообще говоря недоступны для программистов. Исключением являются три класса: CHelicopter, CCar и hanging_lamp. До всех остальных попросту никак не добраться.
  • Класс game_object, который используется для управления всеми онлайновыми игровыми объектами. Убогий уродец, страшный сон архитектора и головная боль скриптёров. Этот класс объединяет в себе все интерфейсы всех объектов, для управления которыми предназначен. В этом чуде около 300 (три сотни) методов! Вызов метода, не соответствующего типу объекта, может быть просто проигнорирован, а скорее всего приведёт к вылету, причём чаще всего без лога. И при этом никак и нигде не документировано, какие же методы относятся к каким типам объектов. Создать этого монстра - это был несомненно дьявольский план, который пришёл в головы творцов игры, вероятно, по большой укурке.

Что можно делать с game_object? В основном он позволяет получить разную информацию об онлайновом объекте, изменять некоторые его свойства, а также выполнять над объектом разные действия. Например, можно пнуть объект, проиграть анимацию, вылечить, убить (но не воскресить) и т.п.

Это всё хорошо, но помимо изменения свойств довольно часто надо решить следующую задачу: надо выполнить некое действие, когда с объектом происходит что-то конкретное, например объект переходит в онлайн/оффлайн, получил удар/повреждение, взял/выбросил предмет из инвентаря, наконец помер и т.п..

Как раз для этого у game_object есть методы set_callback, set_patrol_extrapolate_callback и set_enemy_callback, которые позволяют решить эту задачу. Но во-первых, некоторые события так не отследить, например, переход в онлайн/оффлайн, а во-вторых, независимое использование этих методов не очень-то удобно. Не буду объяснять почему. Знатоки ООП поймут это сразу, а если не верите, попробуйте наладить мало-мальски структурированную обработку событий только этим методом и всё поймёте сами.

Посему эти методы как правило используются в совокупности с классом object_binder, о чём и пойдёт речь в следующей статье.


Описание класса object_binder

Данная статья посвящёна классу object_binder и, кроме того, является в каком-то смысле иллюстрацией к одной из предыдущих статей "Наследование от экспортированных классов".

Один из важнейших классов в скриптовой модели сталкера - это object_binder. Он используется для того, чтобы присоединять к клиентским объектам собственные обработчики событий.

Для того, чтобы использовать object_binder, надо:

1. Создать на его основе свой класс. Теорбазис смотри в статье "Наследование от экспортированных классов".
2. В нужном месте с помощью функций set_callback указать методы класса в качестве функций-обработчиков событий.
3. Указать этот класс в свойстве script_binding в секции объекта в файле *.ltx.

После создания объекта на основе этой секции будет автоматически создан объект, написанного Вами класса, а его методы начнут вызываться по указанным событиям. Получается, что наш объект как-бы присоединился к клиентскому. Отсюда и его название "биндер" от слова bind (англ. присоединять, привязывать).

Теперь подробности. Класс object_binder - это экспортированный класс CScriptBinderObject.

Его (псевдо)описание:

 
class object_binder {
    game_object* object;
 
    object_binder(game_object*);
 
    void save(net_packet*);
    void update(number);
    void reload(string);
    void net_export(net_packet*);
    bool net_save_relevant();
    void load(reader*);
    void net_destroy();
    void reinit();
    void net_Relcase(game_object*);
    bool net_spawn(cse_alife_object*);
    void net_import(net_packet*);
};
 

Прежде чем разобраться с коллбеками, разберём методы класса. В каком-то смысле - они тоже обработчики событий, поскольку вызываются в определённые моменты жизни объекта.

object_binder(game_object*) - конструктор. Хоть и показан на манер C++ как метод, но, как и для многих других классов, вызывается как отдельная глобальная функция. Аргумент - клиентский объект, к которому будем биндиться.

Несколько забегая вперёд, опишу сейчас, как создавать биндер.

В каком-либо модуле, обычно в том же, где и класс биндера, должна быть функция следующего вида:

 
function init(obj)
    local new_binder = my_binder(obj) -- создаём объект биндера
    obj:bind_object(new_binder) -- присобачиваем его к объекту
end
 

здесь my_binder - это новый класс биндера.

Теперь эту функцию надо указать в секции объекта. Допустим, модуль называется my_cool_binder.script. В этом случае в секции объекта будет такая запись:

 script_binding = my_cool_binder.init

При создании объекта на основе этой секции будет вызвана эта функция, которая с помощью метода bind_object класса game_object присоединит наш биндер к свежесозданному объекту.


object - свойство на чтение. Указывает на объект типа game_object, к которому присоединён биндер. Обращаться к свойству надо так: self.object .
net_spawn(cse_alife_object*) - вызывается при переходе объекта в онлайн. Аргумент - серверный объект для того клиентского, к которому присоединились. Метод должен вернуть логическое значение true, если успешно, false, если что-либо не срослось, и в этом случае объект не будет создан, а в логе появится запись: Failed to spawn entity '<имя секции>' .
net_destroy() - аналогично вызывается при уходе в оффлайн, т.е. при удалении данного объекта.
update(delta) - очень важный метод. Для большинства объектов этот метод вызывается постоянно, причём период его срабатывания зависит от расстояния до актора. Минимальный период составлял около 40 мс при нахождении предмета в инвентаре ГГ (если он там конечно может находиться). При удалении объекта от ГГ период растёт линейно вплоть до расстояния в 200 метров. На таком расстоянии период составляет одну секунду. После этого расстояния остаётся равным примерно одной секунде. Аргумент этого метода - целое число, которое в точности равно числу миллисекунд, прошедших с прошлого вызова. Правда в точности оно равно только если меньше 1000. Если выше, то оно просто равно 1000, при том, что реальное время может несколько отличаться (может быть больше на пару десятков миллисекунд). Вероятно, это позволяет точно просчитывать физику движения (в том случае, если я решил управлять ею из скрипта). Судя по комментариям в скриптах для некоторых объектов, метод update не вызывается.
load(reader*) - метод для загрузки состояния объекта.
save(net_packet*) - метод для сохранения состояния объекта.
Здесь есть некоторые непонятки. Точно известно, что вызываются эти методы не для всех объектов. Как минимум, вызываются для монстров, сталкеров и ГГ. А вот для, скажем, гранаты вызываться не хочет (ни load, ни save). C парностью вызова этих методов, судя по всему, такая ситуация: save вызывается при сохранении игры (если вообще вызывается), а load вызывается при создании биндера (как описано выше) только в том случае, если до этого был сделан хоть один save. Ну или иными словами, если есть чего загружать. А есть чего загружать, если до этого что-то сохранили.
reload(section) - вызывается при создании объекта. Аргумент - имя секции, на основе которой создан объект.
reinit() - ещё один метод, который вызывается в самом начале. Аргументов нет. Обычно, именно в этом методе устанавливаются колбеки.
net_export(net_packet*) - назначение неизвестно, примеров использования нет.
net_import(net_packet*) - назначение неизвестно, примеров использования нет.
net_save_relevant() - назначение неизвестно. Если его определяют, то как возвращающий всегда true .
net_Relcase(game_object*) - назначение неизвестно. Один пример использования в bind_smart_terrain.script .

Кроме указанных методов есть и ещё два, которые есть в любом скриптовом классе на основе luabind, __init и __finalize .

__finalize обычно не используется, но для полноты картины стоит упомянуть.
__init - вызывается при создании объекта и получает те-же аргументы, что и конструктор. Фактически - это и есть конструктор (см. теорбазис).
__finalize - вызывается при удалении объекта сборщиком мусора.

Кроме того, поскольку мы пишем свой класс, то кроме указанных методов ничто не мешает добавить и свои. См. дальше про коллбеки. Также можно добавить и свои свойства. Это можно сделать так:

self.my_property = 1 -- добавили своё свойство

Надо только помнить, что если не сохранить это свойство в save, то оно пропадёт при уходе объекта в оффлайн. Что делать при этом с теми объектами, у которых save и/или load не вызывается, непонятно.


Жизненный цикл биндера примерно такой:

1. Объект переходит в онлайн и вызывается функция init, прописанная в секции.
2. Там создаётся объект биндера, при этом вызывается конструктор __init .
3. Биндер присоединяется к объекту и работа функции init заканчивается.
4. Теперь пачкой вызываются методы биндера
reload
reinit
load - если вообще вызывается
netspawn
5. Затем начинает вызываться update до тех пор, пока объект находится в пределах радиуса A-Life.
6. При уходе объекта в оффлайн вызывается метод net_destroy (апдейты, естественно, до этого прекращаются).
7. Наконец, несколько секунд спустя, объект удаляется окончательно сборщиком мусора. При этом вызывается __finalize .

Метод save, если вообще вызывается, вызывается при сохранении игры.

Для прочих методов не удалось выяснить ни назначение, ни момент вызова.


Теперь о коллбеках. Для того, чтобы с помощью функции set_callback и биндера сделать свой обработчик события, надо:

1. Создать дополнительный метод в нашем классе биндера.
2. Указать этот метод как обработчик определённого события. Как правило, это делается в методе reinit .
 
function my_binder:reinit()
    object_binder.reinit(self)
    self.object:set_callback(callback.use_object, self.use_callback,   self)
end
function my_binder:use_callback(obj, who)
    --здесь выполняю действия, которые надо сделать при использовании объекта (нажатии на нём клавиши 'F')
end
 

Подробно разбирать функцию set_callback не будем. Только важные сейчас моменты:

первый аргумент - один из членов перечисления callback.

Полный список констант из этого перечисления:

 
C++ class callback {
const action_animation = 20;
const action_movement = 18;
const action_object = 23;
const action_particle = 22;
const action_sound = 21;
const action_watch = 19;
const actor_sleep = 24;
const article_info = 12;
const death = 8;
const helicopter_on_hit = 26;
const helicopter_on_point = 25;
const hit = 16;
const inventory_info = 11;
const inventory_pda = 10;
const level_border_enter = 7;
const level_border_exit = 6;
const map_location_added = 14;
const on_item_drop = 28;
const on_item_take = 27;
const patrol_path_in_point = 9;
const script_animation = 29;
const sound = 17;
const take_item_from_box = 33;
const task_state = 13;
const trade_perform_operation = 3;
const trade_sell_buy_item = 2;
const trade_start = 0;
const trade_stop = 1;
const trader_global_anim_request = 30;
const trader_head_anim_request = 31;
const trader_sound_end = 32;
const use_object = 15;
const zone_enter = 4;
const zone_exit = 5;
};
 

В общем, видно, что можно сделать. Даже иногда понятно, для какого объекта применимо. Надо заметить, что для конкретного типа объектов будут работать только конкретный набор коллбеков. Например, точно известно, что для неписей работает death и hit. Для ГГ работает death, on_item_drop и on_item_take. Для ящиков - use_object. А вот для гранат, похоже, ничего не работает.

Список работающих коллбеков для каждого типа объектов пока ещё никто не составил.


Теперь простой пример, который я сделал для ящика.

Содержимое файла my_cool_binder.script:

 
function init(obj)
    local new_binder = my_binder(obj)
    obj:bind_object(new_binder)
end
 
class "my_binder" (object_binder)
function my_binder:__init(obj) super(obj)
    get_console():execute("my_binder:__init")
end
 
function my_binder:reload(section)
    get_console():execute("my_binder:reload")
    object_binder.reload(self, section)
end
 
function my_binder:reinit()
    get_console():execute("my_binder:reinit")
    object_binder.reinit(self)
    self.object:set_callback(callback.use_object, self.use_callback,   self)
end
 
function my_binder:update(delta)
    local actor_pos = db.actor:position()
    local obj_pos = self.object:position()
    local dist = actor_pos:distance_to(obj_pos)
    get_console():execute("my_binder:update_dist="..dist.."_delta="..delta)
    object_binder.update(self, delta)
end
 
function my_binder:net_spawn(data)
    get_console():execute("my_binder:net_spawn")
    return object_binder.net_spawn(self, data)
end
 
function my_binder:net_destroy()
    get_console():execute("my_binder:net_destroy")
    object_binder.net_destroy(self)
end
 
function my_binder:net_save_relevant()
    get_console():execute("my_binder:net_save_relevant")
    return true
end
 
function my_binder:save(packet)
    get_console():execute("my_binder:save")
    object_binder.save(self, packet)
end
 
function my_binder:load(reader)
    get_console():execute("my_binder:load")
    object_binder.load(self, reader)
end
 
function my_binder:use_callback(obj, who)
    get_console():execute("my_binder:use_callback")
end
 

Кроме того, в файле gamedata\config\misc\devices.ltx добавил новую секцию:

 
[inventory_box_my]:inventory_box
script_binding      = my_cool_binder.init
 

Это будет ящик с визуалом ящика с динамитом с одним только отличием от стандартного - у него будет нестандартный наш биндер.


На очереди net_packet. Простой, но весьма важный класс.


Описание класса net_packet

net_packet - это один из вспомогательных классов. Представляет собой буфер размером ровно 8 кбайт (т.е. 8192 байта). Из буфера можно последовательно читать и записывать данные, используя методы класса. Имеется текущая позиция чтения и записи. Теперь подробности.

Сигнатура класса следующая:

 
class net_packet {
    net_packet ();
 
    number w_tell();
    number r_tell();
    void w_begin(number);
    function r_begin(number&);
    function r_advance(number);
    function r_seek(number);
    number r_elapsed();
    bool r_eof();
 
    number r_u8();
    void w_u8(number);
    number r_s8();
 
    number r_u16();
    void w_u16(number);
    number r_s16();
    void w_s16(number);
    number r_u24();
    void w_u24(number);
 
    number r_u32();
    void w_u32(number);
    number r_s32();
    void w_s32(number);
 
    number r_u64();
    void w_u64(unsigned __int64);
    number r_s64();
    void w_s64(__int64);
 
    number r_float();
    function w_float(number);
    string r_stringZ();
    void w_stringZ(string);
    bool r_bool();
    void w_bool(bool);
 
    vector r_vec3();
    void w_vec3(const vector&);
    matrix r_matrix();
    void w_matrix(matrix&);
 
    vector r_sdir();
    void w_sdir(const vector&);
    vector r_dir();
    void w_dir(const vector&);
 
    number r_angle8();
    void w_angle8(number);
    number r_angle16();
    void w_angle16(number);
 
    function r_float_q8(number&, number, number);
    function w_float_q8(number, number, number);
    function r_float_q16(number&, number, number);
    function w_float_q16(number, number, number);
 
    function w_chunk_open8(number&);
    function w_chunk_open16(number&);
    function w_chunk_close16(number);
    function w_chunk_close8(number);
 
    ClientID r_clientID();
    void w_clientID(ClientID&);
};
 

Я немного изменил описание по сравнению с оригинальным из lua_help.script. Добавил типы возвращаемых значений, перегруппировал и убрал некоторую шелуху.

Более подробно о методах класса:

net_packet() - конструктор, вызывается в виде глобальной функции так:
local packet = net_packet()
Созданный пакет по умолчанию имеет позиции чтения и записи, установленные в 0.

Не всегда надо создавать свой пакет. Часто приходится иметь дело с уже готовым пакетом (см. следующую статью об использовании нетпакетов).

w_tell() - возвращает текущую позицию записи.
r_tell() - возвращает текущую позицию чтения.
r_advance(shift) - смещает позицию чтения на shift байт. Смещение может быть отрицательным.
r_seek(pos) - устанавливает позицию чтения в pos .
r_elapsed() - возвращает w_tell() - r_tell() .
r_eof() - возвращает true, если r_tell() < w_tell(), иначе false .
w_begin(number) - пишет двухбайтовое число в начало пакета и устанавливает позицию записи равной 2. Единственным другим способом начать запись с начала пакета - это создать новый пакет, у которого позиция записи установлена в 0.
r_begin(number&) - непонятный метод. По аналогии с w_begin должен читать с начала пакета два байта и устанавливать позицию чтения в 2. Однако у меня приводит к вылету. Ну, в любом случае можно обойтись и без него.

Важный момент! Нет никакого способа узнать, что в процессе чтения или записи позиция чтения или записи вышла за пределы пакета. При чтении подразумевается, что прочитать можно столько, сколько записано. Для этого и есть функции r_elapsed() и r_eof(). А вот при записи никакой границы сверху нет, поэтому можно запросто записать больше, чем 8 кбайт. Ясно, что при этом произойдёт переполнение буфера с почти обязательным последующим вылетом игры. Так что надо самостоятельно следить за размером позиции записи и проверять, чтобы при последующей записи она не вышла бы за размеры пакета. Для этого надо знать, сколько мы запишем, ещё до того, как запишем. В особенности это важно для строк, которые имеют переменную длину. Поступаем примерно так:

 
if string.len(s) + 1 + packet:w_tell() > 8192 then
    -- здесь делаем что-то, например крашим игру и выводим сообщение, что не надо жадничать =)
end
packet:w_stringZ(s)
 

Обратите внимание на "+ 1" в вычислении новой позиции записи. Строки имеют дополнительный невидимый нулевой символ в конце, и их физическая длина на один больше, чем длина в символах, которую возвращает функция string.len() .

Остальные методы предназначены собственно для чтения и записи. Описывать их все в подробностях нет смысла, только общий принцип.

Читаем так:

local value = packet:r_XXX()
Здесь XXX - это тип значения, которое читается. Значение читается из буфера начиная с текущей позиции чтения, а позиция чтения увеличивается на размер читаемого значения. В переменную value будет помещено значение того типа, которое прочитано.

Аналогично пишем в буфер:

packet:w_XXX(value)
Значение пишется начиная с текущей позиции записи, а позиция записи увеличивается на размер типа XXX.

Для целых типов:

u - знаковое значение
s - беззнаковое значение
8, 16, 24, 32, 64 - один, два, три, четыре, восемь байт соответственно.
Обратите внимаение, что для типа s8 (знаковый байт) нет метода записи. Подозреваю, что вместо него можно использовать соответствующий метод для беззнакового типа.
float - число с плавающей запятой одинарной точности, 4 байта.
stringZ - строка (размер равен длине + 1 байт).
bool - логическое значение (1 байт).
vec3 - объект типа vector - вектор из трёх float (12 байт).
matrix - объект типа matrix. Состоит из 4-х векторов (48 байт).
sdir - (?) непонятно, на запись берёт вектор и пишет 6 байт.
dir - (?) аналогично, но пишет 2 байта.
angle8 - (?) берёт float и пишет 1 байт.
angle16 - (?) аналогично, но пишет 2 байта.

Четыре последних метода при чтении у меня вызывают вылет. Зачем нужны, неизвестно.

ClientID - объект класса ClientID. Судя по всему, это надо для сетевой игры.
float_q8, float_q16 - пока непонятно.

Назначение методов с w_chunk_ вообще непонятно, тем более, что для них отсутствуют соответствующие методы на чтение.


Использование объектов класса net_packet

Собственно, законное использование нетпакетов - это буфер, в котором объект сохраняет своё состояние. Скорее всего это используется в первую очередь при передаче данных по сети. Потому и net_packet, т.е. буквально "сетевой пакет". Сначала объект записывает себя в пакет, затем он отправляется по сети. Похоже, однако, что используется не только при передаче по сети, но и для сохранения объектов вообще.

Теперь конкретнее.

Сохранение состояния серверного класса

На серверной стороне есть классы, унаследованные от cse_abstract. У них есть методы STATE_Read и STATE_Write.

Метод STATE_Read вызывается при загрузке состояния объекта из сохранёнки, в нём данные читаются из переданного методу пакета. STATE_Write вызывается при сохранении объекта, в нём данные объекта сохраняются в пакет.

Если создать свой класс и перегрузить эти методы, то увидим такую картину:

 
class "se_my_server_class"    (<имя_базового_класса>)
 
function se_my_server_class:STATE_Write(packet)
    <имя_базового_класса>.STATE_Write(self, packet) -- базовый класс сохраняет свои данные
    -- здесь можно сохранить какие-то данные, в дополнение к данным базового класса
    packet:w_stringZ("моя строка")
end
 
function se_my_server_class:STATE_Read(packet, size)
    <имя_базового_класса>.STATE_Read(self, packet) -- здесь базовый класс читает своё состояние из пакета
    -- здесь можно прочитать состояние своего класса, которое было сохранено ранее
    local s = packet:r_stringZ() -- получим строку "моя строка"
end
 

Сохранение состояния клиентского класса

На клиентской стороне также имеется нечто подобное. Это реализуется методами биндера save и load. При создании биндера (см. мою статью насчёт класса object_binder) можно в этих методах что-то сохранить. Однако, нетпакет используется для сохранения в методе save. в этом он подобен методу STATE_Write серверного класса. А вот при загрузке почему-то вместо нетпакета передаётся поток на чтение (класс reader). Таким образом трюкачество, описанное в следующем пункте для клиентских объектов не выйдет.

Перепаковка серверных объектов с целью изменения их параметров

Предыдущие два варианта использования нетпакетов - это, так сказать, "законное" их использование. Но это не всё. Ничто не мешает вызывать методы STATE_Read и STATE_Write в произвольный момент времени, имитируя процесс сохранения и загрузки объекта. При этом можно сделать следующее:

 
local packet = net_packet() -- создаём пустой пакет
sobj:STATE_Write(packet) -- загрузили в наш пакет состояние серверного объекта
-- используя методы класса net_packet меняем нужные нам значения.
sobj:STATE_Read(packet, packet:w_tell()) -- записали в объект изменённое состояние обратно, имитируя процесс его загрузки
 

Несколько замечаний:

  • Все эти манипуляции надо проводить над объектами в оффлайне, поскольку онлайновый объект регулярно обновляет серверную часть и полностью его переписывает, так что любые изменения такого рода будут потеряны.
  • Надо знать структуру нетпакета для того объекта, который Вы собираетесь изменить. При таких хирургических операциях запросто можно всё испортить.


Регистрация скриптовых классов с помощью object_factory

Класс object_factory предназначен для регистрации пользовательских скриптовых классов. Конструктора у этого класса нет, нет и функции, с помощью которой можно его получить (по крайней мере я не знаю такой). Экземпляр этого класса доступен исключительно в качестве аргумента специальных функций, которые вызываются движком в определённое время. Функции эти прописаны в config\script.ltx в параметре class_registrators в секции common.

Например, там прописана class_registrator.register . Эта функция вызывается в самом начале один раз ещё при старте программы.

В функцию register движок передаёт один аргумент типа object_factory. Здесь как раз и можно зарегистрировать свой скриптовый класс.

У объекта object_factory два метода. Один принимает четыре аргумента строкового типа и предназначен для регистрации связки серверный-клиентский классы + идентификатор. Второй метод принимает три аргумента строкового типа и предназначен для регистрации чисто клиентских классов.

class object_factory {
    // регистрация связки клиентский класс / серверный класс
    void register(
        <клиентский_класс>, <скриптовый_серверный_класс>,
        <clsid_для конфигов>, <clsid_для_скриптов>);
    // регистрация чисто клиентских классов
    void register(
        <скриптовый_клиентский_класс>,
        <clsid_для конфигов>, <clsid_для_скриптов>);
};

Чтобы объяснить, зачем нужен этот класс, требуется немного лирики. Допустим, мы хотим создать некий объект (вертолёт к примеру). Игровые объекты вообще создаются с помощью метода create класса alife_simulator (см. ранее статью про этот класс). Первым и главным аргументов этого метода является имя секции в system.ltx. Даже если объект создаётся по индексу из all.spawn, всё равно имя секции неявно присутствует (прописана в самом all.spawn). Что такое секция? Это просто именованный набор параметров, это все знают. Однако вопрос, как движок определяет, какой именно класс создаётся для конкретно этой секции? Есть мнение, что для этого там есть параметр class. Например, в секции вертолёта этот параметр имеет такое значение:

class = C_HLCP_S

Как теперь можно представить себе последовательность создания объекта:

1. В функцию create передаётся имя секции (например для вертолёта имя секции будет "helicopter" ).
2. Движок читает секцию и находит там параметр class (для вертолёта имеет значение C_HLCP_S). Теперь известно, объект какого класса надо создать.
3. Движок создаёт объект нужного класса и инициализирует его значениями, полученными из секции. Если набор параметров не соответствует созданному классу (т.е. входит в противоречие с clsid), то будет ошибка при создании.
После создания объекта ему назначается id и ссылка на него передаётся как результат работы функции create.
4. Всё это происходит на серверной стороне. Ещё один момент - это какой клиентский объект создать при переходе объекта в онлайн. Эта информация также должна быть связана с соответствующим clsid.

Если теперь проверить идентификатор класса свежесозданного объекта с помощью метода серверного класса cobj:clsid(), то мы получим некое число. Это число соответствует одной из констант класса clsid (для вертолёта это clsid.helicopter). С другой стороны, это же число можно получить, прочитав значение параметра class из секции с помощью метода r_clsid() класса ini_file. Конкретное число не важно. Более того, вредно использовать конкретное число, поскольку оно может измениться при добавлении новых классов. Надо использовать вот эти идентификаторы.

Вроде как всё понятно. В движке имеется набор зарегистрированных сочетаний <серверный класс>/<клиентский класс>. Каждому такому сочетанию присвоен уникальный номер - идентификатор класса. Этот номер в секции файла system.ltx указывается через строковый идентификатор, а в коде ему же соответствует один из членов класса clsid. Набор этих сочетаний имеется в движке исходно, даже если ничего не делать.

К чему теперь объект object_factory? А он позволяет добавлять в этот набор свои пары, причём серверный класс там можно указывать свой, скриптовый (собственно ради чего это и делается). С помощью скриптового класса, унаследованного от одного из серверных классов, можно добавить на серверную сторону дополнительный функционал, такой как:

  • сохранение на серверной стороне дополнительных данных. Это происходит примерно так же, как сохранение в биндере.
  • можно дополнить или изменить некоторые аспекты поведения серверного класса. Например, можно изменить правила перехода в онлайн/оффлайн.

Пример реализации такого серверного класса (для того же вертолёта, в этом случае он создаётся на основе класса cse_alife_helicopter) можно посмотреть в файле se_car.script (он там, правда, весьма короткий и ненаглядный). Имя скриптового класса - se_heli. Имя клиентского класса для вертолёта - CHelicopter. Это один из классов, унаследованных от CGameObject. Создавать на их основе свои скриптовые нельзя.

Теперь можно посмотреть пример применения этого класса в модуле class_registrator.script. Выглядит это так (с сокращениями):

 
function cs_register(factory,client_object_class,server_object_class,clsid,script_clsid)
    factory:register    (client_object_class,server_object_class,clsid,script_clsid)
end
 
function c_register(factory,client_object_class,clsid,script_clsid)
    if (editor() == false) then
        factory:register(client_object_class,clsid,script_clsid)
    end
end
function register(object_factory)
    c_register(object_factory, "ui_main_menu.main_menu", "MAIN_MNU", "main_menu")
    ...
    cs_register    (object_factory, "CHelicopter", "se_car.se_heli", "C_HLCP_S", "script_heli")
    ...
end
 

Здесь видим, что методы класса вызываются опосредованно с помощью двух вспомогательных функций:

c_register - для чисто клиентских классов.
cs_register - для связки клиентский/серверный.

Сделано это, скорее всего, для наглядности, и никто не мешает в функции register непосредственно использовать методы object_factory.

Здесь мы видим, что скриптовый серверный класс se_car.se_heli, связывается с клиентским классом CHelicopter, для указания этой связки в секциях system.ltx задаётся строковый идентификатор "C_HLCP_S" , а для регистрации его в классе clsid ему назначается имя script_heli .

Собственно, всё. Эта регистрация происходит в самом начале, ещё до запуска главного меню. После выполнения модуля класс clsid прирастает новыми константами, а при создании соответствующих объектов получают управление соответствующие зарегистрированные скриптовые классы.

Что касается варианта для регистрации чисто клиентских скриптовых классов. Единственный пример использования класса object_factory таким образом показан в коде выше. Это регистрация класса для главного меню. Окна, ясное дело, существуют только на клиентской стороне. Кроме того, идентификатор для меню используется уже знакомый системе - он вшит в движок. Можно ли использовать это как-либо иначе - неизвестно.

Несколько ремарок.

  • Идентификаторы для создаваемой связки могут быть любыми. Но если они равны существующим значениям, то регистрируемая связка заменяет существующую.
  • Естественно, не получится комбинировать клиентский и серверный классы в произвольных сочетаниях. Они друг другу должны соответствовать. Кроме того, должен быть соответствующий экспортированный клиентский класс, а они имеются далеко не для всех игровых объектов. Например, для актора нет (появился только в Clear Sky). Учитывая это и изучив содержимое class_registrator.script, можно сделать вывод, что ресурс применения объекта object_factory практически исчерпан, поскольку почти все объекты, производные от CGameObject там уже задействованы. Впрочем, можно создавать какие-либо хитрые специализации уже знакомых объектов.


Серверные классы. Часть I. Иерархия серверных классов (описание не закончено)

При первом взгляде на lua_help.script возникает впечатление "дофига всего - объять нереально". Особенно это касается больших иерархий классов. Наиболее "развесистыми" являются три: оконные классы для создания пользовательского интерфейса и модификации элементов худа, серверные классы и клиентские классы для серверной и клиентской частей игровых объектов соответственно.

Про оконные классы поговорим в другой раз, а сейчас займёмся серверными и клиентскими классами.

Что потребуется.

Первое. Необходимо представлять себе, что такое классы и с чем их едят. Основное понятие, которое надо понимать - это наследование классов. Неплохо бы понимать идею базового класса и интерфейса.
Второе. Необходимо достаточно хорошо понимать природу разделения классов на серверные и клиентские. Можете почитать мой трёп здесь в статье про object_binder.
Третье. Вероятно, имеет смысл почитать мою статью о реализации классов в сталкере.
Четвёртое. Между серверными и клиентскими классами есть взаимосвязь. Кое что я попробую описать, кое-что уже описывал в статье о классе object_factory. Но это неполная информация.
Пятое. Я вынужден ввести некоторые искусственные соглашения при описании классов. Это необходимо для сокращения объёма дублирующей информации. Внимательно прочитайте это вступление!
Шестое. Информация приводится для Зова Припяти. Впрочем, отличия от ЧН и ТЧ минимальны. Я постарался пометить отличающиеся места, но где-то мог и не сделать этого. Смотрите внимательней!
Наконец. Данное описание - это не то чтобы описание в полном смысле этого слова. Я не в состоянии описать в одиночку более сотни классов и несколько сотен методов и свойств. Это скорее попытка привести в некоторую систему помойку из lua_help, убрать смысловой шум и создать основу для дальнейшего полного описания. Я постарался дать по возможности точную информацию о типах аргументов и возвращаемых значений функций, но назначение их не описано.

Итак. Все игровые сущности (сталкеры, монстры, предметы и пр.) в общем случае существуют одновременно на двух сторонах - на сервере и на клиенте. На каждой стороне для этого есть соответствующие классы. На серверной - это классы, имя которых начинается на "cse" . С некоторыми поправками можно считать, что все серверные классы происходят от cse_abstract. На клиентской стороне есть соответствующая иерархия классов. Можно считать, что все они происходят от CGameObject. На клиентской стороне есть ещё и game_object, который также является интерфейсом к клиентским объектам, причём основным интерфейсом. Его описание уже начато, но оно далеко от завершения.

Классы как серверные, так и клиентские, образуют иерархию наследования классов. На самом примитивном уровне можно считать, что наследование означает включение всех членов базового класса (т.е. свойств и методов) в производный класс. В свою очередь это означает, что нет необходимости описывать унаследованные члены для всех производных классов, достаточно это сделать один раз для базового. Кроме того, я ввожу важное соглашение для описания классов: я не буду приводить полный список членов класса для каждого из них, а только список методов и свойств, которые добавляет конкретный класс сверх унаследованных. Поэтому, если по описанию в классе нет ничего, то смотрите на классы, от которых он унаследован и смотрите список их методов и список методов их базовых классов и т.д.

Начнём с серверных.

Можно обратить внимание, что имеется некоторое количество базовых классов и интерфейсов, которые вообще не содержат никаких методов или свойств. Наследование от них не даёт конкретной информации о свойствах серверного класса, но парадоксальным образом может дать некоторые догадки о применимости методов game_object. Вот список этих базовых интерфейсов:

// все приведённые интерфейсы не содержат ни методов ни свойств, поэтому
// приводится только их список и отношение их наследования
// в принципе, можно погадать и на тему
ipure_alife_load_object
ipure_alife_save_object
ipure_alife_load_save_object : ipure_alife_load_object,ipure_alife_save_object
ipure_server_object : ipure_alife_load_save_object
cpure_server_object : ipure_server_object
// все вообще классы наследованы от cpure_server_object,
// его можно описать, как "объект, который умеет сохраняться и восстанавливать своё состояние"
 
ipure_schedulable_object
cse_alife_schedulable : ipure_schedulable_object
// класс cse_alife_schedulable - это вроде как объекты, с которыми работает планировщик
 
cse_visual // для этих объектов нужна модель. Их "видно"
cse_motion // эти объекты умеют двигаться
cse_ph_skeleton // у этих есть кости
cse_shape // вроде как объекты, с которыми связана некоторая область пространства: аномалии, смарты и т.п.
cse_alife_group_abstract // без понятия, зачем нужен
cse_alife_inventory_item // объекты, которые можно взять в инвентарь
 
// этот интерфейс является некоторым исключением, поскольку содержит методы
// судя по использованию отвечает за возможность торговать: актор и сталкеры
C++ class cse_alife_trader_abstract {
    string profile_name();
    int reputation();
    int rank();
    int community() const;
};


Несколько предваряющих комментариев к дальнейшему материалу.

Почти каждый из описанных классов имеет конструктор, который по традиции языка C++ имеет имя, совпадающее с именем класса, и не возвращает никакого значения. У каждого объекта из иерархии cse_abstract всего один конструктор, который всегда принимает один аргумент - строку. Очевидно, что аргумент - это имя секции из system.ltx. Как и для остальных классов, экспортированных с помощью технологии Luabind, это означает, что имеется глобальная функция с именем класса, которая принимает такие же аргументы, как и конструктор, и возвращает объект класса. И такие функции на самом деле есть для каждого серверного класса. Однако попытка их вызвать у меня лично приводит к неизменному вылету. Так и должно быть! Серверные объекты создаются опосредованно, с помощью вызова метода create класса alife_simulator. См. эту статью.
Итак, конструктор всегда один, имеет определённое имя и один и тот же аргумент, да и толку от него нет. Кроме того, его наличие в составе класса - вообще изрядная условность, поскольку он не внутри класса, а снаружи. Так что в качестве дополнительного соглашения, позволяющего экономить пространство форума, я не буду приводить эти конструкторы в описании классов.
Если класс не добавляет ничего к базовому, то ради экономии места его пишу по сокращённому варианту - одной строкой с указанием только его имени и базовых классов.
Серверные классы - это классы, экспортированные таким образом, что на их основе можно создавать свои скриптовые классы. Опять же см. эту статью. Примеры таких скриптовых классов в игре можно найти в файлах, которые в основном начинаются с "se_": se_artefact.script, se_item.script и т.д.
  • Вся иерархия серверных классов:
class cse_abstract : cpure_server_object {
    vector* angle; // начиная с ЗП
    int id;
    int parent_id;
    vector* position;
    int script_version;
 
    void UPDATE_Read(net_packet&);
    void STATE_Read(net_packet&, number);
    string name(const cse_abstract*);
    void UPDATE_Write(net_packet&);
    void STATE_Write(net_packet&);
    int clsid() const;
    ini_file* spawn_ini();
    string section_name();
 
    cse_abstract* init(); //
    // FillProps и OnEvent начиная с ЧН убраны
    void FillProps(string, class xr_vector<class PropItem *,class xalloc<class PropItem *> >&);
    void OnEvent(net_packet&, number, number, ClientID);
};

От класса cse_abstract унаследованы ВСЕ остальные классы. Иногда примешиваются дополнительные интерфейсы.

    • Список классов, унаследованных от cse_abstract:
 // так выглядит сокращённое описание, класс ничего не добавляет к родительскому
class cse_alife_graph_point : cse_abstract
class cse_spectator : cse_abstract
class cse_temporary : cse_abstract
class cse_alife_object_climable : cse_shape,cse_abstract
 
class CSE_AbstractVisual : cse_visual,cse_abstract {
    string getStartupAnimation(); // добавлен всего один метод.
};
 
class cse_alife_object : cse_abstract {
    int m_game_vertex_id;
    int m_level_vertex_id;
    int m_story_id;
    bool online;
 
    bool used_ai_locations() const;
    ??void?? use_ai_locations(boolean); // начиная с ЗП
    bool can_save() const;
    bool can_switch_online() const;
    void can_switch_online(boolean);
    bool interactive() const;
    bool visible_for_map() const;
    void visible_for_map(boolean);
    bool can_switch_offline() const;
    void can_switch_offline(boolean);
    bool move_offline() const;
    void move_offline(boolean);
};
    • От класса cse_alife_object унаследован всего один класс cse_alife_dynamic_object:
class cse_alife_dynamic_object : cse_alife_object {
    void switch_offline();
    void switch_online();
    bool keep_saved_data_anyway() const; // либо synchronize_location, либо redundant
    void on_spawn();
    void on_before_register();
    void on_register();
    void on_unregister();
};
      • Список классов, унаследованных от cse_alife_dynamic_object:
class cse_alife_space_restrictor : cse_alife_dynamic_object,cse_shape
        • Список классов, унаследованных от cse_alife_space_restrictor:
class cse_alife_level_changer : cse_alife_space_restrictor
class cse_alife_team_base_zone : cse_alife_space_restrictor
class cse_alife_smart_zone : cse_alife_space_restrictor,cse_alife_schedulable {
    float detect_probability();
    void smart_touch(cse_alife_monster_abstract*);
    void unregister_npc(cse_alife_monster_abstract*);
    void update(); // явно идёт от cse_alife_schedulable
    function register_npc(cse_alife_monster_abstract*);
    float suitable(cse_alife_monster_abstract*) const;
    CALifeSmartTerrainTask* task(cse_alife_monster_abstract*);
    bool enabled(cse_alife_monster_abstract*) const;
};
class cse_alife_online_offline_group : cse_alife_dynamic_object,cse_alife_schedulable {
    // все методы класса экспортированы только начиная с ЗП
    void update();
    void register_member(number);
    void unregister_member(number);
    function clear_location_types();
    function get_current_task();
    function commander_id();
    function squad_members() const;
    function force_change_position(vector);
    function add_location_type(string);
    ??int?? npc_count() const;
};
 
class cse_smart_cover : cse_alife_dynamic_object {
    function description() const;
    function set_available_loopholes(object);
};
 
class cse_custom_zone : cse_alife_dynamic_object,cse_shape
        • Список классов, унаследованных от cse_custom_zone:
class cse_torrid_zone : cse_custom_zone,cse_motion
class cse_anomalous_zone : cse_custom_zone
class cse_zone_visual : cse_anomalous_zone,cse_visual
class cse_alife_dynamic_object_visual : cse_alife_dynamic_object,cse_visual
        • Список классов, унаследованных от cse_alife_dynamic_object_visual:
class cse_alife_car : cse_alife_dynamic_object_visual,cse_ph_skeleton
class cse_alife_helicopter : cse_alife_dynamic_object_visual,cse_motion,cse_ph_skeleton
// класс cse_alife_inventory_box появился в ЗП
 class cse_alife_inventory_box : cse_alife_dynamic_object_visual
class cse_alife_mounted_weapon : cse_alife_dynamic_object_visual
class cse_alife_object_breakable : cse_alife_dynamic_object_visual
class cse_alife_object_hanging_lamp : cse_alife_dynamic_object_visual,cse_ph_skeleton
class cse_alife_object_physic : cse_alife_dynamic_object_visual,cse_ph_skeleton
class cse_alife_object_projector : cse_alife_dynamic_object_visual
class cse_alife_ph_skeleton_object : cse_alife_dynamic_object_visual,cse_ph_skeleton
class cse_alife_trader : cse_alife_dynamic_object_visual,cse_alife_trader_abstract
 
class cse_alife_creature_abstract : cse_alife_dynamic_object_visual {
    int team;
    int group;
    int squad;
 
    void on_death(cse_abstract*);
    bool alive() const;
    int g_team();
    int g_group();
    int g_squad();
    float health() const;
    // o_torso появилась начиная с ЧН
    Rotation o_torso();
};
          • Список классов, унаследованных от cse_alife_creature_abstract:
class cse_alife_creature_actor : cse_alife_creature_abstract,cse_alife_trader_abstract,cse_ph_skeleton
class cse_alife_creature_crow : cse_alife_creature_abstract // начиная с ЧН
class cse_alife_creature_phantom : cse_alife_creature_abstract // начиная с ЧН
 
class cse_alife_monster_abstract : cse_alife_creature_abstract,cse_alife_schedulable {
    int group_id;   // начиная с ЗП
    int m_smart_terrain_id; //есть во всех версиях, но в ТЧ не внесён в lua_help
 
    function kill(); // начиная с ЗП
    void update();
    function force_set_goodwill(number, number); // начиная с ЗП
    function clear_smart_terrain();  //устанавливает m_smart_terrain_id = 65535
    float travel_speed(); // начиная с ЧН
    void travel_speed(float); // начиная с ЧН
    function smart_terrain_task_deactivate();
    function smart_terrain_task_activate();
    float current_level_travel_speed(); // начиная с ЧН
    void current_level_travel_speed(float); // начиная с ЧН
    CALifeMonsterBrain* brain();
    bool has_detector(); // начиная с ЗП
    int smart_terrain_id(); // всегда тоже самое, что и m_smart_terrain_id
    int rank();
};
            • Список классов, унаследованных от cse_alife_monster_abstract:
class cse_alife_monster_rat : cse_alife_monster_abstract,cse_alife_inventory_item
class cse_alife_monster_zombie : cse_alife_monster_abstract
class cse_alife_monster_base : cse_alife_monster_abstract,cse_ph_skeleton
class cse_alife_psydog_phantom : cse_alife_monster_base
class cse_alife_human_stalker : cse_alife_human_abstract,cse_ph_skeleton
class cse_alife_human_abstract : cse_alife_trader_abstract,cse_alife_monster_abstract {
    // rank из интерфейса cse_alife_trader_abstract конфликтует с такой же из cse_alife_monster_abstract
    // непонятно, какая из них "побеждает" и непонятно, к чему тогда относится вот эта
    void set_rank(int); //
};
        • Список классов, унаследованных от cse_alife_item:
class cse_alife_item_ammo : cse_alife_item
class cse_alife_item_artefact : cse_alife_item
class cse_alife_item_bolt : cse_alife_item
class cse_alife_item_custom_outfit : cse_alife_item
class cse_alife_item_detector : cse_alife_item
class cse_alife_item_document : cse_alife_item
class cse_alife_item_explosive : cse_alife_item
class cse_alife_item_grenade : cse_alife_item
class cse_alife_item_pda : cse_alife_item
class cse_alife_item_torch : cse_alife_item
 
class cse_alife_item_weapon : cse_alife_item {
    // clone_addons появилась в ЗП
    function clone_addons(cse_alife_item_weapon*);
};
 
// иерархия наследования от cse_alife_item_weapon простая, так что без спойлера
// класс cse_alife_item_weapon_auto_shotgun появился в ЗП
class cse_alife_item_weapon_auto_shotgun : cse_alife_item_weapon
class cse_alife_item_weapon_magazined : cse_alife_item_weapon
class cse_alife_item_weapon_magazined_w_gl : cse_alife_item_weapon_magazined
class cse_alife_item_weapon_shotgun : cse_alife_item_weapon

Продолжение следует...


Серверные классы. Часть II. Структура наследования и несколько заключительных слов

Полагаю, красивая картинка никому не помешает. Вот вам диаграмма серверных классов с высоты птичьего полёта =)

Обращаю внимание на нотацию записи: большая пустая стрелка означает отношение обобщения (или наследования иными словами) и направлена от унаследованного класса к базовому. Включение абстрактного интерфейса обозначается стрелкой с кружочком и указанием, какой интерфейс наследуется. Это сокращённая форма записи, позволяющая избавиться от нагромождения линий и лучше понять основную линию наследования.

Genealogy of the server classes.png

С серверными классами пока всё. Некоторые выводы по данному этапу.

1. Большинство классов не добавляют новых методов и свойств. В целом, информации существенно меньше, нежели кажется на первый взгляд.
2. Между ТЧ, ЧН и ЗП не так уж и много отличий. Хотя отличия имеются: добавлено несколько новых классов, изрядно добавлено новых методов в существующих классах и пара методов исчезла.
3. Я не описывал ни назначение классов, ни назначение их методов. В некоторых местах (в основном для новых методов в ЧН и ЗП) не удалось пока получить информации о параметрах и возвращаемых значениях. Наверняка где-то упустил продокументировать различие между играми.

В принципе, практически все методы классов до cse_alife_object включительно известны, как и некоторые из остальных классов. Однако у меня сейчас нет возможности заниматься документированием каждого из нескольких десятков методов. Надеюсь на помощь сообщества.

Продолжение следует...


Клиентские классы

Разбираем систему клиентских классов. Сначала немного лирики. Итак, на клиентской стороне есть своя иерархия классов, отвечающих за онлайновые (то есть клиентские) объекты. Все они унаследованы от CGameObject. Дерево наследования там не столь сложное, как для серверных классов. Тем не менее для каждого (почти) типа игровых объектов есть свой класс, и при первом взгляде всё смотрится вполне прилично. Но это только на первый взгляд. При более внимательном рассмотрении выясняется, что классы хоть и есть, но добраться до произвольного клиентского объекта невозможно. То есть нет никаких функций, с помощью которых можно было бы получить произвольный клиентский объект, производный от CGameObject. Соответственно, нет и возможности вызывать их методы. Кроме того, эти классы не предназначены для создания на их основе скриптовых классов. Таким образом, все общие пути для использования объектов этого класса закрыты. Соответственно, основным и почти единственным интерфейсом к объектам на клиентской стороне остаётся game_object.

Однако не всё так трагично =) Есть исключения. Если в классе добавлены какие-либо методы сверх основного набора из CGameObject, то для получения объектов такого класса в game_object имеются специальные методы. В ТЧ исходно всего три таких класса: CCar, CHelicopter и hanging_lamp. Соответственно в game_object есть методы:

  • get_car() - для CCar
  • get_helicopter() - для CHelicopter
  • get_hanging_lamp() - для hanging_lamp

В ЧН методы добавлены в классы CPhysicObject, CArtefact, CZoneCampfire и CAI_Bloodsucker. Соответственно появились и новые методы в game_object: get_physics_object(), get_artefact() и get_campfire(). А вот для кровососа почему-то функции нет.

К этому могу добавить, что посмотрев на состав методов CGameObject, можно увидеть их сходство с набором методов object_binder. Таким образом, скорее всего, методы CGameObject опосредованно доступны через биндер. Для тех, что недоступны, вроде как имеются некие аналоги в game_object, и даже для работы с визуалом, похоже, начиная с ЗП появились функции (но это неподтверждённая информация).

Наконец, надо заметить, что главным использованием системы этих классов является не прямое использование, а опосредованное - через механизм регистрации скриптовых серверных классов. Я об этом рассказывал здесь, рекомендую почитать. Возможность регистрации своего скриптового серверного класса определяется наличием соответствующего ему экспортированного клиентского. Достаточно, чтобы он просто был. И вот как раз здесь имеется приличный прорыв: по сравнению с ТЧ в ЧН и ЗП добавлено много новых классов - теперь охвачены почти все игровые объекты (почти, но таки не все).

Теперь можно делать скриптовые серверные классы для ящиков, PDA, аддонов и аномалий. В первую очередь это означает, что теперь можно без проблем хранить серверное состояние этих объектов, что существенно облегчает создание модов, где эти объекты используются в оффлайне. Также добавлен актор, что означает возможность сделать серверного актора. Как-то это используется в игре.

Довольно лирики. Далее представлена система клиентских классов. Используются те же соглашения, что и раньше: конструктор не показан, если класс не добавляет своих методов, то показан одной строкой.


Иерархия клиентских классов

Сперва базовые интерфейсы. Их не так много, как у серверных.

class DLL_Pure {
    DLL_Pure* _construct();
};

Как будет видно далее, на самом деле самым базовым классом является DLL_Pure. Похоже, что именно этот класс является необходимым для механизма регистрации скриптовых классов. Есть ещё классы, не относящиеся к игровым объектам, но унаследованные от DLL_Pure, которые тоже можно использовать с классом object_factory:

  • ICollidable
  • IRenderable
  • ISheduled

Непосредственно от DLL_Pure наследованы всего три класса:

  • class ce_smart_zone : DLL_Pure
  • class ce_script_zone : DLL_Pure
class CGameObject : DLL_Pure,ISheduled,ICollidable,IRenderable {
    ??string?? Visual() const;
    function getEnabled() const;
    void net_Export(net_packet&);
    void net_Import(net_packet&);
    function getVisible() const;
    int net_Spawn(cse_abstract*);
    bool use(CGameObject*);
};

Все остальные классы имеют своим предком CGameObject.


Классы, производные от CGameObject

 
CActor // начиная с ЧН
CAI_Stalker
CAI_Trader
 
//монстры
class CAI_Bloodsucker : CGameObject {
    function force_visibility_state(number);
};
 
CAI_Boar
CAI_Dog
CAI_Flesh
CAI_PseudoDog
CBurer
CCat
CChimera
CController
CFracture
CPoltergeist
CPseudoGigant
CPsyDog
CPsyDogPhantom
CSnork
CTushkano
CZombie
 
//съедаемые предметы
CAntirad
CBottleItem
CFoodItem
CMedkit
 
//устройства
CTorch
CSimpleDetector     //класс есть, но не документирован в lua_help
CAdvancedDetector   //класс есть, но не документирован в lua_help
CEliteDetector      //класс есть, но не документирован в lua_help
CScientificDetector //класс есть, но не документирован в lua_help
 
//аддоны
CGrenadeLauncher
CScope
CSilencer
 
 
//разные объекты
 
//hanging_lamp
class hanging_lamp : CGameObject {
    hanging_lamp ();
    void turn_on();
    void turn_off();
};
 
//CCar
class holder {
    bool engaged();
    void Action(int, int);
    void SetParam(int, vector);
};
class CCar : CGameObject,holder {
    // CCarWeapon::__unnamed
    const eWpnDesiredDir = 1;
    const eWpnDesiredPos = 2;
    const eWpnActivate = 3;
    const eWpnFire = 4;
    const eWpnAutoFire = 5;
    const eWpnToDefaultDir = 6;
 
    CCar ();
 
    float GetfHealth() const;
    float SetfHealth(float);
    vector CurrentVel();
    bool IsObjectVisible(game_object*);
    int ExplodeTime();
    void SetExplodeTime(int);
    bool HasWeapon();
    float FireDirDiff();
    void CarExplode();
    bool CanHit();
};
 
//CHelicopter
class CHelicopter : CGameObject {
    //EHeliState
    const eAlive = 0;
    const eDead = 1;
    //EHeliBodyState
    const eBodyByPath = 0;
    const eBodyToPoint = 1;
    //EHeliHuntState
    const eEnemyNone = 0;
    const eEnemyPoint = 1;
    const eEnemyEntity = 2;
    //EHeilMovementState
    const eMovNone = 0;
    const eMovToPoint = 1;
    const eMovPatrolPath = 2;
    const eMovRoundPath = 3;
    const eMovLanding = 4;
    const eMovTakeOff = 5;
 
    bool m_dead;
    bool m_exploded;
    bool m_flame_started;
    bool m_light_started;
    float m_max_mgun_dist;
    float m_max_rocket_dist;
    float m_min_mgun_dist;
    float m_min_rocket_dist;
    bool m_syncronize_rocket;
    int m_time_between_rocket_attack;
    bool m_use_mgun_on_attack;
    bool m_use_rocket_on_attack;
 
    CHelicopter ();
 
    void Die(); // красиво падать, потом взорваться
    void Explode(); // сразу взорваться
 
    float GetfHealth() const;
    float SetfHealth(float);
 
    EHeliHuntState GetHuntState();
    EHeliBodyState GetBodyState();
    EHeilMovementState GetMovementState();
    ??EHeliState?? GetState();
 
    void ClearEnemy(); // ни в кого не стрелять
    function SetEnemy(game_object*); // в кого стрелять
    function SetEnemy(vector*); // куда стрелять
 
 
    float GetCurrVelocity();
    vector GetCurrVelocityVec();
    float GetMaxVelocity();
    void SetMaxVelocity(float);
 
    float GetDistanceToDestPosition();
    float GetOnPointRangeDist();
    float GetRealAltitude(); // абсолютная высота
    float GetSafeAltitude();
    float GetSpeedInDestPoint(number);
    void SetSpeedInDestPoint(float);
    void SetBarrelDirTolerance(float);
    function SetLinearAcc(number, number);
 
    void GoPatrolByPatrolPath(string, int);
    void GoPatrolByRoundPath(vector, float, boolean);
    bool isVisible(game_object*);
    void LookAtPoint(vector, boolean); // развернуться носом в точку
    void SetDestPosition(vector*); // куда лететь
    bool UseFireTrail();
    void UseFireTrail(boolean);
    void SetFireTrailLength(float);
    void SetOnPointRangeDist(float);
    void StartFlame(); // начать гореть
    void TurnEngineSound(boolean); // звук двигателя
    void TurnLighting(boolean); // включить прожектор
};
 
//CPhysicObject
class CPhysicObject : CGameObject {
    function anim_time_get();
    function anim_time_set(number);
    function play_bones_sound();
    function run_anim_back();
    function run_anim_forward();
    function set_door_ignore_dynamics();
    function stop_anim();
    function stop_bones_sound();
    function unset_door_ignore_dynamics();
};
 
CDestroyablePhysicsObject
CExplosiveItem
CWeaponAmmo
CInventoryBox
CPda
 
//гранаты
CF1
CRGD5
 
//оружие
CWeaponAK74
CWeaponAutomaticShotgun
CWeaponBinoculars
CWeaponBM16
CWeaponFN2000
CWeaponFORT
CWeaponGroza
CWeaponHPSA
CWeaponKnife
CWeaponLR300
CWeaponPM
CWeaponRG6
CWeaponRPG7
CWeaponShotgun
CWeaponSVD
CWeaponSVU
CWeaponUSP45
CWeaponVal
CWeaponVintorez
CWeaponWalther
 
//аномалии
CMincer
CMosquitoBald
CHairsZone
CRadioactiveZone
CTorridZone
 
//разные области
class CZoneCampfire : CGameObject {
    function is_on();
    function turn_on();
    function turn_off();
};
 
CLevelChanger
CSpaceRestrictor
smart_cover_object
 
//Шкурки
CStalkerOutfit
CHelmet //класс есть, но не документирован в lua_help
 
//Артефакты
class CArtefact : CGameObject {
    function SwitchVisibility(boolean);
    function FollowByPath(string, number, vector);
    function GetAfRank() const;
};
 
//Производные от CArtefact
CBastArtefact
CBlackDrops
CBlackGraviArtefact
CDummyArtefact
CElectricBall
CFadedBall
CGalantineArtefact
CGraviArtefact
CMercuryBall
CRustyHairArtefact
CThornArtefact
CZudaArtefact
 

Рисовать картинку с иерархией наследования смысла нет, поскольку и так всё довольно очевидно. За исключением малого числа классов остальные к CGameObject ничего не добавляют. Соответственно и описывать нечего. Названия классов более или менее говорят о том, для каких объектов они предназначены. Где мог, указал точные сигнатуры функций (CCar, CHelicopter и hanging_lamp). Для новых классов и методов по сравнению с ТЧ такой информации, к сожалению, нет.

Несколько рассуждений на тему взаимодействия серверного и клиентского объектов. Попробуем рассуждать логически. Есть сервер, есть клиент. Это в общем случае разные компьютеры. Каким может быть взаимодействие объектов, находящихся по разную сторону сети? Могут ли они, к примеру, вызывать методы друг друга, или непосредственно читать данные своего "напарника"? Очевидно нет. Моё мнение такое, что их взаимодействие сводится в основном к посылке данных друг другу. При этом, данные посылаются "пачкой", т.е. обо всём состоянии сразу. То, что я наблюдал, выглядит так: при переходе в онлайн создаётся клиентский объект, серверный его инициализирует, посылая ему при создании нетпакет. Этот момент можно отловить в биндере клиентского класса. Все дальнейшие движения происходят от клиентского объекта к серверному. Это означает, что периодически клиентский затирает данные серверного. Точные моменты, когда это происходит, периодичность, а также механизм передачи данных мне лично понять пока не удалось. По крайней мере на серверной стороне ничего не видно, просто тихо меняются данные, и понять, когда это происходит, крайне непросто.

Однако некоторые выводы таки можно сделать. Если взаимодействие классов примерно такое, как я здесь предположил, то совместимость классов определяется совместимостью бинарных данных, которые они друг другу посылают. Ну в общем классы должны друг другу соответствовать, хотя, как далее будет видно, это правило на практике (очень редко) нарушается. Если не забыли ещё, то зарегистрированные пары классов задаются идентификаторами класса. В файлах конфигурации (в секциях объектов) этим идентификаторам соответствует параметр class. В скриптах - это один из членов clsid. В движке уже зарегистрированы соответствующие пары классов, и вы можете добавлять свои пары с помощью класса object_factory. Я это всё описывал здесь.

Далее я привожу таблицу соответствия классов. Для полноты картины в ней указаны классы, которые не экспортированы в Lua. Если класс не экспортирован, то это означает, что вам не удастся зарегистрировать на его основе свою пару классов. Однако это не мешает (теоретически) создавать объекты этого класса при условии, что имеется и известен его идентификатор. Приведённая таблица собрана из разных источников, в том числе основана на примерах регистрации классов из игры. Видно, что иногда клиентский класс использует в качестве "напарника" серверный, который является базовым для своего "родного" серверного. Видимо, это технически в норме вещей и так можно поступать и нам. Из этого ряда выбивается CZombie, для которого вроде бы родным должен быть серверный класс cse_alife_monster_zombie, а регистрируют его в cse_alife_monster_base. Это вроде как нарушает вышеописанную логику, поскольку cse_alife_monster_base не является базовым для cse_alife_monster_zombie. С другой стороны, он так зарегистрирован в самом движке. Возможно cse_alife_monster_zombie попросту не задействован и никакого отношения к CZombie на самом деле не имеет.

Таблица соответствия серверных и клиентских классов

 
  Серверный класс                      |  Клиентский класс
---------------------------------------+-------------------------------------------------
cse_abstract                           |  
cse_alife_graph_point                  |  
cse_spectator                          |  CSpectator -- не экспортированный класс
cse_temporary                          |  CExplosiveRocket -- не экспортированный класс
cse_alife_object_climable              |  CClimableObject -- не экспортированный класс
CSE_AbstractVisual                     |  
cse_alife_object                       |  
cse_alife_dynamic_object               |  
cse_smart_cover                        |  smart_cover_object
cse_alife_online_offline_group         |  
cse_custom_zone                        |  CCustomZone -- не экспортированный класс
cse_alife_space_restrictor             |  ce_smart_zone -- cse_alife_space_restrictor - это базовый
                                       |  для cse_alife_smart_zone
                                       |  видимо так можно делать
                                       |  CSpaceRestrictor
                                       |  ce_script_zone
cse_alife_dynamic_object_visual        |  CScriptObject -- не экспортированный класс
cse_alife_creature_abstract            |  
cse_alife_inventory_box                |  CInventoryBox
cse_alife_mounted_weapon               |  
cse_alife_object_breakable             |  CBreakableObject -- не экспортированный класс
cse_alife_object_projector             |  CProjector -- не экспортированный класс
cse_alife_item                         |  CAntirad
                                       |  CBottleItem
                                       |  CFoodItem
                                       |  CMedkit
                                       |  CGrenadeLauncher
                                       |  CScope
                                       |  CSilencer
                                       |  CInventoryItemObject -- не экспортированный класс
                                       |  CMPPlayersBag -- не экспортированный класс
                                       |  CFlare -- не экспортированный класс
cse_alife_trader                       |  CAI_Trader
cse_alife_helicopter                   |  CHelicopter
cse_alife_car                          |  CCar
cse_alife_object_hanging_lamp          |  hanging_lamp
cse_alife_object_physic                |  CDestroyablePhysicsObject
                                       |  CPhysicObject
cse_alife_ph_skeleton_object           |  CPhysicsSkeletonObject -- не экспортированный класс
cse_alife_creature_crow                |  CAI_Crow -- не экспортированный класс
cse_alife_creature_phantom             |  CPhantom -- не экспортированный класс
cse_alife_creature_actor               |  CActor
cse_alife_monster_abstract             |  
cse_alife_monster_zombie               |  CZombie -- вроде должен быть здесь
cse_alife_monster_rat                  |  CAI_Rat -- не экспортированный класс
cse_alife_monster_base                 |  CAI_Bloodsucker
                                       |  CAI_Boar
                                       |  CAI_Dog
                                       |  CAI_Flesh
                                       |  CAI_PseudoDog
                                       |  CBurer
                                       |  CCat
                                       |  CChimera
                                       |  CController
                                       |  CFracture
                                       |  CPoltergeist
                                       |  CPseudoGigant
                                       |  CPsyDog
                                       |  CPsyDogPhantom
                                       |  CSnork
                                       |  CTushkano
                                       |  CZombie -- это нарушает логику.
                                       |  cse_alife_monster_base - не базовый для cse_alife_monster_zombie
cse_alife_psydog_phantom               |  CPsyDogPhantom // должно быть так, вместо
                                       |  этого записан в cse_alife_monster_base
cse_alife_human_abstract               |  
cse_alife_human_stalker                |  CAI_Stalker
cse_alife_item_ammo                    |  CWeaponAmmo
cse_alife_item_artefact                |  CElectricBall
                                       |  CBastArtefact
                                       |  CBlackDrops
                                       |  CBlackGraviArtefact
                                       |  CDummyArtefact
                                       |  CFadedBall
                                       |  CGalantineArtefact
                                       |  CGraviArtefact
                                       |  CMercuryBall
                                       |  CRustyHairArtefact
                                       |  CThornArtefact
                                       |  CZudaArtefact
                                       |  CtaGameArtefact -- не экспортированный класс
cse_alife_item_bolt                    |  CBolt -- не экспортированный класс
cse_alife_item_custom_outfit           |  CStalkerOutfit
                                       |  CScientificOutfit -- не экспортированный класс
                                       |  CMilitaryOutfit -- не экспортированный класс
                                       |  CExoOutfit -- не экспортированный класс
cse_alife_item_helmet                  |  CHelmet
cse_alife_item_detector                |  CAdvancedDetector
                                       |  CEliteDetector
                                       |  CScientificDetector
                                       |  CSimpleDetector
cse_alife_item_document                |  CInfoDocument -- не экспортированный класс
cse_alife_item_explosive               |  CExplosiveItem
cse_alife_item_grenade                 |  CF1
                                       |  CRGD5
cse_alife_item_pda                     |  CPda
cse_alife_item_torch                   |  CTorch
cse_alife_item_weapon                  |  CWeaponKnife
cse_alife_item_weapon_auto_shotgun     |  CWeaponAutomaticShotgun
cse_alife_item_weapon_magazined        |  CWeaponBinoculars
                                       |  CWeaponHPSA
                                       |  CWeaponLR300
                                       |  CWeaponMagazined
                                       |  CWeaponPM
                                       |  CWeaponRPG7
                                       |  CWeaponSVD
                                       |  CWeaponSVU
                                       |  CWeaponUSP45
                                       |  CWeaponVal
                                       |  CWeaponVintorez
                                       |  CWeaponWalther
                                       |  CWeaponMagazined -- не экспортированный класс
                                       |  CWeaponFORT -- не экспортированный класс
cse_alife_item_weapon_shotgun          |  CWeaponBM16
                                       |  CWeaponRG6
                                       |  CWeaponShotgun
cse_alife_item_weapon_magazined_w_gl   |  CWeaponAK74
                                       |  CWeaponGroza
                                       |  CWeaponFN2000
                                       |  CWeaponMagazinedWGrenade -- не экспортированный класс
cse_alife_level_changer                |  CLevelChanger
cse_alife_team_base_zone               |  CTeamBaseZone -- не экспортированный класс
cse_alife_smart_zone                   |  ce_smart_zone
cse_anomalous_zone                     |  CMincer
                                       |  CMosquitoBald
                                       |  CRadioactiveZone
                                       |  CNoGravityZone -- не экспортированный класс
                                       |  CZoneCampfire
cse_zone_visual                        |  CHairsZone
cse_torrid_zone                        |  CTorridZone
CSE_ALifeStationaryMgun                |  CWeaponStatMgun -- не экспортированный класс
-- серверный также не экспортирован    |
 
Другие места
LANGUAGE