Telegram Web
Кажется я поехал кукухой

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

Вот почему при выборе нового места мне крайне важно было выйти именно в офис. Благо, в Швеции ⅔ населения уже вакцинированы и народ постепенно возвращается в офисы, так что с этим проблемы нет. Я также прикупил основную мебель в квартиру и разгрёб первичную бюрократию, так что у меня и в самом деле появилось время на свои проекты.

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

Вот только за последние 5 лет немножко изменились мои требования к движку мечты. Во-первых, IDE повсеместно теперь поддерживают Language Server Protocol, и писать скрипты без возможностей хотя бы EmmyLua нынче уже не комильфо, как говорится. Во-вторых, на старте Encased я сильно вложился в проектирование системы сериализации. Это многократно нам окупилось, а также по мере развития сильно повлияло на мои взгляды по теме. Теперь я хочу что-то подобное встроенное в скриптовый язык. Причём из коробки. Ясное дело, я не мог не пересмотреть немного концепцию Judy.

Меня всё ещё кидает из стороны в сторону, потому что я хочу слишком много всего и сразу, да и отказываться от Lua, которая уже подключена и работает, тоже жалко; но последние 2 дня я размышляю над своим языком. Пока только формулирую основные Design Goals, но одновременно прикидываю к носу, как конкретные фичи будут технически исполнены.

Писать свой скриптовый язык это особый уровень безумия. Даже по меркам движкописателей. Я прекрасно понимаю, что это адски объёмная задача и вряд ли вообще что-то путное получится, но похоже, что кукуха моя уже поехала и остановить будет сложно. Все нормальные названия для языка уже заняты, поэтому пусть пока будет рабочее — Ku-ku language. О том, чем я занимаюсь на работе всё равно больше рассказывать нельзя, так что ближайшее время блог будет в основном про Ku-ku.
Ku-ku design goals

Вот я и накатал Design Goals для моего скриптового языка мечты Ku-ku.

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

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

Но это будет чуть позже. А пока я очень жду ваших комментариев по тому, что есть. Заряжайте холивар по каждому пункту. Дайте мне жару, как следует!
Вы точно понимаете корутины?
#код
Много-много лет назад, когда я первый раз пришёл на настоящую работу программировать за настоящие деньги, меня научили корутинам. Ну, не то, чтобы прям научили. Просто там был проект с корутинами, и мне волей-неволей пришлось вникнуть, что это за yield такой непонятный. Новый мир, зазиявший передо мной, в корне перевернул моё тогдашнее представление о том, как можно писать геймплейный код. Я и до сих пор это воспринимаю, как одну из важнейших ментальных ступенек для программиста.

Речь сейчас идёт о тех диких временах, когда воевали с луками и стрелами против боевых машин самописными движками на плюсах. Я тогда успел немного походить из конторы в контору, но везде обнаруживал несчастных грустных людей, не вкусивших плодов корутин. Многие и слов таких не знали. И я нёс знания в простой народ. И жизнь людская преображалась на этих проектах.

Но эти времена давно прошли. Теперь каждый знает, что такое StartCoroutine() в Unity. Все к ним привыкли, и никого ими не удивишь; но я заметил, что самое крутое назначение корутин все ещё ускользает от многих в наше время.

Новички считают, что это просто удобный способ делать задержки. Каждый второй, делающий тестовое про самолётик в DarkCrystalGames, делал таймаут выстрелам через запуск корутины и WaitForSeconds(). Мой внутренний эстет при чтении такого кода всегда бьёт себя по лицу, но это вкусовщина. А проблема в том, что эти люди, как правило, только таким использованием и ограничиваются. Ничего более интересного, чем задержки, на корутинах и не пишут. Какое неуважение.

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

Нет, нет и ещё раз нет! Мультипоточка тут вообще ни при чём!

Недопонимание происходит из-за того, что к одной и той же вещи можно прийти с двух сторон. Можно с одной стороны постепенно облегчать мультизадачность и повышать безопасность и прийти от потоков к так называемым Green Threads. А можно вообще идти с другой стороны, пытаясь в однопоточном приложении улучшить читабельность колбеков и стейт-машин, и прийти внезапно к тому же самому. В этом случае результат скорее назовут корутинами. Разница между получившимися вещами с точки зрения функционала будет весьма условна, но отличается мотивация и назначение. Если вы познакомились с корутинами не с той стороны, то эта статья для вас.
Идём со стороны многопоточки

