neptools

Modding tools to Neptunia games
git clone https://git.neptards.moe/neptards/neptools.git
Log | Files | Refs | Submodules | README | LICENSE

stcm-editor.cpp (18658B)


      1 #include "../format/item.hpp"
      2 #include "../format/cl3.hpp"
      3 #include "../format/primitive_item.hpp"
      4 #include "../format/stcm/file.hpp"
      5 #include "../format/stcm/gbnl.hpp"
      6 #include "../format/stcm/string_data.hpp"
      7 #include "../format/stsc/file.hpp"
      8 #include "../open.hpp"
      9 #include "../txt_serializable.hpp"
     10 #include "../utils.hpp"
     11 #include "version.hpp"
     12 
     13 #include <libshit/except.hpp>
     14 #include <libshit/lua/base.hpp>
     15 #include <libshit/options.hpp>
     16 #include <libshit/platform.hpp>
     17 
     18 #include <iostream>
     19 #include <fstream>
     20 #include <deque>
     21 #include <boost/algorithm/string/predicate.hpp>
     22 #include <boost/filesystem/path.hpp>
     23 #include <boost/filesystem/operations.hpp>
     24 
     25 #if LIBSHIT_STDLIB_IS_MSVC
     26 #  undef _CRT_NONSTDC_DEPRECATE // fuck off m$
     27 #  define _CRT_NONSTDC_DEPRECATE(x)
     28 #  include <io.h>
     29 #endif
     30 
     31 #define LIBSHIT_LOG_NAME "stcm-editor"
     32 #include <libshit/logger_helper.hpp>
     33 
     34 using namespace Neptools;
     35 using namespace Libshit;
     36 
     37 namespace
     38 {
     39   struct State
     40   {
     41     SmartPtr<Dumpable> dump;
     42     Cl3* cl3 = nullptr;
     43     Stcm::File* stcm = nullptr;
     44     TxtSerializable* txt = nullptr;
     45   };
     46 }
     47 
     48 static State SmartOpen(const boost::filesystem::path& fname)
     49 {
     50   auto x = OpenFactory::Open(fname);
     51   return {x, dynamic_cast<Cl3*>(x.get()), dynamic_cast<Stcm::File*>(x.get()),
     52       dynamic_cast<TxtSerializable*>(x.get())};
     53 }
     54 
     55 template <typename T>
     56 static void ShellDump(const T* item, const char* name)
     57 {
     58   RefCountedPtr<Sink> sink;
     59   if (name[0] == '-' && name[1] == '\0')
     60     sink = Sink::ToStdOut();
     61   else
     62     sink = Sink::ToFile(name, item->GetSize());
     63   item->Dump(*sink);
     64 }
     65 
     66 template <typename T, typename Fun>
     67 static void ShellInspectGen(const T* item, const char* name, Fun f)
     68 {
     69   if (name[0] == '-' && name[1] == '\0')
     70     f(item, std::cout);
     71   else
     72     f(item, OpenOut(name));
     73 }
     74 
     75 template <typename T>
     76 static void ShellInspect(const T* item, const char* name)
     77 {
     78   ShellInspectGen(
     79     item, name, [](auto x, auto&& y) { y << "return " << *x << '\n'; });
     80 }
     81 
     82 static void EnsureStcm(State& st)
     83 {
     84   if (st.stcm) return;
     85   if (!st.dump) throw InvalidParam{"no file loaded"};
     86   if (!st.cl3)
     87     throw InvalidParam{"invalid file loaded: can't find STCM without CL3"};
     88 
     89   st.stcm = &st.cl3->GetStcm();
     90 }
     91 
     92 static void EnsureTxt(State& st)
     93 {
     94   if (st.txt) return;
     95   EnsureStcm(st);
     96   if (!st.stcm->GetGbnl())
     97     LIBSHIT_THROW(DecodeError, "No GBNL found in STCM");
     98   st.txt = st.stcm;
     99 }
    100 
    101 static bool auto_failed = false;
    102 template <typename Pred, typename Fun>
    103 static void RecDo(
    104   const boost::filesystem::path& path, Pred p, Fun f, bool rec = false)
    105 {
    106   if (p(path, rec))
    107   {
    108     try { f(path); }
    109     catch (const std::exception& e)
    110     {
    111       auto_failed = true;
    112       ERR << "Failed: " << Libshit::PrintException(true) << std::endl;
    113     }
    114   }
    115   else if (boost::filesystem::is_directory(path))
    116     for (auto& e: boost::filesystem::directory_iterator(path))
    117       RecDo(e, p, f, true);
    118   else if (!rec)
    119     ERR << "Invalid filename: " << path << std::endl;
    120 }
    121 
    122 namespace
    123 {
    124   enum class Mode
    125   {
    126 #define MODE_PARS_PRE(X)                                                        \
    127     X(AUTO_STRTOOL,   "auto-strtool",   "import/export .cl3/.gbin/.gstr texts") \
    128     X(EXPORT_STRTOOL, "export-strtool", "export .cl3/.gbin/.gstr to .txt")      \
    129     X(IMPORT_STRTOOL, "import-strtool", "import .cl3/.gbin/.gstr from .txt")    \
    130     X(AUTO_CL3,       "auto-cl3",       "unpack/pack .cl3 files")               \
    131     X(UNPACK_CL3,     "unpack-cl3",     "unpack .cl3 files")                    \
    132     X(PACK_CL3,       "pack-cl3",       "pack .cl3 files")
    133 #define MODE_PARS_LUA(X)                                                        \
    134     X(AUTO_LUA,       "auto-lua",       "import/export stcms")                  \
    135     X(EXPORT_LUA,     "export-lua",     "export stcms")                         \
    136     X(IMPORT_LUA,     "import-lua",     "import lua")
    137 #define MODE_PARS_POST(X)                                                       \
    138     X(MANUAL,         "manual",         "manual processing (set automatically)")
    139 #if LIBSHIT_WITH_LUA
    140 #   define MODE_PARS(X) MODE_PARS_PRE(X) MODE_PARS_LUA(X) MODE_PARS_POST(X)
    141 #else
    142 #   define MODE_PARS(X) MODE_PARS_PRE(X) MODE_PARS_POST(X)
    143 #endif
    144 #define GEN_ENUM(name, shit1, shit2) name,
    145     MODE_PARS(GEN_ENUM)
    146 #undef GEN_ENUM
    147   } mode = Mode::AUTO_STRTOOL;
    148 }
    149 
    150 static auto BaseDoAutoFun(const boost::filesystem::path& p, const char* ext)
    151 {
    152   boost::filesystem::path cl3, txt;
    153   bool import;
    154   if (boost::ends_with(p.native(), ext))
    155   {
    156     cl3 = p.native().substr(0, p.native().size()-4);
    157     txt = p;
    158     import = true;
    159     INF << "Importing: " << cl3 << " <- " << txt << std::endl;
    160   }
    161   else
    162   {
    163     cl3 = txt = p;
    164     txt += ext;
    165     import = false;
    166     INF << "Exporting: " << cl3 << " -> " << txt << std::endl;
    167   }
    168 
    169   return std::make_tuple(import, cl3, txt);
    170 }
    171 
    172 static void DoAutoTxt(const boost::filesystem::path& p)
    173 {
    174   auto [import, cl3, txt] = BaseDoAutoFun(p, ".txt");
    175   auto st = SmartOpen(cl3);
    176   EnsureTxt(st);
    177   if (import)
    178   {
    179     st.txt->ReadTxt(OpenIn(txt));
    180     if (st.stcm) st.stcm->Fixup();
    181     st.dump->Fixup();
    182     st.dump->Dump(cl3);
    183   }
    184   else
    185     st.txt->WriteTxt(OpenOut(txt));
    186 }
    187 
    188 #if LIBSHIT_WITH_LUA
    189 static void DoAutoLua(const boost::filesystem::path& p)
    190 {
    191   auto [import, bin, lua] = BaseDoAutoFun(p, ".lua");
    192   if (import)
    193   {
    194     Lua::State vm;
    195     lua_getglobal(vm, "debug"); // +1
    196     lua_getfield(vm, -1, "traceback"); // +2
    197     if (luaL_loadfile(vm, lua.string().c_str()) || lua_pcall(vm, 0, 1, -2))
    198     {
    199       Logger::Log("lua", Logger::ERROR, nullptr, 0, nullptr)
    200         << lua_tostring(vm, -1) << std::endl;
    201       return;
    202     }
    203     auto dmp = vm.Get<NotNull<SmartPtr<Dumpable>>>(-1);
    204     // hack? when importing a cl3, and we get a gbnl, put it into the
    205     // existing cl3
    206     if (boost::iends_with(bin.native(), ".cl3") &&
    207         dynamic_cast<Stcm::File*>(dmp.get()))
    208     {
    209       auto cl3 = MakeSmart<Cl3>(Source::FromFile(bin));
    210       auto stcme = cl3->entries.find("main.DAT", std::less<>{});
    211       if (stcme == cl3->entries.end())
    212         LIBSHIT_THROW(DecodeError, "Invalid CL3 file: no main.DAT");
    213       dmp->Fixup();
    214       stcme->src = dmp;
    215       dmp = cl3;
    216     }
    217     dmp->Fixup();
    218     dmp->Dump(bin);
    219   }
    220   else
    221   {
    222     auto st = SmartOpen(bin);
    223     EnsureStcm(st);
    224     OpenOut(lua) << "return " << *(st.stcm ? st.stcm : st.dump.get()) << '\n';
    225   }
    226 }
    227 #endif
    228 
    229 static void DoAutoCl3(const boost::filesystem::path& p)
    230 {
    231   if (boost::filesystem::is_directory(p))
    232   {
    233     boost::filesystem::path cl3_file =
    234       p.native().substr(0, p.native().size() - 4);
    235     INF << "Packing " << cl3_file << std::endl;
    236     Cl3 cl3{Source::FromFile(cl3_file)};
    237     cl3.UpdateFromDir(p);
    238     cl3.Fixup();
    239     cl3.Dump(cl3_file);
    240   }
    241   else
    242   {
    243     INF << "Extracting " << p << std::endl;
    244     Cl3 cl3{Source::FromFile(p)};
    245     auto out = p;
    246     cl3.ExtractTo(out += ".out");
    247   }
    248 }
    249 
    250 static inline bool is_file(const boost::filesystem::path& pth)
    251 {
    252   auto stat = boost::filesystem::status(pth);
    253   return boost::filesystem::is_regular_file(stat) ||
    254     boost::filesystem::is_symlink(stat);
    255 }
    256 
    257 static bool IsBin(const boost::filesystem::path& p, bool = false)
    258 {
    259   return is_file(p) && (
    260     boost::iends_with(p.native(), ".cl3") ||
    261     boost::iends_with(p.native(), ".gbin") ||
    262     boost::iends_with(p.native(), ".gstr") ||
    263     boost::iends_with(p.native(), ".bin"));
    264 }
    265 
    266 static bool IsTxt(const boost::filesystem::path& p, bool = false)
    267 {
    268   return is_file(p) && (
    269     boost::iends_with(p.native(), ".cl3.txt") ||
    270     boost::iends_with(p.native(), ".gbin.txt") ||
    271     boost::iends_with(p.native(), ".gstr.txt") ||
    272     boost::iends_with(p.native(), ".bin.txt"));
    273 }
    274 
    275 #if LIBSHIT_WITH_LUA
    276 static bool IsLua(const boost::filesystem::path& p, bool = false)
    277 {
    278   return is_file(p) && (
    279     boost::iends_with(p.native(), ".cl3.lua") ||
    280     boost::iends_with(p.native(), ".gbin.lua") ||
    281     boost::iends_with(p.native(), ".gstr.lua") ||
    282     boost::iends_with(p.native(), ".bin.lua"));
    283 }
    284 #endif
    285 
    286 static bool IsCl3(const boost::filesystem::path& p, bool = false)
    287 {
    288   return is_file(p) && boost::iends_with(p.native(), ".cl3");
    289 }
    290 
    291 static bool IsCl3Dir(const boost::filesystem::path& p, bool = false)
    292 {
    293   return boost::filesystem::is_directory(p) &&
    294     boost::iends_with(p.native(), ".cl3.out");
    295 }
    296 
    297 static void DoAuto(const boost::filesystem::path& path)
    298 {
    299   bool (*pred)(const boost::filesystem::path&, bool);
    300   void (*fun)(const boost::filesystem::path& p);
    301 
    302   switch (mode)
    303   {
    304   case Mode::AUTO_STRTOOL:
    305     pred = [](auto& p, bool rec)
    306     {
    307       if (rec)
    308         return (IsTxt(p) && boost::filesystem::exists(
    309                   p.native().substr(0, p.native().size()-4))) ||
    310           (IsBin(p) && !boost::filesystem::exists(
    311              boost::filesystem::path(p)+=".txt"));
    312       else
    313         return IsBin(p) || IsTxt(p);
    314     };
    315     fun = DoAutoTxt;
    316     break;
    317 
    318   case Mode::EXPORT_STRTOOL:
    319     pred = IsBin;
    320     fun = DoAutoTxt;
    321     break;
    322   case Mode::IMPORT_STRTOOL:
    323     pred = IsTxt;
    324     fun = DoAutoTxt;
    325     break;
    326 
    327   case Mode::AUTO_CL3:
    328     pred = [](auto& p, bool rec)
    329     {
    330       if (rec)
    331         return IsCl3Dir(p) || (IsCl3(p) && !boost::filesystem::exists(
    332                                  boost::filesystem::path(p)+=".out"));
    333       else
    334         return IsCl3(p) || IsCl3Dir(p);
    335     };
    336     fun = DoAutoCl3;
    337     break;
    338 
    339   case Mode::UNPACK_CL3:
    340     pred = IsCl3;
    341     fun = DoAutoCl3;
    342     break;
    343   case Mode::PACK_CL3:
    344     pred = IsCl3Dir;
    345     fun = DoAutoCl3;
    346     break;
    347 
    348 #if LIBSHIT_WITH_LUA
    349   case Mode::AUTO_LUA:
    350     pred = [](auto& p, bool rec)
    351     {
    352       if (rec)
    353         return (IsLua(p) && boost::filesystem::exists(
    354                   p.native().substr(0, p.native().size()-4))) ||
    355           (IsBin(p) && !boost::filesystem::exists(
    356             boost::filesystem::path(p)+=".lua"));
    357       else
    358         return IsBin(p) || IsLua(p);
    359     };
    360     fun = DoAutoLua;
    361     break;
    362 
    363   case Mode::EXPORT_LUA:
    364     pred = IsBin;
    365     fun = DoAutoLua;
    366     break;
    367 
    368   case Mode::IMPORT_LUA:
    369     pred = IsLua;
    370     fun = DoAutoLua;
    371     break;
    372 #endif
    373 
    374   case Mode::MANUAL:
    375     throw InvalidParam{"Can't use auto files in manual mode"};
    376   }
    377   RecDo(path, pred, fun);
    378 }
    379 
    380 int main(int argc, char** argv)
    381 {
    382   State st;
    383   auto& parser = OptionParser::GetGlobal();
    384   OptionGroup hgrp{parser, "High-level options"};
    385   OptionGroup lgrp{parser, "Low-level options", "See README for details"};
    386 
    387   Option mode_opt{
    388     hgrp, "mode", 'm', 1, "OPTION",
    389 #define GEN_HELP(_, key, help) "\t\t" key ": " help "\n"
    390     "Set operating mode:\n" MODE_PARS(GEN_HELP),
    391 #undef GEN_HELP
    392     [](auto&, auto&& args)
    393     {
    394       if (false); // NOLINT
    395 #define GEN_IFS(c, str, _) else if (strcmp(args.front(), str) == 0) mode = Mode::c;
    396       MODE_PARS(GEN_IFS)
    397 #undef GEN_IFS
    398       else throw InvalidParam{"invalid argument"};
    399     }};
    400 
    401   Option open_opt{
    402     lgrp, "open", 1, "FILE", "Opens FILE as cl3 or stcm file",
    403     [&](auto&, auto&& args)
    404     {
    405       mode = Mode::MANUAL;
    406       st = SmartOpen(args.front());
    407     }};
    408   Option save_opt{
    409     lgrp, "save", 1, "FILE|-", "Saves the loaded file to FILE or stdout",
    410     [&](auto&, auto&& args)
    411     {
    412       mode = Mode::MANUAL;
    413       if (!st.dump) throw InvalidParam{"no file loaded"};
    414       st.dump->Fixup();
    415       ShellDump(st.dump.get(), args.front());
    416     }};
    417   Option create_cl3_opt{
    418     lgrp, "create-cl3", 0, nullptr, "Creates an empty cl3 file",
    419     [&](auto&, auto&&)
    420     {
    421       mode = Mode::MANUAL;
    422       SmartPtr<Cl3> c = MakeSmart<Cl3>();
    423       st = {c, c.get(), nullptr, nullptr};
    424     }};
    425   Option list_files_opt{
    426     lgrp, "list-files", 0, nullptr, "Lists the contents of the cl3 archive",
    427     [&](auto&, auto&&)
    428     {
    429       mode = Mode::MANUAL;
    430       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    431       size_t i = 0;
    432       for (const auto& e : st.cl3->entries)
    433       {
    434         std::cout << i++ << '\t' << e.name << '\t' << e.src->GetSize()
    435                   << "\tlinks:";
    436         for (const auto& l : e.links)
    437           std::cout << ' ' << st.cl3->IndexOf(l);
    438         std::cout << std::endl;
    439       }
    440     }};
    441   Option extract_file_opt{
    442     lgrp, "extract-file", 2, "NAME OUT_FILE|-",
    443     "Extract NAME from cl3 archive to OUT_FILE or stdout",
    444     [&](auto&, auto&& args)
    445     {
    446       mode = Mode::MANUAL;
    447       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    448       auto& entries = st.cl3->entries;
    449       auto e = entries.find(args[0]);
    450 
    451       if (e == entries.end())
    452         throw InvalidParam{"specified file not found"};
    453       else
    454         ShellDump(e->src.get(), args[1]);
    455     }};
    456   Option extract_files_opt{
    457     lgrp, "extract-files", 1, "DIR", "Extract the cl3 archive to DIR",
    458     [&](auto&, auto&& args)
    459     {
    460       mode = Mode::MANUAL;
    461       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    462       st.cl3->ExtractTo(args.front());
    463     }};
    464   Option replace_file_opt{
    465     lgrp, "replace-file", 2, "NAME IN_FILE",
    466     "Adds or replaces NAME in cl3 archive with IN_FILE",
    467     [&](auto&, auto&& args)
    468     {
    469       mode = Mode::MANUAL;
    470       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    471 
    472       auto& e = st.cl3->GetOrCreateFile(args[0]);
    473       e.src = MakeSmart<DumpableSource>(Source::FromFile(args[1]));
    474     }};
    475   Option remove_file_opt{
    476     lgrp, "remove-file", 1, "NAME", "Removes NAME from cl3 archive",
    477     [&](auto&, auto&& args)
    478     {
    479       mode = Mode::MANUAL;
    480       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    481       auto& entries = st.cl3->entries;
    482       auto e = entries.find(args.front());
    483       if (e == entries.end())
    484         throw InvalidParam{"specified file not found"};
    485       else
    486         entries.erase(e);
    487     }};
    488   Option set_link_opt{
    489     lgrp, "set-link", 3, "NAME ID DEST", "Sets link at NAME, ID to DEST",
    490     [&](auto&, auto&& args)
    491     {
    492       mode = Mode::MANUAL;
    493       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    494       auto& entries = st.cl3->entries;
    495       auto e = entries.find(args[0]);
    496       auto i = std::stoul(args[1]);
    497       auto e2 = entries.find(args[2]);
    498       if (e == entries.end() || e2 == entries.end())
    499         throw InvalidParam{"specified file not found"};
    500 
    501       if (i < e->links.size())
    502         e->links[i] = &entries[entries.index_of(e2)];
    503       else if (i == e->links.size())
    504         e->links.push_back(&entries[entries.index_of(e2)]);
    505       else
    506         throw InvalidParam{"invalid link id"};
    507     }};
    508   Option remove_link_opt{
    509     lgrp, "remove-link", 2, "NAME ID", "Remove link ID from NAME",
    510     [&](auto&, auto&& args)
    511     {
    512       mode = Mode::MANUAL;
    513       if (!st.cl3) throw InvalidParam{"no cl3 loaded"};
    514       auto& entries = st.cl3->entries;
    515       auto e = entries.find(args[0]);
    516       auto i = std::stoul(args[1]);
    517       if (e == entries.end())
    518         throw InvalidParam{"specified file not found"};
    519 
    520       if (i < e->links.size())
    521         e->links.erase(e->links.begin() + i);
    522       else
    523         throw InvalidParam{"invalid link id"};
    524     }};
    525   Option inspect_opt{
    526     lgrp, "inspect", 1, "OUT|-",
    527     "Inspects currently loaded file into OUT or stdout",
    528     [&](auto&, auto&& args)
    529     {
    530       mode = Mode::MANUAL;
    531       if (!st.dump) throw InvalidParam{"No file loaded"};
    532       ShellInspect(st.dump.get(), args.front());
    533     }};
    534   Option inspect_stcm_opt{
    535     lgrp, "inspect-stcm", 1, "OUT|-",
    536     "Inspects only the stcm portion of the currently loaded file into OUT or stdout",
    537     [&](auto&, auto&& args)
    538     {
    539       mode = Mode::MANUAL;
    540       EnsureStcm(st);
    541       ShellInspect(st.stcm, args.front());
    542     }};
    543   Option parse_stcmp_opt{
    544     lgrp, "parse-stcm", 0, nullptr,
    545     "Parse STCM-inside-CL3 (usually done automatically)",
    546     [&](auto&, auto&&)
    547     {
    548       mode = Mode::MANUAL;
    549       EnsureStcm(st);
    550     }};
    551 
    552   Option export_txt_opt{
    553     lgrp, "export-txt", 1, "OUT_FILE|-", "Export text to OUT_FILE or stdout",
    554     [&](auto&, auto&& args)
    555     {
    556       mode = Mode::MANUAL;
    557       EnsureTxt(st);
    558       ShellInspectGen(st.txt, args.front(),
    559                       [](auto& x, auto&& y) { x->WriteTxt(y); });
    560     }};
    561   Option import_txt_opt{
    562     lgrp, "import-txt", 1, "IN_FILE|-", "Read text from IN_FILE or stdin",
    563     [&](auto&, auto&& args)
    564     {
    565       mode = Mode::MANUAL;
    566       EnsureTxt(st);
    567       auto fname = args.front();
    568       if (fname[0] == '-' && fname[1] == '\0')
    569         st.txt->ReadTxt(std::cin);
    570       else
    571         st.txt->ReadTxt(OpenIn(fname));
    572       if (st.stcm) st.stcm->Fixup();
    573     }};
    574 
    575 #if LIBSHIT_WITH_LUA
    576   Option lua{
    577     lgrp, "lua", 'i', 0, nullptr, "Interactive lua prompt",
    578     [&](auto&, auto&&)
    579     {
    580       Lua::State vm;
    581       std::string str;
    582 
    583       // use print (I'm lazy to write my own)
    584       lua_getglobal(vm, "print"); // 1
    585       lua_getglobal(vm, "debug"); // 2
    586       lua_getfield(vm, 2, "traceback"); // 3
    587       lua_remove(vm, 2); // 2 = traceback
    588 
    589       auto prompt = isatty(0);
    590 
    591       while ((prompt && std::cout << "> ", std::getline(std::cin, str)))
    592       {
    593         // if input starts with "> " it's a copy-pasted prompt, remove
    594         if (boost::algorithm::starts_with(str, "> "))
    595           str.erase(0, 2);
    596 
    597         lua_pushvalue(vm, 1); // 3 // push print
    598         if ((luaL_loadstring(vm, ("return "+str).c_str()) &&
    599              (lua_pop(vm, 1), luaL_loadstring(vm, str.c_str()))) || // 4
    600             lua_pcall(vm, 0, LUA_MULTRET, 2)) // 3+?
    601         {
    602           Logger::Log("lua", Logger::ERROR, nullptr, 0, nullptr)
    603             << lua_tostring(vm, -1) << std::endl;
    604           lua_pop(vm, 2);
    605         }
    606         else
    607         {
    608           auto top = lua_gettop(vm);
    609           if (top > 3)
    610             lua_call(vm, top-3, 0);
    611           else
    612             lua_pop(vm, 1);
    613         }
    614       }
    615     }};
    616     Option lua_script{
    617       lgrp, "lua-script", 'L', 1, "FILE", "Run lua script",
    618       [&](auto&, auto&& args)
    619       {
    620         Lua::State vm;
    621         lua_getglobal(vm, "debug"); // +1
    622         lua_getfield(vm, -1, "traceback"); // +2
    623         if (luaL_loadfile(vm, args[0]) || lua_pcall(vm, 0, 0, -2))
    624           Logger::Log("lua", Logger::ERROR, nullptr, 0, nullptr)
    625             << lua_tostring(vm, -1) << std::endl;
    626       }};
    627 #endif
    628 
    629     boost::filesystem::path self{argv[0]};
    630     if (boost::iequals(self.filename().string(), "cl3-tool")
    631 #if LIBSHIT_OS_IS_WINDOWS
    632         || boost::iequals(self.filename().string(), "cl3-tool.exe")
    633 #endif
    634         )
    635       mode = Mode::AUTO_CL3;
    636 
    637     parser.SetVersion("NepTools stcm-editor v" NEPTOOLS_VERSION);
    638     parser.SetUsage("[--options] [<file/directory>...]");
    639     parser.SetShowHelpOnNoOptions();
    640     parser.SetNonArgHandler(FUNC<DoAuto>);
    641 
    642     try { parser.Run(argc, argv); }
    643     catch (const Exit& e) { return !e.success; }
    644     catch (...)
    645     {
    646       ERR << "Fatal error, aborting\n" << Libshit::PrintException(true)
    647           << std::endl;
    648       return 2;
    649     }
    650     return auto_failed;
    651 }