Перейти к контенту
Malandrinus

Справочник по функциям и классам

Рекомендуемые сообщения

log & wait

первое не работает, второе кажется имеет особенность класть игру в лог. CoP

Изменено пользователем Unnamed Black Wolf
Ссылка на комментарий

Из спойлера "Описание класса game_object:" :

...

// функции с memory в имени имеют отношение к "памяти" неписей. Можно получать

// разную информацию о том, что видел, слышал непись и т.п.

...

А можно об этом немножечко поподробнее.

Ссылка на комментарий

self.object:position():distance_to(self.object:memory_position(self.object:best_enemy())) < self.a.radius

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

self.object:memory_time(self.object:best_enemy())

это уже узнавание времени скорее всего времени нападение худшего врага....

 

Про остальное надо экспериментировать...

 

Примеры взяты из xr_camper.script.

Ссылка на комментарий
А можно об этом немножечко поподробнее.

 

not_yet_visible_objects

memory_visible_objects

memory_sound_objects

memory_hit_objects

 

вроде как эти методы возвращают итераторы для соответствующих объектов. Каких именно, смотри описание game_object. Итератор - это функция, которая при вызове возвращает очередной объект из набора, а под конец возвращает nil. Это позволяет итерировать по набору с циклом for также, как по таблицам (для справки, функция pairs - это тоже итератор).

 

Информация непроверенная, основана на устаревшей документации из билда 1935. Было бы неплохо, если бы кто проверил и привел рабочие примеры.

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

Все именно так и работает.

Вот вам рабочий пример:

    local friends = {}
    local function check_object(obj)
        if obj and obj.clsid and obj:alive() and obj:id() ~= sid then
            if IsStalker(obj) and npc:relation(obj) ~= game_object.enemy then
                table.insert(friends,obj)
            end
        end
    end
    for o in npc:memory_visible_objects() do
        check_object(o:object())
    end
    for o in npc:memory_sound_objects() do
        check_object(o:object())
    end

 

 

Ссылка на комментарий

Небольшое дополнение к ранее описанному здесь классу net_packet. Ранее я писал, что при создании нового объекта этого класса позиции чтения и записи установлены в 0. Оказалось, что в ЗП это не так, и позиция записи и чтения для свежесозданного пакета могут быть попросту случайными числами. При этом в большинстве случаев эти значения всё-таки равны нулю. Полагаю, что в этом и была причина проблем с созданием множества пакетов и вылетом при создании и использовании очередного. Также не подтверждается утверждение о том, что в ЗП нельзя создать много пакетов. Можно, я создал 50 тыс. в одном цикле, и проблем это не вызвало.

 

Резюме:

1. в ЗП нужно перед использованием в обязательном порядке устанавливать как позицию записи, так и позицию чтения вновь созданного пакета. Для позиции чтения есть метод r_seek(<позиция>). Для записи метода нет, надо использовать w_begin(<значение>), который устанавливает позицию записи в ноль, записывает туда двухбайтовое <значение> и смещает позицию на два байта.

2. Исходя из сказанного вовсе нет необходимости создавать пакет заново. В ТЧ это имело смысл по причине простоты (пакет был нормально инициализирован). В ЗП имеет смысл создать один глобальный пакет и везде использовать только его. Всё равно надо будет переустанавливать позиции при каждом использовании. Заодно сэкономим такты и память. Естественно возникнут проблемы с реентерабельностью функций, но это уже другой разговор.

  • Нравится 1
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

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


Инфопорция (infoportion - порция информации) представляет собой нечто вроде логической переменной. Инфопорция имеет имя и связанное с ним логическое значение.


Инфопорция всегда связана с каким-либо объектом. Инфопорция может быть установлена только для актора, сталкеров, монстров и торговца (иначе трейдера, единственный такой объект - Сидор). Если Инфопорция для объекта установлена в true, то говорят ещё "объект имеет инфопорцию, или объект имеет информацию". Инфопорции являются частью общего механизма построения сюжета. Комбинация выданных инфопорций у всех сюжетных персонажей, включая актора, как раз и определяет текущую точку сюжета. Инфопорции активно используются во всевозможных предусловиях в диалогах, влияют на условия торговли и т.п.

Имя инфопорции регистрируется заранее. Система такая:
В файле config\system.ltx есть секция [info_portions]. В этой секции единственный параметр files содержит перечень XML-файлов, содержащих описания инфопорций. Вы можете создать новый файл и вписать его в этот список, что более предпочтительно, или создать свою инфопорцию в одном из существующих файлов.


<?xml version='1.0' encoding="windows-1251"?>
<game_information_portions>
    <info_portion id="very_simple_infoportion"/>
    
    <info_portion id="simple_infoportion">
    </info_portion>
    
    <info_portion id="some_infoportion">
        <action>some_module.some_action1</action>
        <action>some_module.some_action1</action>
        <disable>infoportion_to_disable_1</disable>
        <disable>infoportion_to_disable_2</disable>
        <article>article_name1</article>
        <article>article_name2</article>
        <article_disable>article_name3</article_disable>
        <article_disable>article_name4</article_disable>
        <task>task_name_1</task>
        <task>task_name_2</task>
        <dialog>some_dialog_1</dialog>
        <dialog>some_dialog_2</dialog>
    </info_portion>
</game_information_portions>


Показаны три варианта регистрации инфопорций. Все инфопорции во всех файлах должны иметь уникальные имена.

в основном это методы класса game_object:

bool give_info_portion(string <имя инфопорции>);  // выдать порцию информации
bool disable_info_portion(string <имя инфопорции>);  // забрать порцию информации

bool has_info(string <имя инфопорции>);   // проверка на наличие инфопорции
bool dont_has_info(string <имя инфопорции>);  // проверка на отсутствие инфопорции
CTime* get_info_time(string <имя инфопорции>); // получить время выдачи инфопорции. В ЗП убрана


