#include #include #include "CharacterXmlCheck.h" namespace { // Shaped like what the server writes (UserManager's new character plus what the components save), made-up values const std::string VALID = "" "" "" "" "" "" "" "" "" "" "" "

" "" ""; CharacterXmlCheck::Context MakeContext() { CharacterXmlCheck::Context context; context.ownerAccountId = 7; auto& l = context.lookups; l.isItem = [](LOT lot) { return lot == 1001 || lot == 1002 || lot == 2000 || lot == 6666; }; l.stackSize = [](LOT lot) { return lot == 2000 ? 999 : 1; }; l.missionExists = [](int32_t id) { return id == 1 || id == 2; }; l.levelUScore = [](uint32_t level) -> std::optional { if (level == 1) return 0; if (level == 2) return 100; if (level == 3) return 300; return std::nullopt; }; l.maxLevel = 3; return context; } std::string Replace(std::string text, const std::string& from, const std::string& to) { const auto at = text.find(from); EXPECT_NE(at, std::string::npos) << from; if (at != std::string::npos) text.replace(at, from.size(), to); return text; } bool Mentions(const std::vector& list, const std::string& part) { return std::any_of(list.begin(), list.end(), [&](const std::string& s) { return s.find(part) != std::string::npos; }); } CharacterXmlCheck::Result Check(const std::string& xml) { return CharacterXmlCheck::Check(xml, MakeContext()); } void ExpectError(const std::string& xml, const std::string& part) { const auto result = Check(xml); EXPECT_FALSE(result.Ok()) << part; EXPECT_TRUE(Mentions(result.errors, part)) << "expected an error mentioning: " << part << (result.errors.empty() ? "" : " got: " + result.errors.front()); } void ExpectWarning(const std::string& xml, const std::string& part) { const auto result = Check(xml); EXPECT_TRUE(result.Ok()) << (result.errors.empty() ? "" : result.errors.front()); EXPECT_TRUE(Mentions(result.warnings, part)) << "expected a warning mentioning: " << part; } } TEST(CharacterXmlCheckTests, ValidCharacterPasses) { const auto result = Check(VALID); EXPECT_TRUE(result.errors.empty()) << (result.errors.empty() ? "" : result.errors.front()); EXPECT_TRUE(result.warnings.empty()) << (result.warnings.empty() ? "" : result.warnings.front()); } TEST(CharacterXmlCheckTests, Structure) { ExpectError("", "empty"); ExpectError("", "not valid XML"); ExpectError("", ""); ExpectError(Replace(Replace(VALID, "", ""), ""); ExpectError(Replace(VALID, ""); ExpectError(Replace(Replace(VALID, "", ""), "", ""), ""); ExpectError(Replace(VALID, VALID.substr(VALID.find(""), VALID.find("") - VALID.find("")), ""), "at least one "); ExpectError(Replace(Replace(VALID, "", ""), ""); } TEST(CharacterXmlCheckTests, Numbers) { ExpectError(Replace(VALID, "cc=\"150\"", "cc=\"lots\""), "cc=\"lots\""); ExpectError(Replace(VALID, "cc=\"150\"", "cc=\"-5\""), "negative"); ExpectError(Replace(VALID, "ls=\"150\"", "ls=\"-1\""), "negative"); ExpectError(Replace(VALID, "lzx=\"-626.5\"", "lzx=\"west\""), "lzx"); ExpectError(Replace(VALID, "hc=\"1\"", "hc=\"red\""), ""); ExpectError(Replace(VALID, "hm=\"4\"", "hm=\"full\""), ""); ExpectError(Replace(VALID, "refCount=\"1\"", "refCount=\"x\""), ""); ExpectError(Replace(VALID, ""); ExpectError(Replace(VALID, "", ""), "emote: id is missing"); ExpectError(Replace(VALID, "map=\"1000\" ", ""), "map is missing"); ExpectError(Replace(VALID, ""); ExpectError(Replace(VALID, "l=\"2\" cv", "l=\"two\" cv"), ""); ExpectError(Replace(VALID, "csl=\"-1\"", "csl=\"none\""), "csl"); ExpectError(Replace(VALID, "

"); } TEST(CharacterXmlCheckTests, Flags) { // The game reads these with std::stoul/std::stoull, which throw ExpectError(Replace(VALID, ": id"); ExpectError(Replace(VALID, "v=\"18446744073709551615\"", "v=\"18446744073709551616\""), ": v"); ExpectError(Replace(VALID, ": id"); } TEST(CharacterXmlCheckTests, Enums) { ExpectError(Replace(VALID, "gm=\"0\"", "gm=\"12\""), "not a GM level"); ExpectError(Replace(VALID, "cv=\"1\"", "cv=\"99\""), "newer than"); ExpectError(Replace(VALID, "", ""), "not an inventory type"); ExpectError(Replace(VALID, "state=\"2\"", "state=\"3\""), "not a mission state"); } TEST(CharacterXmlCheckTests, Items) { ExpectError(Replace(VALID, " s=\"1\" c=\"1\"", " c=\"1\""), "s is missing"); ExpectError(Replace(VALID, "c=\"500\" eq=\"0\"", "c=\"500\""), "eq is missing"); ExpectError(Replace(VALID, "id=\"101\"", "id=\"abc\""), "id=\"abc\""); ExpectError(Replace(VALID, "id=\"101\"", "id=\"0\""), "id can't be 0"); ExpectError(Replace(VALID, "id=\"101\"", "id=\"100\""), "used by another item"); ExpectError(Replace(VALID, "id=\"101\" s=\"1\"", "id=\"101\" s=\"0\""), "slot 0"); ExpectError(Replace(VALID, "l=\"1002\"", "l=\"4242\""), "not an item"); ExpectError(Replace(VALID, "c=\"500\"", "c=\"-3\""), "c=\"-3\""); // Build inventories are re-slotted when loading, so shared slots there are fine const auto bbb = Replace(VALID, "", ""); EXPECT_TRUE(Check(bbb).Ok()); } TEST(CharacterXmlCheckTests, MissionsAndOwnership) { ExpectError(Replace(VALID, "", ""); const auto removed = CharacterXmlCheck::RemoveItems(withProxy, { 102 }); EXPECT_EQ(removed.find("id=\"102\""), std::string::npos); EXPECT_EQ(removed.find("id=\"103\""), std::string::npos); EXPECT_NE(removed.find("id=\"101\""), std::string::npos); EXPECT_TRUE(Check(removed).Ok()); }