diff options
author | Star Rauchenberger <fefferburbia@gmail.com> | 2025-08-18 12:56:13 -0400 |
---|---|---|
committer | Star Rauchenberger <fefferburbia@gmail.com> | 2025-08-18 12:56:13 -0400 |
commit | 1ac21d4a67ddd211fda841aa6e368bc2cf52a3d6 (patch) | |
tree | bdcf651c156c27982e37bddb7cb7e0b09aa90d5a | |
parent | 15b8794bbe80be0bcf1f482674455efe002cec2c (diff) | |
download | lingo2-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.
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 | } |
6 | connections { | 6 | connections { |
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 | } |
11 | connections { | ||
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 | } | ||
26 | connections { | ||
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 | } | ||
41 | connections { | ||
42 | from_room: "B2 Back Room" | ||
43 | to_room: "B2 Front Room" | ||
44 | # via collecting B2 | ||
45 | oneway: true | ||
46 | } | ||
47 | connections { | ||
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 @@ | |||
1 | name: "B2 Back Room" | ||
2 | display_name: "B2 Room" | ||
3 | letters { | ||
4 | key: "b" | ||
5 | level2: true | ||
6 | path: "Components/Collectables/collectable" | ||
7 | } | ||
8 | paintings { | ||
9 | name: "EYES2" | ||
10 | path: "Components/Paintings/eyes2" | ||
11 | orientation: "east" | ||
12 | } | ||
13 | paintings { | ||
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 @@ | |||
1 | name: "B2 Front Room" | ||
2 | display_name: "B2 Room" | ||
3 | paintings { | ||
4 | name: "EYES" | ||
5 | path: "Components/Paintings/eyes" | ||
6 | orientation: "east" | ||
7 | } | ||
8 | paintings { | ||
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 @@ | |||
1 | name: "B2 Room" | ||
2 | display_name: "B2 Room" | ||
3 | letters { | ||
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 | } |
204 | ports { | ||
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 @@ | |||
1 | name: "Plaza Entrance" | ||
2 | display_name: "Main Area" | ||
3 | ports { | ||
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. | ||
2 | excluded_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. | ||
3 | excluded_nodes: "Components/Warps/worldport-test" | ||
4 | # Proxy stuff related to the Ctrl Tutorial. | ||
5 | excluded_nodes: "Panels/Back Left/backleft_2_proxied_1" | ||
6 | excluded_nodes: "Panels/Back Left/backleft_2_proxied_2" | ||
7 | excluded_nodes: "Panels/Back Left/backleft_3_proxied_1" | ||
8 | excluded_nodes: "Panels/Back Left/backleft_3_proxied_2" | ||
9 | excluded_nodes: "Panels/Back Left/backleft_4_proxied_1" | ||
10 | excluded_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. | ||
2 | excluded_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. | ||
2 | excluded_nodes: "Components/Paintings/Starting/eye" | ||
3 | excluded_nodes: "Components/Paintings/Starting/eye2" | ||
4 | excluded_nodes: "Components/Paintings/Starting/eye3" | ||
5 | excluded_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. | ||
8 | excluded_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. | ||
2 | excluded_nodes: "Components/Paintings/grave" | ||
3 | excluded_nodes: "Components/Paintings/grave2" | ||
4 | # I'll be real, I have no idea what this is. | ||
5 | excluded_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. | ||
2 | excluded_nodes: "Components/Paintings/u" | ||
3 | # This can't be shuffled because it is on the ground. | ||
4 | excluded_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. | ||
7 | excluded_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 | } |
135 | ports { | 135 | ports { |
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 | } |
10 | ports { | 10 | ports { |
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 | } |
122 | ports { | 122 | ports { |
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 | } |
127 | ports { | 127 | ports { |
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 | } |
132 | ports { | 132 | ports { |
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 | } |
137 | ports { | 137 | ports { |
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 | } |
142 | ports { | 142 | ports { |
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" | |||
2 | display_name: "Main Area" | 2 | display_name: "Main Area" |
3 | ports { | 3 | ports { |
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" | |||
2 | display_name: "Main Area" | 2 | display_name: "Main Area" |
3 | ports { | 3 | ports { |
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. | ||
2 | excluded_nodes: "Components/Paintings/C" | ||
3 | excluded_nodes: "Components/Paintings/E" | ||
4 | excluded_nodes: "Components/Paintings/F/F_0" | ||
5 | excluded_nodes: "Components/Paintings/F/F_1" | ||
6 | excluded_nodes: "Components/Paintings/U" | ||
7 | excluded_nodes: "Components/Paintings/U_1" | ||
8 | excluded_nodes: "Components/Paintings/X" | ||
9 | excluded_nodes: "Components/Paintings/z2_1" | ||
10 | excluded_nodes: "Components/Paintings/z2_2" | ||
11 | excluded_nodes: "Components/Warps/worldport3_deco" | ||
12 | excluded_nodes: "Components/Warps/worldport3_deco2" | ||
13 | excluded_nodes: "Components/Warps/worldport3_deco3" | ||
14 | excluded_nodes: "Components/Warps/worldport3_deco4" | ||
15 | excluded_nodes: "Components/Warps/worldport3_deco5" | ||
16 | excluded_nodes: "Panels/G/court" | ||
17 | excluded_nodes: "Panels/J/red" | ||
18 | excluded_nodes: "Panels/K/no" | ||
19 | excluded_nodes: "Panels/L/trick" | ||
20 | excluded_nodes: "Panels/Misc/hi" | ||
21 | excluded_nodes: "Panels/Misc/hint" | ||
22 | excluded_nodes: "Panels/O/oh" | ||
23 | excluded_nodes: "Panels/P/hint" | ||
24 | excluded_nodes: "Panels/Q/gray" | ||
25 | excluded_nodes: "Panels/V/grin" | ||
26 | excluded_nodes: "Panels/Z/art" | ||
27 | excluded_nodes: "Panels/k2/place" | ||
28 | excluded_nodes: "Panels/m2/green" | ||
29 | excluded_nodes: "Panels/o2/orange2" | ||
30 | excluded_nodes: "Panels/p2/foxes" | ||
31 | excluded_nodes: "Panels/u2/open" | ||
32 | excluded_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. | ||
3 | excluded_nodes: "Meshes/eye" | ||
4 | excluded_nodes: "Meshes/eye2" | ||
5 | excluded_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. | ||
3 | excluded_nodes: "Components/Warps/worldport2" | ||
4 | excluded_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? |
16 | ports { | 15 | ports { |
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 | ||
166 | message HumanMap { | ||
167 | optional string display_name = 1; | ||
168 | repeated string excluded_nodes = 2; | ||
169 | } | ||
170 | |||
166 | message IdMappings { | 171 | message 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 @@ | |||
1 | find_package(Protobuf REQUIRED) | 1 | find_package(Protobuf REQUIRED) |
2 | 2 | ||
3 | add_library(util | 3 | add_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 | |||
9 | namespace com::fourisland::lingo2_archipelago { | ||
10 | |||
11 | namespace { | ||
12 | |||
13 | class 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 | |||
39 | struct 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 | |||
51 | Heading 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 | |||
162 | void GodotNode::AddChild(GodotNode& child) { | ||
163 | children_[child.GetName()] = &child; | ||
164 | child.parent_ = this; | ||
165 | } | ||
166 | |||
167 | std::string GodotNode::GetPath() const { | ||
168 | if (parent_ == nullptr || parent_->GetName() == "") { | ||
169 | return name_; | ||
170 | } else { | ||
171 | return parent_->GetPath() + "/" + name_; | ||
172 | } | ||
173 | } | ||
174 | |||
175 | const 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 | |||
192 | GodotNode* GodotNode::GetNode(absl::string_view path) { | ||
193 | return const_cast<GodotNode*>( | ||
194 | const_cast<const GodotNode*>(this)->GetNode(path)); | ||
195 | } | ||
196 | |||
197 | std::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 | |||
12 | namespace com::fourisland::lingo2_archipelago { | ||
13 | |||
14 | struct GodotExtResource { | ||
15 | std::string type; | ||
16 | std::string path; | ||
17 | }; | ||
18 | |||
19 | struct GodotExtResourceRef { | ||
20 | std::string id; | ||
21 | }; | ||
22 | |||
23 | using GodotInstanceType = std::variant<std::monostate, GodotExtResourceRef>; | ||
24 | |||
25 | class 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 | |||
56 | class GodotScene { | ||
57 | public: | ||
58 | virtual const GodotExtResource* GetExtResource( | ||
59 | const std::string& id) const = 0; | ||
60 | virtual const GodotNode& GetRoot() const = 0; | ||
61 | }; | ||
62 | |||
63 | std::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 @@ | |||
1 | find_package(Protobuf REQUIRED) | 1 | find_package(Protobuf REQUIRED) |
2 | 2 | ||
3 | add_executable(validator | 3 | add_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 | |||
11 | namespace com::fourisland::lingo2_archipelago { | ||
12 | |||
13 | namespace { | ||
14 | |||
15 | static 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 | |||
20 | class 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 | |||
71 | void 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 | |||
6 | namespace com::fourisland::lingo2_archipelago { | ||
7 | |||
8 | struct CollectedInfo; | ||
9 | |||
10 | void 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 @@ | |||
5 | namespace com::fourisland::lingo2_archipelago { | 6 | namespace com::fourisland::lingo2_archipelago { |
6 | namespace { | 7 | namespace { |
7 | 8 | ||
8 | void Run(const std::string& mapdir) { | 9 | void 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 | ||
19 | int main(int argc, char** argv) { | 21 | int 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 | ||
23 | struct GameNodeInfo { | 23 | struct 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 | } |