Есть также два метода у класса alife_simulator:

bool has_info(const alife_simulator*, id, string <имя инфопорции>);
bool dont_has_info(const alife_simulator*, id, string <имя инфопорции>);


Видно, что в оффлайне не получится установить/снять инфопорции, только узнать его значение.


В минимальном случае для регистрации инфопорции достаточно имени, как это сделано в примере для very_simple_infoportion и simple_infoportion.
В этом случае он почти подобен простой логической переменной, и часто именно так его и используют. Но, как видно из формата файла,
инфопорция при установке может также делать дополнительные действия:
1. выполнить заданную скриптовую функцию, заданную тегом action. Функция должна иметь вид
function action_name(who)
end

Где who - это клиентский объект того, кому был выдан инфопорция.
2. забрать другую инфопорцию, которая определяется тегом disable
3. активировать в PDA статью, заданную тегом article и деактивировать статью, заданную тегом article_disable
4. выдать задачу, заданную тегом task
5. добавить диалог, заданный тегом dialog
К сожалению нет возможности выполнить действие при снятии инфопорции.
Всех этих действий может быть произвольное количество. Естественно, не все действия имеют смысл для всех объектов, часть только для актора. Тут до конца не проверял, смотрите сами.


  • Нравится 1
  • Полезно 1
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

Info Portions System (сокращенно принято называть, infoportions, на русском языке кто как пишет...)

 

Система сюжетной информации (ССИ) нужна для обеспечения происхождения и запоминания сюжетных событий в игре.

 

При помощи ССИ можно создавать порции сюжетной информации (info portions).

 

 

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

 

К возможностям info_portion относится:

 

запуск функции по отношению к персонажу при получении info_portion (action)

добавления локаций на карту (location)

возможность инициирования диалогов актером (dialog)

возможность инициирования диалогов у актера (при разговоре с персонажем, который имеет info_portion) (actor_dialog)

добавления статей в энциклопедию (article)

добавление заданий и в меню заданий (task)

удаление из реестра уже известных info_portions (disable)

добавление каких-либо данных в секцию "данные"

 

И ещё не совсем уверен, give_info_portion_via_pda(info_id, who) - получить информацию по идентификатору от указанного объекта

 

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

 

Некоторые комментарии (malandrinus):

- Добавление локаций (тег location) и тег actor_dialog по всей видимости были в ранних билдах. В релизе их нет.

- вроде как можно косвенно инициировать простановку метки на карте через выдачу задания.

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

- Что такое "добавление в секцию 'данные' ", не совсем представляю. Возможно тоже что-то из старых билдов.

- Функция give_info_portion_via_pda опять же была в билде 1935. В релизе такой нет. Могу только предположить, что она имела отношение к полносимуляционной игре, когда сталкеры были равноправными игроками наравне с актором. Возможно, с ними можно было как-то общаться через PDA. Но сейчас это уже из области фантазий.

 

Вводные слова добавлю, спасибо.

 

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

ui_pda

ui_pda_hide - закрытия кпк

ui_pda_contacts - открытие окна контактов в кпк

ui_pda_contacts_hide - закрытие или переключения окна контактов

ui_pda_map_local

ui_pda_map_global - открытие вкладки глобальной карты

ui_pda_news - открытия вкладки новостей

ui_pda_news_hide - переключение с вкладки новостей

ui_pda_jobs_failed - окрытие вкладки проваленных задач

ui_pda_jobs_accomplished - открытие вкладки завершенных успешно задач

ui_pda_jobs_current - открытие вкладки текущих задач

ui_pda_jobs - открытие вкладки задач

ui_pda_jobs_hide - переключение с вкладки задач

ui_pda_diary - открытия вкладки дневника

ui_pda_diary_hide - закрытия=е или переключение вкладки дневника

ui_pda_encyclopedia - открытие вкладки энциклопедии

ui_pda_encyclopedia_hide - переключение с вкладки энцклопедии

ui_pda_actor_info

ui_pda_ranking - открытие вкладки рейтинга

ui_pda_events - открытие вкладки

ui_inventory открытие инвентаря

ui_inventory_hide - закрытие инвентаря

ui_talk - открытие диалога

ui_talk_hide - закрытие диалога

ui_trade - открытие торговли

ui_trade_hide - закрытие торговли

ui_car_body - открытие инвентаря трупа

ui_car_body_hide - закрытие инвенторя трупа

autojump_enabled

 

Назначение более-менее ясно из названия. Если прописать в эти инфопоршены тег action, то можно получить скриптовые колбеки на соответствующие события.

Изменено пользователем Unnamed Black Wolf
  • Нравится 1
Ссылка на комментарий

Не думал что ты будешь упоминать рабочие/системны инфопорции, я расписал что знал.

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

На ЗП не надо мучиться с action, достаточно открыть actor_menu и вписать функцию или вызов в нужном действие. У нпс есть аналогичный скрипт, но действие мной не проверялось. (Не замечено работоспособность с моей стороны.)

Изменено пользователем Unnamed Black Wolf
  • Нравится 1
Ссылка на комментарий

Продолжим. На очереди система профилей сталкеров, включая систему генерации имён.


Если коротко, то система профилей такая:


- В секции сталкера указан параметр character_profile. Если не указан, то принимается значение default.
- это имя указывает на профиль из файла, который прописан в system.ltx в разделе [profiles] в параметре files. Обычно это config\gameplay\npc_profile.xml
- По информации из этого профиля и по специальному алгоритму уже выбирается один из профилей, содержащих конкретную информацию о NPC. Все эти профили находятся в нескольких файлах, перечисленных в system.ltx в разделе [profiles] в параметре specific_characters_files.

