Everlasting Summer

Everlasting Summer

Not enough ratings
Перевод текста с различных языков с помощью клика
By alex6712
Создадим оператор перевода для вашего мода.
   
Award
Favorite
Favorited
Unfavorite
Введение
Зачем?

Этот вопрос мог возникнуть у многих, кто зашёл сюда просто увидев новое руководство по мододельству.
Те, кто ищет именно возможность устроить свой "переводчик" или хочет разобраться, как работает такой же в "Зелёном Свете", могут смело пролистать введение.

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

Мододелы придумали массу способов сделать это! Начиная от весьма простого, но не позволяющего видеть сразу оба варианта (с переводом / без перевода) способа в "Саманте", заканчивая просто написанием перевода рядом с оригинальным текстом, как в "7 Дней с Мику", или не использованием английского текста вообще, как в "Чистовике".

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

Часть 1. Creator-Defined Statements?
Creator-Defined Statements (далее - CDS)

Это словосочетание редко встречается в русскоязычных материалах на тему Ren'Py ( упоминания есть на сайтах anivisual[anivisual.net] и русского перевода документации Ren'Py[ru.renpypedia.shoutwiki.com]).

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

Все разработчики, садящиеся за создание мода для Бесконечного Лета (да и вообще проекта на движке Ren'Py), используют операторы. Например:
show bg black # вот уже определённый в самом движке оператор "show" hide bg # оператор "hide" play music music_list["two_glasses_of_melancholy"] fadein 3.0 # оператор "play music"
В последнем случае используется именно оператор "play music", а не "play". Объяснять разницу сейчас не буду.

И благодаря CDS мы можем создать своё кодовое слово или кодовое словосочетание, которое будет восприниматься движком Ren'Py, обрабатываться по тем правилам, которые мы ему зададим и исполняться по заданному нами алгоритму.

Конечно, всё это можно реализовать с помощью различного рода функций (или если Вы прям могёте, то и в ООП можно залезть), но хочу показать Вам пример того, насколько всё лаконично выглядит в коде при использовании CDS:
gl_translate mi "Bye-Bye!" "Пока-пока!"
"gl_translate" здесь - это определённый с помощью CDS оператор из мода "Зелёный Свет", для которого я его и написал и который постараюсь разобрать.
Часть 2. Начинаем погружение
Для нетерпеливых:

Создание

Весь код оператора должен находиться внутри блока python early!!!

Итак, каким образом создаётся собственный оператор?
Его создание происходит в четыре этапа:
  1. Создание парсера
  2. Создание валидатора
  3. Создание исполнителя
  4. Добавление оператора в список воспринимаемых движком

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

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

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

Парсинг

В движке Ren'Py есть класс Lexer (в переводе что-то вроде "лексиколог" от слова "лексика"), который предоставляет инструменты для парсинга кода. Инструментов много (вот их полный список[www.renpy.org]), но мы будем использовать лишь несколько, а именно:
  • Lexer.eol()
    Этот метод возвращает True, если мы находимся в конце строки и False, если иначе.
  • Lexer.simple_expression()
    Этот метод возвращает выражение на языке Python в виде строки (всё будет объяснено на примерах, когда мы дойдём до кода).
  • Lexer.match(re)
    Этот метод возвращает строку "re", если далее в коде следует она, или возвращает None, если далее в коде стоит что-либо другое.
  • Lexer.expect_eol()
    Этот метод вызовет ошибку, если мы не находимся в данный момент в конце строки.
  • Lexer.expect_block(statement)
    Этот метод вызовет ошибку, если у оператора не найден блок.
  • Lexer.expect_noblock(stmt)
    Этот метод вызовет ошибку, если у оператора найден блок.
  • Lexer.subblock_lexer()
    Этот метод возвратит новый объект Lexer, который будет использоваться для парсинга строк в блоке.
  • Lexer.advance()
    Этот метод используется для парсинга блока. Он переходит на следующую строку блока, возвращая True, а если текущая строка последняя, то он возвратит False.
  • Lexer.catch_error()
    Этот метод является неким декоратором. О его предназначении расскажу позже.
  • Lexer.string()
    Этот метод возвращает строку, если она следует в коде, и вызывает ошибку, если следует не строка.

Линтинг

Также в Ren'Py присутствуют инструменты для валидатора (вот они[www.renpy.org]). Нам нужны эти:
  • renpy.error(msg)
    Выводит ошибку с текстом "msg".
  • renpy.check_text_tags(s)
    Проверяет правильность написания текстовых тегов в строке "s". Возвращает текст ошибки, если ошибка есть или None, если ошибки не обнаружено.
Часть 3. Парсер
Начинать создание оператора мы будем с парсера, чтобы сразу обозначить тот синтаксис, который мы хотим видеть и использовать.

Для нетерпеливых
Код парсера, к которому мы придём:
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: lexer.expect_eol() lexer.expect_block("gl_translate") l = lexer.subblock_lexer() while l.advance(): with l.catch_error(): items.append(l.string()) l.expect_noblock("block inside gl_translate block") l.expect_eol() else: while not lexer.eol(): items.append(lexer.string()) return { "who" : who, "items" : items }

А теперь хардкор

Итак, начнём с того, что создадим функцию (название может быть любым, но принято называть её "parse_" + название вашего оператора, в моём случае: "parse_gl_translate"), принимающую один аргумент - объект класса Lexer.
def parse_gl_translate(lexer): pass

Для своего оператора я выбрал такой синтаксис:
gl_translate sl: # можно с блоком "Hello!" "Привет!" gl_translate sl "Bye!" "Пока!" # а можно без

Синтаксис:
  • Сразу после названия оператора следует персонаж, который произносит реплику.
    • Либо далее идёт символ ":" и две строки, каждая из которых на новой строчке
    • Либо обе строки идут подряд в одну линию с оператором.

Объект Lexer - это куча необработанных символов, следующих после оператора. Символы самого оператора ("gl_translate" в моём случае) в парсер не передаются, поэтому парсинг начинается со слова "sl" в примере выше.

Персонажи в Ren'Py - это объекты класса ADVCharacter, поэтому парсер воспринимает их, абсолютно логично, как выражения на языке программирования Python. Для парсинга Питоновских выражений используем simple_expression():

def parse_gl_translate(lexer): who = lexer.simple_expression() return { "who" : who }

simple_expression() возвращает Питоновское выражение в виде строки, поэтому позже нам придётся использовать функцию eval().
Пусть наш парсер будет возвращать словарь, а переменная, в которую мы запишем говорящего персонажа, пусть называется "who".

Далее добавим переменную, которая будет содержать в себе строки с текстом и переводом. Я назвал её "items":
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] return { "who" : who, "items" : items }
Логично, что и её мы тоже должны добавить в возвращаемый словарь.

