diff --git a/.gitignore b/.gitignore index a3f1b5409b..6109096010 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ subprojects/fmt subprojects/gtest subprojects/sol2 subprojects/luajit +subprojects/emmyluacodestyle subprojects/yaml-cpp subprojects/zlib /1/ diff --git a/README.md b/README.md index c5ee5af405..56dec6e469 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ * [Windows (MSVC / Clang)](#windows-msvc--clang) * [Windows (MinGW / MSYS2)](#windows-mingw--msys2) * [Cross-компиляция под Windows с Linux (MinGW)](#cross-компиляция-под-windows-с-linux-mingw) + * [LuaJIT и форматирование Lua](#luajit-и-форматирование-lua) * [Unity-сборка](#unity-сборка) * [Пересборка](#пересборка) * [Тесты](#тесты) @@ -140,6 +141,40 @@ meson setup build --cross-file toolchains/windows-mingw64-cross.txt -Dbuild_prof meson compile -C build ``` +### LuaJIT и форматирование Lua + +Для сборки движка с Lua-триггерами и встроенным форматтером EmmyLuaCodeStyle используйте встроенные Meson-зависимости: + +```bash +meson setup build_lua \ + -Dbuild_profile=release \ + -Dbuild_tests=false \ + -Dluajit=builtin \ + -Dlua_formatter=true +meson compile -C build_lua +``` + +`-Dluajit=builtin` включает LuaJIT и интеграцию через sol2. При включенном LuaJIT опция `lua_formatter` по умолчанию равна `true`; она указана в примере явно, чтобы конфигурация была очевидна. Meson использует subprojects из репозитория или загружает недостающие исходники через wrap-файлы. + +Если LuaJIT установлен в системе и доступен через `pkg-config`, вместо встроенной версии можно использовать: + +```bash +meson setup build_lua \ + -Dbuild_profile=release \ + -Dbuild_tests=false \ + -Dluajit=system \ + -Dlua_formatter=true +``` + +Чтобы оставить Lua-триггеры, но отключить форматтер: + +```bash +meson configure build_lua -Dlua_formatter=false +meson compile -C build_lua +``` + +В итоговой сводке конфигурации Meson строки `LuaJIT` и `Lua formatter` показывают фактически выбранные режимы. Форматтер доступен в OLC только в сборке с LuaJIT. + --- ## Unity-сборка @@ -257,8 +292,10 @@ docker stop mud | `nocrypt` | boolean | `false` | Отключить использование `crypt()` | | `with_asan` | boolean | `false` | Address Sanitizer | | `use_pch` | boolean | `true` | Предкомпилированные заголовки | +| `lua_formatter` | boolean | `true` | Встроенный EmmyLuaCodeStyle; используется только при включенном LuaJIT | | `linker` | string | `` (системный) | Линковщик: `gold`, `mold`, `lld`, `bfd` | | `full_world_path` | string | `` | Абсолютный путь к данным мира для создания симлинков | +| `sol2_root` | string | `` | Необязательный путь к заголовкам sol2; пустое значение использует Meson dependency/fallback | ### Зависимости @@ -276,6 +313,7 @@ meson setup build -Ddependency1=builtin -Ddependency2=system -Ddependency3=disab | `zlib` | `auto` | Поддержка MCCP через ZLib | | `iconv` | `disabled` | Поддержка iconv | | `telegram` | `disabled` | Telegram-интеграция (требует CURL + OpenSSL) | +| `luajit` | `disabled` | Lua-триггеры через LuaJIT и sol2; `builtin` собирает встроенные зависимости | | `boost` | `disabled` | Boost (нужен для scripting) | | `sqlite` | `disabled` | SQLite как источник данных мира | | `yaml` | `disabled` | YAML как источник данных мира | diff --git a/docs/LUA_SCRIPTS_FOR_BUILDERS_RU.md b/docs/LUA_SCRIPTS_FOR_BUILDERS_RU.md index 65068b2644..84ddfad3f0 100644 --- a/docs/LUA_SCRIPTS_FOR_BUILDERS_RU.md +++ b/docs/LUA_SCRIPTS_FOR_BUILDERS_RU.md @@ -2,7 +2,7 @@ Этот документ описывает Lua-триггеры, которые можно использовать вместо DG Script в world-файлах триггеров. -Lua-поддержка сейчас является прототипом и работает только в сборке с `-Dluajit_prototype=true`. Обычная сборка без LuaJIT должна продолжать читать DG-триггеры как раньше. +Lua-поддержка сейчас является прототипом и работает только в сборке с включенной опцией `luajit`, например `-Dluajit=builtin` или `-Dluajit=system`. Обычная сборка без LuaJIT должна продолжать читать DG-триггеры как раньше. **Безопасность:** глобалы `os`, `io`, `debug`, `package` и `require` намеренно отключены. Для реального календарного времени используйте `mud.date("%j")`, `mud.date("*t")`, `mud.date("exact")` и т.п. (не os.date). @@ -40,6 +40,8 @@ script: | Lua-триггер можно писать как обычное тело функции: движок выполняет такой chunk с доступным `ctx` - контекстом владельца триггера, актера, объекта и других данных события. Старый явный формат `return function(ctx) ... end` продолжает работать. +В OLC-меню Lua-триггера доступен пункт `Проверить синтаксис и отформатировать Lua`. Он форматирует текущий текст встроенной библиотекой EmmyLuaCodeStyle с отступом в 2 пробела. При выходе из OLC с сохранением Lua-код проверяется и форматируется автоматически. Если проверка или форматирование завершается ошибкой, OLC показывает сообщение об ошибке, сохраняет исходную версию кода без форматирования и закрывает редактор. Форматтер работает в отдельном рабочем потоке, поэтому игровой цикл не останавливается; запросы от разных редакторов выполняются последовательно в порядке очереди. Во время автоматического форматирования команда `Q` отменяет форматирование, сохраняет исходный код и закрывает редактор. При разрыве соединения после подтверждения сохранения исходный код также сохраняется. При ручном запуске форматирования из меню ожидание можно отменить командой `Q` без сохранения. В сборке без LuaJIT пункт не показывается; встроенный форматтер можно отключить опцией Meson `-Dlua_formatter=false`. + Номер строки в ошибке Lua соответствует номеру в `tstat -n ` и в нумерованном выводе `/n` текстового редактора OLC. Пустые строки учитываются. Пока изменения в OLC не сохранены, runtime-сообщения относятся к предыдущей сохраненной версии триггера. Номер строки в диагностике DG и Lua имеет разный смысл. Для DG строки команд нумерует сам движок. Для Lua номер сообщает LuaJIT: это строка, на которой парсер или выполняемый код обнаружил ошибку. Первопричина синтаксической ошибки может находиться раньше. Например, если на строке 7 написано незаконченное выражение `asdasd`, а на строке 8 начинается `mud.wait(1)`, Lua может сообщить об ошибке на строке 8: только встретив `mud`, парсер понял, что после `asdasd` ожидался оператор присваивания. Поэтому при синтаксической ошибке проверяйте не только указанную строку, но и несколько строк перед ней, включая незакрытые скобки, строки и блоки. diff --git a/meson.build b/meson.build index 4d3e1af952..310311ae7d 100644 --- a/meson.build +++ b/meson.build @@ -212,6 +212,7 @@ threads_dep = dependency('threads') dependencies += threads_dep luajit_opt = get_option('luajit') +lua_formatter_enabled = false if luajit_opt != 'disabled' if luajit_opt == 'builtin' luajit_sub = subproject('luajit', default_options: ['default_library=static', 'luajit=false']) @@ -238,6 +239,16 @@ if luajit_opt != 'disabled' ) project_args += '-DWITH_LUAJIT_PROTOTYPE' dependencies += [luajit_dep, sol2_dep] + if get_option('lua_formatter') + emmy_dep = dependency( + 'emmyluacodestyle', + fallback: ['emmyluacodestyle', 'emmyluacodestyle_dep'], + default_options: ['warning_level=0'], + ) + dependencies += emmy_dep + project_args += '-DWITH_LUA_FORMATTER' + lua_formatter_enabled = true + endif endif # Admin API @@ -459,6 +470,7 @@ main_sources = files( 'src/engine/scripting/dg_triggers.cpp', 'src/engine/scripting/dg_wldcmd.cpp', 'src/engine/scripting/lua/lua_entity_bindings.cpp', + 'src/engine/scripting/lua/lua_formatter.cpp', 'src/engine/scripting/lua/lua_mud_api.cpp', 'src/engine/scripting/lua/lua_script_engine.cpp', 'src/engine/scripting/lua/lua_trigger_context.cpp', @@ -1071,6 +1083,7 @@ summary({ 'fmt': fmt_opt, 'gtest': gtest_opt, 'LuaJIT': luajit_opt, + 'Lua formatter': lua_formatter_enabled, 'SQLite': sqlite_opt, 'YAML': yaml_opt, 'Telegram': telegram_opt, diff --git a/meson_options.txt b/meson_options.txt index 7a750afc23..bf8598bb5d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,6 +6,7 @@ option('nocrypt', type: 'boolean', value: false, description: 'Disable use of cr option('has_epoll', type: 'boolean', value: true, description: 'Use epoll (Linux or freebsd)') option('scripting', type: 'boolean', value: false, description: 'Enable scripting support (Python + Boost.Python)') option('sol2_root', type: 'string', value: '', description: 'Optional path to sol2 root for luajit_prototype; empty uses Meson dependency/fallback') +option('lua_formatter', type: 'boolean', value: true, description: 'Build the embedded EmmyLuaCodeStyle formatter when LuaJIT is enabled') option('with_asan', type: 'boolean', value: false, description: 'Build with Address Sanitizer') option('use_pch', type: 'boolean', value: true, description: 'Use precompiled headers for circle.library') diff --git a/src/engine/core/comm.cpp b/src/engine/core/comm.cpp index 3bde407b27..377ffb71fa 100644 --- a/src/engine/core/comm.cpp +++ b/src/engine/core/comm.cpp @@ -51,6 +51,7 @@ #include "gameplay/mechanics/illumination.h" #include "gameplay/clans/house.h" #include "engine/olc/olc.h" +#include "engine/scripting/lua/lua_formatter.h" #include "engine/olc/vedun/vedun.h" #include "administration/ban.h" #include "administration/proxy.h" @@ -910,10 +911,16 @@ void stop_game(ush_int port) { } } #endif - game_loop(epoll, mother_desc); + { + lua_scripting::LuaFormatterShutdownGuard lua_formatter_shutdown_guard; + game_loop(epoll, mother_desc); + } #else log("Polling using select()."); - game_loop(mother_desc); + { + lua_scripting::LuaFormatterShutdownGuard lua_formatter_shutdown_guard; + game_loop(mother_desc); + } #endif // Shutdown OTEL providers to flush remaining telemetry diff --git a/src/engine/core/heartbeat.cpp b/src/engine/core/heartbeat.cpp index e1da594afa..5ebfd4bbba 100644 --- a/src/engine/core/heartbeat.cpp +++ b/src/engine/core/heartbeat.cpp @@ -25,6 +25,7 @@ #include "gameplay/abilities/timed_abilities.h" #include "gameplay/ai/mobact.h" #include "engine/scripting/dg_event.h" +#include "engine/scripting/dg_olc.h" #include "engine/scripting/lua/lua_script_engine.h" #include "gameplay/mechanics/corpse.h" #include "engine/db/global_objects.h" @@ -276,6 +277,7 @@ Heartbeat::steps_t &pulse_steps() { 0, std::make_shared(GlobalDrop::reload_tables)), Heartbeat::PulseStep("Events processing", 1, 0, std::make_shared(process_events)), + Heartbeat::PulseStep("Lua formatter results", 1, 0, std::make_shared(ProcessLuaFormatterResults)), Heartbeat::PulseStep("Lua scripting cleanup", 1, 0, std::make_shared(lua_scripting::LuaScriptEngine::HeartbeatCleanup)), Heartbeat::PulseStep("Triggers check mobile", PULSE_DG_SCRIPT, diff --git a/src/engine/olc/olc.cpp b/src/engine/olc/olc.cpp index 06146be2fd..c7bbfc0764 100644 --- a/src/engine/olc/olc.cpp +++ b/src/engine/olc/olc.cpp @@ -103,6 +103,8 @@ olc_data::olc_data() script_mode(0), trigger_position(0), item_type(0), + lua_format_request_id(0), + lua_format_save_on_completion(false), script(0), storage(0) { @@ -473,6 +475,7 @@ void strip_string(char *buffer) { */ void cleanup_olc(DescriptorData *d, byte cleanup_type) { if (d->olc) { + TrigeditSavePendingLuaOnCleanup(d); // Освободить редактируемый триггер if (OLC_TRIG(d)) { @@ -539,6 +542,7 @@ void cleanup_olc(DescriptorData *d, byte cleanup_type) { true, d->character.get(), 0, 0, kToRoom); } delete d->olc; + d->olc = nullptr; } } diff --git a/src/engine/olc/olc.h b/src/engine/olc/olc.h index fa415966b0..78f3dd29e0 100644 --- a/src/engine/olc/olc.h +++ b/src/engine/olc/olc.h @@ -11,6 +11,8 @@ #ifndef _OLC_H_ #define _OLC_H_ +#include + #include "engine/db/db.h" #include "engine/structs/structs.h" @@ -109,6 +111,8 @@ struct olc_data { int script_mode; int trigger_position; int item_type; + std::uint64_t lua_format_request_id; + bool lua_format_save_on_completion; ObjData::triggers_list_t script; char *storage; // for holding commands etc.. }; diff --git a/src/engine/scripting/dg_olc.cpp b/src/engine/scripting/dg_olc.cpp index dbe2c85169..0c041cd78b 100644 --- a/src/engine/scripting/dg_olc.cpp +++ b/src/engine/scripting/dg_olc.cpp @@ -27,6 +27,7 @@ #include "engine/entities/zone.h" #include #include "engine/db/world_characters.h" +#include "engine/scripting/lua/lua_formatter.h" #include "engine/db/global_objects.h" #include "engine/db/world_data_source_manager.h" #include "dg_db_scripts.h" @@ -73,6 +74,55 @@ bool TrigeditIsLuaTrigger(const Trigger *trig) return trig && trig->get_script_language() == TriggerScriptLanguage::Lua; } +bool TrigeditQueueLuaFormat(DescriptorData *d, bool save_on_completion) +{ + const std::string source = OLC_STORAGE(d) ? OLC_STORAGE(d) : ""; + const auto request_id = lua_scripting::QueueLuaFormat(source); + if (request_id == 0) { + SendMsgToChar("Очередь форматтера Lua переполнена. Повторите позже.\r\n", d->character.get()); + return false; + } + d->olc->lua_format_request_id = request_id; + d->olc->lua_format_save_on_completion = save_on_completion; + SendMsgToChar(save_on_completion + ? "Проверка и форматирование Lua-скрипта перед сохранением запущены. Дождитесь результата.\r\n" + : "Форматирование Lua-скрипта запущено. Дождитесь результата.\r\n", + d->character.get()); + return true; +} + +bool TrigeditFormatLua(DescriptorData *d) +{ + return TrigeditQueueLuaFormat(d, false); +} + +bool TrigeditFormatLuaBeforeSave(DescriptorData *d) +{ + return TrigeditQueueLuaFormat(d, true); +} + +bool TrigeditApplyFormattedLua(DescriptorData *d, const lua_scripting::LuaFormatResult& result) +{ + if (!result.success) { + SendMsgToChar(d->character.get(), "Ошибка проверки/форматирования Lua: %s\r\n", result.error.c_str()); + return false; + } + if (result.formatted.size() >= MAX_CMD_LENGTH) { + SendMsgToChar(d->character.get(), "Ошибка проверки/форматирования Lua: результат слишком большой.\r\n"); + return false; + } + RECREATE(OLC_STORAGE(d), MAX_CMD_LENGTH); + memcpy(OLC_STORAGE(d), result.formatted.c_str(), result.formatted.size() + 1); + OLC_VAL(d)++; + SendMsgToChar(d->character.get(), "Lua-скрипт отформатирован.\r\n"); + return true; +} + +bool TrigeditLuaFormatPending(const DescriptorData *d) +{ + return d->olc && d->olc->lua_format_request_id != 0; +} + void TrigeditLoadStorageFromTrigger(Trigger *trig, char *storage) { if (!trig || !storage) { @@ -174,8 +224,70 @@ void TrigeditFprintEscapedBody(FILE *fp, const std::string &body) fprintf(fp, "~\n"); } +void TrigeditCommitSave(DescriptorData *d) +{ + trigedit_save(d); + snprintf(buf, sizeof(buf), "OLC: %s edits trigger %d", GET_NAME(d->character), OLC_NUM(d)); + olc_log("%s end trig %d", GET_NAME(d->character), OLC_NUM(d)); + mudlog(buf, NRM, MAX(kLvlBuilder, GET_INVIS_LEV(d->character)), SYSLOG, true); +} + +void TrigeditSaveAndExit(DescriptorData *d) +{ + TrigeditCommitSave(d); + cleanup_olc(d, CLEANUP_ALL); +} + } // namespace +void TrigeditSavePendingLuaOnCleanup(DescriptorData *d) +{ + if (!d || !d->olc || !d->olc->lua_format_save_on_completion || !OLC_TRIG(d)) { + return; + } + d->olc->lua_format_request_id = 0; + d->olc->lua_format_save_on_completion = false; + TrigeditCommitSave(d); +} + +void ProcessLuaFormatterResults() +{ + lua_scripting::LuaFormatResult result; + while (lua_scripting::TryPopLuaFormatResult(result)) { + DescriptorData *d = descriptor_list; + while (d && (d->state != EConState::kTrigedit + || !d->olc + || d->olc->lua_format_request_id != result.request_id)) { + d = d->next; + } + if (!d) { + continue; + } + d->olc->lua_format_request_id = 0; + const bool save_on_completion = d->olc->lua_format_save_on_completion; + d->olc->lua_format_save_on_completion = false; + if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { + if (save_on_completion) { + SendMsgToChar("Форматирование не применено. Исходный код триггера сохраняется.\r\n", + d->character.get()); + TrigeditSaveAndExit(d); + } else { + trigedit_disp_menu(d); + } + continue; + } + const bool formatted = TrigeditApplyFormattedLua(d, result); + if (save_on_completion) { + if (!formatted) { + SendMsgToChar("Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); + } + TrigeditSaveAndExit(d); + continue; + } + trigedit_disp_menu(d); + } +} + inline void fprint_script(FILE *fp, const ObjData::triggers_list_t &scripts) { for (const auto vnum : scripts) { fprintf(fp, "T %d\n", vnum); @@ -262,6 +374,9 @@ void trigedit_disp_menu(DescriptorData *d) { #if defined(WITH_LUAJIT_PROTOTYPE) out << "&g8)&n Язык скрипта : &y" << (TrigeditIsLuaTrigger(trig) ? "Lua" : "DG") << "&n\r\n"; #endif + if (TrigeditIsLuaTrigger(trig) && lua_scripting::LuaFormatterAvailable()) { + out << "&g9)&n Проверить синтаксис и отформатировать Lua\r\n"; + } out << "&gQ)&n Завершить редактирование\r\n" "Введите Выбранное :"; SendMsgToChar(out.str(), d->character.get()); OLC_MODE(d) = TRIGEDIT_MAIN_MENU; @@ -298,8 +413,34 @@ void trigedit_disp_types(DescriptorData *d) { void trigedit_parse(DescriptorData *d, char *arg) { int i = 0; + if (TrigeditLuaFormatPending(d) && d->olc->lua_format_save_on_completion) { + if (tolower(*arg) == 'q') { + d->olc->lua_format_request_id = 0; + d->olc->lua_format_save_on_completion = false; + SendMsgToChar("Форматирование отменено. Исходный Lua-код сохраняется без форматирования.\r\n", + d->character.get()); + TrigeditSaveAndExit(d); + } else { + SendMsgToChar("Проверка и форматирование Lua-скрипта перед сохранением еще выполняются. " + "Для отмены форматирования и сохранения исходного кода используйте Q.\r\n", + d->character.get()); + } + return; + } + switch (OLC_MODE(d)) { case TRIGEDIT_MAIN_MENU: + if (TrigeditLuaFormatPending(d)) { + if (tolower(*arg) == 'q') { + d->olc->lua_format_request_id = 0; + d->olc->lua_format_save_on_completion = false; + } else { + SendMsgToChar("Форматирование Lua-скрипта еще выполняется. Для выхода используйте Q.\r\n", + d->character.get()); + trigedit_disp_menu(d); + return; + } + } switch (tolower(*arg)) { case 'q': if (OLC_VAL(d)) // Anything been changed? @@ -371,6 +512,17 @@ void trigedit_parse(DescriptorData *d, char *arg) { break; #endif + case '9': + if (!TrigeditIsLuaTrigger(OLC_TRIG(d))) { + SendMsgToChar("Автоформат доступен только для Lua-триггеров.\r\n", d->character.get()); + } else if (!lua_scripting::LuaFormatterAvailable()) { + SendMsgToChar("Форматтер Lua недоступен в этой сборке.\r\n", d->character.get()); + } else if (TrigeditFormatLua(d)) { + return; + } + trigedit_disp_menu(d); + return; + default: trigedit_disp_menu(d); return; } @@ -378,11 +530,15 @@ void trigedit_parse(DescriptorData *d, char *arg) { case TRIGEDIT_CONFIRM_SAVESTRING: switch (tolower(*arg)) { - case 'y': trigedit_save(d); - snprintf(buf, sizeof(buf), "OLC: %s edits trigger %d", GET_NAME(d->character), OLC_NUM(d)); - olc_log("%s end trig %d", GET_NAME(d->character), OLC_NUM(d)); - mudlog(buf, NRM, MAX(kLvlBuilder, GET_INVIS_LEV(d->character)), SYSLOG, true); - // fall through + case 'y': + if (TrigeditIsLuaTrigger(OLC_TRIG(d)) && lua_scripting::LuaFormatterAvailable()) { + if (TrigeditFormatLuaBeforeSave(d)) { + return; + } + SendMsgToChar("Исходный Lua-код сохраняется без форматирования.\r\n", d->character.get()); + } + TrigeditSaveAndExit(d); + return; case 'n': cleanup_olc(d, CLEANUP_ALL); return; diff --git a/src/engine/scripting/dg_olc.h b/src/engine/scripting/dg_olc.h index 8baf965490..dd1ae53ea6 100644 --- a/src/engine/scripting/dg_olc.h +++ b/src/engine/scripting/dg_olc.h @@ -29,6 +29,8 @@ void dg_olc_script_copy(DescriptorData *d); void dg_script_menu(DescriptorData *d); int dg_script_edit_parse(DescriptorData *d, char *arg); void indent_trigger(std::string &cmd, int *level); +void ProcessLuaFormatterResults(); +void TrigeditSavePendingLuaOnCleanup(DescriptorData *d); // define the largest set of commands for as trigger #define MAX_CMD_LENGTH 32768 // 16k should be plenty and then some diff --git a/src/engine/scripting/lua/lua_formatter.cpp b/src/engine/scripting/lua/lua_formatter.cpp new file mode 100644 index 0000000000..f225ff5c64 --- /dev/null +++ b/src/engine/scripting/lua/lua_formatter.cpp @@ -0,0 +1,365 @@ +#include "engine/scripting/lua/lua_formatter.h" + +#include + +#if defined(WITH_LUA_FORMATTER) +#include "CodeFormatCLib.h" +#include "utils/logger.h" +#include "utils/utils_encoding.h" + +extern "C" { +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace lua_scripting { + +bool LuaFormatterAvailable() { +#if defined(WITH_LUA_FORMATTER) + return true; +#else + return false; +#endif +} + +namespace { + +#if defined(WITH_LUA_FORMATTER) +bool ValidateLuaSource(lua_State* lua, const std::string& source, std::string& error) { + if (!lua) { + error = "Проверка синтаксиса LuaJIT недоступна"; + return false; + } + + lua_settop(lua, 0); + const auto status = luaL_loadbuffer(lua, source.data(), source.size(), "=OLC Lua trigger"); + if (status != LUA_OK) { + size_t error_size = 0; + const char* lua_error = lua_tolstring(lua, -1, &error_size); + if (lua_error) { + error.assign(lua_error, error_size); + } else { + error = "LuaJIT отклонил исходный код"; + } + lua_settop(lua, 0); + return false; + } + + lua_settop(lua, 0); + return true; +} + +bool FormatLuaSource(lua_State* lua, const std::string& source, std::string& formatted, std::string& error) { + formatted.clear(); + error.clear(); + + try { + if (source.find('\0') != std::string::npos) { + error = "Исходный Lua-код содержит нулевой байт"; + return false; + } + if (!ValidateLuaSource(lua, source, error)) { + return false; + } + + std::vector koi_input(source.begin(), source.end()); + koi_input.push_back('\0'); + std::vector utf8_input(source.size() * 3 + 1, '\0'); + codepages::koi_to_utf8(koi_input.data(), utf8_input.data()); + + FormattingOptions options{}; + options.indent_size = 2; + options.use_tabs = false; + options.insert_final_newline = true; + options.non_standard_symbol = false; + + using FormatResultPtr = std::unique_ptr; + FormatResultPtr result(ReformatLuaCode(utf8_input.data(), "trigger.lua", options), FreeReformatResult); + if (!result) { + error = "Форматтер Lua не смог разобрать исходный код"; + return false; + } + + const std::string formatted_utf8(result.get()); + std::vector utf8_output(formatted_utf8.begin(), formatted_utf8.end()); + utf8_output.push_back('\0'); + std::vector koi_output(formatted_utf8.size() + 1, '\0'); + codepages::utf8_to_koi(utf8_output.data(), koi_output.data()); + formatted = koi_output.data(); + + std::vector round_trip_input(formatted.begin(), formatted.end()); + round_trip_input.push_back('\0'); + std::vector round_trip_utf8(formatted.size() * 3 + 1, '\0'); + codepages::koi_to_utf8(round_trip_input.data(), round_trip_utf8.data()); + if (formatted_utf8 != round_trip_utf8.data()) { + formatted.clear(); + error = "Результат форматирования Lua нельзя представить в KOI8-R"; + return false; + } + std::string syntax_error; + if (!ValidateLuaSource(lua, formatted, syntax_error)) { + formatted.clear(); + error = "Форматтер создал некорректный Lua-код: " + syntax_error; + return false; + } + return true; + } catch (const std::exception&) { + formatted.clear(); + error = "внутренняя ошибка форматтера"; + return false; + } catch (...) { + formatted.clear(); + error = "внутренняя ошибка форматтера"; + return false; + } +} + +template +class SpscQueue { +public: + static_assert(Capacity > 1); + static_assert(std::atomic_size_t::is_always_lock_free); + + bool Push(T&& value) { + const auto tail = m_tail.load(std::memory_order_relaxed); + const auto next = (tail + 1) % Capacity; + if (next == m_head.load(std::memory_order_acquire)) { + return false; + } + m_slots[tail] = std::move(value); + m_tail.store(next, std::memory_order_release); + return true; + } + + bool Pop(T& value) { + const auto head = m_head.load(std::memory_order_relaxed); + if (head == m_tail.load(std::memory_order_acquire)) { + return false; + } + value = std::move(m_slots[head]); + m_head.store((head + 1) % Capacity, std::memory_order_release); + return true; + } + + bool Empty() const { + return m_head.load(std::memory_order_relaxed) == m_tail.load(std::memory_order_acquire); + } + + bool Full() const { + const auto tail = m_tail.load(std::memory_order_relaxed); + return (tail + 1) % Capacity == m_head.load(std::memory_order_acquire); + } + +private: + std::array m_slots; + alignas(64) std::atomic_size_t m_head{0}; + alignas(64) std::atomic_size_t m_tail{0}; +}; + +struct LuaFormatJob { + std::uint64_t request_id{}; + std::string source; +}; + +class LuaFormatterWorker { +public: + LuaFormatterWorker() : m_thread(&LuaFormatterWorker::Run, this) {} + + ~LuaFormatterWorker() { + Shutdown(); + } + + void Shutdown() { + if (m_stopping.exchange(true, std::memory_order_acq_rel)) { + return; + } + m_jobs_changed.fetch_add(1, std::memory_order_release); + m_jobs_changed.notify_one(); + m_results_changed.fetch_add(1, std::memory_order_release); + m_results_changed.notify_one(); + if (m_thread.joinable()) { + m_thread.join(); + } + } + + std::uint64_t Submit(std::string source) { + if (m_stopping.load(std::memory_order_acquire)) { + return 0; + } + LuaFormatJob job; + const auto request_id = m_next_request_id++; + job.request_id = request_id; + job.source = std::move(source); + if (!m_jobs.Push(std::move(job))) { + return 0; + } + m_jobs_changed.fetch_add(1, std::memory_order_release); + m_jobs_changed.notify_one(); + return request_id; + } + + bool TryPop(LuaFormatResult& result) { + if (!m_results.Pop(result)) { + return false; + } + m_results_changed.fetch_add(1, std::memory_order_release); + m_results_changed.notify_one(); + return true; + } + +private: + static_assert(std::atomic_bool::is_always_lock_free); + static_assert(std::atomic_uint32_t::is_always_lock_free); + static constexpr std::size_t kQueueCapacity = 64; + + void Run() { + using LuaStatePtr = std::unique_ptr; + LuaStatePtr lua(nullptr, lua_close); + bool lua_state_failure_logged = false; + while (!m_stopping.load(std::memory_order_acquire)) { + LuaFormatJob job; + while (!m_jobs.Pop(job)) { + const auto observed = m_jobs_changed.load(std::memory_order_acquire); + if (m_stopping.load(std::memory_order_acquire)) { + return; + } + if (m_jobs.Empty()) { + m_jobs_changed.wait(observed, std::memory_order_acquire); + } + } + if (!lua) { + lua.reset(luaL_newstate()); + if (!lua && !lua_state_failure_logged) { + log("SYSERR: lua_formatter: luaL_newstate failed"); + lua_state_failure_logged = true; + } + } + LuaFormatResult result; + result.request_id = job.request_id; + result.success = FormatLuaSource(lua.get(), job.source, result.formatted, result.error); + while (!m_results.Push(std::move(result))) { + const auto observed = m_results_changed.load(std::memory_order_acquire); + if (m_stopping.load(std::memory_order_acquire)) { + return; + } + if (m_results.Full()) { + m_results_changed.wait(observed, std::memory_order_acquire); + } + } + } + } + + SpscQueue m_jobs; + SpscQueue m_results; + std::atomic_bool m_stopping{false}; + std::atomic_uint32_t m_jobs_changed{0}; + std::atomic_uint32_t m_results_changed{0}; + std::thread m_thread; + std::uint64_t m_next_request_id{1}; +}; + +#endif + +} // namespace + +namespace { + +struct FormatterRuntime { +#if defined(WITH_LUA_FORMATTER) + LuaFormatterWorker& EnsureWorker() { + if (!worker) { + worker = std::make_unique(); + } + return *worker; + } + + std::unique_ptr worker; +#endif +}; + +void DeleteFormatterRuntime(void* runtime) { + delete static_cast(runtime); +} + +#if defined(WITH_LUA_FORMATTER) +LuaFormatterShutdownGuard*& ActiveFormatterGuard() { + static LuaFormatterShutdownGuard* guard = nullptr; + return guard; +} +#endif + +} // namespace + +LuaFormatterShutdownGuard::LuaFormatterShutdownGuard() +#if defined(WITH_LUA_FORMATTER) + : m_runtime(new FormatterRuntime, DeleteFormatterRuntime) { + if (ActiveFormatterGuard() != nullptr) { + std::abort(); + } + ActiveFormatterGuard() = this; +#else + : m_runtime(nullptr, DeleteFormatterRuntime) { +#endif +} + +std::uint64_t LuaFormatterShutdownGuard::Submit(std::string source) { +#if defined(WITH_LUA_FORMATTER) + return static_cast(m_runtime.get())->EnsureWorker().Submit(std::move(source)); +#else + (void)source; + return 0; +#endif +} + +bool LuaFormatterShutdownGuard::TryPop(LuaFormatResult& result) { +#if defined(WITH_LUA_FORMATTER) + auto* runtime = static_cast(m_runtime.get()); + auto* worker = runtime ? runtime->worker.get() : nullptr; + return worker && worker->TryPop(result); +#else + (void)result; + return false; +#endif +} + +std::uint64_t QueueLuaFormat(std::string source) { +#if defined(WITH_LUA_FORMATTER) + auto* guard = ActiveFormatterGuard(); + return guard ? guard->Submit(std::move(source)) : 0; +#else + (void)source; + return 0; +#endif +} + +bool TryPopLuaFormatResult(LuaFormatResult& result) { +#if defined(WITH_LUA_FORMATTER) + auto* guard = ActiveFormatterGuard(); + return guard && guard->TryPop(result); +#else + (void)result; + return false; +#endif +} + +LuaFormatterShutdownGuard::~LuaFormatterShutdownGuard() { +#if defined(WITH_LUA_FORMATTER) + if (ActiveFormatterGuard() != this) { + std::abort(); + } + ActiveFormatterGuard() = nullptr; +#endif +} + +} // namespace lua_scripting + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/engine/scripting/lua/lua_formatter.h b/src/engine/scripting/lua/lua_formatter.h new file mode 100644 index 0000000000..9aca413653 --- /dev/null +++ b/src/engine/scripting/lua/lua_formatter.h @@ -0,0 +1,44 @@ +#ifndef BYLINS_SRC_ENGINE_SCRIPTING_LUA_LUA_FORMATTER_H_ +#define BYLINS_SRC_ENGINE_SCRIPTING_LUA_LUA_FORMATTER_H_ + +#include +#include +#include + +namespace lua_scripting { + +struct LuaFormatResult { + std::uint64_t request_id{}; + bool success{}; + std::string formatted; + std::string error; +}; + +// Одновременно может существовать только один guard, охватывающий game_loop. +// Деструктор останавливает и уничтожает лениво созданный worker форматтера. +class LuaFormatterShutdownGuard { +public: + LuaFormatterShutdownGuard(); + ~LuaFormatterShutdownGuard(); + LuaFormatterShutdownGuard(const LuaFormatterShutdownGuard&) = delete; + LuaFormatterShutdownGuard& operator=(const LuaFormatterShutdownGuard&) = delete; + +private: + using RuntimeDeleter = void (*)(void*); + std::unique_ptr m_runtime; + std::uint64_t Submit(std::string source); + bool TryPop(LuaFormatResult& result); + friend std::uint64_t QueueLuaFormat(std::string source); + friend bool TryPopLuaFormatResult(LuaFormatResult& result); +}; + +bool LuaFormatterAvailable(); +// Вызываются только из основного игрового потока. +std::uint64_t QueueLuaFormat(std::string source); +bool TryPopLuaFormatResult(LuaFormatResult& result); + +} // namespace lua_scripting + +#endif // BYLINS_SRC_ENGINE_SCRIPTING_LUA_LUA_FORMATTER_H_ + +// vim: ts=4 sw=4 tw=0 noet syntax=cpp : diff --git a/src/engine/ui/cmd_god/do_luainfo.cpp b/src/engine/ui/cmd_god/do_luainfo.cpp index 8bd9cb8d1a..31e81f8f80 100644 --- a/src/engine/ui/cmd_god/do_luainfo.cpp +++ b/src/engine/ui/cmd_god/do_luainfo.cpp @@ -4,6 +4,7 @@ #include #include "engine/entities/char_data.h" +#include "engine/scripting/lua/lua_formatter.h" #include "engine/scripting/lua/lua_internal.h" #include "engine/ui/modify.h" @@ -71,6 +72,8 @@ void AppendLuaRuntimeDiagnostics(std::ostringstream &out) { void DoLuaInfo(CharData *ch, char * /*argument*/, int /*cmd*/, int /*subcmd*/) { std::ostringstream out; out << "Информация Lua:\r\n"; + out << " форматтер: " + << (lua_scripting::LuaFormatterAvailable() ? "включен (EmmyLuaCodeStyle)" : "выключен") << "\r\n"; #if defined(WITH_LUAJIT_PROTOTYPE) out << " статус: включено\r\n"; diff --git a/subprojects/emmyluacodestyle.wrap b/subprojects/emmyluacodestyle.wrap new file mode 100644 index 0000000000..5847e289a6 --- /dev/null +++ b/subprojects/emmyluacodestyle.wrap @@ -0,0 +1,9 @@ +[wrap-git] +directory = emmyluacodestyle +url = https://github.com/CppCXY/EmmyLuaCodeStyle.git +revision = fdbaa0fa411abda0dd172eb7e360a08f4500208b +depth = 1 +patch_directory = emmyluacodestyle + +[provide] +emmyluacodestyle = emmyluacodestyle_dep diff --git a/subprojects/packagefiles/emmyluacodestyle/meson.build b/subprojects/packagefiles/emmyluacodestyle/meson.build new file mode 100644 index 0000000000..8559ebe2d8 --- /dev/null +++ b/subprojects/packagefiles/emmyluacodestyle/meson.build @@ -0,0 +1,108 @@ +project('emmyluacodestyle', ['c', 'cpp'], version: '1.7.3', default_options: ['cpp_std=c++20']) + +uri_inc = include_directories('3rd/uriparser/include', '3rd/uriparser/src') +uri_sources = files( + '3rd/uriparser/src/UriCommon.c', + '3rd/uriparser/src/UriCompare.c', + '3rd/uriparser/src/UriEscape.c', + '3rd/uriparser/src/UriFile.c', + '3rd/uriparser/src/UriIp4Base.c', + '3rd/uriparser/src/UriIp4.c', + '3rd/uriparser/src/UriMemory.c', + '3rd/uriparser/src/UriNormalizeBase.c', + '3rd/uriparser/src/UriNormalize.c', + '3rd/uriparser/src/UriParseBase.c', + '3rd/uriparser/src/UriParse.c', + '3rd/uriparser/src/UriQuery.c', + '3rd/uriparser/src/UriRecompose.c', + '3rd/uriparser/src/UriResolve.c', + '3rd/uriparser/src/UriShorten.c', +) +uri_lib = static_library( + 'emmy_uri', + uri_sources, + include_directories: uri_inc, + c_args: ['-DURI_LIBRARY_BUILD', '-DURI_STATIC_BUILD'], + override_options: ['unity=off'], + pic: true, +) + +emmy_inc = include_directories( + 'CodeFormatCLib/include', + 'CodeFormatCLib/src', + 'CodeFormatCore/include', + 'CodeFormatCore/src', + 'LuaParser/include', + 'LuaParser/src', + 'Util/include', + 'Util/src', + '3rd/uriparser/include', + '3rd/wildcards/include', +) +emmy_sources = files( + 'CodeFormatCLib/src/CodeFormatCLib.cpp', + 'CodeFormatCLib/src/CodeFormat.cpp', + 'CodeFormatCore/src/Config/FunctionOption.cpp', + 'CodeFormatCore/src/Config/LuaStyle.cpp', + 'CodeFormatCore/src/Config/LuaEditorConfig.cpp', + 'CodeFormatCore/src/Config/LuaDiagnosticStyle.cpp', + 'CodeFormatCore/src/Config/LanguageTranslator.cpp', + 'CodeFormatCore/src/Config/EditorconfigPattern.cpp', + 'CodeFormatCore/src/Format/FormatBuilder.cpp', + 'CodeFormatCore/src/Format/FormatState.cpp', + 'CodeFormatCore/src/Format/Analyzer/FormatAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/SpaceAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/IndentationAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/LineBreakAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/FormatDocAnalyze.cpp', + 'CodeFormatCore/src/Format/Analyzer/AlignAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/TokenAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/SemicolonAnalyzer.cpp', + 'CodeFormatCore/src/Format/Analyzer/FormatResolve.cpp', + 'CodeFormatCore/src/Format/Analyzer/SyntaxNodeHelper.cpp', + 'CodeFormatCore/src/RangeFormat/RangeFormatBuilder.cpp', + 'CodeFormatCore/src/TypeFormat/LuaTypeFormat.cpp', + 'CodeFormatCore/src/TypeFormat/LuaTypeFormatFeatures.cpp', + 'CodeFormatCore/src/Diagnostic/DiagnosticBuilder.cpp', + 'CodeFormatCore/src/Diagnostic/NameStyle/NameStyleChecker.cpp', + 'CodeFormatCore/src/Diagnostic/NameStyle/NameStyleRuleMatcher.cpp', + 'CodeFormatCore/src/Diagnostic/Spell/CodeSpellChecker.cpp', + 'CodeFormatCore/src/Diagnostic/Spell/Util.cpp', + 'CodeFormatCore/src/Diagnostic/CodeStyle/CodeStyleChecker.cpp', + 'LuaParser/src/Parse/LuaParser.cpp', + 'LuaParser/src/Parse/Mark.cpp', + 'LuaParser/src/Lexer/LuaLexer.cpp', + 'LuaParser/src/Lexer/LuaIdentify.cpp', + 'LuaParser/src/Lexer/TextReader.cpp', + 'LuaParser/src/Ast/LuaSyntaxNode.cpp', + 'LuaParser/src/Ast/LuaSyntaxTree.cpp', + 'LuaParser/src/Ast/LuaSyntaxMultiKind.cpp', + 'LuaParser/src/Ast/LuaSyntaxNodeKind.cpp', + 'LuaParser/src/Types/TextRange.cpp', + 'LuaParser/src/File/LuaSource.cpp', + 'LuaParser/src/exception/LuaParseException.cpp', + 'Util/src/CommandLine.cpp', + 'Util/src/StringUtil.cpp', + 'Util/src/Utf8.cpp', + 'Util/src/Url.cpp', + 'Util/src/FileFinder.cpp', + 'Util/src/SymSpell/SymSpell.cpp', + 'Util/src/SymSpell/SuggestItem.cpp', + 'Util/src/SymSpell/EditDistance.cpp', + 'Util/src/InfoTree/InfoTree.cpp', + 'Util/src/InfoTree/InfoNode.cpp', +) +emmy_lib = static_library( + 'emmyluacodestyle', + emmy_sources, + include_directories: emmy_inc, + link_with: uri_lib, + cpp_args: ['-DURI_STATIC_BUILD'], + override_options: ['unity=off'], + pic: true, +) + +emmyluacodestyle_dep = declare_dependency( + include_directories: include_directories('CodeFormatCLib/include'), + link_with: emmy_lib, +) diff --git a/subprojects/packagefiles/luajit/src/host/meson.build b/subprojects/packagefiles/luajit/src/host/meson.build index 023aa8950e..246a109e5d 100644 --- a/subprojects/packagefiles/luajit/src/host/meson.build +++ b/subprojects/packagefiles/luajit/src/host/meson.build @@ -1,5 +1,5 @@ # No need for sanitizing and no point in wasting time with LTO on native helpers. -native_overrides = ['b_sanitize=none', 'b_lto=false'] +native_overrides = ['b_sanitize=none', 'b_lto=false', 'unity=off'] minilua_exe = executable( 'minilua', diff --git a/subprojects/packagefiles/luajit/src/meson.build b/subprojects/packagefiles/luajit/src/meson.build index fe2c591201..289d2f3aea 100644 --- a/subprojects/packagefiles/luajit/src/meson.build +++ b/subprojects/packagefiles/luajit/src/meson.build @@ -177,7 +177,7 @@ luajit_lib_name = 'luajit-' + abiver luajit_lib_kwargs = { 'dependencies' : [m_dep, dl_dep], 'install' : true, - 'override_options': ['b_sanitize=none'], + 'override_options': ['b_sanitize=none', 'unity=off'], } # Shared luajit library. {{{ @@ -265,6 +265,7 @@ if get_option('luajit') export_dynamic: luajit_exe_export_dynamic, install: get_option('luajit'), link_with: luajit_exe_lib, + override_options: ['unity=off'], ) meson.override_find_program('luajit', luajit_exe)