about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--CHANGELOG.md85
-rw-r--r--CMakeLists.txt24
-rw-r--r--README.md4
-rw-r--r--VERSION2
-rw-r--r--VERSION.yaml31
-rw-r--r--assets/checked_owl.pngbin0 -> 262 bytes
-rw-r--r--assets/subway.pngbin184533 -> 232357 bytes
-rw-r--r--assets/subway.yaml600
-rw-r--r--src/achievements_pane.cpp15
-rw-r--r--src/achievements_pane.h6
-rw-r--r--src/ap_state.cpp211
-rw-r--r--src/ap_state.h27
-rw-r--r--src/area_popup.cpp196
-rw-r--r--src/area_popup.h38
-rw-r--r--src/connection_dialog.cpp44
-rw-r--r--src/connection_dialog.h6
-rw-r--r--src/game_data.cpp213
-rw-r--r--src/game_data.h13
-rw-r--r--src/global.cpp16
-rw-r--r--src/global.h2
-rw-r--r--src/godot_variant.cpp84
-rw-r--r--src/godot_variant.h28
-rw-r--r--src/icons.cpp22
-rw-r--r--src/icons.h25
-rw-r--r--src/ipc_dialog.cpp7
-rw-r--r--src/ipc_dialog.h2
-rw-r--r--src/ipc_state.cpp23
-rw-r--r--src/ipc_state.h2
-rw-r--r--src/items_pane.cpp145
-rw-r--r--src/items_pane.h33
-rw-r--r--src/log_dialog.cpp37
-rw-r--r--src/log_dialog.h24
-rw-r--r--src/logger.cpp42
-rw-r--r--src/logger.h6
-rw-r--r--src/main.cpp2
-rw-r--r--src/options_pane.cpp71
-rw-r--r--src/options_pane.h19
-rw-r--r--src/paintings_pane.cpp86
-rw-r--r--src/paintings_pane.h28
-rw-r--r--src/report_popup.cpp131
-rw-r--r--src/report_popup.h38
-rw-r--r--src/settings_dialog.cpp45
-rw-r--r--src/settings_dialog.h13
-rw-r--r--src/subway_map.cpp293
-rw-r--r--src/subway_map.h7
-rw-r--r--src/tracker_config.cpp10
-rw-r--r--src/tracker_config.h9
-rw-r--r--src/tracker_frame.cpp242
-rw-r--r--src/tracker_frame.h66
-rw-r--r--src/tracker_panel.cpp206
-rw-r--r--src/tracker_panel.h22
-rw-r--r--src/tracker_state.cpp203
-rw-r--r--src/tracker_state.h6
-rw-r--r--src/updater.cpp309
-rw-r--r--src/updater.h46
-rw-r--r--src/version.h2
-rw-r--r--src/windows.rc3
-rw-r--r--vcpkg.json1
m---------vendor/vcpkg0
m---------vendor/websocketpp0
61 files changed, 2700 insertions, 1174 deletions
diff --git a/.gitmodules b/.gitmodules index ebe016f..1a69477 100644 --- a/.gitmodules +++ b/.gitmodules
@@ -16,3 +16,6 @@
16[submodule "vendor/vcpkg"] 16[submodule "vendor/vcpkg"]
17 path = vendor/vcpkg 17 path = vendor/vcpkg
18 url = https://github.com/Microsoft/vcpkg.git 18 url = https://github.com/Microsoft/vcpkg.git
19[submodule "vendor/websocketpp"]
20 path = vendor/websocketpp
21 url = https://github.com/zaphoyd/websocketpp
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c94f10..e2444b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,90 @@
1# lingo-ap-tracker Releases 1# lingo-ap-tracker Releases
2 2
3## v2.0.2 - 2025-05-24
4
5- Fixed issue connecting to the Archipelago 0.6.2 RC server.
6
7Download:
8[lingo-ap-tracker-v2.0.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.2-win64.zip)<br/>
9Source: [v2.0.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.2)
10
11## v2.0.1 - 2025-04-06
12
13- The tracker now assumes postgame shuffle is enabled when the flag is not
14 present in slot data (as is the case with Archipelago 0.6.1 and earlier).
15 Players who have postgame shuffle disabled will unfortunately see locations
16 that are not in their world, until this problem can be fixed.
17
18Download:
19[lingo-ap-tracker-v2.0.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.1-win64.zip)<br/>
20Source: [v2.0.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.1)
21
22## v2.0.0 - 2025-04-01
23
24- Compatibility update for Archipelago 0.6.0.
25
26Download:
27[lingo-ap-tracker-v2.0.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.0-win64.zip)<br/>
28Source: [v2.0.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.0)
29
30## v1.0.0 - 2025-03-21
31
32After almost two years of development, we have finally hit version 1!
33
34- The subway map now uses Kinrah's updated map image, including fully separated
35 paintings for places like The Wondrous and Orange Tower Sixth Floor, as well
36 as indicators for the color items.
37- Paintings have friendly names now instead of the internal game IDs. They are
38 also distinguished from regular checks by using an owl icon instead of an eye.
39- The tracker is now able to read your solved panel state from the multiworld
40 (requires v5.3.0 of the client). This obsoletes save file parsing and
41 receiving panel solves by connecting to the game.
42- Numerous optimizations to reachability detection and rendering.
43- Added a pane that shows all items received, similar to the web tracker.
44- Added a pane that shows your currently revealed painting mapping, as well as a
45 button that reveals it all immediately.
46- Added a pane that shows your slot options.
47- Postgame shuffle being disabled is handled properly now, and removed locations
48 are no longer shown.
49- Improved support for high-DPI screens. The tracker should no longer appear
50 "blurry" on a high-DPI screen, and should also be able to react properly to
51 being moved between screens with different DPIs.
52- The update checker is now able to download and install tracker updates in
53 place.
54
55Download:
56[lingo-ap-tracker-v1.0.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v1.0.0-win64.zip)<br/>
57Source: [v1.0.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v1.0.0)
58
59## v0.12.3 - 2025-03-03
60
61- Fixed issue with non-ASCII connection details.
62
63Download:
64[lingo-ap-tracker-v0.12.3-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.3-win64.zip)<br/>
65Source: [v0.12.3](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.3)
66
67## v0.12.2 - 2025-02-10
68
69- Added a scrollbar to the subway map access requirement popups.
70- Fixed an issue with a small number of doors having too-strict access
71 requirements in vanilla or panels mode door shuffle.
72
73Download:
74[lingo-ap-tracker-v0.12.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.2-win64.zip)<br/>
75Source: [v0.12.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.2)
76
77## v0.12.1 - 2025-01-27
78
79- Fixed sunwarp mapping not showing up on metro map when sunwarp shuffle is
80 enabled.
81- Fixed metro map door requirements sometimes saying you cannot reach areas when
82 you really can.
83
84Download:
85[lingo-ap-tracker-v0.12.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.1-win64.zip)<br/>
86Source: [v0.12.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.1)
87
3## v0.12.0 - 2024-12-20 88## v0.12.0 - 2024-12-20
4 89
5- The tracker can now connect to a game of Lingo that is running the Archipelago 90- The tracker can now connect to a game of Lingo that is running the Archipelago
diff --git a/CMakeLists.txt b/CMakeLists.txt index f46c158..ef741fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -1,15 +1,15 @@
1cmake_minimum_required (VERSION 3.1) 1cmake_minimum_required (VERSION 3.20)
2project (lingo_ap_tracker) 2project (lingo_ap_tracker)
3 3
4if (MSVC) 4if (MSVC)
5set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") 5set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
6set(CMAKE_WIN32_EXECUTABLE true) 6set(CMAKE_WIN32_EXECUTABLE true)
7set(CMAKE_EXE_LINKER_FLAGS /MANIFEST:NO)
7endif(MSVC) 8endif(MSVC)
8 9
9find_package(wxWidgets CONFIG REQUIRED) 10find_package(wxWidgets CONFIG REQUIRED)
10find_package(OpenSSL REQUIRED) 11find_package(OpenSSL REQUIRED)
11find_package(yaml-cpp REQUIRED) 12find_package(yaml-cpp REQUIRED)
12find_package(websocketpp REQUIRED)
13find_package(fmt REQUIRED) 13find_package(fmt REQUIRED)
14 14
15include_directories( 15include_directories(
@@ -18,7 +18,7 @@ include_directories(
18 vendor/asio/asio/include 18 vendor/asio/asio/include
19 vendor/nlohmann 19 vendor/nlohmann
20 vendor/valijson/include 20 vendor/valijson/include
21 ${websocketpp_INCLUDE_DIRS} 21 vendor/websocketpp
22 vendor/wswrap/include 22 vendor/wswrap/include
23 ${yaml-cpp_INCLUDE_DIRS} 23 ${yaml-cpp_INCLUDE_DIRS}
24 ${OpenSSL_INCLUDE_DIRS} 24 ${OpenSSL_INCLUDE_DIRS}
@@ -32,7 +32,7 @@ include_directories(${SYSTEM_INCLUDE_DIR})
32 32
33link_directories(${openssl_LIBRARY_DIRS}) 33link_directories(${openssl_LIBRARY_DIRS})
34 34
35add_executable(lingo_ap_tracker 35set(SOURCE_FILES
36 "src/main.cpp" 36 "src/main.cpp"
37 "src/tracker_frame.cpp" 37 "src/tracker_frame.cpp"
38 "src/tracker_panel.cpp" 38 "src/tracker_panel.cpp"
@@ -48,14 +48,26 @@ add_executable(lingo_ap_tracker
48 "src/subway_map.cpp" 48 "src/subway_map.cpp"
49 "src/network_set.cpp" 49 "src/network_set.cpp"
50 "src/logger.cpp" 50 "src/logger.cpp"
51 "src/godot_variant.cpp"
52 "src/ipc_state.cpp" 51 "src/ipc_state.cpp"
53 "src/ipc_dialog.cpp" 52 "src/ipc_dialog.cpp"
53 "src/report_popup.cpp"
54 "src/updater.cpp"
55 "src/icons.cpp"
56 "src/paintings_pane.cpp"
57 "src/items_pane.cpp"
58 "src/options_pane.cpp"
59 "src/log_dialog.cpp"
54 "vendor/whereami/whereami.c" 60 "vendor/whereami/whereami.c"
55) 61)
62
63if (MSVC)
64list(APPEND SOURCE_FILES "src/windows.rc")
65endif(MSVC)
66
67add_executable(lingo_ap_tracker ${SOURCE_FILES})
56set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) 68set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
57set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON) 69set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON)
58target_link_libraries(lingo_ap_tracker PRIVATE fmt::fmt OpenSSL::SSL OpenSSL::Crypto websocketpp::websocketpp wx::core wx::base wx::net yaml-cpp::yaml-cpp) 70target_link_libraries(lingo_ap_tracker PRIVATE fmt::fmt OpenSSL::SSL OpenSSL::Crypto wx::core wx::base wx::net yaml-cpp::yaml-cpp)
59 71
60set(SRC_DIR "${CMAKE_SOURCE_DIR}/assets") 72set(SRC_DIR "${CMAKE_SOURCE_DIR}/assets")
61set(DST_DIR "${CMAKE_BINARY_DIR}/$<CONFIG>/assets") 73set(DST_DIR "${CMAKE_BINARY_DIR}/$<CONFIG>/assets")
diff --git a/README.md b/README.md index 1fbbc2f..cb3e90d 100644 --- a/README.md +++ b/README.md
@@ -25,3 +25,7 @@ To build the app:
256. (Optional) Build the application in release mode: `cmake --build --preset=x64-release-preset` 256. (Optional) Build the application in release mode: `cmake --build --preset=x64-release-preset`
26 26
27LL1.yaml and ids.yaml sometimes receive breaking changes that need to be kept in sync with the tracker. If the application crashes with an unknown error, try making sure that you are using the right versions of those files. In general, the main branch of the tracker will require config files from the latest Archipelago release. Branches may require config files from Archipelago main, or from pending pull requests. 27LL1.yaml and ids.yaml sometimes receive breaking changes that need to be kept in sync with the tracker. If the application crashes with an unknown error, try making sure that you are using the right versions of those files. In general, the main branch of the tracker will require config files from the latest Archipelago release. Branches may require config files from Archipelago main, or from pending pull requests.
28
29### Using Visual Studio
30
31If you're using Visual Studio on Windows, you can simplify the above process. You can clone the repository directly from Visual Studio, and it will automatically download the submodules as well as configure cmake and build the vcpkg packages. You will still have to do steps 2 and 3 manually. Then, look for the dropdown next to the green arrow and select `lingo_ap_tracker.exe` so that you can build it. You can build in release mode by choosing it from the preset dropdown.
diff --git a/VERSION b/VERSION index 3061e9e..b02d37b 100644 --- a/VERSION +++ b/VERSION
@@ -1 +1 @@
v0.12.0 \ No newline at end of file v2.0.2 \ No newline at end of file
diff --git a/VERSION.yaml b/VERSION.yaml new file mode 100644 index 0000000..8f86a39 --- /dev/null +++ b/VERSION.yaml
@@ -0,0 +1,31 @@
1version: v2.0.2
2packages:
3 win64:
4 url: "https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.2-win64.zip"
5 checksum: "dfb2fce2f5b14c09f4af7e7fcf6478e420a070db9fb23f9f9ad196f2db4b7518"
6 files:
7 - fmt.dll
8 - jpeg62.dll
9 - libcrypto-3-x64.dll
10 - liblzma.dll
11 - libpng16.dll
12 - libssl-3-x64.dll
13 - lingo_ap_tracker.exe
14 - pcre2-16.dll
15 - tiff.dll
16 - wxbase32u_net_vc_custom.dll
17 - wxbase32u_vc_custom.dll
18 - wxmsw32u_core_vc_custom.dll
19 - yaml-cpp.dll
20 - zlib1.dll
21 - assets/areas.yaml
22 - assets/checked.png
23 - assets/checked_owl.png
24 - assets/ids.yaml
25 - assets/lingo_map.png
26 - assets/LL1.yaml
27 - assets/owl.png
28 - assets/player.png
29 - assets/subway.png
30 - assets/subway.yaml
31 - assets/unchecked.png
diff --git a/assets/checked_owl.png b/assets/checked_owl.png new file mode 100644 index 0000000..f53ddd3 --- /dev/null +++ b/assets/checked_owl.png
Binary files differ
diff --git a/assets/subway.png b/assets/subway.png index 3860d2c..8128004 100644 --- a/assets/subway.png +++ b/assets/subway.png
Binary files differ
diff --git a/assets/subway.yaml b/assets/subway.yaml index b6f1e1c..8c0df38 100644 --- a/assets/subway.yaml +++ b/assets/subway.yaml
@@ -1,123 +1,117 @@
1--- 1---
2- pos: [1050, 954] 2- pos: [1051, 954]
3 room: Starting Room 3 room: Starting Room
4 door: Back Right Door 4 door: Back Right Door
5- pos: [986, 1034] 5- pos: [986, 1034]
6 room: Starting Room 6 room: Starting Room
7 door: Rhyme Room Entrance 7 door: Rhyme Room Entrance
8- pos: [990, 956] 8- pos: [996, 964]
9 special: starting_room_paintings # Early Color Hallways painting is a hardcoded special case 9 special: starting_room_overhead
10 paintings: 10 painting: arrows_painting
11 - arrows_painting 11- pos: [1044, 1012]
12- pos: [905, 841] 12 special: early_color_hallways
13 painting: color_hallways
14- pos: [915, 851]
13 room: Hedge Maze 15 room: Hedge Maze
14 door: Painting Shortcut 16 door: Painting Shortcut
15 paintings: 17 painting: garden_painting_tower2
16 - garden_painting_tower2
17 entrances: 18 entrances:
18 - garden_starting 19 - garden_starting
19- pos: [1066, 841] 20- pos: [1076, 851]
20 room: Courtyard 21 room: Courtyard
21 door: Painting Shortcut 22 door: Painting Shortcut
22 paintings: 23 painting: flower_painting_8
23 - flower_painting_8
24 entrances: 24 entrances:
25 - flower_starting 25 - flower_starting
26- pos: [905, 895] 26- pos: [915, 905]
27 room: The Wondrous (Doorknob) 27 room: The Wondrous (Doorknob)
28 door: Painting Shortcut 28 door: Painting Shortcut
29 paintings: 29 painting: symmetry_painting_a_starter
30 - symmetry_painting_a_starter
31 entrances: 30 entrances:
32 - symmetry_starting 31 - symmetry_starting
33- pos: [1066, 868] 32- pos: [1076, 905]
34 room: Outside The Bold 33 room: Outside The Bold
35 door: Painting Shortcut 34 door: Painting Shortcut
36 paintings: 35 painting: pencil_painting6
37 - pencil_painting6
38 entrances: 36 entrances:
39 - pencil_starting 37 - pencil_starting
40- pos: [1066, 895] 38- pos: [1076, 878]
41 room: Outside The Undeterred 39 room: Outside The Undeterred
42 door: Painting Shortcut 40 door: Painting Shortcut
43 paintings: 41 painting: blueman_painting_3
44 - blueman_painting_3
45 entrances: 42 entrances:
46 - blueman_starting 43 - blueman_starting
47- pos: [905, 868] 44- pos: [915, 878]
48 room: Outside The Agreeable 45 room: Outside The Agreeable
49 door: Painting Shortcut 46 door: Painting Shortcut
50 paintings: 47 painting: eyes_yellow_painting2
51 - eyes_yellow_painting2
52 entrances: 48 entrances:
53 - street_starting 49 - street_starting
54- pos: [1211, 879] 50- pos: [1211, 879]
55 room: Hidden Room 51 room: Hidden Room
56 door: Dead End Door 52 door: Dead End Door
57- pos: [1291, 906] 53- pos: [1292, 906]
58 room: Hidden Room 54 room: Hidden Room
59 door: Knight Night Entrance 55 door: Knight Night Entrance
60- pos: [1103, 980] 56- pos: [1104, 981]
61 room: Hidden Room 57 room: Hidden Room
62 door: Seeker Entrance 58 door: Seeker Entrance
63- pos: [1173, 980] 59- pos: [1174, 981]
64 room: Hidden Room 60 room: Hidden Room
65 door: Rhyme Room Entrance 61 door: Rhyme Room Entrance
66- pos: [1116, 939] 62- pos: [1114, 937]
67 paintings: 63 painting: owl_painting
68 - owl_painting
69 entrances: 64 entrances:
70 - owl_hidden 65 - owl_hidden
71- pos: [986, 793] 66- pos: [986, 793]
72 room: Second Room 67 room: Second Room
73 door: Exit Door 68 door: Exit Door
74- pos: [798, 584] 69- pos: [799, 584]
75 room: Hub Room 70 room: Hub Room
76 door: Crossroads Entrance 71 door: Crossroads Entrance
77- pos: [932, 665] 72- pos: [933, 665]
78 room: Hub Room 73 room: Hub Room
79 door: Tenacious Entrance 74 door: Tenacious Entrance
80- pos: [1361, 578] 75- pos: [1361, 579]
81 room: Hub Room 76 room: Hub Room
82 door: Shortcut to Hedge Maze 77 door: Shortcut to Hedge Maze
83- pos: [1312, 841] 78- pos: [1313, 842]
84 room: Hub Room 79 room: Hub Room
85 door: Near RAT Door 80 door: Near RAT Door
86- pos: [1371, 729] 81- pos: [1372, 729]
87 room: Hub Room 82 room: Hub Room
88 door: Traveled Entrance 83 door: Traveled Entrance
89- pos: [1313, 686] 84- pos: [1312, 696]
90 paintings: 85 painting: maze_painting
91 - maze_painting
92 exits: 86 exits:
93 - green_owl 87 - green_owl
94 - green_numbers 88 - green_numbers
95- pos: [1172, 760] 89- pos: [1174, 761]
96 sunwarp: 90 sunwarp:
97 dots: 1 91 dots: 1
98 type: enter 92 type: enter
99- pos: [1302, 638] 93- pos: [1302, 638]
100 room: Outside The Undeterred 94 room: Outside The Undeterred
101 door: Fours 95 door: Fours
102- pos: [1243, 819] 96- pos: [1243, 820]
103 room: Outside The Undeterred 97 room: Outside The Undeterred
104 door: Fours 98 door: Fours
105- pos: [1276, 819] 99- pos: [1276, 820]
106 room: Number Hunt 100 room: Number Hunt
107 door: Eights 101 door: Eights
108- pos: [1263, 867] 102- pos: [1253, 889]
109 paintings: 103 painting: smile_painting_6
110 - smile_painting_6
111 entrances: 104 entrances:
112 - smiley_deadend 105 - smiley_deadend
113- pos: [1012, 1086] 106- pos: [1013, 1087]
114 sunwarp: 107 sunwarp:
115 dots: 6 108 dots: 6
116 type: final 109 type: final
117- pos: [938, 1002] 110- pos: [948, 1012]
118 room: Pilgrim Antechamber 111 room: Pilgrim Antechamber
119 door: Sun Painting 112 door: Sun Painting
120 special: sun_painting 113 special: sun_painting
114 painting: pilgrim_painting2
121- pos: [1053, 1090] 115- pos: [1053, 1090]
122 invisible: true 116 invisible: true
123 special: sun_painting_exit 117 special: sun_painting_exit
@@ -136,7 +130,7 @@
136- pos: [932, 477] 130- pos: [932, 477]
137 room: Crossroads 131 room: Crossroads
138 door: Tenacious Entrance 132 door: Tenacious Entrance
139- pos: [638, 477] 133- pos: [509, 477]
140 room: Crossroads 134 room: Crossroads
141 door: Discerning Entrance 135 door: Discerning Entrance
142- pos: [905, 290] 136- pos: [905, 290]
@@ -145,15 +139,14 @@
145- pos: [894, 423] 139- pos: [894, 423]
146 room: Crossroads 140 room: Crossroads
147 door: Words Sword Door 141 door: Words Sword Door
148- pos: [632, 643] 142- pos: [718, 557]
149 room: Crossroads 143 room: Crossroads
150 door: Eye Wall 144 door: Eye Wall
151- pos: [638, 520] 145- pos: [509, 434]
152 room: Crossroads 146 room: Crossroads
153 door: Roof Access 147 door: Roof Access
154- pos: [756, 400] 148- pos: [712, 514]
155 paintings: 149 painting: smile_painting_4
156 - smile_painting_4
157 entrances: 150 entrances:
158 - smiley_crossroads 151 - smiley_crossroads
159- pos: [878, 509] 152- pos: [878, 509]
@@ -168,13 +161,12 @@
168 door: Exit 161 door: Exit
169- pos: [986, 290] 162- pos: [986, 290]
170 room: Number Hunt 163 room: Number Hunt
171 door: Eights 164 door: Nines
172- pos: [954, 247] 165- pos: [954, 247]
173 room: Amen Name Area 166 room: Amen Name Area
174 door: Exit 167 door: Exit
175- pos: [954, 222] 168- pos: [985, 235]
176 paintings: 169 painting: west_afar
177 - west_afar
178- pos: [986, 697] 170- pos: [986, 697]
179 room: The Tenacious 171 room: The Tenacious
180 door: Shortcut to Hub Room 172 door: Shortcut to Hub Room
@@ -205,21 +197,19 @@
205- pos: [1216, 525] 197- pos: [1216, 525]
206 room: Outside The Agreeable 198 room: Outside The Agreeable
207 door: Agreeable Entrance 199 door: Agreeable Entrance
208- pos: [1138, 287] 200- pos: [1156, 299]
209 paintings: 201 painting: eyes_yellow_painting
210 - eyes_yellow_painting
211 exits: 202 exits:
212 - street_starting 203 - street_starting
213- pos: [1088, 385] 204- pos: [1088, 385]
214 sunwarp: 205 sunwarp:
215 dots: 6 206 dots: 6
216 type: enter 207 type: enter
217- pos: [1195, 450] 208- pos: [1195, 445]
218 room: Compass Room 209 room: Compass Room
219 door: Lookout Entrance 210 door: Lookout Entrance
220- pos: [1214, 457] 211- pos: [1226, 481]
221 paintings: 212 painting: pencil_painting7
222 - pencil_painting7
223 entrances: 213 entrances:
224 - pencil_compass 214 - pencil_compass
225- pos: [1196, 417] 215- pos: [1196, 417]
@@ -248,9 +238,8 @@
248- pos: [1714, 434] 238- pos: [1714, 434]
249 room: Hedge Maze 239 room: Hedge Maze
250 door: Observant Entrance 240 door: Observant Entrance
251- pos: [1477, 343] 241- pos: [1526, 401]
252 paintings: 242 painting: garden_painting_tower
253 - garden_painting_tower
254 exits: 243 exits:
255 - garden_starting 244 - garden_starting
256- pos: [1565, 311] 245- pos: [1565, 311]
@@ -262,45 +251,48 @@
262- pos: [1414, 209] 251- pos: [1414, 209]
263 room: The Observant 252 room: The Observant
264 door: Backside Door 253 door: Backside Door
265- pos: [1624, 188] 254- pos: [1592, 188]
266 room: The Observant 255 room: The Observant
267 door: Stairs 256 door: Stairs
268- pos: [1667, 686] 257- pos: [1694, 659]
269 room: The Incomparable 258 room: The Incomparable
270 door: Eight Door 259 door: Eight Door
271- pos: [1784, 569] 260- pos: [1799, 583]
272 paintings: 261 painting: crown_painting
273 - crown_painting
274 exits: 262 exits:
275 - crown_tower6 263 - crown_tower6
276- pos: [1653, 717] 264- pos: [1644, 685]
277 paintings: 265 painting: eight_painting2
278 - eight_painting2
279 entrances: 266 entrances:
280 - eight_alcove 267 - eight_alcove
281- pos: [1653, 662] 268- pos: [1660, 664]
282 paintings: 269 painting: eight_painting
283 - eight_painting
284 exits: 270 exits:
285 - eight_alcove 271 - eight_alcove
286- pos: [697, 1471] 272- pos: [695, 1471]
287 room: Orange Tower 273 room: Orange Tower
288 door: Second Floor 274 door: Second Floor
289- pos: [633, 1406] 275 tilted: true
276- pos: [631, 1406]
290 room: Orange Tower 277 room: Orange Tower
291 door: Third Floor 278 door: Third Floor
292- pos: [570, 1343] 279 tilted: true
280- pos: [567, 1341]
293 room: Orange Tower 281 room: Orange Tower
294 door: Fourth Floor 282 door: Fourth Floor
295- pos: [504, 1279] 283 tilted: true
284- pos: [502, 1277]
296 room: Orange Tower 285 room: Orange Tower
297 door: Fifth Floor 286 door: Fifth Floor
298- pos: [440, 1215] 287 tilted: true
288- pos: [438, 1213]
299 room: Orange Tower 289 room: Orange Tower
300 door: Sixth Floor 290 door: Sixth Floor
301- pos: [379, 1153] 291 tilted: true
292- pos: [378, 1152]
302 room: Orange Tower 293 room: Orange Tower
303 door: Seventh Floor 294 door: Seventh Floor
295 tilted: true
304- pos: [905, 793] 296- pos: [905, 793]
305 room: Orange Tower First Floor 297 room: Orange Tower First Floor
306 door: Shortcut to Hub Room 298 door: Shortcut to Hub Room
@@ -320,7 +312,7 @@
320- pos: [722, 1439] 312- pos: [722, 1439]
321 tags: 313 tags:
322 - tower2_undeterred 314 - tower2_undeterred
323- pos: [533, 1375] 315- pos: [549, 1375]
324 tags: 316 tags:
325 - tower3_tower3 317 - tower3_tower3
326- pos: [662, 1375] 318- pos: [662, 1375]
@@ -377,10 +369,10 @@
377- pos: [1173, 1248] 369- pos: [1173, 1248]
378 room: Orange Tower Third Floor 370 room: Orange Tower Third Floor
379 door: Rhyme Room Entrance 371 door: Rhyme Room Entrance
380- pos: [1270, 1231] 372- pos: [1274, 1253]
381 paintings: 373 painting: arrows_painting_6
382 - arrows_painting_6 374- pos: [1293, 1234]
383 - flower_painting_5 375 painting: flower_painting_5
384- pos: [1216, 1216] 376- pos: [1216, 1216]
385 sunwarp: 377 sunwarp:
386 dots: 2 378 dots: 2
@@ -396,77 +388,82 @@
396 sunwarp: 388 sunwarp:
397 dots: 5 389 dots: 5
398 type: enter 390 type: enter
399- pos: [877, 155] 391- pos: [878, 155]
400 room: Number Hunt 392 room: Number Hunt
401 door: Eights 393 door: Eights
402- pos: [844, 134] 394- pos: [862, 144]
403 paintings: 395 painting: smile_painting_8
404 - smile_painting_8
405 entrances: 396 entrances:
406 - smiley_hotcrusts 397 - smiley_hotcrusts
407- pos: [797, 155] 398- pos: [798, 155]
408 sunwarp: 399 sunwarp:
409 dots: 2 400 dots: 2
410 type: enter 401 type: enter
411- pos: [679, 985] 402- pos: [680, 986]
412 room: Number Hunt 403 room: Number Hunt
413 door: Nines 404 door: Nines
414- pos: [723, 953] 405- pos: [723, 953]
415 room: Orange Tower Fifth Floor 406 room: Orange Tower Fifth Floor
416 door: Welcome Back 407 door: Welcome Back
417- pos: [683, 944] 408- pos: [690, 937]
418 paintings: 409 painting: east_afar
419 - east_afar 410- pos: [535, 1205]
420- pos: [548, 1221] 411 painting: hi_solved_painting3
421 paintings: 412- pos: [1574, 1424]
422 - hi_solved_painting3 413 painting: hi_solved_painting2
423- pos: [1574, 1425] 414- pos: [316, 1269]
424 paintings: 415 painting: arrows_painting_10
425 - hi_solved_painting2 416- pos: [332, 1253]
426- pos: [411, 1186] 417 painting: scenery_painting_5d_2
427 paintings: 418- pos: [347, 1237]
428 - arrows_painting_10 419 painting: panda_painting_2
429 - owl_painting_3
430 - clock_painting
431 - scenery_painting_5d_2
432 - symmetry_painting_b_7
433 - panda_painting_2
434 - crown_painting2
435 - colors_painting2
436 - cherry_painting2
437 - hi_solved_painting
438 entrances: 420 entrances:
439 - owl_tower6
440 - clock_tower6
441 - panda_tower6 421 - panda_tower6
442 - crown_tower6 422- pos: [363, 1221]
443 - apple_tower6 423 painting: colors_painting2
424- pos: [380, 1205]
425 painting: clock_painting
426 entrances:
427 - clock_tower6
428- pos: [396, 1221]
429 painting: hi_solved_painting
444 exits: 430 exits:
445 - hi_scientific 431 - hi_scientific
446- pos: [349, 1124] 432- pos: [380, 1237]
447 paintings: 433 painting: crown_painting2
448 - map_painting2 434 entrances:
435 - crown_tower6
436- pos: [363, 1253]
437 painting: owl_painting_3
438 entrances:
439 - owl_tower6
440- pos: [347, 1269]
441 painting: symmetry_painting_b_7
442- pos: [332, 1285]
443 painting: cherry_painting2
444 entrances:
445 - apple_tower6
446- pos: [360, 1135]
447 # TODO: there isn't really a spot for this one
448 painting: map_painting2
449- pos: [436, 1159] 449- pos: [436, 1159]
450 room: Orange Tower Seventh Floor 450 room: Orange Tower Seventh Floor
451 door: Mastery 451 door: Mastery
452- pos: [544, 1159] 452- pos: [535, 1146]
453 paintings: 453 painting: arrows_painting_11
454 - arrows_painting_11
455- pos: [498, 284] 454- pos: [498, 284]
456 room: Courtyard 455 room: Courtyard
457 door: Green Barrier 456 door: Green Barrier
458- pos: [556, 233] 457- pos: [589, 230]
459 paintings: 458 painting: flower_painting_7
460 - flower_painting_7
461 exits: 459 exits:
462 - flower_starting 460 - flower_starting
463 - flower_arrow 461 - flower_arrow
464- pos: [600, 332] 462- pos: [600, 332]
465 room: Number Hunt 463 room: Number Hunt
466 door: Nines 464 door: Nines
467- pos: [579, 350] 465- pos: [589, 369]
468 paintings: 466 painting: blueman_painting
469 - blueman_painting
470 entrances: 467 entrances:
471 - blueman_courtyard 468 - blueman_courtyard
472- pos: [530, 310] 469- pos: [530, 310]
@@ -502,9 +499,8 @@
502- pos: [922, 107] 499- pos: [922, 107]
503 room: The Colorful (Gray) 500 room: The Colorful (Gray)
504 door: Progress Door 501 door: Progress Door
505- pos: [967, 107] 502- pos: [969, 96]
506 paintings: 503 painting: arrows_painting_12
507 - arrows_painting_12
508- pos: [878, 954] 504- pos: [878, 954]
509 room: Welcome Back Area 505 room: Welcome Back Area
510 door: Shortcut to Starting Room 506 door: Shortcut to Starting Room
@@ -537,10 +533,10 @@
537- pos: [1737, 1053] 533- pos: [1737, 1053]
538 entrances: 534 entrances:
539 - gallery_wb 535 - gallery_wb
540- pos: [1690, 268] 536- pos: [1784, 395]
541 entrances: 537 entrances:
542 - observant_wb 538 - observant_wb
543- pos: [250, 604] 539- pos: [122, 350]
544 entrances: 540 entrances:
545 - scientific_wb 541 - scientific_wb
546- pos: [1553, 1467] 542- pos: [1553, 1467]
@@ -549,17 +545,19 @@
549- pos: [1478, 498] 545- pos: [1478, 498]
550 room: Owl Hallway 546 room: Owl Hallway
551 door: Shortcut to Hedge Maze 547 door: Shortcut to Hedge Maze
552- pos: [1480, 551] 548- pos: [1467, 535]
553 paintings: 549 painting: arrows_painting_8
554 - arrows_painting_8 550- pos: [1467, 562]
555 - maze_painting_2 551 painting: maze_painting_2
556 - owl_painting_2
557 - clock_painting_4
558 entrances: 552 entrances:
559 - green_owl 553 - green_owl
554- pos: [1510, 535]
555 painting: owl_painting_2
560 exits: 556 exits:
561 - owl_hidden 557 - owl_hidden
562 - owl_tower6 558 - owl_tower6
559- pos: [1510, 562]
560 painting: clock_painting_4
563- pos: [1478, 938] 561- pos: [1478, 938]
564 room: Number Hunt 562 room: Number Hunt
565 door: Sevens 563 door: Sevens
@@ -584,40 +582,38 @@
584- pos: [1511, 841] 582- pos: [1511, 841]
585 room: Outside The Initiated 583 room: Outside The Initiated
586 door: Initiated Entrance 584 door: Initiated Entrance
587- pos: [1141, 1441] 585- pos: [1071, 1441]
588 room: Orange Tower Third Floor 586 room: Orange Tower Third Floor
589 door: Orange Barrier 587 door: Orange Barrier
590- pos: [1173, 1441] 588- pos: [1104, 1441]
591 room: Outside The Initiated 589 room: Outside The Initiated
592 door: Green Barrier 590 door: Green Barrier
593- pos: [1206, 1441] 591- pos: [1137, 1441]
594 room: Outside The Initiated 592 room: Outside The Initiated
595 door: Purple Barrier 593 door: Purple Barrier
596- pos: [1189, 1355] 594- pos: [1120, 1355]
597 room: Outside The Initiated 595 room: Outside The Initiated
598 door: Entrance 596 door: Entrance
599- pos: [1580, 729] 597- pos: [1580, 729]
600 room: The Incomparable 598 room: The Incomparable
601 door: "Eight Door (Outside The Initiated)" 599 door: "Eight Door (Outside The Initiated)"
602- pos: [1530, 938] 600- pos: [1548, 948]
603 paintings: 601 painting: clock_painting_5
604 - clock_painting_5
605 entrances: 602 entrances:
606 - clock_initiated 603 - clock_initiated
607- pos: [1546, 938] 604- pos: [1575, 969]
608 paintings: 605 painting: arrows_painting_2
609 - clock_painting_2 606- pos: [1575, 926]
610 - arrows_painting_2 607 painting: clock_painting_2
611 exits: 608 exits:
612 - clock_tower6 609 - clock_tower6
613 - clock_initiated 610 - clock_initiated
614- pos: [1579, 813] 611- pos: [1580, 814]
615 sunwarp: 612 sunwarp:
616 dots: 3 613 dots: 3
617 type: exit 614 type: exit
618- pos: [1444, 896] 615- pos: [1462, 915]
619 paintings: 616 painting: smile_painting_1
620 - smile_painting_1
621 entrances: 617 entrances:
622 - smiley_initiated 618 - smiley_initiated
623- pos: [1430, 691] 619- pos: [1430, 691]
@@ -626,18 +622,25 @@
626- pos: [1468, 728] 622- pos: [1468, 728]
627 room: The Traveled 623 room: The Traveled
628 door: Color Hallways Entrance 624 door: Color Hallways Entrance
629- pos: [1533, 707] 625- pos: [1535, 644]
630 tags: 626 tags:
631 - red_ch 627 - red_ch
628- pos: [1535, 667]
629 tags:
632 - blue_ch 630 - blue_ch
631- pos: [1535, 689]
632 tags:
633 - yellow_ch 633 - yellow_ch
634- pos: [1535, 711]
635 tags:
634 - green_ch 636 - green_ch
637- pos: [1499, 678]
635 exits: 638 exits:
636 - early_ch 639 - early_ch
637- pos: [1567, 1264] 640- pos: [1567, 1264]
638 tags: 641 tags:
639 - red_ch 642 - red_ch
640- pos: [150, 808] 643- pos: [150, 748]
641 tags: 644 tags:
642 - blue_ch 645 - blue_ch
643- pos: [626, 371] 646- pos: [626, 371]
@@ -655,21 +658,21 @@
655- pos: [1468, 1377] 658- pos: [1468, 1377]
656 room: Outside The Bold 659 room: Outside The Bold
657 door: Bold Entrance 660 door: Bold Entrance
658- pos: [1425, 1358] 661- pos: [1446, 1344]
659 paintings: 662 painting: pencil_painting2
660 - pencil_painting2
661 - north_missing2
662 exits: 663 exits:
663 - pencil_compass 664 - pencil_compass
664 - pencil_starting 665 - pencil_starting
665 - pencil_directional 666 - pencil_directional
667- pos: [1580, 1344]
668 painting: north_missing2
666- pos: [1334, 1419] 669- pos: [1334, 1419]
667 room: Outside The Bold 670 room: Outside The Bold
668 door: Steady Entrance 671 door: Steady Entrance
669- pos: [445, 1048] 672- pos: [445, 1048]
670 exits: 673 exits:
671 - undeterred_artistic 674 - undeterred_artistic
672- pos: [279, 1071] 675- pos: [273, 1071]
673 room: Number Hunt 676 room: Number Hunt
674 door: Zero Door 677 door: Zero Door
675- pos: [338, 1071] 678- pos: [338, 1071]
@@ -684,17 +687,15 @@
684- pos: [242, 1071] 687- pos: [242, 1071]
685 room: Outside The Undeterred 688 room: Outside The Undeterred
686 door: Undeterred Entrance 689 door: Undeterred Entrance
687- pos: [60, 1017] 690- pos: [149, 937]
688 paintings: 691 painting: blueman_painting_2
689 - blueman_painting_2
690 exits: 692 exits:
691 - blueman_courtyard 693 - blueman_courtyard
692 - blueman_starting 694 - blueman_starting
693- pos: [402, 1012] 695- pos: [412, 1017]
694 room: Outside The Undeterred 696 room: Outside The Undeterred
695 door: Green Painting 697 door: Green Painting
696 paintings: 698 painting: maze_painting_3
697 - maze_painting_3
698 entrances: 699 entrances:
699 - green_numbers 700 - green_numbers
700- pos: [134, 1007] 701- pos: [134, 1007]
@@ -704,7 +705,7 @@
704- pos: [719, 1039] 705- pos: [719, 1039]
705 room: Outside The Undeterred 706 room: Outside The Undeterred
706 door: Challenge Entrance 707 door: Challenge Entrance
707- pos: [483, 1039] 708- pos: [456, 1039]
708 room: Outside The Undeterred 709 room: Outside The Undeterred
709 door: Number Hunt 710 door: Number Hunt
710- pos: [563, 1071] 711- pos: [563, 1071]
@@ -725,11 +726,10 @@
725- pos: [525, 1002] 726- pos: [525, 1002]
726 room: Number Hunt 727 room: Number Hunt
727 door: Door to Directional Gallery 728 door: Door to Directional Gallery
728- pos: [659, 1014] 729- pos: [572, 1017]
729 room: Number Hunt 730 room: Number Hunt
730 door: Eights 731 door: Eights
731 paintings: 732 painting: smile_painting_5
732 - smile_painting_5
733 entrances: 733 entrances:
734 - smiley_numbers 734 - smiley_numbers
735- pos: [557, 953] 735- pos: [557, 953]
@@ -747,87 +747,67 @@
747- pos: [268, 825] 747- pos: [268, 825]
748 room: Directional Gallery 748 room: Directional Gallery
749 door: Yellow Barrier 749 door: Yellow Barrier
750- pos: [231, 681] 750- pos: [214, 563]
751 room: Number Hunt 751 room: Number Hunt
752 door: Sixes 752 door: Sixes
753- pos: [242, 980] 753- pos: [242, 980]
754 room: Directional Gallery 754 room: Directional Gallery
755 door: Shortcut to The Undeterred 755 door: Shortcut to The Undeterred
756- pos: [351, 927] 756- pos: [364, 942]
757 paintings: 757 painting: boxes_painting
758 - boxes_painting
759 entrances: 758 entrances:
760 - lattice_directional 759 - lattice_directional
761- pos: [272, 927] 760- pos: [278, 942]
762 paintings: 761 painting: smile_painting_7
763 - smile_painting_7
764 entrances: 762 entrances:
765 - smiley_directional 763 - smiley_directional
766- pos: [214, 822] 764- pos: [278, 803]
767 paintings: 765 painting: cherry_painting
768 - cherry_painting
769 entrances: 766 entrances:
770 - apple_directional 767 - apple_directional
771- pos: [266, 735] 768- pos: [262, 573]
772 room: Number Hunt 769 room: Number Hunt
773 door: Sixes 770 door: Sixes
774 paintings: 771 painting: pencil_painting3
775 - pencil_painting3
776 entrances: 772 entrances:
777 - pencil_directional 773 - pencil_directional
778- pos: [215, 735] 774- pos: [278, 685]
779 paintings: 775 painting: flower_painting_4
780 - flower_painting_4
781- pos: [626, 851] 776- pos: [626, 851]
782 sunwarp: 777 sunwarp:
783 dots: 6 778 dots: 6
784 type: exit 779 type: exit
785- pos: [1141, 1441]
786 room: Orange Tower Third Floor
787 door: Orange Barrier
788- pos: [1174, 1441]
789 room: Outside The Initiated
790 door: Green Barrier
791- pos: [1205, 1441]
792 room: Outside The Initiated
793 door: Purple Barrier
794- pos: [1334, 1377] 780- pos: [1334, 1377]
795 room: Color Hunt 781 room: Color Hunt
796 door: Shortcut to The Steady 782 door: Shortcut to The Steady
797- pos: [1280, 1375] 783- pos: [1312, 1333]
798 paintings: 784 painting: arrows_painting_7
799 - arrows_painting_7 785- pos: [1226, 1333]
800- pos: [1233, 1321]
801 room: Outside The Initiated 786 room: Outside The Initiated
802 door: Entrance 787 door: Entrance
803 paintings: 788 painting: fruitbowl_painting3
804 - fruitbowl_painting3 789- pos: [1260, 1323]
805- pos: [1290, 1323]
806 sunwarp: 790 sunwarp:
807 dots: 5 791 dots: 5
808 type: exit 792 type: exit
809- pos: [1189, 1356] 793- pos: [1092, 1333]
810 room: Outside The Initiated 794 painting: colors_painting
811 door: Entrance
812- pos: [1154, 1332]
813 paintings:
814 - colors_painting
815- pos: [1640, 1260] 795- pos: [1640, 1260]
816 room: The Bearer 796 room: The Bearer
817 door: Backside Door 797 door: Backside Door
818- pos: [1468, 1287] 798- pos: [1468, 1287]
819 room: The Bearer 799 room: The Bearer
820 door: Entrance 800 door: Entrance
821- pos: [1430, 1232] 801- pos: [1431, 1233]
822 room: Number Hunt 802 room: Number Hunt
823 door: Sixes 803 door: Sixes
824- pos: [1388, 1152] 804- pos: [1388, 1152]
825 room: Bearer Side Area 805 room: Bearer Side Area
826 door: Shortcut to Tower 806 door: Shortcut to Tower
827- pos: [1273, 1442] 807- pos: [1264, 1430]
828 paintings: 808 painting: pencil_painting5
829 - pencil_painting5 809- pos: [1291, 1430]
830 - pencil_painting4 810 painting: pencil_painting4
831- pos: [1355, 1092] 811- pos: [1355, 1092]
832 room: Knight Night (Final) 812 room: Knight Night (Final)
833 door: Exit 813 door: Exit
@@ -838,9 +818,8 @@
838 # Complex case, because this is also blocked by Knight Night (Final) - Exit 818 # Complex case, because this is also blocked by Knight Night (Final) - Exit
839 room: Number Hunt 819 room: Number Hunt
840 door: Sevens 820 door: Sevens
841- pos: [1653, 101] 821- pos: [1687, 117]
842 paintings: 822 painting: smile_painting_9
843 - smile_painting_9
844 exits: 823 exits:
845 - smiley_crossroads 824 - smiley_crossroads
846 - smiley_deadend 825 - smiley_deadend
@@ -850,88 +829,113 @@
850 - smiley_initiated 829 - smiley_initiated
851 - smiley_gallery 830 - smiley_gallery
852 - smiley_theysee 831 - smiley_theysee
853- pos: [1656, 139] 832- pos: [1677, 161]
854 room: The Artistic (Smiley) 833 room: The Artistic (Smiley)
855 door: Door to Panda 834 door: Door to Panda
856- pos: [1711, 140] 835- pos: [1711, 140]
857 entrances: 836 entrances:
858 - undeterred_artistic 837 - undeterred_artistic
859- pos: [1653, 169] 838- pos: [1687, 224]
860 paintings: 839 painting: panda_painting_3
861 - panda_painting_3
862 exits: 840 exits:
863 - panda_tower6 841 - panda_tower6
864 - panda_hallway 842 - panda_hallway
865- pos: [1708, 171] 843- pos: [1731, 215]
866 room: The Artistic (Panda) 844 room: The Artistic (Panda)
867 door: Door to Lattice 845 door: Door to Lattice
868- pos: [1761, 169] 846- pos: [1794, 224]
869 paintings: 847 painting: boxes_painting2
870 - boxes_painting2
871 exits: 848 exits:
872 - lattice_directional 849 - lattice_directional
873- pos: [1762, 139] 850- pos: [1785, 161]
874 room: The Artistic (Lattice) 851 room: The Artistic (Lattice)
875 door: Door to Apple 852 door: Door to Apple
876- pos: [1761, 101] 853- pos: [1794, 117]
877 paintings: 854 painting: cherry_painting3
878 - cherry_painting3
879 exits: 855 exits:
880 - apple_tower6 856 - apple_tower6
881 - apple_directional 857 - apple_directional
882- pos: [1708, 107] 858- pos: [1731, 107]
883 room: The Artistic (Apple) 859 room: The Artistic (Apple)
884 door: Door to Smiley 860 door: Door to Smiley
885- pos: [370, 681] 861- pos: [370, 563]
886 room: Number Hunt 862 room: Number Hunt
887 door: Eights 863 door: Eights
888- pos: [411, 685] 864- pos: [637, 605]
889 paintings: 865 painting: eye_painting
890 - eye_painting_2 866 entrances:
891 - smile_painting_2 867 - crossroads_eyewall
868- pos: [610, 605]
869 exits:
870 - crossroads_eyewall
871- pos: [417, 573]
872 painting: eye_painting_2
873 exits:
874 - crossroads_eyewall
875- pos: [342, 573]
876 painting: smile_painting_2
892 entrances: 877 entrances:
893 - smiley_theysee 878 - smiley_theysee
894- pos: [310, 750] 879- pos: [311, 750]
895 room: The Eyes They See 880 room: The Eyes They See
896 door: Exit 881 door: Exit
897- pos: [334, 798] 882- pos: [348, 803]
898 paintings: 883 painting: arrows_painting_5
899 - arrows_painting_5 884- pos: [370, 751]
900- pos: [370, 792]
901 room: Outside The Wondrous 885 room: Outside The Wondrous
902 door: Wondrous Entrance 886 door: Wondrous Entrance
903- pos: [367, 752] 887- pos: [428, 696]
904 paintings: 888 painting: symmetry_painting_a_1
905 - symmetry_painting_a_1
906 - symmetry_painting_b_1
907 - symmetry_painting_a_3
908 - symmetry_painting_a_5
909 - symmetry_painting_b_4
910 - symmetry_painting_a_2
911 - symmetry_painting_b_2
912 - symmetry_painting_a_6
913 - symmetry_painting_b_6
914 exits: 889 exits:
915 - symmetry_starting 890 - symmetry_starting
916- pos: [407, 755] 891 - symmetry_a_chandelier
892 - symmetry_a_table
893- pos: [428, 749]
894 painting: symmetry_painting_b_1
895 entrances:
896 - symmetry_b_table
897- pos: [471, 696]
898 painting: symmetry_painting_a_3
899- pos: [538, 699]
900 painting: symmetry_painting_a_5
901 entrances:
902 - symmetry_a_chandelier
903- pos: [562, 728]
904 painting: symmetry_painting_b_4
905- pos: [422, 647]
906 painting: symmetry_painting_a_2
907 entrances:
908 - symmetry_a_table
909- pos: [364, 648]
910 painting: symmetry_painting_b_2
911 exits:
912 - symmetry_b_table
913 - symmetry_b_fire
914- pos: [449, 647]
915 painting: symmetry_painting_a_6
916- pos: [508, 647]
917 painting: symmetry_painting_b_6
918 entrances:
919 - symmetry_b_fire
920- pos: [558, 665]
921 room: The Wondrous
922 door: Exit
923- pos: [535, 647]
917 room: The Wondrous 924 room: The Wondrous
918 door: Exit 925 door: Exit
919 paintings: 926 painting: arrows_painting_9
920 - arrows_painting_9 927- pos: [610, 674]
921- pos: [449, 755] 928 painting: flower_painting_6
922 paintings:
923 - flower_painting_6
924 entrances: 929 entrances:
925 - flower_arrow 930 - flower_arrow
926- pos: [1101, 222] 931- pos: [1156, 262]
927 paintings: 932 painting: panda_painting
928 - panda_painting
929 entrances: 933 entrances:
930 - panda_hallway 934 - panda_hallway
931- pos: [1152, 209] 935- pos: [1152, 209]
932 room: Hallway Room (1) 936 room: Hallway Room (1)
933 door: Exit 937 door: Exit
934- pos: [1189, 170] 938- pos: [1190, 171]
935 room: Hallway Room (2) 939 room: Hallway Room (2)
936 door: Exit 940 door: Exit
937- pos: [1238, 124] 941- pos: [1238, 124]
@@ -946,16 +950,17 @@
946- pos: [1415, 140] 950- pos: [1415, 140]
947 room: Number Hunt 951 room: Number Hunt
948 door: Nines 952 door: Nines
949- pos: [1458, 133] 953- pos: [1424, 85]
950 paintings: 954 painting: south_afar
951 - south_afar
952- pos: [826, 1452] 955- pos: [826, 1452]
953 room: Outside The Wanderer 956 room: Outside The Wanderer
954 door: Wanderer Entrance 957 door: Wanderer Entrance
958 tilted: true
955- pos: [763, 1465] 959- pos: [763, 1465]
956 room: Outside The Wanderer 960 room: Outside The Wanderer
957 door: Tower Entrance 961 door: Tower Entrance
958- pos: [1655, 1151] 962 tilted: true
963- pos: [1656, 1152]
959 room: Number Hunt 964 room: Number Hunt
960 door: Eights 965 door: Eights
961- pos: [1623, 1044] 966- pos: [1623, 1044]
@@ -973,15 +978,18 @@
973- pos: [1511, 1119] 978- pos: [1511, 1119]
974 room: Art Gallery 979 room: Art Gallery
975 door: Exit 980 door: Exit
976- pos: [1654, 1116] 981- pos: [1730, 1162]
977 paintings: 982 painting: flower_painting_2
978 - smile_painting_3 983- pos: [1730, 1189]
979 - flower_painting_2 984 painting: map_painting
980 - scenery_painting_0a 985- pos: [1698, 1189]
981 - map_painting 986 painting: smile_painting_3
982 - fruitbowl_painting4
983 entrances: 987 entrances:
984 - smiley_gallery 988 - smiley_gallery
989- pos: [1714, 1215]
990 painting: fruitbowl_painting4
991- pos: [1714, 1242]
992 painting: scenery_painting_0a
985- pos: [1120, 1286] 993- pos: [1120, 1286]
986 room: Rhyme Room (Smiley) 994 room: Rhyme Room (Smiley)
987 door: Door to Target 995 door: Door to Target
@@ -1000,9 +1008,8 @@
1000- pos: [1120, 1195] 1008- pos: [1120, 1195]
1001 room: Rhyme Room (Circle) 1009 room: Rhyme Room (Circle)
1002 door: Door to Smiley 1010 door: Door to Smiley
1003- pos: [1118, 1137] 1011- pos: [1130, 1124]
1004 paintings: 1012 painting: arrows_painting_3
1005 - arrows_painting_3
1006- pos: [1050, 1142] 1013- pos: [1050, 1142]
1007 room: Rhyme Room (Looped Square) 1014 room: Rhyme Room (Looped Square)
1008 door: Door to Circle 1015 door: Door to Circle
@@ -1015,26 +1022,41 @@
1015- pos: [852, 1200] 1022- pos: [852, 1200]
1016 room: Rhyme Room (Target) 1023 room: Rhyme Room (Target)
1017 door: Door to Cross 1024 door: Door to Cross
1018- pos: [850, 1138] 1025- pos: [862, 1124]
1019 paintings: 1026 painting: arrows_painting_4
1020 - arrows_painting_4
1021- pos: [1592, 1442] 1027- pos: [1592, 1442]
1022 room: Room Room 1028 room: Room Room
1023 door: Cellar Exit 1029 door: Cellar Exit
1024- pos: [1570, 938] 1030- pos: [1623, 938]
1025 room: Outside The Wise 1031 room: Outside The Wise
1026 door: Wise Entrance 1032 door: Wise Entrance
1027- pos: [1653, 935] 1033- pos: [1665, 920]
1028 paintings: 1034 painting: clock_painting_3
1029 - clock_painting_3 1035- pos: [241, 348]
1030- pos: [369, 605]
1031 room: Outside The Scientific 1036 room: Outside The Scientific
1032 door: Scientific Entrance 1037 door: Scientific Entrance
1033- pos: [294, 602] 1038- pos: [176, 326]
1034 paintings: 1039 painting: hi_solved_painting4
1035 - hi_solved_painting4
1036 entrances: 1040 entrances:
1037 - hi_scientific 1041 - hi_scientific
1038- pos: [814, 1001] 1042- pos: [815, 1002]
1039 room: Challenge Room 1043 room: Challenge Room
1040 door: Welcome Door 1044 door: Welcome Door
1045- pos: [104, 1208]
1046 special: color_black
1047- pos: [104, 1249]
1048 special: color_red
1049- pos: [104, 1290]
1050 special: color_yellow
1051- pos: [104, 1330]
1052 special: color_blue
1053- pos: [104, 1371]
1054 special: color_purple
1055- pos: [104, 1411]
1056 special: color_orange
1057- pos: [104, 1451]
1058 special: color_green
1059- pos: [104, 1491]
1060 special: color_brown
1061- pos: [104, 1531]
1062 special: color_gray
diff --git a/src/achievements_pane.cpp b/src/achievements_pane.cpp index 8ec3727..d23c434 100644 --- a/src/achievements_pane.cpp +++ b/src/achievements_pane.cpp
@@ -8,23 +8,24 @@ AchievementsPane::AchievementsPane(wxWindow* parent)
8 AppendColumn("Achievement"); 8 AppendColumn("Achievement");
9 9
10 for (int panel_id : GD_GetAchievementPanels()) { 10 for (int panel_id : GD_GetAchievementPanels()) {
11 achievement_names_.push_back(GD_GetPanel(panel_id).achievement_name); 11 const Panel& panel = GD_GetPanel(panel_id);
12 achievements_.emplace_back(panel.achievement_name, panel.solve_index);
12 } 13 }
13 14
14 std::sort(std::begin(achievement_names_), std::end(achievement_names_)); 15 std::sort(std::begin(achievements_), std::end(achievements_));
15 16
16 for (int i = 0; i < achievement_names_.size(); i++) { 17 for (int i = 0; i < achievements_.size(); i++) {
17 InsertItem(i, achievement_names_.at(i)); 18 InsertItem(i, std::get<0>(achievements_.at(i)));
18 } 19 }
19 20
20 SetColumnWidth(0, wxLIST_AUTOSIZE); 21 SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
21 22
22 UpdateIndicators(); 23 UpdateIndicators();
23} 24}
24 25
25void AchievementsPane::UpdateIndicators() { 26void AchievementsPane::UpdateIndicators() {
26 for (int i = 0; i < achievement_names_.size(); i++) { 27 for (int i = 0; i < achievements_.size(); i++) {
27 if (AP_HasAchievement(achievement_names_.at(i))) { 28 if (AP_IsPanelSolved(std::get<1>(achievements_.at(i)))) {
28 SetItemTextColour(i, *wxBLACK); 29 SetItemTextColour(i, *wxBLACK);
29 } else { 30 } else {
30 SetItemTextColour(i, *wxRED); 31 SetItemTextColour(i, *wxRED);
diff --git a/src/achievements_pane.h b/src/achievements_pane.h index ac88cac..941b5e3 100644 --- a/src/achievements_pane.h +++ b/src/achievements_pane.h
@@ -9,6 +9,10 @@
9 9
10#include <wx/listctrl.h> 10#include <wx/listctrl.h>
11 11
12#include <string>
13#include <tuple>
14#include <vector>
15
12class AchievementsPane : public wxListView { 16class AchievementsPane : public wxListView {
13 public: 17 public:
14 explicit AchievementsPane(wxWindow* parent); 18 explicit AchievementsPane(wxWindow* parent);
@@ -16,7 +20,7 @@ class AchievementsPane : public wxListView {
16 void UpdateIndicators(); 20 void UpdateIndicators();
17 21
18 private: 22 private:
19 std::vector<std::string> achievement_names_; 23 std::vector<std::tuple<std::string, int>> achievements_; // name, solve index
20}; 24};
21 25
22#endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file 26#endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index 4ac0cce..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -10,6 +10,7 @@
10#include <any> 10#include <any>
11#include <apclient.hpp> 11#include <apclient.hpp>
12#include <apuuid.hpp> 12#include <apuuid.hpp>
13#include <bitset>
13#include <chrono> 14#include <chrono>
14#include <exception> 15#include <exception>
15#include <filesystem> 16#include <filesystem>
@@ -28,8 +29,8 @@
28#include "tracker_state.h" 29#include "tracker_state.h"
29 30
30constexpr int AP_MAJOR = 0; 31constexpr int AP_MAJOR = 0;
31constexpr int AP_MINOR = 4; 32constexpr int AP_MINOR = 6;
32constexpr int AP_REVISION = 5; 33constexpr int AP_REVISION = 1;
33 34
34constexpr const char* CERT_STORE_PATH = "cacert.pem"; 35constexpr const char* CERT_STORE_PATH = "cacert.pem";
35constexpr int ITEM_HANDLING = 7; // <- all 36constexpr int ITEM_HANDLING = 7; // <- all
@@ -37,8 +38,24 @@ constexpr int ITEM_HANDLING = 7; // <- all
37constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds 38constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds
38constexpr int CONNECTION_BACKOFF_INTERVAL = 100; 39constexpr int CONNECTION_BACKOFF_INTERVAL = 100;
39 40
41constexpr int PANEL_COUNT = 803;
42constexpr int PANEL_BITFIELD_LENGTH = 48;
43constexpr int PANEL_BITFIELDS = 17;
44
40namespace { 45namespace {
41 46
47const std::set<long> kNonProgressionItems = {
48 444409, // :)
49 444575, // The Feeling of Being Lost
50 444576, // Wanderlust
51 444577, // Empty White Hallways
52 444410, // Slowness Trap
53 444411, // Iceland Trap
54 444412, // Atbash Trap
55 444413, // Puzzle Skip
56 444680, // Speed Boost
57};
58
42struct APState { 59struct APState {
43 // Initialized on main thread 60 // Initialized on main thread
44 bool initialized = false; 61 bool initialized = false;
@@ -67,10 +84,12 @@ struct APState {
67 std::set<int64_t> checked_locations; 84 std::set<int64_t> checked_locations;
68 std::map<std::string, std::any> data_storage; 85 std::map<std::string, std::any> data_storage;
69 std::optional<std::tuple<int, int>> player_pos; 86 std::optional<std::tuple<int, int>> player_pos;
87 std::bitset<PANEL_COUNT> solved_panels;
70 88
71 DoorShuffleMode door_shuffle_mode = kNO_DOORS; 89 DoorShuffleMode door_shuffle_mode = kNO_DOORS;
72 bool group_doors = false; 90 bool group_doors = false;
73 bool color_shuffle = false; 91 bool color_shuffle = false;
92 PanelShuffleMode panel_shuffle_mode = kNO_PANELS;
74 bool painting_shuffle = false; 93 bool painting_shuffle = false;
75 int mastery_requirement = 21; 94 int mastery_requirement = 21;
76 int level_2_requirement = 223; 95 int level_2_requirement = 223;
@@ -82,6 +101,7 @@ struct APState {
82 bool pilgrimage_allows_paintings = false; 101 bool pilgrimage_allows_paintings = false;
83 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; 102 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
84 bool sunwarp_shuffle = false; 103 bool sunwarp_shuffle = false;
104 bool postgame_shuffle = true;
85 105
86 std::map<std::string, std::string> painting_mapping; 106 std::map<std::string, std::string> painting_mapping;
87 std::set<std::string> painting_codomain; 107 std::set<std::string> painting_codomain;
@@ -128,10 +148,12 @@ struct APState {
128 checked_locations.clear(); 148 checked_locations.clear();
129 data_storage.clear(); 149 data_storage.clear();
130 player_pos = std::nullopt; 150 player_pos = std::nullopt;
151 solved_panels.reset();
131 victory_data_storage_key.clear(); 152 victory_data_storage_key.clear();
132 door_shuffle_mode = kNO_DOORS; 153 door_shuffle_mode = kNO_DOORS;
133 group_doors = false; 154 group_doors = false;
134 color_shuffle = false; 155 color_shuffle = false;
156 panel_shuffle_mode = kNO_PANELS;
135 painting_shuffle = false; 157 painting_shuffle = false;
136 painting_mapping.clear(); 158 painting_mapping.clear();
137 painting_codomain.clear(); 159 painting_codomain.clear();
@@ -146,6 +168,7 @@ struct APState {
146 sunwarp_access = kSUNWARP_ACCESS_NORMAL; 168 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
147 sunwarp_shuffle = false; 169 sunwarp_shuffle = false;
148 sunwarp_mapping.clear(); 170 sunwarp_mapping.clear();
171 postgame_shuffle = true;
149 } 172 }
150 173
151 apclient->set_room_info_handler( 174 apclient->set_room_info_handler(
@@ -200,24 +223,13 @@ struct APState {
200 return checked_locations.count(location_id); 223 return checked_locations.count(location_id);
201 } 224 }
202 225
203 bool HasCheckedHuntPanel(int location_id) {
204 std::lock_guard state_guard(state_mutex);
205
206 std::string key =
207 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
208 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
209 }
210
211 bool HasItem(int item_id, int quantity) { 226 bool HasItem(int item_id, int quantity) {
212 return inventory.count(item_id) && inventory.at(item_id) >= quantity; 227 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
213 } 228 }
214 229
215 bool HasAchievement(const std::string& name) { 230 bool HasItemSafe(int item_id, int quantity) {
216 std::lock_guard state_guard(state_mutex); 231 std::lock_guard state_guard(state_mutex);
217 232 return HasItem(item_id, quantity);
218 std::string key =
219 fmt::format("{}Achievement|{}", data_storage_prefix, name);
220 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
221 } 233 }
222 234
223 const std::set<std::string>& GetCheckedPaintings() { 235 const std::set<std::string>& GetCheckedPaintings() {
@@ -241,7 +253,21 @@ struct APState {
241 checked_paintings.count(painting_mapping.at(painting_id))); 253 checked_paintings.count(painting_mapping.at(painting_id)));
242 } 254 }
243 255
244 std::string GetItemName(int id) { return apclient->get_item_name(id); } 256 void RevealPaintings() {
257 std::lock_guard state_guard(state_mutex);
258
259 std::vector<std::string> paintings;
260 for (const PaintingExit& painting : GD_GetPaintings()) {
261 paintings.push_back(painting.internal_id);
262 }
263
264 APClient::DataStorageOperation operation;
265 operation.operation = "replace";
266 operation.value = paintings;
267
268 apclient->Set(fmt::format("{}Paintings", data_storage_prefix), "", true,
269 {operation});
270 }
245 271
246 bool HasReachedGoal() { 272 bool HasReachedGoal() {
247 std::lock_guard state_guard(state_mutex); 273 std::lock_guard state_guard(state_mutex);
@@ -251,6 +277,12 @@ struct APState {
251 30; // CLIENT_GOAL 277 30; // CLIENT_GOAL
252 } 278 }
253 279
280 bool IsPanelSolved(int solve_index) {
281 std::lock_guard state_guard(state_mutex);
282
283 return solved_panels.test(solve_index);
284 }
285
254 private: 286 private:
255 void Initialize() { 287 void Initialize() {
256 if (!initialized) { 288 if (!initialized) {
@@ -258,16 +290,8 @@ struct APState {
258 290
259 std::thread([this]() { Thread(); }).detach(); 291 std::thread([this]() { Thread(); }).detach();
260 292
261 for (int panel_id : GD_GetAchievementPanels()) { 293 for (int i = 0; i < PANEL_BITFIELDS; i++) {
262 tracked_data_storage_keys.push_back(fmt::format( 294 tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i));
263 "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
264 }
265
266 for (const MapArea& map_area : GD_GetMapAreas()) {
267 for (const Location& location : map_area.locations) {
268 tracked_data_storage_keys.push_back(
269 fmt::format("Hunt|{}", location.ap_location_id));
270 }
271 } 295 }
272 296
273 tracked_data_storage_keys.push_back("PlayerPos"); 297 tracked_data_storage_keys.push_back("PlayerPos");
@@ -351,7 +375,7 @@ struct APState {
351 } 375 }
352 } 376 }
353 377
354 RefreshTracker(false); 378 RefreshTracker(StateUpdate{.cleared_locations = true});
355 } 379 }
356 380
357 void OnSlotDisconnected() { 381 void OnSlotDisconnected() {
@@ -373,37 +397,59 @@ struct APState {
373 } 397 }
374 398
375 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) { 399 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) {
400 std::vector<ItemState> item_states;
401 bool progression_items = false;
402
376 { 403 {
377 std::lock_guard state_guard(state_mutex); 404 std::lock_guard state_guard(state_mutex);
378 405
406 std::map<int64_t, int> index_by_item;
407
379 for (const APClient::NetworkItem& item : items) { 408 for (const APClient::NetworkItem& item : items) {
380 inventory[item.item]++; 409 inventory[item.item]++;
381 TrackerLog(fmt::format("Item: {}", item.item)); 410 TrackerLog(fmt::format("Item: {}", item.item));
411
412 index_by_item[item.item] = item.index;
413
414 if (!kNonProgressionItems.count(item.item)) {
415 progression_items = true;
416 }
417 }
418
419 for (const auto& [item_id, item_index] : index_by_item) {
420 item_states.push_back(ItemState{.name = GD_GetItemName(item_id),
421 .amount = inventory[item_id],
422 .index = item_index});
382 } 423 }
383 } 424 }
384 425
385 RefreshTracker(false); 426 RefreshTracker(StateUpdate{.items = item_states,
427 .progression_items = progression_items});
386 } 428 }
387 429
388 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) { 430 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) {
431 StateUpdate state_update;
432
389 { 433 {
390 std::lock_guard state_guard(state_mutex); 434 std::lock_guard state_guard(state_mutex);
391 435
392 for (const auto& [key, value] : data) { 436 for (const auto& [key, value] : data) {
393 HandleDataStorage(key, value); 437 HandleDataStorage(key, value, state_update);
394 } 438 }
395 } 439 }
396 440
397 RefreshTracker(false); 441 RefreshTracker(state_update);
398 } 442 }
399 443
400 void OnSetReply(const std::string& key, const nlohmann::json& value) { 444 void OnSetReply(const std::string& key, const nlohmann::json& value) {
445 StateUpdate state_update;
446
401 { 447 {
402 std::lock_guard state_guard(state_mutex); 448 std::lock_guard state_guard(state_mutex);
403 HandleDataStorage(key, value); 449 HandleDataStorage(key, value, state_update);
404 } 450 }
405 451
406 RefreshTracker(false); 452 RefreshTracker(state_update);
407 } 453 }
408 454
409 void OnSlotConnected(std::string player, std::string server, 455 void OnSlotConnected(std::string player, std::string server,
@@ -435,6 +481,7 @@ struct APState {
435 } 481 }
436 } 482 }
437 color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; 483 color_shuffle = slot_data["shuffle_colors"].get<int>() == 1;
484 panel_shuffle_mode = slot_data["shuffle_panels"].get<PanelShuffleMode>();
438 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 485 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
439 mastery_requirement = slot_data["mastery_achievements"].get<int>(); 486 mastery_requirement = slot_data["mastery_achievements"].get<int>();
440 level_2_requirement = slot_data["level_2_requirement"].get<int>(); 487 level_2_requirement = slot_data["level_2_requirement"].get<int>();
@@ -456,6 +503,9 @@ struct APState {
456 : kSUNWARP_ACCESS_NORMAL; 503 : kSUNWARP_ACCESS_NORMAL;
457 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && 504 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
458 slot_data["shuffle_sunwarps"].get<int>() == 1; 505 slot_data["shuffle_sunwarps"].get<int>() == 1;
506 postgame_shuffle = slot_data.contains("shuffle_postgame")
507 ? (slot_data["shuffle_postgame"].get<int>() == 1)
508 : true;
459 509
460 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { 510 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
461 painting_mapping.clear(); 511 painting_mapping.clear();
@@ -497,7 +547,7 @@ struct APState {
497 } 547 }
498 548
499 ResetReachabilityRequirements(); 549 ResetReachabilityRequirements();
500 RefreshTracker(true); 550 RefreshTracker(std::nullopt);
501 } 551 }
502 552
503 void OnSlotRefused(const std::list<std::string>& errors) { 553 void OnSlotRefused(const std::list<std::string>& errors) {
@@ -540,19 +590,40 @@ struct APState {
540 } 590 }
541 591
542 // Assumes state mutex is locked. 592 // Assumes state mutex is locked.
543 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 593 void HandleDataStorage(const std::string& key, const nlohmann::json& value, StateUpdate& state_update) {
544 if (value.is_boolean()) { 594 if (value.is_boolean()) {
545 data_storage[key] = value.get<bool>(); 595 data_storage[key] = value.get<bool>();
546 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 596 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
547 (value.get<bool>() ? "true" : "false"))); 597 (value.get<bool>() ? "true" : "false")));
598
548 } else if (value.is_number()) { 599 } else if (value.is_number()) {
549 data_storage[key] = value.get<int>(); 600 data_storage[key] = value.get<int>();
550 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 601 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
551 value.get<int>())); 602 value.get<int>()));
603
604 if (key == victory_data_storage_key) {
605 state_update.cleared_locations = true;
606 } else if (key.find("Panels_") != std::string::npos) {
607 int bitfield_num =
608 std::stoi(key.substr(data_storage_prefix.size() + 7));
609 uint64_t bitfield_value = value.get<uint64_t>();
610 for (int i = 0; i < PANEL_BITFIELD_LENGTH; i++) {
611 if ((bitfield_value & (1LL << i)) != 0) {
612 int solve_index = bitfield_num * PANEL_BITFIELD_LENGTH + i;
613
614 if (!solved_panels.test(solve_index)) {
615 state_update.panels.insert(solve_index);
616 }
617
618 solved_panels.set(solve_index);
619 }
620 }
621 }
552 } else if (value.is_object()) { 622 } else if (value.is_object()) {
553 if (key.ends_with("PlayerPos")) { 623 if (key.ends_with("PlayerPos")) {
554 auto map_value = value.get<std::map<std::string, int>>(); 624 auto map_value = value.get<std::map<std::string, int>>();
555 player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]); 625 player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]);
626 state_update.player_position = true;
556 } else { 627 } else {
557 data_storage[key] = value.get<std::map<std::string, int>>(); 628 data_storage[key] = value.get<std::map<std::string, int>>();
558 } 629 }
@@ -561,6 +632,7 @@ struct APState {
561 } else if (value.is_null()) { 632 } else if (value.is_null()) {
562 if (key.ends_with("PlayerPos")) { 633 if (key.ends_with("PlayerPos")) {
563 player_pos = std::nullopt; 634 player_pos = std::nullopt;
635 state_update.player_position = true;
564 } else { 636 } else {
565 data_storage.erase(key); 637 data_storage.erase(key);
566 } 638 }
@@ -572,6 +644,8 @@ struct APState {
572 if (key.ends_with("Paintings")) { 644 if (key.ends_with("Paintings")) {
573 data_storage[key] = 645 data_storage[key] =
574 std::set<std::string>(list_value.begin(), list_value.end()); 646 std::set<std::string>(list_value.begin(), list_value.end());
647 state_update.paintings =
648 std::vector<std::string>(list_value.begin(), list_value.end());
575 } else { 649 } else {
576 data_storage[key] = list_value; 650 data_storage[key] = list_value;
577 } 651 }
@@ -582,29 +656,34 @@ struct APState {
582 } 656 }
583 657
584 // State mutex should NOT be locked. 658 // State mutex should NOT be locked.
585 void RefreshTracker(bool reset) { 659 // nullopt state_update indicates a reset.
660 void RefreshTracker(std::optional<StateUpdate> state_update) {
586 TrackerLog("Refreshing display..."); 661 TrackerLog("Refreshing display...");
587 662
588 std::string prev_msg; 663 if (!state_update || state_update->progression_items ||
589 { 664 !state_update->paintings.empty()) {
590 std::lock_guard state_guard(state_mutex); 665 std::string prev_msg;
666 {
667 std::lock_guard state_guard(state_mutex);
591 668
592 prev_msg = status_message; 669 prev_msg = status_message;
593 SetStatusMessage(fmt::format("{} Recalculating...", status_message)); 670 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
594 } 671 }
595 672
596 RecalculateReachability(); 673 RecalculateReachability();
597 674
598 if (reset) { 675 {
599 tracker_frame->ResetIndicators(); 676 std::lock_guard state_guard(state_mutex);
600 } else {
601 tracker_frame->UpdateIndicators();
602 }
603 677
604 { 678 SetStatusMessage(prev_msg);
605 std::lock_guard state_guard(state_mutex); 679 }
680 }
681
606 682
607 SetStatusMessage(prev_msg); 683 if (!state_update) {
684 tracker_frame->ResetIndicators();
685 } else {
686 tracker_frame->UpdateIndicators(*state_update);
608 } 687 }
609 } 688 }
610 689
@@ -639,16 +718,12 @@ bool AP_HasCheckedGameLocation(int location_id) {
639 return GetState().HasCheckedGameLocation(location_id); 718 return GetState().HasCheckedGameLocation(location_id);
640} 719}
641 720
642bool AP_HasCheckedHuntPanel(int location_id) {
643 return GetState().HasCheckedHuntPanel(location_id);
644}
645
646bool AP_HasItem(int item_id, int quantity) { 721bool AP_HasItem(int item_id, int quantity) {
647 return GetState().HasItem(item_id, quantity); 722 return GetState().HasItem(item_id, quantity);
648} 723}
649 724
650std::string AP_GetItemName(int item_id) { 725bool AP_HasItemSafe(int item_id, int quantity) {
651 return GetState().GetItemName(item_id); 726 return GetState().HasItemSafe(item_id, quantity);
652} 727}
653 728
654DoorShuffleMode AP_GetDoorShuffleMode() { 729DoorShuffleMode AP_GetDoorShuffleMode() {
@@ -695,6 +770,8 @@ bool AP_IsPaintingChecked(const std::string& painting_id) {
695 return GetState().IsPaintingChecked(painting_id); 770 return GetState().IsPaintingChecked(painting_id);
696} 771}
697 772
773void AP_RevealPaintings() { GetState().RevealPaintings(); }
774
698int AP_GetMasteryRequirement() { 775int AP_GetMasteryRequirement() {
699 std::lock_guard state_guard(GetState().state_mutex); 776 std::lock_guard state_guard(GetState().state_mutex);
700 777
@@ -707,6 +784,12 @@ int AP_GetLevel2Requirement() {
707 return GetState().level_2_requirement; 784 return GetState().level_2_requirement;
708} 785}
709 786
787LocationChecks AP_GetLocationsChecks() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
790 return GetState().location_checks;
791}
792
710bool AP_IsLocationVisible(int classification) { 793bool AP_IsLocationVisible(int classification) {
711 std::lock_guard state_guard(GetState().state_mutex); 794 std::lock_guard state_guard(GetState().state_mutex);
712 795
@@ -734,14 +817,16 @@ bool AP_IsLocationVisible(int classification) {
734 return (world_state & classification); 817 return (world_state & classification);
735} 818}
736 819
737VictoryCondition AP_GetVictoryCondition() { 820PanelShuffleMode AP_GetPanelShuffleMode() {
738 std::lock_guard state_guard(GetState().state_mutex); 821 std::lock_guard state_guard(GetState().state_mutex);
739 822
740 return GetState().victory_condition; 823 return GetState().panel_shuffle_mode;
741} 824}
742 825
743bool AP_HasAchievement(const std::string& achievement_name) { 826VictoryCondition AP_GetVictoryCondition() {
744 return GetState().HasAchievement(achievement_name); 827 std::lock_guard state_guard(GetState().state_mutex);
828
829 return GetState().victory_condition;
745} 830}
746 831
747bool AP_HasEarlyColorHallways() { 832bool AP_HasEarlyColorHallways() {
@@ -784,6 +869,8 @@ std::map<int, SunwarpMapping> AP_GetSunwarpMapping() {
784 return GetState().sunwarp_mapping; 869 return GetState().sunwarp_mapping;
785} 870}
786 871
872bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; }
873
787bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 874bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
788 875
789std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { 876std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
@@ -791,3 +878,7 @@ std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
791 878
792 return GetState().player_pos; 879 return GetState().player_pos;
793} 880}
881
882bool AP_IsPanelSolved(int solve_index) {
883 return GetState().IsPanelSolved(solve_index);
884}
diff --git a/src/ap_state.h b/src/ap_state.h index 2da0b8e..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -26,6 +26,8 @@ enum LocationChecks {
26 kPANELSANITY = 2 26 kPANELSANITY = 2
27}; 27};
28 28
29enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 };
30
29enum SunwarpAccess { 31enum SunwarpAccess {
30 kSUNWARP_ACCESS_NORMAL = 0, 32 kSUNWARP_ACCESS_NORMAL = 0,
31 kSUNWARP_ACCESS_DISABLED = 1, 33 kSUNWARP_ACCESS_DISABLED = 1,
@@ -39,6 +41,12 @@ struct SunwarpMapping {
39 int exit_index; 41 int exit_index;
40}; 42};
41 43
44struct ItemState {
45 std::string name;
46 int amount = 0;
47 int index = 0;
48};
49
42void AP_SetTrackerFrame(TrackerFrame* tracker_frame); 50void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
43 51
44void AP_Connect(std::string server, std::string player, std::string password); 52void AP_Connect(std::string server, std::string player, std::string password);
@@ -49,16 +57,11 @@ std::string AP_GetSaveName();
49 57
50bool AP_HasCheckedGameLocation(int location_id); 58bool AP_HasCheckedGameLocation(int location_id);
51 59
52bool AP_HasCheckedHuntPanel(int location_id);
53
54// This doesn't lock the state mutex, for speed, so it must ONLY be called from 60// This doesn't lock the state mutex, for speed, so it must ONLY be called from
55// RecalculateReachability, which is only called from the APState thread anyway. 61// RecalculateReachability, which is only called from the APState thread anyway.
56bool AP_HasItem(int item_id, int quantity = 1); 62bool AP_HasItem(int item_id, int quantity = 1);
57 63
58// This doesn't lock the client mutex because it is ONLY to be called from 64bool AP_HasItemSafe(int item_id, int quantity = 1);
59// RecalculateReachability, which is only called from within a client callback
60// anyway.
61std::string AP_GetItemName(int item_id);
62 65
63DoorShuffleMode AP_GetDoorShuffleMode(); 66DoorShuffleMode AP_GetDoorShuffleMode();
64 67
@@ -76,15 +79,19 @@ std::set<std::string> AP_GetCheckedPaintings();
76 79
77bool AP_IsPaintingChecked(const std::string& painting_id); 80bool AP_IsPaintingChecked(const std::string& painting_id);
78 81
82void AP_RevealPaintings();
83
79int AP_GetMasteryRequirement(); 84int AP_GetMasteryRequirement();
80 85
81int AP_GetLevel2Requirement(); 86int AP_GetLevel2Requirement();
82 87
88LocationChecks AP_GetLocationsChecks();
89
83bool AP_IsLocationVisible(int classification); 90bool AP_IsLocationVisible(int classification);
84 91
85VictoryCondition AP_GetVictoryCondition(); 92PanelShuffleMode AP_GetPanelShuffleMode();
86 93
87bool AP_HasAchievement(const std::string& achievement_name); 94VictoryCondition AP_GetVictoryCondition();
88 95
89bool AP_HasEarlyColorHallways(); 96bool AP_HasEarlyColorHallways();
90 97
@@ -100,8 +107,12 @@ bool AP_IsSunwarpShuffle();
100 107
101std::map<int, SunwarpMapping> AP_GetSunwarpMapping(); 108std::map<int, SunwarpMapping> AP_GetSunwarpMapping();
102 109
110bool AP_IsPostgameShuffle();
111
103bool AP_HasReachedGoal(); 112bool AP_HasReachedGoal();
104 113
105std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); 114std::optional<std::tuple<int, int>> AP_GetPlayerPosition();
106 115
116bool AP_IsPanelSolved(int solve_index);
117
107#endif /* end of include guard: AP_STATE_H_664A4180 */ 118#endif /* end of include guard: AP_STATE_H_664A4180 */
diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 8d6487e..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -7,6 +7,7 @@
7#include "ap_state.h" 7#include "ap_state.h"
8#include "game_data.h" 8#include "game_data.h"
9#include "global.h" 9#include "global.h"
10#include "icons.h"
10#include "tracker_config.h" 11#include "tracker_config.h"
11#include "tracker_panel.h" 12#include "tracker_panel.h"
12#include "tracker_state.h" 13#include "tracker_state.h"
@@ -15,60 +16,54 @@ AreaPopup::AreaPopup(wxWindow* parent, int area_id)
15 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 16 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
16 SetBackgroundStyle(wxBG_STYLE_PAINT); 17 SetBackgroundStyle(wxBG_STYLE_PAINT);
17 18
18 unchecked_eye_ = 19 LoadIcons();
19 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
20 wxBITMAP_TYPE_PNG)
21 .Scale(32, 32));
22 checked_eye_ = wxBitmap(
23 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
24 .Scale(32, 32));
25 20
21 // TODO: This is slow on high-DPI screens.
26 SetScrollRate(5, 5); 22 SetScrollRate(5, 5);
27 23
28 SetBackgroundColour(*wxBLACK); 24 SetBackgroundColour(*wxBLACK);
29 Hide(); 25 Hide();
30 26
31 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); 27 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this);
28 Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this);
32 29
33 UpdateIndicators(); 30 ResetIndicators();
34} 31}
35 32
36void AreaPopup::UpdateIndicators() { 33void AreaPopup::ResetIndicators() {
34 indicators_.clear();
35
37 const MapArea& map_area = GD_GetMapArea(area_id_); 36 const MapArea& map_area = GD_GetMapArea(area_id_);
37 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
38 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
38 39
39 // Start calculating extents. 40 // Start calculating extents.
40 wxMemoryDC mem_dc; 41 wxMemoryDC mem_dc;
41 mem_dc.SetFont(GetFont().Bold()); 42 mem_dc.SetFont(the_font.Bold());
42 wxSize header_extent = mem_dc.GetTextExtent(map_area.name); 43 header_extent_ = mem_dc.GetTextExtent(map_area.name);
43 44
44 int acc_height = header_extent.GetHeight() + 20; 45 int acc_height = header_extent_.GetHeight() + FromDIP(20);
45 int col_width = 0; 46 int col_width = 0;
46 47
47 mem_dc.SetFont(GetFont()); 48 mem_dc.SetFont(the_font);
48
49 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
50
51 std::vector<int> real_locations;
52 49
53 for (int section_id = 0; section_id < map_area.locations.size(); 50 for (int section_id = 0; section_id < map_area.locations.size();
54 section_id++) { 51 section_id++) {
55 const Location& location = map_area.locations.at(section_id); 52 const Location& location = map_area.locations.at(section_id);
56 53 if ((!AP_IsLocationVisible(location.classification) ||
57 if (tracker_panel->IsPanelsMode()) { 54 IsLocationPostgame(location.ap_location_id)) &&
58 if (!location.single_panel) { 55 !(location.hunt &&
59 continue; 56 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
60 } 57 !(location.single_panel &&
61 } else { 58 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) {
62 if (!AP_IsLocationVisible(location.classification) && 59 continue;
63 !(location.hunt && GetTrackerConfig().show_hunt_panels)) {
64 continue;
65 }
66 } 60 }
67 61
68 real_locations.push_back(section_id); 62 indicators_.emplace_back(section_id, kLOCATION, acc_height);
69 63
70 wxSize item_extent = mem_dc.GetTextExtent(location.name); 64 wxSize item_extent = mem_dc.GetTextExtent(location.name);
71 int item_height = std::max(32, item_extent.GetHeight()) + 10; 65 int item_height =
66 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
72 acc_height += item_height; 67 acc_height += item_height;
73 68
74 if (item_extent.GetWidth() > col_width) { 69 if (item_extent.GetWidth() > col_width) {
@@ -76,11 +71,18 @@ void AreaPopup::UpdateIndicators() {
76 } 71 }
77 } 72 }
78 73
79 if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { 74 if (AP_IsPaintingShuffle()) {
80 for (int painting_id : map_area.paintings) { 75 for (int painting_id : map_area.paintings) {
76 if (IsPaintingPostgame(painting_id)) {
77 continue;
78 }
79
80 indicators_.emplace_back(painting_id, kPAINTING, acc_height);
81
81 const PaintingExit& painting = GD_GetPaintingExit(painting_id); 82 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
82 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name. 83 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
83 int item_height = std::max(32, item_extent.GetHeight()) + 10; 84 int item_height =
85 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
84 acc_height += item_height; 86 acc_height += item_height;
85 87
86 if (item_extent.GetWidth() > col_width) { 88 if (item_extent.GetWidth() > col_width) {
@@ -89,80 +91,86 @@ void AreaPopup::UpdateIndicators() {
89 } 91 }
90 } 92 }
91 93
92 int item_width = col_width + 10 + 32; 94 int item_width = col_width + FromDIP(10 + 32);
93 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 95 full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20);
96 full_height_ = acc_height;
94 97
95 Fit(); 98 Fit();
96 SetVirtualSize(full_width, acc_height); 99 SetVirtualSize(full_width_, full_height_);
100
101 UpdateIndicators();
102}
103
104void AreaPopup::UpdateIndicators() {
105 const MapArea& map_area = GD_GetMapArea(area_id_);
106 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
107 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
108
109 rendered_ = wxBitmap(full_width_, full_height_);
97 110
98 rendered_ = wxBitmap(full_width, acc_height); 111 wxMemoryDC mem_dc;
99 mem_dc.SelectObject(rendered_); 112 mem_dc.SelectObject(rendered_);
100 mem_dc.SetPen(*wxTRANSPARENT_PEN); 113 mem_dc.SetPen(*wxTRANSPARENT_PEN);
101 mem_dc.SetBrush(*wxBLACK_BRUSH); 114 mem_dc.SetBrush(*wxBLACK_BRUSH);
102 mem_dc.DrawRectangle({0, 0}, {full_width, acc_height}); 115 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
103 116
104 mem_dc.SetFont(GetFont().Bold()); 117 mem_dc.SetFont(the_font.Bold());
105 mem_dc.SetTextForeground(*wxWHITE); 118 mem_dc.SetTextForeground(*wxWHITE);
106 mem_dc.DrawText(map_area.name, 119 mem_dc.DrawText(map_area.name,
107 {(full_width - header_extent.GetWidth()) / 2, 10}); 120 {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)});
108 121
109 int cur_height = header_extent.GetHeight() + 20; 122 mem_dc.SetFont(the_font);
110 123
111 mem_dc.SetFont(GetFont()); 124 for (const IndicatorInfo& indicator : indicators_) {
125 switch (indicator.type) {
126 case kLOCATION: {
127 const Location& location = map_area.locations.at(indicator.id);
112 128
113 for (int section_id : real_locations) { 129 bool checked = false;
114 const Location& location = map_area.locations.at(section_id); 130 if (IsLocationWinCondition(location)) {
131 checked = AP_HasReachedGoal();
132 } else {
133 checked = AP_HasCheckedGameLocation(location.ap_location_id) ||
134 (location.single_panel &&
135 AP_IsPanelSolved(
136 GD_GetPanel(*location.single_panel).solve_index));
137 }
115 138
116 bool checked = false; 139 const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_;
117 if (IsLocationWinCondition(location)) {
118 checked = AP_HasReachedGoal();
119 } else if (tracker_panel->IsPanelsMode()) {
120 const Panel& panel = GD_GetPanel(*location.single_panel);
121 if (panel.non_counting) {
122 checked = AP_HasCheckedGameLocation(location.ap_location_id);
123 } else {
124 checked = tracker_panel->GetSolvedPanels().contains(panel.nodepath);
125 }
126 } else {
127 checked =
128 AP_HasCheckedGameLocation(location.ap_location_id) ||
129 (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id));
130 }
131
132 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
133 140
134 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 141 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
135 142
136 bool reachable = IsLocationReachable(location.ap_location_id); 143 bool reachable = IsLocationReachable(location.ap_location_id);
137 const wxColour* text_color = reachable ? wxWHITE : wxRED; 144 const wxColour* text_color = reachable ? wxWHITE : wxRED;
138 mem_dc.SetTextForeground(*text_color); 145 mem_dc.SetTextForeground(*text_color);
139 146
140 wxSize item_extent = mem_dc.GetTextExtent(location.name); 147 wxSize item_extent = mem_dc.GetTextExtent(location.name);
141 mem_dc.DrawText( 148 mem_dc.DrawText(
142 location.name, 149 location.name,
143 {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 150 {FromDIP(10 + 32 + 10),
144 151 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
145 cur_height += 10 + 32;
146 }
147 152
148 if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { 153 break;
149 for (int painting_id : map_area.paintings) { 154 }
150 const PaintingExit& painting = GD_GetPaintingExit(painting_id); 155 case kPAINTING: {
156 const PaintingExit& painting = GD_GetPaintingExit(indicator.id);
151 157
152 bool reachable = IsPaintingReachable(painting_id); 158 bool reachable = IsPaintingReachable(indicator.id);
153 const wxColour* text_color = reachable ? wxWHITE : wxRED; 159 const wxColour* text_color = reachable ? wxWHITE : wxRED;
154 mem_dc.SetTextForeground(*text_color); 160 mem_dc.SetTextForeground(*text_color);
155 161
156 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); 162 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id);
157 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; 163 const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_;
158 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 164 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
159 165
160 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name. 166 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
161 mem_dc.DrawText(painting.internal_id, 167 mem_dc.DrawText(
162 {10 + 32 + 10, 168 painting.display_name,
163 cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 169 {FromDIP(10 + 32 + 10),
170 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
164 171
165 cur_height += 10 + 32; 172 break;
173 }
166 } 174 }
167 } 175 }
168} 176}
@@ -174,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) {
174 182
175 event.Skip(); 183 event.Skip();
176} 184}
185
186void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) {
187 LoadIcons();
188 ResetIndicators();
189
190 event.Skip();
191}
192
193void AreaPopup::LoadIcons() {
194 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
195 FromDIP(wxSize{32, 32}));
196 checked_eye_ =
197 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
198 unchecked_owl_ =
199 GetTheIconCache().GetIcon("assets/owl.png", FromDIP(wxSize{32, 32}));
200 checked_owl_ = GetTheIconCache().GetIcon("assets/checked_owl.png",
201 FromDIP(wxSize{32, 32}));
202}
diff --git a/src/area_popup.h b/src/area_popup.h index 00c644d..f8a2355 100644 --- a/src/area_popup.h +++ b/src/area_popup.h
@@ -7,19 +7,53 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <vector>
11
10class AreaPopup : public wxScrolledCanvas { 12class AreaPopup : public wxScrolledCanvas {
11 public: 13 public:
12 AreaPopup(wxWindow* parent, int area_id); 14 AreaPopup(wxWindow* parent, int area_id);
13 15
16 void ResetIndicators();
14 void UpdateIndicators(); 17 void UpdateIndicators();
15 18
19 int GetFullWidth() const { return full_width_; }
20 int GetFullHeight() const { return full_height_; }
21
16 private: 22 private:
23 enum IndicatorType {
24 kLOCATION,
25 kPAINTING,
26 };
27
28 struct IndicatorInfo {
29 // For locations, the id is an index into the map area's locations list.
30 // For paintings, it is a real painting id.
31 int id;
32 IndicatorType type;
33 int y;
34
35 IndicatorInfo(int id, IndicatorType type, int y)
36 : id(id), type(type), y(y) {}
37 };
38
17 void OnPaint(wxPaintEvent& event); 39 void OnPaint(wxPaintEvent& event);
40 void OnDPIChanged(wxDPIChangedEvent& event);
41
42 void LoadIcons();
18 43
19 int area_id_; 44 int area_id_;
20 45
21 wxBitmap unchecked_eye_; 46 const wxBitmap* unchecked_eye_;
22 wxBitmap checked_eye_; 47 const wxBitmap* checked_eye_;
48 const wxBitmap* unchecked_owl_;
49 const wxBitmap* checked_owl_;
50
51 int full_width_ = 0;
52 int full_height_ = 0;
53 wxSize header_extent_;
54
55 std::vector<IndicatorInfo> indicators_;
56
23 wxBitmap rendered_; 57 wxBitmap rendered_;
24}; 58};
25 59
diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp index 64fee98..b55a138 100644 --- a/src/connection_dialog.cpp +++ b/src/connection_dialog.cpp
@@ -4,17 +4,21 @@
4 4
5ConnectionDialog::ConnectionDialog() 5ConnectionDialog::ConnectionDialog()
6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { 6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
7 server_box_ = 7 server_box_ = new wxTextCtrl(
8 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_server, 8 this, -1,
9 wxDefaultPosition, {300, -1}); 9 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server),
10 player_box_ = 10 wxDefaultPosition, FromDIP(wxSize{300, -1}));
11 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_player, 11 player_box_ = new wxTextCtrl(
12 wxDefaultPosition, {300, -1}); 12 this, -1,
13 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_player),
14 wxDefaultPosition, FromDIP(wxSize{300, -1}));
13 password_box_ = new wxTextCtrl( 15 password_box_ = new wxTextCtrl(
14 this, -1, GetTrackerConfig().connection_details.ap_password, 16 this, -1,
15 wxDefaultPosition, {300, -1}); 17 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_password),
18 wxDefaultPosition, FromDIP(wxSize{300, -1}));
16 19
17 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10); 20 wxFlexGridSizer* form_sizer =
21 new wxFlexGridSizer(2, FromDIP(10), FromDIP(10));
18 22
19 form_sizer->Add( 23 form_sizer->Add(
20 new wxStaticText(this, -1, "Server:"), 24 new wxStaticText(this, -1, "Server:"),
@@ -30,17 +34,19 @@ ConnectionDialog::ConnectionDialog()
30 form_sizer->Add(password_box_, wxSizerFlags().Expand()); 34 form_sizer->Add(password_box_, wxSizerFlags().Expand());
31 35
32 history_list_ = new wxListBox(this, -1); 36 history_list_ = new wxListBox(this, -1);
33 for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { 37 for (const ConnectionDetails& details :
38 GetTrackerConfig().connection_history) {
34 wxString display_text; 39 wxString display_text;
35 display_text << details.ap_player; 40 display_text << wxString::FromUTF8(details.ap_player);
36 display_text << " ("; 41 display_text << " (";
37 display_text << details.ap_server; 42 display_text << wxString::FromUTF8(details.ap_server);
38 display_text << ")"; 43 display_text << ")";
39 44
40 history_list_->Append(display_text); 45 history_list_->Append(display_text);
41 } 46 }
42 47
43 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, this); 48 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen,
49 this);
44 50
45 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); 51 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL);
46 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); 52 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand());
@@ -52,7 +58,8 @@ ConnectionDialog::ConnectionDialog()
52 this, -1, "Enter the details to connect to Archipelago."), 58 this, -1, "Enter the details to connect to Archipelago."),
53 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); 59 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
54 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); 60 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand());
55 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Border().Center()); 61 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
62 wxSizerFlags().Border().Center());
56 63
57 SetSizerAndFit(top_sizer); 64 SetSizerAndFit(top_sizer);
58 65
@@ -62,9 +69,10 @@ ConnectionDialog::ConnectionDialog()
62 69
63void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { 70void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) {
64 if (e.IsSelection()) { 71 if (e.IsSelection()) {
65 const ConnectionDetails& details = GetTrackerConfig().connection_history.at(e.GetSelection()); 72 const ConnectionDetails& details =
66 server_box_->SetValue(details.ap_server); 73 GetTrackerConfig().connection_history.at(e.GetSelection());
67 player_box_->SetValue(details.ap_player); 74 server_box_->SetValue(wxString::FromUTF8(details.ap_server));
68 password_box_->SetValue(details.ap_password); 75 player_box_->SetValue(wxString::FromUTF8(details.ap_player));
76 password_box_->SetValue(wxString::FromUTF8(details.ap_password));
69 } 77 }
70} 78}
diff --git a/src/connection_dialog.h b/src/connection_dialog.h index 9fe62fd..ec2ee72 100644 --- a/src/connection_dialog.h +++ b/src/connection_dialog.h
@@ -14,12 +14,12 @@ class ConnectionDialog : public wxDialog {
14 public: 14 public:
15 ConnectionDialog(); 15 ConnectionDialog();
16 16
17 std::string GetServerValue() { return server_box_->GetValue().ToStdString(); } 17 std::string GetServerValue() { return server_box_->GetValue().utf8_string(); }
18 18
19 std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); } 19 std::string GetPlayerValue() { return player_box_->GetValue().utf8_string(); }
20 20
21 std::string GetPasswordValue() { 21 std::string GetPasswordValue() {
22 return password_box_->GetValue().ToStdString(); 22 return password_box_->GetValue().utf8_string();
23 } 23 }
24 24
25 private: 25 private:
diff --git a/src/game_data.cpp b/src/game_data.cpp index 0ac77af..94b9888 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -12,32 +12,6 @@
12 12
13namespace { 13namespace {
14 14
15LingoColor GetColorForString(const std::string &str) {
16 if (str == "black") {
17 return LingoColor::kBlack;
18 } else if (str == "red") {
19 return LingoColor::kRed;
20 } else if (str == "blue") {
21 return LingoColor::kBlue;
22 } else if (str == "yellow") {
23 return LingoColor::kYellow;
24 } else if (str == "orange") {
25 return LingoColor::kOrange;
26 } else if (str == "green") {
27 return LingoColor::kGreen;
28 } else if (str == "gray") {
29 return LingoColor::kGray;
30 } else if (str == "brown") {
31 return LingoColor::kBrown;
32 } else if (str == "purple") {
33 return LingoColor::kPurple;
34 } else {
35 TrackerLog(fmt::format("Invalid color: {}", str));
36
37 return LingoColor::kNone;
38 }
39}
40
41struct GameData { 15struct GameData {
42 std::vector<Room> rooms_; 16 std::vector<Room> rooms_;
43 std::vector<Door> doors_; 17 std::vector<Door> doors_;
@@ -55,10 +29,10 @@ struct GameData {
55 std::map<std::string, int> painting_by_id_; 29 std::map<std::string, int> painting_by_id_;
56 30
57 std::vector<int> door_definition_order_; 31 std::vector<int> door_definition_order_;
58 std::vector<int> room_definition_order_;
59 32
60 std::map<std::string, int> room_by_painting_; 33 std::map<std::string, int> room_by_painting_;
61 std::map<int, int> room_by_sunwarp_; 34 std::map<int, int> room_by_sunwarp_;
35 std::map<int, int> panel_by_solve_index_;
62 36
63 std::vector<int> achievement_panels_; 37 std::vector<int> achievement_panels_;
64 38
@@ -69,6 +43,8 @@ struct GameData {
69 std::map<std::string, int> subway_item_by_painting_; 43 std::map<std::string, int> subway_item_by_painting_;
70 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; 44 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
71 45
46 std::map<int, std::string> item_by_ap_id_;
47
72 bool loaded_area_data_ = false; 48 bool loaded_area_data_ = false;
73 std::set<std::string> malconfigured_areas_; 49 std::set<std::string> malconfigured_areas_;
74 50
@@ -84,7 +60,7 @@ struct GameData {
84 ids_config["special_items"][color_name]) { 60 ids_config["special_items"][color_name]) {
85 std::string input_name = color_name; 61 std::string input_name = color_name;
86 input_name[0] = std::tolower(input_name[0]); 62 input_name[0] = std::tolower(input_name[0]);
87 ap_id_by_color_[GetColorForString(input_name)] = 63 ap_id_by_color_[GetLingoColorForString(input_name)] =
88 ids_config["special_items"][color_name].as<int>(); 64 ids_config["special_items"][color_name].as<int>();
89 } else { 65 } else {
90 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); 66 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
@@ -101,11 +77,20 @@ struct GameData {
101 init_color_id("Brown"); 77 init_color_id("Brown");
102 init_color_id("Gray"); 78 init_color_id("Gray");
103 79
80 if (ids_config["special_items"]) {
81 for (const auto& special_item_it : ids_config["special_items"])
82 {
83 item_by_ap_id_[special_item_it.second.as<int>()] =
84 special_item_it.first.as<std::string>();
85 }
86 }
87
104 rooms_.reserve(lingo_config.size() * 2); 88 rooms_.reserve(lingo_config.size() * 2);
105 89
90 std::vector<int> panel_location_ids;
91
106 for (const auto &room_it : lingo_config) { 92 for (const auto &room_it : lingo_config) {
107 int room_id = AddOrGetRoom(room_it.first.as<std::string>()); 93 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
108 room_definition_order_.push_back(room_id);
109 94
110 for (const auto &entrance_it : room_it.second["entrances"]) { 95 for (const auto &entrance_it : room_it.second["entrances"]) {
111 int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>()); 96 int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>());
@@ -181,12 +166,12 @@ struct GameData {
181 166
182 if (panel_it.second["colors"]) { 167 if (panel_it.second["colors"]) {
183 if (panel_it.second["colors"].IsScalar()) { 168 if (panel_it.second["colors"].IsScalar()) {
184 panels_[panel_id].colors.push_back(GetColorForString( 169 panels_[panel_id].colors.push_back(GetLingoColorForString(
185 panel_it.second["colors"].as<std::string>())); 170 panel_it.second["colors"].as<std::string>()));
186 } else { 171 } else {
187 for (const auto &color_node : panel_it.second["colors"]) { 172 for (const auto &color_node : panel_it.second["colors"]) {
188 panels_[panel_id].colors.push_back( 173 panels_[panel_id].colors.push_back(
189 GetColorForString(color_node.as<std::string>())); 174 GetLingoColorForString(color_node.as<std::string>()));
190 } 175 }
191 } 176 }
192 } 177 }
@@ -292,10 +277,11 @@ struct GameData {
292 ids_config["panels"][rooms_[room_id].name] && 277 ids_config["panels"][rooms_[room_id].name] &&
293 ids_config["panels"][rooms_[room_id].name] 278 ids_config["panels"][rooms_[room_id].name]
294 [panels_[panel_id].name]) { 279 [panels_[panel_id].name]) {
295 panels_[panel_id].ap_location_id = 280 int location_id = ids_config["panels"][rooms_[room_id].name]
296 ids_config["panels"][rooms_[room_id].name] 281 [panels_[panel_id].name]
297 [panels_[panel_id].name] 282 .as<int>();
298 .as<int>(); 283 panels_[panel_id].ap_location_id = location_id;
284 panel_location_ids.push_back(location_id);
299 } else { 285 } else {
300 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", 286 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
301 rooms_[room_id].name, 287 rooms_[room_id].name,
@@ -361,6 +347,9 @@ struct GameData {
361 ids_config["doors"][rooms_[room_id].name] 347 ids_config["doors"][rooms_[room_id].name]
362 [doors_[door_id].name]["item"] 348 [doors_[door_id].name]["item"]
363 .as<int>(); 349 .as<int>();
350
351 item_by_ap_id_[doors_[door_id].ap_item_id] =
352 doors_[door_id].item_name;
364 } else { 353 } else {
365 TrackerLog(fmt::format("Missing AP item ID for door {} - {}", 354 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
366 rooms_[room_id].name, 355 rooms_[room_id].name,
@@ -377,6 +366,9 @@ struct GameData {
377 doors_[door_id].group_ap_item_id = 366 doors_[door_id].group_ap_item_id =
378 ids_config["door_groups"][doors_[door_id].group_name] 367 ids_config["door_groups"][doors_[door_id].group_name]
379 .as<int>(); 368 .as<int>();
369
370 item_by_ap_id_[doors_[door_id].group_ap_item_id] =
371 doors_[door_id].group_name;
380 } else { 372 } else {
381 TrackerLog(fmt::format("Missing AP item ID for door group {}", 373 TrackerLog(fmt::format("Missing AP item ID for door group {}",
382 doors_[door_id].group_name)); 374 doors_[door_id].group_name));
@@ -440,21 +432,50 @@ struct GameData {
440 int panel_door_id = 432 int panel_door_id =
441 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name); 433 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name);
442 434
435 std::map<std::string, std::vector<std::string>> panel_per_room;
436 int num_panels = 0;
443 for (const auto &panel_node : panel_door_it.second["panels"]) { 437 for (const auto &panel_node : panel_door_it.second["panels"]) {
438 num_panels++;
439
444 int panel_id = -1; 440 int panel_id = -1;
445 441
446 if (panel_node.IsScalar()) { 442 if (panel_node.IsScalar()) {
447 panel_id = AddOrGetPanel(rooms_[room_id].name, 443 panel_id = AddOrGetPanel(rooms_[room_id].name,
448 panel_node.as<std::string>()); 444 panel_node.as<std::string>());
445
446 panel_per_room[rooms_[room_id].name].push_back(
447 panel_node.as<std::string>());
449 } else { 448 } else {
450 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(), 449 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(),
451 panel_node["panel"].as<std::string>()); 450 panel_node["panel"].as<std::string>());
451
452 panel_per_room[panel_node["room"].as<std::string>()].push_back(
453 panel_node["panel"].as<std::string>());
452 } 454 }
453 455
454 Panel &panel = panels_[panel_id]; 456 Panel &panel = panels_[panel_id];
455 panel.panel_door = panel_door_id; 457 panel.panel_door = panel_door_id;
456 } 458 }
457 459
460 if (panel_door_it.second["item_name"]) {
461 panel_doors_[panel_door_id].item_name =
462 panel_door_it.second["item_name"].as<std::string>();
463 } else {
464 std::vector<std::string> room_strs;
465 for (const auto &[room_str, panels_str] : panel_per_room) {
466 room_strs.push_back(fmt::format(
467 "{} - {}", room_str, hatkirby::implode(panels_str, ", ")));
468 }
469
470 if (num_panels == 1) {
471 panel_doors_[panel_door_id].item_name =
472 fmt::format("{} (Panel)", room_strs[0]);
473 } else {
474 panel_doors_[panel_door_id].item_name = fmt::format(
475 "{} (Panels)", hatkirby::implode(room_strs, " and "));
476 }
477 }
478
458 if (ids_config["panel_doors"] && 479 if (ids_config["panel_doors"] &&
459 ids_config["panel_doors"][rooms_[room_id].name] && 480 ids_config["panel_doors"][rooms_[room_id].name] &&
460 ids_config["panel_doors"][rooms_[room_id].name] 481 ids_config["panel_doors"][rooms_[room_id].name]
@@ -462,6 +483,9 @@ struct GameData {
462 panel_doors_[panel_door_id].ap_item_id = 483 panel_doors_[panel_door_id].ap_item_id =
463 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name] 484 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name]
464 .as<int>(); 485 .as<int>();
486
487 item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] =
488 panel_doors_[panel_door_id].item_name;
465 } else { 489 } else {
466 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}", 490 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}",
467 rooms_[room_id].name, panel_door_name)); 491 rooms_[room_id].name, panel_door_name));
@@ -475,6 +499,9 @@ struct GameData {
475 ids_config["panel_groups"][panel_group]) { 499 ids_config["panel_groups"][panel_group]) {
476 panel_doors_[panel_door_id].group_ap_item_id = 500 panel_doors_[panel_door_id].group_ap_item_id =
477 ids_config["panel_groups"][panel_group].as<int>(); 501 ids_config["panel_groups"][panel_group].as<int>();
502
503 item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] =
504 panel_group;
478 } else { 505 } else {
479 TrackerLog(fmt::format( 506 TrackerLog(fmt::format(
480 "Missing AP item ID for panel door group {}", panel_group)); 507 "Missing AP item ID for panel door group {}", panel_group));
@@ -490,6 +517,13 @@ struct GameData {
490 PaintingExit &painting_exit = paintings_[painting_id]; 517 PaintingExit &painting_exit = paintings_[painting_id];
491 painting_exit.room = room_id; 518 painting_exit.room = room_id;
492 519
520 if (painting["display_name"]) {
521 painting_exit.display_name =
522 painting["display_name"].as<std::string>();
523 } else {
524 painting_exit.display_name = painting_exit.internal_id;
525 }
526
493 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && 527 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
494 (!painting["disable"] || !painting["disable"].as<bool>())) { 528 (!painting["disable"] || !painting["disable"].as<bool>())) {
495 painting_exit.entrance = true; 529 painting_exit.entrance = true;
@@ -531,6 +565,8 @@ struct GameData {
531 ids_config["progression"][progressive_item_name]) { 565 ids_config["progression"][progressive_item_name]) {
532 progressive_item_id = 566 progressive_item_id =
533 ids_config["progression"][progressive_item_name].as<int>(); 567 ids_config["progression"][progressive_item_name].as<int>();
568
569 item_by_ap_id_[progressive_item_id] = progressive_item_name;
534 } else { 570 } else {
535 TrackerLog(fmt::format("Missing AP item ID for progressive item {}", 571 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
536 progressive_item_name)); 572 progressive_item_name));
@@ -582,6 +618,21 @@ struct GameData {
582 } 618 }
583 } 619 }
584 620
621 // Determine the panel solve indices from the sorted location IDs.
622 std::sort(panel_location_ids.begin(), panel_location_ids.end());
623
624 std::map<int, int> solve_index_by_location_id;
625 for (int i = 0; i < panel_location_ids.size(); i++) {
626 solve_index_by_location_id[panel_location_ids[i]] = i;
627 }
628
629 for (Panel &panel : panels_) {
630 if (panel.ap_location_id != -1) {
631 panel.solve_index = solve_index_by_location_id[panel.ap_location_id];
632 panel_by_solve_index_[panel.solve_index] = panel.id;
633 }
634 }
635
585 map_areas_.reserve(areas_config.size()); 636 map_areas_.reserve(areas_config.size());
586 637
587 std::map<std::string, int> fold_areas; 638 std::map<std::string, int> fold_areas;
@@ -602,7 +653,7 @@ struct GameData {
602 // Only locations for the panels are kept here. 653 // Only locations for the panels are kept here.
603 std::map<std::string, std::tuple<int, int>> locations_by_name; 654 std::map<std::string, std::tuple<int, int>> locations_by_name;
604 655
605 for (const Panel &panel : panels_) { 656 for (Panel &panel : panels_) {
606 int room_id = panel.room; 657 int room_id = panel.room;
607 std::string room_name = rooms_[room_id].name; 658 std::string room_name = rooms_[room_id].name;
608 659
@@ -618,6 +669,8 @@ struct GameData {
618 area_name = location_name.substr(0, divider_pos); 669 area_name = location_name.substr(0, divider_pos);
619 section_name = location_name.substr(divider_pos + 3); 670 section_name = location_name.substr(divider_pos + 3);
620 } 671 }
672 } else {
673 panel.location_name = location_name;
621 } 674 }
622 675
623 if (fold_areas.count(area_name)) { 676 if (fold_areas.count(area_name)) {
@@ -716,7 +769,8 @@ struct GameData {
716 MapArea &map_area = map_areas_[area_id]; 769 MapArea &map_area = map_areas_[area_id];
717 770
718 for (int painting_id : room.paintings) { 771 for (int painting_id : room.paintings) {
719 const PaintingExit &painting_obj = paintings_.at(painting_id); 772 PaintingExit &painting_obj = paintings_.at(painting_id);
773 painting_obj.map_area = area_id;
720 if (painting_obj.entrance) { 774 if (painting_obj.entrance) {
721 map_area.paintings.push_back(painting_id); 775 map_area.paintings.push_back(painting_id);
722 } 776 }
@@ -724,31 +778,6 @@ struct GameData {
724 } 778 }
725 } 779 }
726 780
727 // As a workaround for a generator bug in 0.5.1, we are going to remove the
728 // panel door requirement on panels that are defined earlier in the file than
729 // the panel door is. This results in logic that matches the generator, even
730 // if it is not true to how the game should work. This will be reverted once
731 // the logic bug is fixed and released.
732 // See: https://github.com/ArchipelagoMW/Archipelago/pull/4342
733 for (Panel& panel : panels_) {
734 if (panel.panel_door == -1) {
735 continue;
736 }
737 const PanelDoor &panel_door = panel_doors_[panel.panel_door];
738 for (int room_id : room_definition_order_) {
739 if (room_id == panel_door.room) {
740 // The panel door was defined first (or at the same time as the panel),
741 // so we're good.
742 break;
743 } else if (room_id == panel.room) {
744 // The panel was defined first, so we have to pretend the panel door is
745 // not required for this panel.
746 panel.panel_door = -1;
747 break;
748 }
749 }
750 }
751
752 // Report errors. 781 // Report errors.
753 for (const std::string &area : malconfigured_areas_) { 782 for (const std::string &area : malconfigured_areas_) {
754 TrackerLog(fmt::format("Area data not found for: {}", area)); 783 TrackerLog(fmt::format("Area data not found for: {}", area));
@@ -768,13 +797,10 @@ struct GameData {
768 subway_it["door"].as<std::string>()); 797 subway_it["door"].as<std::string>());
769 } 798 }
770 799
771 if (subway_it["paintings"]) { 800 if (subway_it["painting"]) {
772 for (const auto &painting_it : subway_it["paintings"]) { 801 std::string painting_id = subway_it["painting"].as<std::string>();
773 std::string painting_id = painting_it.as<std::string>(); 802 subway_item.painting = painting_id;
774 803 subway_item_by_painting_[painting_id] = subway_item.id;
775 subway_item.paintings.push_back(painting_id);
776 subway_item_by_painting_[painting_id] = subway_item.id;
777 }
778 } 804 }
779 805
780 if (subway_it["tags"]) { 806 if (subway_it["tags"]) {
@@ -821,6 +847,10 @@ struct GameData {
821 subway_item.special = subway_it["special"].as<std::string>(); 847 subway_item.special = subway_it["special"].as<std::string>();
822 } 848 }
823 849
850 if (subway_it["tilted"]) {
851 subway_item.tilted = subway_it["tilted"].as<bool>();
852 }
853
824 subway_items_.push_back(subway_item); 854 subway_items_.push_back(subway_item);
825 } 855 }
826 856
@@ -880,7 +910,7 @@ struct GameData {
880 if (!panel_doors_by_id_.count(full_name)) { 910 if (!panel_doors_by_id_.count(full_name)) {
881 int panel_door_id = panel_doors_.size(); 911 int panel_door_id = panel_doors_.size();
882 panel_doors_by_id_[full_name] = panel_door_id; 912 panel_doors_by_id_[full_name] = panel_door_id;
883 panel_doors_.push_back({.room = AddOrGetRoom(room)}); 913 panel_doors_.push_back({});
884 } 914 }
885 915
886 return panel_doors_by_id_[full_name]; 916 return panel_doors_by_id_[full_name];
@@ -953,6 +983,14 @@ const Panel &GD_GetPanel(int panel_id) {
953 return GetState().panels_.at(panel_id); 983 return GetState().panels_.at(panel_id);
954} 984}
955 985
986int GD_GetPanelBySolveIndex(int solve_index) {
987 return GetState().panel_by_solve_index_.at(solve_index);
988}
989
990const std::vector<PaintingExit> &GD_GetPaintings() {
991 return GetState().paintings_;
992}
993
956const PaintingExit &GD_GetPaintingExit(int painting_id) { 994const PaintingExit &GD_GetPaintingExit(int painting_id) {
957 return GetState().paintings_.at(painting_id); 995 return GetState().paintings_.at(painting_id);
958} 996}
@@ -995,3 +1033,38 @@ std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
995int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 1033int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
996 return GetState().subway_item_by_sunwarp_.at(sunwarp); 1034 return GetState().subway_item_by_sunwarp_.at(sunwarp);
997} 1035}
1036
1037std::string GD_GetItemName(int id) {
1038 auto it = GetState().item_by_ap_id_.find(id);
1039 if (it != GetState().item_by_ap_id_.end()) {
1040 return it->second;
1041 } else {
1042 return "Unknown";
1043 }
1044}
1045
1046LingoColor GetLingoColorForString(const std::string &str) {
1047 if (str == "black") {
1048 return LingoColor::kBlack;
1049 } else if (str == "red") {
1050 return LingoColor::kRed;
1051 } else if (str == "blue") {
1052 return LingoColor::kBlue;
1053 } else if (str == "yellow") {
1054 return LingoColor::kYellow;
1055 } else if (str == "orange") {
1056 return LingoColor::kOrange;
1057 } else if (str == "green") {
1058 return LingoColor::kGreen;
1059 } else if (str == "gray") {
1060 return LingoColor::kGray;
1061 } else if (str == "brown") {
1062 return LingoColor::kBrown;
1063 } else if (str == "purple") {
1064 return LingoColor::kPurple;
1065 } else {
1066 TrackerLog(fmt::format("Invalid color: {}", str));
1067
1068 return LingoColor::kNone;
1069 }
1070}
diff --git a/src/game_data.h b/src/game_data.h index 31a1e78..ac911e5 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -57,6 +57,7 @@ struct Panel {
57 int ap_location_id = -1; 57 int ap_location_id = -1;
58 bool hunt = false; 58 bool hunt = false;
59 int panel_door = -1; 59 int panel_door = -1;
60 int solve_index = -1;
60}; 61};
61 62
62struct ProgressiveRequirement { 63struct ProgressiveRequirement {
@@ -85,10 +86,10 @@ struct Door {
85}; 86};
86 87
87struct PanelDoor { 88struct PanelDoor {
88 int room;
89 int ap_item_id = -1; 89 int ap_item_id = -1;
90 int group_ap_item_id = -1; 90 int group_ap_item_id = -1;
91 std::vector<ProgressiveRequirement> progressives; 91 std::vector<ProgressiveRequirement> progressives;
92 std::string item_name;
92}; 93};
93 94
94struct Exit { 95struct Exit {
@@ -102,8 +103,10 @@ struct PaintingExit {
102 int id; 103 int id;
103 int room; 104 int room;
104 std::string internal_id; 105 std::string internal_id;
106 std::string display_name;
105 std::optional<int> door; 107 std::optional<int> door;
106 bool entrance = false; 108 bool entrance = false;
109 int map_area;
107}; 110};
108 111
109struct Room { 112struct Room {
@@ -154,8 +157,9 @@ struct SubwayItem {
154 int id; 157 int id;
155 int x; 158 int x;
156 int y; 159 int y;
160 bool tilted = false;
157 std::optional<int> door; 161 std::optional<int> door;
158 std::vector<std::string> paintings; 162 std::optional<std::string> painting;
159 std::vector<std::string> tags; // 2-way teleports 163 std::vector<std::string> tags; // 2-way teleports
160 std::vector<std::string> entrances; // teleport entrances 164 std::vector<std::string> entrances; // teleport entrances
161 std::vector<std::string> exits; // teleport exits 165 std::vector<std::string> exits; // teleport exits
@@ -173,7 +177,9 @@ const std::vector<Door>& GD_GetDoors();
173const Door& GD_GetDoor(int door_id); 177const Door& GD_GetDoor(int door_id);
174int GD_GetDoorByName(const std::string& name); 178int GD_GetDoorByName(const std::string& name);
175const Panel& GD_GetPanel(int panel_id); 179const Panel& GD_GetPanel(int panel_id);
180int GD_GetPanelBySolveIndex(int solve_index);
176const PanelDoor& GD_GetPanelDoor(int panel_door_id); 181const PanelDoor& GD_GetPanelDoor(int panel_door_id);
182const std::vector<PaintingExit>& GD_GetPaintings();
177const PaintingExit& GD_GetPaintingExit(int painting_id); 183const PaintingExit& GD_GetPaintingExit(int painting_id);
178int GD_GetPaintingByName(const std::string& name); 184int GD_GetPaintingByName(const std::string& name);
179const std::vector<int>& GD_GetAchievementPanels(); 185const std::vector<int>& GD_GetAchievementPanels();
@@ -184,5 +190,8 @@ const std::vector<SubwayItem>& GD_GetSubwayItems();
184const SubwayItem& GD_GetSubwayItem(int id); 190const SubwayItem& GD_GetSubwayItem(int id);
185std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); 191std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
186int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 192int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
193std::string GD_GetItemName(int id);
194
195LingoColor GetLingoColorForString(const std::string& str);
187 196
188#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 197#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/global.cpp b/src/global.cpp index 1eb3f8d..63f4a19 100644 --- a/src/global.cpp +++ b/src/global.cpp
@@ -26,17 +26,19 @@ std::string GetAbsolutePath(std::string_view path) {
26 return (GetExecutableDirectory() / path).string(); 26 return (GetExecutableDirectory() / path).string();
27} 27}
28 28
29bool IsLocationWinCondition(const Location& location) { 29std::string GetWinCondition() {
30 switch (AP_GetVictoryCondition()) { 30 switch (AP_GetVictoryCondition()) {
31 case kTHE_END: 31 case kTHE_END:
32 return location.ap_location_name == 32 return "Orange Tower Seventh Floor - THE END";
33 "Orange Tower Seventh Floor - THE END";
34 case kTHE_MASTER: 33 case kTHE_MASTER:
35 return location.ap_location_name == 34 return "Orange Tower Seventh Floor - THE MASTER";
36 "Orange Tower Seventh Floor - THE MASTER";
37 case kLEVEL_2: 35 case kLEVEL_2:
38 return location.ap_location_name == "Second Room - LEVEL 2"; 36 return "Second Room - LEVEL 2";
39 case kPILGRIMAGE: 37 case kPILGRIMAGE:
40 return location.ap_location_name == "Pilgrim Antechamber - PILGRIM"; 38 return "Pilgrim Antechamber - PILGRIM";
41 } 39 }
42} 40}
41
42bool IsLocationWinCondition(const Location& location) {
43 return location.ap_location_name == GetWinCondition();
44}
diff --git a/src/global.h b/src/global.h index 31ebde3..bdfa7ae 100644 --- a/src/global.h +++ b/src/global.h
@@ -10,6 +10,8 @@ const std::filesystem::path& GetExecutableDirectory();
10 10
11std::string GetAbsolutePath(std::string_view path); 11std::string GetAbsolutePath(std::string_view path);
12 12
13std::string GetWinCondition();
14
13bool IsLocationWinCondition(const Location& location); 15bool IsLocationWinCondition(const Location& location);
14 16
15#endif /* end of include guard: GLOBAL_H_44945DBA */ 17#endif /* end of include guard: GLOBAL_H_44945DBA */
diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp deleted file mode 100644 index 152b9ef..0000000 --- a/src/godot_variant.cpp +++ /dev/null
@@ -1,84 +0,0 @@
1// Godot save decoder algorithm by Chris Souvey.
2
3#include "godot_variant.h"
4
5#include <algorithm>
6#include <charconv>
7#include <cstddef>
8#include <cstdint>
9#include <fstream>
10#include <string>
11#include <tuple>
12#include <variant>
13#include <vector>
14
15namespace {
16
17uint16_t ReadUint16(std::basic_istream<char>& stream) {
18 uint16_t result;
19 stream.read(reinterpret_cast<char*>(&result), 2);
20 return result;
21}
22
23uint32_t ReadUint32(std::basic_istream<char>& stream) {
24 uint32_t result;
25 stream.read(reinterpret_cast<char*>(&result), 4);
26 return result;
27}
28
29GodotVariant ParseVariant(std::basic_istream<char>& stream) {
30 uint16_t type = ReadUint16(stream);
31 stream.ignore(2);
32
33 switch (type) {
34 case 1: {
35 // bool
36 bool boolval = (ReadUint32(stream) == 1);
37 return {boolval};
38 }
39 case 15: {
40 // nodepath
41 uint32_t name_length = ReadUint32(stream) & 0x7fffffff;
42 uint32_t subname_length = ReadUint32(stream) & 0x7fffffff;
43 uint32_t flags = ReadUint32(stream);
44
45 std::vector<std::string> result;
46 for (size_t i = 0; i < name_length + subname_length; i++) {
47 uint32_t char_length = ReadUint32(stream);
48 uint32_t padded_length = (char_length % 4 == 0)
49 ? char_length
50 : (char_length + 4 - (char_length % 4));
51 std::vector<char> next_bytes(padded_length);
52 stream.read(next_bytes.data(), padded_length);
53 std::string next_piece;
54 std::copy(next_bytes.begin(),
55 std::next(next_bytes.begin(), char_length),
56 std::back_inserter(next_piece));
57 result.push_back(next_piece);
58 }
59
60 return {result};
61 }
62 case 19: {
63 // array
64 uint32_t length = ReadUint32(stream) & 0x7fffffff;
65 std::vector<GodotVariant> result;
66 for (size_t i = 0; i < length; i++) {
67 result.push_back(ParseVariant(stream));
68 }
69 return {result};
70 }
71 default: {
72 // eh
73 return {std::monostate{}};
74 }
75 }
76}
77
78} // namespace
79
80GodotVariant ParseGodotFile(std::string filename) {
81 std::ifstream file_stream(filename, std::ios_base::binary);
82 file_stream.ignore(4);
83 return ParseVariant(file_stream);
84}
diff --git a/src/godot_variant.h b/src/godot_variant.h deleted file mode 100644 index 620e569..0000000 --- a/src/godot_variant.h +++ /dev/null
@@ -1,28 +0,0 @@
1#ifndef GODOT_VARIANT_H_ED7F2EB6
2#define GODOT_VARIANT_H_ED7F2EB6
3
4#include <string>
5#include <variant>
6#include <vector>
7
8struct GodotVariant {
9 using value_type = std::variant<std::monostate, bool, std::vector<std::string>, std::vector<GodotVariant>>;
10
11 value_type value;
12
13 GodotVariant(value_type v) : value(v) {}
14
15 bool AsBool() const { return std::get<bool>(value); }
16
17 const std::vector<std::string>& AsNodePath() const {
18 return std::get<std::vector<std::string>>(value);
19 }
20
21 const std::vector<GodotVariant>& AsArray() const {
22 return std::get<std::vector<GodotVariant>>(value);
23 }
24};
25
26GodotVariant ParseGodotFile(std::string filename);
27
28#endif /* end of include guard: GODOT_VARIANT_H_ED7F2EB6 */
diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..87ba037 --- /dev/null +++ b/src/icons.cpp
@@ -0,0 +1,22 @@
1#include "icons.h"
2
3#include "global.h"
4
5const wxBitmap* IconCache::GetIcon(const std::string& filename, wxSize size) {
6 std::tuple<std::string, int, int> key = {filename, size.x, size.y};
7
8 if (!icons_.count(key)) {
9 icons_[key] =
10 wxBitmap(wxImage(GetAbsolutePath(filename).c_str(),
11 wxBITMAP_TYPE_PNG)
12 .Scale(size.x, size.y));
13 }
14
15 return &icons_.at(key);
16}
17
18static IconCache* ICON_CACHE_INSTANCE = nullptr;
19
20void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; }
21
22IconCache& GetTheIconCache() { return *ICON_CACHE_INSTANCE; }
diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 0000000..23dca2a --- /dev/null +++ b/src/icons.h
@@ -0,0 +1,25 @@
1#ifndef ICONS_H_B95159A6
2#define ICONS_H_B95159A6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <map>
11#include <string>
12#include <tuple>
13
14class IconCache {
15 public:
16 const wxBitmap* GetIcon(const std::string& filename, wxSize size);
17
18 private:
19 std::map<std::tuple<std::string, int, int>, wxBitmap> icons_;
20};
21
22void SetTheIconCache(IconCache* instance);
23IconCache& GetTheIconCache();
24
25#endif /* end of include guard: ICONS_H_B95159A6 */
diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp index f17c2d8..6763b7f 100644 --- a/src/ipc_dialog.cpp +++ b/src/ipc_dialog.cpp
@@ -12,13 +12,14 @@ IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") {
12 address_value = GetTrackerConfig().ipc_address; 12 address_value = GetTrackerConfig().ipc_address;
13 } 13 }
14 14
15 address_box_ = 15 address_box_ = new wxTextCtrl(this, -1, wxString::FromUTF8(address_value),
16 new wxTextCtrl(this, -1, address_value, wxDefaultPosition, {300, -1}); 16 wxDefaultPosition, FromDIP(wxSize{300, -1}));
17 17
18 wxButton* reset_button = new wxButton(this, -1, "Use Default"); 18 wxButton* reset_button = new wxButton(this, -1, "Use Default");
19 reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this); 19 reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this);
20 20
21 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(3, 10, 10); 21 wxFlexGridSizer* form_sizer =
22 new wxFlexGridSizer(3, FromDIP(10), FromDIP(10));
22 form_sizer->Add( 23 form_sizer->Add(
23 new wxStaticText(this, -1, "Address:"), 24 new wxStaticText(this, -1, "Address:"),
24 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); 25 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h index 1caed01..a8c4512 100644 --- a/src/ipc_dialog.h +++ b/src/ipc_dialog.h
@@ -13,7 +13,7 @@ class IpcDialog : public wxDialog {
13 public: 13 public:
14 IpcDialog(); 14 IpcDialog();
15 15
16 std::string GetIpcAddress() { return address_box_->GetValue().ToStdString(); } 16 std::string GetIpcAddress() { return address_box_->GetValue().utf8_string(); }
17 17
18 private: 18 private:
19 void OnResetClicked(wxCommandEvent& event); 19 void OnResetClicked(wxCommandEvent& event);
diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp index 1f8d286..6e2a440 100644 --- a/src/ipc_state.cpp +++ b/src/ipc_state.cpp
@@ -39,7 +39,6 @@ struct IPCState {
39 std::string game_ap_user; 39 std::string game_ap_user;
40 40
41 std::optional<std::tuple<int, int>> player_position; 41 std::optional<std::tuple<int, int>> player_position;
42 std::set<std::string> solved_panels;
43 42
44 // Thread state 43 // Thread state
45 std::unique_ptr<wswrap::WS> ws; 44 std::unique_ptr<wswrap::WS> ws;
@@ -103,12 +102,6 @@ struct IPCState {
103 return player_position; 102 return player_position;
104 } 103 }
105 104
106 std::set<std::string> GetSolvedPanels() {
107 std::lock_guard state_guard(state_mutex);
108
109 return solved_panels;
110 }
111
112 private: 105 private:
113 void Thread() { 106 void Thread() {
114 for (;;) { 107 for (;;) {
@@ -134,7 +127,6 @@ struct IPCState {
134 game_ap_user.clear(); 127 game_ap_user.clear();
135 128
136 player_position = std::nullopt; 129 player_position = std::nullopt;
137 solved_panels.clear();
138 130
139 if (address.empty()) { 131 if (address.empty()) {
140 initialized = false; 132 initialized = false;
@@ -273,7 +265,6 @@ struct IPCState {
273 265
274 slot_matches = false; 266 slot_matches = false;
275 player_position = std::nullopt; 267 player_position = std::nullopt;
276 solved_panels.clear();
277 } 268 }
278 } 269 }
279 270
@@ -313,15 +304,7 @@ struct IPCState {
313 player_position = 304 player_position =
314 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]); 305 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);
315 306
316 tracker_frame->RedrawPosition(); 307 tracker_frame->UpdateIndicators(StateUpdate{.player_position = true});
317 } else if (msg["cmd"] == "SolvePanels") {
318 std::lock_guard state_guard(state_mutex);
319
320 for (std::string panel : msg["panels"]) {
321 solved_panels.insert(std::move(panel));
322 }
323
324 tracker_frame->UpdateIndicators(kUPDATE_ONLY_PANELS);
325 } 308 }
326 } 309 }
327 310
@@ -382,7 +365,3 @@ bool IPC_IsConnected() { return GetState().IsConnected(); }
382std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { 365std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
383 return GetState().GetPlayerPosition(); 366 return GetState().GetPlayerPosition();
384} 367}
385
386std::set<std::string> IPC_GetSolvedPanels() {
387 return GetState().GetSolvedPanels();
388}
diff --git a/src/ipc_state.h b/src/ipc_state.h index 7c9d68d..0e6fa51 100644 --- a/src/ipc_state.h +++ b/src/ipc_state.h
@@ -20,6 +20,4 @@ bool IPC_IsConnected();
20 20
21std::optional<std::tuple<int, int>> IPC_GetPlayerPosition(); 21std::optional<std::tuple<int, int>> IPC_GetPlayerPosition();
22 22
23std::set<std::string> IPC_GetSolvedPanels();
24
25#endif /* end of include guard: IPC_STATE_H_6B3B0958 */ 23#endif /* end of include guard: IPC_STATE_H_6B3B0958 */
diff --git a/src/items_pane.cpp b/src/items_pane.cpp new file mode 100644 index 0000000..055eec0 --- /dev/null +++ b/src/items_pane.cpp
@@ -0,0 +1,145 @@
1#include "items_pane.h"
2
3#include <map>
4
5namespace {
6
7enum SortInstruction {
8 SI_NONE = 0,
9 SI_ASC = 1 << 0,
10 SI_DESC = 1 << 1,
11 SI_NAME = 1 << 2,
12 SI_AMOUNT = 1 << 3,
13 SI_ORDER = 1 << 4,
14};
15
16inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) {
17 return static_cast<SortInstruction>(static_cast<int>(lhs) |
18 static_cast<int>(rhs));
19}
20
21template <typename T>
22int ItemCompare(const T& lhs, const T& rhs, bool ascending) {
23 if (lhs < rhs) {
24 return ascending ? -1 : 1;
25 } else if (lhs > rhs) {
26 return ascending ? 1 : -1;
27 } else {
28 return 0;
29 }
30}
31
32int wxCALLBACK RowCompare(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) {
33 const ItemState& lhs = *reinterpret_cast<const ItemState*>(item1);
34 const ItemState& rhs = *reinterpret_cast<const ItemState*>(item2);
35 SortInstruction instruction = static_cast<SortInstruction>(sortData);
36
37 bool ascending = (instruction & SI_ASC) != 0;
38 if ((instruction & SI_NAME) != 0) {
39 return ItemCompare(lhs.name, rhs.name, ascending);
40 } else if ((instruction & SI_AMOUNT) != 0) {
41 return ItemCompare(lhs.amount, rhs.amount, ascending);
42 } else if ((instruction & SI_ORDER) != 0) {
43 return ItemCompare(lhs.index, rhs.index, ascending);
44 } else {
45 return 0;
46 }
47}
48
49} // namespace
50
51ItemsPane::ItemsPane(wxWindow* parent)
52 : wxListView(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
53 wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_HRULES) {
54 AppendColumn("Item", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
55 AppendColumn("Amount", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
56 AppendColumn("Order", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
57
58 Bind(wxEVT_LIST_COL_CLICK, &ItemsPane::OnColClick, this);
59 Bind(wxEVT_DPI_CHANGED, &ItemsPane::OnDPIChanged, this);
60}
61
62void ItemsPane::ResetIndicators() {
63 DeleteAllItems();
64 items_.clear();
65}
66
67void ItemsPane::UpdateIndicators(const std::vector<ItemState>& items) {
68 std::map<std::string, ItemState> items_by_name;
69
70 for (const ItemState& item : items) {
71 items_by_name[item.name] = item;
72 }
73
74 for (int i = 0; i < GetItemCount(); i++) {
75 std::string item_name = GetItemText(i).utf8_string();
76 auto it = items_by_name.find(item_name);
77
78 if (it != items_by_name.end()) {
79 SetItem(i, 1, std::to_string(it->second.amount));
80 SetItem(i, 2, std::to_string(it->second.index));
81
82 *reinterpret_cast<ItemState*>(GetItemData(i)) = it->second;
83
84 items_by_name.erase(item_name);
85 }
86 }
87
88 for (const auto& [name, item] : items_by_name) {
89 int i = InsertItem(GetItemCount(), name);
90 SetItem(i, 1, std::to_string(item.amount));
91 SetItem(i, 2, std::to_string(item.index));
92
93 auto item_ptr = std::make_unique<ItemState>(item);
94 SetItemPtrData(i, reinterpret_cast<wxUIntPtr>(item_ptr.get()));
95 items_.push_back(std::move(item_ptr));
96 }
97
98 SetColumnWidth(0, wxLIST_AUTOSIZE);
99 SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
100 SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER);
101
102 if (GetSortIndicator() != -1) {
103 DoSort(GetSortIndicator(), IsAscendingSortIndicator());
104 }
105}
106
107void ItemsPane::OnColClick(wxListEvent& event) {
108 int col = event.GetColumn();
109 if (col == -1) {
110 return;
111 }
112
113 bool ascending = GetUpdatedAscendingSortIndicator(col);
114
115 DoSort(col, ascending);
116}
117
118void ItemsPane::OnDPIChanged(wxDPIChangedEvent& event) {
119 SetColumnWidth(0, wxLIST_AUTOSIZE);
120 SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
121 SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER);
122
123 event.Skip();
124}
125
126void ItemsPane::DoSort(int col, bool ascending) {
127 SortInstruction instruction = SI_NONE;
128 if (ascending) {
129 instruction = instruction | SI_ASC;
130 } else {
131 instruction = instruction | SI_DESC;
132 }
133
134 if (col == 0) {
135 instruction = instruction | SI_NAME;
136 } else if (col == 1) {
137 instruction = instruction | SI_AMOUNT;
138 } else if (col == 2) {
139 instruction = instruction | SI_ORDER;
140 }
141
142 if (SortItems(RowCompare, instruction)) {
143 ShowSortIndicator(col, ascending);
144 }
145}
diff --git a/src/items_pane.h b/src/items_pane.h new file mode 100644 index 0000000..aa09c49 --- /dev/null +++ b/src/items_pane.h
@@ -0,0 +1,33 @@
1#ifndef ITEMS_PANE_H_EB637EE3
2#define ITEMS_PANE_H_EB637EE3
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <wx/listctrl.h>
11
12#include <memory>
13#include <vector>
14
15#include "ap_state.h"
16
17class ItemsPane : public wxListView {
18 public:
19 explicit ItemsPane(wxWindow* parent);
20
21 void ResetIndicators();
22 void UpdateIndicators(const std::vector<ItemState>& items);
23
24 private:
25 void OnColClick(wxListEvent& event);
26 void OnDPIChanged(wxDPIChangedEvent& event);
27
28 void DoSort(int col, bool ascending);
29
30 std::vector<std::unique_ptr<ItemState>> items_;
31};
32
33#endif /* end of include guard: ITEMS_PANE_H_EB637EE3 */
diff --git a/src/log_dialog.cpp b/src/log_dialog.cpp new file mode 100644 index 0000000..3f0a8ad --- /dev/null +++ b/src/log_dialog.cpp
@@ -0,0 +1,37 @@
1#include "log_dialog.h"
2
3#include "logger.h"
4
5wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent);
6
7LogDialog::LogDialog(wxWindow* parent)
8 : wxDialog(parent, wxID_ANY, "Debug Log", wxDefaultPosition, wxDefaultSize,
9 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
10 SetSize(FromDIP(wxSize{512, 280}));
11
12 text_area_ =
13 new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize,
14 wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP);
15 text_area_->SetValue(TrackerReadPastLog());
16
17 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
18 top_sizer->Add(text_area_,
19 wxSizerFlags().DoubleBorder().Expand().Proportion(1));
20
21 SetSizer(top_sizer);
22
23 Bind(LOG_MESSAGE, &LogDialog::OnLogMessage, this);
24}
25
26void LogDialog::LogMessage(const std::string& message) {
27 wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE);
28 event->SetString(message);
29 QueueEvent(event);
30}
31
32void LogDialog::OnLogMessage(wxCommandEvent& event) {
33 if (!text_area_->IsEmpty()) {
34 text_area_->AppendText("\n");
35 }
36 text_area_->AppendText(event.GetString());
37}
diff --git a/src/log_dialog.h b/src/log_dialog.h new file mode 100644 index 0000000..c29251f --- /dev/null +++ b/src/log_dialog.h
@@ -0,0 +1,24 @@
1#ifndef LOG_DIALOG_H_EEFD45B6
2#define LOG_DIALOG_H_EEFD45B6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent);
11
12class LogDialog : public wxDialog {
13 public:
14 explicit LogDialog(wxWindow* parent);
15
16 void LogMessage(const std::string& message);
17
18 private:
19 void OnLogMessage(wxCommandEvent& event);
20
21 wxTextCtrl* text_area_;
22};
23
24#endif /* end of include guard: LOG_DIALOG_H_EEFD45B6 */
diff --git a/src/logger.cpp b/src/logger.cpp index 09fc331..8a08b58 100644 --- a/src/logger.cpp +++ b/src/logger.cpp
@@ -3,8 +3,10 @@
3#include <chrono> 3#include <chrono>
4#include <fstream> 4#include <fstream>
5#include <mutex> 5#include <mutex>
6#include <sstream>
6 7
7#include "global.h" 8#include "global.h"
9#include "log_dialog.h"
8 10
9namespace { 11namespace {
10 12
@@ -14,19 +16,49 @@ class Logger {
14 16
15 void LogLine(const std::string& text) { 17 void LogLine(const std::string& text) {
16 std::lock_guard guard(file_mutex_); 18 std::lock_guard guard(file_mutex_);
17 logfile_ << "[" << std::chrono::system_clock::now() << "] " << text 19 std::ostringstream line;
18 << std::endl; 20 line << "[" << std::chrono::system_clock::now() << "] " << text;
21
22 logfile_ << line.str() << std::endl;
19 logfile_.flush(); 23 logfile_.flush();
24
25 if (log_dialog_ != nullptr) {
26 log_dialog_->LogMessage(line.str());
27 }
28 }
29
30 std::string GetContents() {
31 std::lock_guard guard(file_mutex_);
32
33 std::ifstream file_in(GetAbsolutePath("debug.log"));
34 std::ostringstream buffer;
35 buffer << file_in.rdbuf();
36
37 return buffer.str();
38 }
39
40 void SetLogDialog(LogDialog* log_dialog) {
41 std::lock_guard guard(file_mutex_);
42 log_dialog_ = log_dialog;
20 } 43 }
21 44
22 private: 45 private:
23 std::ofstream logfile_; 46 std::ofstream logfile_;
24 std::mutex file_mutex_; 47 std::mutex file_mutex_;
48 LogDialog* log_dialog_ = nullptr;
25}; 49};
26 50
51Logger& GetLogger() {
52 static Logger* instance = new Logger();
53 return *instance;
54}
55
27} // namespace 56} // namespace
28 57
29void TrackerLog(std::string text) { 58void TrackerLog(std::string text) { GetLogger().LogLine(text); }
30 static Logger* instance = new Logger(); 59
31 instance->LogLine(text); 60std::string TrackerReadPastLog() { return GetLogger().GetContents(); }
61
62void TrackerSetLogDialog(LogDialog* log_dialog) {
63 GetLogger().SetLogDialog(log_dialog);
32} 64}
diff --git a/src/logger.h b/src/logger.h index a27839f..f669790 100644 --- a/src/logger.h +++ b/src/logger.h
@@ -3,6 +3,12 @@
3 3
4#include <string> 4#include <string>
5 5
6class LogDialog;
7
6void TrackerLog(std::string message); 8void TrackerLog(std::string message);
7 9
10std::string TrackerReadPastLog();
11
12void TrackerSetLogDialog(LogDialog* log_dialog);
13
8#endif /* end of include guard: LOGGER_H_9BDD07EA */ 14#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index 1d7cc9e..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -10,7 +10,7 @@
10 10
11class TrackerApp : public wxApp { 11class TrackerApp : public wxApp {
12 public: 12 public:
13 virtual bool OnInit() { 13 virtual bool OnInit() override {
14 GetTrackerConfig().Load(); 14 GetTrackerConfig().Load();
15 15
16 TrackerFrame *frame = new TrackerFrame(); 16 TrackerFrame *frame = new TrackerFrame();
diff --git a/src/options_pane.cpp b/src/options_pane.cpp new file mode 100644 index 0000000..844e145 --- /dev/null +++ b/src/options_pane.cpp
@@ -0,0 +1,71 @@
1#include "options_pane.h"
2
3#include "ap_state.h"
4
5namespace {
6
7const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"};
8const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"};
9const char* kPanelShuffleLabels[] = {"None", "Rearrange"};
10const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2",
11 "Pilgrimage"};
12const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock",
13 "Individual", "Progressive"};
14
15void AddRow(wxDataViewListCtrl* list, const std::string& text) {
16 wxVector<wxVariant> data;
17 data.push_back(wxVariant{text + ": "});
18 data.push_back(wxVariant{""});
19 list->AppendItem(data);
20}
21
22} // namespace
23
24OptionsPane::OptionsPane(wxWindow* parent)
25 : wxDataViewListCtrl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
26 wxDV_ROW_LINES) {
27 AppendTextColumn("Name", wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE);
28 AppendTextColumn("Value");
29 AddRow(this, "Shuffle Doors");
30 AddRow(this, "Group Doors");
31 AddRow(this, "Location Checks");
32 AddRow(this, "Shuffle Colors");
33 AddRow(this, "Shuffle Panels");
34 AddRow(this, "Shuffle Paintings");
35 AddRow(this, "Victory Condition");
36 AddRow(this, "Early Color Hallways");
37 AddRow(this, "Shuffle Postgame");
38 AddRow(this, "Enable Pilgrimage");
39 AddRow(this, "Pilgrimage Roof Access");
40 AddRow(this, "Pilgrimage Paintings");
41 AddRow(this, "Sunwarp Access");
42 AddRow(this, "Shuffle Sunwarps");
43 AddRow(this, "Mastery Achievements");
44 AddRow(this, "Level 2 Requirement");
45}
46
47void OptionsPane::OnConnect() {
48 SetTextValue(kDoorShuffleLabels[static_cast<size_t>(AP_GetDoorShuffleMode())],
49 0, 1);
50 SetTextValue(AP_AreDoorsGrouped() ? "Yes" : "No", 1, 1);
51 SetTextValue(
52 kLocationChecksLabels[static_cast<size_t>(AP_GetLocationsChecks())], 2,
53 1);
54 SetTextValue(AP_IsColorShuffle() ? "Yes" : "No", 3, 1);
55 SetTextValue(
56 kPanelShuffleLabels[static_cast<size_t>(AP_GetPanelShuffleMode())], 4, 1);
57 SetTextValue(AP_IsPaintingShuffle() ? "Yes" : "No", 5, 1);
58 SetTextValue(
59 kVictoryConditionLabels[static_cast<size_t>(AP_GetVictoryCondition())], 6,
60 1);
61 SetTextValue(AP_HasEarlyColorHallways() ? "Yes" : "No", 7, 1);
62 SetTextValue(AP_IsPostgameShuffle() ? "Yes" : "No", 8, 1);
63 SetTextValue(AP_IsPilgrimageEnabled() ? "Yes" : "No", 9, 1);
64 SetTextValue(AP_DoesPilgrimageAllowRoofAccess() ? "Yes" : "No", 10, 1);
65 SetTextValue(AP_DoesPilgrimageAllowPaintings() ? "Yes" : "No", 11, 1);
66 SetTextValue(kSunwarpAccessLabels[static_cast<size_t>(AP_GetSunwarpAccess())],
67 12, 1);
68 SetTextValue(AP_IsSunwarpShuffle() ? "Yes" : "No", 13, 1);
69 SetTextValue(std::to_string(AP_GetMasteryRequirement()), 14, 1);
70 SetTextValue(std::to_string(AP_GetLevel2Requirement()), 15, 1);
71}
diff --git a/src/options_pane.h b/src/options_pane.h new file mode 100644 index 0000000..e9df9f0 --- /dev/null +++ b/src/options_pane.h
@@ -0,0 +1,19 @@
1#ifndef OPTIONS_PANE_H_026A0EC0
2#define OPTIONS_PANE_H_026A0EC0
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <wx/dataview.h>
11
12class OptionsPane : public wxDataViewListCtrl {
13 public:
14 explicit OptionsPane(wxWindow* parent);
15
16 void OnConnect();
17};
18
19#endif /* end of include guard: OPTIONS_PANE_H_026A0EC0 */
diff --git a/src/paintings_pane.cpp b/src/paintings_pane.cpp new file mode 100644 index 0000000..bf5d71b --- /dev/null +++ b/src/paintings_pane.cpp
@@ -0,0 +1,86 @@
1#include "paintings_pane.h"
2
3#include <fmt/core.h>
4#include <wx/dataview.h>
5
6#include <map>
7#include <set>
8
9#include "ap_state.h"
10#include "game_data.h"
11#include "tracker_state.h"
12
13namespace {
14
15std::string GetPaintingDisplayName(const std::string& id) {
16 const PaintingExit& painting = GD_GetPaintingExit(GD_GetPaintingByName(id));
17 const MapArea& map_area = GD_GetMapArea(painting.map_area);
18
19 return fmt::format("{} - {}", map_area.name, painting.display_name);
20}
21
22} // namespace
23
24PaintingsPane::PaintingsPane(wxWindow* parent) : wxPanel(parent, wxID_ANY) {
25 wxStaticText* label = new wxStaticText(
26 this, wxID_ANY, "Shuffled paintings grouped by destination:");
27 tree_ctrl_ = new wxDataViewTreeCtrl(this, wxID_ANY);
28
29 reveal_btn_ = new wxButton(this, wxID_ANY, "Reveal shuffled paintings");
30 reveal_btn_->Disable();
31
32 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
33 top_sizer->Add(label, wxSizerFlags().Border());
34 top_sizer->Add(tree_ctrl_, wxSizerFlags().Expand().Proportion(1));
35 top_sizer->Add(reveal_btn_, wxSizerFlags().Border().Expand());
36
37 SetSizerAndFit(top_sizer);
38
39 tree_ctrl_->Bind(wxEVT_DATAVIEW_ITEM_START_EDITING,
40 &PaintingsPane::OnStartEditingCell, this);
41 reveal_btn_->Bind(wxEVT_BUTTON, &PaintingsPane::OnClickRevealPaintings, this);
42}
43
44void PaintingsPane::ResetIndicators() {
45 tree_ctrl_->DeleteAllItems();
46 reveal_btn_->Enable(AP_IsPaintingShuffle());
47}
48
49void PaintingsPane::UpdateIndicators(const std::vector<std::string>&) {
50 // TODO: Optimize this by using the paintings delta.
51
52 tree_ctrl_->DeleteAllItems();
53
54 std::map<std::string, std::set<std::string>> grouped_paintings;
55
56 for (const auto& [from, to] : AP_GetPaintingMapping()) {
57 if (IsPaintingReachable(GD_GetPaintingByName(from)) &&
58 AP_IsPaintingChecked(from)) {
59 grouped_paintings[GetPaintingDisplayName(to)].insert(
60 GetPaintingDisplayName(from));
61 }
62 }
63
64 for (const auto& [to, froms] : grouped_paintings) {
65 wxDataViewItem tree_branch =
66 tree_ctrl_->AppendContainer(wxDataViewItem(0), to);
67
68 for (const std::string& from : froms) {
69 tree_ctrl_->AppendItem(tree_branch, from);
70 }
71 }
72}
73
74void PaintingsPane::OnClickRevealPaintings(wxCommandEvent& event) {
75 if (wxMessageBox("Clicking yes will reveal the mapping between all shuffled "
76 "paintings. This is usually considered a spoiler, and is "
77 "likely not allowed during competitions. This action is not "
78 "reversible. Are you sure you want to proceed?",
79 "Warning", wxYES_NO | wxICON_WARNING) == wxNO) {
80 return;
81 }
82
83 AP_RevealPaintings();
84}
85
86void PaintingsPane::OnStartEditingCell(wxDataViewEvent& event) { event.Veto(); }
diff --git a/src/paintings_pane.h b/src/paintings_pane.h new file mode 100644 index 0000000..1d14510 --- /dev/null +++ b/src/paintings_pane.h
@@ -0,0 +1,28 @@
1#ifndef PAINTINGS_PANE_H_815370D2
2#define PAINTINGS_PANE_H_815370D2
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class wxDataViewEvent;
11class wxDataViewTreeCtrl;
12
13class PaintingsPane : public wxPanel {
14 public:
15 explicit PaintingsPane(wxWindow* parent);
16
17 void ResetIndicators();
18 void UpdateIndicators(const std::vector<std::string>& paintings);
19
20 private:
21 void OnClickRevealPaintings(wxCommandEvent& event);
22 void OnStartEditingCell(wxDataViewEvent& event);
23
24 wxDataViewTreeCtrl* tree_ctrl_;
25 wxButton* reveal_btn_;
26};
27
28#endif /* end of include guard: PAINTINGS_PANE_H_815370D2 */
diff --git a/src/report_popup.cpp b/src/report_popup.cpp new file mode 100644 index 0000000..703e87f --- /dev/null +++ b/src/report_popup.cpp
@@ -0,0 +1,131 @@
1#include "report_popup.h"
2
3#include <wx/dcbuffer.h>
4
5#include <map>
6#include <string>
7
8#include "global.h"
9#include "icons.h"
10#include "tracker_state.h"
11
12ReportPopup::ReportPopup(wxWindow* parent)
13 : wxScrolledCanvas(parent, wxID_ANY) {
14 SetBackgroundStyle(wxBG_STYLE_PAINT);
15
16 LoadIcons();
17
18 // TODO: This is slow on high-DPI screens.
19 SetScrollRate(5, 5);
20
21 SetBackgroundColour(*wxBLACK);
22 Hide();
23
24 Bind(wxEVT_PAINT, &ReportPopup::OnPaint, this);
25 Bind(wxEVT_DPI_CHANGED, &ReportPopup::OnDPIChanged, this);
26}
27
28void ReportPopup::SetDoorId(int door_id) {
29 door_id_ = door_id;
30
31 ResetIndicators();
32}
33
34void ReportPopup::Reset() {
35 door_id_ = -1;
36}
37
38void ReportPopup::ResetIndicators() {
39 if (door_id_ == -1) {
40 return;
41 }
42
43 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
44 const std::map<std::string, bool>& report = GetDoorRequirements(door_id_);
45
46 wxMemoryDC mem_dc;
47 mem_dc.SetFont(the_font);
48
49 int acc_height = FromDIP(10);
50 int col_width = 0;
51
52 for (const auto& [text, obtained] : report) {
53 wxSize item_extent = mem_dc.GetTextExtent(text);
54 int item_height =
55 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
56 acc_height += item_height;
57
58 if (item_extent.GetWidth() > col_width) {
59 col_width = item_extent.GetWidth();
60 }
61 }
62
63 int item_width = col_width + FromDIP(10 + 32);
64 full_width_ = item_width + FromDIP(20);
65 full_height_ = acc_height;
66
67 Fit();
68 SetVirtualSize(full_width_, full_height_);
69
70 UpdateIndicators();
71}
72
73void ReportPopup::UpdateIndicators() {
74 if (door_id_ == -1) {
75 return;
76 }
77
78 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
79 const std::map<std::string, bool>& report = GetDoorRequirements(door_id_);
80
81 rendered_ = wxBitmap(full_width_, full_height_);
82
83 wxMemoryDC mem_dc;
84 mem_dc.SelectObject(rendered_);
85 mem_dc.SetPen(*wxTRANSPARENT_PEN);
86 mem_dc.SetBrush(*wxBLACK_BRUSH);
87 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
88
89 mem_dc.SetFont(the_font);
90
91 int cur_height = FromDIP(10);
92
93 for (const auto& [text, obtained] : report) {
94 const wxBitmap* eye_ptr = obtained ? checked_eye_ : unchecked_eye_;
95
96 mem_dc.DrawBitmap(*eye_ptr, wxPoint{FromDIP(10), cur_height});
97
98 mem_dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
99 wxSize item_extent = mem_dc.GetTextExtent(text);
100 mem_dc.DrawText(
101 text, wxPoint{FromDIP(10 + 32 + 10),
102 cur_height +
103 (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
104
105 cur_height += FromDIP(10 + 32);
106 }
107}
108
109void ReportPopup::OnPaint(wxPaintEvent& event) {
110 if (door_id_ != -1) {
111 wxBufferedPaintDC dc(this);
112 PrepareDC(dc);
113 dc.DrawBitmap(rendered_, 0, 0);
114 }
115
116 event.Skip();
117}
118
119void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) {
120 LoadIcons();
121 ResetIndicators();
122
123 event.Skip();
124}
125
126void ReportPopup::LoadIcons() {
127 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
128 FromDIP(wxSize{32, 32}));
129 checked_eye_ =
130 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
131}
diff --git a/src/report_popup.h b/src/report_popup.h new file mode 100644 index 0000000..bbb0bef --- /dev/null +++ b/src/report_popup.h
@@ -0,0 +1,38 @@
1#ifndef REPORT_POPUP_H_E065BED4
2#define REPORT_POPUP_H_E065BED4
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class ReportPopup : public wxScrolledCanvas {
11 public:
12 explicit ReportPopup(wxWindow* parent);
13
14 void SetDoorId(int door_id);
15
16 void Reset();
17
18 void ResetIndicators();
19 void UpdateIndicators();
20
21 private:
22 void OnPaint(wxPaintEvent& event);
23 void OnDPIChanged(wxDPIChangedEvent& event);
24
25 void LoadIcons();
26
27 int door_id_ = -1;
28
29 const wxBitmap* unchecked_eye_;
30 const wxBitmap* checked_eye_;
31
32 int full_width_ = 0;
33 int full_height_ = 0;
34
35 wxBitmap rendered_;
36};
37
38#endif /* end of include guard: REPORT_POPUP_H_E065BED4 */
diff --git a/src/settings_dialog.cpp b/src/settings_dialog.cpp index 0321b5a..95df577 100644 --- a/src/settings_dialog.cpp +++ b/src/settings_dialog.cpp
@@ -3,30 +3,43 @@
3#include "tracker_config.h" 3#include "tracker_config.h"
4 4
5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { 5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") {
6 should_check_for_updates_box_ = new wxCheckBox( 6 wxStaticBoxSizer* main_box =
7 this, wxID_ANY, "Check for updates when the tracker opens"); 7 new wxStaticBoxSizer(wxVERTICAL, this, "General settings");
8
9 should_check_for_updates_box_ =
10 new wxCheckBox(main_box->GetStaticBox(), wxID_ANY,
11 "Check for updates when the tracker opens");
8 hybrid_areas_box_ = new wxCheckBox( 12 hybrid_areas_box_ = new wxCheckBox(
9 this, wxID_ANY, 13 main_box->GetStaticBox(), wxID_ANY,
10 "Use two colors to show that an area has partial availability"); 14 "Use two colors to show that an area has partial availability");
11 show_hunt_panels_box_ = new wxCheckBox(this, wxID_ANY, "Show hunt panels"); 15 track_position_box_ = new wxCheckBox(main_box->GetStaticBox(), wxID_ANY,
16 "Track player position");
12 17
13 should_check_for_updates_box_->SetValue( 18 should_check_for_updates_box_->SetValue(
14 GetTrackerConfig().should_check_for_updates); 19 GetTrackerConfig().should_check_for_updates);
15 hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); 20 hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas);
16 show_hunt_panels_box_->SetValue(GetTrackerConfig().show_hunt_panels); 21 track_position_box_->SetValue(GetTrackerConfig().track_position);
22
23 main_box->Add(should_check_for_updates_box_, wxSizerFlags().Border());
24 main_box->AddSpacer(2);
25 main_box->Add(hybrid_areas_box_, wxSizerFlags().Border());
26 main_box->AddSpacer(2);
27 main_box->Add(track_position_box_, wxSizerFlags().Border());
28
29 const wxString visible_panels_choices[] = {"Only show locations",
30 "Show locations and hunt panels",
31 "Show all panels"};
32 visible_panels_box_ =
33 new wxRadioBox(this, wxID_ANY, "Visible panels", wxDefaultPosition,
34 wxDefaultSize, 3, visible_panels_choices, 1);
35 visible_panels_box_->SetSelection(
36 static_cast<int>(GetTrackerConfig().visible_panels));
17 37
18 wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); 38 wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL);
19 39 form_sizer->Add(main_box, wxSizerFlags().Border().Expand());
20 form_sizer->Add(should_check_for_updates_box_, wxSizerFlags().HorzBorder()); 40 form_sizer->Add(visible_panels_box_, wxSizerFlags().Border().Expand());
21 form_sizer->AddSpacer(2); 41 form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
22 42 wxSizerFlags().Center().Border());
23 form_sizer->Add(hybrid_areas_box_, wxSizerFlags().HorzBorder());
24 form_sizer->AddSpacer(2);
25
26 form_sizer->Add(show_hunt_panels_box_, wxSizerFlags().HorzBorder());
27 form_sizer->AddSpacer(2);
28
29 form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center());
30 43
31 SetSizerAndFit(form_sizer); 44 SetSizerAndFit(form_sizer);
32 45
diff --git a/src/settings_dialog.h b/src/settings_dialog.h index d7c1ed3..c4dacfa 100644 --- a/src/settings_dialog.h +++ b/src/settings_dialog.h
@@ -7,6 +7,10 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <wx/radiobox.h>
11
12#include "tracker_config.h"
13
10class SettingsDialog : public wxDialog { 14class SettingsDialog : public wxDialog {
11 public: 15 public:
12 SettingsDialog(); 16 SettingsDialog();
@@ -15,12 +19,17 @@ class SettingsDialog : public wxDialog {
15 return should_check_for_updates_box_->GetValue(); 19 return should_check_for_updates_box_->GetValue();
16 } 20 }
17 bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } 21 bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); }
18 bool GetShowHuntPanels() const { return show_hunt_panels_box_->GetValue(); } 22 TrackerConfig::VisiblePanels GetVisiblePanels() const {
23 return static_cast<TrackerConfig::VisiblePanels>(
24 visible_panels_box_->GetSelection());
25 }
26 bool GetTrackPosition() const { return track_position_box_->GetValue(); }
19 27
20 private: 28 private:
21 wxCheckBox* should_check_for_updates_box_; 29 wxCheckBox* should_check_for_updates_box_;
22 wxCheckBox* hybrid_areas_box_; 30 wxCheckBox* hybrid_areas_box_;
23 wxCheckBox* show_hunt_panels_box_; 31 wxRadioBox* visible_panels_box_;
32 wxCheckBox* track_position_box_;
24}; 33};
25 34
26#endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ 35#endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */
diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 0a250fb..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -9,12 +9,28 @@
9#include "ap_state.h" 9#include "ap_state.h"
10#include "game_data.h" 10#include "game_data.h"
11#include "global.h" 11#include "global.h"
12#include "report_popup.h"
12#include "tracker_state.h" 13#include "tracker_state.h"
13 14
14constexpr int AREA_ACTUAL_SIZE = 21; 15constexpr int AREA_ACTUAL_SIZE = 21;
15constexpr int OWL_ACTUAL_SIZE = 32; 16constexpr int OWL_ACTUAL_SIZE = 32;
17constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11
18constexpr int PAINTING_EXIT_RADIUS = 6;
16 19
17enum class ItemDrawType { kNone, kBox, kOwl }; 20enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit };
21
22namespace {
23
24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
25 if (subway_item.painting) {
26 return {subway_item.x, subway_item.y};
27 } else {
28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
29 subway_item.y + AREA_ACTUAL_SIZE / 2};
30 }
31}
32
33} // namespace
18 34
19SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 35SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
20 SetBackgroundStyle(wxBG_STYLE_PAINT); 36 SetBackgroundStyle(wxBG_STYLE_PAINT);
@@ -31,14 +47,6 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
31 return; 47 return;
32 } 48 }
33 49
34 unchecked_eye_ =
35 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
36 wxBITMAP_TYPE_PNG)
37 .Scale(32, 32));
38 checked_eye_ = wxBitmap(
39 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
40 .Scale(32, 32));
41
42 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( 50 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
43 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), 51 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
44 static_cast<float>(map_image_.GetHeight())}); 52 static_cast<float>(map_image_.GetHeight())});
@@ -57,12 +65,14 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
57 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); 65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
58 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); 66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
59 67
60 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15}); 68 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15}));
61 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); 69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
62 70
63 help_button_ = new wxButton(this, wxID_ANY, "Help"); 71 help_button_ = new wxButton(this, wxID_ANY, "Help");
64 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); 72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
65 SetUpHelpButton(); 73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
66} 76}
67 77
68void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
@@ -73,11 +83,11 @@ void SubwayMap::OnConnect() {
73 std::map<std::string, std::vector<int>> exits; 83 std::map<std::string, std::vector<int>> exits;
74 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
75 if (AP_HasEarlyColorHallways() && 85 if (AP_HasEarlyColorHallways() &&
76 subway_item.special == "starting_room_paintings") { 86 subway_item.special == "early_color_hallways") {
77 entrances["early_ch"].push_back(subway_item.id); 87 entrances["early_ch"].push_back(subway_item.id);
78 } 88 }
79 89
80 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 90 if (AP_IsPaintingShuffle() && subway_item.painting) {
81 continue; 91 continue;
82 } 92 }
83 93
@@ -174,6 +184,8 @@ void SubwayMap::OnConnect() {
174 } 184 }
175 185
176 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
177} 189}
178 190
179void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
@@ -202,6 +214,8 @@ void SubwayMap::UpdateIndicators() {
202 } 214 }
203 } 215 }
204 216
217 report_popup_->UpdateIndicators();
218
205 Redraw(); 219 Redraw();
206} 220}
207 221
@@ -255,6 +269,9 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
255 SetZoomPos({zoom_x_, zoom_y_}); 269 SetZoomPos({zoom_x_, zoom_y_});
256 270
257 SetUpHelpButton(); 271 SetUpHelpButton();
272
273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
274 wxDefaultCoord, wxSIZE_AUTO);
258 } 275 }
259 276
260 wxBufferedPaintDC dc(this); 277 wxBufferedPaintDC dc(this);
@@ -310,67 +327,6 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
310 } 327 }
311 328
312 if (hovered_item_) { 329 if (hovered_item_) {
313 // Note that these requirements are duplicated on OnMouseClick so that it
314 // knows when an item has a hover effect.
315 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
316 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
317
318 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
319 const std::map<std::string, bool> &report =
320 GetDoorRequirements(*subway_door);
321
322 int acc_height = 10;
323 int col_width = 0;
324
325 for (const auto &[text, obtained] : report) {
326 wxSize item_extent = dc.GetTextExtent(text);
327 int item_height = std::max(32, item_extent.GetHeight()) + 10;
328 acc_height += item_height;
329
330 if (item_extent.GetWidth() > col_width) {
331 col_width = item_extent.GetWidth();
332 }
333 }
334
335 int item_width = col_width + 10 + 32;
336 int full_width = item_width + 20;
337
338 wxPoint popup_pos =
339 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
340 subway_item.y + AREA_ACTUAL_SIZE / 2});
341
342 if (popup_pos.x + full_width > GetSize().GetWidth()) {
343 popup_pos.x = GetSize().GetWidth() - full_width;
344 }
345 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
346 popup_pos.y = GetSize().GetHeight() - acc_height;
347 }
348
349 dc.SetPen(*wxTRANSPARENT_PEN);
350 dc.SetBrush(*wxBLACK_BRUSH);
351 dc.DrawRectangle(popup_pos, {full_width, acc_height});
352
353 dc.SetFont(GetFont());
354
355 int cur_height = 10;
356
357 for (const auto &[text, obtained] : report) {
358 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
359
360 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
361
362 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
363 wxSize item_extent = dc.GetTextExtent(text);
364 dc.DrawText(
365 text,
366 popup_pos +
367 wxPoint{10 + 32 + 10,
368 cur_height + (32 - dc.GetFontMetrics().height) / 2});
369
370 cur_height += 10 + 32;
371 }
372 }
373
374 if (networks_.IsItemInNetwork(*hovered_item_)) { 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
375 dc.SetBrush(*wxTRANSPARENT_BRUSH); 331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
376 332
@@ -378,10 +334,8 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
378 const SubwayItem &item1 = GD_GetSubwayItem(node.entry); 334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
379 const SubwayItem &item2 = GD_GetSubwayItem(node.exit); 335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
380 336
381 wxPoint item1_pos = MapPosToRenderPos( 337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
382 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); 338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
383 wxPoint item2_pos = MapPosToRenderPos(
384 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
385 339
386 int left = std::min(item1_pos.x, item2_pos.x); 340 int left = std::min(item1_pos.x, item2_pos.x);
387 int top = std::min(item1_pos.y, item2_pos.y); 341 int top = std::min(item1_pos.y, item2_pos.y);
@@ -470,9 +424,7 @@ void SubwayMap::OnMouseMove(wxMouseEvent &event) {
470 } 424 }
471 425
472 if (!sticky_hover_ && actual_hover_ != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
473 hovered_item_ = actual_hover_; 427 EvaluateHover();
474
475 Refresh();
476 } 428 }
477 429
478 if (scroll_mode_) { 430 if (scroll_mode_) {
@@ -514,13 +466,11 @@ void SubwayMap::OnMouseClick(wxMouseEvent &event) {
514 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || 466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
515 networks_.IsItemInNetwork(*hovered_item_)) { 467 networks_.IsItemInNetwork(*hovered_item_)) {
516 if (actual_hover_ != hovered_item_) { 468 if (actual_hover_ != hovered_item_) {
517 hovered_item_ = actual_hover_; 469 EvaluateHover();
518 470
519 if (!hovered_item_) { 471 if (!hovered_item_) {
520 sticky_hover_ = false; 472 sticky_hover_ = false;
521 } 473 }
522
523 Refresh();
524 } else { 474 } else {
525 sticky_hover_ = !sticky_hover_; 475 sticky_hover_ = !sticky_hover_;
526 } 476 }
@@ -571,11 +521,12 @@ void SubwayMap::OnClickHelp(wxCommandEvent &event) {
571 "your mouse. Click again to stop.\nHover over a door to see the " 521 "your mouse. Click again to stop.\nHover over a door to see the "
572 "requirements to open it.\nHover over a warp or active painting to see " 522 "requirements to open it.\nHover over a warp or active painting to see "
573 "what it is connected to.\nFor one-way connections, there will be a " 523 "what it is connected to.\nFor one-way connections, there will be a "
574 "circle at the exit.\nIn painting shuffle, paintings that have not " 524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
575 "yet been checked will not show their connections.\nA green shaded owl " 525 "that the painting is locked by a door.\nA blue circle means painting "
576 "means that there is a painting entrance there.\nA red shaded owl means " 526 "shuffle is enabled and the painting has not been checked yet.\nA black "
577 "that there are only painting exits there.\nClick on a door or " 527 "circle means the painting is not a warp.\nA green circle means that the "
578 "warp to make the popup stick until you click again.", 528 "painting is a warp.\nPainting exits will be indicated with an X.\nClick "
529 "on a door or warp to make the popup stick until you click again.",
579 "Subway Map Help"); 530 "Subway Map Help");
580} 531}
581 532
@@ -592,20 +543,32 @@ void SubwayMap::Redraw() {
592 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
593 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
594 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
595 std::optional<wxColour> shade_color;
596 std::optional<int> subway_door = GetRealSubwayDoor(subway_item); 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
597 547
598 if (AP_HasEarlyColorHallways() && 548 if (AP_HasEarlyColorHallways() &&
599 subway_item.special == "starting_room_paintings") { 549 subway_item.special == "early_color_hallways") {
600 draw_type = ItemDrawType::kOwl; 550 draw_type = ItemDrawType::kOwl;
601 shade_color = wxColour(0, 255, 0, 128); 551 brush_color = wxGREEN_BRUSH;
552 } else if (subway_item.special == "starting_room_overhead") {
553 // Do not draw.
554 } else if (AP_IsColorShuffle() && subway_item.special &&
555 subway_item.special->starts_with("color_")) {
556 std::string color_name = subway_item.special->substr(6);
557 LingoColor lingo_color = GetLingoColorForString(color_name);
558 int color_item_id = GD_GetItemIdForColor(lingo_color);
559
560 draw_type = ItemDrawType::kBox;
561 if (AP_HasItemSafe(color_item_id)) {
562 brush_color = wxGREEN_BRUSH;
563 } else {
564 brush_color = wxRED_BRUSH;
565 }
602 } else if (subway_item.special == "sun_painting") { 566 } else if (subway_item.special == "sun_painting") {
603 if (!AP_IsPilgrimageEnabled()) { 567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
604 if (IsDoorOpen(*subway_item.door)) { 569 if (IsDoorOpen(*subway_item.door)) {
605 draw_type = ItemDrawType::kOwl; 570 brush_color = wxGREEN_BRUSH;
606 shade_color = wxColour(0, 255, 0, 128);
607 } else { 571 } else {
608 draw_type = ItemDrawType::kBox;
609 brush_color = wxRED_BRUSH; 572 brush_color = wxRED_BRUSH;
610 } 573 }
611 } 574 }
@@ -619,41 +582,28 @@ void SubwayMap::Redraw() {
619 } else { 582 } else {
620 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
621 } 584 }
622 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
623 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
624 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
625 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
626 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
627 bool has_codomain_painting = false; 590 if (!checked_paintings_.count(*subway_item.painting)) {
628
629 for (const std::string &painting_id : subway_item.paintings) {
630 if (checked_paintings_.count(painting_id)) {
631 has_checked_painting = true;
632
633 if (painting_mapping.count(painting_id)) {
634 has_mapped_painting = true;
635 } else if (AP_IsPaintingMappedTo(painting_id)) {
636 has_codomain_painting = true;
637 }
638 } else {
639 has_unchecked_painting = true;
640 }
641 }
642
643 if (has_unchecked_painting || has_mapped_painting ||
644 has_codomain_painting) {
645 draw_type = ItemDrawType::kOwl; 591 draw_type = ItemDrawType::kOwl;
646 592 brush_color = wxBLUE_BRUSH;
647 if (has_checked_painting) { 593 } else if (painting_mapping.count(*subway_item.painting)) {
648 if (has_mapped_painting) { 594 draw_type = ItemDrawType::kOwl;
649 shade_color = wxColour(0, 255, 0, 128); 595 brush_color = wxGREEN_BRUSH;
650 } else { 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
651 shade_color = wxColour(255, 0, 0, 128); 597 draw_type = ItemDrawType::kOwlExit;
652 } 598 brush_color = wxGREEN_BRUSH;
653 }
654 } 599 }
655 } else if (subway_item.HasWarps()) { 600 } else if (subway_item.HasWarps()) {
656 draw_type = ItemDrawType::kOwl; 601 brush_color = wxGREEN_BRUSH;
602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
605 draw_type = ItemDrawType::kOwl;
606 }
657 } 607 }
658 } else if (subway_door) { 608 } else if (subway_door) {
659 draw_type = ItemDrawType::kBox; 609 draw_type = ItemDrawType::kBox;
@@ -673,21 +623,40 @@ void SubwayMap::Redraw() {
673 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
674 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
675 gcdc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
676 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 626
677 } else if (draw_type == ItemDrawType::kOwl) { 627 if (subway_item.tilted) {
678 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( 628 constexpr int AREA_TILTED_SIDE =
679 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
680 gcdc.DrawBitmap(owl_bitmap, real_area_pos); 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
681 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
682 if (shade_color) { 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
683 gcdc.SetBrush(wxBrush(*shade_color)); 633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
684 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
685 } 637 }
638 } else if (draw_type == ItemDrawType::kOwl || draw_type == ItemDrawType::kOwlExit) {
639 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
640 gcdc.SetBrush(*brush_color);
641 gcdc.DrawCircle(real_area_pos, PAINTING_RADIUS);
642
643 if (draw_type == ItemDrawType::kOwlExit) {
644 gcdc.DrawLine(subway_item.x - PAINTING_EXIT_RADIUS,
645 subway_item.y - PAINTING_EXIT_RADIUS,
646 subway_item.x + PAINTING_EXIT_RADIUS,
647 subway_item.y + PAINTING_EXIT_RADIUS);
648 gcdc.DrawLine(subway_item.x + PAINTING_EXIT_RADIUS,
649 subway_item.y - PAINTING_EXIT_RADIUS,
650 subway_item.x - PAINTING_EXIT_RADIUS,
651 subway_item.y + PAINTING_EXIT_RADIUS);
652 }
686 } 653 }
687 } 654 }
688} 655}
689 656
690void SubwayMap::SetUpHelpButton() { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
691 help_button_->SetPosition({ 660 help_button_->SetPosition({
692 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, 661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
693 15, 662 15,
@@ -723,6 +692,51 @@ void SubwayMap::EvaluateScroll(wxPoint pos) {
723 SetScrollSpeed(scroll_x, scroll_y); 692 SetScrollSpeed(scroll_x, scroll_y);
724} 693}
725 694
695void SubwayMap::EvaluateHover() {
696 hovered_item_ = actual_hover_;
697
698 if (hovered_item_) {
699 // Note that these requirements are duplicated on OnMouseClick so that it
700 // knows when an item has a hover effect.
701 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
702 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
703
704 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
705 report_popup_->SetDoorId(*subway_door);
706
707 wxPoint popupPos =
708 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
709 subway_item.y + AREA_ACTUAL_SIZE / 2});
710
711 report_popup_->SetClientSize(
712 report_popup_->GetVirtualSize().GetWidth(),
713 std::min(GetSize().GetHeight(),
714 report_popup_->GetVirtualSize().GetHeight()));
715
716 if (popupPos.x + report_popup_->GetSize().GetWidth() >
717 GetSize().GetWidth()) {
718 popupPos.x = GetSize().GetWidth() - report_popup_->GetSize().GetWidth();
719 }
720 if (popupPos.y + report_popup_->GetSize().GetHeight() >
721 GetSize().GetHeight()) {
722 popupPos.y =
723 GetSize().GetHeight() - report_popup_->GetSize().GetHeight();
724 }
725 report_popup_->SetPosition(popupPos);
726
727 report_popup_->Show();
728 } else {
729 report_popup_->Reset();
730 report_popup_->Hide();
731 }
732 } else {
733 report_popup_->Reset();
734 report_popup_->Hide();
735 }
736
737 Refresh();
738}
739
726wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { 740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
727 return {static_cast<int>(pos.x * render_width_ * zoom_ / 741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
728 map_image_.GetSize().GetWidth() + 742 map_image_.GetSize().GetWidth() +
@@ -812,6 +826,13 @@ std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) {
812 826
813quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { 827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
814 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
815 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
816 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; 830 return {static_cast<float>(subway_item.x) - PAINTING_RADIUS,
831 static_cast<float>(subway_item.y) - PAINTING_RADIUS,
832 PAINTING_RADIUS * 2, PAINTING_RADIUS * 2};
833 } else {
834 return {static_cast<float>(subway_item.x),
835 static_cast<float>(subway_item.y), AREA_ACTUAL_SIZE,
836 AREA_ACTUAL_SIZE};
837 }
817} 838}
diff --git a/src/subway_map.h b/src/subway_map.h index 6aa31f5..b04c2fd 100644 --- a/src/subway_map.h +++ b/src/subway_map.h
@@ -19,6 +19,8 @@
19#include "game_data.h" 19#include "game_data.h"
20#include "network_set.h" 20#include "network_set.h"
21 21
22class ReportPopup;
23
22class SubwayMap : public wxPanel { 24class SubwayMap : public wxPanel {
23 public: 25 public:
24 SubwayMap(wxWindow *parent); 26 SubwayMap(wxWindow *parent);
@@ -46,6 +48,7 @@ class SubwayMap : public wxPanel {
46 wxPoint RenderPosToMapPos(wxPoint pos) const; 48 wxPoint RenderPosToMapPos(wxPoint pos) const;
47 49
48 void EvaluateScroll(wxPoint pos); 50 void EvaluateScroll(wxPoint pos);
51 void EvaluateHover();
49 52
50 void SetZoomPos(wxPoint pos); 53 void SetZoomPos(wxPoint pos);
51 void SetScrollSpeed(int scroll_x, int scroll_y); 54 void SetScrollSpeed(int scroll_x, int scroll_y);
@@ -55,8 +58,6 @@ class SubwayMap : public wxPanel {
55 58
56 wxImage map_image_; 59 wxImage map_image_;
57 wxImage owl_image_; 60 wxImage owl_image_;
58 wxBitmap unchecked_eye_;
59 wxBitmap checked_eye_;
60 61
61 wxBitmap rendered_; 62 wxBitmap rendered_;
62 int render_x_ = 0; 63 int render_x_ = 0;
@@ -88,6 +89,8 @@ class SubwayMap : public wxPanel {
88 std::optional<int> actual_hover_; 89 std::optional<int> actual_hover_;
89 bool sticky_hover_ = false; 90 bool sticky_hover_ = false;
90 91
92 ReportPopup *report_popup_;
93
91 NetworkSet networks_; 94 NetworkSet networks_;
92 std::set<std::string> checked_paintings_; 95 std::set<std::string> checked_paintings_;
93 96
diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 129dbbc..da5d60a 100644 --- a/src/tracker_config.cpp +++ b/src/tracker_config.cpp
@@ -16,7 +16,9 @@ void TrackerConfig::Load() {
16 asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>(); 16 asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>();
17 should_check_for_updates = file["should_check_for_updates"].as<bool>(); 17 should_check_for_updates = file["should_check_for_updates"].as<bool>();
18 hybrid_areas = file["hybrid_areas"].as<bool>(); 18 hybrid_areas = file["hybrid_areas"].as<bool>();
19 show_hunt_panels = file["show_hunt_panels"].as<bool>(); 19 if (file["show_hunt_panels"] && file["show_hunt_panels"].as<bool>()) {
20 visible_panels = kHUNT_PANELS;
21 }
20 22
21 if (file["connection_history"]) { 23 if (file["connection_history"]) {
22 for (const auto& connection : file["connection_history"]) { 24 for (const auto& connection : file["connection_history"]) {
@@ -29,6 +31,9 @@ void TrackerConfig::Load() {
29 } 31 }
30 32
31 ipc_address = file["ipc_address"].as<std::string>(); 33 ipc_address = file["ipc_address"].as<std::string>();
34 track_position = file["track_position"].as<bool>();
35 visible_panels =
36 static_cast<VisiblePanels>(file["visible_panels"].as<int>());
32 } catch (const std::exception&) { 37 } catch (const std::exception&) {
33 // It's fine if the file can't be loaded. 38 // It's fine if the file can't be loaded.
34 } 39 }
@@ -42,7 +47,6 @@ void TrackerConfig::Save() {
42 output["asked_to_check_for_updates"] = asked_to_check_for_updates; 47 output["asked_to_check_for_updates"] = asked_to_check_for_updates;
43 output["should_check_for_updates"] = should_check_for_updates; 48 output["should_check_for_updates"] = should_check_for_updates;
44 output["hybrid_areas"] = hybrid_areas; 49 output["hybrid_areas"] = hybrid_areas;
45 output["show_hunt_panels"] = show_hunt_panels;
46 50
47 output.remove("connection_history"); 51 output.remove("connection_history");
48 for (const ConnectionDetails& details : connection_history) { 52 for (const ConnectionDetails& details : connection_history) {
@@ -55,6 +59,8 @@ void TrackerConfig::Save() {
55 } 59 }
56 60
57 output["ipc_address"] = ipc_address; 61 output["ipc_address"] = ipc_address;
62 output["track_position"] = track_position;
63 output["visible_panels"] = static_cast<int>(visible_panels);
58 64
59 std::ofstream filewriter(filename_); 65 std::ofstream filewriter(filename_);
60 filewriter << output; 66 filewriter << output;
diff --git a/src/tracker_config.h b/src/tracker_config.h index 9244b74..df4105d 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h
@@ -23,13 +23,20 @@ class TrackerConfig {
23 23
24 void Save(); 24 void Save();
25 25
26 enum VisiblePanels {
27 kLOCATIONS_ONLY,
28 kHUNT_PANELS,
29 kALL_PANELS,
30 };
31
26 ConnectionDetails connection_details; 32 ConnectionDetails connection_details;
27 bool asked_to_check_for_updates = false; 33 bool asked_to_check_for_updates = false;
28 bool should_check_for_updates = false; 34 bool should_check_for_updates = false;
29 bool hybrid_areas = false; 35 bool hybrid_areas = false;
30 bool show_hunt_panels = false;
31 std::deque<ConnectionDetails> connection_history; 36 std::deque<ConnectionDetails> connection_history;
32 std::string ipc_address; 37 std::string ipc_address;
38 bool track_position = true;
39 VisiblePanels visible_panels = kLOCATIONS_ONLY;
33 40
34 private: 41 private:
35 std::string filename_; 42 std::string filename_;
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 587d87b..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -5,9 +5,11 @@
5#include <wx/choicebk.h> 5#include <wx/choicebk.h>
6#include <wx/filedlg.h> 6#include <wx/filedlg.h>
7#include <wx/notebook.h> 7#include <wx/notebook.h>
8#include <wx/splitter.h>
8#include <wx/stdpaths.h> 9#include <wx/stdpaths.h>
9#include <wx/webrequest.h> 10#include <wx/webrequest.h>
10 11
12#include <algorithm>
11#include <nlohmann/json.hpp> 13#include <nlohmann/json.hpp>
12#include <sstream> 14#include <sstream>
13 15
@@ -16,6 +18,11 @@
16#include "connection_dialog.h" 18#include "connection_dialog.h"
17#include "ipc_dialog.h" 19#include "ipc_dialog.h"
18#include "ipc_state.h" 20#include "ipc_state.h"
21#include "items_pane.h"
22#include "log_dialog.h"
23#include "logger.h"
24#include "options_pane.h"
25#include "paintings_pane.h"
19#include "settings_dialog.h" 26#include "settings_dialog.h"
20#include "subway_map.h" 27#include "subway_map.h"
21#include "tracker_config.h" 28#include "tracker_config.h"
@@ -44,14 +51,13 @@ enum TrackerFrameIds {
44 ID_SETTINGS = 3, 51 ID_SETTINGS = 3,
45 ID_ZOOM_IN = 4, 52 ID_ZOOM_IN = 4,
46 ID_ZOOM_OUT = 5, 53 ID_ZOOM_OUT = 5,
47 ID_OPEN_SAVE_FILE = 6,
48 ID_IPC_CONNECT = 7, 54 ID_IPC_CONNECT = 7,
55 ID_LOG_DIALOG = 8,
49}; 56};
50 57
51wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); 58wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
52wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 59wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent);
53wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 60wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
54wxDEFINE_EVENT(REDRAW_POSITION, wxCommandEvent);
55wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); 61wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent);
56 62
57TrackerFrame::TrackerFrame() 63TrackerFrame::TrackerFrame()
@@ -62,16 +68,22 @@ TrackerFrame::TrackerFrame()
62 AP_SetTrackerFrame(this); 68 AP_SetTrackerFrame(this);
63 IPC_SetTrackerFrame(this); 69 IPC_SetTrackerFrame(this);
64 70
71 SetTheIconCache(&icons_);
72
73 updater_ = std::make_unique<Updater>(this);
74 updater_->Cleanup();
75
65 wxMenu *menuFile = new wxMenu(); 76 wxMenu *menuFile = new wxMenu();
66 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago"); 77 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
67 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo"); 78 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
68 menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O");
69 menuFile->Append(ID_SETTINGS, "&Settings"); 79 menuFile->Append(ID_SETTINGS, "&Settings");
70 menuFile->Append(wxID_EXIT); 80 menuFile->Append(wxID_EXIT);
71 81
72 wxMenu *menuView = new wxMenu(); 82 wxMenu *menuView = new wxMenu();
73 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+"); 83 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+");
74 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--"); 84 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--");
85 menuView->AppendSeparator();
86 menuView->Append(ID_LOG_DIALOG, "Show Log Window\tCtrl-L");
75 87
76 zoom_in_menu_item_->Enable(false); 88 zoom_in_menu_item_->Enable(false);
77 zoom_out_menu_item_->Enable(false); 89 zoom_out_menu_item_->Enable(false);
@@ -98,30 +110,43 @@ TrackerFrame::TrackerFrame()
98 ID_CHECK_FOR_UPDATES); 110 ID_CHECK_FOR_UPDATES);
99 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); 111 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
100 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); 112 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
113 Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG);
101 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); 114 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
102 Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE); 115 Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged,
116 this);
103 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); 117 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
104 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 118 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
105 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); 119 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
106 Bind(REDRAW_POSITION, &TrackerFrame::OnRedrawPosition, this);
107 Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this); 120 Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this);
108 121
109 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); 122 wxSize logicalSize = FromDIP(wxSize(1280, 728));
123
124 splitter_window_ = new wxSplitterWindow(this, wxID_ANY);
125 splitter_window_->SetMinimumPaneSize(logicalSize.x / 5);
126
127 wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY);
128
110 achievements_pane_ = new AchievementsPane(choicebook); 129 achievements_pane_ = new AchievementsPane(choicebook);
111 choicebook->AddPage(achievements_pane_, "Achievements"); 130 choicebook->AddPage(achievements_pane_, "Achievements");
112 131
113 notebook_ = new wxNotebook(this, wxID_ANY); 132 items_pane_ = new ItemsPane(choicebook);
133 choicebook->AddPage(items_pane_, "Items");
134
135 options_pane_ = new OptionsPane(choicebook);
136 choicebook->AddPage(options_pane_, "Options");
137
138 paintings_pane_ = new PaintingsPane(choicebook);
139 choicebook->AddPage(paintings_pane_, "Paintings");
140
141 notebook_ = new wxNotebook(splitter_window_, wxID_ANY);
114 tracker_panel_ = new TrackerPanel(notebook_); 142 tracker_panel_ = new TrackerPanel(notebook_);
115 subway_map_ = new SubwayMap(notebook_); 143 subway_map_ = new SubwayMap(notebook_);
116 notebook_->AddPage(tracker_panel_, "Map"); 144 notebook_->AddPage(tracker_panel_, "Map");
117 notebook_->AddPage(subway_map_, "Subway"); 145 notebook_->AddPage(subway_map_, "Subway");
118 146
119 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 147 splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4);
120 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
121 top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3));
122 148
123 SetSizerAndFit(top_sizer); 149 SetSize(logicalSize);
124 SetSize(1280, 728);
125 150
126 if (!GetTrackerConfig().asked_to_check_for_updates) { 151 if (!GetTrackerConfig().asked_to_check_for_updates) {
127 GetTrackerConfig().asked_to_check_for_updates = true; 152 GetTrackerConfig().asked_to_check_for_updates = true;
@@ -138,7 +163,7 @@ TrackerFrame::TrackerFrame()
138 } 163 }
139 164
140 if (GetTrackerConfig().should_check_for_updates) { 165 if (GetTrackerConfig().should_check_for_updates) {
141 CheckForUpdates(/*manual=*/false); 166 updater_->CheckForUpdates(/*invisible=*/true);
142 } 167 }
143 168
144 SetStatusText(GetStatusMessage()); 169 SetStatusText(GetStatusMessage());
@@ -158,15 +183,8 @@ void TrackerFrame::ResetIndicators() {
158 QueueEvent(new wxCommandEvent(STATE_RESET)); 183 QueueEvent(new wxCommandEvent(STATE_RESET));
159} 184}
160 185
161void TrackerFrame::UpdateIndicators(UpdateIndicatorsMode mode) { 186void TrackerFrame::UpdateIndicators(StateUpdate state) {
162 auto evt = new wxCommandEvent(STATE_CHANGED); 187 QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state)));
163 evt->SetInt(static_cast<int>(mode));
164
165 QueueEvent(evt);
166}
167
168void TrackerFrame::RedrawPosition() {
169 QueueEvent(new wxCommandEvent(REDRAW_POSITION));
170} 188}
171 189
172void TrackerFrame::OnAbout(wxCommandEvent &event) { 190void TrackerFrame::OnAbout(wxCommandEvent &event) {
@@ -231,15 +249,18 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
231 GetTrackerConfig().should_check_for_updates = 249 GetTrackerConfig().should_check_for_updates =
232 dlg.GetShouldCheckForUpdates(); 250 dlg.GetShouldCheckForUpdates();
233 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); 251 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
234 GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); 252 GetTrackerConfig().visible_panels = dlg.GetVisiblePanels();
253 GetTrackerConfig().track_position = dlg.GetTrackPosition();
235 GetTrackerConfig().Save(); 254 GetTrackerConfig().Save();
236 255
237 UpdateIndicators(); 256 UpdateIndicators(StateUpdate{.cleared_locations = true,
257 .player_position = true,
258 .changed_settings = true});
238 } 259 }
239} 260}
240 261
241void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 262void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
242 CheckForUpdates(/*manual=*/true); 263 updater_->CheckForUpdates(/*invisible=*/false);
243} 264}
244 265
245void TrackerFrame::OnZoomIn(wxCommandEvent &event) { 266void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
@@ -254,132 +275,93 @@ void TrackerFrame::OnZoomOut(wxCommandEvent &event) {
254 } 275 }
255} 276}
256 277
257void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { 278void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) {
258 zoom_in_menu_item_->Enable(event.GetSelection() == 1); 279 if (log_dialog_ == nullptr) {
259 zoom_out_menu_item_->Enable(event.GetSelection() == 1); 280 log_dialog_ = new LogDialog(this);
260} 281 log_dialog_->Show();
282 TrackerSetLogDialog(log_dialog_);
261 283
262void TrackerFrame::OnOpenFile(wxCommandEvent &event) { 284 log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow,
263 wxFileDialog open_file_dialog( 285 this);
264 this, "Open Lingo Save File", 286 } else {
265 fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable", 287 log_dialog_->SetFocus();
266 wxStandardPaths::Get().GetUserConfigDir().ToStdString()),
267 AP_GetSaveName(), "Lingo save file (*.save)|*.save",
268 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
269 if (open_file_dialog.ShowModal() == wxID_CANCEL) {
270 return;
271 } 288 }
289}
272 290
273 std::string savedata_path = open_file_dialog.GetPath().ToStdString(); 291void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) {
292 TrackerSetLogDialog(nullptr);
293 log_dialog_ = nullptr;
274 294
275 if (panels_panel_ == nullptr) { 295 event.Skip();
276 panels_panel_ = new TrackerPanel(notebook_); 296}
277 notebook_->AddPage(panels_panel_, "Panels");
278 }
279 297
280 notebook_->SetSelection(notebook_->FindPage(panels_panel_)); 298void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
281 panels_panel_->SetSavedataPath(savedata_path); 299 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
300 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
301}
302
303void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) {
304 notebook_->Refresh();
282} 305}
283 306
284void TrackerFrame::OnStateReset(wxCommandEvent &event) { 307void TrackerFrame::OnStateReset(wxCommandEvent &event) {
285 tracker_panel_->UpdateIndicators(); 308 tracker_panel_->UpdateIndicators(/*reset=*/true);
286 achievements_pane_->UpdateIndicators(); 309 achievements_pane_->UpdateIndicators();
310 items_pane_->ResetIndicators();
311 options_pane_->OnConnect();
312 paintings_pane_->ResetIndicators();
287 subway_map_->OnConnect(); 313 subway_map_->OnConnect();
288 if (panels_panel_ != nullptr) {
289 notebook_->DeletePage(notebook_->FindPage(panels_panel_));
290 panels_panel_ = nullptr;
291 }
292 Refresh(); 314 Refresh();
293} 315}
294 316
295void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 317void TrackerFrame::OnStateChanged(StateChangedEvent &event) {
296 UpdateIndicatorsMode mode = static_cast<UpdateIndicatorsMode>(event.GetInt()); 318 const StateUpdate &state = event.GetState();
297 319
298 if (mode == kUPDATE_ALL_INDICATORS) { 320 bool hunt_panels = false;
299 tracker_panel_->UpdateIndicators(); 321 if (GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) {
300 achievements_pane_->UpdateIndicators(); 322 hunt_panels = std::any_of(
323 state.panels.begin(), state.panels.end(), [](int solve_index) {
324 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index)).hunt;
325 });
326 } else if (GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) {
327 hunt_panels = true;
328 }
329
330 if (!state.items.empty() || !state.paintings.empty() ||
331 state.cleared_locations || hunt_panels) {
332 // TODO: The only real reason to reset tracker_panel during an active
333 // connection is if the hunt panels setting changes. If we remove hunt
334 // panels later, we can get rid of this.
335 tracker_panel_->UpdateIndicators(/*reset=*/state.changed_settings);
301 subway_map_->UpdateIndicators(); 336 subway_map_->UpdateIndicators();
302 if (panels_panel_ != nullptr) {
303 panels_panel_->UpdateIndicators();
304 }
305 Refresh(); 337 Refresh();
306 } else if (mode == kUPDATE_ONLY_PANELS) { 338 } else if (state.player_position && GetTrackerConfig().track_position) {
307 if (panels_panel_ == nullptr) { 339 if (notebook_->GetSelection() == 0) {
308 panels_panel_ = new TrackerPanel(notebook_); 340 tracker_panel_->Refresh();
309 panels_panel_->SetPanelsMode();
310 notebook_->AddPage(panels_panel_, "Panels");
311 }
312 panels_panel_->UpdateIndicators();
313 if (notebook_->GetSelection() == 2) {
314 Refresh();
315 } 341 }
316 } 342 }
317}
318 343
319void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { 344 if (std::any_of(state.panels.begin(), state.panels.end(),
320 SetStatusText(GetStatusMessage()); 345 [](int solve_index) {
321} 346 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index))
322 347 .achievement;
323void TrackerFrame::OnRedrawPosition(wxCommandEvent &event) { 348 })) {
324 if (notebook_->GetSelection() == 0) { 349 achievements_pane_->UpdateIndicators();
325 tracker_panel_->Refresh();
326 } else if (notebook_->GetSelection() == 2) {
327 panels_panel_->Refresh();
328 } 350 }
329}
330
331void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
332 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
333}
334
335void TrackerFrame::CheckForUpdates(bool manual) {
336 wxWebRequest request = wxWebSession::GetDefault().CreateRequest(
337 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION");
338 351
339 if (!request.IsOk()) { 352 if (!state.items.empty()) {
340 if (manual) { 353 items_pane_->UpdateIndicators(state.items);
341 wxMessageBox("Could not check for updates.", "Error", 354 }
342 wxOK | wxICON_ERROR);
343 } else {
344 SetStatusText("Could not check for updates.");
345 }
346 355
347 return; 356 if (!state.paintings.empty()) {
357 paintings_pane_->UpdateIndicators(state.paintings);
348 } 358 }
359}
349 360
350 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { 361void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
351 if (evt.GetState() == wxWebRequest::State_Completed) { 362 SetStatusText(wxString::FromUTF8(GetStatusMessage()));
352 std::string response = evt.GetResponse().AsString().ToStdString(); 363}
353
354 Version latest_version(response);
355 if (kTrackerVersion < latest_version) {
356 std::ostringstream message_text;
357 message_text << "There is a newer version of Lingo AP Tracker "
358 "available. You have "
359 << kTrackerVersion.ToString()
360 << ", and the latest version is "
361 << latest_version.ToString()
362 << ". Would you like to update?";
363
364 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
365 wxYES) {
366 wxLaunchDefaultBrowser(
367 "https://code.fourisland.com/lingo-ap-tracker/about/"
368 "CHANGELOG.md");
369 }
370 } else if (manual) {
371 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
372 wxOK);
373 }
374 } else if (evt.GetState() == wxWebRequest::State_Failed) {
375 if (manual) {
376 wxMessageBox("Could not check for updates.", "Error",
377 wxOK | wxICON_ERROR);
378 } else {
379 SetStatusText("Could not check for updates.");
380 }
381 }
382 });
383 364
384 request.Start(); 365void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
366 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
385} 367}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index e9fec17..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,11 +7,24 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <memory>
11#include <set>
12
13#include "ap_state.h"
14#include "icons.h"
15#include "updater.h"
16
10class AchievementsPane; 17class AchievementsPane;
18class ItemsPane;
19class LogDialog;
20class OptionsPane;
21class PaintingsPane;
11class SubwayMap; 22class SubwayMap;
12class TrackerPanel; 23class TrackerPanel;
13class wxBookCtrlEvent; 24class wxBookCtrlEvent;
14class wxNotebook; 25class wxNotebook;
26class wxSplitterEvent;
27class wxSplitterWindow;
15 28
16class ApConnectEvent : public wxEvent { 29class ApConnectEvent : public wxEvent {
17 public: 30 public:
@@ -36,17 +49,34 @@ class ApConnectEvent : public wxEvent {
36 std::string ap_pass_; 49 std::string ap_pass_;
37}; 50};
38 51
52struct StateUpdate {
53 std::vector<ItemState> items;
54 bool progression_items = false;
55 std::vector<std::string> paintings;
56 bool cleared_locations = false;
57 std::set<int> panels;
58 bool player_position = false;
59 bool changed_settings = false;
60};
61
62class StateChangedEvent : public wxEvent {
63 public:
64 StateChangedEvent(wxEventType eventType, int winid, StateUpdate state)
65 : wxEvent(winid, eventType), state_(std::move(state)) {}
66
67 const StateUpdate &GetState() const { return state_; }
68
69 virtual wxEvent *Clone() const { return new StateChangedEvent(*this); }
70
71 private:
72 StateUpdate state_;
73};
74
39wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); 75wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
40wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 76wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent);
41wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 77wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
42wxDECLARE_EVENT(REDRAW_POSITION, wxCommandEvent);
43wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); 78wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent);
44 79
45enum UpdateIndicatorsMode {
46 kUPDATE_ALL_INDICATORS = 0,
47 kUPDATE_ONLY_PANELS = 1,
48};
49
50class TrackerFrame : public wxFrame { 80class TrackerFrame : public wxFrame {
51 public: 81 public:
52 TrackerFrame(); 82 TrackerFrame();
@@ -55,8 +85,7 @@ class TrackerFrame : public wxFrame {
55 void UpdateStatusMessage(); 85 void UpdateStatusMessage();
56 86
57 void ResetIndicators(); 87 void ResetIndicators();
58 void UpdateIndicators(UpdateIndicatorsMode mode = kUPDATE_ALL_INDICATORS); 88 void UpdateIndicators(StateUpdate state);
59 void RedrawPosition();
60 89
61 private: 90 private:
62 void OnExit(wxCommandEvent &event); 91 void OnExit(wxCommandEvent &event);
@@ -67,25 +96,32 @@ class TrackerFrame : public wxFrame {
67 void OnCheckForUpdates(wxCommandEvent &event); 96 void OnCheckForUpdates(wxCommandEvent &event);
68 void OnZoomIn(wxCommandEvent &event); 97 void OnZoomIn(wxCommandEvent &event);
69 void OnZoomOut(wxCommandEvent &event); 98 void OnZoomOut(wxCommandEvent &event);
99 void OnOpenLogWindow(wxCommandEvent &event);
100 void OnCloseLogWindow(wxCloseEvent &event);
70 void OnChangePage(wxBookCtrlEvent &event); 101 void OnChangePage(wxBookCtrlEvent &event);
71 void OnOpenFile(wxCommandEvent &event); 102 void OnSashPositionChanged(wxSplitterEvent &event);
72 103
73 void OnStateReset(wxCommandEvent &event); 104 void OnStateReset(wxCommandEvent &event);
74 void OnStateChanged(wxCommandEvent &event); 105 void OnStateChanged(StateChangedEvent &event);
75 void OnStatusChanged(wxCommandEvent &event); 106 void OnStatusChanged(wxCommandEvent &event);
76 void OnRedrawPosition(wxCommandEvent &event);
77 void OnConnectToAp(ApConnectEvent &event); 107 void OnConnectToAp(ApConnectEvent &event);
108
109 std::unique_ptr<Updater> updater_;
78 110
79 void CheckForUpdates(bool manual); 111 wxSplitterWindow *splitter_window_;
80
81 wxNotebook *notebook_; 112 wxNotebook *notebook_;
82 TrackerPanel *tracker_panel_; 113 TrackerPanel *tracker_panel_;
83 AchievementsPane *achievements_pane_; 114 AchievementsPane *achievements_pane_;
115 ItemsPane *items_pane_;
116 OptionsPane *options_pane_;
117 PaintingsPane *paintings_pane_;
84 SubwayMap *subway_map_; 118 SubwayMap *subway_map_;
85 TrackerPanel *panels_panel_ = nullptr; 119 LogDialog *log_dialog_ = nullptr;
86 120
87 wxMenuItem *zoom_in_menu_item_; 121 wxMenuItem *zoom_in_menu_item_;
88 wxMenuItem *zoom_out_menu_item_; 122 wxMenuItem *zoom_out_menu_item_;
123
124 IconCache icons_;
89}; 125};
90 126
91#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ 127#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */
diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index 04b970c..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -9,7 +9,6 @@
9#include "area_popup.h" 9#include "area_popup.h"
10#include "game_data.h" 10#include "game_data.h"
11#include "global.h" 11#include "global.h"
12#include "godot_variant.h"
13#include "ipc_state.h" 12#include "ipc_state.h"
14#include "tracker_config.h" 13#include "tracker_config.h"
15#include "tracker_state.h" 14#include "tracker_state.h"
@@ -44,58 +43,46 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
44 areas_.push_back(area); 43 areas_.push_back(area);
45 } 44 }
46 45
46 Resize();
47 Redraw(); 47 Redraw();
48 48
49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); 49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); 50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
51} 51}
52 52
53void TrackerPanel::UpdateIndicators() { 53void TrackerPanel::UpdateIndicators(bool reset) {
54 if (panels_mode_ && !savedata_path_) { 54 if (reset) {
55 solved_panels_ = IPC_GetSolvedPanels(); 55 for (AreaIndicator &area : areas_) {
56 } 56 const MapArea &map_area = GD_GetMapArea(area.area_id);
57 57
58 for (AreaIndicator &area : areas_) { 58 if ((!AP_IsLocationVisible(map_area.classification) ||
59 area.popup->UpdateIndicators(); 59 IsAreaPostgame(area.area_id)) &&
60 } 60 !(map_area.hunt &&
61 61 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
62 Redraw(); 62 !(map_area.has_single_panel &&
63} 63 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) &&
64 64 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
65void TrackerPanel::SetPanelsMode() { panels_mode_ = true; } 65 area.active = false;
66 66 } else {
67void TrackerPanel::SetSavedataPath(std::string savedata_path) { 67 area.active = true;
68 if (!savedata_path_) { 68 }
69 wxButton *refresh_button =
70 new wxButton(this, wxID_ANY, "Refresh", {15, 15});
71 refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this);
72 }
73
74 savedata_path_ = savedata_path;
75 panels_mode_ = true;
76
77 RefreshSavedata();
78}
79 69
80void TrackerPanel::RefreshSavedata() { 70 area.popup->ResetIndicators();
81 solved_panels_.clear(); 71 }
82 72
83 GodotVariant godot_variant = ParseGodotFile(*savedata_path_); 73 Resize();
84 for (const GodotVariant &panel_node : godot_variant.AsArray()) { 74 } else {
85 const std::vector<GodotVariant> &fields = panel_node.AsArray(); 75 for (AreaIndicator &area : areas_) {
86 if (fields[1].AsBool()) { 76 area.popup->UpdateIndicators();
87 const std::vector<std::string> &nodepath = fields[0].AsNodePath();
88 std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]);
89 solved_panels_.insert(key);
90 } 77 }
91 } 78 }
92 79
93 UpdateIndicators(); 80 Redraw();
94 Refresh();
95} 81}
96 82
97void TrackerPanel::OnPaint(wxPaintEvent &event) { 83void TrackerPanel::OnPaint(wxPaintEvent &event) {
98 if (GetSize() != rendered_.GetSize()) { 84 if (GetSize() != rendered_.GetSize()) {
85 Resize();
99 Redraw(); 86 Redraw();
100 } 87 }
101 88
@@ -103,10 +90,13 @@ void TrackerPanel::OnPaint(wxPaintEvent &event) {
103 dc.DrawBitmap(rendered_, 0, 0); 90 dc.DrawBitmap(rendered_, 0, 0);
104 91
105 std::optional<std::tuple<int, int>> player_position; 92 std::optional<std::tuple<int, int>> player_position;
106 if (IPC_IsConnected()) { 93 if (GetTrackerConfig().track_position)
107 player_position = IPC_GetPlayerPosition(); 94 {
108 } else { 95 if (IPC_IsConnected()) {
109 player_position = AP_GetPlayerPosition(); 96 player_position = IPC_GetPlayerPosition();
97 } else {
98 player_position = AP_GetPlayerPosition();
99 }
110 } 100 }
111 101
112 if (player_position.has_value()) { 102 if (player_position.has_value()) {
@@ -142,12 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
142 event.Skip(); 132 event.Skip();
143} 133}
144 134
145void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { 135void TrackerPanel::Resize() {
146 RefreshSavedata(); 136 wxSize panel_size = GetClientSize();
147}
148
149void TrackerPanel::Redraw() {
150 wxSize panel_size = GetSize();
151 wxSize image_size = map_image_.GetSize(); 137 wxSize image_size = map_image_.GetSize();
152 138
153 int final_x = 0; 139 int final_x = 0;
@@ -166,7 +152,7 @@ void TrackerPanel::Redraw() {
166 final_x = (panel_size.GetWidth() - final_width) / 2; 152 final_x = (panel_size.GetWidth() - final_width) / 2;
167 } 153 }
168 154
169 rendered_ = wxBitmap( 155 scaled_map_ = wxBitmap(
170 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) 156 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
171 .Size(panel_size, {final_x, final_y}, 0, 0, 0)); 157 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
172 158
@@ -181,30 +167,61 @@ void TrackerPanel::Redraw() {
181 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, 167 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1,
182 player_height > 0 ? player_height : 1)); 168 player_height > 0 ? player_height : 1));
183 169
170 real_area_size_ = final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
171
172 for (AreaIndicator &area : areas_) {
173 const MapArea &map_area = GD_GetMapArea(area.area_id);
174
175 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
176 final_width / image_size.GetWidth();
177 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
178 final_width / image_size.GetWidth();
179
180 area.real_x1 = real_area_x;
181 area.real_x2 = real_area_x + real_area_size_;
182 area.real_y1 = real_area_y;
183 area.real_y2 = real_area_y + real_area_size_;
184
185 int popup_x =
186 final_x + map_area.map_x * final_width / image_size.GetWidth();
187 int popup_y =
188 final_y + map_area.map_y * final_width / image_size.GetWidth();
189
190 area.popup->SetClientSize(
191 area.popup->GetFullWidth(),
192 std::min(panel_size.GetHeight(), area.popup->GetFullHeight()));
193
194 if (area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
195 area.popup->SetSize(area.popup->GetSize().GetWidth(),
196 panel_size.GetHeight());
197 }
198
199 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
200 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
201 }
202 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
203 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
204 }
205 area.popup->SetPosition({popup_x, popup_y});
206 }
207}
208
209void TrackerPanel::Redraw() {
210 rendered_ = scaled_map_;
211
184 wxMemoryDC dc; 212 wxMemoryDC dc;
185 dc.SelectObject(rendered_); 213 dc.SelectObject(rendered_);
186 214
187 int real_area_size =
188 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
189 int actual_border_size = 215 int actual_border_size =
190 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; 216 real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
191 const wxPoint upper_left_triangle[] = { 217 const wxPoint upper_left_triangle[] = {
192 {0, 0}, {0, real_area_size}, {real_area_size, 0}}; 218 {0, 0}, {0, real_area_size_}, {real_area_size_, 0}};
193 const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, 219 const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1},
194 {real_area_size - 1, 0}, 220 {real_area_size_ - 1, 0},
195 {real_area_size, real_area_size}}; 221 {real_area_size_, real_area_size_}};
196 222
197 for (AreaIndicator &area : areas_) { 223 for (AreaIndicator &area : areas_) {
198 const MapArea &map_area = GD_GetMapArea(area.area_id); 224 const MapArea &map_area = GD_GetMapArea(area.area_id);
199 if (panels_mode_) {
200 area.active = map_area.has_single_panel;
201 } else if (!AP_IsLocationVisible(map_area.classification) &&
202 !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
203 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
204 area.active = false;
205 } else {
206 area.active = true;
207 }
208 225
209 if (!area.active) { 226 if (!area.active) {
210 continue; 227 continue;
@@ -216,19 +233,15 @@ void TrackerPanel::Redraw() {
216 bool has_unchecked = false; 233 bool has_unchecked = false;
217 if (IsLocationWinCondition(section)) { 234 if (IsLocationWinCondition(section)) {
218 has_unchecked = !AP_HasReachedGoal(); 235 has_unchecked = !AP_HasReachedGoal();
219 } else if (panels_mode_) { 236 } else if (AP_IsLocationVisible(section.classification) &&
220 if (section.single_panel) { 237 !IsLocationPostgame(section.ap_location_id)) {
221 const Panel &panel = GD_GetPanel(*section.single_panel);
222 if (panel.non_counting) {
223 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
224 } else {
225 has_unchecked = !GetSolvedPanels().contains(panel.nodepath);
226 }
227 }
228 } else if (AP_IsLocationVisible(section.classification)) {
229 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); 238 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
230 } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { 239 } else if ((section.hunt && GetTrackerConfig().visible_panels ==
231 has_unchecked = !AP_HasCheckedHuntPanel(section.ap_location_id); 240 TrackerConfig::kHUNT_PANELS) ||
241 (section.single_panel && GetTrackerConfig().visible_panels ==
242 TrackerConfig::kALL_PANELS)) {
243 has_unchecked =
244 !AP_IsPanelSolved(GD_GetPanel(*section.single_panel).solve_index);
232 } 245 }
233 246
234 if (has_unchecked) { 247 if (has_unchecked) {
@@ -240,8 +253,12 @@ void TrackerPanel::Redraw() {
240 } 253 }
241 } 254 }
242 255
243 if (AP_IsPaintingShuffle() && !panels_mode_) { 256 if (AP_IsPaintingShuffle()) {
244 for (int painting_id : map_area.paintings) { 257 for (int painting_id : map_area.paintings) {
258 if (IsPaintingPostgame(painting_id)) {
259 continue;
260 }
261
245 const PaintingExit &painting = GD_GetPaintingExit(painting_id); 262 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
246 bool reachable = IsPaintingReachable(painting_id); 263 bool reachable = IsPaintingReachable(painting_id);
247 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { 264 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) {
@@ -254,10 +271,8 @@ void TrackerPanel::Redraw() {
254 } 271 }
255 } 272 }
256 273
257 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 274 int real_area_x = area.real_x1;
258 final_width / image_size.GetWidth(); 275 int real_area_y = area.real_y1;
259 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
260 final_width / image_size.GetWidth();
261 276
262 if (has_reachable_unchecked && has_unreachable_unchecked && 277 if (has_reachable_unchecked && has_unreachable_unchecked &&
263 GetTrackerConfig().hybrid_areas) { 278 GetTrackerConfig().hybrid_areas) {
@@ -271,7 +286,7 @@ void TrackerPanel::Redraw() {
271 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
272 dc.SetBrush(*wxTRANSPARENT_BRUSH); 287 dc.SetBrush(*wxTRANSPARENT_BRUSH);
273 dc.DrawRectangle({real_area_x, real_area_y}, 288 dc.DrawRectangle({real_area_x, real_area_y},
274 {real_area_size, real_area_size}); 289 {real_area_size_, real_area_size_});
275 290
276 } else { 291 } else {
277 const wxBrush *brush_color = wxGREY_BRUSH; 292 const wxBrush *brush_color = wxGREY_BRUSH;
@@ -286,30 +301,7 @@ void TrackerPanel::Redraw() {
286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 301 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
287 dc.SetBrush(*brush_color); 302 dc.SetBrush(*brush_color);
288 dc.DrawRectangle({real_area_x, real_area_y}, 303 dc.DrawRectangle({real_area_x, real_area_y},
289 {real_area_size, real_area_size}); 304 {real_area_size_, real_area_size_});
290 } 305 }
291
292 area.real_x1 = real_area_x;
293 area.real_x2 = real_area_x + real_area_size;
294 area.real_y1 = real_area_y;
295 area.real_y2 = real_area_y + real_area_size;
296
297 int popup_x =
298 final_x + map_area.map_x * final_width / image_size.GetWidth();
299 int popup_y =
300 final_y + map_area.map_y * final_width / image_size.GetWidth();
301
302 area.popup->SetClientSize(
303 area.popup->GetVirtualSize().GetWidth(),
304 std::min(panel_size.GetHeight(),
305 area.popup->GetVirtualSize().GetHeight()));
306
307 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
308 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
309 }
310 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
311 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
312 }
313 area.popup->SetPosition({popup_x, popup_y});
314 } 306 }
315} 307}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index 822d181..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h
@@ -17,17 +17,7 @@ class TrackerPanel : public wxPanel {
17 public: 17 public:
18 TrackerPanel(wxWindow *parent); 18 TrackerPanel(wxWindow *parent);
19 19
20 void UpdateIndicators(); 20 void UpdateIndicators(bool reset);
21
22 void SetPanelsMode();
23
24 void SetSavedataPath(std::string savedata_path);
25
26 bool IsPanelsMode() const { return panels_mode_; }
27
28 const std::set<std::string> &GetSolvedPanels() const {
29 return solved_panels_;
30 }
31 21
32 private: 22 private:
33 struct AreaIndicator { 23 struct AreaIndicator {
@@ -42,14 +32,13 @@ class TrackerPanel : public wxPanel {
42 32
43 void OnPaint(wxPaintEvent &event); 33 void OnPaint(wxPaintEvent &event);
44 void OnMouseMove(wxMouseEvent &event); 34 void OnMouseMove(wxMouseEvent &event);
45 void OnRefreshSavedata(wxCommandEvent &event);
46 35
36 void Resize();
47 void Redraw(); 37 void Redraw();
48 38
49 void RefreshSavedata();
50
51 wxImage map_image_; 39 wxImage map_image_;
52 wxImage player_image_; 40 wxImage player_image_;
41 wxBitmap scaled_map_;
53 wxBitmap rendered_; 42 wxBitmap rendered_;
54 wxBitmap scaled_player_; 43 wxBitmap scaled_player_;
55 44
@@ -57,12 +46,9 @@ class TrackerPanel : public wxPanel {
57 int offset_y_ = 0; 46 int offset_y_ = 0;
58 double scale_x_ = 0; 47 double scale_x_ = 0;
59 double scale_y_ = 0; 48 double scale_y_ = 0;
49 int real_area_size_ = 0;
60 50
61 std::vector<AreaIndicator> areas_; 51 std::vector<AreaIndicator> areas_;
62
63 bool panels_mode_ = false;
64 std::optional<std::string> savedata_path_;
65 std::set<std::string> solved_panels_;
66}; 52};
67 53
68#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ 54#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index eee43e4..bf2725a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -12,6 +12,7 @@
12 12
13#include "ap_state.h" 13#include "ap_state.h"
14#include "game_data.h" 14#include "game_data.h"
15#include "global.h"
15#include "logger.h" 16#include "logger.h"
16 17
17namespace { 18namespace {
@@ -25,6 +26,7 @@ struct Requirements {
25 std::set<int> rooms; // maybe 26 std::set<int> rooms; // maybe
26 bool mastery = false; // maybe 27 bool mastery = false; // maybe
27 bool panel_hunt = false; // maybe 28 bool panel_hunt = false; // maybe
29 bool postgame = false;
28 30
29 void Merge(const Requirements& rhs) { 31 void Merge(const Requirements& rhs) {
30 if (rhs.disabled) { 32 if (rhs.disabled) {
@@ -45,6 +47,7 @@ struct Requirements {
45 } 47 }
46 mastery = mastery || rhs.mastery; 48 mastery = mastery || rhs.mastery;
47 panel_hunt = panel_hunt || rhs.panel_hunt; 49 panel_hunt = panel_hunt || rhs.panel_hunt;
50 postgame = postgame || rhs.postgame;
48 } 51 }
49}; 52};
50 53
@@ -83,8 +86,6 @@ class RequirementCalculator {
83 break; 86 break;
84 } 87 }
85 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) { 88 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) {
86 requirements.rooms.insert(door_obj.room);
87
88 for (int panel_id : door_obj.panels) { 89 for (int panel_id : door_obj.panels) {
89 const Requirements& panel_reqs = GetPanel(panel_id); 90 const Requirements& panel_reqs = GetPanel(panel_id);
90 requirements.Merge(panel_reqs); 91 requirements.Merge(panel_reqs);
@@ -148,6 +149,10 @@ class RequirementCalculator {
148 } 149 }
149 } 150 }
150 151
152 if (panel_obj.location_name == GetWinCondition()) {
153 requirements.postgame = true;
154 }
155
151 panels_[panel_id] = requirements; 156 panels_[panel_id] = requirements;
152 } 157 }
153 158
@@ -162,11 +167,17 @@ class RequirementCalculator {
162struct TrackerState { 167struct TrackerState {
163 std::map<int, bool> reachability; 168 std::map<int, bool> reachability;
164 std::set<int> reachable_doors; 169 std::set<int> reachable_doors;
170 std::set<int> solveable_panels;
165 std::set<int> reachable_paintings; 171 std::set<int> reachable_paintings;
166 std::mutex reachability_mutex; 172 std::mutex reachability_mutex;
167 RequirementCalculator requirements; 173 RequirementCalculator requirements;
168 std::map<int, std::map<std::string, bool>> door_reports; 174 std::map<int, std::map<std::string, bool>> door_reports;
169 bool pilgrimage_doable = false; 175 bool pilgrimage_doable = false;
176
177 // If these are empty, it actually means everything is non-postgame.
178 std::set<int> non_postgame_areas;
179 std::set<int> non_postgame_locations;
180 std::set<int> non_postgame_paintings;
170}; 181};
171 182
172enum Decision { kYes, kNo, kMaybe }; 183enum Decision { kYes, kNo, kMaybe };
@@ -181,6 +192,11 @@ class StateCalculator;
181struct StateCalculatorOptions { 192struct StateCalculatorOptions {
182 int start; 193 int start;
183 bool pilgrimage = false; 194 bool pilgrimage = false;
195
196 // Treats all items as collected and all paintings as checked, but postgame
197 // areas cannot be reached.
198 bool postgame_detection = false;
199
184 StateCalculator* parent = nullptr; 200 StateCalculator* parent = nullptr;
185}; 201};
186 202
@@ -191,6 +207,16 @@ class StateCalculator {
191 explicit StateCalculator(StateCalculatorOptions options) 207 explicit StateCalculator(StateCalculatorOptions options)
192 : options_(options) {} 208 : options_(options) {}
193 209
210 void PreloadPanels(const std::set<int>& panels) {
211 solveable_panels_ = panels;
212 }
213
214 void PreloadDoors(const std::set<int>& doors) {
215 for (int door_id : doors) {
216 door_decisions_[door_id] = kYes;
217 }
218 }
219
194 void Calculate() { 220 void Calculate() {
195 painting_mapping_ = AP_GetPaintingMapping(); 221 painting_mapping_ = AP_GetPaintingMapping();
196 checked_paintings_ = AP_GetCheckedPaintings(); 222 checked_paintings_ = AP_GetCheckedPaintings();
@@ -236,7 +262,8 @@ class StateCalculator {
236 262
237 PaintingExit cur_painting = GD_GetPaintingExit(painting_id); 263 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
238 if (painting_mapping_.count(cur_painting.internal_id) && 264 if (painting_mapping_.count(cur_painting.internal_id) &&
239 checked_paintings_.count(cur_painting.internal_id)) { 265 (checked_paintings_.count(cur_painting.internal_id) ||
266 options_.postgame_detection)) {
240 Exit painting_exit; 267 Exit painting_exit;
241 PaintingExit target_painting = 268 PaintingExit target_painting =
242 GD_GetPaintingExit(GD_GetPaintingByName( 269 GD_GetPaintingExit(GD_GetPaintingByName(
@@ -360,6 +387,10 @@ class StateCalculator {
360 // evaluated. 387 // evaluated.
361 for (const Door& door : GD_GetDoors()) { 388 for (const Door& door : GD_GetDoors()) {
362 int discard = IsDoorReachable(door.id); 389 int discard = IsDoorReachable(door.id);
390
391 door_report_[door.id] = {};
392 discard = AreRequirementsSatisfied(
393 GetState().requirements.GetDoor(door.id), &door_report_[door.id]);
363 } 394 }
364 } 395 }
365 396
@@ -417,43 +448,48 @@ class StateCalculator {
417 return kNo; 448 return kNo;
418 } 449 }
419 450
451 if (reqs.postgame && options_.postgame_detection) {
452 return kNo;
453 }
454
420 Decision final_decision = kYes; 455 Decision final_decision = kYes;
421 456
422 for (int door_id : reqs.doors) { 457 if (!options_.postgame_detection) {
423 const Door& door_obj = GD_GetDoor(door_id); 458 for (int door_id : reqs.doors) {
424 Decision decision = IsNonGroupedDoorReachable(door_obj); 459 const Door& door_obj = GD_GetDoor(door_id);
460 Decision decision = IsNonGroupedDoorReachable(door_obj);
425 461
426 if (report) { 462 if (report) {
427 (*report)[door_obj.item_name] = (decision == kYes); 463 (*report)[door_obj.item_name] = (decision == kYes);
428 } 464 }
429 465
430 if (decision != kYes) { 466 if (decision != kYes) {
431 final_decision = decision; 467 final_decision = decision;
468 }
432 } 469 }
433 }
434 470
435 for (int panel_door_id : reqs.panel_doors) { 471 for (int panel_door_id : reqs.panel_doors) {
436 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id); 472 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id);
437 Decision decision = IsNonGroupedDoorReachable(panel_door_obj); 473 Decision decision = IsNonGroupedDoorReachable(panel_door_obj);
438 474
439 if (report) { 475 if (report) {
440 (*report)[AP_GetItemName(panel_door_obj.ap_item_id)] = 476 (*report)[panel_door_obj.item_name] = (decision == kYes);
441 (decision == kYes); 477 }
442 }
443 478
444 if (decision != kYes) { 479 if (decision != kYes) {
445 final_decision = decision; 480 final_decision = decision;
481 }
446 } 482 }
447 }
448 483
449 for (int item_id : reqs.items) { 484 for (int item_id : reqs.items) {
450 bool has_item = AP_HasItem(item_id); 485 bool has_item = AP_HasItem(item_id);
451 if (report) { 486 if (report) {
452 (*report)[AP_GetItemName(item_id)] = has_item; 487 (*report)[GD_GetItemName(item_id)] = has_item;
453 } 488 }
454 489
455 if (!has_item) { 490 if (!has_item) {
456 final_decision = kNo; 491 final_decision = kNo;
492 }
457 } 493 }
458 } 494 }
459 495
@@ -522,14 +558,7 @@ class StateCalculator {
522 } 558 }
523 559
524 Decision IsDoorReachable_Helper(int door_id) { 560 Decision IsDoorReachable_Helper(int door_id) {
525 if (door_report_.count(door_id)) { 561 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id));
526 door_report_[door_id].clear();
527 } else {
528 door_report_[door_id] = {};
529 }
530
531 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id),
532 &door_report_[door_id]);
533 } 562 }
534 563
535 Decision IsDoorReachable(int door_id) { 564 Decision IsDoorReachable(int door_id) {
@@ -661,18 +690,85 @@ class StateCalculator {
661} // namespace 690} // namespace
662 691
663void ResetReachabilityRequirements() { 692void ResetReachabilityRequirements() {
693 TrackerLog("Resetting tracker state...");
694
664 std::lock_guard reachability_guard(GetState().reachability_mutex); 695 std::lock_guard reachability_guard(GetState().reachability_mutex);
665 GetState().requirements.Reset(); 696 GetState().requirements.Reset();
697 GetState().reachable_doors.clear();
698 GetState().solveable_panels.clear();
699
700 if (AP_IsPostgameShuffle()) {
701 GetState().non_postgame_areas.clear();
702 GetState().non_postgame_locations.clear();
703 GetState().non_postgame_paintings.clear();
704 } else {
705 StateCalculator postgame_calculator(
706 {.start = GD_GetRoomByName("Menu"), .postgame_detection = true});
707 postgame_calculator.Calculate();
708
709 std::set<int>& non_postgame_areas = GetState().non_postgame_areas;
710 non_postgame_areas.clear();
711
712 std::set<int>& non_postgame_locations = GetState().non_postgame_locations;
713 non_postgame_locations.clear();
714
715 const std::set<int>& reachable_rooms =
716 postgame_calculator.GetReachableRooms();
717 const std::set<int>& solveable_panels =
718 postgame_calculator.GetSolveablePanels();
719
720 for (const MapArea& map_area : GD_GetMapAreas()) {
721 bool area_reachable = false;
722
723 for (const Location& location_section : map_area.locations) {
724 bool reachable = reachable_rooms.count(location_section.room);
725 if (reachable) {
726 for (int panel_id : location_section.panels) {
727 reachable &= (solveable_panels.count(panel_id) == 1);
728 }
729 }
730
731 if (!reachable && IsLocationWinCondition(location_section)) {
732 reachable = true;
733 }
734
735 if (reachable) {
736 non_postgame_locations.insert(location_section.ap_location_id);
737 area_reachable = true;
738 }
739 }
740
741 for (int painting_id : map_area.paintings) {
742 if (postgame_calculator.GetReachablePaintings().count(painting_id)) {
743 area_reachable = true;
744 }
745 }
746
747 if (area_reachable) {
748 non_postgame_areas.insert(map_area.id);
749 }
750 }
751
752 GetState().non_postgame_paintings =
753 postgame_calculator.GetReachablePaintings();
754 }
666} 755}
667 756
668void RecalculateReachability() { 757void RecalculateReachability() {
758 TrackerLog("Calculating reachability...");
759
669 std::lock_guard reachability_guard(GetState().reachability_mutex); 760 std::lock_guard reachability_guard(GetState().reachability_mutex);
670 761
762 // Receiving items and checking paintings should never remove access to doors
763 // or panels, so we can preload any doors and panels we already know are
764 // accessible from previous runs, in order to reduce the work.
671 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 765 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
766 state_calculator.PreloadDoors(GetState().reachable_doors);
767 state_calculator.PreloadPanels(GetState().solveable_panels);
672 state_calculator.Calculate(); 768 state_calculator.Calculate();
673 769
674 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); 770 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms();
675 const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); 771 std::set<int> solveable_panels = state_calculator.GetSolveablePanels();
676 772
677 std::map<int, bool> new_reachability; 773 std::map<int, bool> new_reachability;
678 for (const MapArea& map_area : GD_GetMapAreas()) { 774 for (const MapArea& map_area : GD_GetMapAreas()) {
@@ -703,6 +799,7 @@ void RecalculateReachability() {
703 799
704 std::swap(GetState().reachability, new_reachability); 800 std::swap(GetState().reachability, new_reachability);
705 std::swap(GetState().reachable_doors, new_reachable_doors); 801 std::swap(GetState().reachable_doors, new_reachable_doors);
802 std::swap(GetState().solveable_panels, solveable_panels);
706 std::swap(GetState().reachable_paintings, reachable_paintings); 803 std::swap(GetState().reachable_paintings, reachable_paintings);
707 std::swap(GetState().door_reports, door_reports); 804 std::swap(GetState().door_reports, door_reports);
708 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); 805 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable();
@@ -741,3 +838,33 @@ bool IsPilgrimageDoable() {
741 838
742 return GetState().pilgrimage_doable; 839 return GetState().pilgrimage_doable;
743} 840}
841
842bool IsAreaPostgame(int area_id) {
843 std::lock_guard reachability_guard(GetState().reachability_mutex);
844
845 if (GetState().non_postgame_areas.empty()) {
846 return false;
847 } else {
848 return !GetState().non_postgame_areas.count(area_id);
849 }
850}
851
852bool IsLocationPostgame(int location_id) {
853 std::lock_guard reachability_guard(GetState().reachability_mutex);
854
855 if (GetState().non_postgame_locations.empty()) {
856 return false;
857 } else {
858 return !GetState().non_postgame_locations.count(location_id);
859 }
860}
861
862bool IsPaintingPostgame(int painting_id) {
863 std::lock_guard reachability_guard(GetState().reachability_mutex);
864
865 if (GetState().non_postgame_paintings.empty()) {
866 return false;
867 } else {
868 return !GetState().non_postgame_paintings.count(painting_id);
869 }
870}
diff --git a/src/tracker_state.h b/src/tracker_state.h index a8f155d..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h
@@ -18,4 +18,10 @@ const std::map<std::string, bool>& GetDoorRequirements(int door_id);
18 18
19bool IsPilgrimageDoable(); 19bool IsPilgrimageDoable();
20 20
21bool IsAreaPostgame(int area_id);
22
23bool IsLocationPostgame(int location_id);
24
25bool IsPaintingPostgame(int painting_id);
26
21#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ 27#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..2b05daf --- /dev/null +++ b/src/updater.cpp
@@ -0,0 +1,309 @@
1#include "updater.h"
2
3#include <fmt/core.h>
4#include <openssl/evp.h>
5#include <openssl/sha.h>
6#include <wx/evtloop.h>
7#include <wx/progdlg.h>
8#include <wx/webrequest.h>
9#include <wx/wfstream.h>
10#include <wx/zipstrm.h>
11#include <yaml-cpp/yaml.h>
12
13#include <cstdio>
14#include <deque>
15#include <filesystem>
16#include <fstream>
17
18#include "global.h"
19#include "logger.h"
20#include "version.h"
21
22constexpr const char* kVersionFileUrl =
23 "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
24constexpr const char* kChangelogUrl =
25 "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
26
27namespace {
28
29std::string CalculateStringSha256(const wxString& data) {
30 unsigned char hash[SHA256_DIGEST_LENGTH];
31 EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
32 EVP_DigestInit(sha256, EVP_sha256());
33 EVP_DigestUpdate(sha256, data.c_str(), data.length());
34 EVP_DigestFinal_ex(sha256, hash, nullptr);
35 EVP_MD_CTX_free(sha256);
36
37 char output[65] = {0};
38 for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
39 snprintf(output + (i * 2), 3, "%02x", hash[i]);
40 }
41
42 return std::string(output);
43}
44
45} // namespace
46
47Updater::Updater(wxFrame* parent) : parent_(parent) {
48 Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
49}
50
51void Updater::Cleanup() {
52 std::filesystem::path oldDir = GetExecutableDirectory() / "old";
53 if (std::filesystem::is_directory(oldDir)) {
54 std::filesystem::remove_all(oldDir);
55 }
56}
57
58void Updater::CheckForUpdates(bool invisible) {
59 wxWebRequest versionRequest =
60 wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);
61
62 if (invisible) {
63 update_state_ = UpdateState::GetVersionInvisible;
64
65 versionRequest.Start();
66 } else {
67 update_state_ = UpdateState::GetVersionManual;
68
69 if (DownloadWithProgress(versionRequest)) {
70 if (versionRequest.GetState() == wxWebRequest::State_Failed) {
71 wxMessageBox("Could not check for updates.", "Error",
72 wxOK | wxICON_ERROR);
73 } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
74 ProcessVersionFile(
75 versionRequest.GetResponse().AsString().utf8_string());
76 }
77 }
78 }
79}
80
81void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
82 if (update_state_ == UpdateState::GetVersionInvisible) {
83 if (evt.GetState() == wxWebRequest::State_Completed) {
84 ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
85 } else if (evt.GetState() == wxWebRequest::State_Failed) {
86 parent_->SetStatusText("Could not check for updates.");
87 }
88 }
89}
90
91void Updater::ProcessVersionFile(std::string data) {
92 try {
93 YAML::Node versionInfo = YAML::Load(data);
94 Version latestVersion(versionInfo["version"].as<std::string>());
95
96 if (kTrackerVersion < latestVersion) {
97 if (versionInfo["packages"]) {
98 std::string platformIdentifier;
99
100 if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
101 platformIdentifier = "win64";
102 }
103
104 if (!platformIdentifier.empty() &&
105 versionInfo["packages"][platformIdentifier]) {
106 wxMessageDialog dialog(
107 nullptr,
108 fmt::format("There is a newer version of Lingo AP Tracker "
109 "available. You have {}, and the latest version is "
110 "{}. Would you like to update?",
111 kTrackerVersion.ToString(), latestVersion.ToString()),
112 "Update available", wxYES_NO | wxCANCEL);
113 dialog.SetYesNoLabels("Install update", "Open changelog");
114
115 int dlgResult = dialog.ShowModal();
116 if (dlgResult == wxID_YES) {
117 const YAML::Node& packageInfo =
118 versionInfo["packages"][platformIdentifier];
119 std::string packageUrl = packageInfo["url"].as<std::string>();
120 std::string packageChecksum =
121 packageInfo["checksum"].as<std::string>();
122
123 std::vector<std::filesystem::path> packageFiles;
124 if (packageInfo["files"]) {
125 for (const YAML::Node& filename : packageInfo["files"]) {
126 packageFiles.push_back(filename.as<std::string>());
127 }
128 }
129
130 std::vector<std::filesystem::path> deletedFiles;
131 if (packageInfo["deleted_files"]) {
132 for (const YAML::Node& filename : packageInfo["deleted_files"]) {
133 deletedFiles.push_back(filename.as<std::string>());
134 }
135 }
136
137 InstallUpdate(packageUrl, packageChecksum, packageFiles,
138 deletedFiles);
139 } else if (dlgResult == wxID_NO) {
140 wxLaunchDefaultBrowser(kChangelogUrl);
141 }
142
143 return;
144 }
145 }
146
147 if (wxMessageBox(
148 fmt::format("There is a newer version of Lingo AP Tracker "
149 "available. You have {}, and the latest version is "
150 "{}. Would you like to update?",
151 kTrackerVersion.ToString(), latestVersion.ToString()),
152 "Update available", wxYES_NO) == wxYES) {
153 wxLaunchDefaultBrowser(kChangelogUrl);
154 }
155 } else if (update_state_ == UpdateState::GetVersionManual) {
156 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
157 }
158 } catch (const std::exception& ex) {
159 wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
160 }
161}
162
163void Updater::InstallUpdate(std::string url, std::string checksum,
164 std::vector<std::filesystem::path> files,
165 std::vector<std::filesystem::path> deletedFiles) {
166 update_state_ = UpdateState::GetPackage;
167
168 wxWebRequest packageRequest =
169 wxWebSession::GetDefault().CreateRequest(this, url);
170
171 if (!DownloadWithProgress(packageRequest)) {
172 return;
173 }
174
175 bool download_issue = false;
176
177 wxFileName package_path;
178 package_path.AssignTempFileName("");
179
180 if (!package_path.IsOk()) {
181 download_issue = true;
182 } else {
183 wxFileOutputStream writeOut(package_path.GetFullPath());
184 wxString fileData = packageRequest.GetResponse().AsString();
185 writeOut.WriteAll(fileData.c_str(), fileData.length());
186
187 std::string downloadedChecksum = CalculateStringSha256(fileData);
188 if (downloadedChecksum != checksum) {
189 download_issue = true;
190 }
191 }
192
193 if (download_issue) {
194 if (wxMessageBox("There was an issue downloading the update. Would you "
195 "like to manually download it instead?",
196 "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
197 wxLaunchDefaultBrowser(kChangelogUrl);
198 }
199 return;
200 }
201
202 std::filesystem::path newArea = GetExecutableDirectory();
203 std::filesystem::path oldArea = newArea / "old";
204 std::set<std::filesystem::path> folders;
205 std::set<std::filesystem::path> filesToMove;
206 for (const std::filesystem::path& existingFile : files) {
207 std::filesystem::path movedPath = oldArea / existingFile;
208 std::filesystem::path movedDir = movedPath;
209 movedDir.remove_filename();
210 folders.insert(movedDir);
211 filesToMove.insert(existingFile);
212 }
213 for (const std::filesystem::path& existingFile : deletedFiles) {
214 std::filesystem::path movedPath = oldArea / existingFile;
215 std::filesystem::path movedDir = movedPath;
216 movedDir.remove_filename();
217 folders.insert(movedDir);
218 }
219
220 for (const std::filesystem::path& newFolder : folders) {
221 TrackerLog(fmt::format("Creating directory {}", newFolder.string()));
222
223 std::filesystem::create_directories(newFolder);
224 }
225
226 for (const std::filesystem::path& existingFile : files) {
227 std::filesystem::path existingPath = newArea / existingFile;
228
229 if (std::filesystem::is_regular_file(existingPath)) {
230 std::filesystem::path movedPath = oldArea / existingFile;
231
232 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
233 movedPath.string()));
234
235 std::filesystem::rename(existingPath, movedPath);
236 }
237 }
238 for (const std::filesystem::path& existingFile : deletedFiles) {
239 std::filesystem::path existingPath = newArea / existingFile;
240
241 if (std::filesystem::is_regular_file(existingPath)) {
242 std::filesystem::path movedPath = oldArea / existingFile;
243
244 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
245 movedPath.string()));
246
247 std::filesystem::rename(existingPath, movedPath);
248 }
249 }
250
251 wxFileInputStream fileInputStream(package_path.GetFullPath());
252 wxZipInputStream zipStream(fileInputStream);
253 std::unique_ptr<wxZipEntry> zipEntry;
254 while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
255 nullptr) {
256 if (zipEntry->IsDir()) {
257 continue;
258 }
259
260 std::filesystem::path archivePath = zipEntry->GetName().utf8_string();
261
262 TrackerLog(fmt::format("Found {} in archive", archivePath.string()));
263
264 // Cut off the root folder name
265 std::filesystem::path subPath;
266 for (auto it = std::next(archivePath.begin()); it != archivePath.end();
267 it++) {
268 subPath /= *it;
269 }
270
271 std::filesystem::path pastePath = newArea / subPath;
272
273 wxFileOutputStream fileOutput(pastePath.string());
274 zipStream.Read(fileOutput);
275 }
276
277 if (wxMessageBox(
278 "Update installed! The tracker must be restarted for the changes to take "
279 "effect. Do you want to close the tracker?",
280 "Update installed", wxYES_NO) == wxYES) {
281 wxExit();
282 }
283}
284
285bool Updater::DownloadWithProgress(wxWebRequest& request) {
286 request.Start();
287
288 wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
289 100, nullptr,
290 wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
291 wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
292 while (request.GetState() != wxWebRequest::State_Completed &&
293 request.GetState() != wxWebRequest::State_Failed) {
294 if (request.GetBytesExpectedToReceive() == -1) {
295 if (!dialog.Pulse()) {
296 request.Cancel();
297 return false;
298 }
299 } else {
300 dialog.SetRange(request.GetBytesExpectedToReceive());
301 if (!dialog.Update(request.GetBytesReceived())) {
302 request.Cancel();
303 return false;
304 }
305 }
306 }
307
308 return true;
309}
diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..c604a49 --- /dev/null +++ b/src/updater.h
@@ -0,0 +1,46 @@
1#ifndef UPDATER_H_809E7381
2#define UPDATER_H_809E7381
3
4#include <filesystem>
5#include <set>
6#include <string>
7
8#include <wx/wxprec.h>
9
10#ifndef WX_PRECOMP
11#include <wx/wx.h>
12#endif
13
14class wxWebRequest;
15class wxWebRequestEvent;
16
17class Updater : public wxEvtHandler {
18 public:
19 explicit Updater(wxFrame* parent);
20
21 void Cleanup();
22
23 void CheckForUpdates(bool invisible);
24
25 private:
26 enum class UpdateState {
27 GetVersionInvisible,
28 GetVersionManual,
29 GetPackage,
30 };
31
32 void OnWebRequestState(wxWebRequestEvent& event);
33
34 void ProcessVersionFile(std::string data);
35
36 void InstallUpdate(std::string url, std::string checksum,
37 std::vector<std::filesystem::path> files,
38 std::vector<std::filesystem::path> deletedFiles);
39
40 bool DownloadWithProgress(wxWebRequest& request);
41
42 wxFrame* parent_;
43 UpdateState update_state_ = UpdateState::GetVersionInvisible;
44};
45
46#endif /* end of include guard: UPDATER_H_809E7381 */
diff --git a/src/version.h b/src/version.h index f734f02..3439fda 100644 --- a/src/version.h +++ b/src/version.h
@@ -36,6 +36,6 @@ struct Version {
36 } 36 }
37}; 37};
38 38
39constexpr const Version kTrackerVersion = Version(0, 12, 0); 39constexpr const Version kTrackerVersion = Version(2, 0, 2);
40 40
41#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file 41#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file
diff --git a/src/windows.rc b/src/windows.rc new file mode 100644 index 0000000..8ba30ed --- /dev/null +++ b/src/windows.rc
@@ -0,0 +1,3 @@
1#define wxUSE_RC_MANIFEST 1
2#define wxUSE_DPI_AWARE_MANIFEST 2
3#include "wx/msw/wx.rc"
diff --git a/vcpkg.json b/vcpkg.json index e13d228..581a507 100644 --- a/vcpkg.json +++ b/vcpkg.json
@@ -1,6 +1,5 @@
1{ 1{
2 "dependencies": [ 2 "dependencies": [
3 "websocketpp",
4 "wxwidgets", 3 "wxwidgets",
5 "openssl", 4 "openssl",
6 "yaml-cpp", 5 "yaml-cpp",
diff --git a/vendor/vcpkg b/vendor/vcpkg
Subproject 3dd44b931481d7a8e9ba412621fa810232b6628 Subproject df8bfe519564ae001903e5cdd32af0999531ef7
diff --git a/vendor/websocketpp b/vendor/websocketpp new file mode 160000
Subproject 1b11fd301531e6df35a6107c1e8665b1e77a2d8