scraps

Abandon all hope, ye who enter here.
git clone https://git.neptards.moe/neptards/scraps.git
Log | Files | Refs | Submodules | README | LICENSE

importer_base.cpp (12488B)


      1 #include "scraps/format/rags_common/importer_base.hpp"
      2 
      3 #include "scraps/format/archive.hpp"
      4 #include "scraps/format/rags_common/description.hpp"
      5 #include "scraps/game/character_state.hpp" // IWYU pragma: keep
      6 #include "scraps/game/object_state.hpp" // IWYU pragma: keep
      7 #include "scraps/game/game_state.hpp"
      8 
      9 #include <libshit/assert.hpp>
     10 #include <libshit/char_utils.hpp>
     11 #include <libshit/doctest_std.hpp>
     12 #include <libshit/memory_utils.hpp>
     13 #include <libshit/utils.hpp>
     14 
     15 #include <capnp/common.h>
     16 #include <capnp/layout.h>
     17 #include <capnp/list.h>
     18 #include <capnp/orphan.h>
     19 
     20 #include <boost/endian/buffers.hpp>
     21 
     22 #include <algorithm>
     23 #include <cstring>
     24 #include <tuple>
     25 #include <vector>
     26 
     27 #define LIBSHIT_LOG_NAME "import"
     28 #include <libshit/logger_helper.hpp>
     29 
     30 namespace Scraps::Format::RagsCommon
     31 {
     32   TEST_SUITE_BEGIN("Scraps::Format::RagsCommon");
     33 
     34   ImporterBase::ImporterBase()
     35     : bld{1024*1024 / sizeof(capnp::word)},
     36       game{bld.initRoot<Proto::Game>()}
     37   {
     38     game.SetPlayerId(GetId());
     39     LIBSHIT_ASSERT(game.GetPlayerId() == 1);
     40   }
     41 
     42   std::uint64_t ImporterBase::FindId(
     43     const IdMap& map, Libshit::StringView key, Libshit::StringView what,
     44     std::optional<Libshit::StringView> missing_key)
     45   {
     46     if (key == missing_key) return 0;
     47     if (auto it = map.find(key); it != map.end()) return it->second;
     48 
     49     WARN << "Missing " << what << " " << Libshit::Quoted(key)
     50          << ", ignored" << std::endl;
     51     return 0;
     52   }
     53 
     54   std::uint64_t ImporterBase::FindId(
     55     const IdMap& name_map, const UuidMap& uuid_map, Libshit::StringView key,
     56     Libshit::StringView what, std::optional<Libshit::StringView> missing_key)
     57   {
     58     if (key == missing_key) return 0;
     59     if (auto uuid = Uuid::TryParse(key))
     60     {
     61       if (auto it = uuid_map.find(*uuid); it != uuid_map.end())
     62         return it->second;
     63     }
     64     else if (auto it = name_map.find(key); it != name_map.end())
     65       return it->second;
     66 
     67     WARN << "Missing " << what << " UUID or name " << Libshit::Quoted(key)
     68          << ", ignored" << std::endl;
     69     return 0;
     70   }
     71 
     72   // Game
     73   static std::pair<ImporterBase::IdMap, std::vector<std::string>>
     74   GenListIds(ImporterBase& imp, std::vector<std::string>&& list,
     75              Libshit::StringView what)
     76   {
     77     ImporterBase::IdMap res;
     78     std::vector<std::string> vect;
     79     for (auto&& x : list)
     80     {
     81       auto [it, inserted] = res.try_emplace(x, imp.GetId());
     82       if (inserted)
     83         vect.push_back(Libshit::Move(x));
     84       else
     85         WARN << "Duplicate " << what << " " << Libshit::Quoted(x) << ", ignored"
     86              << std::endl;
     87     }
     88     return {Libshit::Move(res), Libshit::Move(vect)};
     89   }
     90 
     91   TEST_CASE("GenListIds")
     92   {
     93     ImporterBase imp;
     94     SUBCASE("empty")
     95     {
     96       auto [m, v] = GenListIds(imp, {}, "");
     97       CHECK(m.empty());
     98       CHECK(v.empty());
     99     }
    100 
    101     SUBCASE("one item")
    102     {
    103       auto [m, v] = GenListIds(imp, {"abc"}, "");
    104       CHECK(m.size() == 1);
    105       CHECK(m.at("abc") == 2);
    106       CHECK(v == std::vector<std::string>{"abc"});
    107     }
    108 
    109     SUBCASE("three item, good order")
    110     {
    111       auto [m, v] = GenListIds(imp, {"aa","bc","xxx"}, "");
    112       CHECK(m.size() == 3);
    113       CHECK(m.at("aa") == 2);
    114       CHECK(m.at("bc") == 3);
    115       CHECK(m.at("xxx") == 4);
    116       CHECK(v == std::vector<std::string>{"aa", "bc", "xxx"});
    117     }
    118 
    119     SUBCASE("three item, bad order")
    120     {
    121       auto [m, v] = GenListIds(imp, {"xxx","aa","bc"}, "");
    122       CHECK(m.size() == 3);
    123       CHECK(m.at("xxx") == 2);
    124       CHECK(m.at("aa") == 3);
    125       CHECK(m.at("bc") == 4);
    126       CHECK(v == std::vector<std::string>{"xxx", "aa", "bc"});
    127     }
    128 
    129     SUBCASE("dup")
    130     {
    131       auto [m, v] = GenListIds(imp, {"abc","abc"}, "test normal");
    132       CHECK(m.size() == 1);
    133       CHECK(m.at("abc") == 2);
    134       CHECK(v == std::vector<std::string>{"abc"});
    135     }
    136   }
    137 
    138   template <auto Func>
    139   static void CommaSepCommon(
    140     ImporterBase& imp, ImporterBase::IdMap& groups,
    141     Proto::Game::Builder game,
    142     std::vector<std::string>&& list,
    143     Libshit::StringView what)
    144   {
    145     std::vector<std::string> order;
    146     std::tie(groups, order) = GenListIds(imp, Libshit::Move(list), what);
    147     auto capnp_list = (game.*Func)(order.size());
    148     std::uint32_t i = 0;
    149     for (auto&& name : order)
    150     {
    151       capnp_list[i].SetId(groups.at(name));
    152       capnp_list[i].SetName(imp.sp.InternString(Libshit::Move(name)));
    153       ++i;
    154     }
    155   }
    156 
    157   ImporterBase::IdMap::iterator ImporterBase::InsertUniqueName(
    158     IdMap& ids, std::string&& name, Libshit::StringView what, std::uint64_t id)
    159   {
    160     if (!id) id = GetId();
    161     if (auto [it, inserted] = ids.try_emplace(Libshit::Move(name), id); inserted)
    162       return it;
    163 
    164     if (!what.empty())
    165       WARN << "Duplicate " << what << " name " << Libshit::Quoted(name)
    166            << ", renamed" << std::endl;
    167 
    168     name += "_conflict";
    169     auto orig_len = name.size();
    170 
    171     for (std::uint32_t i = 0; ; ++i)
    172     {
    173       name.resize(orig_len);
    174       name += std::to_string(i);
    175       if (auto [it, inserted] = ids.try_emplace(Libshit::Move(name), id);
    176           inserted)
    177         return it;
    178     }
    179   }
    180 
    181   TEST_CASE("InsertUniqueName")
    182   {
    183     ImporterBase imp;
    184     auto it = imp.InsertUniqueName(imp.file_ids, "foo", "");
    185     CHECK(it->first == "foo");
    186     CHECK(it->second == 2);
    187 
    188     it = imp.InsertUniqueName(imp.file_ids, "bar", "", 69);
    189     CHECK(it->first == "bar");
    190     CHECK(it->second == 69);
    191 
    192     it = imp.InsertUniqueName(imp.file_ids, "foo", "");
    193     CHECK(it->first == "foo_conflict0");
    194     CHECK(it->second == 3);
    195 
    196     it = imp.InsertUniqueName(imp.file_ids, "foo", "", 1337);
    197     CHECK(it->first == "foo_conflict1");
    198     CHECK(it->second == 1337);
    199   }
    200 
    201   std::string ImporterBase::GetUniqueName(NameSet& set, std::string&& name)
    202   {
    203     if (auto [it, inserted] = set.insert(name); inserted)
    204       return Libshit::Move(name);
    205 
    206     name += "_conflict";
    207     auto orig_len = name.size();
    208 
    209     for (std::uint32_t i = 0; ; ++i)
    210     {
    211       name.resize(orig_len);
    212       name += std::to_string(i);
    213       if (auto [it, inserted] = set.insert(name); inserted)
    214         return Libshit::Move(name);
    215     }
    216   }
    217 
    218   TEST_CASE("GetUniqueName")
    219   {
    220     ImporterBase::NameSet set;
    221     CHECK(ImporterBase::GetUniqueName(set, "foo") == "foo");
    222     CHECK(ImporterBase::GetUniqueName(set, "foo") == "foo_conflict0");
    223     CHECK(ImporterBase::GetUniqueName(set, "bar") == "bar");
    224     CHECK(ImporterBase::GetUniqueName(set, "foo") == "foo_conflict1");
    225   }
    226 
    227   void ImporterBase::SetRoomGroups(
    228     Proto::Game::Builder game, std::vector<std::string>&& list)
    229   {
    230     CommaSepCommon<&Proto::Game::Builder::InitRoomGroups>(
    231       *this, room_group_ids, game, Libshit::Move(list), "room group");
    232   }
    233 
    234   void ImporterBase::SetClothingZones(
    235     Proto::Game::Builder game, std::vector<std::string>&& list)
    236   {
    237     CommaSepCommon<&Proto::Game::Builder::InitClothingZones>(
    238       *this, clothing_zone_ids, game, Libshit::Move(list), "clothing zone");
    239   }
    240 
    241   template <typename Proxy>
    242   static std::vector<std::uint32_t> CalcOrder(Proxy coll, std::uint32_t offs)
    243   {
    244     if (coll.Size() <= offs) return {};
    245 
    246     std::vector<std::uint32_t> res;
    247     std::vector<std::string> names;
    248     res.reserve(coll.Size());
    249     names.reserve(coll.Size());
    250     for (std::uint32_t i = 0, n = coll.Size(); i < n; ++i)
    251     {
    252       res.push_back(i);
    253       coll.At(i).CalculateToString(names.emplace_back());
    254     }
    255     std::sort(res.begin() + offs, res.end(), [&](std::uint32_t a, std::uint32_t b)
    256     { return names[a] < names[b]; });
    257     return res;
    258   }
    259 
    260   template <typename List>
    261   static ImporterBase::IdRemap GenRemap(
    262     List lst, const std::vector<std::uint32_t>& order)
    263   {
    264     ImporterBase::IdRemap res;
    265     LIBSHIT_ASSERT(lst.size() == order.size());
    266     for (std::uint32_t i = 0, n = lst.size(); i < n; ++i)
    267     {
    268       auto ins = res.try_emplace(lst[order[i]].GetId(), lst[i].GetId()).second;
    269       LIBSHIT_ASSERT(ins);
    270     }
    271     return res;
    272   }
    273 
    274   static void SortCapnp(
    275     const std::vector<std::uint32_t>& order, capnp::_::ListBuilder lst,
    276     capnp::_::StructSize struct_size)
    277   {
    278     LIBSHIT_ASSERT(order.size() == lst.size());
    279     const auto n = lst.size();
    280     auto elem_siz = struct_size.total() * sizeof(capnp::word);
    281     auto siz = n * elem_siz;
    282     auto buf = Libshit::MakeUnique<char[]>(siz, Libshit::uninitialized);
    283     auto cap_dat = reinterpret_cast<char*>(lst.getLocation() + 1);
    284     std::memcpy(buf.get(), cap_dat, siz);
    285     std::memset(cap_dat, 0, siz);
    286 
    287     for (std::size_t i = 0; i < n; ++i)
    288     {
    289       std::memcpy(cap_dat + elem_siz * i, buf.get() + elem_siz * order[i],
    290                   elem_siz);
    291 
    292       // Unfortunately pointers contains offsets relative to the pointer itself,
    293       // not the segment start, so we have to recalculate those. I have no
    294       // freakin idea how could you achive this using the "public" capnp api
    295       // (if it is possible at all), so just bit-bang it.
    296       std::int32_t diff = std::int32_t(elem_siz / sizeof(capnp::word)) *
    297         (std::int32_t(order[i]) - std::int32_t(i));
    298 
    299       auto p = reinterpret_cast<boost::endian::little_uint32_buf_t*>(
    300         cap_dat + elem_siz * i + struct_size.data * sizeof(capnp::word));
    301       for (std::size_t j = 0; j < struct_size.pointers; ++j)
    302       {
    303         auto pv = p[j*2].value();
    304         // but far pointers contain offset relative to the segment start for the
    305         // sake of consistency, I think
    306         if ((pv & 3) == 2) continue;
    307 
    308         // leave null pointers alone
    309         if (std::int32_t off = std::int32_t(pv) >> 2)
    310           p[j*2] = (std::uint32_t(off + diff) << 2) | (pv & 3);
    311       }
    312     }
    313   }
    314 
    315   void ImporterBase::FinishGame(
    316     std::int32_t object_so, std::int32_t chara_so, std::int32_t inventory_so)
    317   {
    318     if (game.GetShowMainImage()) game.SetInlineImages(false);
    319     game.AdoptStringPool(sp.ToCapnp(bld.getOrphanage()));
    320 
    321     // yes, NATURAL and LILO means unordered, and if any of [object, inventory]
    322     // sort order is alpha, both of them will be sorted. seriously, what did you
    323     // expect?
    324     bool sort_chara = chara_so == SortOrder::ALPHA;
    325     bool sort_obj = object_so == SortOrder::ALPHA || inventory_so == SortOrder::ALPHA;
    326     if (!sort_chara && !sort_obj) return;
    327 
    328     std::vector<std::uint32_t> chara_order, obj_order;
    329     {
    330       Game::GameState gs{Libshit::MakeUnique<Format::DummyArchiveReader>(game)};
    331 
    332       if (sort_chara) chara_order = CalcOrder(gs.GetCharacterColl(), 1);
    333       if (sort_obj) obj_order = CalcOrder(gs.GetObjectColl(), 0);
    334     }
    335 
    336     if (!chara_order.empty())
    337     {
    338       RemapCharacter(GenRemap(game.GetCharacters(), chara_order));
    339       SortCapnp(chara_order, PrivateGetter::GetBuilder(game.GetCharacters()),
    340                 capnp::_::structSize<Proto::Character>());
    341     }
    342     if (!obj_order.empty())
    343     {
    344       RemapObject(GenRemap(game.GetObjects(), obj_order));
    345       SortCapnp(obj_order, PrivateGetter::GetBuilder(game.GetObjects()),
    346                 capnp::_::structSize<Proto::Object>());
    347     }
    348   }
    349 
    350   // Character
    351   void ImporterBase::RemapCharacter(const IdRemap& remap)
    352   {
    353     for (auto c : game.GetCharacters())
    354       c.SetId(remap.at(c.GetId()));
    355     for (auto o : game.GetObjects())
    356       if (auto l = o.GetLocation(); l.IsCharacterId())
    357         l.SetCharacterId(remap.at(l.GetCharacterId()));
    358   }
    359 
    360   // Player
    361   void ImporterBase::FinishPlayer(Proto::Character::Builder player)
    362   {
    363     LIBSHIT_ASSERT(game.GetPlayerId() == 1);
    364 
    365     player.SetId(game.GetPlayerId());
    366     // don't warn when we already have a "player" character, it's fine
    367     auto name = InsertUniqueName(character_ids, "player", "", 1);
    368     player.SetName(sp.InternCopy(name->first));
    369   }
    370 
    371   // Room
    372   Proto::Room::Exit::Direction ImporterBase::ConvertDirection(std::int32_t dir)
    373   {
    374     if (dir <= Direction::EMPTY || dir > Direction::OUT)
    375       LIBSHIT_THROW(Libshit::DecodeError, "Invalid exit direction",
    376                     "Direction", dir);
    377     return static_cast<Proto::Room::Exit::Direction>(dir);
    378   }
    379 
    380   // Timer
    381   void ImporterBase::FinishTimer(Proto::Timer::Builder timer, TimerType type)
    382   {
    383     timer.SetId(timer_ids.at(sp.Get(timer.GetName())));
    384 
    385     switch (type)
    386     {
    387     case TimerType::RUN_ALWAYS: timer.SetWithLength(false); break;
    388     case TimerType::LENGTH:     timer.SetWithLength(true);  break;
    389     default:
    390       LIBSHIT_THROW(ImportError, "Unknown timer type", "Type", type);
    391     }
    392   }
    393 
    394   // StatusBarItem
    395   void ImporterBase::FinishStatusBarItem(Proto::StatusBarItem::Builder it)
    396   { it.SetId(statusbar_ids.at(sp.Get(it.GetName()))); }
    397 
    398   TEST_SUITE_END();
    399 }