From 1ac21d4a67ddd211fda841aa6e368bc2cf52a3d6 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 18 Aug 2025 12:56:13 -0400 Subject: Validate that nodes in game files are used You can now also list out nodes that you are explicitly not mapping out. The current state of the repo does produce some warnings when the validator is run and they're either endings, paintings that I'm not sure what to do with yet, and weird proxy stuff I'm not sure how to handle yet. --- tools/util/CMakeLists.txt | 1 + tools/util/godot_scene.cpp | 269 ++++++++++++++++++++++++++++++++++++ tools/util/godot_scene.h | 67 +++++++++ tools/validator/CMakeLists.txt | 1 + tools/validator/godot_processor.cpp | 76 ++++++++++ tools/validator/godot_processor.h | 14 ++ tools/validator/human_processor.cpp | 15 ++ tools/validator/main.cpp | 11 +- tools/validator/structs.h | 1 + tools/validator/validator.cpp | 8 ++ 10 files changed, 459 insertions(+), 4 deletions(-) create mode 100644 tools/util/godot_scene.cpp create mode 100644 tools/util/godot_scene.h create mode 100644 tools/validator/godot_processor.cpp create mode 100644 tools/validator/godot_processor.h (limited to 'tools') diff --git a/tools/util/CMakeLists.txt b/tools/util/CMakeLists.txt index f086e10..4d19c3b 100644 --- a/tools/util/CMakeLists.txt +++ b/tools/util/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(Protobuf REQUIRED) add_library(util + godot_scene.cpp identifiers.cpp naming.cpp ) diff --git a/tools/util/godot_scene.cpp b/tools/util/godot_scene.cpp new file mode 100644 index 0000000..272111d --- /dev/null +++ b/tools/util/godot_scene.cpp @@ -0,0 +1,269 @@ +#include "godot_scene.h" + +#include + +#include +#include +#include + +namespace com::fourisland::lingo2_archipelago { + +namespace { + +class GodotSceneImpl : public GodotScene { + public: + GodotSceneImpl(std::map ext_resources, + std::unique_ptr root, + std::vector> descendents) + : ext_resources_(std::move(ext_resources)), + root_(std::move(root)), + descendents_(std::move(descendents)) {} + + virtual const GodotExtResource* GetExtResource(const std::string& id) const { + auto it = ext_resources_.find(id); + if (it != ext_resources_.end()) { + return &it->second; + } else { + return nullptr; + } + } + + virtual const GodotNode& GetRoot() const { return *root_; } + + private: + std::map ext_resources_; + std::unique_ptr root_; + std::vector> descendents_; +}; + +struct Heading { + std::string type; + + std::string id; + std::string path; + std::string resource_type; + + std::string name; + std::string parent; + GodotInstanceType instance_type; +}; + +Heading ParseTscnHeading(absl::string_view line) { + std::string original_line(line); + Heading heading; + + if (line[0] != '[') { + std::ostringstream errormsg; + errormsg << "Heading must start with [." << std::endl + << "Bad heading: " << original_line; + throw std::invalid_argument(errormsg.str()); + } + + line.remove_prefix(1); + int divider = line.find_first_of(" ]"); + if (divider == std::string_view::npos) { + std::ostringstream errormsg; + errormsg << "Malformatted heading: " << line << std::endl + << "Original line: " << original_line; + throw std::invalid_argument(errormsg.str()); + } + + heading.type = std::string(line.substr(0, divider)); + line.remove_prefix(divider + 1); + + while (!line.empty()) { + divider = line.find_first_of("="); + if (divider == std::string_view::npos) { + std::ostringstream errormsg; + errormsg << "Malformatted heading: " << line << std::endl + << "Original line: " << original_line; + throw std::invalid_argument(errormsg.str()); + } + + std::string key(line.substr(0, divider)); + line.remove_prefix(divider + 1); + + if (line[0] == '"') { + line.remove_prefix(1); + divider = line.find_first_of("\""); + + if (divider == std::string_view::npos) { + std::ostringstream errormsg; + errormsg << "Malformatted heading: " << line << std::endl + << "Original line: " << original_line; + throw std::invalid_argument(errormsg.str()); + } + + std::string strval(line.substr(0, divider)); + line.remove_prefix(divider + 2); + + if (key == "name") { + heading.name = strval; + } else if (key == "parent") { + heading.parent = strval; + } else if (key == "path") { + heading.path = strval; + } else if (key == "type") { + heading.resource_type = strval; + } else if (key == "id") { + heading.id = strval; + } + } else if (line[0] == 'S' || line[0] == 'E') { + GodotInstanceType rrval; + char internal = line[0]; + + line.remove_prefix(13); // SubResource(" + divider = line.find_first_of("\""); + + if (divider == std::string_view::npos) { + std::ostringstream errormsg; + errormsg << "Malformatted heading: " << line << std::endl + << "Original line: " << original_line; + throw std::invalid_argument(errormsg.str()); + } + + std::string refid = std::string(line.substr(0, divider)); + line.remove_prefix(divider + 3); + + GodotInstanceType instance_type; + if (internal == 'E') { + instance_type = GodotExtResourceRef{.id = refid}; + } else { + // SubResource is not supported right now. + } + + if (key == "instance") { + heading.instance_type = instance_type; + } else { + // Other keys aren't supported right now. + } + } else { + divider = line.find_first_of(" ]"); + + if (divider == std::string_view::npos) { + std::ostringstream errormsg; + errormsg << "Malformatted heading: " << line << std::endl + << "Original line: " << original_line; + throw std::invalid_argument(errormsg.str()); + } + + int numval = std::atoi(line.substr(0, divider).data()); + line.remove_prefix(divider + 1); + + // keyvals_[key] = numval; + } + } + + return heading; +} + +} // namespace + +void GodotNode::AddChild(GodotNode& child) { + children_[child.GetName()] = &child; + child.parent_ = this; +} + +std::string GodotNode::GetPath() const { + if (parent_ == nullptr || parent_->GetName() == "") { + return name_; + } else { + return parent_->GetPath() + "/" + name_; + } +} + +const GodotNode* GodotNode::GetNode(absl::string_view path) const { + std::vector names = absl::StrSplit(path, "/"); + + auto it = children_.find(names[0]); + if (it == children_.end()) { + return nullptr; + } else { + if (names.size() == 1) { + return it->second; + } else { + path.remove_prefix(names[0].size() + 1); + + return it->second->GetNode(path); + } + } +} + +GodotNode* GodotNode::GetNode(absl::string_view path) { + return const_cast( + const_cast(this)->GetNode(path)); +} + +std::unique_ptr ReadGodotSceneFromFile(const std::string& path) { + std::map ext_resources; + auto root = std::make_unique("", GodotInstanceType{}); + std::vector> descendents; + + std::ifstream input(path); + + std::string line; + bool section_started = false; + Heading cur_heading; + std::ostringstream cur_value; + bool value_started = false; + auto handle_end_of_section = [&]() { + section_started = false; + value_started = false; + + if (cur_heading.type == "sub_resource") { + // sub_resources_[std::get(cur_heading.GetKeyval("id"))] = + // {cur_heading, cur_value.str(), ""}; + } else { + // other_.emplace_back(cur_heading, cur_value.str()); + } + + cur_value = {}; + }; + while (std::getline(input, line)) { + if (section_started && (line.empty() || line[0] == '[')) { + handle_end_of_section(); + } + if (!line.empty() && line[0] == '[') { + Heading heading = ParseTscnHeading(line); + if (heading.type == "gd_scene") { + // file_descriptor_ = heading; + } else if (heading.type == "ext_resource") { + GodotExtResource ext_resource; + ext_resource.path = heading.path; + ext_resource.type = heading.resource_type; + + ext_resources[heading.id] = ext_resource; + } else if (heading.type == "node") { + if (heading.parent != "") { + descendents.push_back( + std::make_unique(heading.name, heading.instance_type)); + GodotNode* child = descendents.back().get(); + + if (heading.parent == ".") { + root->AddChild(*child); + } else { + root->GetNode(heading.parent)->AddChild(*child); + } + } + } else { + cur_heading = heading; + section_started = true; + } + } else if (!line.empty()) { + if (value_started) { + cur_value << std::endl; + } else { + value_started = true; + } + cur_value << line; + } + } + if (section_started) { + handle_end_of_section(); + } + + return std::make_unique( + std::move(ext_resources), std::move(root), std::move(descendents)); +} + +} // namespace com::fourisland::lingo2_archipelago diff --git a/tools/util/godot_scene.h b/tools/util/godot_scene.h new file mode 100644 index 0000000..529e38e --- /dev/null +++ b/tools/util/godot_scene.h @@ -0,0 +1,67 @@ +#ifndef TOOLS_UTIL_TSCN_H_ +#define TOOLS_UTIL_TSCN_H_ + +#include + +#include +#include +#include +#include +#include + +namespace com::fourisland::lingo2_archipelago { + +struct GodotExtResource { + std::string type; + std::string path; +}; + +struct GodotExtResourceRef { + std::string id; +}; + +using GodotInstanceType = std::variant; + +class GodotNode { + public: + GodotNode(std::string name, GodotInstanceType instance_type) + : name_(std::move(name)), instance_type_(std::move(instance_type)) {} + + const std::string& GetName() const { return name_; } + + const GodotInstanceType& GetInstanceType() const { return instance_type_; } + + const GodotNode* GetParent() const { return parent_; } + GodotNode* GetParent() { return parent_; } + + std::string GetPath() const; + + void AddChild(GodotNode& child); + + const GodotNode* GetNode(absl::string_view path) const; + GodotNode* GetNode(absl::string_view path); + + const std::map GetChildren() const { + return children_; + } + + private: + std::string name_; + GodotInstanceType instance_type_; + + GodotNode* parent_ = nullptr; + std::map children_; +}; + +class GodotScene { + public: + virtual const GodotExtResource* GetExtResource( + const std::string& id) const = 0; + virtual const GodotNode& GetRoot() const = 0; +}; + +std::unique_ptr ReadGodotSceneFromFile(const std::string& path); + +} // namespace com::fourisland::lingo2_archipelago + +#endif /* TOOLS_UTIL_TSCN_H_ */ diff --git a/tools/validator/CMakeLists.txt b/tools/validator/CMakeLists.txt index 0ad58c2..967b890 100644 --- a/tools/validator/CMakeLists.txt +++ b/tools/validator/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(Protobuf REQUIRED) add_executable(validator + godot_processor.cpp human_processor.cpp main.cpp validator.cpp diff --git a/tools/validator/godot_processor.cpp b/tools/validator/godot_processor.cpp new file mode 100644 index 0000000..f345cff --- /dev/null +++ b/tools/validator/godot_processor.cpp @@ -0,0 +1,76 @@ +#include "godot_processor.h" + +#include +#include +#include +#include + +#include "structs.h" +#include "util/godot_scene.h" + +namespace com::fourisland::lingo2_archipelago { + +namespace { + +static const std::set kImportantNodeTypes = { + "res://objects/nodes/panel.tscn", "res://objects/nodes/worldport.tscn", + "res://objects/nodes/keyHolder.tscn", + "res://objects/nodes/collectable.tscn"}; + +class GodotProcessor { + public: + GodotProcessor(const std::string& repodir, CollectedInfo& info) + : repodir_(repodir), info_(info) {} + + void Run() { + for (auto& [map_name, map_info] : info_.maps) { + ProcessMap(map_name, map_info); + } + } + + void ProcessMap(const std::string& map_name, MapInfo& map_info) { + std::filesystem::path scene_path = std::filesystem::path(repodir_) / + "objects" / "scenes" / + (map_name + ".tscn"); + std::string scene_path_str = scene_path.string(); + std::cout << "Processing " << scene_path_str << std::endl; + + std::unique_ptr scene = + ReadGodotSceneFromFile(scene_path_str); + + ProcessMapNode(*scene, scene->GetRoot(), map_info); + } + + void ProcessMapNode(const GodotScene& scene, const GodotNode& node, + MapInfo& map_info) { + if (std::holds_alternative(node.GetInstanceType())) { + const GodotExtResourceRef& ext_resource_ref = + std::get(node.GetInstanceType()); + const GodotExtResource* ext_resource = + scene.GetExtResource(ext_resource_ref.id); + + if (ext_resource != nullptr && + (kImportantNodeTypes.count(ext_resource->path) || + ext_resource->path.starts_with("res://objects/meshes/paintings/"))) { + map_info.game_nodes[node.GetPath()].defined = true; + } + } + + for (const auto& [child_name, child_node] : node.GetChildren()) { + ProcessMapNode(scene, *child_node, map_info); + } + } + + private: + std::string repodir_; + CollectedInfo& info_; +}; + +} // namespace + +void ProcessGodotData(const std::string& repodir, CollectedInfo& info) { + GodotProcessor godot_processor(repodir, info); + godot_processor.Run(); +} + +} // namespace com::fourisland::lingo2_archipelago diff --git a/tools/validator/godot_processor.h b/tools/validator/godot_processor.h new file mode 100644 index 0000000..97bcea6 --- /dev/null +++ b/tools/validator/godot_processor.h @@ -0,0 +1,14 @@ +#ifndef TOOLS_VALIDATOR_GODOT_PROCESSOR_H_ +#define TOOLS_VALIDATOR_GODOT_PROCESSOR_H_ + +#include + +namespace com::fourisland::lingo2_archipelago { + +struct CollectedInfo; + +void ProcessGodotData(const std::string& repodir, CollectedInfo& info); + +} // namespace com::fourisland::lingo2_archipelago + +#endif /* TOOLS_VALIDATOR_GODOT_PROCESSOR_H_ */ diff --git a/tools/validator/human_processor.cpp b/tools/validator/human_processor.cpp index 0846bb8..af40980 100644 --- a/tools/validator/human_processor.cpp +++ b/tools/validator/human_processor.cpp @@ -55,11 +55,26 @@ class HumanProcessor { void ProcessMap(std::filesystem::path path) { std::string map_name = path.filename().string(); + ProcessMetadataFile(path / "metadata.txtpb", map_name); ProcessConnectionsFile(path / "connections.txtpb", map_name); ProcessDoorsFile(path / "doors.txtpb", map_name); ProcessRooms(path / "rooms", map_name); } + void ProcessMetadataFile(std::filesystem::path path, + const std::string& current_map_name) { + if (!std::filesystem::exists(path)) { + return; + } + + MapInfo& map_info = info_.maps[current_map_name]; + + auto metadata = ReadMessageFromFile(path.string()); + for (const std::string& path : metadata.excluded_nodes()) { + map_info.game_nodes[path].uses++; + } + } + void ProcessRooms(std::filesystem::path path, const std::string& current_map_name) { for (auto const& dir_entry : std::filesystem::directory_iterator(path)) { diff --git a/tools/validator/main.cpp b/tools/validator/main.cpp index af9842b..1a72e9a 100644 --- a/tools/validator/main.cpp +++ b/tools/validator/main.cpp @@ -1,3 +1,4 @@ +#include "godot_processor.h" #include "human_processor.h" #include "structs.h" #include "validator.h" @@ -5,10 +6,11 @@ namespace com::fourisland::lingo2_archipelago { namespace { -void Run(const std::string& mapdir) { +void Run(const std::string& mapdir, const std::string& repodir) { CollectedInfo info; ProcessHumanData(mapdir, info); + ProcessGodotData(repodir, info); ValidateCollectedInfo(info); } @@ -17,15 +19,16 @@ void Run(const std::string& mapdir) { } // namespace com::fourisland::lingo2_archipelago int main(int argc, char** argv) { - if (argc != 2) { + if (argc != 3) { std::cout << "Incorrect argument count." << std::endl; - std::cout << "Usage: validator [path to map directory]" << std::endl; + std::cout << "Usage: validator [path to map directory] [path to Lingo 2 repository]" << std::endl; return 1; } std::string mapdir = argv[1]; + std::string repodir = argv[2]; - com::fourisland::lingo2_archipelago::Run(mapdir); + com::fourisland::lingo2_archipelago::Run(mapdir, repodir); return 0; } diff --git a/tools/validator/structs.h b/tools/validator/structs.h index 1b61f77..406dc0c 100644 --- a/tools/validator/structs.h +++ b/tools/validator/structs.h @@ -21,6 +21,7 @@ struct MalformedIdentifiers { }; struct GameNodeInfo { + bool defined = false; int uses = 0; }; diff --git a/tools/validator/validator.cpp b/tools/validator/validator.cpp index f2ec280..6d01b7c 100644 --- a/tools/validator/validator.cpp +++ b/tools/validator/validator.cpp @@ -14,6 +14,14 @@ void ValidateMap(const std::string& map_name, const MapInfo& map_info) { if (node_info.uses > 1) { std::cout << "Map " << map_name << " node " << node_path << " is used in multiple places." << std::endl; + } else if (node_info.uses == 0) { + std::cout << "Map " << map_name << " node " << node_path + << " is not used." << std::endl; + } + + if (!node_info.defined) { + std::cout << "Map " << map_name << " node " << node_path + << " is not defined in the game file." << std::endl; } } } -- cgit 1.4.1