summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2025-08-18 12:56:13 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2025-08-18 12:56:13 -0400
commit1ac21d4a67ddd211fda841aa6e368bc2cf52a3d6 (patch)
treebdcf651c156c27982e37bddb7cb7e0b09aa90d5a
parent15b8794bbe80be0bcf1f482674455efe002cec2c (diff)
downloadlingo2-archipelago-1ac21d4a67ddd211fda841aa6e368bc2cf52a3d6.tar.gz
lingo2-archipelago-1ac21d4a67ddd211fda841aa6e368bc2cf52a3d6.tar.bz2
lingo2-archipelago-1ac21d4a67ddd211fda841aa6e368bc2cf52a3d6.zip
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.
-rw-r--r--data/connections.txtpb2
-rw-r--r--data/maps/the_between/connections.txtpb43
-rw-r--r--data/maps/the_between/rooms/B2 Back Room.txtpb17
-rw-r--r--data/maps/the_between/rooms/B2 Front Room.txtpb12
-rw-r--r--data/maps/the_between/rooms/B2 Room.txtpb8
-rw-r--r--data/maps/the_between/rooms/Main Area.txtpb6
-rw-r--r--data/maps/the_between/rooms/Plaza Entrance.txtpb7
-rw-r--r--data/maps/the_colorful/metadata.txtpb2
-rw-r--r--data/maps/the_entry/metadata.txtpb10
-rw-r--r--data/maps/the_extravagant/metadata.txtpb2
-rw-r--r--data/maps/the_gallery/metadata.txtpb8
-rw-r--r--data/maps/the_graveyard/metadata.txtpb5
-rw-r--r--data/maps/the_great/metadata.txtpb7
-rw-r--r--data/maps/the_great/rooms/Back Area.txtpb2
-rw-r--r--data/maps/the_great/rooms/Daedalus Entrance.txtpb2
-rw-r--r--data/maps/the_great/rooms/Main Area.txtpb10
-rw-r--r--data/maps/the_great/rooms/Purple Room.txtpb2
-rw-r--r--data/maps/the_great/rooms/Salmon Room.txtpb2
-rw-r--r--data/maps/the_hinterlands/metadata.txtpb32
-rw-r--r--data/maps/the_impressive/metadata.txtpb5
-rw-r--r--data/maps/the_orb/metadata.txtpb4
-rw-r--r--data/maps/the_orb/rooms/B Room.txtpb1
-rw-r--r--proto/human.proto5
-rw-r--r--tools/util/CMakeLists.txt1
-rw-r--r--tools/util/godot_scene.cpp269
-rw-r--r--tools/util/godot_scene.h67
-rw-r--r--tools/validator/CMakeLists.txt1
-rw-r--r--tools/validator/godot_processor.cpp76
-rw-r--r--tools/validator/godot_processor.h14
-rw-r--r--tools/validator/human_processor.cpp15
-rw-r--r--tools/validator/main.cpp11
-rw-r--r--tools/validator/structs.h1
-rw-r--r--tools/validator/validator.cpp8
33 files changed, 627 insertions, 30 deletions
diff --git a/data/connections.txtpb b/data/connections.txtpb index 5e67500..e5014f7 100644 --- a/data/connections.txtpb +++ b/data/connections.txtpb
@@ -915,7 +915,7 @@ connections {
915 from { 915 from {
916 port { 916 port {
917 map: "the_between" 917 map: "the_between"
918 room: "Main Area" 918 room: "Plaza Entrance"
919 name: "PLAZA" 919 name: "PLAZA"
920 } 920 }
921 } 921 }
diff --git a/data/maps/the_between/connections.txtpb b/data/maps/the_between/connections.txtpb index e2e7dc8..4e2e9a5 100644 --- a/data/maps/the_between/connections.txtpb +++ b/data/maps/the_between/connections.txtpb
@@ -5,6 +5,47 @@ connections {
5} 5}
6connections { 6connections {
7 from_room: "Main Area" 7 from_room: "Main Area"
8 to_room: "B2 Room" 8 to_room: "B2 Front Room"
9 door { name: "B2 Door" } 9 door { name: "B2 Door" }
10} 10}
11connections {
12 from {
13 painting {
14 room: "B2 Front Room"
15 name: "EYES4"
16 }
17 }
18 to {
19 painting {
20 room: "B2 Back Room"
21 name: "EYES3"
22 }
23 }
24 oneway: true
25}
26connections {
27 from {
28 painting {
29 room: "B2 Front Room"
30 name: "EYES"
31 }
32 }
33 to {
34 painting {
35 room: "B2 Back Room"
36 name: "EYES2"
37 }
38 }
39 oneway: true
40}
41connections {
42 from_room: "B2 Back Room"
43 to_room: "B2 Front Room"
44 # via collecting B2
45 oneway: true
46}
47connections {
48 from_room: "Main Area"
49 to_room: "Plaza Entrance"
50 door { name: "Plaza Entrance" }
51}
diff --git a/data/maps/the_between/rooms/B2 Back Room.txtpb b/data/maps/the_between/rooms/B2 Back Room.txtpb new file mode 100644 index 0000000..132ba56 --- /dev/null +++ b/data/maps/the_between/rooms/B2 Back Room.txtpb
@@ -0,0 +1,17 @@
1name: "B2 Back Room"
2display_name: "B2 Room"
3letters {
4 key: "b"
5 level2: true
6 path: "Components/Collectables/collectable"
7}
8paintings {
9 name: "EYES2"
10 path: "Components/Paintings/eyes2"
11 orientation: "east"
12}
13paintings {
14 name: "EYES3"
15 path: "Components/Paintings/eyes3"
16 orientation: "west"
17}
diff --git a/data/maps/the_between/rooms/B2 Front Room.txtpb b/data/maps/the_between/rooms/B2 Front Room.txtpb new file mode 100644 index 0000000..02c09fb --- /dev/null +++ b/data/maps/the_between/rooms/B2 Front Room.txtpb
@@ -0,0 +1,12 @@
1name: "B2 Front Room"
2display_name: "B2 Room"
3paintings {
4 name: "EYES"
5 path: "Components/Paintings/eyes"
6 orientation: "east"
7}
8paintings {
9 name: "EYES4"
10 path: "Components/Paintings/eyes4"
11 orientation: "west"
12}
diff --git a/data/maps/the_between/rooms/B2 Room.txtpb b/data/maps/the_between/rooms/B2 Room.txtpb deleted file mode 100644 index aad5d15..0000000 --- a/data/maps/the_between/rooms/B2 Room.txtpb +++ /dev/null
@@ -1,8 +0,0 @@
1name: "B2 Room"
2display_name: "B2 Room"
3letters {
4 key: "b"
5 level2: true
6 path: "Components/Collectables/collectable"
7}
8# Uhh idk if the paintings and door in here should be randomized.
diff --git a/data/maps/the_between/rooms/Main Area.txtpb b/data/maps/the_between/rooms/Main Area.txtpb index 1e0e291..164493d 100644 --- a/data/maps/the_between/rooms/Main Area.txtpb +++ b/data/maps/the_between/rooms/Main Area.txtpb
@@ -201,9 +201,3 @@ ports {
201 path: "Components/Warps/worldport" 201 path: "Components/Warps/worldport"
202 orientation: "east" 202 orientation: "east"
203} 203}
204ports {
205 name: "PLAZA"
206 path: "Components/Warps/worldport4"
207 orientation: "north"
208 required_door { name: "Plaza Entrance" }
209}
diff --git a/data/maps/the_between/rooms/Plaza Entrance.txtpb b/data/maps/the_between/rooms/Plaza Entrance.txtpb new file mode 100644 index 0000000..b99081a --- /dev/null +++ b/data/maps/the_between/rooms/Plaza Entrance.txtpb
@@ -0,0 +1,7 @@
1name: "Plaza Entrance"
2display_name: "Main Area"
3ports {
4 name: "PLAZA"
5 path: "Components/Warps/worldport4"
6 orientation: "north"
7}
diff --git a/data/maps/the_colorful/metadata.txtpb b/data/maps/the_colorful/metadata.txtpb new file mode 100644 index 0000000..4c475a0 --- /dev/null +++ b/data/maps/the_colorful/metadata.txtpb
@@ -0,0 +1,2 @@
1# This has something to do with the FISH/FISHES proxy.
2excluded_nodes: "Components/panel_fake"
diff --git a/data/maps/the_entry/metadata.txtpb b/data/maps/the_entry/metadata.txtpb new file mode 100644 index 0000000..24d17bf --- /dev/null +++ b/data/maps/the_entry/metadata.txtpb
@@ -0,0 +1,10 @@
1# This is a debug warp to The Ancient and as far as I can tell there is no way
2# to access it.
3excluded_nodes: "Components/Warps/worldport-test"
4# Proxy stuff related to the Ctrl Tutorial.
5excluded_nodes: "Panels/Back Left/backleft_2_proxied_1"
6excluded_nodes: "Panels/Back Left/backleft_2_proxied_2"
7excluded_nodes: "Panels/Back Left/backleft_3_proxied_1"
8excluded_nodes: "Panels/Back Left/backleft_3_proxied_2"
9excluded_nodes: "Panels/Back Left/backleft_4_proxied_1"
10excluded_nodes: "Panels/Back Left/backleft_4_proxied_2"
diff --git a/data/maps/the_extravagant/metadata.txtpb b/data/maps/the_extravagant/metadata.txtpb new file mode 100644 index 0000000..36fc80a --- /dev/null +++ b/data/maps/the_extravagant/metadata.txtpb
@@ -0,0 +1,2 @@
1# This appears to be completely inaccessible.
2excluded_nodes: "Components/Warps/worldport"
diff --git a/data/maps/the_gallery/metadata.txtpb b/data/maps/the_gallery/metadata.txtpb new file mode 100644 index 0000000..c07cb5c --- /dev/null +++ b/data/maps/the_gallery/metadata.txtpb
@@ -0,0 +1,8 @@
1# These are the eyes in the foyer, and aren't normal paintings.
2excluded_nodes: "Components/Paintings/Starting/eye"
3excluded_nodes: "Components/Paintings/Starting/eye2"
4excluded_nodes: "Components/Paintings/Starting/eye3"
5excluded_nodes: "Components/Paintings/Starting/eye4"
6# This is the E that got removed but not deleted. It's above the ceiling and
7# cannot be accessed.
8excluded_nodes: "Components/Paintings/Starting/e"
diff --git a/data/maps/the_graveyard/metadata.txtpb b/data/maps/the_graveyard/metadata.txtpb new file mode 100644 index 0000000..fbac6ab --- /dev/null +++ b/data/maps/the_graveyard/metadata.txtpb
@@ -0,0 +1,5 @@
1# These really shouldn't be shuffled because it would make Black Ending trivial.
2excluded_nodes: "Components/Paintings/grave"
3excluded_nodes: "Components/Paintings/grave2"
4# I'll be real, I have no idea what this is.
5excluded_nodes: "Panels/panel_4"
diff --git a/data/maps/the_great/metadata.txtpb b/data/maps/the_great/metadata.txtpb new file mode 100644 index 0000000..d3b3018 --- /dev/null +++ b/data/maps/the_great/metadata.txtpb
@@ -0,0 +1,7 @@
1# This can't be shuffled because it is tilted.
2excluded_nodes: "Components/Paintings/u"
3# This can't be shuffled because it is on the ground.
4excluded_nodes: "Components/blare"
5# This is the fake HI panel that used to be in the Control Center entrance. It
6# is neither visible nor accessible.
7excluded_nodes: "Panels/General/entry_4"
diff --git a/data/maps/the_great/rooms/Back Area.txtpb b/data/maps/the_great/rooms/Back Area.txtpb index 0a8b168..013b0e2 100644 --- a/data/maps/the_great/rooms/Back Area.txtpb +++ b/data/maps/the_great/rooms/Back Area.txtpb
@@ -134,7 +134,7 @@ ports {
134} 134}
135ports { 135ports {
136 name: "TOWER" 136 name: "TOWER"
137 path: "Components/Warps/worldport10" 137 path: "Meshes/Blocks/Warps/worldport10"
138 orientation: "south" 138 orientation: "south"
139 required_door { name: "Tower Entrance" } 139 required_door { name: "Tower Entrance" }
140 # The reverse warp bypasses the door, so there needs to be two oneway connections. 140 # The reverse warp bypasses the door, so there needs to be two oneway connections.
diff --git a/data/maps/the_great/rooms/Daedalus Entrance.txtpb b/data/maps/the_great/rooms/Daedalus Entrance.txtpb index c7e71f0..f4c159a 100644 --- a/data/maps/the_great/rooms/Daedalus Entrance.txtpb +++ b/data/maps/the_great/rooms/Daedalus Entrance.txtpb
@@ -9,7 +9,7 @@ panels {
9} 9}
10ports { 10ports {
11 name: "DAEDALUS" 11 name: "DAEDALUS"
12 path: "Components/Warps/worldport8" 12 path: "Meshes/Blocks/Warps/worldport8"
13 orientation: "south" 13 orientation: "south"
14 required_door { name: "Daedalus Entrance" } 14 required_door { name: "Daedalus Entrance" }
15 # The reverse warp bypasses the door, so there needs to be two oneway connections. 15 # The reverse warp bypasses the door, so there needs to be two oneway connections.
diff --git a/data/maps/the_great/rooms/Main Area.txtpb b/data/maps/the_great/rooms/Main Area.txtpb index b562909..cf6285a 100644 --- a/data/maps/the_great/rooms/Main Area.txtpb +++ b/data/maps/the_great/rooms/Main Area.txtpb
@@ -121,27 +121,27 @@ panels {
121} 121}
122ports { 122ports {
123 name: "ENTRY" 123 name: "ENTRY"
124 path: "Components/Warps/worldport" 124 path: "Meshes/Blocks/Warps/worldport"
125 orientation: "south" 125 orientation: "south"
126} 126}
127ports { 127ports {
128 name: "KEEN" 128 name: "KEEN"
129 path: "Components/Warps/worldport6" 129 path: "Meshes/Blocks/Warps/worldport6"
130 orientation: "north" 130 orientation: "north"
131} 131}
132ports { 132ports {
133 name: "ORB" 133 name: "ORB"
134 path: "Components/Warps/worldport3" 134 path: "Meshes/Blocks/Warps/worldport3"
135 orientation: "north" 135 orientation: "north"
136} 136}
137ports { 137ports {
138 name: "LINEAR" 138 name: "LINEAR"
139 path: "Components/Warps/worldport15" 139 path: "Meshes/Blocks/Warps/worldport15"
140 orientation: "south" 140 orientation: "south"
141} 141}
142ports { 142ports {
143 name: "DIGITAL" 143 name: "DIGITAL"
144 path: "Components/Warps/worldport4" 144 path: "Meshes/Blocks/Warps/worldport4"
145 orientation: "down" 145 orientation: "down"
146 required_door { name: "Digital Entrance" } 146 required_door { name: "Digital Entrance" }
147} 147}
diff --git a/data/maps/the_great/rooms/Purple Room.txtpb b/data/maps/the_great/rooms/Purple Room.txtpb index 8edc789..ff9bd9a 100644 --- a/data/maps/the_great/rooms/Purple Room.txtpb +++ b/data/maps/the_great/rooms/Purple Room.txtpb
@@ -2,6 +2,6 @@ name: "Purple Room"
2display_name: "Main Area" 2display_name: "Main Area"
3ports { 3ports {
4 name: "DAEDALUS" 4 name: "DAEDALUS"
5 path: "Components/Warps/worldport18" 5 path: "Meshes/Blocks/Warps/worldport18"
6 orientation: "north" 6 orientation: "north"
7} 7}
diff --git a/data/maps/the_great/rooms/Salmon Room.txtpb b/data/maps/the_great/rooms/Salmon Room.txtpb index 9e29860..96efd1a 100644 --- a/data/maps/the_great/rooms/Salmon Room.txtpb +++ b/data/maps/the_great/rooms/Salmon Room.txtpb
@@ -2,6 +2,6 @@ name: "Salmon Room"
2display_name: "Main Area" 2display_name: "Main Area"
3ports { 3ports {
4 name: "BETWEEN" 4 name: "BETWEEN"
5 path: "Components/Warps/worldport11" 5 path: "Meshes/Blocks/Warps/worldport11"
6 orientation: "east" 6 orientation: "east"
7} 7}
diff --git a/data/maps/the_hinterlands/metadata.txtpb b/data/maps/the_hinterlands/metadata.txtpb new file mode 100644 index 0000000..b15ef2e --- /dev/null +++ b/data/maps/the_hinterlands/metadata.txtpb
@@ -0,0 +1,32 @@
1# I'm not currently planning on shuffling anything in here.
2excluded_nodes: "Components/Paintings/C"
3excluded_nodes: "Components/Paintings/E"
4excluded_nodes: "Components/Paintings/F/F_0"
5excluded_nodes: "Components/Paintings/F/F_1"
6excluded_nodes: "Components/Paintings/U"
7excluded_nodes: "Components/Paintings/U_1"
8excluded_nodes: "Components/Paintings/X"
9excluded_nodes: "Components/Paintings/z2_1"
10excluded_nodes: "Components/Paintings/z2_2"
11excluded_nodes: "Components/Warps/worldport3_deco"
12excluded_nodes: "Components/Warps/worldport3_deco2"
13excluded_nodes: "Components/Warps/worldport3_deco3"
14excluded_nodes: "Components/Warps/worldport3_deco4"
15excluded_nodes: "Components/Warps/worldport3_deco5"
16excluded_nodes: "Panels/G/court"
17excluded_nodes: "Panels/J/red"
18excluded_nodes: "Panels/K/no"
19excluded_nodes: "Panels/L/trick"
20excluded_nodes: "Panels/Misc/hi"
21excluded_nodes: "Panels/Misc/hint"
22excluded_nodes: "Panels/O/oh"
23excluded_nodes: "Panels/P/hint"
24excluded_nodes: "Panels/Q/gray"
25excluded_nodes: "Panels/V/grin"
26excluded_nodes: "Panels/Z/art"
27excluded_nodes: "Panels/k2/place"
28excluded_nodes: "Panels/m2/green"
29excluded_nodes: "Panels/o2/orange2"
30excluded_nodes: "Panels/p2/foxes"
31excluded_nodes: "Panels/u2/open"
32excluded_nodes: "Panels/v2/snipe"
diff --git a/data/maps/the_impressive/metadata.txtpb b/data/maps/the_impressive/metadata.txtpb new file mode 100644 index 0000000..3d4e2b5 --- /dev/null +++ b/data/maps/the_impressive/metadata.txtpb
@@ -0,0 +1,5 @@
1# These are apparently little eyes on the Green Eye panel pedestals? I don't
2# think they're ever visible in gameplay.
3excluded_nodes: "Meshes/eye"
4excluded_nodes: "Meshes/eye2"
5excluded_nodes: "Meshes/eye3"
diff --git a/data/maps/the_orb/metadata.txtpb b/data/maps/the_orb/metadata.txtpb new file mode 100644 index 0000000..920422d --- /dev/null +++ b/data/maps/the_orb/metadata.txtpb
@@ -0,0 +1,4 @@
1# These are inaccessible, and were probably just copy pasted from the other
2# rooms.
3excluded_nodes: "Components/Warps/worldport2"
4excluded_nodes: "Components/Warps/worldport3"
diff --git a/data/maps/the_orb/rooms/B Room.txtpb b/data/maps/the_orb/rooms/B Room.txtpb index 08dce6e..833c659 100644 --- a/data/maps/the_orb/rooms/B Room.txtpb +++ b/data/maps/the_orb/rooms/B Room.txtpb
@@ -10,7 +10,6 @@ paintings {
10 # TODO: This is too high up to enter. It's also a hint painting. 10 # TODO: This is too high up to enter. It's also a hint painting.
11 exit_only: true 11 exit_only: true
12} 12}
13# I believe worldport2 and worldport3 are completely inaccessible.
14# TODO: Should these two be independent for shuffling purposes, or always tied 13# TODO: Should these two be independent for shuffling purposes, or always tied
15# to the Main Area's port? 14# to the Main Area's port?
16ports { 15ports {
diff --git a/proto/human.proto b/proto/human.proto index 64231a2..6cec66d 100644 --- a/proto/human.proto +++ b/proto/human.proto
@@ -163,6 +163,11 @@ message HumanRoom {
163 repeated HumanMastery masteries = 8; 163 repeated HumanMastery masteries = 8;
164} 164}
165 165
166message HumanMap {
167 optional string display_name = 1;
168 repeated string excluded_nodes = 2;
169}
170
166message IdMappings { 171message IdMappings {
167 message RoomIds { 172 message RoomIds {
168 map<string, uint64> panels = 1; 173 map<string, uint64> panels = 1;
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 @@
1find_package(Protobuf REQUIRED) 1find_package(Protobuf REQUIRED)
2 2
3add_library(util 3add_library(util
4 godot_scene.cpp
4 identifiers.cpp 5 identifiers.cpp
5 naming.cpp 6 naming.cpp
6) 7)
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 @@
1#include "godot_scene.h"
2
3#include <absl/strings/str_split.h>
4
5#include <fstream>
6#include <sstream>
7#include <variant>
8
9namespace com::fourisland::lingo2_archipelago {
10
11namespace {
12
13class GodotSceneImpl : public GodotScene {
14 public:
15 GodotSceneImpl(std::map<std::string, GodotExtResource> ext_resources,
16 std::unique_ptr<GodotNode> root,
17 std::vector<std::unique_ptr<GodotNode>> descendents)
18 : ext_resources_(std::move(ext_resources)),
19 root_(std::move(root)),
20 descendents_(std::move(descendents)) {}
21
22 virtual const GodotExtResource* GetExtResource(const std::string& id) const {
23 auto it = ext_resources_.find(id);
24 if (it != ext_resources_.end()) {
25 return &it->second;
26 } else {
27 return nullptr;
28 }
29 }
30
31 virtual const GodotNode& GetRoot() const { return *root_; }
32
33 private:
34 std::map<std::string, GodotExtResource> ext_resources_;
35 std::unique_ptr<GodotNode> root_;
36 std::vector<std::unique_ptr<GodotNode>> descendents_;
37};
38
39struct Heading {
40 std::string type;
41
42 std::string id;
43 std::string path;
44 std::string resource_type;
45
46 std::string name;
47 std::string parent;
48 GodotInstanceType instance_type;
49};
50
51Heading ParseTscnHeading(absl::string_view line) {
52 std::string original_line(line);
53 Heading heading;
54
55 if (line[0] != '[') {
56 std::ostringstream errormsg;
57 errormsg << "Heading must start with [." << std::endl
58 << "Bad heading: " << original_line;
59 throw std::invalid_argument(errormsg.str());
60 }
61
62 line.remove_prefix(1);
63 int divider = line.find_first_of(" ]");
64 if (divider == std::string_view::npos) {
65 std::ostringstream errormsg;
66 errormsg << "Malformatted heading: " << line << std::endl
67 << "Original line: " << original_line;
68 throw std::invalid_argument(errormsg.str());
69 }
70
71 heading.type = std::string(line.substr(0, divider));
72 line.remove_prefix(divider + 1);
73
74 while (!line.empty()) {
75 divider = line.find_first_of("=");
76 if (divider == std::string_view::npos) {
77 std::ostringstream errormsg;
78 errormsg << "Malformatted heading: " << line << std::endl
79 << "Original line: " << original_line;
80 throw std::invalid_argument(errormsg.str());
81 }
82
83 std::string key(line.substr(0, divider));
84 line.remove_prefix(divider + 1);
85
86 if (line[0] == '"') {
87 line.remove_prefix(1);
88 divider = line.find_first_of("\"");
89
90 if (divider == std::string_view::npos) {
91 std::ostringstream errormsg;
92 errormsg << "Malformatted heading: " << line << std::endl
93 << "Original line: " << original_line;
94 throw std::invalid_argument(errormsg.str());
95 }
96
97 std::string strval(line.substr(0, divider));
98 line.remove_prefix(divider + 2);
99
100 if (key == "name") {
101 heading.name = strval;
102 } else if (key == "parent") {
103 heading.parent = strval;
104 } else if (key == "path") {
105 heading.path = strval;
106 } else if (key == "type") {
107 heading.resource_type = strval;
108 } else if (key == "id") {
109 heading.id = strval;
110 }
111 } else if (line[0] == 'S' || line[0] == 'E') {
112 GodotInstanceType rrval;
113 char internal = line[0];
114
115 line.remove_prefix(13); // SubResource("
116 divider = line.find_first_of("\"");
117
118 if (divider == std::string_view::npos) {
119 std::ostringstream errormsg;
120 errormsg << "Malformatted heading: " << line << std::endl
121 << "Original line: " << original_line;
122 throw std::invalid_argument(errormsg.str());
123 }
124
125 std::string refid = std::string(line.substr(0, divider));
126 line.remove_prefix(divider + 3);
127
128 GodotInstanceType instance_type;
129 if (internal == 'E') {
130 instance_type = GodotExtResourceRef{.id = refid};
131 } else {
132 // SubResource is not supported right now.
133 }
134
135 if (key == "instance") {
136 heading.instance_type = instance_type;
137 } else {
138 // Other keys aren't supported right now.
139 }
140 } else {
141 divider = line.find_first_of(" ]");
142
143 if (divider == std::string_view::npos) {
144 std::ostringstream errormsg;
145 errormsg << "Malformatted heading: " << line << std::endl
146 << "Original line: " << original_line;
147 throw std::invalid_argument(errormsg.str());
148 }
149
150 int numval = std::atoi(line.substr(0, divider).data());
151 line.remove_prefix(divider + 1);
152
153 // keyvals_[key] = numval;
154 }
155 }
156
157 return heading;
158}
159
160} // namespace
161
162void GodotNode::AddChild(GodotNode& child) {
163 children_[child.GetName()] = &child;
164 child.parent_ = this;
165}
166
167std::string GodotNode::GetPath() const {
168 if (parent_ == nullptr || parent_->GetName() == "") {
169 return name_;
170 } else {
171 return parent_->GetPath() + "/" + name_;
172 }
173}
174
175const GodotNode* GodotNode::GetNode(absl::string_view path) const {
176 std::vector<std::string> names = absl::StrSplit(path, "/");
177
178 auto it = children_.find(names[0]);
179 if (it == children_.end()) {
180 return nullptr;
181 } else {
182 if (names.size() == 1) {
183 return it->second;
184 } else {
185 path.remove_prefix(names[0].size() + 1);
186
187 return it->second->GetNode(path);
188 }
189 }
190}
191
192GodotNode* GodotNode::GetNode(absl::string_view path) {
193 return const_cast<GodotNode*>(
194 const_cast<const GodotNode*>(this)->GetNode(path));
195}
196
197std::unique_ptr<GodotScene> ReadGodotSceneFromFile(const std::string& path) {
198 std::map<std::string, GodotExtResource> ext_resources;
199 auto root = std::make_unique<GodotNode>("", GodotInstanceType{});
200 std::vector<std::unique_ptr<GodotNode>> descendents;
201
202 std::ifstream input(path);
203
204 std::string line;
205 bool section_started = false;
206 Heading cur_heading;
207 std::ostringstream cur_value;
208 bool value_started = false;
209 auto handle_end_of_section = [&]() {
210 section_started = false;
211 value_started = false;
212
213 if (cur_heading.type == "sub_resource") {
214 // sub_resources_[std::get<int>(cur_heading.GetKeyval("id"))] =
215 // {cur_heading, cur_value.str(), ""};
216 } else {
217 // other_.emplace_back(cur_heading, cur_value.str());
218 }
219
220 cur_value = {};
221 };
222 while (std::getline(input, line)) {
223 if (section_started && (line.empty() || line[0] == '[')) {
224 handle_end_of_section();
225 }
226 if (!line.empty() && line[0] == '[') {
227 Heading heading = ParseTscnHeading(line);
228 if (heading.type == "gd_scene") {
229 // file_descriptor_ = heading;
230 } else if (heading.type == "ext_resource") {
231 GodotExtResource ext_resource;
232 ext_resource.path = heading.path;
233 ext_resource.type = heading.resource_type;
234
235 ext_resources[heading.id] = ext_resource;
236 } else if (heading.type == "node") {
237 if (heading.parent != "") {
238 descendents.push_back(
239 std::make_unique<GodotNode>(heading.name, heading.instance_type));
240 GodotNode* child = descendents.back().get();
241
242 if (heading.parent == ".") {
243 root->AddChild(*child);
244 } else {
245 root->GetNode(heading.parent)->AddChild(*child);
246 }
247 }
248 } else {
249 cur_heading = heading;
250 section_started = true;
251 }
252 } else if (!line.empty()) {
253 if (value_started) {
254 cur_value << std::endl;
255 } else {
256 value_started = true;
257 }
258 cur_value << line;
259 }
260 }
261 if (section_started) {
262 handle_end_of_section();
263 }
264
265 return std::make_unique<GodotSceneImpl>(
266 std::move(ext_resources), std::move(root), std::move(descendents));
267}
268
269} // 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 @@
1#ifndef TOOLS_UTIL_TSCN_H_
2#define TOOLS_UTIL_TSCN_H_
3
4#include <absl/strings/string_view.h>
5
6#include <map>
7#include <memory>
8#include <string>
9#include <string_view>
10#include <variant>
11
12namespace com::fourisland::lingo2_archipelago {
13
14struct GodotExtResource {
15 std::string type;
16 std::string path;
17};
18
19struct GodotExtResourceRef {
20 std::string id;
21};
22
23using GodotInstanceType = std::variant<std::monostate, GodotExtResourceRef>;
24
25class GodotNode {
26 public:
27 GodotNode(std::string name, GodotInstanceType instance_type)
28 : name_(std::move(name)), instance_type_(std::move(instance_type)) {}
29
30 const std::string& GetName() const { return name_; }
31
32 const GodotInstanceType& GetInstanceType() const { return instance_type_; }
33
34 const GodotNode* GetParent() const { return parent_; }
35 GodotNode* GetParent() { return parent_; }
36
37 std::string GetPath() const;
38
39 void AddChild(GodotNode& child);
40
41 const GodotNode* GetNode(absl::string_view path) const;
42 GodotNode* GetNode(absl::string_view path);
43
44 const std::map<std::string, GodotNode*> GetChildren() const {
45 return children_;
46 }
47
48 private:
49 std::string name_;
50 GodotInstanceType instance_type_;
51
52 GodotNode* parent_ = nullptr;
53 std::map<std::string, GodotNode*> children_;
54};
55
56class GodotScene {
57 public:
58 virtual const GodotExtResource* GetExtResource(
59 const std::string& id) const = 0;
60 virtual const GodotNode& GetRoot() const = 0;
61};
62
63std::unique_ptr<GodotScene> ReadGodotSceneFromFile(const std::string& path);
64
65} // namespace com::fourisland::lingo2_archipelago
66
67#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 @@
1find_package(Protobuf REQUIRED) 1find_package(Protobuf REQUIRED)
2 2
3add_executable(validator 3add_executable(validator
4 godot_processor.cpp
4 human_processor.cpp 5 human_processor.cpp
5 main.cpp 6 main.cpp
6 validator.cpp 7 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 @@
1#include "godot_processor.h"
2
3#include <filesystem>
4#include <iostream>
5#include <memory>
6#include <set>
7
8#include "structs.h"
9#include "util/godot_scene.h"
10
11namespace com::fourisland::lingo2_archipelago {
12
13namespace {
14
15static const std::set<std::string> kImportantNodeTypes = {
16 "res://objects/nodes/panel.tscn", "res://objects/nodes/worldport.tscn",
17 "res://objects/nodes/keyHolder.tscn",
18 "res://objects/nodes/collectable.tscn"};
19
20class GodotProcessor {
21 public:
22 GodotProcessor(const std::string& repodir, CollectedInfo& info)
23 : repodir_(repodir), info_(info) {}
24
25 void Run() {
26 for (auto& [map_name, map_info] : info_.maps) {
27 ProcessMap(map_name, map_info);
28 }
29 }
30
31 void ProcessMap(const std::string& map_name, MapInfo& map_info) {
32 std::filesystem::path scene_path = std::filesystem::path(repodir_) /
33 "objects" / "scenes" /
34 (map_name + ".tscn");
35 std::string scene_path_str = scene_path.string();
36 std::cout << "Processing " << scene_path_str << std::endl;
37
38 std::unique_ptr<GodotScene> scene =
39 ReadGodotSceneFromFile(scene_path_str);
40
41 ProcessMapNode(*scene, scene->GetRoot(), map_info);
42 }
43
44 void ProcessMapNode(const GodotScene& scene, const GodotNode& node,
45 MapInfo& map_info) {
46 if (std::holds_alternative<GodotExtResourceRef>(node.GetInstanceType())) {
47 const GodotExtResourceRef& ext_resource_ref =
48 std::get<GodotExtResourceRef>(node.GetInstanceType());
49 const GodotExtResource* ext_resource =
50 scene.GetExtResource(ext_resource_ref.id);
51
52 if (ext_resource != nullptr &&
53 (kImportantNodeTypes.count(ext_resource->path) ||
54 ext_resource->path.starts_with("res://objects/meshes/paintings/"))) {
55 map_info.game_nodes[node.GetPath()].defined = true;
56 }
57 }
58
59 for (const auto& [child_name, child_node] : node.GetChildren()) {
60 ProcessMapNode(scene, *child_node, map_info);
61 }
62 }
63
64 private:
65 std::string repodir_;
66 CollectedInfo& info_;
67};
68
69} // namespace
70
71void ProcessGodotData(const std::string& repodir, CollectedInfo& info) {
72 GodotProcessor godot_processor(repodir, info);
73 godot_processor.Run();
74}
75
76} // 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 @@
1#ifndef TOOLS_VALIDATOR_GODOT_PROCESSOR_H_
2#define TOOLS_VALIDATOR_GODOT_PROCESSOR_H_
3
4#include <string>
5
6namespace com::fourisland::lingo2_archipelago {
7
8struct CollectedInfo;
9
10void ProcessGodotData(const std::string& repodir, CollectedInfo& info);
11
12} // namespace com::fourisland::lingo2_archipelago
13
14#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 {
55 void ProcessMap(std::filesystem::path path) { 55 void ProcessMap(std::filesystem::path path) {
56 std::string map_name = path.filename().string(); 56 std::string map_name = path.filename().string();
57 57
58 ProcessMetadataFile(path / "metadata.txtpb", map_name);
58 ProcessConnectionsFile(path / "connections.txtpb", map_name); 59 ProcessConnectionsFile(path / "connections.txtpb", map_name);
59 ProcessDoorsFile(path / "doors.txtpb", map_name); 60 ProcessDoorsFile(path / "doors.txtpb", map_name);
60 ProcessRooms(path / "rooms", map_name); 61 ProcessRooms(path / "rooms", map_name);
61 } 62 }
62 63
64 void ProcessMetadataFile(std::filesystem::path path,
65 const std::string& current_map_name) {
66 if (!std::filesystem::exists(path)) {
67 return;
68 }
69
70 MapInfo& map_info = info_.maps[current_map_name];
71
72 auto metadata = ReadMessageFromFile<HumanMap>(path.string());
73 for (const std::string& path : metadata.excluded_nodes()) {
74 map_info.game_nodes[path].uses++;
75 }
76 }
77
63 void ProcessRooms(std::filesystem::path path, 78 void ProcessRooms(std::filesystem::path path,
64 const std::string& current_map_name) { 79 const std::string& current_map_name) {
65 for (auto const& dir_entry : std::filesystem::directory_iterator(path)) { 80 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 @@
1#include "godot_processor.h"
1#include "human_processor.h" 2#include "human_processor.h"
2#include "structs.h" 3#include "structs.h"
3#include "validator.h" 4#include "validator.h"
@@ -5,10 +6,11 @@
5namespace com::fourisland::lingo2_archipelago { 6namespace com::fourisland::lingo2_archipelago {
6namespace { 7namespace {
7 8
8void Run(const std::string& mapdir) { 9void Run(const std::string& mapdir, const std::string& repodir) {
9 CollectedInfo info; 10 CollectedInfo info;
10 11
11 ProcessHumanData(mapdir, info); 12 ProcessHumanData(mapdir, info);
13 ProcessGodotData(repodir, info);
12 14
13 ValidateCollectedInfo(info); 15 ValidateCollectedInfo(info);
14} 16}
@@ -17,15 +19,16 @@ void Run(const std::string& mapdir) {
17} // namespace com::fourisland::lingo2_archipelago 19} // namespace com::fourisland::lingo2_archipelago
18 20
19int main(int argc, char** argv) { 21int main(int argc, char** argv) {
20 if (argc != 2) { 22 if (argc != 3) {
21 std::cout << "Incorrect argument count." << std::endl; 23 std::cout << "Incorrect argument count." << std::endl;
22 std::cout << "Usage: validator [path to map directory]" << std::endl; 24 std::cout << "Usage: validator [path to map directory] [path to Lingo 2 repository]" << std::endl;
23 return 1; 25 return 1;
24 } 26 }
25 27
26 std::string mapdir = argv[1]; 28 std::string mapdir = argv[1];
29 std::string repodir = argv[2];
27 30
28 com::fourisland::lingo2_archipelago::Run(mapdir); 31 com::fourisland::lingo2_archipelago::Run(mapdir, repodir);
29 32
30 return 0; 33 return 0;
31} 34}
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 {
21}; 21};
22 22
23struct GameNodeInfo { 23struct GameNodeInfo {
24 bool defined = false;
24 int uses = 0; 25 int uses = 0;
25}; 26};
26 27
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) {
14 if (node_info.uses > 1) { 14 if (node_info.uses > 1) {
15 std::cout << "Map " << map_name << " node " << node_path 15 std::cout << "Map " << map_name << " node " << node_path
16 << " is used in multiple places." << std::endl; 16 << " is used in multiple places." << std::endl;
17 } else if (node_info.uses == 0) {
18 std::cout << "Map " << map_name << " node " << node_path
19 << " is not used." << std::endl;
20 }
21
22 if (!node_info.defined) {
23 std::cout << "Map " << map_name << " node " << node_path
24 << " is not defined in the game file." << std::endl;
17 } 25 }
18 } 26 }
19} 27}