Далее нам нужно понять, какой именно способ сейчас использует кодер. Пишет ли он всё в одну строчку или друг под другом? В выбранном мною синтаксисе (как и в синтаксисе Python) символ ":" обозначает начало блока, поэтому попросим парсер его найти, и если он его найдёт, мы поймём, что далее следует блок, а в ином случае всё записано в одну строчку. Для этого используем метод match(re), где "re" будет ":":
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: pass else: pass return { "who" : who, "items" : items }

Здесь стоит отметить, что методы класса Lexer при парсинге удаляют из списка необработанных символов то, что было ими найдено (пробелы и комментарии они игнорируют). Например, после применения simple_expression() к оператору, описанному в примере со "sl" без блока, "sl" будет удалена. Простыми словами: сначала в парсер передали строку 'sl "Bye!" "Пока!"', а после применения simple_expression() там осталось '"Bye!" "Пока!"'. То же самое происходит, когда мы ищем двоеточие: если мы его найдём, оно удалится, если мы его не найдём, строка, которую мы парсим, не изменится.

Давайте напишем алгоритм для парсинга той версии оператора, когда всё пишется в одну строчку.
Вспомним метод eol(), который возвращает False, пока мы не доберёмся до конца строки. Значит, мы можем использовать его, чтобы прекратить выполнение парсинга, как только доберёмся до конца строки:
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: pass else: while not lexer.eol(): pass return { "who" : who, "items" : items }
Ну а так как мы сейчас ищем строку с фразой персонажа и строку с её переводом, то будем использовать описанный во второй части руководства метод string():
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: pass else: while not lexer.eol(): items.append(lexer.string()) return { "who" : who, "items" : items }
Таким образом, метод string() будет добавлять в список "items" строку, которая стоит на пути парсера (вспомним, что теперь наша обрабатываемая строка выглядит как '"Bye!" "Пока!"' - после первого использования string() в "items" добавится "Bye!", а обрабатываемая строка сократится до '"Пока!"'), пока не доберётся до конца строки, и нас выкинет из цикла, и парсинг закончится.

В целом, это уже рабочая версия парсера, но давайте всё-таки напишем алгоритм обработки блока.

