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 }