Для начала позанудствуем (если не осилите, пропускайте этот раздел) и вспомним, что многозадачность у нас бывает вытесняющей и кооперативной. Вытесняющая — это обычные потоки. У нас есть несколько физических процессоров и несколько потоков выполнения. В каждый момент времени один поток выполняется на одном процессоре, каждый поток имеет свой собственный стек, а операционная система часто-часто переключает потоки на конкретном процессоре (подменяя стек и регистры процессора). В общем, всё как мы любим: никаких сюрпризов.
Сразу обозначим, что всякие фреймворки на основе Job’ов или Promise’ов, если они приводят к созданию новых настоящих тредов, являются обёрткой над этим же типом многозадачности.

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

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

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

Это неплохой бонус в сравнении с потоками, но на деле это не так уж и легковесно. При стандартном размере стека в 1 MB, запуск 1024 фиберов приведёт к выделению, как минимум, 1 GB памяти. Это нормально для Green Thread, но совершенно недопустимо для корутин. Для концепции корутин не должно быть проблемой и десять тысяч.

Очевидное решение — это делать динамические стеки, расширяющиеся по требованию. Так делают, например, горутины. И это уже чертовски близко к stackful-корутинам (например тем, что есть в boost). Но разница в мотивации. Горутины всё равно почти всегда используются в контексте многопоточности. Как правило, запускается несколько полноценных потоков по числу ядер процессора, а уже поверх них сотни лёгковесных горутин. Все эти свистопляски были для того, чтобы облегчить накладные ресурсы в изначально многопоточном приложении. Замасштабировав их с небольшого числа рутин до сотен и тысяч без изменения кода.

Фиберы и зелёные треды тоже применяются обычно в тех контекстах, когда речь идёт, так или иначе, о многопоточных приложениях. Будь то портирование с другой платформы или нечто похожее. А корутины это вообще не про это.
Идём со стороны стейт-машин

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

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

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

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

проснуться();
расчистить_вход_в_норку_от_песка();
пока ещё_не_перегрелись_на_солнце {
выбрать_точку_куда_идти();
пока не_дошли {
сделать_шаг_по_маршруту()
если встретили_пищу, то
есть();
если встретили_врага, то
сражаться();
}
}
идти_в_тенёк().

Согласитесь, намного читабельнее и удобнее, чем писать штук 10 классов под это дело. Единственная проблемка в том, что каждая операция здесь потенциально может выполняться больше одного игрового тика (то есть длиться дольше одного кадра; например, из-за анимации), а потому простой функцией здесь не обойтись. Как вы уже догадались, нам просто нужна возможность писать функции с несколькими точками входа. Или, проще говоря, возможность нашпиговать этот код yield()-ами через строчку:

проснуться();
yield расчистить_вход_в_норку_от_песка();
пока ещё_не_перегрелись_на_солнце {
выбрать_точку_куда_идти();
пока не_дошли {
yield сделать_шаг_по_маршруту()
если встретили_пищу, то
yield есть();
если встретили_врага, то
yield сражаться();
}
}
yield идти_в_тенёк().

Такие фаршированные функции называют генераторами. Они могут быть знакомы многим по C#. И по большому счёту это и есть машина состояний, только записанная более удобным синтаксисом; поскольку внутренне это всё разбивается всё равно на несколько маленьких функций от одного yield до другого, а общие переменные выносятся в небольшую структуру, которая передаётся дальше (на манер того, как это происходит с замыканиями). Каждая такая сгенерированная структурка и соответствующая ей функция по сути и образуют наши олдскульные классы состояний конечного автомата.
Но, по правде говоря, прямо функции и классы в терминах конкретного языка виртуальная машина вам генерить не обязана, потому что они наружу не видны, но по смыслу там ровно это происходит. Но вот эта простая смена формы записи и есть та самая ментальная ступенька, о которой я говорил в начале, так как она сильнейшим образом развязывает руки и мозги геймплейному программисту в плане стейт-машин.

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

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

У нас всё ещё не полноценные корутины, а симулякр. Если мы захотим сделать yield где-то далеко на глубине стека вызовов в C#, нам хоть и не придётся теперь крутить цикл в каждом месте, но протаскивать yield всё же придётся через все уровни. Если бы корутины были реализованы на уровне языка, как, например, в Kotlin, то мы могли бы ещё немного улучшить жизнь нашим сурикатам, прокидывая yield с любой глубины. Это намного удобнее. Но не идеально, потому что в Kotlin все промежуточные функции придётся пометить как suspend. Тогда компилятор будет обращаться с этими функциями, как с генераторами (для их промежуточного состояния будет создаваться объект), но эти функции тогда будет нельзя вызывать из non-suspend функций. То есть помечая функцию как suspend, мы рискуем, что часть старого кода отвалится.