Так как после двоеточия блок начинается со следующей строчки, то мы обязаны быть в конце строки. Просим парсер это проверить, а также пусть проверит наличие самого блока (используются методы expect_eol() и expect_block(statement), назначение которых описано в предыдущей части):
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: lexer.expect_eol() lexer.expect_block("gl_translate") else: while not lexer.eol(): items.append(lexer.string()) return { "who" : who, "items" : items }

Далее нам нужно перейти от парсинга одной строки к парсингу блока. Используем для этого subblock_lexer() и запишем новый объект Lexer в переменную "l":
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: lexer.expect_eol() lexer.expect_block("gl_translate") l = lexer.subblock_lexer() else: while not lexer.eol(): items.append(lexer.string()) return { "who" : who, "items" : items }

А дальше, пользуясь advance(), идём по всем строчкам блока и парсим каждую.
def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: lexer.expect_eol() lexer.expect_block("gl_translate") l = lexer.subblock_lexer() while l.advance(): with l.catch_error(): items.append(l.string()) l.expect_noblock("block inside gl_translate block") l.expect_eol() else: while not lexer.eol(): items.append(lexer.string()) return { "who" : who, "items" : items }

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

expect_noblock(stmt) используется для того, чтобы предотвратить написание блока в блоке. Другими словами, в данном контексте он выдаст ошибку, если мы напишем
gl_translate mi: "What a wonderful day!" "Какой прекрасный день!"
Часть 4. Валидатор
Наш парсер готов. Теперь нам нужно проверить правильность введённых данных.

Парсер лишь проверяет, правильно ли всё оформлено: за словами "gl_translate" должен следовать персонаж, после - либо двоеточие, либо строка. И хотя парсер не позволит вместо строки написать число, проверить на правильность написания саму строку он не может. Для этого будем использовать валидатор.

Для нетерпеливых
Код валидатора, к которому мы придём:
def lint_gl_translate(o): who, items = o["who"], o["items"] try: eval(who) except: renpy.error("Character not defined: %s" % who) if len(items) != 2: renpy.error("A 'gl_translate' block expects 2 strings, you give %d." % len(items)) for i in items: tte = renpy.check_text_tags(i) if tte is not None: renpy.error(tte)

Валидатор

Главная наша проблема - количество строк. Обратите внимание на то, что мы в парсере всегда двигаемся "до конца". Когда мы обрабатываем блок, мы двигаемся до самой последней строчки (см. описание метода Lexer.advance() во второй части), а когда обрабатываем однострочный оператор - до конца строки. И дело в том, что в нашем парсере не установлено ни одной проверки на количество строк. Их должно быть две: фраза и её перевод. А может быть хоть сто. Поэтому нужно проверить длину списка "items", чтобы понять, сколько строк нашёл парсер.

Создадим функцию валидатора (опять же, принято называть "lint_" + название оператора, но это не принципиально) с одним аргументом:
def lint_gl_translate(o): pass

В валидатор, или линтер, передаётся значение, которое возвращается парсером. То есть тот словарь, который возвращает парсер после своей работы передаётся в валидатор в аргумент "o".

Поэтому давайте получим значения, которые парсер так активно искал:
def lint_gl_translate(o): who, items = o["who"], o["items"]

Теперь давайте проверим, а вообще существует персонаж, который записан в "who"? Вспомним, что метод simple_expression() возвращает строку, поэтому нам нужно её оценить с помощью Питоновской функции eval(). Если такого персонажа не существует, вызовем ошибку с текстом "Персонаж не определён " + имя персонажа:
def lint_gl_translate(o): who, items = o["who"], o["items"] try: eval(who) except: renpy.error("Character not defined: %s" % who)

Теперь проверим, сколько всё-таки строк в списке "items". Если их не две (больше, меньше - не важно: нам нужно именно две строки), выведем ошибку с текстом "Оператор 'gl_translate' ожидает две строки, а получает " + количество строк:
def lint_gl_translate(o): who, items = o["who"], o["items"] try: eval(who) except: renpy.error("Character not defined: %s" % who) if len(items) != 2: renpy.error("A 'gl_translate' block expects 2 strings, you give %d." % len(items))

Теперь нам нужно проверить сами строки. Что в них проверять? Ну, например, текстовые теги. Для этого воспользуемся функцией renpy.check_text_tags(s), описанной во второй части.
def lint_gl_translate(o): who, items = o["who"], o["items"] try: eval(who) except: renpy.error("Character not defined: %s" % who) if len(items) != 2: renpy.error("A 'gl_translate' block expects 2 strings, you give %d." % len(items)) for i in items: tte = renpy.check_text_tags(i) if tte is not None: renpy.error(tte)

И, в целом, всё. Больше проверять ничего не нужно. Валидатор готов.
Часть 5. Подготовка к исполнению
Все значения готовы к передаче в исполнитель, но вот визуальная часть пока ещё не работает. Нам нужно создать стиль, который будет изменять цвет при наведении мышкой на него и который будет выполнять действие при клике.

Для нетерпеливых
Готовый код для стиля ссылки в тексте:
init: # Стиль для текста "переводчика" # style gl_text_translate is normal_day: hover_color "#ffc219" selected_color "#ffdd7d" selected_idle_color "#ffdd7d" selected_hover_color "#ffc219" insensitive_color "#ffdd7d" hyperlink_functions ( None, gl_translate_text_clicked, None ) init python: # Функция действия для текста "переводчика" # def gl_translate_text_clicked(arg): who, what = arg.split(":", 1) renpy.call_in_new_context("gl_translate", who=who, what=what) # Лэйбл-транслейтер # label gl_translate(who, what): $ eval(who)(what, interact=True) return

Стиль для ссылки внутри текста

Итак, чтобы при наведении мышкой текст последний менял цвет, особо сложных манипуляций не нужно. Создадим новый стиль и возьмём за основу стиль "normal_day" - оригинальный стиль для реплик персонажей Бесконечного Лета днём:
style gl_text_translate is normal_day: hover_color "#ffc219" selected_color "#ffdd7d" selected_idle_color "#ffdd7d" selected_hover_color "#ffc219" insensitive_color "#ffdd7d"
Нас интересуют поля hover_color и selected_hover_color, а остальные, в целом, можно и не трогать (я в них записал оригинальный цвет текста просто, чтобы было). В интересующие нас поля записываем цвет, в который хотим чтобы окрашивался текст при наведении на него мыши.

Теперь нам нужно сделать так, чтобы на этот текст можно было кликнуть.
Для этого используем поле hyperlink_functions, котрое принимает кортеж из трех функций: первая - стайлер (функция, которая возвращает объект-стиль, который применится к тексту), вторая - функция-исполнитель (вызывается, когда на текст кликнули), третья - функция фокуса (вызывается, когда на текст навелись мышкой). Третья и первые функции нам ни к чему, поэтому на их место в кортеже можно поставить None.

Все три функции получают один аргумент - аргумент текстового тега {a}. Пример:
srting = "{a=аргумент}А я сяду в кабриолет{/a}"
Предположим, что к этому тексту применили стиль с готовой функцией-исполнителем. Тогда в аргумент исполнителя (и других функций при их наличии) передастся строка "аргумент".

Поле hyperlink_functions создано исключительно для работы с текстовым тегом ссылки {a}!

Итак, каким образом мы реализуем перевод?
Я выбрал такой способ:
При нажатии на текст Ren'Py вызовет в новом контексте лэйбл, в котором тот же самый персонаж скажет свою реплику второй раз, только это будет уже переведённая фраза. Для реализации этого нам нужно:
  • Знать, какой персонаж говорит
  • Знать перевод фразы
Саму фразу мы можем не знать. Она нам не пригодится.

Таким образом, в функцию-исполнитель мы должны передать в виде аргумента персонажа и перевод фразы. Учитывая, что мы получаем лишь один аргумент, будем использовать разделение по определённому символу. Пример:
srting = "{a=mi:Привет!}Hello!{/a}"
В функцию-исполнитель передастся строка "mi:Привет!". Нам нужно её обработать.
def gl_translate_text_clicked(arg): who, what = arg.split(":", 1)

Отлично! Теперь мы готовы показать игроку перевод. Для этого создадим лэйбл, который будем вызывать:
label gl_translate(who, what): $ eval(who)(what, interact=True) return
У этого лэйбла имеется два аргумента: персонаж, который произносит реплику и текст этой реплики. Благодаря тому, что мы будем вызывать этот лэйбл в новом контексте, оператор "return" вернёт нас в тот момент, когда была показана реплика без перевода.

Усовершенствуем функцию-исполнитель:
def gl_translate_text_clicked(arg): who, what = arg.split(":", 1) renpy.call_in_new_context("gl_translate", who=who, what=what)

Теперь всё готово. Переходим к исполнению оператора.
Часть 6. Исполнитель
Мы получили значения с помощью парсера и проверили их с помощью валидатора. Теперь нашему оператору осталось только исполнить своё предназначение.