Теперь подробнее. Вот пример файла с двумя вариантами создания character profile:

<?xml version='1.0' encoding="UTF-8"?>
<xml>
    <character id="profile_1">
        <specific_character>specific_character_profile_1</specific_character>
    </character>
    <character id="profile_2">
        <class>class_name</class>
        <rank>500</rank>
        <reputation>300</reputation>
    </character>
</xml>


Два этих варианта определяют два разных алгоритма выбора конкретного specific characters profile.
Первый вариант profile_1 содержит тег specific_character, который указывает на конкретный specific characters profile. В этом случае все остальные теги игнорируются. Этот вариант используется к примеру для актора. Его можно использовать для разных квестовых персонажей.
Второй вариант profile_2 используется для выбора одного из многих возможных профилей, используя параметры class, rank и reputation. Алгоритм выбора будет описан далее.

Пример файла с профилем персонажа:

<?xml version='1.0' encoding="windows-1251"?>
<xml>
    <specific_character id="specific_character_profile_name" no_random = "0" team_default = "1">
        <class>class_name_1</class>
        <class>class_name_2</class>
        <bio>...</bio>
        <npc_config>...</npc_config>
        <icon>...</icon>
        <name>...</name>
        <panic_threshold>...</panic_threshold>
        <hit_probability_factor>...</hit_probability_factor>
        <crouch_type>...</crouch_type>
        <critical_wound_weights>...</critical_wound_weights>
        <visual>...</visual>
        <supplies>...</supplies>
        <snd_config>...</snd_config>
        <terrain_sect>...</terrain_sect>
        <community>...</community>
        <rank>...</rank>
        <reputation>...</reputation>
        <money min="100" max="500" infinitive="0"/>
        <start_dialog>...</start_dialog>
        <actor_dialog>...</actor_dialog>
        <actor_dialog>...</actor_dialog>
    </specific_character>
</xml>


no_random исключает данный профиль из списка случайного выбора (см. далее описание алгоритма выбора)
team_default включает профиль в список дефолтовых при случайном выборе (см. далее описание алгоритма выбора)
no_random и team_default не могут быть установлены одновременно.

class - по этому параметру будет найден профиль. Этих тегов может быть несколько, т.е. разные character_profile с разными классами могут ссылаться на один и тот-же specific_character_profile.
name - человеческое имя персонажа. Алгоритм генерации описан далее.
icon - иконка персонажа в окне торговли
panic_threshold - некое пороговое значение для паники. Непонятно, влияет ли на что-то.
hit_probability_factor - фактор "меткости" сталкера. При нуле будет мазать стопроцентно.
crouch_type - тип приседания: 0 - обычное, 1 - низкое (у бандитов такое), -1 - случайный выбор
critical_wound_weights - какие-то весовые коэффициенты. Обычно выглядит так:

<critical_wound_weights>75,25,25</critical_wound_weights>

как-то управляет параметрами ранения, но как именно - не знаю.
visual - путь к модели
supplies - стартовый инвентарь. Примеры смотри в игре.
snd_config - имя папки, из которой берутся звуки
terrain_sect - ссылается на секцию в system.ltx. Пример такой секции для сталкеров.

[stalker_terrain]
    255,000,255,255
    008,001,255,255;Разрешено ходить по опасным местам на баре
    012,001,255,255;Разрешено ходить по опасным местам на Милитари

Что-то это всё означает, но что - точно не знаю.
community - группировка
rank - ранг
reputation - репутация
money - количество денег. Имеют значение только атрибуты min, max и infinitive. Значение тега задавать не надо. infinitive имеет смысл задавать для торговцев.
start_dialog - стартовый диалог. Это диалог, с которого всегда начинается общение. Первым в нём говорит собеседник. Стартовый диалог может быть только один. Если не задан, то устанавливается диалог с именем "hello_dialog".
actor_dialog - диалог актора. В этом диалоге первую фразу говорит актор. Этот дилог можно начать после того, как закончится стартовый дилог. Их может быть несколько.

читаются, но не используются
bio - биография, в игре нигде не отображается.
npc_config - понятия не имею, зачем нужен.


Обращаю внимание на то, что часть параметров дублируются здесь и в секции спавна. Что имеет приоритет, точно не знаю. Только насчёт визуала точно знаю, что берётся именно из профиля.

Теперь об алгоритме выбора профиля для варианта с тегом class.

Профилей specific_character с одним классом может быть несколько. Движок находит все профайлы, в которых присутствует тег class с нужным значением (напоминаю на всякий случай, что тегов class может быть несколько). Конкретный профиль случайным образом выбирается из всех найденных. При этом имена профилей, заданные атрибутом id не важны и могут быть совершенно любыми (но не могут повторяться). Профили с включённым флажком no_random исключаются из этого алгоритма. Теперь несколько непонятных моментов. Имеется также флажок team_default и два параметра в character_profile - rank и reputation. Похоже на то, что при выборе профиля из многих подходящих по классу можно ещё и фильтровать их по этим двум параметрам. Флажок team_default также каким-то образом влияет на процесс фильтрации. Но подробностей выяснить пока не удалось.

Часть из параметров, описанных в профиле, доступны со скриптовой стороны.


profile_name - имя профиля. К сожалению - это имя корневого профиля character_profile, а не specific_character. Если профиль был выбран случайно, то узнать, какой именно, практически невозможно.
character_name - человеческое имя, сгенерированное по описанному здесь алгоритму.
character_reputation - узнать репутацию
change_character_reputation - изменить репутацию
character_rank - получить ранг
set_character_rank - установить ранг
character_community - получить имя группировки
set_character_community - сменить группировку (там ещё можно менять команду и сквад)
money - узнать, сколько денег
transfer_money - передать деньги
give_money - дать денег
sound_prefix - расположение звуков (параметр, задаваемый тегом snd_config)
set_start_dialog - установить новый стартовый диалог
restore_default_start_dialog - вернуть исходный из профиля
get_start_dialog - по идее должна вернуть строку с именем диалога, но совершенно точно не работает и возвращает nil



