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 }