Для нетерпеливых
Код исполнителя, к которому мы придём:
def execute_gl_translate(o): who, text, translation = o["who"], o["items"][0], o["items"][1] what = "{a=%s:%s}%s{/a}" % (who, translation, text) renpy.say(eval(who), what, what_style="gl_text_translate")

Исполнитель

Создаём функцию (принято "execute_" + название оператора, вы можете как хотите), принимающую один аргумент - тот же словарь, что принимает валидатор:
def execute_gl_translate(o): pass

Получим значения из словаря:
def execute_gl_translate(o): who, text, translation = o["who"], o["items"][0], o["items"][1]

Теперь нам нужно создать такую строку, чтобы она передавала в функцию-исполнитель из прошлой части персонажа и перевод:
def execute_gl_translate(o): who, text, translation = o["who"], o["items"][0], o["items"][1] what = "{a=%s:%s}%s{/a}" % (who, translation, text)
Отлично! А теперь пусть персонаж "who" скажет реплику "what", причём к тексту мы применим стиль для ссылок внутри текста, который мы написали в прошлой части (с помощью аргумента "what_style"):
def execute_gl_translate(o): who, text, translation = o["who"], o["items"][0], o["items"][1] what = "{a=%s:%s}%s{/a}" % (who, translation, text) renpy.say(eval(who), what, what_style="gl_text_translate")
Часть 7. Завершение
Вот и всё. Наш оператор готов. Осталось только добавить его в список операторов, которые воспринимает Ren'Py.

Это можно провернуть с помощью функции
renpy.register_statement(name, parse=None, lint=None, execute=None, predict=None, next=None, scry=None, block=False, init=False, translatable=False, execute_init=None, init_priority=0, label=None, warp=None, translation_strings=None, force_begin_rollback=False, post_execute=None, post_label=None, predict_all=True, predict_next=None)

Как видите, у неё очень много аргументов. Обо всех них модно почитать здесь[www.renpy.org], а нам понадобятся только эти:
  • name. В него передаём название нашего оператора
  • parse. В него передаём парсер
  • execute. В него передаём исполнитель
  • lint. В него передаём валидатор
  • block. Здесь нам нужно передать строку "possible", так как наш оператор може иметь блок, а может и не иметь его

renpy.register_statement("gl_translate", parse=parse_gl_translate, execute=execute_gl_translate, lint=lint_gl_translate, block="possible" )

Конец

С вами был alex6712, кодер мода "Зелёный Свет".
Код оператора целиком
python early hide: def parse_gl_translate(lexer): who = lexer.simple_expression() items = [] if lexer.match(":") is not None: lexer.expect_eol() lexer.expect_block("gl_translate") l = lexer.subblock_lexer() while l.advance(): with l.catch_error(): items.append(l.string()) l.expect_noblock("block inside gl_translate block") l.expect_eol() else: while not lexer.eol(): items.append(lexer.string()) return { "who" : who, "items" : items } def execute_gl_translate(o): who, text, translation = o["who"], o["items"][0], o["items"][1] what = "{a=%s:%s}%s{/a}" % (who, translation, text) renpy.say(eval(who), what, what_style="gl_text_translate") def lint_gl_translate(o): who, items = o["who"], o["items"] try: eval(who) except: renpy.error("Character not defined: %s" % who) if len(items) != 2: renpy.error("A 'gl_translate' block expects 2 strings, you give %d." % len(items)) for i in items: tte = renpy.check_text_tags(i) if tte is not None: renpy.error(tte) renpy.register_statement("gl_translate", parse=parse_gl_translate, execute=execute_gl_translate, lint=lint_gl_translate, block="possible" ) init: style gl_text_translate is normal_day: hover_color "#ffc219" selected_color "#ffdd7d" selected_idle_color "#ffdd7d" selected_hover_color "#ffc219" insensitive_color "#ffdd7d" hyperlink_functions ( None, gl_translate_text_clicked, None ) init python: def gl_translate_text_clicked(arg): who, what = arg.split(":", 1) renpy.call_in_new_context("gl_translate", who=who, what=what) label gl_translate(who, what): $ eval(who)(what, interact=True) return
4 Comments
Kuzm1tch 10 hours ago 
КУ! Я пишу мод про Алису, напишите кому не сложно в профиль: идеи, предложения или можете поддержать юного модера.
и я считаю - раз 23 Sep @ 10:17am 
Лайк автоматом
poi 21 Apr, 2021 @ 5:09am 
Спасибо за работу, Алекс. Прекрасное разъяснение кода, новичкам будет полезно.
Алиса Двачевская 17 Oct, 2020 @ 5:39pm 
Полезный пост, пожалуй выдам награду.