1. Если имя может быть транслировано функцией game.translate_string(), то это и будет имя. Так можно задавать имена уникальным персонажам.
2. Если имя имеет вид "GENERATE_NAME_<suffix>", то имя составляется из имени и фамилии так:
- из system.ltx читается секция stalker_names_<suffix>, где должны быть два параметра name_cnt - количество вариантов имён, last_name_cnt - количество вариантов фамилий.
- далее составляется строка вида "name_<suffix>_N", где N - случайное число в пределах [0..name_cnt] (а может до name_cnt-1). Эта строка преобразуется в локализованное имя, как если бы обрабатывалась функцией game.translate_string() (см. описание этой функции в разделе "пространства имён"). Файл с именами обычно имеет имя stable_generate_fnames.xml, но может быть и другим. И сами имена можно размещать где угодно, см. опять же описание translate_string.
- аналогично получается фамилия. Составляется строка вида "lname_<suffix>_N" и получается локализованная фамилия по тому же алгоритму.
- результирующее имя составляется из имени и фамилии, раделённых пробелом.
3. Если не работает ни первый, ни второй вариант, то имя принимается прямо таким, как прописано в теге name.


  • Спасибо 1
  • Нравится 1
  • Полезно 1
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

[stalker_terrain]
    255,000,255,255
    008,001,255,255;Разрешено ходить по опасным местам на баре
    012,001,255,255;Разрешено ходить по опасным местам на Милитари

У каждого геймвертекса в гейм.графе есть поле type(по GraphViewer), а в stalker_terrain записано что-то вроде маски.

На баре у геймвертексов type бывает 08, 00, 00, 00 и 08, 01, 00, 00

 

Собственно расшифровка чисел в файле game_graphs.ltx

location_0 - первое число, location_1 - второе и так далее

 

В ЧН/ЗП добавили в типы гейм-вертексов еще и привязку к смарттерейнам - как я понимаю, чтобы через вражеские лагеря не ходили.

Также добавились функции: add_location_type, clear_location_types

При переходе из смарта в смартc помощью этих функций НПЦ добавляются новые stalker_terrain

 

 

Кстати, по поводу инфопорций в ЗП: в хмл прописывать их необязательно - можно выдавать абсолютно любую инфопорцию. И теги(типа action, disable) не работают

 

Дополню про money и infinitive, на форуме периодически проскакивают вопросы про это :)

<money min="100" max="500" infinitive="1"/>

Если поставить infinitive = "1", то денег у него все равно будет столько, сколько прописано в max. Просто при начале каждой торговли количество денег будет устанавливаться в max

Изменено пользователем Kolmogor
  • Полезно 1
Ссылка на комментарий

биография в ТЧ отображалась, и в ЗП кажется тоже. Это что написано про сталкера. Посмотри примеры на квестовых нпс.

Если не путаю ничего.

Кст, где нашел npc_config?

Чтение такого тега есть в той-же функции, где и чтение остальных тегов (CSpecificCharacter__load_shared). В IDA в отладочном билде это хорошо видно. Но в игре этот тег никак не задействован, возможно и не используется вовсе. Самое странное, что он читается и в ЗП и так же никак не используется. Вот глядя на это просто удивляюсь, убивать полезный функционал у GSC времени хватает, а вот этот явно никак не используемый мусор почистить - это видимо религия не позволяет.

(Malandrinus)

Изменено пользователем malandrinus
Ссылка на комментарий

Kolmogor,

Кстати, по поводу инфопорций в ЗП: в хмл прописывать их необязательно - можно выдавать абсолютно любую инфопорцию. И теги(типа action, disable) не работают

Да, в самом деле. С одной стороны ненужность предварительной регистрации вроде удобна. С другой стороны урезание тегов огорчает. И вообще, только было подумал: "а как же теперь статьи в энциклопедии выдавать?", а потом дошло, что ведь вовсе нет энциклопедии в ЗП. Вырезали с концами.

 

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

Внесу все правки позже, а пока продолжим основной цикл статей. На очереди диалоги в двух трёх частях.

 

Сначала терминология и базовые понятия.

Процесс общения актора и NPC будем называть беседой или разговором. НЕ диалогом, почему - дальше будет ясно.

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

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

Окончание разговора - это закрытие окна.

Нас же больше интересует разговор, как обмен фразами. С этой точки зрения разговор заключается в инициировании отдельных диалогов. Итак, диалог - это отдельный именованный набор фраз, объединённых в граф. Граф направленный, у него есть корневая вершина-фраза, которая является первой в диалоге. Кто какую фразу скажет определяется тем, кто начинает этот диалог. Далее по ходу собеседники просто чередуются. Правила выбора фраз из возможных ветвлений я опишу дальше. Диалог заканчивается, когда сказана последняя фраза в ветке, т.е. такая, у которой нет никаких следующих фраз. Заканчивается диалог, но не разговор!

Разговор может содержать несколько таких диалогов, которые делятся на две категории: стартовые диалоги (иначе называются приветственные) и диалоги актора (иначе называются темы для разговора).

Стартовый диалог может быть только один.

Если точнее, один можно задавать. Ещё один с именем "hello_dialog" прописывается движком. Если задан ваш диалог, то сработает он, если не задан или отключён предусловием, то сработает "hello_dialog". Если предусловием отключить и его, то будет попросу пустое окно, и никакого общения не начнётся.

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

Когда стартовый диалог заканчивается, появляется возможность начать один из диалогов актора, которые ещё иначе называют темами для разговора. Технически эти диалоги не отличаются от стартового, но отличаются в том что:

- первую фразу в них говорит актор (значит, меняется и очередность фраз)

- таких диалогов может быть несколько

- по окончании диалог можно повторить

Разговор можно закончить, только закрыв окно. Окончание очередного диалога не означает автоматически закрытие окна. Его можно закрыть вручную, кнопочками "Esc" и "F", а можно скриптовыми командами. Для второго варианта в оригинальной игре актору приписан специальный диалог "actor_break_dialog" с одной единственной фразой "До встречи!" и действием dialogs.break_dialog, которое скриптово остановит разговор.

Разговор заключается в открытии окна разговора. При этом NPC атоматом начинает говорить первую фразу из стартового диалога. По окончании стартового диалога актор выбирает любой из доступных диалогов актора, по его окончании опять может выбрать любой из диалогов актора и так до тех пор, пока не будет закрыто тем или иным способом окно разговора.

 

 

Теперь всё в подробностях.

 

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

<xml>
    <specific_character id="specific_character_profile_name" no_random = "0" team_default = "1">
        ...
        <start_dialog>start_dialog_name</start_dialog>
        <actor_dialog>actor_dlg_1</actor_dialog>
        <actor_dialog>actor_dlg_2</actor_dialog>
    </specific_character>
</xml>

Где start_dialog_name - имя стартового диалога, actor_dlg_1 и actor_dlg_2 - имена диалогов актора. Указанный здесь стартовый диалог можно впоследствии заменить скриптами, а набор диалогов актора можно также скриптами дополнить. Повторюсь, что стартовый диалог может быть только один. Прописать можно несколько, но работать будет только самый первый. Если не установлен никакой, то движок устанавливает диалог hello_dialog. Если заглянуть в него, то увидим всего две фразы: приветствие и просьба о помощи, если сталкер ранен.

Все диалоги должны быть заранее зарегистрированы в XML файлах. Все XML файлы с диалогами должны быть перечислены в файле system.ltx в разделе [dialogs] в параметре files.

 

<?xml version="1.0" encoding="windows-1251" ?>
<game_dialogs>
    <dialog id="dialog1" priority="123" caption="theme_title">
        <precondition>module_name1.prec_func1</precondition>
        <has_info>info_portion1</has_info>
        <dont_has_info>info_portion2</dont_has_info>
        <phrase_list>
            <phrase id="0">
                <text>Starting_phrase_text</text>
                <next>1_1</next>
                <next>1_2</next>
                <next>1_3</next>
            </phrase>
            <phrase id="1_1">
                <has_info>info_portion2</has_info>
                <text>Level_1_phrase_text_v1</text>
            </phrase>
            <phrase id="1_2">
                <dont_has_info>info_portion3</dont_has_info>
                <text>Level_1_phrase_text_v2</text>
                <next>2</next>
            </phrase>
            <phrase id="1_3">
                <precondition>module_name6.prec_func11</precondition>
                <text>Level_1_phrase_text_v3</text>
                <next>0</next>
            </phrase>
            <phrase id="2">
                <text>Level_2_phrase_text</text>
                <give_info>info_portion_5</give_info>
                <disable_info>info_portion_6</disable_info>
            </phrase>
        </phrase_list>
    </dialog>
    <dialog id="dialog2">
        <precondition>...</precondition>
        <has_info>...</has_info>
        <dont_has_info>...</dont_has_info>
        <init_func>module_name3.init_func_name</init_func>
    </dialog>
<game_dialogs>

Атрибуты тега dialog:

id - задаёт имя диалога, на которое ссылается тег start_dialog или actor_dialog в профиле персонажа.

priority - произвольное целое число. Имеет смысл только для диалогов актора. Диалоги сортируются по этому признаку в окне выбора.

caption - заголовок диалога. Имеет смысл только для диалога актора. Выбор одного из нескольких диалогов актора - это как бы выбор темы для разговора, а атрибут caption задаёт название этой темы. Это название и будет отображаться в списке диалогов, доступных для выбора. Если этот атрибут не задан, то заголовок устанавливается равным тексту первой фразы диалога, имеющей идентификатор "0".

 

Теги has_info, dont_has_info и precondition позволяют задавать условия доступности диалога по наличию/отсутствию инфопорции или скриптовой функцией. Функция должна иметь вид:

function dialog_prec(first_speaker, second_speaker, dlg_id)
    ...
    return true/false
end

здесь:

dlg_id - идентификатор диалога, для которого проверяется предусловие

first_speaker - тот, кто будет говорить первым. Если это стартовый диалог, то это NPC, если диалог актора, то актор.

second_speaker - соответственно, второй собеседник

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

 

Граф фраз можно инициализировать одним из двух способов - прямо в XML с помощью тега phrase_list (в примере выше - dialog1) или скриптовой функцией инициализации (в примере - dialog2).

Независимо от способа, которым был инициализирован конкретный диалог, это происходит один раз для этого диалога за всё время запуска игры. Вот к примеру, если один и тот-же диалог прописан двум разным персонажам, то он будет инициализирован при первом разговоре с тем персонажем, с которым актор заговорил первым. При разговоре со вторым персонажем будет уже использован тот-же самый диалог, с тем же графом фраз. Более того, если вы перезагрузили игру (но не вышли из программы), то диалог будет использован с прошлой игры и уже не будет заново инициализирован ни для кого из этих двух NPC.

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

О том, что можно, поговорим в самом конце.

 

Инициализация с помощью тега phrase_list показана в примере для dialog1. Граф диалога, который строится этим фрагментом, показан на рис.WKx4Vz4o71.png

 

Возможные атрибуты и теги для отдельной фразы:

Атрибут "id" - задаёт идентификатор фразы. Фраза с идентификатором "0" будет первой в диалоге. За исключением этой фразы идентификатор может быть произвольным текстом.

тег "next" - задаёт идентификатор фразы, которую можно сказать после текущей. Таких тегов может быть несколько, что задаст возможные ветвления. Актору даётся возможность выбрать фразу, а NPC выбирает фразу случайным образом. Выбирать что актор, что NPC могут только фразы, прошедшие проверку по предусловиям.

тег "text" - задаёт текст фразы. Как и большинство других текстовых элементов допускает трансляцию в локализованный текст (см. описание функции game.translate_string()). Фразе можно сопоставить звук. Для этого в каталог sounds\characters_voice\dialogs\ нужно поместить звук с именем, как у тега "text" и расширением "*.ogg". Но говорить будут только NPC. Актор по жизни немой =)

теги has_info, dont_has_info и precondition задают предусловия аналогично как для диалога в целом. Но функция для предусловия фразы должна иметь следующий вид:

function phrase_prec(first_speaker, second_speaker, dialog_id, prev_phrase_id, phrase_id)
    ...
    return true/false
end

здесь

phrase_id - идентификатор фразы, для которой запрашивается предусловие

first_speaker - кто говорит эту фразу,

second_speaker - соответственно, другой собеседник

dialog_id - имя диалога

prev_phrase_id - фраза, которая была перед текущей

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

тег action позволяет задать функцию, которая выполнится после того, как будет сказана фраза. Функция действия должна иметь вид:

function phrase_action(first_speaker, second_speaker, dialog_id, phrase_id)
    ...
end

здесь

first_speaker - кто говорит эту фразу,

second_speaker - соответственно, другой собеседник

dialog_id - имя текущего диалога

phrase_id - имя сказанной фразы

Теги "give_info" и "disable_info" позволяют включать и выключать указанные инфопорции.

Также имеется тег "goodwill", который не используется в оригинальной игре, но читается и даже имеет некий эффект. Это явно как-то связано с отношением собеседника к актору, но прояснить точное влияние до конца не удалось. Если атрибут ставить на фразы актора, то они сортируются по убыванию значения. Выходит как бы фразы с наибольшей доброжелательностью стоят первыми, а с наименьшей (самые недружелюбные) стоят последними. Если ставить атрибут на фразы непися, то эффект неоднозначный. Пока только удалось понять, что если хотя бы одно из значений будет меньше нуля, то будет вылет без лога. А если все больше, то никакого влияния не обнаружено - т.е. фразы выбираются случайно из доступного набора.

 

Новое в ЗП!!!

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

function get_text(first_speaker, dialog_id, phrase_id)
    ...
    return <строка для вывода>
end

При этом аргумент second_speaker равен nil, когда фразу произносит актор.

 

Также в ЗП во фразах встречается тег "is_final" со значением 1. Что это означает - непонятно. Никакого влияния на ход диалога я не обнаружил.

 

Относительно навигации по графу фраз. Начинается разговор всегда с фразы с идентификатором "0". Кто её скажет, определяется исключительно тем, прописан ли данный диалог стартовым или диалогом актора. Теоретически может быть одновременно и тем и другим. Кто скажет какую фразу после первой определяется простым чередованием. Диалог заканчивается как только сказана фраза, у которой не заданы дальнейшие разветвления. Если разветвления есть, но все оказались отключены предусловиями, то будет вылет.

No available phrase to say

Тег "next" может ссылаться на фразу, сказанную ранее, и таким образом в графе могут быть петли. При этом сохранение той-же очерёдности чередования фраз остаётся исключительно на совести автора диалога. Если, допустим, в примере выше я поставлю в фразу "1_3" ссылку не на "0", а на "1_1", то это создаст ситуацию неоднозначности: Фразу "1_1" скажет второй собеседник, если она следует за "0" и первый, если она следует за "1_3". Технически это будет возможно, хотя почти наверняка неправильно с точки зрения игровой логики (и скорее всего приведёт к косвенным ошибкам).

 

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

 

 

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

<!-- id - уникальный в пределах диалога номер фразы --> 
<phrase id="6"> 

<!-- текстовое представление фразы --> 

<text>Sorry, but I have to go.</text> 

<!-- или скриптовая функция которая возвращает строку текста --> 

<script_text>info_test1.test_text</script_text> 

<!-- минимальный уровень благосклонности персонажа, чтоб он смог сказать фразу --> 

<goodwill>60</goodwill> 

<!-- cкриптовые предикаты, если они все вернут true то фраза станет доступной --> 

<precondition>info_test1.test_action</precondition> 

<precondition>info_test1.test_action1</precondition> 

<!-- особые предикаты проверяют наличие/отсутствие порций информации у того кто говорит фразу --> 

<has_info>info_name1</has_info> 

<dont_has_info>info_name1</dont_has_info> 

<!-- cписок тех id фраз которые станут доступны собеседнику после того как будет сказана эта фраза --> 

<next>7</next> 

<next>5</next> 

<!-- cкриптовые функции, которые могут быть вызваны --> 

<action>info_test1.test_action</action> 

<action>info_test1.test_action1</action> 

<!-- особые функции, которые дают/убирают порции информации тому кто говорит фразу --> 

<give_info>info_name1</give_info> 

<disable_info>info_name1</disable_info> 

</phrase>

Все вызываемые скриптовые функции (как action так и precondition) задаются своим ПОЛНЫМ именем: NAMESPACE.FUNC_NAME, NAMESPACE - обычно просто имя файла скрипта с функцией.

 

Все скриптовые функции получают на вход 2 параметра: собеседник1 и собеседник2. Эти параметры представляют собой объекты персонажей ведущих диалог (актер, сталкеры, торговцы). Причем первым параметром идет тот, кто говорит фразу. (Я не ловил диалога ид, ты уверен в диалоге ида? )

 

pda="1"/"0" - аттрибут указывающий на то что диалого будет использоваться исключительно при связи по PDA (модеррам билдов. В оффе нету такого.)

priority="-1" - целое число (может быть отрицательным), чем меньше число тем ниже диалог появится при выборе из меню актера

 

Особой фразой является "пустышка" - фраза в которой отсутствует тег <text>. Фразы - пустышки, полезны, если мы хотим пропустить очередь говорить одного из собеседников. Также с помощью пустышек можно делать стартовые "развилки", то есть нужно сделать возможность начинать диалог не с одной, а со списка различных фраз (в таком случае делается 2 пустышки с id 0 и 1).

 

Кстати функция возврата строки в диалог, может быть совсем и перемененной.

 

P.S. Поправь, раскидай, покритикуй (но не сильно бей головой об стену). (Мог что-то повторить)

Изменено пользователем Unnamed Black Wolf
  • Полезно 1
Ссылка на комментарий

Все правки уже написанного сделаю позже, а пока...

 

Сейчас речь пойдёт о полностью скриптовом создании диалога. Эта возможность реализуется тегом

init_func (как показано в примере dialog2 выше).
Не смотря на то, что при таком способе создание диалога происходит во время выполнения, эту возможность
не получится использовать для создания диалогов с графом, изменяющимся в зависимости от собеседника и/или ситуации.

Теги init_func и phrase_list взаимоисключающие. Точнее, при их одновременном наличии тег init_func будет проигнорирован.

Функция, на которую ссылается этот тег должна иметь следующий вид:

function init_dlg(dlg)
    -- здесь создаём диалог
end

где dlg - это объект класса CPhraseDialog. Этот класс выглядит так:

class CPhraseDialog {
    CPhrase* AddPhrase(const char *text, string phrase_id, string prev_phrase_id, int goodwil_level);
};

Т.е. имеет всего один метод AddPhrase. С помощью этого метода можно добавлять в граф диалога новые фразы и связи между ними. Аргументы метода AddPhrase:

text - текст фразы, соответствует тегу text

phrase_id - идентификатор фразы, соответствует атрибуту id тега text.

prev_phrase_id - идентификатор фразы, после которой можно сказать данную. Для первой фразы с идентификатором "0", нужно указать пустую строку "".

goodwil_level - соответствует тегу goodwil

 

В принципе, этого метода достаточно, чтобы построить простейший диалог. Здесь требуется некоторое пояснение по поводу указания множественных связей между фразами. Если при задании графа через XML во фразе указывались все исходящие из неё фразы, то здесь мы видим иную картину - указывается фраза, для которой исходящей является создаваемая. Может возникнуть вопрос, как при этом задавать ситуацию, когда после нескольких фраз нужно сказать одну и ту-же? В этом случае работает такой принцип: нужно вызвать AddPhrase с одинаковым аргументом phrase_id столько раз, сколько требуется задать входящих связей для фразы с этим идентификатором. При этом создание фразы произойдёт при первом вызове, а при всех последующих будут только добавляться дополнительные входящие связи. При повторных вызовах аргументы text и goodwil_level будут игнорироваться.JpEJ3IYaUv.png

Вот функция для создания такого графа:

function init_dlg(dlg)
    dlg:AddPhrase("Starting_phrase_text",    "0", "", -10000) -- корневая фраза
    -- фраза "1_1" - первая альтернатива после фразы "0"
    dlg:AddPhrase("level_1_phrase_text_v1", "1_1", "0", -10000)
    -- фраза "1_2" - вторая альтернатива после фразы "0"
    dlg:AddPhrase("level_1_phrase_text_v2", "1_2", "0", -10000)
    -- фраза "2", идущая после "1_1"
    dlg:AddPhrase("level_2_phrase_text",    "2", "1_1", -10000)
    -- создание дополнительной входящей связи для фразы "2" из фразы "1_2"
    dlg:AddPhrase("",                       "2", "1_2", 0)
end

Функция AddPhrase возвращает объект типа CPhrase.

Описание класса CPhrase:

class CPhrase {
    CPhraseScript* GetPhraseScript();
};

Видно, что у этого класса есть всего один метод, который возвращает объект типа CPhraseScript. С помощью этого объекта можно настроить свойства свежесозданной фразы.

Описание класса CPhraseScript:

class CPhraseScript {
    void SetScriptText(string <имя функции для установки текста фразы>); // только ЧН и ЗП
    void AddPrecondition(string <имя функции предусловия>);
    void AddAction(string <имя функции действия>);
    void AddHasInfo(string <имя инфопорции>);
    void AddDontHasInfo(string <имя инфопорции>);
    void AddGiveInfo(string <имя инфопорции>);
    void AddDisableInfo(string <имя инфопорции>);
};

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

function init_dlg(dlg)
    ...
    local phrase = dlg:AddPhrase("phrase_text", "123", "23", -10000):GetPhraseScript()
    phrase:AddPrecondition("some_module.some_precondition")
    phrase:AddDisableInfo("some_infoportion")
    -- и т.д.
end

Может возникнуть вопрос, а зачем нужен такой способ создания диалогов, если это не даёт видимых преимуществ перед созданием с помощью XML? Ну, очевидно, без настоящей причины пользоваться этим способом на самом деле смысла нет. Однако в оригинальной игре скриптовое создание диалога используется. Если мы посмотрим, к примеру, диалог dm_hello_dialog (находится в config\gameplay\dialogs.xml), то увидим, что его инициализация происходит в функции dialog_manager.init_intro_dialog. Там, во-первых, используется трюк с пустыми фразами, и первые две фразы таким образом пропускаются. А вот на третьем уровне создаётся множество альтернативных фраз с предусловиями, которые заполняются из таблицы, которая в свою очередь заполняется из файла конфигурации config\misc\dialog_manager.ltx. Т.е. фактически, вместо создания диалога на основе XML реализовано создание на основе ltx. При этом появляются дополнительные возможности, поскольку таблица, на основе которой сделан диалог, осталась в нашем распоряжении (и она используется), и из файла ltx можно читать данные, а из XML нельзя и т.д. По такой же схеме сделаны диалоги торговцев для выдачи заданий.

 

 