— А бывают ли вообще идеальные корутины? — спросите вы. — Чтобы вызываться без боли из любого места и не думать о том, какая там была функция.

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

Я встречал stack-full корутины как минимум 2 раза. Этол boost-овые в С++. Но сами понимаете: С++ сам по себе такой, что «без боли» к нему слабоприменимое понятие. И, конечно же, в Lua. Это мои первые и самые любимые корутины, потому что корутины там, как говорится, first-class-citizen: они yield-ятся в любой момент и с любого уровня, не требуют менять существующий код и никак не мешают вызывать эти же функции вне корутин — одно удовольствие. Идеально для написания толп сурикат.

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

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

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

P.S. Но, кстати говоря, в моём будущем языке KuKu, одной из целей я себе ставлю сделать легко сериализуемые корутины прямо из коробки. Далеко не факт, что получится, конечно. Но будем пытаться.
Угадай мелодию из видеоигр

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

Игре уже немножко плохо: что-то не то с кодировкой в некоторых браузерах и после примерно сотни вопросов счётчик проглючивает. Я всё думал обновить, починив и добавив современных игр, но что-то руки так и не дошли. Подумываю вообще в ближайшее время грохнуть этот сайт и переехать на полностью статичный github.io.

Поэтому предлагаю поиграть, пока ещё работает. Оно прикольное. Кстати, по статистике чаще всего отгадывают саундтрек Super Mario Bros. и NFS:Underground.

http://alprog.net/namegametune/
Комментарии по ку-ку
#kuku
Пришло время немного рассказать об успехах по разработке моего скриптового языка.

Я не ищу лёгких путей и мне нафиг не сдался язык без поддержки в IDE рефакторинга и дебагера, поэтому kuku изначально пишется с поддержкой Language Server Protocol. Как минимум я хочу поддержку куку в VS Code, и, скорее всего, в Vim или Neovim. Ну а потенциально во всех более-менее толковых IDE.

Не знаю, удастся ли запихать language server в браузер, но вы можете рассчитывать, что playground языка будет доступен онлайн по крайней мере с простой подсветкой синтаксиса и возможностью запустить код (хвала emscripten’у!), а также посмотреть его байт-код.

Ну так вот: по задумке kuku.exe должно будет уметь выполнять одновременно функции и парсера-компилятора, и language-сервера, и interactive mode, и декомпилятора с дебагером и всего на свете, в общем-то. И чтобы не делать одну и ту же работу дважды, парсер пишется сразу с оглядкой на LSP. В частности, например, это означает, что внутренне документы всегда представлены массивом строк, а строки соответственно последовательностью UTF-16 code unit’ов без символов конца строки. Таким образом всё позиционирование (даже у лексера) и обновление документов происходит только в этих координатах.

Я, конечно, полистал немного книгу дракона и “crafting interpretors”, но больше всего мне понравилась фраза из последней: “parsing doesn’t matter”. Действительно, можно долго зарубаться на тему математической красоты LL(1) и LR и прочих аббревиатур, но они лишь усложняют жизнь. Куку достаточно простой язык, чтобы осилить его разбор рекурсивным спуском. И даже при разборе выражений я собираюсь вручную резолвить приоритет операторов вместо того, чтобы заморачиваться с грамматиками. Чем ближе структуры AST будут отражать концепты языка, тем лучше в моём случае. В конце концов, мне ещё семантический разбор и рефакторинг поверх этого писать. А вот где действительно имеет смысл угореть по Computer Science, так это в генерации кода и оптимизациях. Но до этого пока далеко.

Ещё, кстати, появились мысли о том, как конкретно я хочу имплементировать пункт 4.2 моих design goals. Речь идёт о транзакционной памяти, но до этого совсем-совсем далеко.

Ну а пока зацените как сделаны комментарии в куку, например: https://alprog.github.io/kuku/comments.html
Похороны Judy

Здесь уже было несколько радостных постов о моём новом pet-project’е — скриптовом языке kuku. С ним всё хорошо и я действительно в кои-то веки регулярно пишу что-то для себя (хвала возможности разделять офис и дом), но я совсем забыл сказать хотя бы пару слов о моём прошлом питомце. А надо бы.

Идея писать kuku мне не случайно стукнула в голову, а родилась в попытке воскресить мой движок Judy и переосмыслить его с учётом опыта, который я получил в процессе написания Encased. И тот факт, что я занялся переписыванием скриптового языка — по сути центрального элемента старой концепции движка — фактически означает, что проект Judy закрыт. Во всяком случае в старом его виде.