По диалогам ещё не всё. Остались неохваченными много функций и методов, имеющих отношение к диалогам. Так что будет третья часть.

To be continued...

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий

[spoiler=Описание класса CTime]

C++ class CTime {
    -- используются в функции dateToString
    const DateToDay = 0;   // дата в виде "01/05/2012"
    const DateToMonth = 1; // дата в виде "05/2012"
    const DateToYear = 2;  // дата в виде "2012"
    -- используются в функции timeToString
    const TimeToHours = 0;    // время в виде "05"
    const TimeToMinutes = 1;  // время в виде "05:43"
    const TimeToSeconds = 2;  // время в виде "05:43:02"
    const TimeToMilisecs = 3; // время в виде "05:43:02:280"

    CTime(); // конструктор по умолчанию. Дефолтовые значения: y,m,d = 1; h,m,s,ms = 0
    CTime(CTime& t); // конструктор копирования. Создаёт копию аргумента

    string timeToString(int <способ преобразования>); // получение форматированной строки со временем
    string dateToString(int <способ преобразования>); // получение форматированной строки с датой
    void get(int &year, int &month, int &day, int &hour, int &minutes, int &seconds, int &ms); // получение всех значений, функция возвращает 7 значений
    void set(int year, int month, int day, int hour, int minutes, int seconds, int ms); // установка всех значений
    void setHMSms(int hour, int minutes, int seconds, int ms); // установка только часов/минут/секунд/миллисекунд
    void setHMS(int hour, int minutes, int seconds); // установка только часов/минут/секунд
    void add(CTime& what_to_add); // прибавить к текущему объекту время из аргумента, текущий объект изменится
    void sub(CTime& what_to_sub); // аналогично отнять
    CTime operator+(CTime &what_to_add); // прибавить и создать новый объект
    CTime operator-(CTime& what_to_sub); // отнять и создать новый объект
    float diffSec(CTime& t); -- разница между двумя временами в секундах
    bool operator==(CTime& what_to_compare); // сравнение на равенство с аргументом
    bool operator<(CTime& what_to_compare); // меньше
    bool operator<=(CTime& what_to_compare); // меньше или равно
    bool operator>(CTime& what_to_compare); // больше
    bool operator>=(CTime& what_to_compare); // больше или равно
};

 

Примеры и пояснения.

Класс CTime предоставляет возможности для хранения и расчётов времени. Внутри находится 64-х разрядный счётчик. Значение 0 этого счётчика соответствует параметрам конструктора по умолчанию. Экземпляры этого класса возвращает функция game.get_game_time() (получение текущего игрового времени). Однако можно создавать объекты класса CTime независимо. Для этого есть два конструктора. По неведомым причинам создатели игры поместили эти конструкторы в пространство имён game, так что вызывать их надо так:

local t1 = game.CTime() -- создаёт объект со значениями по умолчанию

local t2 = game.CTime(t1) -- создаёт копию t1

Использование timeToString и dateToString - вывод времени на печать:

function print_ctime(t)
    get_console():execute(t:timeToString(game.CTime.TimeToMilisecs).."_"..t:dateToString(game.CTime.DateToDay))
end

Использование get - другой вариант вывода на печать:

function print_ctime(t)
    local y,m,d,h,min,sec,ms = t:get()
    get_console():execute(string.format("[%d:%d:%d]_[%d:%d:%d:%d]", y,m,d,h,min,sec,ms))
end

Использование add:

local t = game.get_game_time() -- получить текущее время
local dt = game.CTime() -- объект с нулевым счётчиком
dt:setHMS(1,0,0) -- устанавливаем смещение в один час
t:add(dt) -- меняем t, увеличивая на один час

Аналогично с использованием оператора сложения:

local t = game.get_game_time()
local dt = game.CTime()
dt:setHMS(1,0,0)
local t2 = t + dt -- в результате создался новый объект, а t не изменился

Использование diffSec:

-- t2  и t имеют значения из предыдущего примера
local d = t2:diffSec(t) -- должно получиться значение 3600, т.е. один час в секундах

Использование операторов сравнения:

-- для тех-же t2  и t
local v = (t2 == t) -- получится false
local v = (t2 > t) -- получится true

 

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Ссылка на комментарий
ок, отвлечёмся немного от диалогов =)

Ладно... :) Мне-же этот класс в связи с диалогами и понадобился.

А даты-время, этот класс, обслуживает в пределах с 2000.1.1.0.0.0.0 по 2029.12.31.23.59.59.999

И функция sub() вроде криво работает, ещё раз перепроверюсь - отпишусь про неё.

А ещё - не нашёл способа вычислять даты прибавляя время, например: дата + дни (часы, ...).

Изменено пользователем 7.9

всё легко

Ссылка на комментарий

local dt = game.CTime()
local year, month, day, hour, minutes, seconds, ms = dt:get()
dt:set(year, month, day+1, hour+3, minutes, second, ms)
dt:add(game.get_game_time():get())

Не очень уверен, первое в голову пришло...

Ссылка на комментарий

Создайте аккаунт или авторизуйтесь, чтобы оставить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

Создать аккаунт

Зарегистрировать новый аккаунт в нашем сообществе. Это несложно!

Зарегистрировать новый аккаунт

Войти

Есть аккаунт? Войти.

Войти
  • Недавно просматривали   0 пользователей

    Ни один зарегистрированный пользователь не просматривает эту страницу.

AMK-Team.ru

×
×
  • Создать...