На самом деле Джуди перестала подавать признаки жизни ещё в 2016, но я до самого последнего времени отказывался в это верить. На картинке выше вы можете также видеть гробик её предшественника — Holly. Почему-то всегда хотелось назвать движок женским именем. Не уверен, продолжится ли эта традиция, потому что не так много красивых имён, начинающихся на “ku”.

Но что можно сказать уверенно, так это то, что правопреемница больше не будет включать в себя самодельную IDE. В движке моей мечты теперь эту роль выполняет VS Code и Language Server. Я также вряд ли снова буду писать инициализацию окошек (для этого есть SDL) и скорее всего использую какую-нибудь библиотеку для рендера (на данный момент Diligent Engine выглядит очень секси). В общем, в следующий раз значительно уменьшу уровень велосипедирования. Ну, кроме скриптового языка. Тут я ку-ку, конечно.
(продолжение)

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

Во-первых, @zenkovich_an в одиночку пилит O2 Engine. Движок, правда, как ясно из названия, исключительно 2D, но зато сделано уж точно гораздо больше, чем было у меня :)

Ну и во-вторых я всегда говорил, что идеологически Defold довольно близок к тому, что я пытался сделать. Над Defold’ом работает, конечно, целая команда, но есть среди его разработчиков и мои кореша с личными каналами. Так что подписывайтесь на бложек АГулева, также некогда известного, как евангелиста Defold всея Руси.

Но и от меня не отписывайтесь: про язык будет интересно.
Почему Швеция и Paradox — лучшее место для меня

Ох, до чего же эмоциональные у меня вышли последние 4 месяца. За это время меня по разным причинам трижды к ряду выбило из колеи. Причём война даже не самый сильный удар в этой череде. Порой даже на работе было трудно фокусироваться, не говоря уже о каких-то своих pet’ах или блоге. Но пора бы уже возвращаться в нормальный режим. И начать хочется с какого-нибудь по-летнему лёгкого поста.

Проще всего рассказать о том, почему я работаю на текущем месте. Дело в том, что я не эмигрировал куда попало, а устроился ровно туда, куда и хотел — в идеально подходящую мне компанию, в максимально подходящей стране, да ещё и занимаюсь я там самым лучшим проектом студии. За 9 месяцев работы здесь я ни капли не разочаровался, так что давайте подробно расскажу, что же такого я тут нашёл.
Почему валить?

Чёткое осознание, что я буду эмигрировать, у меня появилось ещё далёким летом 2018-го, когда я выпивал с друзьями в моём любимом питерском баре «‎Let it Bar». Как и многие другие подобные штуки в моей жизни, это был щелчок. Внезапный щелчок, завершивший какие-то скрытые процессы в голове, после которого мне было абсолютно понятно, что решение уже принято. Было понятно, что это больше не праздные рассуждения «‎когда-нибудь может быть», а исключительно дело времени. Я занялся изучением вопроса всерьез, и уже примерно на следующий день понимал, куда именно хочу.

Предпосылок к переезду была тьма. Прежде всего это ощущение, что я уже «прошёл» Россию. Серьёзно: действительно крутых и интересных проектов для меня в РФ можно было пересчитать по пальцам одной руки и я уже работал лидом в одном из них. Куда дальше? На тот момент я был лидом Encased всего полгода и уходить, конечно, не собирался пока не доведу проект до какой-то серьёзной точки (спойлер: получилось довести до самого релиза). Но было чёткое понимание, что после Encased более интересную позицию в стране найти будет трудно. А так, чтобы при этом ещё и не на грёбаной Unity — так и подавно нереально.

Добавьте к этому ощущение остановки развития. Как в личном плане (причём я уже дважды на тот момент переезжал внутри России и это каждый раз действовало на меня крайне ободряюще и вытаскивало из личного застоя, так что повторить этот трюк казалось хорошей идеей); так и в профессиональном плане. Это по российским меркам я уже был спец выше гор, которого любая студия оторвёт со всеми конечностями, но по мировым стандартам — такое себе. Объективно. Ходить с надписью «CTO» на погонах, конечно, очень прикольно и полезно для ЧСВ, но умнее ты от этого не становишься.

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

Ну и само собой отлично подгонял политико-экономический фактор. Очередной раунд оппозиционной борьбы был проигран, только начинался 4-ый срок Путина и конца и края этому видно не было. К тому же на митинги выходить становилось всё более и более стрёмно и не хотелось обнаружить себя в какой-то момент либо за решёткой, либо зассавшим выходить. Ну и разумеется не хотелось обнаружить себя в старости живущего на российскую пенсию (да ещё и после преимущественно чёрных зарплат ру-геймдева).
2025/07/08 17:56:17
Back to Top
HTML Embed Code: