about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules3
-rw-r--r--CHANGELOG.md154
-rw-r--r--CMakeLists.txt36
-rw-r--r--README.md21
-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.yaml717
-rw-r--r--src/achievements_pane.cpp15
-rw-r--r--src/achievements_pane.h6
-rw-r--r--src/ap_state.cpp849
-rw-r--r--src/ap_state.h38
-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.cpp320
-rw-r--r--src/game_data.h26
-rw-r--r--src/global.cpp16
-rw-r--r--src/global.h2
-rw-r--r--src/godot_variant.cpp83
-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.cpp54
-rw-r--r--src/ipc_dialog.h24
-rw-r--r--src/ipc_state.cpp367
-rw-r--r--src/ipc_state.h23
-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/network_set.cpp29
-rw-r--r--src/network_set.h16
-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.cpp419
-rw-r--r--src/subway_map.h13
-rw-r--r--src/tracker_config.cpp14
-rw-r--r--src/tracker_config.h10
-rw-r--r--src/tracker_frame.cpp279
-rw-r--r--src/tracker_frame.h89
-rw-r--r--src/tracker_panel.cpp211
-rw-r--r--src/tracker_panel.h20
-rw-r--r--src/tracker_state.cpp251
-rw-r--r--src/tracker_state.h6
-rw-r--r--src/updater.cpp309
-rw-r--r--src/updater.h46
-rw-r--r--src/version.cpp5
-rw-r--r--src/version.h2
-rw-r--r--src/windows.rc3
-rw-r--r--vcpkg.json1
m---------vendor/vcpkg0
m---------vendor/websocketpp0
65 files changed, 4143 insertions, 1447 deletions
diff --git a/.gitignore b/.gitignore index a7cadc7..1ca77eb 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,5 +1,6 @@
1build/ 1build/
2builds/ 2builds/
3assets/LL1.yaml 3assets/LL1.yaml
4assets/ids.yaml
4.DS_Store 5.DS_Store
5.vs 6.vs
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 a9ce76d..e2444b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,159 @@
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
88## v0.12.0 - 2024-12-20
89
90- The tracker can now connect to a game of Lingo that is running the Archipelago
91 client (requires v5.1.0 or later). This allows the tracker to show the
92 player's position more precisely, as well as get automatically updated on what
93 non-check panels have been solved.
94- Fixed one of the doors (Outside The Initiated - Eight Door) showing up
95 incorrectly on the subway map.
96
97Download:
98[lingo-ap-tracker-v0.12.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.0-win64.zip)<br/>
99Source: [v0.12.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.0)
100
101## v0.11.5 - 2024-12-08
102
103- [A logic error](https://github.com/ArchipelagoMW/Archipelago/pull/4342) was
104 found in Archipelago 0.5.1 related to the number hunt when playing on panels
105 mode door shuffle. In order to work around this, the tracker now uses the same
106 incorrect logic as the generator. This means that you will be able to tell
107 what panels you are expected to solve, even if it is not intuitive. There is
108 already a fix for the logic error, which will likely be included in the next
109 major Archipelago release, at which point this workaround will be removed.
110
111Download:
112[lingo-ap-tracker-v0.11.5-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.5-win64.zip)<br/>
113Source: [v0.11.5](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.5)
114
115## v0.11.4 - 2024-12-05
116
117- Fixed an issue where Starting Room panels would show up as checks when playing
118 panels mode door shuffle.
119
120Download:
121[lingo-ap-tracker-v0.11.4-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.4-win64.zip)<br/>
122Source: [v0.11.4](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.4)
123
124## v0.11.3 - 2024-11-26
125
126- Compatibility update for Archipelago 0.5.1
127
128Download:
129[lingo-ap-tracker-v0.11.3-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.3-win64.zip)<br/>
130Source: [v0.11.3](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.3)
131
132## v0.11.2 - 2024-09-24
133
134- One-way connections on the subway map are now indicated by a circle at the
135 exit. Contributed by art0007i.
136
137Download:
138[lingo-ap-tracker-v0.11.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.2-win64.zip)<br/>
139Source: [v0.11.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.2)
140
141## v0.11.1 - 2024-07-25
142
143- The Pilgrim Antechamber sunwarp on the subway map now shows all sunwarp
144 connections, and is red if a pilgrimage is not possible.
145- The save analysis panel now uses the remote location status for non-counting
146 panels.
147- Fixed positioning of Outside The Undeterred - Number Hunt door on subway map.
148- Fixed subway map issue when sunwarp shuffle and individual/progressive sunwarp
149 access were combined where the icons on the map would show unshuffled access.
150- Map area indicators now correctly treat unreachable pre-checked paintings as
151 unchecked.
152
153Download:
154[lingo-ap-tracker-v0.11.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.1-win64.zip)<br/>
155Source: [v0.11.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.1)
156
3## v0.11.0 - 2024-07-19 157## v0.11.0 - 2024-07-19
4 158
5- Added a savedata analyzer. When connected to a world, the user can open up the 159- Added a savedata analyzer. When connected to a world, the user can open up the
diff --git a/CMakeLists.txt b/CMakeLists.txt index e1cb7f0..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,9 +48,33 @@ 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" 51 "src/ipc_state.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"
52 "vendor/whereami/whereami.c" 60 "vendor/whereami/whereami.c"
53) 61)
62
63if (MSVC)
64list(APPEND SOURCE_FILES "src/windows.rc")
65endif(MSVC)
66
67add_executable(lingo_ap_tracker ${SOURCE_FILES})
54set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) 68set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
55set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON) 69set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON)
56target_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)
71
72set(SRC_DIR "${CMAKE_SOURCE_DIR}/assets")
73set(DST_DIR "${CMAKE_BINARY_DIR}/$<CONFIG>/assets")
74
75add_custom_target(copy_assets ALL
76 COMMAND ${CMAKE_COMMAND} -E copy_directory ${SRC_DIR} ${DST_DIR}
77 COMMENT "Copying folder from ${SRC_DIR} to ${DST_DIR}"
78)
79
80add_dependencies(lingo_ap_tracker copy_assets)
diff --git a/README.md b/README.md index 83525dd..cb3e90d 100644 --- a/README.md +++ b/README.md
@@ -9,4 +9,23 @@ Releases of the tracker can be found [on the releases page](https://code.fourisl
9 9
10## Acknowledgments 10## Acknowledgments
11 11
12Thanks to Kinrah for making the subway map image! 12* Brenton Wildes: Created Lingo, and drew some of the images used in the tracker.
13* Kinrah: Made the subway map image.
14* art0007i: Contributed to the display of the subway map.
15
16## Building
17
18To build the app:
19
201. Clone the repository including submodules: `git clone --recursive https://code.fourisland.com/lingo-ap-tracker`
212. Put [LL1.yaml from archipelago](https://github.com/ArchipelagoMW/Archipelago/raw/main/worlds/lingo/data/LL1.yaml) in ./assets
223. Put [ids.yaml from archipelago](https://github.com/ArchipelagoMW/Archipelago/raw/main/worlds/lingo/data/ids.yaml) in ./assets
234. Configure the project: `cmake --preset=lingo-ap-tracker-preset`
245. Build the application in debug mode: `cmake --build --preset=lingo-ap-tracker-preset`
256. (Optional) Build the application in release mode: `cmake --build --preset=x64-release-preset`
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.
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 e88c34f..b02d37b 100644 --- a/VERSION +++ b/VERSION
@@ -1 +1 @@
v0.11.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 dcc58b2..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 18 entrances:
17 tags:
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 tags:
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 30 entrances:
31 tags:
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 36 entrances:
38 tags:
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 42 entrances:
45 tags:
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 48 entrances:
52 tags:
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 64 entrances:
69 tags:
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 86 exits:
92 tags:
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 104 entrances:
111 tags:
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,16 +139,15 @@
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 150 entrances:
157 tags:
158 - smiley_crossroads 151 - smiley_crossroads
159- pos: [878, 509] 152- pos: [878, 509]
160 sunwarp: 153 sunwarp:
@@ -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,22 +197,20 @@
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 202 exits:
211 tags:
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 213 entrances:
223 tags:
224 - pencil_compass 214 - pencil_compass
225- pos: [1196, 417] 215- pos: [1196, 417]
226 invisible: true 216 invisible: true
@@ -248,10 +238,9 @@
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 243 exits:
254 tags:
255 - garden_starting 244 - garden_starting
256- pos: [1565, 311] 245- pos: [1565, 311]
257 room: The Fearless (First Floor) 246 room: The Fearless (First Floor)
@@ -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 262 exits:
274 tags:
275 - crown_tower6 263 - crown_tower6
276- pos: [1653, 717] 264- pos: [1644, 685]
277 paintings: 265 painting: eight_painting2
278 - eight_painting2 266 entrances:
279 tags:
280 - eight_alcove 267 - eight_alcove
281- pos: [1653, 662] 268- pos: [1660, 664]
282 paintings: 269 painting: eight_painting
283 - eight_painting 270 exits:
284 tags:
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,83 @@
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 396 entrances:
405 tags:
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 420 entrances:
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 tags:
439 - owl_tower6
440 - clock_tower6
441 - panda_tower6 421 - panda_tower6
422- pos: [363, 1221]
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
430 exits:
431 - hi_scientific
432- pos: [380, 1237]
433 painting: crown_painting2
434 entrances:
442 - crown_tower6 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:
443 - apple_tower6 445 - apple_tower6
444 - hi_scientific 446- pos: [360, 1135]
445- pos: [349, 1124] 447 # TODO: there isn't really a spot for this one
446 paintings: 448 painting: map_painting2
447 - map_painting2
448- pos: [436, 1159] 449- pos: [436, 1159]
449 room: Orange Tower Seventh Floor 450 room: Orange Tower Seventh Floor
450 door: Mastery 451 door: Mastery
451- pos: [544, 1159] 452- pos: [535, 1146]
452 paintings: 453 painting: arrows_painting_11
453 - arrows_painting_11
454- pos: [498, 284] 454- pos: [498, 284]
455 room: Courtyard 455 room: Courtyard
456 door: Green Barrier 456 door: Green Barrier
457- pos: [556, 233] 457- pos: [589, 230]
458 paintings: 458 painting: flower_painting_7
459 - flower_painting_7 459 exits:
460 tags:
461 - flower_starting 460 - flower_starting
462 - flower_arrow 461 - flower_arrow
463- pos: [600, 332] 462- pos: [600, 332]
464 room: Number Hunt 463 room: Number Hunt
465 door: Nines 464 door: Nines
466- pos: [579, 350] 465- pos: [589, 369]
467 paintings: 466 painting: blueman_painting
468 - blueman_painting 467 entrances:
469 tags:
470 - blueman_courtyard 468 - blueman_courtyard
471- pos: [530, 310] 469- pos: [530, 310]
472 room: First Second Third Fourth 470 room: First Second Third Fourth
@@ -501,14 +499,13 @@
501- pos: [922, 107] 499- pos: [922, 107]
502 room: The Colorful (Gray) 500 room: The Colorful (Gray)
503 door: Progress Door 501 door: Progress Door
504- pos: [967, 107] 502- pos: [969, 96]
505 paintings: 503 painting: arrows_painting_12
506 - arrows_painting_12
507- pos: [878, 954] 504- pos: [878, 954]
508 room: Welcome Back Area 505 room: Welcome Back Area
509 door: Shortcut to Starting Room 506 door: Shortcut to Starting Room
510- pos: [773, 954] 507- pos: [773, 954]
511 tags: 508 exits:
512 - hub_wb 509 - hub_wb
513 - wondrous_wb 510 - wondrous_wb
514 - undeterred_wb 511 - undeterred_wb
@@ -519,45 +516,48 @@
519 - scientific_wb 516 - scientific_wb
520 - cellar_wb 517 - cellar_wb
521- pos: [1107, 749] 518- pos: [1107, 749]
522 tags: 519 entrances:
523 - hub_wb 520 - hub_wb
524- pos: [408, 817] 521- pos: [408, 817]
525 tags: 522 entrances:
526 - wondrous_wb 523 - wondrous_wb
527- pos: [281, 1017] 524- pos: [281, 1017]
528 tags: 525 entrances:
529 - undeterred_wb 526 - undeterred_wb
530- pos: [1017, 289] 527- pos: [1017, 289]
531 tags: 528 entrances:
532 - agreeable_wb 529 - agreeable_wb
533- pos: [907, 1385] 530- pos: [907, 1385]
534 tags: 531 entrances:
535 - wanderer_wb 532 - wanderer_wb
536- pos: [1737, 1053] 533- pos: [1737, 1053]
537 tags: 534 entrances:
538 - gallery_wb 535 - gallery_wb
539- pos: [1690, 268] 536- pos: [1784, 395]
540 tags: 537 entrances:
541 - observant_wb 538 - observant_wb
542- pos: [250, 604] 539- pos: [122, 350]
543 tags: 540 entrances:
544 - scientific_wb 541 - scientific_wb
545- pos: [1553, 1467] 542- pos: [1553, 1467]
546 tags: 543 entrances:
547 - cellar_wb 544 - cellar_wb
548- pos: [1478, 498] 545- pos: [1478, 498]
549 room: Owl Hallway 546 room: Owl Hallway
550 door: Shortcut to Hedge Maze 547 door: Shortcut to Hedge Maze
551- pos: [1480, 551] 548- pos: [1467, 535]
552 paintings: 549 painting: arrows_painting_8
553 - arrows_painting_8 550- pos: [1467, 562]
554 - maze_painting_2 551 painting: maze_painting_2
555 - owl_painting_2 552 entrances:
556 - clock_painting_4
557 tags:
558 - green_owl 553 - green_owl
554- pos: [1510, 535]
555 painting: owl_painting_2
556 exits:
559 - owl_hidden 557 - owl_hidden
560 - owl_tower6 558 - owl_tower6
559- pos: [1510, 562]
560 painting: clock_painting_4
561- pos: [1478, 938] 561- pos: [1478, 938]
562 room: Number Hunt 562 room: Number Hunt
563 door: Sevens 563 door: Sevens
@@ -582,41 +582,39 @@
582- pos: [1511, 841] 582- pos: [1511, 841]
583 room: Outside The Initiated 583 room: Outside The Initiated
584 door: Initiated Entrance 584 door: Initiated Entrance
585- pos: [1141, 1441] 585- pos: [1071, 1441]
586 room: Orange Tower Third Floor 586 room: Orange Tower Third Floor
587 door: Orange Barrier 587 door: Orange Barrier
588- pos: [1173, 1441] 588- pos: [1104, 1441]
589 room: Outside The Initiated 589 room: Outside The Initiated
590 door: Green Barrier 590 door: Green Barrier
591- pos: [1206, 1441] 591- pos: [1137, 1441]
592 room: Outside The Initiated 592 room: Outside The Initiated
593 door: Purple Barrier 593 door: Purple Barrier
594- pos: [1189, 1355] 594- pos: [1120, 1355]
595 room: Outside The Initiated 595 room: Outside The Initiated
596 door: Entrance 596 door: Entrance
597- pos: [1580, 729] 597- pos: [1580, 729]
598 room: Outside The Initiated 598 room: The Incomparable
599 door: Eight Door 599 door: "Eight Door (Outside The Initiated)"
600- pos: [1530, 938] 600- pos: [1548, 948]
601 paintings: 601 painting: clock_painting_5
602 - clock_painting_5 602 entrances:
603 tags:
604 - clock_initiated 603 - clock_initiated
605- pos: [1546, 938] 604- pos: [1575, 969]
606 paintings: 605 painting: arrows_painting_2
607 - clock_painting_2 606- pos: [1575, 926]
608 - arrows_painting_2 607 painting: clock_painting_2
609 tags: 608 exits:
610 - clock_tower6 609 - clock_tower6
611 - clock_initiated 610 - clock_initiated
612- pos: [1579, 813] 611- pos: [1580, 814]
613 sunwarp: 612 sunwarp:
614 dots: 3 613 dots: 3
615 type: exit 614 type: exit
616- pos: [1444, 896] 615- pos: [1462, 915]
617 paintings: 616 painting: smile_painting_1
618 - smile_painting_1 617 entrances:
619 tags:
620 - smiley_initiated 618 - smiley_initiated
621- pos: [1430, 691] 619- pos: [1430, 691]
622 room: Outside The Undeterred 620 room: Outside The Undeterred
@@ -624,17 +622,25 @@
624- pos: [1468, 728] 622- pos: [1468, 728]
625 room: The Traveled 623 room: The Traveled
626 door: Color Hallways Entrance 624 door: Color Hallways Entrance
627- pos: [1533, 707] 625- pos: [1535, 644]
628 tags: 626 tags:
629 - red_ch 627 - red_ch
628- pos: [1535, 667]
629 tags:
630 - blue_ch 630 - blue_ch
631- pos: [1535, 689]
632 tags:
631 - yellow_ch 633 - yellow_ch
634- pos: [1535, 711]
635 tags:
632 - green_ch 636 - green_ch
637- pos: [1499, 678]
638 exits:
633 - early_ch 639 - early_ch
634- pos: [1567, 1264] 640- pos: [1567, 1264]
635 tags: 641 tags:
636 - red_ch 642 - red_ch
637- pos: [150, 808] 643- pos: [150, 748]
638 tags: 644 tags:
639 - blue_ch 645 - blue_ch
640- pos: [626, 371] 646- pos: [626, 371]
@@ -652,21 +658,21 @@
652- pos: [1468, 1377] 658- pos: [1468, 1377]
653 room: Outside The Bold 659 room: Outside The Bold
654 door: Bold Entrance 660 door: Bold Entrance
655- pos: [1425, 1358] 661- pos: [1446, 1344]
656 paintings: 662 painting: pencil_painting2
657 - pencil_painting2 663 exits:
658 - north_missing2
659 tags:
660 - pencil_compass 664 - pencil_compass
661 - pencil_starting 665 - pencil_starting
662 - pencil_directional 666 - pencil_directional
667- pos: [1580, 1344]
668 painting: north_missing2
663- pos: [1334, 1419] 669- pos: [1334, 1419]
664 room: Outside The Bold 670 room: Outside The Bold
665 door: Steady Entrance 671 door: Steady Entrance
666- pos: [445, 1048] 672- pos: [445, 1048]
667 tags: 673 exits:
668 - undeterred_artistic 674 - undeterred_artistic
669- pos: [279, 1071] 675- pos: [273, 1071]
670 room: Number Hunt 676 room: Number Hunt
671 door: Zero Door 677 door: Zero Door
672- pos: [338, 1071] 678- pos: [338, 1071]
@@ -681,18 +687,16 @@
681- pos: [242, 1071] 687- pos: [242, 1071]
682 room: Outside The Undeterred 688 room: Outside The Undeterred
683 door: Undeterred Entrance 689 door: Undeterred Entrance
684- pos: [60, 1017] 690- pos: [149, 937]
685 paintings: 691 painting: blueman_painting_2
686 - blueman_painting_2 692 exits:
687 tags:
688 - blueman_courtyard 693 - blueman_courtyard
689 - blueman_starting 694 - blueman_starting
690- pos: [402, 1012] 695- pos: [412, 1017]
691 room: Outside The Undeterred 696 room: Outside The Undeterred
692 door: Green Painting 697 door: Green Painting
693 paintings: 698 painting: maze_painting_3
694 - maze_painting_3 699 entrances:
695 tags:
696 - green_numbers 700 - green_numbers
697- pos: [134, 1007] 701- pos: [134, 1007]
698 sunwarp: 702 sunwarp:
@@ -701,7 +705,7 @@
701- pos: [719, 1039] 705- pos: [719, 1039]
702 room: Outside The Undeterred 706 room: Outside The Undeterred
703 door: Challenge Entrance 707 door: Challenge Entrance
704- pos: [483, 1039] 708- pos: [456, 1039]
705 room: Outside The Undeterred 709 room: Outside The Undeterred
706 door: Number Hunt 710 door: Number Hunt
707- pos: [563, 1071] 711- pos: [563, 1071]
@@ -722,12 +726,11 @@
722- pos: [525, 1002] 726- pos: [525, 1002]
723 room: Number Hunt 727 room: Number Hunt
724 door: Door to Directional Gallery 728 door: Door to Directional Gallery
725- pos: [659, 1014] 729- pos: [572, 1017]
726 room: Number Hunt 730 room: Number Hunt
727 door: Eights 731 door: Eights
728 paintings: 732 painting: smile_painting_5
729 - smile_painting_5 733 entrances:
730 tags:
731 - smiley_numbers 734 - smiley_numbers
732- pos: [557, 953] 735- pos: [557, 953]
733 room: Number Hunt 736 room: Number Hunt
@@ -744,87 +747,67 @@
744- pos: [268, 825] 747- pos: [268, 825]
745 room: Directional Gallery 748 room: Directional Gallery
746 door: Yellow Barrier 749 door: Yellow Barrier
747- pos: [231, 681] 750- pos: [214, 563]
748 room: Number Hunt 751 room: Number Hunt
749 door: Sixes 752 door: Sixes
750- pos: [242, 980] 753- pos: [242, 980]
751 room: Directional Gallery 754 room: Directional Gallery
752 door: Shortcut to The Undeterred 755 door: Shortcut to The Undeterred
753- pos: [351, 927] 756- pos: [364, 942]
754 paintings: 757 painting: boxes_painting
755 - boxes_painting 758 entrances:
756 tags:
757 - lattice_directional 759 - lattice_directional
758- pos: [272, 927] 760- pos: [278, 942]
759 paintings: 761 painting: smile_painting_7
760 - smile_painting_7 762 entrances:
761 tags:
762 - smiley_directional 763 - smiley_directional
763- pos: [214, 822] 764- pos: [278, 803]
764 paintings: 765 painting: cherry_painting
765 - cherry_painting 766 entrances:
766 tags:
767 - apple_directional 767 - apple_directional
768- pos: [266, 735] 768- pos: [262, 573]
769 room: Number Hunt 769 room: Number Hunt
770 door: Sixes 770 door: Sixes
771 paintings: 771 painting: pencil_painting3
772 - pencil_painting3 772 entrances:
773 tags:
774 - pencil_directional 773 - pencil_directional
775- pos: [215, 735] 774- pos: [278, 685]
776 paintings: 775 painting: flower_painting_4
777 - flower_painting_4
778- pos: [626, 851] 776- pos: [626, 851]
779 sunwarp: 777 sunwarp:
780 dots: 6 778 dots: 6
781 type: exit 779 type: exit
782- pos: [1141, 1441]
783 room: Orange Tower Third Floor
784 door: Orange Barrier
785- pos: [1174, 1441]
786 room: Outside The Initiated
787 door: Green Barrier
788- pos: [1205, 1441]
789 room: Outside The Initiated
790 door: Purple Barrier
791- pos: [1334, 1377] 780- pos: [1334, 1377]
792 room: Color Hunt 781 room: Color Hunt
793 door: Shortcut to The Steady 782 door: Shortcut to The Steady
794- pos: [1280, 1375] 783- pos: [1312, 1333]
795 paintings: 784 painting: arrows_painting_7
796 - arrows_painting_7 785- pos: [1226, 1333]
797- pos: [1233, 1321]
798 room: Outside The Initiated 786 room: Outside The Initiated
799 door: Entrance 787 door: Entrance
800 paintings: 788 painting: fruitbowl_painting3
801 - fruitbowl_painting3 789- pos: [1260, 1323]
802- pos: [1290, 1323]
803 sunwarp: 790 sunwarp:
804 dots: 5 791 dots: 5
805 type: exit 792 type: exit
806- pos: [1189, 1356] 793- pos: [1092, 1333]
807 room: Outside The Initiated 794 painting: colors_painting
808 door: Entrance
809- pos: [1154, 1332]
810 paintings:
811 - colors_painting
812- pos: [1640, 1260] 795- pos: [1640, 1260]
813 room: The Bearer 796 room: The Bearer
814 door: Backside Door 797 door: Backside Door
815- pos: [1468, 1287] 798- pos: [1468, 1287]
816 room: The Bearer 799 room: The Bearer
817 door: Entrance 800 door: Entrance
818- pos: [1430, 1232] 801- pos: [1431, 1233]
819 room: Number Hunt 802 room: Number Hunt
820 door: Sixes 803 door: Sixes
821- pos: [1388, 1152] 804- pos: [1388, 1152]
822 room: Bearer Side Area 805 room: Bearer Side Area
823 door: Shortcut to Tower 806 door: Shortcut to Tower
824- pos: [1273, 1442] 807- pos: [1264, 1430]
825 paintings: 808 painting: pencil_painting5
826 - pencil_painting5 809- pos: [1291, 1430]
827 - pencil_painting4 810 painting: pencil_painting4
828- pos: [1355, 1092] 811- pos: [1355, 1092]
829 room: Knight Night (Final) 812 room: Knight Night (Final)
830 door: Exit 813 door: Exit
@@ -835,10 +818,9 @@
835 # 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
836 room: Number Hunt 819 room: Number Hunt
837 door: Sevens 820 door: Sevens
838- pos: [1653, 101] 821- pos: [1687, 117]
839 paintings: 822 painting: smile_painting_9
840 - smile_painting_9 823 exits:
841 tags:
842 - smiley_crossroads 824 - smiley_crossroads
843 - smiley_deadend 825 - smiley_deadend
844 - smiley_hotcrusts 826 - smiley_hotcrusts
@@ -847,88 +829,113 @@
847 - smiley_initiated 829 - smiley_initiated
848 - smiley_gallery 830 - smiley_gallery
849 - smiley_theysee 831 - smiley_theysee
850- pos: [1656, 139] 832- pos: [1677, 161]
851 room: The Artistic (Smiley) 833 room: The Artistic (Smiley)
852 door: Door to Panda 834 door: Door to Panda
853- pos: [1711, 140] 835- pos: [1711, 140]
854 tags: 836 entrances:
855 - undeterred_artistic 837 - undeterred_artistic
856- pos: [1653, 169] 838- pos: [1687, 224]
857 paintings: 839 painting: panda_painting_3
858 - panda_painting_3 840 exits:
859 tags:
860 - panda_tower6 841 - panda_tower6
861 - panda_hallway 842 - panda_hallway
862- pos: [1708, 171] 843- pos: [1731, 215]
863 room: The Artistic (Panda) 844 room: The Artistic (Panda)
864 door: Door to Lattice 845 door: Door to Lattice
865- pos: [1761, 169] 846- pos: [1794, 224]
866 paintings: 847 painting: boxes_painting2
867 - boxes_painting2 848 exits:
868 tags:
869 - lattice_directional 849 - lattice_directional
870- pos: [1762, 139] 850- pos: [1785, 161]
871 room: The Artistic (Lattice) 851 room: The Artistic (Lattice)
872 door: Door to Apple 852 door: Door to Apple
873- pos: [1761, 101] 853- pos: [1794, 117]
874 paintings: 854 painting: cherry_painting3
875 - cherry_painting3 855 exits:
876 tags:
877 - apple_tower6 856 - apple_tower6
878 - apple_directional 857 - apple_directional
879- pos: [1708, 107] 858- pos: [1731, 107]
880 room: The Artistic (Apple) 859 room: The Artistic (Apple)
881 door: Door to Smiley 860 door: Door to Smiley
882- pos: [370, 681] 861- pos: [370, 563]
883 room: Number Hunt 862 room: Number Hunt
884 door: Eights 863 door: Eights
885- pos: [411, 685] 864- pos: [637, 605]
886 paintings: 865 painting: eye_painting
887 - eye_painting_2 866 entrances:
888 - smile_painting_2 867 - crossroads_eyewall
889 tags: 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
877 entrances:
890 - smiley_theysee 878 - smiley_theysee
891- pos: [310, 750] 879- pos: [311, 750]
892 room: The Eyes They See 880 room: The Eyes They See
893 door: Exit 881 door: Exit
894- pos: [334, 798] 882- pos: [348, 803]
895 paintings: 883 painting: arrows_painting_5
896 - arrows_painting_5 884- pos: [370, 751]
897- pos: [370, 792]
898 room: Outside The Wondrous 885 room: Outside The Wondrous
899 door: Wondrous Entrance 886 door: Wondrous Entrance
900- pos: [367, 752] 887- pos: [428, 696]
901 paintings: 888 painting: symmetry_painting_a_1
902 - symmetry_painting_a_1 889 exits:
903 - symmetry_painting_b_1
904 - symmetry_painting_a_3
905 - symmetry_painting_a_5
906 - symmetry_painting_b_4
907 - symmetry_painting_a_2
908 - symmetry_painting_b_2
909 - symmetry_painting_a_6
910 - symmetry_painting_b_6
911 tags:
912 - symmetry_starting 890 - symmetry_starting
913- 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]
914 room: The Wondrous 921 room: The Wondrous
915 door: Exit 922 door: Exit
916 paintings: 923- pos: [535, 647]
917 - arrows_painting_9 924 room: The Wondrous
918- pos: [449, 755] 925 door: Exit
919 paintings: 926 painting: arrows_painting_9
920 - flower_painting_6 927- pos: [610, 674]
921 tags: 928 painting: flower_painting_6
929 entrances:
922 - flower_arrow 930 - flower_arrow
923- pos: [1101, 222] 931- pos: [1156, 262]
924 paintings: 932 painting: panda_painting
925 - panda_painting 933 entrances:
926 tags:
927 - panda_hallway 934 - panda_hallway
928- pos: [1152, 209] 935- pos: [1152, 209]
929 room: Hallway Room (1) 936 room: Hallway Room (1)
930 door: Exit 937 door: Exit
931- pos: [1189, 170] 938- pos: [1190, 171]
932 room: Hallway Room (2) 939 room: Hallway Room (2)
933 door: Exit 940 door: Exit
934- pos: [1238, 124] 941- pos: [1238, 124]
@@ -943,16 +950,17 @@
943- pos: [1415, 140] 950- pos: [1415, 140]
944 room: Number Hunt 951 room: Number Hunt
945 door: Nines 952 door: Nines
946- pos: [1458, 133] 953- pos: [1424, 85]
947 paintings: 954 painting: south_afar
948 - south_afar
949- pos: [826, 1452] 955- pos: [826, 1452]
950 room: Outside The Wanderer 956 room: Outside The Wanderer
951 door: Wanderer Entrance 957 door: Wanderer Entrance
958 tilted: true
952- pos: [763, 1465] 959- pos: [763, 1465]
953 room: Outside The Wanderer 960 room: Outside The Wanderer
954 door: Tower Entrance 961 door: Tower Entrance
955- pos: [1655, 1151] 962 tilted: true
963- pos: [1656, 1152]
956 room: Number Hunt 964 room: Number Hunt
957 door: Eights 965 door: Eights
958- pos: [1623, 1044] 966- pos: [1623, 1044]
@@ -970,23 +978,26 @@
970- pos: [1511, 1119] 978- pos: [1511, 1119]
971 room: Art Gallery 979 room: Art Gallery
972 door: Exit 980 door: Exit
973- pos: [1654, 1116] 981- pos: [1730, 1162]
974 paintings: 982 painting: flower_painting_2
975 - smile_painting_3 983- pos: [1730, 1189]
976 - flower_painting_2 984 painting: map_painting
977 - scenery_painting_0a 985- pos: [1698, 1189]
978 - map_painting 986 painting: smile_painting_3
979 - fruitbowl_painting4 987 entrances:
980 tags:
981 - smiley_gallery 988 - smiley_gallery
989- pos: [1714, 1215]
990 painting: fruitbowl_painting4
991- pos: [1714, 1242]
992 painting: scenery_painting_0a
982- pos: [1120, 1286] 993- pos: [1120, 1286]
983 room: Rhyme Room (Smiley) 994 room: Rhyme Room (Smiley)
984 door: Door to Target 995 door: Door to Target
985- pos: [1120, 1315] 996- pos: [1120, 1315]
986 tags: 997 entrances: # this could be considered 2 way since the subway map has a one way gate at the exit anyway
987 - rhyme_smiley_target 998 - rhyme_smiley_target
988- pos: [792, 1137] 999- pos: [792, 1137]
989 tags: 1000 exits:
990 - rhyme_smiley_target 1001 - rhyme_smiley_target
991- pos: [895, 1217] 1002- pos: [895, 1217]
992 room: Number Hunt 1003 room: Number Hunt
@@ -997,9 +1008,8 @@
997- pos: [1120, 1195] 1008- pos: [1120, 1195]
998 room: Rhyme Room (Circle) 1009 room: Rhyme Room (Circle)
999 door: Door to Smiley 1010 door: Door to Smiley
1000- pos: [1118, 1137] 1011- pos: [1130, 1124]
1001 paintings: 1012 painting: arrows_painting_3
1002 - arrows_painting_3
1003- pos: [1050, 1142] 1013- pos: [1050, 1142]
1004 room: Rhyme Room (Looped Square) 1014 room: Rhyme Room (Looped Square)
1005 door: Door to Circle 1015 door: Door to Circle
@@ -1012,26 +1022,41 @@
1012- pos: [852, 1200] 1022- pos: [852, 1200]
1013 room: Rhyme Room (Target) 1023 room: Rhyme Room (Target)
1014 door: Door to Cross 1024 door: Door to Cross
1015- pos: [850, 1138] 1025- pos: [862, 1124]
1016 paintings: 1026 painting: arrows_painting_4
1017 - arrows_painting_4
1018- pos: [1592, 1442] 1027- pos: [1592, 1442]
1019 room: Room Room 1028 room: Room Room
1020 door: Cellar Exit 1029 door: Cellar Exit
1021- pos: [1570, 938] 1030- pos: [1623, 938]
1022 room: Outside The Wise 1031 room: Outside The Wise
1023 door: Wise Entrance 1032 door: Wise Entrance
1024- pos: [1653, 935] 1033- pos: [1665, 920]
1025 paintings: 1034 painting: clock_painting_3
1026 - clock_painting_3 1035- pos: [241, 348]
1027- pos: [369, 605]
1028 room: Outside The Scientific 1036 room: Outside The Scientific
1029 door: Scientific Entrance 1037 door: Scientific Entrance
1030- pos: [294, 602] 1038- pos: [176, 326]
1031 paintings: 1039 painting: hi_solved_painting4
1032 - hi_solved_painting4 1040 entrances:
1033 tags:
1034 - hi_scientific 1041 - hi_scientific
1035- pos: [814, 1001] 1042- pos: [815, 1002]
1036 room: Challenge Room 1043 room: Challenge Room
1037 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 f8d4ee0..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>
@@ -22,34 +23,59 @@
22#include <tuple> 23#include <tuple>
23 24
24#include "game_data.h" 25#include "game_data.h"
26#include "ipc_state.h"
25#include "logger.h" 27#include "logger.h"
26#include "tracker_frame.h" 28#include "tracker_frame.h"
27#include "tracker_state.h" 29#include "tracker_state.h"
28 30
29constexpr int AP_MAJOR = 0; 31constexpr int AP_MAJOR = 0;
30constexpr int AP_MINOR = 4; 32constexpr int AP_MINOR = 6;
31constexpr int AP_REVISION = 5; 33constexpr int AP_REVISION = 1;
32 34
33constexpr const char* CERT_STORE_PATH = "cacert.pem"; 35constexpr const char* CERT_STORE_PATH = "cacert.pem";
34constexpr int ITEM_HANDLING = 7; // <- all 36constexpr int ITEM_HANDLING = 7; // <- all
35 37
38constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds
39constexpr int CONNECTION_BACKOFF_INTERVAL = 100;
40
41constexpr int PANEL_COUNT = 803;
42constexpr int PANEL_BITFIELD_LENGTH = 48;
43constexpr int PANEL_BITFIELDS = 17;
44
36namespace { 45namespace {
37 46
38struct APState { 47const std::set<long> kNonProgressionItems = {
39 std::unique_ptr<APClient> apclient; 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};
40 58
59struct APState {
60 // Initialized on main thread
41 bool initialized = false; 61 bool initialized = false;
42
43 TrackerFrame* tracker_frame = nullptr; 62 TrackerFrame* tracker_frame = nullptr;
63 std::list<std::string> tracked_data_storage_keys;
44 64
45 bool client_active = false; 65 // Client
46 std::mutex client_mutex; 66 std::mutex client_mutex;
67 std::unique_ptr<APClient> apclient;
68
69 // Protected state
70 std::mutex state_mutex;
71
72 std::string status_message = "Not connected to Archipelago.";
47 73
48 bool connected = false; 74 bool connected = false;
49 bool has_connection_result = false; 75 std::string connection_failure;
76 int remaining_loops = 0;
50 77
51 std::string data_storage_prefix; 78 std::string data_storage_prefix;
52 std::list<std::string> tracked_data_storage_keys;
53 std::string victory_data_storage_key; 79 std::string victory_data_storage_key;
54 80
55 std::string save_name; 81 std::string save_name;
@@ -58,9 +84,12 @@ struct APState {
58 std::set<int64_t> checked_locations; 84 std::set<int64_t> checked_locations;
59 std::map<std::string, std::any> data_storage; 85 std::map<std::string, std::any> data_storage;
60 std::optional<std::tuple<int, int>> player_pos; 86 std::optional<std::tuple<int, int>> player_pos;
87 std::bitset<PANEL_COUNT> solved_panels;
61 88
62 DoorShuffleMode door_shuffle_mode = kNO_DOORS; 89 DoorShuffleMode door_shuffle_mode = kNO_DOORS;
90 bool group_doors = false;
63 bool color_shuffle = false; 91 bool color_shuffle = false;
92 PanelShuffleMode panel_shuffle_mode = kNO_PANELS;
64 bool painting_shuffle = false; 93 bool painting_shuffle = false;
65 int mastery_requirement = 21; 94 int mastery_requirement = 21;
66 int level_2_requirement = 223; 95 int level_2_requirement = 223;
@@ -72,171 +101,387 @@ struct APState {
72 bool pilgrimage_allows_paintings = false; 101 bool pilgrimage_allows_paintings = false;
73 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; 102 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
74 bool sunwarp_shuffle = false; 103 bool sunwarp_shuffle = false;
104 bool postgame_shuffle = true;
75 105
76 std::map<std::string, std::string> painting_mapping; 106 std::map<std::string, std::string> painting_mapping;
77 std::set<std::string> painting_codomain; 107 std::set<std::string> painting_codomain;
78 std::map<int, SunwarpMapping> sunwarp_mapping; 108 std::map<int, SunwarpMapping> sunwarp_mapping;
79 109
80 void Connect(std::string server, std::string player, std::string password) { 110 void Connect(std::string server, std::string player, std::string password) {
111 Initialize();
112
113 {
114 std::lock_guard state_guard(state_mutex);
115 SetStatusMessage("Connecting to Archipelago server....");
116 }
117 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
118
119 // Creating and setting up the client has to all be done while holding the
120 // client mutex, so that the other thread doesn't try to poll before we add
121 // handlers, etc.
122 {
123 TrackerLog("Destroying old AP client...");
124
125 std::lock_guard client_guard(client_mutex);
126
127 if (apclient) {
128 DestroyClient();
129 }
130
131 std::string cert_store = "";
132 if (std::filesystem::exists(CERT_STORE_PATH)) {
133 cert_store = CERT_STORE_PATH;
134 }
135
136 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server,
137 cert_store);
138
139 {
140 std::lock_guard state_guard(state_mutex);
141
142 connected = false;
143 connection_failure.clear();
144 remaining_loops = CONNECTION_TIMEOUT / CONNECTION_BACKOFF_INTERVAL;
145
146 save_name.clear();
147 inventory.clear();
148 checked_locations.clear();
149 data_storage.clear();
150 player_pos = std::nullopt;
151 solved_panels.reset();
152 victory_data_storage_key.clear();
153 door_shuffle_mode = kNO_DOORS;
154 group_doors = false;
155 color_shuffle = false;
156 panel_shuffle_mode = kNO_PANELS;
157 painting_shuffle = false;
158 painting_mapping.clear();
159 painting_codomain.clear();
160 mastery_requirement = 21;
161 level_2_requirement = 223;
162 location_checks = kNORMAL_LOCATIONS;
163 victory_condition = kTHE_END;
164 early_color_hallways = false;
165 pilgrimage_enabled = false;
166 pilgrimage_allows_roof_access = false;
167 pilgrimage_allows_paintings = false;
168 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
169 sunwarp_shuffle = false;
170 sunwarp_mapping.clear();
171 postgame_shuffle = true;
172 }
173
174 apclient->set_room_info_handler(
175 [this, player, password]() { OnRoomInfo(player, password); });
176
177 apclient->set_location_checked_handler(
178 [this](const std::list<int64_t>& locations) {
179 OnLocationChecked(locations);
180 });
181
182 apclient->set_slot_disconnected_handler(
183 [this]() { OnSlotDisconnected(); });
184
185 apclient->set_socket_disconnected_handler(
186 [this]() { OnSocketDisconnected(); });
187
188 apclient->set_items_received_handler(
189 [this](const std::list<APClient::NetworkItem>& items) {
190 OnItemsReceived(items);
191 });
192
193 apclient->set_retrieved_handler(
194 [this](const std::map<std::string, nlohmann::json>& data) {
195 OnRetrieved(data);
196 });
197
198 apclient->set_set_reply_handler(
199 [this](const std::string& key, const nlohmann::json& value,
200 const nlohmann::json&) { OnSetReply(key, value); });
201
202 apclient->set_slot_connected_handler(
203 [this, player, server](const nlohmann::json& slot_data) {
204 OnSlotConnected(player, server, slot_data);
205 });
206
207 apclient->set_slot_refused_handler(
208 [this](const std::list<std::string>& errors) {
209 OnSlotRefused(errors);
210 });
211 }
212 }
213
214 std::string GetStatusMessage() {
215 std::lock_guard state_guard(state_mutex);
216
217 return status_message;
218 }
219
220 bool HasCheckedGameLocation(int location_id) {
221 std::lock_guard state_guard(state_mutex);
222
223 return checked_locations.count(location_id);
224 }
225
226 bool HasItem(int item_id, int quantity) {
227 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
228 }
229
230 bool HasItemSafe(int item_id, int quantity) {
231 std::lock_guard state_guard(state_mutex);
232 return HasItem(item_id, quantity);
233 }
234
235 const std::set<std::string>& GetCheckedPaintings() {
236 std::lock_guard state_guard(state_mutex);
237
238 std::string key = fmt::format("{}Paintings", data_storage_prefix);
239 if (!data_storage.count(key)) {
240 data_storage[key] = std::set<std::string>();
241 }
242
243 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
244 }
245
246 bool IsPaintingChecked(const std::string& painting_id) {
247 const auto& checked_paintings = GetCheckedPaintings();
248
249 std::lock_guard state_guard(state_mutex);
250
251 return checked_paintings.count(painting_id) ||
252 (painting_mapping.count(painting_id) &&
253 checked_paintings.count(painting_mapping.at(painting_id)));
254 }
255
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 }
271
272 bool HasReachedGoal() {
273 std::lock_guard state_guard(state_mutex);
274
275 return data_storage.count(victory_data_storage_key) &&
276 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
277 30; // CLIENT_GOAL
278 }
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
286 private:
287 void Initialize() {
81 if (!initialized) { 288 if (!initialized) {
82 TrackerLog("Initializing APState..."); 289 TrackerLog("Initializing APState...");
83 290
84 std::thread([this]() { 291 std::thread([this]() { Thread(); }).detach();
85 for (;;) { 292
293 for (int i = 0; i < PANEL_BITFIELDS; i++) {
294 tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i));
295 }
296
297 tracked_data_storage_keys.push_back("PlayerPos");
298 tracked_data_storage_keys.push_back("Paintings");
299
300 initialized = true;
301 }
302 }
303
304 void Thread() {
305 std::string display_error;
306
307 for (;;) {
308 {
309 std::lock_guard client_guard(client_mutex);
310 if (apclient) {
311 apclient->poll();
312
86 { 313 {
87 std::lock_guard client_guard(client_mutex); 314 std::lock_guard state_guard(state_mutex);
88 if (apclient) { 315
89 apclient->poll(); 316 if (!connected) {
317 if (!connection_failure.empty()) {
318 TrackerLog(connection_failure);
319
320 display_error = connection_failure;
321 connection_failure.clear();
322
323 DestroyClient();
324 } else {
325 remaining_loops--;
326
327 if (remaining_loops <= 0) {
328 DestroyClient();
329
330 SetStatusMessage("Disconnected from Archipelago.");
331 TrackerLog("Timeout while connecting to Archipelago server.");
332
333 display_error =
334 "Timeout while connecting to Archipelago server.";
335 }
336 }
90 } 337 }
91 } 338 }
92
93 std::this_thread::sleep_for(std::chrono::milliseconds(100));
94 } 339 }
95 }).detach();
96
97 for (int panel_id : GD_GetAchievementPanels()) {
98 tracked_data_storage_keys.push_back(fmt::format(
99 "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
100 } 340 }
101 341
102 for (const MapArea& map_area : GD_GetMapAreas()) { 342 if (!display_error.empty()) {
103 for (const Location& location : map_area.locations) { 343 wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR);
104 tracked_data_storage_keys.push_back( 344 display_error.clear();
105 fmt::format("Hunt|{}", location.ap_location_id));
106 }
107 } 345 }
108 346
109 tracked_data_storage_keys.push_back("PlayerPos"); 347 std::this_thread::sleep_for(std::chrono::milliseconds(100));
110 tracked_data_storage_keys.push_back("Paintings"); 348 }
349 }
111 350
112 initialized = true; 351 void OnRoomInfo(std::string player, std::string password) {
352 {
353 std::lock_guard state_guard(state_mutex);
354
355 inventory.clear();
356
357 SetStatusMessage("Connected to Archipelago server. Authenticating...");
113 } 358 }
114 359
115 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 360 TrackerLog(fmt::format(
116 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server)); 361 "Connected to Archipelago server. Authenticating as {} {}", player,
362 (password.empty() ? "without password" : "with password " + password)));
363
364 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
365 {AP_MAJOR, AP_MINOR, AP_REVISION});
366 }
117 367
368 void OnLocationChecked(const std::list<int64_t>& locations) {
118 { 369 {
119 TrackerLog("Destroying old AP client..."); 370 std::lock_guard state_guard(state_mutex);
120 371
121 std::lock_guard client_guard(client_mutex); 372 for (const int64_t location_id : locations) {
373 checked_locations.insert(location_id);
374 TrackerLog(fmt::format("Location: {}", location_id));
375 }
376 }
122 377
123 if (apclient) { 378 RefreshTracker(StateUpdate{.cleared_locations = true});
124 DestroyClient(); 379 }
380
381 void OnSlotDisconnected() {
382 std::lock_guard state_guard(state_mutex);
383
384 SetStatusMessage(
385 "Disconnected from Archipelago. Attempting to reconnect...");
386 TrackerLog(
387 "Slot disconnected from Archipelago. Attempting to reconnect...");
388 }
389
390 void OnSocketDisconnected() {
391 std::lock_guard state_guard(state_mutex);
392
393 SetStatusMessage(
394 "Disconnected from Archipelago. Attempting to reconnect...");
395 TrackerLog(
396 "Socket disconnected from Archipelago. Attempting to reconnect...");
397 }
398
399 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) {
400 std::vector<ItemState> item_states;
401 bool progression_items = false;
402
403 {
404 std::lock_guard state_guard(state_mutex);
405
406 std::map<int64_t, int> index_by_item;
407
408 for (const APClient::NetworkItem& item : items) {
409 inventory[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 }
125 } 417 }
126 418
127 std::string cert_store = ""; 419 for (const auto& [item_id, item_index] : index_by_item) {
128 if (std::filesystem::exists(CERT_STORE_PATH)) { 420 item_states.push_back(ItemState{.name = GD_GetItemName(item_id),
129 cert_store = CERT_STORE_PATH; 421 .amount = inventory[item_id],
422 .index = item_index});
130 } 423 }
424 }
131 425
132 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, 426 RefreshTracker(StateUpdate{.items = item_states,
133 cert_store); 427 .progression_items = progression_items});
428 }
429
430 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) {
431 StateUpdate state_update;
432
433 {
434 std::lock_guard state_guard(state_mutex);
435
436 for (const auto& [key, value] : data) {
437 HandleDataStorage(key, value, state_update);
438 }
134 } 439 }
135 440
136 save_name.clear(); 441 RefreshTracker(state_update);
137 inventory.clear(); 442 }
138 checked_locations.clear();
139 data_storage.clear();
140 player_pos = std::nullopt;
141 victory_data_storage_key.clear();
142 door_shuffle_mode = kNO_DOORS;
143 color_shuffle = false;
144 painting_shuffle = false;
145 painting_mapping.clear();
146 painting_codomain.clear();
147 mastery_requirement = 21;
148 level_2_requirement = 223;
149 location_checks = kNORMAL_LOCATIONS;
150 victory_condition = kTHE_END;
151 early_color_hallways = false;
152 pilgrimage_enabled = false;
153 pilgrimage_allows_roof_access = false;
154 pilgrimage_allows_paintings = false;
155 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
156 sunwarp_shuffle = false;
157 sunwarp_mapping.clear();
158
159 std::mutex connection_mutex;
160 connected = false;
161 has_connection_result = false;
162
163 apclient->set_room_info_handler([this, player, password]() {
164 inventory.clear();
165 443
166 TrackerLog(fmt::format( 444 void OnSetReply(const std::string& key, const nlohmann::json& value) {
167 "Connected to Archipelago server. Authenticating as {} {}", player, 445 StateUpdate state_update;
168 (password.empty() ? "without password"
169 : "with password " + password)));
170 tracker_frame->SetStatusMessage(
171 "Connected to Archipelago server. Authenticating...");
172
173 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
174 {AP_MAJOR, AP_MINOR, AP_REVISION});
175 });
176
177 apclient->set_location_checked_handler(
178 [this](const std::list<int64_t>& locations) {
179 for (const int64_t location_id : locations) {
180 checked_locations.insert(location_id);
181 TrackerLog(fmt::format("Location: {}", location_id));
182 }
183 446
184 RefreshTracker(false); 447 {
185 }); 448 std::lock_guard state_guard(state_mutex);
186 449 HandleDataStorage(key, value, state_update);
187 apclient->set_slot_disconnected_handler([this]() { 450 }
188 tracker_frame->SetStatusMessage(
189 "Disconnected from Archipelago. Attempting to reconnect...");
190 TrackerLog(
191 "Slot disconnected from Archipelago. Attempting to reconnect...");
192 });
193
194 apclient->set_socket_disconnected_handler([this]() {
195 tracker_frame->SetStatusMessage(
196 "Disconnected from Archipelago. Attempting to reconnect...");
197 TrackerLog(
198 "Socket disconnected from Archipelago. Attempting to reconnect...");
199 });
200
201 apclient->set_items_received_handler(
202 [this](const std::list<APClient::NetworkItem>& items) {
203 for (const APClient::NetworkItem& item : items) {
204 inventory[item.item]++;
205 TrackerLog(fmt::format("Item: {}", item.item));
206 }
207 451
208 RefreshTracker(false); 452 RefreshTracker(state_update);
209 }); 453 }
210 454
211 apclient->set_retrieved_handler( 455 void OnSlotConnected(std::string player, std::string server,
212 [this](const std::map<std::string, nlohmann::json>& data) { 456 const nlohmann::json& slot_data) {
213 for (const auto& [key, value] : data) { 457 IPC_SetTrackerSlot(server, player);
214 HandleDataStorage(key, value);
215 }
216 458
217 RefreshTracker(false); 459 TrackerLog("Connected to Archipelago!");
218 });
219 460
220 apclient->set_set_reply_handler([this](const std::string& key, 461 {
221 const nlohmann::json& value, 462 std::lock_guard state_guard(state_mutex);
222 const nlohmann::json&) {
223 HandleDataStorage(key, value);
224 RefreshTracker(false);
225 });
226 463
227 apclient->set_slot_connected_handler([this, player, server, 464 SetStatusMessage(
228 &connection_mutex]( 465 fmt::format("Connected to Archipelago! ({}@{}).", player, server));
229 const nlohmann::json& slot_data) {
230 tracker_frame->SetStatusMessage(
231 fmt::format("Connected to Archipelago! ({}@{})", player, server));
232 TrackerLog("Connected to Archipelago!");
233 466
234 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), 467 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(),
235 apclient->get_player_number()); 468 apclient->get_player_number());
236 data_storage_prefix = 469 data_storage_prefix =
237 fmt::format("Lingo_{}_", apclient->get_player_number()); 470 fmt::format("Lingo_{}_", apclient->get_player_number());
238 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); 471 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
472 if (slot_data.contains("group_doors")) {
473 group_doors = slot_data.contains("group_doors") &&
474 slot_data["group_doors"].get<int>() == 1;
475 } else {
476 // If group_doors doesn't exist yet, that means kPANELS_MODE is
477 // actually kSIMPLE_DOORS.
478 if (door_shuffle_mode == kPANELS_MODE) {
479 door_shuffle_mode = kDOORS_MODE;
480 group_doors = true;
481 }
482 }
239 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>();
240 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 485 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
241 mastery_requirement = slot_data["mastery_achievements"].get<int>(); 486 mastery_requirement = slot_data["mastery_achievements"].get<int>();
242 level_2_requirement = slot_data["level_2_requirement"].get<int>(); 487 level_2_requirement = slot_data["level_2_requirement"].get<int>();
@@ -258,6 +503,9 @@ struct APState {
258 : kSUNWARP_ACCESS_NORMAL; 503 : kSUNWARP_ACCESS_NORMAL;
259 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && 504 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
260 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;
261 509
262 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { 510 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
263 painting_mapping.clear(); 511 painting_mapping.clear();
@@ -295,111 +543,87 @@ struct APState {
295 apclient->Get(corrected_keys); 543 apclient->Get(corrected_keys);
296 apclient->SetNotify(corrected_keys); 544 apclient->SetNotify(corrected_keys);
297 545
298 ResetReachabilityRequirements(); 546 connected = true;
299 RefreshTracker(true); 547 }
300
301 {
302 std::lock_guard connection_lock(connection_mutex);
303 if (!has_connection_result) {
304 connected = true;
305 has_connection_result = true;
306 }
307 }
308 });
309
310 apclient->set_slot_refused_handler(
311 [this, &connection_mutex](const std::list<std::string>& errors) {
312 {
313 std::lock_guard connection_lock(connection_mutex);
314 connected = false;
315 has_connection_result = true;
316 }
317
318 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
319
320 std::vector<std::string> error_messages;
321 error_messages.push_back("Could not connect to Archipelago.");
322
323 for (const std::string& error : errors) {
324 if (error == "InvalidSlot") {
325 error_messages.push_back("Invalid player name.");
326 } else if (error == "InvalidGame") {
327 error_messages.push_back(
328 "The specified player is not playing Lingo.");
329 } else if (error == "IncompatibleVersion") {
330 error_messages.push_back(
331 "The Archipelago server is not the correct version for this "
332 "client.");
333 } else if (error == "InvalidPassword") {
334 error_messages.push_back("Incorrect password.");
335 } else if (error == "InvalidItemsHandling") {
336 error_messages.push_back(
337 "Invalid item handling flag. This is a bug with the tracker. "
338 "Please report it to the lingo-ap-tracker GitHub.");
339 } else {
340 error_messages.push_back("Unknown error.");
341 }
342 }
343
344 std::string full_message = hatkirby::implode(error_messages, " ");
345 TrackerLog(full_message);
346
347 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
348 });
349
350 client_active = true;
351
352 int timeout = 5000; // 5 seconds
353 int interval = 100;
354 int remaining_loops = timeout / interval;
355 while (true) {
356 {
357 std::lock_guard connection_lock(connection_mutex);
358 if (has_connection_result) {
359 break;
360 }
361 }
362
363 if (interval == 0) {
364 DestroyClient();
365
366 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
367 TrackerLog("Timeout while connecting to Archipelago server.");
368 wxMessageBox("Timeout while connecting to Archipelago server.",
369 "Connection failed", wxOK | wxICON_ERROR);
370 548
371 { 549 ResetReachabilityRequirements();
372 std::lock_guard connection_lock(connection_mutex); 550 RefreshTracker(std::nullopt);
373 connected = false; 551 }
374 has_connection_result = true;
375 }
376 552
377 break; 553 void OnSlotRefused(const std::list<std::string>& errors) {
554 std::vector<std::string> error_messages;
555 error_messages.push_back("Could not connect to Archipelago.");
556
557 for (const std::string& error : errors) {
558 if (error == "InvalidSlot") {
559 error_messages.push_back("Invalid player name.");
560 } else if (error == "InvalidGame") {
561 error_messages.push_back("The specified player is not playing Lingo.");
562 } else if (error == "IncompatibleVersion") {
563 error_messages.push_back(
564 "The Archipelago server is not the correct version for this "
565 "client.");
566 } else if (error == "InvalidPassword") {
567 error_messages.push_back("Incorrect password.");
568 } else if (error == "InvalidItemsHandling") {
569 error_messages.push_back(
570 "Invalid item handling flag. This is a bug with the tracker. "
571 "Please report it to the lingo-ap-tracker GitHub.");
572 } else {
573 error_messages.push_back("Unknown error.");
378 } 574 }
575 }
379 576
380 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 577 {
578 std::lock_guard state_guard(state_mutex);
579 connection_failure = hatkirby::implode(error_messages, " ");
381 580
382 interval--; 581 SetStatusMessage("Disconnected from Archipelago.");
383 } 582 }
583 }
384 584
385 if (connected) { 585 // Assumes state mutex is locked.
386 client_active = false; 586 void SetStatusMessage(std::string msg) {
387 } 587 status_message = std::move(msg);
588
589 tracker_frame->UpdateStatusMessage();
388 } 590 }
389 591
390 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 592 // Assumes state mutex is locked.
593 void HandleDataStorage(const std::string& key, const nlohmann::json& value, StateUpdate& state_update) {
391 if (value.is_boolean()) { 594 if (value.is_boolean()) {
392 data_storage[key] = value.get<bool>(); 595 data_storage[key] = value.get<bool>();
393 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 596 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
394 (value.get<bool>() ? "true" : "false"))); 597 (value.get<bool>() ? "true" : "false")));
598
395 } else if (value.is_number()) { 599 } else if (value.is_number()) {
396 data_storage[key] = value.get<int>(); 600 data_storage[key] = value.get<int>();
397 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 601 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
398 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 }
399 } else if (value.is_object()) { 622 } else if (value.is_object()) {
400 if (key.ends_with("PlayerPos")) { 623 if (key.ends_with("PlayerPos")) {
401 auto map_value = value.get<std::map<std::string, int>>(); 624 auto map_value = value.get<std::map<std::string, int>>();
402 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;
403 } else { 627 } else {
404 data_storage[key] = value.get<std::map<std::string, int>>(); 628 data_storage[key] = value.get<std::map<std::string, int>>();
405 } 629 }
@@ -408,6 +632,7 @@ struct APState {
408 } else if (value.is_null()) { 632 } else if (value.is_null()) {
409 if (key.ends_with("PlayerPos")) { 633 if (key.ends_with("PlayerPos")) {
410 player_pos = std::nullopt; 634 player_pos = std::nullopt;
635 state_update.player_position = true;
411 } else { 636 } else {
412 data_storage.erase(key); 637 data_storage.erase(key);
413 } 638 }
@@ -419,6 +644,8 @@ struct APState {
419 if (key.ends_with("Paintings")) { 644 if (key.ends_with("Paintings")) {
420 data_storage[key] = 645 data_storage[key] =
421 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());
422 } else { 649 } else {
423 data_storage[key] = list_value; 650 data_storage[key] = list_value;
424 } 651 }
@@ -428,74 +655,39 @@ struct APState {
428 } 655 }
429 } 656 }
430 657
431 bool HasCheckedGameLocation(int location_id) { 658 // State mutex should NOT be locked.
432 return checked_locations.count(location_id); 659 // nullopt state_update indicates a reset.
433 } 660 void RefreshTracker(std::optional<StateUpdate> state_update) {
434 661 TrackerLog("Refreshing display...");
435 bool HasCheckedHuntPanel(int location_id) {
436 std::string key =
437 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
438 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
439 }
440
441 bool HasItem(int item_id, int quantity) {
442 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
443 }
444
445 bool HasAchievement(const std::string& name) {
446 std::string key =
447 fmt::format("{}Achievement|{}", data_storage_prefix, name);
448 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
449 }
450
451 const std::set<std::string>& GetCheckedPaintings() {
452 std::string key = fmt::format("{}Paintings", data_storage_prefix);
453 if (!data_storage.count(key)) {
454 data_storage[key] = std::set<std::string>();
455 }
456 662
457 return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); 663 if (!state_update || state_update->progression_items ||
458 } 664 !state_update->paintings.empty()) {
665 std::string prev_msg;
666 {
667 std::lock_guard state_guard(state_mutex);
459 668
460 bool IsPaintingChecked(const std::string& painting_id) { 669 prev_msg = status_message;
461 const auto& checked_paintings = GetCheckedPaintings(); 670 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
671 }
462 672
463 return checked_paintings.count(painting_id) || 673 RecalculateReachability();
464 (painting_mapping.count(painting_id) &&
465 checked_paintings.count(painting_mapping.at(painting_id)));
466 }
467 674
468 void RefreshTracker(bool reset) { 675 {
469 TrackerLog("Refreshing display..."); 676 std::lock_guard state_guard(state_mutex);
470 677
471 RecalculateReachability(); 678 SetStatusMessage(prev_msg);
679 }
680 }
681
472 682
473 if (reset) { 683 if (!state_update) {
474 tracker_frame->ResetIndicators(); 684 tracker_frame->ResetIndicators();
475 } else { 685 } else {
476 tracker_frame->UpdateIndicators(); 686 tracker_frame->UpdateIndicators(*state_update);
477 }
478 }
479
480 int64_t GetItemId(const std::string& item_name) {
481 int64_t ap_id = apclient->get_item_id(item_name);
482 if (ap_id == APClient::INVALID_NAME_ID) {
483 TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
484 } 687 }
485
486 return ap_id;
487 }
488
489 std::string GetItemName(int id) { return apclient->get_item_name(id); }
490
491 bool HasReachedGoal() {
492 return data_storage.count(victory_data_storage_key) &&
493 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
494 30; // CLIENT_GOAL
495 } 688 }
496 689
497 void DestroyClient() { 690 void DestroyClient() {
498 client_active = false;
499 apclient->reset(); 691 apclient->reset();
500 apclient.reset(); 692 apclient.reset();
501 } 693 }
@@ -514,39 +706,63 @@ void AP_Connect(std::string server, std::string player, std::string password) {
514 GetState().Connect(server, player, password); 706 GetState().Connect(server, player, password);
515} 707}
516 708
517std::string AP_GetSaveName() { return GetState().save_name; } 709std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); }
518 710
519bool AP_HasCheckedGameLocation(int location_id) { 711std::string AP_GetSaveName() {
520 return GetState().HasCheckedGameLocation(location_id); 712 std::lock_guard state_guard(GetState().state_mutex);
713
714 return GetState().save_name;
521} 715}
522 716
523bool AP_HasCheckedHuntPanel(int location_id) { 717bool AP_HasCheckedGameLocation(int location_id) {
524 return GetState().HasCheckedHuntPanel(location_id); 718 return GetState().HasCheckedGameLocation(location_id);
525} 719}
526 720
527bool AP_HasItem(int item_id, int quantity) { 721bool AP_HasItem(int item_id, int quantity) {
528 return GetState().HasItem(item_id, quantity); 722 return GetState().HasItem(item_id, quantity);
529} 723}
530 724
531std::string AP_GetItemName(int item_id) { 725bool AP_HasItemSafe(int item_id, int quantity) {
532 return GetState().GetItemName(item_id); 726 return GetState().HasItemSafe(item_id, quantity);
727}
728
729DoorShuffleMode AP_GetDoorShuffleMode() {
730 std::lock_guard state_guard(GetState().state_mutex);
731
732 return GetState().door_shuffle_mode;
733}
734
735bool AP_AreDoorsGrouped() {
736 std::lock_guard state_guard(GetState().state_mutex);
737
738 return GetState().group_doors;
533} 739}
534 740
535DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 741bool AP_IsColorShuffle() {
742 std::lock_guard state_guard(GetState().state_mutex);
536 743
537bool AP_IsColorShuffle() { return GetState().color_shuffle; } 744 return GetState().color_shuffle;
745}
538 746
539bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 747bool AP_IsPaintingShuffle() {
748 std::lock_guard state_guard(GetState().state_mutex);
749
750 return GetState().painting_shuffle;
751}
752
753std::map<std::string, std::string> AP_GetPaintingMapping() {
754 std::lock_guard state_guard(GetState().state_mutex);
540 755
541const std::map<std::string, std::string>& AP_GetPaintingMapping() {
542 return GetState().painting_mapping; 756 return GetState().painting_mapping;
543} 757}
544 758
545bool AP_IsPaintingMappedTo(const std::string& painting_id) { 759bool AP_IsPaintingMappedTo(const std::string& painting_id) {
760 std::lock_guard state_guard(GetState().state_mutex);
761
546 return GetState().painting_codomain.count(painting_id); 762 return GetState().painting_codomain.count(painting_id);
547} 763}
548 764
549const std::set<std::string>& AP_GetCheckedPaintings() { 765std::set<std::string> AP_GetCheckedPaintings() {
550 return GetState().GetCheckedPaintings(); 766 return GetState().GetCheckedPaintings();
551} 767}
552 768
@@ -554,11 +770,29 @@ bool AP_IsPaintingChecked(const std::string& painting_id) {
554 return GetState().IsPaintingChecked(painting_id); 770 return GetState().IsPaintingChecked(painting_id);
555} 771}
556 772
557int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 773void AP_RevealPaintings() { GetState().RevealPaintings(); }
774
775int AP_GetMasteryRequirement() {
776 std::lock_guard state_guard(GetState().state_mutex);
777
778 return GetState().mastery_requirement;
779}
558 780
559int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 781int AP_GetLevel2Requirement() {
782 std::lock_guard state_guard(GetState().state_mutex);
783
784 return GetState().level_2_requirement;
785}
786
787LocationChecks AP_GetLocationsChecks() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
790 return GetState().location_checks;
791}
560 792
561bool AP_IsLocationVisible(int classification) { 793bool AP_IsLocationVisible(int classification) {
794 std::lock_guard state_guard(GetState().state_mutex);
795
562 int world_state = 0; 796 int world_state = 0;
563 797
564 switch (GetState().location_checks) { 798 switch (GetState().location_checks) {
@@ -575,43 +809,76 @@ bool AP_IsLocationVisible(int classification) {
575 return false; 809 return false;
576 } 810 }
577 811
578 if (GetState().door_shuffle_mode && !GetState().early_color_hallways) { 812 if (GetState().door_shuffle_mode == kDOORS_MODE &&
813 !GetState().early_color_hallways) {
579 world_state |= kLOCATION_SMALL_SPHERE_ONE; 814 world_state |= kLOCATION_SMALL_SPHERE_ONE;
580 } 815 }
581 816
582 return (world_state & classification); 817 return (world_state & classification);
583} 818}
584 819
820PanelShuffleMode AP_GetPanelShuffleMode() {
821 std::lock_guard state_guard(GetState().state_mutex);
822
823 return GetState().panel_shuffle_mode;
824}
825
585VictoryCondition AP_GetVictoryCondition() { 826VictoryCondition AP_GetVictoryCondition() {
827 std::lock_guard state_guard(GetState().state_mutex);
828
586 return GetState().victory_condition; 829 return GetState().victory_condition;
587} 830}
588 831
589bool AP_HasAchievement(const std::string& achievement_name) { 832bool AP_HasEarlyColorHallways() {
590 return GetState().HasAchievement(achievement_name); 833 std::lock_guard state_guard(GetState().state_mutex);
834
835 return GetState().early_color_hallways;
591} 836}
592 837
593bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } 838bool AP_IsPilgrimageEnabled() {
839 std::lock_guard state_guard(GetState().state_mutex);
594 840
595bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } 841 return GetState().pilgrimage_enabled;
842}
596 843
597bool AP_DoesPilgrimageAllowRoofAccess() { 844bool AP_DoesPilgrimageAllowRoofAccess() {
845 std::lock_guard state_guard(GetState().state_mutex);
846
598 return GetState().pilgrimage_allows_roof_access; 847 return GetState().pilgrimage_allows_roof_access;
599} 848}
600 849
601bool AP_DoesPilgrimageAllowPaintings() { 850bool AP_DoesPilgrimageAllowPaintings() {
851 std::lock_guard state_guard(GetState().state_mutex);
852
602 return GetState().pilgrimage_allows_paintings; 853 return GetState().pilgrimage_allows_paintings;
603} 854}
604 855
605SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } 856SunwarpAccess AP_GetSunwarpAccess() {
857 std::lock_guard state_guard(GetState().state_mutex);
858
859 return GetState().sunwarp_access;
860}
606 861
607bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } 862bool AP_IsSunwarpShuffle() {
863 std::lock_guard state_guard(GetState().state_mutex);
864
865 return GetState().sunwarp_shuffle;
866}
608 867
609const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { 868std::map<int, SunwarpMapping> AP_GetSunwarpMapping() {
610 return GetState().sunwarp_mapping; 869 return GetState().sunwarp_mapping;
611} 870}
612 871
872bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; }
873
613bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 874bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
614 875
615std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { 876std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
877 std::lock_guard state_guard(GetState().state_mutex);
878
616 return GetState().player_pos; 879 return GetState().player_pos;
617} 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 f8936e5..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -11,7 +11,7 @@
11 11
12class TrackerFrame; 12class TrackerFrame;
13 13
14enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 }; 14enum DoorShuffleMode { kNO_DOORS = 0, kPANELS_MODE = 1, kDOORS_MODE = 2 };
15 15
16enum VictoryCondition { 16enum VictoryCondition {
17 kTHE_END = 0, 17 kTHE_END = 0,
@@ -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,43 +41,57 @@ 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);
45 53
54std::string AP_GetStatusMessage();
55
46std::string AP_GetSaveName(); 56std::string AP_GetSaveName();
47 57
48bool AP_HasCheckedGameLocation(int location_id); 58bool AP_HasCheckedGameLocation(int location_id);
49 59
50bool AP_HasCheckedHuntPanel(int location_id); 60// This doesn't lock the state mutex, for speed, so it must ONLY be called from
51 61// RecalculateReachability, which is only called from the APState thread anyway.
52bool AP_HasItem(int item_id, int quantity = 1); 62bool AP_HasItem(int item_id, int quantity = 1);
53 63
54std::string AP_GetItemName(int item_id); 64bool AP_HasItemSafe(int item_id, int quantity = 1);
55 65
56DoorShuffleMode AP_GetDoorShuffleMode(); 66DoorShuffleMode AP_GetDoorShuffleMode();
57 67
68bool AP_AreDoorsGrouped();
69
58bool AP_IsColorShuffle(); 70bool AP_IsColorShuffle();
59 71
60bool AP_IsPaintingShuffle(); 72bool AP_IsPaintingShuffle();
61 73
62const std::map<std::string, std::string>& AP_GetPaintingMapping(); 74std::map<std::string, std::string> AP_GetPaintingMapping();
63 75
64bool AP_IsPaintingMappedTo(const std::string& painting_id); 76bool AP_IsPaintingMappedTo(const std::string& painting_id);
65 77
66const std::set<std::string>& AP_GetCheckedPaintings(); 78std::set<std::string> AP_GetCheckedPaintings();
67 79
68bool AP_IsPaintingChecked(const std::string& painting_id); 80bool AP_IsPaintingChecked(const std::string& painting_id);
69 81
82void AP_RevealPaintings();
83
70int AP_GetMasteryRequirement(); 84int AP_GetMasteryRequirement();
71 85
72int AP_GetLevel2Requirement(); 86int AP_GetLevel2Requirement();
73 87
88LocationChecks AP_GetLocationsChecks();
89
74bool AP_IsLocationVisible(int classification); 90bool AP_IsLocationVisible(int classification);
75 91
76VictoryCondition AP_GetVictoryCondition(); 92PanelShuffleMode AP_GetPanelShuffleMode();
77 93
78bool AP_HasAchievement(const std::string& achievement_name); 94VictoryCondition AP_GetVictoryCondition();
79 95
80bool AP_HasEarlyColorHallways(); 96bool AP_HasEarlyColorHallways();
81 97
@@ -89,10 +105,14 @@ SunwarpAccess AP_GetSunwarpAccess();
89 105
90bool AP_IsSunwarpShuffle(); 106bool AP_IsSunwarpShuffle();
91 107
92const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); 108std::map<int, SunwarpMapping> AP_GetSunwarpMapping();
109
110bool AP_IsPostgameShuffle();
93 111
94bool AP_HasReachedGoal(); 112bool AP_HasReachedGoal();
95 113
96std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); 114std::optional<std::tuple<int, int>> AP_GetPlayerPosition();
97 115
116bool AP_IsPanelSolved(int solve_index);
117
98#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 c39e239..94b9888 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -12,36 +12,11 @@
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_;
44 std::vector<Panel> panels_; 18 std::vector<Panel> panels_;
19 std::vector<PanelDoor> panel_doors_;
45 std::vector<MapArea> map_areas_; 20 std::vector<MapArea> map_areas_;
46 std::vector<SubwayItem> subway_items_; 21 std::vector<SubwayItem> subway_items_;
47 std::vector<PaintingExit> paintings_; 22 std::vector<PaintingExit> paintings_;
@@ -49,6 +24,7 @@ struct GameData {
49 std::map<std::string, int> room_by_id_; 24 std::map<std::string, int> room_by_id_;
50 std::map<std::string, int> door_by_id_; 25 std::map<std::string, int> door_by_id_;
51 std::map<std::string, int> panel_by_id_; 26 std::map<std::string, int> panel_by_id_;
27 std::map<std::string, int> panel_doors_by_id_;
52 std::map<std::string, int> area_by_id_; 28 std::map<std::string, int> area_by_id_;
53 std::map<std::string, int> painting_by_id_; 29 std::map<std::string, int> painting_by_id_;
54 30
@@ -56,6 +32,7 @@ struct GameData {
56 32
57 std::map<std::string, int> room_by_painting_; 33 std::map<std::string, int> room_by_painting_;
58 std::map<int, int> room_by_sunwarp_; 34 std::map<int, int> room_by_sunwarp_;
35 std::map<int, int> panel_by_solve_index_;
59 36
60 std::vector<int> achievement_panels_; 37 std::vector<int> achievement_panels_;
61 38
@@ -66,6 +43,8 @@ struct GameData {
66 std::map<std::string, int> subway_item_by_painting_; 43 std::map<std::string, int> subway_item_by_painting_;
67 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; 44 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
68 45
46 std::map<int, std::string> item_by_ap_id_;
47
69 bool loaded_area_data_ = false; 48 bool loaded_area_data_ = false;
70 std::set<std::string> malconfigured_areas_; 49 std::set<std::string> malconfigured_areas_;
71 50
@@ -81,7 +60,7 @@ struct GameData {
81 ids_config["special_items"][color_name]) { 60 ids_config["special_items"][color_name]) {
82 std::string input_name = color_name; 61 std::string input_name = color_name;
83 input_name[0] = std::tolower(input_name[0]); 62 input_name[0] = std::tolower(input_name[0]);
84 ap_id_by_color_[GetColorForString(input_name)] = 63 ap_id_by_color_[GetLingoColorForString(input_name)] =
85 ids_config["special_items"][color_name].as<int>(); 64 ids_config["special_items"][color_name].as<int>();
86 } else { 65 } else {
87 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); 66 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
@@ -98,8 +77,18 @@ struct GameData {
98 init_color_id("Brown"); 77 init_color_id("Brown");
99 init_color_id("Gray"); 78 init_color_id("Gray");
100 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
101 rooms_.reserve(lingo_config.size() * 2); 88 rooms_.reserve(lingo_config.size() * 2);
102 89
90 std::vector<int> panel_location_ids;
91
103 for (const auto &room_it : lingo_config) { 92 for (const auto &room_it : lingo_config) {
104 int room_id = AddOrGetRoom(room_it.first.as<std::string>()); 93 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
105 94
@@ -177,12 +166,12 @@ struct GameData {
177 166
178 if (panel_it.second["colors"]) { 167 if (panel_it.second["colors"]) {
179 if (panel_it.second["colors"].IsScalar()) { 168 if (panel_it.second["colors"].IsScalar()) {
180 panels_[panel_id].colors.push_back(GetColorForString( 169 panels_[panel_id].colors.push_back(GetLingoColorForString(
181 panel_it.second["colors"].as<std::string>())); 170 panel_it.second["colors"].as<std::string>()));
182 } else { 171 } else {
183 for (const auto &color_node : panel_it.second["colors"]) { 172 for (const auto &color_node : panel_it.second["colors"]) {
184 panels_[panel_id].colors.push_back( 173 panels_[panel_id].colors.push_back(
185 GetColorForString(color_node.as<std::string>())); 174 GetLingoColorForString(color_node.as<std::string>()));
186 } 175 }
187 } 176 }
188 } 177 }
@@ -288,10 +277,11 @@ struct GameData {
288 ids_config["panels"][rooms_[room_id].name] && 277 ids_config["panels"][rooms_[room_id].name] &&
289 ids_config["panels"][rooms_[room_id].name] 278 ids_config["panels"][rooms_[room_id].name]
290 [panels_[panel_id].name]) { 279 [panels_[panel_id].name]) {
291 panels_[panel_id].ap_location_id = 280 int location_id = ids_config["panels"][rooms_[room_id].name]
292 ids_config["panels"][rooms_[room_id].name] 281 [panels_[panel_id].name]
293 [panels_[panel_id].name] 282 .as<int>();
294 .as<int>(); 283 panels_[panel_id].ap_location_id = location_id;
284 panel_location_ids.push_back(location_id);
295 } else { 285 } else {
296 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", 286 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
297 rooms_[room_id].name, 287 rooms_[room_id].name,
@@ -357,6 +347,9 @@ struct GameData {
357 ids_config["doors"][rooms_[room_id].name] 347 ids_config["doors"][rooms_[room_id].name]
358 [doors_[door_id].name]["item"] 348 [doors_[door_id].name]["item"]
359 .as<int>(); 349 .as<int>();
350
351 item_by_ap_id_[doors_[door_id].ap_item_id] =
352 doors_[door_id].item_name;
360 } else { 353 } else {
361 TrackerLog(fmt::format("Missing AP item ID for door {} - {}", 354 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
362 rooms_[room_id].name, 355 rooms_[room_id].name,
@@ -373,6 +366,9 @@ struct GameData {
373 doors_[door_id].group_ap_item_id = 366 doors_[door_id].group_ap_item_id =
374 ids_config["door_groups"][doors_[door_id].group_name] 367 ids_config["door_groups"][doors_[door_id].group_name]
375 .as<int>(); 368 .as<int>();
369
370 item_by_ap_id_[doors_[door_id].group_ap_item_id] =
371 doors_[door_id].group_name;
376 } else { 372 } else {
377 TrackerLog(fmt::format("Missing AP item ID for door group {}", 373 TrackerLog(fmt::format("Missing AP item ID for door group {}",
378 doors_[door_id].group_name)); 374 doors_[door_id].group_name));
@@ -430,6 +426,90 @@ struct GameData {
430 } 426 }
431 } 427 }
432 428
429 if (room_it.second["panel_doors"]) {
430 for (const auto &panel_door_it : room_it.second["panel_doors"]) {
431 std::string panel_door_name = panel_door_it.first.as<std::string>();
432 int panel_door_id =
433 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name);
434
435 std::map<std::string, std::vector<std::string>> panel_per_room;
436 int num_panels = 0;
437 for (const auto &panel_node : panel_door_it.second["panels"]) {
438 num_panels++;
439
440 int panel_id = -1;
441
442 if (panel_node.IsScalar()) {
443 panel_id = AddOrGetPanel(rooms_[room_id].name,
444 panel_node.as<std::string>());
445
446 panel_per_room[rooms_[room_id].name].push_back(
447 panel_node.as<std::string>());
448 } else {
449 panel_id = AddOrGetPanel(panel_node["room"].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>());
454 }
455
456 Panel &panel = panels_[panel_id];
457 panel.panel_door = panel_door_id;
458 }
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
479 if (ids_config["panel_doors"] &&
480 ids_config["panel_doors"][rooms_[room_id].name] &&
481 ids_config["panel_doors"][rooms_[room_id].name]
482 [panel_door_name]) {
483 panel_doors_[panel_door_id].ap_item_id =
484 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name]
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;
489 } else {
490 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}",
491 rooms_[room_id].name, panel_door_name));
492 }
493
494 if (panel_door_it.second["panel_group"]) {
495 std::string panel_group =
496 panel_door_it.second["panel_group"].as<std::string>();
497
498 if (ids_config["panel_groups"] &&
499 ids_config["panel_groups"][panel_group]) {
500 panel_doors_[panel_door_id].group_ap_item_id =
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;
505 } else {
506 TrackerLog(fmt::format(
507 "Missing AP item ID for panel door group {}", panel_group));
508 }
509 }
510 }
511 }
512
433 if (room_it.second["paintings"]) { 513 if (room_it.second["paintings"]) {
434 for (const auto &painting : room_it.second["paintings"]) { 514 for (const auto &painting : room_it.second["paintings"]) {
435 std::string internal_id = painting["id"].as<std::string>(); 515 std::string internal_id = painting["id"].as<std::string>();
@@ -437,6 +517,13 @@ struct GameData {
437 PaintingExit &painting_exit = paintings_[painting_id]; 517 PaintingExit &painting_exit = paintings_[painting_id];
438 painting_exit.room = room_id; 518 painting_exit.room = room_id;
439 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
440 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && 527 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
441 (!painting["disable"] || !painting["disable"].as<bool>())) { 528 (!painting["disable"] || !painting["disable"].as<bool>())) {
442 painting_exit.entrance = true; 529 painting_exit.entrance = true;
@@ -478,33 +565,74 @@ struct GameData {
478 ids_config["progression"][progressive_item_name]) { 565 ids_config["progression"][progressive_item_name]) {
479 progressive_item_id = 566 progressive_item_id =
480 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;
481 } else { 570 } else {
482 TrackerLog(fmt::format("Missing AP item ID for progressive item {}", 571 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
483 progressive_item_name)); 572 progressive_item_name));
484 } 573 }
485 574
486 int index = 1; 575 if (progression_it.second["doors"]) {
487 for (const auto &stage : progression_it.second) { 576 int index = 1;
488 int door_id = -1; 577 for (const auto &stage : progression_it.second["doors"]) {
578 int door_id = -1;
579
580 if (stage.IsScalar()) {
581 door_id =
582 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>());
583 } else {
584 door_id = AddOrGetDoor(stage["room"].as<std::string>(),
585 stage["door"].as<std::string>());
586 }
489 587
490 if (stage.IsScalar()) { 588 doors_[door_id].progressives.push_back(
491 door_id = 589 {.item_name = progressive_item_name,
492 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); 590 .ap_item_id = progressive_item_id,
493 } else { 591 .quantity = index});
494 door_id = AddOrGetDoor(stage["room"].as<std::string>(), 592 index++;
495 stage["door"].as<std::string>());
496 } 593 }
594 }
595
596 if (progression_it.second["panel_doors"]) {
597 int index = 1;
598 for (const auto &stage : progression_it.second["panel_doors"]) {
599 int panel_door_id = -1;
600
601 if (stage.IsScalar()) {
602 panel_door_id = AddOrGetPanelDoor(rooms_[room_id].name,
603 stage.as<std::string>());
604 } else {
605 panel_door_id =
606 AddOrGetPanelDoor(stage["room"].as<std::string>(),
607 stage["panel_door"].as<std::string>());
608 }
497 609
498 doors_[door_id].progressives.push_back( 610 panel_doors_[panel_door_id].progressives.push_back(
499 {.item_name = progressive_item_name, 611 {.item_name = progressive_item_name,
500 .ap_item_id = progressive_item_id, 612 .ap_item_id = progressive_item_id,
501 .quantity = index}); 613 .quantity = index});
502 index++; 614 index++;
615 }
503 } 616 }
504 } 617 }
505 } 618 }
506 } 619 }
507 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
508 map_areas_.reserve(areas_config.size()); 636 map_areas_.reserve(areas_config.size());
509 637
510 std::map<std::string, int> fold_areas; 638 std::map<std::string, int> fold_areas;
@@ -525,7 +653,7 @@ struct GameData {
525 // Only locations for the panels are kept here. 653 // Only locations for the panels are kept here.
526 std::map<std::string, std::tuple<int, int>> locations_by_name; 654 std::map<std::string, std::tuple<int, int>> locations_by_name;
527 655
528 for (const Panel &panel : panels_) { 656 for (Panel &panel : panels_) {
529 int room_id = panel.room; 657 int room_id = panel.room;
530 std::string room_name = rooms_[room_id].name; 658 std::string room_name = rooms_[room_id].name;
531 659
@@ -541,6 +669,8 @@ struct GameData {
541 area_name = location_name.substr(0, divider_pos); 669 area_name = location_name.substr(0, divider_pos);
542 section_name = location_name.substr(divider_pos + 3); 670 section_name = location_name.substr(divider_pos + 3);
543 } 671 }
672 } else {
673 panel.location_name = location_name;
544 } 674 }
545 675
546 if (fold_areas.count(area_name)) { 676 if (fold_areas.count(area_name)) {
@@ -639,7 +769,8 @@ struct GameData {
639 MapArea &map_area = map_areas_[area_id]; 769 MapArea &map_area = map_areas_[area_id];
640 770
641 for (int painting_id : room.paintings) { 771 for (int painting_id : room.paintings) {
642 const PaintingExit &painting_obj = paintings_.at(painting_id); 772 PaintingExit &painting_obj = paintings_.at(painting_id);
773 painting_obj.map_area = area_id;
643 if (painting_obj.entrance) { 774 if (painting_obj.entrance) {
644 map_area.paintings.push_back(painting_id); 775 map_area.paintings.push_back(painting_id);
645 } 776 }
@@ -666,13 +797,10 @@ struct GameData {
666 subway_it["door"].as<std::string>()); 797 subway_it["door"].as<std::string>());
667 } 798 }
668 799
669 if (subway_it["paintings"]) { 800 if (subway_it["painting"]) {
670 for (const auto &painting_it : subway_it["paintings"]) { 801 std::string painting_id = subway_it["painting"].as<std::string>();
671 std::string painting_id = painting_it.as<std::string>(); 802 subway_item.painting = painting_id;
672 803 subway_item_by_painting_[painting_id] = subway_item.id;
673 subway_item.paintings.push_back(painting_id);
674 subway_item_by_painting_[painting_id] = subway_item.id;
675 }
676 } 804 }
677 805
678 if (subway_it["tags"]) { 806 if (subway_it["tags"]) {
@@ -681,6 +809,18 @@ struct GameData {
681 } 809 }
682 } 810 }
683 811
812 if (subway_it["entrances"]) {
813 for (const auto &entrance_it : subway_it["entrances"]) {
814 subway_item.entrances.push_back(entrance_it.as<std::string>());
815 }
816 }
817
818 if (subway_it["exits"]) {
819 for (const auto &exit_it : subway_it["exits"]) {
820 subway_item.exits.push_back(exit_it.as<std::string>());
821 }
822 }
823
684 if (subway_it["sunwarp"]) { 824 if (subway_it["sunwarp"]) {
685 SubwaySunwarp sunwarp; 825 SubwaySunwarp sunwarp;
686 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); 826 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
@@ -707,6 +847,10 @@ struct GameData {
707 subway_item.special = subway_it["special"].as<std::string>(); 847 subway_item.special = subway_it["special"].as<std::string>();
708 } 848 }
709 849
850 if (subway_it["tilted"]) {
851 subway_item.tilted = subway_it["tilted"].as<bool>();
852 }
853
710 subway_items_.push_back(subway_item); 854 subway_items_.push_back(subway_item);
711 } 855 }
712 856
@@ -760,6 +904,18 @@ struct GameData {
760 return panel_by_id_[full_name]; 904 return panel_by_id_[full_name];
761 } 905 }
762 906
907 int AddOrGetPanelDoor(std::string room, std::string panel) {
908 std::string full_name = room + " - " + panel;
909
910 if (!panel_doors_by_id_.count(full_name)) {
911 int panel_door_id = panel_doors_.size();
912 panel_doors_by_id_[full_name] = panel_door_id;
913 panel_doors_.push_back({});
914 }
915
916 return panel_doors_by_id_[full_name];
917 }
918
763 int AddOrGetArea(std::string area) { 919 int AddOrGetArea(std::string area) {
764 if (!area_by_id_.count(area)) { 920 if (!area_by_id_.count(area)) {
765 if (loaded_area_data_) { 921 if (loaded_area_data_) {
@@ -792,6 +948,11 @@ GameData &GetState() {
792 948
793} // namespace 949} // namespace
794 950
951bool SubwayItem::HasWarps() const {
952 return !(this->tags.empty() && this->entrances.empty() &&
953 this->exits.empty());
954}
955
795bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const { 956bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
796 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); 957 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
797} 958}
@@ -810,6 +971,10 @@ const std::vector<Door> &GD_GetDoors() { return GetState().doors_; }
810 971
811const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } 972const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); }
812 973
974const PanelDoor &GD_GetPanelDoor(int panel_door_id) {
975 return GetState().panel_doors_.at(panel_door_id);
976}
977
813int GD_GetDoorByName(const std::string &name) { 978int GD_GetDoorByName(const std::string &name) {
814 return GetState().door_by_id_.at(name); 979 return GetState().door_by_id_.at(name);
815} 980}
@@ -818,6 +983,14 @@ const Panel &GD_GetPanel(int panel_id) {
818 return GetState().panels_.at(panel_id); 983 return GetState().panels_.at(panel_id);
819} 984}
820 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
821const PaintingExit &GD_GetPaintingExit(int painting_id) { 994const PaintingExit &GD_GetPaintingExit(int painting_id) {
822 return GetState().paintings_.at(painting_id); 995 return GetState().paintings_.at(painting_id);
823} 996}
@@ -860,3 +1033,38 @@ std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
860int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 1033int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
861 return GetState().subway_item_by_sunwarp_.at(sunwarp); 1034 return GetState().subway_item_by_sunwarp_.at(sunwarp);
862} 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 3179365..ac911e5 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -56,6 +56,8 @@ struct Panel {
56 bool non_counting = false; 56 bool non_counting = false;
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;
60 int solve_index = -1;
59}; 61};
60 62
61struct ProgressiveRequirement { 63struct ProgressiveRequirement {
@@ -83,6 +85,13 @@ struct Door {
83 DoorType type = DoorType::kNormal; 85 DoorType type = DoorType::kNormal;
84}; 86};
85 87
88struct PanelDoor {
89 int ap_item_id = -1;
90 int group_ap_item_id = -1;
91 std::vector<ProgressiveRequirement> progressives;
92 std::string item_name;
93};
94
86struct Exit { 95struct Exit {
87 int source_room; 96 int source_room;
88 int destination_room; 97 int destination_room;
@@ -94,8 +103,10 @@ struct PaintingExit {
94 int id; 103 int id;
95 int room; 104 int room;
96 std::string internal_id; 105 std::string internal_id;
106 std::string display_name;
97 std::optional<int> door; 107 std::optional<int> door;
98 bool entrance = false; 108 bool entrance = false;
109 int map_area;
99}; 110};
100 111
101struct Room { 112struct Room {
@@ -146,11 +157,16 @@ struct SubwayItem {
146 int id; 157 int id;
147 int x; 158 int x;
148 int y; 159 int y;
160 bool tilted = false;
149 std::optional<int> door; 161 std::optional<int> door;
150 std::vector<std::string> paintings; 162 std::optional<std::string> painting;
151 std::vector<std::string> tags; 163 std::vector<std::string> tags; // 2-way teleports
164 std::vector<std::string> entrances; // teleport entrances
165 std::vector<std::string> exits; // teleport exits
152 std::optional<SubwaySunwarp> sunwarp; 166 std::optional<SubwaySunwarp> sunwarp;
153 std::optional<std::string> special; 167 std::optional<std::string> special;
168
169 bool HasWarps() const;
154}; 170};
155 171
156const std::vector<MapArea>& GD_GetMapAreas(); 172const std::vector<MapArea>& GD_GetMapAreas();
@@ -161,6 +177,9 @@ const std::vector<Door>& GD_GetDoors();
161const Door& GD_GetDoor(int door_id); 177const Door& GD_GetDoor(int door_id);
162int GD_GetDoorByName(const std::string& name); 178int GD_GetDoorByName(const std::string& name);
163const Panel& GD_GetPanel(int panel_id); 179const Panel& GD_GetPanel(int panel_id);
180int GD_GetPanelBySolveIndex(int solve_index);
181const PanelDoor& GD_GetPanelDoor(int panel_door_id);
182const std::vector<PaintingExit>& GD_GetPaintings();
164const PaintingExit& GD_GetPaintingExit(int painting_id); 183const PaintingExit& GD_GetPaintingExit(int painting_id);
165int GD_GetPaintingByName(const std::string& name); 184int GD_GetPaintingByName(const std::string& name);
166const std::vector<int>& GD_GetAchievementPanels(); 185const std::vector<int>& GD_GetAchievementPanels();
@@ -171,5 +190,8 @@ const std::vector<SubwayItem>& GD_GetSubwayItems();
171const SubwayItem& GD_GetSubwayItem(int id); 190const SubwayItem& GD_GetSubwayItem(int id);
172std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); 191std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
173int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 192int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
193std::string GD_GetItemName(int id);
194
195LingoColor GetLingoColorForString(const std::string& str);
174 196
175#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 1bc906f..0000000 --- a/src/godot_variant.cpp +++ /dev/null
@@ -1,83 +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 <fstream>
9#include <string>
10#include <tuple>
11#include <variant>
12#include <vector>
13
14namespace {
15
16uint16_t ReadUint16(std::basic_istream<char>& stream) {
17 uint16_t result;
18 stream.read(reinterpret_cast<char*>(&result), 2);
19 return result;
20}
21
22uint32_t ReadUint32(std::basic_istream<char>& stream) {
23 uint32_t result;
24 stream.read(reinterpret_cast<char*>(&result), 4);
25 return result;
26}
27
28GodotVariant ParseVariant(std::basic_istream<char>& stream) {
29 uint16_t type = ReadUint16(stream);
30 stream.ignore(2);
31
32 switch (type) {
33 case 1: {
34 // bool
35 bool boolval = (ReadUint32(stream) == 1);
36 return {boolval};
37 }
38 case 15: {
39 // nodepath
40 uint32_t name_length = ReadUint32(stream) & 0x7fffffff;
41 uint32_t subname_length = ReadUint32(stream) & 0x7fffffff;
42 uint32_t flags = ReadUint32(stream);
43
44 std::vector<std::string> result;
45 for (size_t i = 0; i < name_length + subname_length; i++) {
46 uint32_t char_length = ReadUint32(stream);
47 uint32_t padded_length = (char_length % 4 == 0)
48 ? char_length
49 : (char_length + 4 - (char_length % 4));
50 std::vector<char> next_bytes(padded_length);
51 stream.read(next_bytes.data(), padded_length);
52 std::string next_piece;
53 std::copy(next_bytes.begin(),
54 std::next(next_bytes.begin(), char_length),
55 std::back_inserter(next_piece));
56 result.push_back(next_piece);
57 }
58
59 return {result};
60 }
61 case 19: {
62 // array
63 uint32_t length = ReadUint32(stream) & 0x7fffffff;
64 std::vector<GodotVariant> result;
65 for (size_t i = 0; i < length; i++) {
66 result.push_back(ParseVariant(stream));
67 }
68 return {result};
69 }
70 default: {
71 // eh
72 return {std::monostate{}};
73 }
74 }
75}
76
77} // namespace
78
79GodotVariant ParseGodotFile(std::string filename) {
80 std::ifstream file_stream(filename, std::ios_base::binary);
81 file_stream.ignore(4);
82 return ParseVariant(file_stream);
83}
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 new file mode 100644 index 0000000..6763b7f --- /dev/null +++ b/src/ipc_dialog.cpp
@@ -0,0 +1,54 @@
1#include "ipc_dialog.h"
2
3#include "tracker_config.h"
4
5constexpr const char* kDefaultIpcAddress = "ws://127.0.0.1:41253";
6
7IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") {
8 std::string address_value;
9 if (GetTrackerConfig().ipc_address.empty()) {
10 address_value = kDefaultIpcAddress;
11 } else {
12 address_value = GetTrackerConfig().ipc_address;
13 }
14
15 address_box_ = new wxTextCtrl(this, -1, wxString::FromUTF8(address_value),
16 wxDefaultPosition, FromDIP(wxSize{300, -1}));
17
18 wxButton* reset_button = new wxButton(this, -1, "Use Default");
19 reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this);
20
21 wxFlexGridSizer* form_sizer =
22 new wxFlexGridSizer(3, FromDIP(10), FromDIP(10));
23 form_sizer->Add(
24 new wxStaticText(this, -1, "Address:"),
25 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
26 form_sizer->Add(address_box_, wxSizerFlags().Expand());
27 form_sizer->Add(reset_button);
28
29 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
30 wxStaticText* top_text = new wxStaticText(this, -1, "");
31 top_sizer->Add(top_text, wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder().Expand());
32 top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand());
33 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
34 wxSizerFlags().Border().Center());
35
36 SetSizer(top_sizer);
37 Layout();
38 Fit();
39
40 int width = top_text->GetClientSize().GetWidth();
41 top_text->SetLabel(
42 "This allows you to connect to a running Lingo game and track non-multiworld "
43 "state, such as the player's position and what panels are solved. Unless "
44 "you are doing something weird, the default value for the address is "
45 "probably correct.");
46 top_text->Wrap(width);
47
48 Fit();
49 Center();
50}
51
52void IpcDialog::OnResetClicked(wxCommandEvent& event) {
53 address_box_->SetValue(kDefaultIpcAddress);
54}
diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h new file mode 100644 index 0000000..a8c4512 --- /dev/null +++ b/src/ipc_dialog.h
@@ -0,0 +1,24 @@
1#ifndef IPC_DIALOG_H_F4C5680C
2#define IPC_DIALOG_H_F4C5680C
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <string>
11
12class IpcDialog : public wxDialog {
13 public:
14 IpcDialog();
15
16 std::string GetIpcAddress() { return address_box_->GetValue().utf8_string(); }
17
18 private:
19 void OnResetClicked(wxCommandEvent& event);
20
21 wxTextCtrl* address_box_;
22};
23
24#endif /* end of include guard: IPC_DIALOG_H_F4C5680C */
diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp new file mode 100644 index 0000000..6e2a440 --- /dev/null +++ b/src/ipc_state.cpp
@@ -0,0 +1,367 @@
1#include "ipc_state.h"
2
3#define _WEBSOCKETPP_CPP11_STRICT_
4
5#include <fmt/core.h>
6
7#include <chrono>
8#include <memory>
9#include <mutex>
10#include <nlohmann/json.hpp>
11#include <optional>
12#include <set>
13#include <string>
14#include <thread>
15#include <tuple>
16#include <wswrap.hpp>
17
18#include "ap_state.h"
19#include "logger.h"
20#include "tracker_frame.h"
21
22namespace {
23
24struct IPCState {
25 std::mutex state_mutex;
26 TrackerFrame* tracker_frame = nullptr;
27
28 // Protected state
29 bool initialized = false;
30 std::string address;
31 bool should_disconnect = false;
32
33 std::optional<std::string> status_message;
34
35 bool slot_matches = false;
36 std::string tracker_ap_server;
37 std::string tracker_ap_user;
38 std::string game_ap_server;
39 std::string game_ap_user;
40
41 std::optional<std::tuple<int, int>> player_position;
42
43 // Thread state
44 std::unique_ptr<wswrap::WS> ws;
45 bool connected = false;
46
47 void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; }
48
49 void Connect(std::string a) {
50 // This is the main concurrency concern, as it mutates protected state in an
51 // important way. Thread() is documented with how it interacts with this
52 // function.
53 std::lock_guard state_guard(state_mutex);
54
55 if (!initialized) {
56 std::thread([this]() { Thread(); }).detach();
57
58 initialized = true;
59 } else if (address != a) {
60 should_disconnect = true;
61 }
62
63 address = a;
64 }
65
66 std::optional<std::string> GetStatusMessage() {
67 std::lock_guard state_guard(state_mutex);
68
69 return status_message;
70 }
71
72 void SetTrackerSlot(std::string server, std::string user) {
73 // This function is called from the APState thread, not the main thread, and
74 // it mutates protected state. It only really competes with OnMessage(), when
75 // a "Connect" message is received. If this is called right before, and the
76 // tracker slot does not match the old game slot, it will initiate a
77 // disconnect, and then the OnMessage() handler will see should_disconnect
78 // and stop processing the "Connect" message. If this is called right after
79 // and the slot does not match, IPC will disconnect, which is tolerable.
80 std::lock_guard state_guard(state_mutex);
81
82 tracker_ap_server = std::move(server);
83 tracker_ap_user = std::move(user);
84
85 CheckIfSlotMatches();
86
87 if (!slot_matches) {
88 should_disconnect = true;
89 address.clear();
90 }
91 }
92
93 bool IsConnected() {
94 std::lock_guard state_guard(state_mutex);
95
96 return slot_matches;
97 }
98
99 std::optional<std::tuple<int, int>> GetPlayerPosition() {
100 std::lock_guard state_guard(state_mutex);
101
102 return player_position;
103 }
104
105 private:
106 void Thread() {
107 for (;;) {
108 // initialized is definitely true because it is set to true when the thread
109 // is created and only set to false within this block, when the thread is
110 // killed. Thus, a call to Connect would always at most set
111 // should_disconnect and address. If this happens before this block, it is
112 // as if we are starting from a new thread anyway because should_disconnect
113 // is immediately reset. If a call to Connect happens after this block,
114 // then a connection attempt will be made to the wrong address, but the
115 // thread will grab the mutex right after this and back out the wrong
116 // connection.
117 std::string ipc_address;
118 {
119 std::lock_guard state_guard(state_mutex);
120
121 SetStatusMessage("Disconnected from game.");
122
123 should_disconnect = false;
124
125 slot_matches = false;
126 game_ap_server.clear();
127 game_ap_user.clear();
128
129 player_position = std::nullopt;
130
131 if (address.empty()) {
132 initialized = false;
133 return;
134 }
135
136 ipc_address = address;
137
138 SetStatusMessage("Connecting to game...");
139 }
140
141 int backoff_amount = 0;
142
143 TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address));
144
145 while (!connected) {
146 if (TryConnect(ipc_address)) {
147 int backoff_limit = (backoff_amount + 1) * 10;
148
149 for (int i = 0; i < backoff_limit && !connected; i++) {
150 // If Connect is called right before this block, we will see and
151 // handle should_disconnect. If it is called right after, we will do
152 // one bad poll, one sleep, and then grab the mutex again right
153 // after.
154 {
155 std::lock_guard state_guard(state_mutex);
156 if (should_disconnect) {
157 break;
158 }
159 }
160
161 ws->poll();
162
163 // Back off
164 std::this_thread::sleep_for(std::chrono::milliseconds(100));
165 }
166
167 backoff_amount++;
168 } else {
169 std::lock_guard state_guard(state_mutex);
170
171 if (!should_disconnect) {
172 should_disconnect = true;
173 address.clear();
174
175 SetStatusMessage("Disconnected from game.");
176 }
177
178 break;
179 }
180
181 // If Connect is called right before this block, we will see and handle
182 // should_disconnect. If it is called right after, and the connection
183 // was unsuccessful, we will grab the mutex after one bad connection
184 // attempt. If the connection was successful, we grab the mutex right
185 // after exiting the loop.
186 bool show_error = false;
187 {
188 std::lock_guard state_guard(state_mutex);
189
190 if (should_disconnect) {
191 break;
192 } else if (!connected) {
193 if (backoff_amount >= 10) {
194 should_disconnect = true;
195 address.clear();
196
197 SetStatusMessage("Disconnected from game.");
198
199 show_error = true;
200 } else {
201 TrackerLog(fmt::format("Retrying IPC in {} second(s)...",
202 backoff_amount + 1));
203 }
204 }
205 }
206
207 // We do this after giving up the mutex because otherwise we could
208 // deadlock with the main thread.
209 if (show_error) {
210 TrackerLog("Giving up on IPC.");
211
212 wxMessageBox("Connection to Lingo timed out.", "Connection failed",
213 wxOK | wxICON_ERROR);
214 break;
215 }
216 }
217
218 // Pretty much every lock guard in the thread is the same. We check for
219 // should_disconnect, and if it gets set directly after the block, we do
220 // minimal bad work before checking for it again.
221 {
222 std::lock_guard state_guard(state_mutex);
223 if (should_disconnect) {
224 ws.reset();
225 continue;
226 }
227 }
228
229 while (connected) {
230 ws->poll();
231
232 std::this_thread::sleep_for(std::chrono::milliseconds(100));
233
234 {
235 std::lock_guard state_guard(state_mutex);
236 if (should_disconnect) {
237 ws.reset();
238 break;
239 }
240 }
241 }
242 }
243 }
244
245 bool TryConnect(std::string ipc_address) {
246 try {
247 ws = std::make_unique<wswrap::WS>(
248 ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); },
249 [this](const std::string& s) { OnMessage(s); },
250 [this](const std::string& s) { OnError(s); });
251 return true;
252 } catch (const std::exception& ex) {
253 TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what()));
254 wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR);
255 ws.reset();
256 return false;
257 }
258 }
259
260 void OnConnect() {
261 connected = true;
262
263 {
264 std::lock_guard state_guard(state_mutex);
265
266 slot_matches = false;
267 player_position = std::nullopt;
268 }
269 }
270
271 void OnClose() {
272 connected = false;
273
274 {
275 std::lock_guard state_guard(state_mutex);
276
277 slot_matches = false;
278 }
279 }
280
281 void OnMessage(const std::string& s) {
282 TrackerLog(s);
283
284 auto msg = nlohmann::json::parse(s);
285
286 if (msg["cmd"] == "Connect") {
287 std::lock_guard state_guard(state_mutex);
288 if (should_disconnect) {
289 return;
290 }
291
292 game_ap_server = msg["slot"]["server"];
293 game_ap_user = msg["slot"]["player"];
294
295 CheckIfSlotMatches();
296
297 if (!slot_matches) {
298 tracker_frame->ConnectToAp(game_ap_server, game_ap_user,
299 msg["slot"]["password"]);
300 }
301 } else if (msg["cmd"] == "UpdatePosition") {
302 std::lock_guard state_guard(state_mutex);
303
304 player_position =
305 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);
306
307 tracker_frame->UpdateIndicators(StateUpdate{.player_position = true});
308 }
309 }
310
311 void OnError(const std::string& s) {}
312
313 // Assumes mutex is locked.
314 void CheckIfSlotMatches() {
315 slot_matches = (tracker_ap_server == game_ap_server &&
316 tracker_ap_user == game_ap_user);
317
318 if (slot_matches) {
319 SetStatusMessage("Connected to game.");
320
321 Sync();
322 } else if (connected) {
323 SetStatusMessage("Local game doesn't match AP slot.");
324 }
325 }
326
327 // Assumes mutex is locked.
328 void SetStatusMessage(std::optional<std::string> msg) {
329 status_message = msg;
330
331 tracker_frame->UpdateStatusMessage();
332 }
333
334 void Sync() {
335 nlohmann::json msg;
336 msg["cmd"] = "Sync";
337
338 ws->send_text(msg.dump());
339 }
340};
341
342IPCState& GetState() {
343 static IPCState* instance = new IPCState();
344 return *instance;
345}
346
347} // namespace
348
349void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) {
350 GetState().SetTrackerFrame(tracker_frame);
351}
352
353void IPC_Connect(std::string address) { GetState().Connect(address); }
354
355std::optional<std::string> IPC_GetStatusMessage() {
356 return GetState().GetStatusMessage();
357}
358
359void IPC_SetTrackerSlot(std::string server, std::string user) {
360 GetState().SetTrackerSlot(server, user);
361}
362
363bool IPC_IsConnected() { return GetState().IsConnected(); }
364
365std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
366 return GetState().GetPlayerPosition();
367}
diff --git a/src/ipc_state.h b/src/ipc_state.h new file mode 100644 index 0000000..0e6fa51 --- /dev/null +++ b/src/ipc_state.h
@@ -0,0 +1,23 @@
1#ifndef IPC_STATE_H_6B3B0958
2#define IPC_STATE_H_6B3B0958
3
4#include <optional>
5#include <set>
6#include <string>
7#include <tuple>
8
9class TrackerFrame;
10
11void IPC_SetTrackerFrame(TrackerFrame* tracker_frame);
12
13void IPC_Connect(std::string address);
14
15std::optional<std::string> IPC_GetStatusMessage();
16
17void IPC_SetTrackerSlot(std::string server, std::string user);
18
19bool IPC_IsConnected();
20
21std::optional<std::tuple<int, int>> IPC_GetPlayerPosition();
22
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/network_set.cpp b/src/network_set.cpp index 2a9e12c..45911e3 100644 --- a/src/network_set.cpp +++ b/src/network_set.cpp
@@ -4,9 +4,8 @@ void NetworkSet::Clear() {
4 network_by_item_.clear(); 4 network_by_item_.clear();
5} 5}
6 6
7void NetworkSet::AddLink(int id1, int id2) { 7void NetworkSet::AddLink(int id1, int id2, bool two_way) {
8 if (id2 > id1) { 8 if (two_way && id2 > id1) {
9 // Make sure id1 < id2
10 std::swap(id1, id2); 9 std::swap(id1, id2);
11 } 10 }
12 11
@@ -17,13 +16,14 @@ void NetworkSet::AddLink(int id1, int id2) {
17 network_by_item_[id2] = {}; 16 network_by_item_[id2] = {};
18 } 17 }
19 18
20 network_by_item_[id1].insert({id1, id2}); 19 NetworkNode node = {id1, id2, two_way};
21 network_by_item_[id2].insert({id1, id2}); 20
21 network_by_item_[id1].insert(node);
22 network_by_item_[id2].insert(node);
22} 23}
23 24
24void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2) { 25void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2, bool two_way) {
25 if (id2 > id1) { 26 if (two_way && id2 > id1) {
26 // Make sure id1 < id2
27 std::swap(id1, id2); 27 std::swap(id1, id2);
28 } 28 }
29 29
@@ -31,13 +31,22 @@ void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2) {
31 network_by_item_[network_id] = {}; 31 network_by_item_[network_id] = {};
32 } 32 }
33 33
34 network_by_item_[network_id].insert({id1, id2}); 34 NetworkNode node = {id1, id2, two_way};
35
36 network_by_item_[network_id].insert(node);
35} 37}
36 38
37bool NetworkSet::IsItemInNetwork(int id) const { 39bool NetworkSet::IsItemInNetwork(int id) const {
38 return network_by_item_.count(id); 40 return network_by_item_.count(id);
39} 41}
40 42
41const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const { 43const std::set<NetworkNode>& NetworkSet::GetNetworkGraph(int id) const {
42 return network_by_item_.at(id); 44 return network_by_item_.at(id);
43} 45}
46
47bool NetworkNode::operator<(const NetworkNode& rhs) const {
48 if (entry != rhs.entry) return entry < rhs.entry;
49 if (exit != rhs.exit) return exit < rhs.exit;
50 if (two_way != rhs.two_way) return two_way < rhs.two_way;
51 return false;
52}
diff --git a/src/network_set.h b/src/network_set.h index cec3f39..0f72052 100644 --- a/src/network_set.h +++ b/src/network_set.h
@@ -7,21 +7,29 @@
7#include <utility> 7#include <utility>
8#include <vector> 8#include <vector>
9 9
10struct NetworkNode {
11 int entry;
12 int exit;
13 bool two_way;
14
15 bool operator<(const NetworkNode& rhs) const;
16};
17
10class NetworkSet { 18class NetworkSet {
11 public: 19 public:
12 void Clear(); 20 void Clear();
13 21
14 void AddLink(int id1, int id2); 22 void AddLink(int id1, int id2, bool two_way);
15 23
16 void AddLinkToNetwork(int network_id, int id1, int id2); 24 void AddLinkToNetwork(int network_id, int id1, int id2, bool two_way);
17 25
18 bool IsItemInNetwork(int id) const; 26 bool IsItemInNetwork(int id) const;
19 27
20 const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const; 28 const std::set<NetworkNode>& GetNetworkGraph(int id) const;
21 29
22 private: 30 private:
23 31
24 std::map<int, std::set<std::pair<int, int>>> network_by_item_; 32 std::map<int, std::set<NetworkNode>> network_by_item_;
25}; 33};
26 34
27#endif /* end of include guard: NETWORK_SET_H_3036B8E3 */ 35#endif /* end of include guard: NETWORK_SET_H_3036B8E3 */
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 5b3ff5f..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -9,31 +9,25 @@
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 };
18 21
19namespace { 22namespace {
20 23
21std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) { 24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
22 if (AP_IsSunwarpShuffle() && subway_item.sunwarp && 25 if (subway_item.painting) {
23 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { 26 return {subway_item.x, subway_item.y};
24 int sunwarp_index = subway_item.sunwarp->dots - 1; 27 } else {
25 if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) { 28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
26 sunwarp_index += 6; 29 subway_item.y + AREA_ACTUAL_SIZE / 2};
27 }
28
29 for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) {
30 if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
31 return GD_GetSunwarpDoors().at(mapping.dots - 1);
32 }
33 }
34 } 30 }
35
36 return subway_item.door;
37} 31}
38 32
39} // namespace 33} // namespace
@@ -53,14 +47,6 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
53 return; 47 return;
54 } 48 }
55 49
56 unchecked_eye_ =
57 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
58 wxBITMAP_TYPE_PNG)
59 .Scale(32, 32));
60 checked_eye_ = wxBitmap(
61 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
62 .Scale(32, 32));
63
64 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( 50 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
65 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), 51 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
66 static_cast<float>(map_image_.GetHeight())}); 52 static_cast<float>(map_image_.GetHeight())});
@@ -79,50 +65,72 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
79 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); 65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
80 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); 66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
81 67
82 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}));
83 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); 69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
84 70
85 help_button_ = new wxButton(this, wxID_ANY, "Help"); 71 help_button_ = new wxButton(this, wxID_ANY, "Help");
86 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); 72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
87 SetUpHelpButton(); 73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
88} 76}
89 77
90void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
91 networks_.Clear(); 79 networks_.Clear();
92 80
93 std::map<std::string, std::vector<int>> tagged; 81 std::map<std::string, std::vector<int>> tagged;
82 std::map<std::string, std::vector<int>> entrances;
83 std::map<std::string, std::vector<int>> exits;
94 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
95 if (AP_HasEarlyColorHallways() && 85 if (AP_HasEarlyColorHallways() &&
96 subway_item.special == "starting_room_paintings") { 86 subway_item.special == "early_color_hallways") {
97 tagged["early_ch"].push_back(subway_item.id); 87 entrances["early_ch"].push_back(subway_item.id);
98 } 88 }
99 89
100 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 90 if (AP_IsPaintingShuffle() && subway_item.painting) {
101 continue; 91 continue;
102 } 92 }
103 93
104 for (const std::string &tag : subway_item.tags) { 94 for (const std::string &tag : subway_item.tags) {
105 tagged[tag].push_back(subway_item.id); 95 tagged[tag].push_back(subway_item.id);
106 } 96 }
97 for (const std::string &tag : subway_item.entrances) {
98 entrances[tag].push_back(subway_item.id);
99 }
100 for (const std::string &tag : subway_item.exits) {
101 exits[tag].push_back(subway_item.id);
102 }
107 103
108 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && 104 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) {
109 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
110 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots); 105 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots);
111 tagged[tag].push_back(subway_item.id); 106 switch (subway_item.sunwarp->type) {
107 case SubwaySunwarpType::kEnter:
108 entrances[tag].push_back(subway_item.id);
109 break;
110 case SubwaySunwarpType::kExit:
111 exits[tag].push_back(subway_item.id);
112 break;
113 default:
114 break;
115 }
112 } 116 }
113 117
114 if (!AP_IsPilgrimageEnabled() && 118 if (!AP_IsPilgrimageEnabled()) {
115 (subway_item.special == "sun_painting" || 119 if (subway_item.special == "sun_painting") {
116 subway_item.special == "sun_painting_exit")) { 120 entrances["sun_painting"].push_back(subway_item.id);
117 tagged["sun_painting"].push_back(subway_item.id); 121 } else if (subway_item.special == "sun_painting_exit") {
122 exits["sun_painting"].push_back(subway_item.id);
123 }
118 } 124 }
119 } 125 }
120 126
121 if (AP_IsSunwarpShuffle()) { 127 if (AP_IsSunwarpShuffle()) {
128 sunwarp_mapping_ = AP_GetSunwarpMapping();
129
122 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal}; 130 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal};
123 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp); 131 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp);
124 132
125 for (const auto &[index, mapping] : AP_GetSunwarpMapping()) { 133 for (const auto &[index, mapping] : sunwarp_mapping_) {
126 std::string tag = fmt::format("sunwarp{}", mapping.dots); 134 std::string tag = fmt::format("sunwarp{}", mapping.dots);
127 135
128 SubwaySunwarp fromWarp; 136 SubwaySunwarp fromWarp;
@@ -143,13 +151,14 @@ void SubwayMap::OnConnect() {
143 toWarp.type = SubwaySunwarpType::kExit; 151 toWarp.type = SubwaySunwarpType::kExit;
144 } 152 }
145 153
146 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); 154 entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
147 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp)); 155 exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
148 156
149 networks_.AddLinkToNetwork( 157 networks_.AddLinkToNetwork(
150 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp), 158 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp),
151 mapping.dots == 6 ? final_sunwarp_item 159 mapping.dots == 6 ? final_sunwarp_item
152 : GD_GetSubwayItemForSunwarp(toWarp)); 160 : GD_GetSubwayItemForSunwarp(toWarp),
161 false);
153 } 162 }
154 } 163 }
155 164
@@ -159,40 +168,61 @@ void SubwayMap::OnConnect() {
159 tag_it1++) { 168 tag_it1++) {
160 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); 169 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
161 tag_it2++) { 170 tag_it2++) {
162 networks_.AddLink(*tag_it1, *tag_it2); 171 // two links because tags are bi-directional
172 networks_.AddLink(*tag_it1, *tag_it2, true);
173 }
174 }
175 }
176
177 for (const auto &[tag, items] : entrances) {
178 if (!exits.contains(tag)) continue;
179 for (auto exit : exits[tag]) {
180 for (auto entrance : items) {
181 networks_.AddLink(entrance, exit, false);
163 } 182 }
164 } 183 }
165 } 184 }
166 185
167 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
168} 189}
169 190
170void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
192 if (AP_IsSunwarpShuffle()) {
193 sunwarp_mapping_ = AP_GetSunwarpMapping();
194 }
195
171 if (AP_IsPaintingShuffle()) { 196 if (AP_IsPaintingShuffle()) {
172 for (const std::string &painting_id : AP_GetCheckedPaintings()) { 197 std::map<std::string, std::string> painting_mapping =
198 AP_GetPaintingMapping();
199 std::set<std::string> remote_checked_paintings = AP_GetCheckedPaintings();
200
201 for (const std::string &painting_id : remote_checked_paintings) {
173 if (!checked_paintings_.count(painting_id)) { 202 if (!checked_paintings_.count(painting_id)) {
174 checked_paintings_.insert(painting_id); 203 checked_paintings_.insert(painting_id);
175 204
176 if (AP_GetPaintingMapping().count(painting_id)) { 205 if (painting_mapping.count(painting_id)) {
177 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id); 206 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
178 std::optional<int> to_id = GD_GetSubwayItemForPainting( 207 std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id));
179 AP_GetPaintingMapping().at(painting_id));
180 208
181 if (from_id && to_id) { 209 if (from_id && to_id) {
182 networks_.AddLink(*from_id, *to_id); 210 networks_.AddLink(*from_id, *to_id, false);
183 } 211 }
184 } 212 }
185 } 213 }
186 } 214 }
187 } 215 }
188 216
217 report_popup_->UpdateIndicators();
218
189 Redraw(); 219 Redraw();
190} 220}
191 221
192void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, 222void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
193 SubwaySunwarp to_sunwarp) { 223 SubwaySunwarp to_sunwarp) {
194 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), 224 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
195 GD_GetSubwayItemForSunwarp(to_sunwarp)); 225 GD_GetSubwayItemForSunwarp(to_sunwarp), false);
196} 226}
197 227
198void SubwayMap::Zoom(bool in) { 228void SubwayMap::Zoom(bool in) {
@@ -239,6 +269,9 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
239 SetZoomPos({zoom_x_, zoom_y_}); 269 SetZoomPos({zoom_x_, zoom_y_});
240 270
241 SetUpHelpButton(); 271 SetUpHelpButton();
272
273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
274 wxDefaultCoord, wxSIZE_AUTO);
242 } 275 }
243 276
244 wxBufferedPaintDC dc(this); 277 wxBufferedPaintDC dc(this);
@@ -294,79 +327,15 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
294 } 327 }
295 328
296 if (hovered_item_) { 329 if (hovered_item_) {
297 // Note that these requirements are duplicated on OnMouseClick so that it
298 // knows when an item has a hover effect.
299 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
300 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
301
302 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
303 const std::map<std::string, bool> &report =
304 GetDoorRequirements(*subway_door);
305
306 int acc_height = 10;
307 int col_width = 0;
308
309 for (const auto &[text, obtained] : report) {
310 wxSize item_extent = dc.GetTextExtent(text);
311 int item_height = std::max(32, item_extent.GetHeight()) + 10;
312 acc_height += item_height;
313
314 if (item_extent.GetWidth() > col_width) {
315 col_width = item_extent.GetWidth();
316 }
317 }
318
319 int item_width = col_width + 10 + 32;
320 int full_width = item_width + 20;
321
322 wxPoint popup_pos =
323 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
324 subway_item.y + AREA_ACTUAL_SIZE / 2});
325
326 if (popup_pos.x + full_width > GetSize().GetWidth()) {
327 popup_pos.x = GetSize().GetWidth() - full_width;
328 }
329 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
330 popup_pos.y = GetSize().GetHeight() - acc_height;
331 }
332
333 dc.SetPen(*wxTRANSPARENT_PEN);
334 dc.SetBrush(*wxBLACK_BRUSH);
335 dc.DrawRectangle(popup_pos, {full_width, acc_height});
336
337 dc.SetFont(GetFont());
338
339 int cur_height = 10;
340
341 for (const auto &[text, obtained] : report) {
342 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
343
344 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
345
346 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
347 wxSize item_extent = dc.GetTextExtent(text);
348 dc.DrawText(
349 text,
350 popup_pos +
351 wxPoint{10 + 32 + 10,
352 cur_height + (32 - dc.GetFontMetrics().height) / 2});
353
354 cur_height += 10 + 32;
355 }
356 }
357
358 if (networks_.IsItemInNetwork(*hovered_item_)) { 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
359 dc.SetBrush(*wxTRANSPARENT_BRUSH); 331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
360 332
361 for (const auto &[item_id1, item_id2] : 333 for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) {
362 networks_.GetNetworkGraph(*hovered_item_)) { 334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
363 const SubwayItem &item1 = GD_GetSubwayItem(item_id1); 335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
364 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
365 336
366 wxPoint item1_pos = MapPosToRenderPos( 337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
367 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); 338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
368 wxPoint item2_pos = MapPosToRenderPos(
369 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
370 339
371 int left = std::min(item1_pos.x, item2_pos.x); 340 int left = std::min(item1_pos.x, item2_pos.x);
372 int top = std::min(item1_pos.y, item2_pos.y); 341 int top = std::min(item1_pos.y, item2_pos.y);
@@ -381,6 +350,12 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
381 dc.DrawLine(item1_pos, item2_pos); 350 dc.DrawLine(item1_pos, item2_pos);
382 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); 351 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
383 dc.DrawLine(item1_pos, item2_pos); 352 dc.DrawLine(item1_pos, item2_pos);
353 if (!node.two_way) {
354 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
355 dc.SetBrush(*wxCYAN_BRUSH);
356 dc.DrawCircle(item2_pos, 4);
357 dc.SetBrush(*wxTRANSPARENT_BRUSH);
358 }
384 } else { 359 } else {
385 int ellipse_x; 360 int ellipse_x;
386 int ellipse_y; 361 int ellipse_y;
@@ -423,6 +398,12 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
423 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); 398 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
424 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, 399 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
425 halfheight * 2, start, end); 400 halfheight * 2, start, end);
401 if (!node.two_way) {
402 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
403 dc.SetBrush(*wxCYAN_BRUSH);
404 dc.DrawCircle(item2_pos, 4);
405 dc.SetBrush(*wxTRANSPARENT_BRUSH);
406 }
426 } 407 }
427 } 408 }
428 } 409 }
@@ -443,9 +424,7 @@ void SubwayMap::OnMouseMove(wxMouseEvent &event) {
443 } 424 }
444 425
445 if (!sticky_hover_ && actual_hover_ != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
446 hovered_item_ = actual_hover_; 427 EvaluateHover();
447
448 Refresh();
449 } 428 }
450 429
451 if (scroll_mode_) { 430 if (scroll_mode_) {
@@ -487,13 +466,11 @@ void SubwayMap::OnMouseClick(wxMouseEvent &event) {
487 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || 466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
488 networks_.IsItemInNetwork(*hovered_item_)) { 467 networks_.IsItemInNetwork(*hovered_item_)) {
489 if (actual_hover_ != hovered_item_) { 468 if (actual_hover_ != hovered_item_) {
490 hovered_item_ = actual_hover_; 469 EvaluateHover();
491 470
492 if (!hovered_item_) { 471 if (!hovered_item_) {
493 sticky_hover_ = false; 472 sticky_hover_ = false;
494 } 473 }
495
496 Refresh();
497 } else { 474 } else {
498 sticky_hover_ = !sticky_hover_; 475 sticky_hover_ = !sticky_hover_;
499 } 476 }
@@ -543,11 +520,13 @@ void SubwayMap::OnClickHelp(wxCommandEvent &event) {
543 "corner.\nClick on a side of the screen to start panning. It will follow " 520 "corner.\nClick on a side of the screen to start panning. It will follow "
544 "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 "
545 "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 "
546 "what it is connected to.\nIn painting shuffle, paintings that have not " 523 "what it is connected to.\nFor one-way connections, there will be a "
547 "yet been checked will not show their connections.\nA green shaded owl " 524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
548 "means that there is a painting entrance there.\nA red shaded owl means " 525 "that the painting is locked by a door.\nA blue circle means painting "
549 "that there are only painting exits there.\nClick on a door or " 526 "shuffle is enabled and the painting has not been checked yet.\nA black "
550 "warp to make the popup stick until you click again.", 527 "circle means the painting is not a warp.\nA green circle means that the "
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.",
551 "Subway Map Help"); 530 "Subway Map Help");
552} 531}
553 532
@@ -559,23 +538,37 @@ void SubwayMap::Redraw() {
559 538
560 wxGCDC gcdc(dc); 539 wxGCDC gcdc(dc);
561 540
541 std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping();
542
562 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
563 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
564 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
565 std::optional<wxColour> shade_color;
566 std::optional<int> subway_door = GetRealSubwayDoor(subway_item); 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
567 547
568 if (AP_HasEarlyColorHallways() && 548 if (AP_HasEarlyColorHallways() &&
569 subway_item.special == "starting_room_paintings") { 549 subway_item.special == "early_color_hallways") {
570 draw_type = ItemDrawType::kOwl; 550 draw_type = ItemDrawType::kOwl;
571 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 }
572 } else if (subway_item.special == "sun_painting") { 566 } else if (subway_item.special == "sun_painting") {
573 if (!AP_IsPilgrimageEnabled()) { 567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
574 if (IsDoorOpen(*subway_item.door)) { 569 if (IsDoorOpen(*subway_item.door)) {
575 draw_type = ItemDrawType::kOwl; 570 brush_color = wxGREEN_BRUSH;
576 shade_color = wxColour(0, 255, 0, 128);
577 } else { 571 } else {
578 draw_type = ItemDrawType::kBox;
579 brush_color = wxRED_BRUSH; 572 brush_color = wxRED_BRUSH;
580 } 573 }
581 } 574 }
@@ -589,41 +582,28 @@ void SubwayMap::Redraw() {
589 } else { 582 } else {
590 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
591 } 584 }
592 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
593 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
594 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
595 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
596 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
597 bool has_codomain_painting = false; 590 if (!checked_paintings_.count(*subway_item.painting)) {
598 591 draw_type = ItemDrawType::kOwl;
599 for (const std::string &painting_id : subway_item.paintings) { 592 brush_color = wxBLUE_BRUSH;
600 if (checked_paintings_.count(painting_id)) { 593 } else if (painting_mapping.count(*subway_item.painting)) {
601 has_checked_painting = true; 594 draw_type = ItemDrawType::kOwl;
602 595 brush_color = wxGREEN_BRUSH;
603 if (AP_GetPaintingMapping().count(painting_id)) { 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
604 has_mapped_painting = true; 597 draw_type = ItemDrawType::kOwlExit;
605 } else if (AP_IsPaintingMappedTo(painting_id)) { 598 brush_color = wxGREEN_BRUSH;
606 has_codomain_painting = true;
607 }
608 } else {
609 has_unchecked_painting = true;
610 }
611 } 599 }
612 600 } else if (subway_item.HasWarps()) {
613 if (has_unchecked_painting || has_mapped_painting || 601 brush_color = wxGREEN_BRUSH;
614 has_codomain_painting) { 602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
615 draw_type = ItemDrawType::kOwl; 605 draw_type = ItemDrawType::kOwl;
616
617 if (has_checked_painting) {
618 if (has_mapped_painting) {
619 shade_color = wxColour(0, 255, 0, 128);
620 } else {
621 shade_color = wxColour(255, 0, 0, 128);
622 }
623 }
624 } 606 }
625 } else if (!subway_item.tags.empty()) {
626 draw_type = ItemDrawType::kOwl;
627 } 607 }
628 } else if (subway_door) { 608 } else if (subway_door) {
629 draw_type = ItemDrawType::kBox; 609 draw_type = ItemDrawType::kBox;
@@ -643,21 +623,40 @@ void SubwayMap::Redraw() {
643 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
644 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
645 gcdc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
646 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 626
647 } else if (draw_type == ItemDrawType::kOwl) { 627 if (subway_item.tilted) {
648 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( 628 constexpr int AREA_TILTED_SIDE =
649 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
650 gcdc.DrawBitmap(owl_bitmap, real_area_pos); 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
651 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
652 if (shade_color) { 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
653 gcdc.SetBrush(wxBrush(*shade_color)); 633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
654 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
655 } 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 }
656 } 653 }
657 } 654 }
658} 655}
659 656
660void SubwayMap::SetUpHelpButton() { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
661 help_button_->SetPosition({ 660 help_button_->SetPosition({
662 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, 661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
663 15, 662 15,
@@ -693,6 +692,51 @@ void SubwayMap::EvaluateScroll(wxPoint pos) {
693 SetScrollSpeed(scroll_x, scroll_y); 692 SetScrollSpeed(scroll_x, scroll_y);
694} 693}
695 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
696wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { 740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
697 return {static_cast<int>(pos.x * render_width_ * zoom_ / 741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
698 map_image_.GetSize().GetWidth() + 742 map_image_.GetSize().GetWidth() +
@@ -762,8 +806,33 @@ void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
762 zoom_slider_->SetValue((zoom - 1.0) / 0.25); 806 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
763} 807}
764 808
809std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) {
810 if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
811 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
812 int sunwarp_index = subway_item.sunwarp->dots - 1;
813 if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) {
814 sunwarp_index += 6;
815 }
816
817 for (const auto &[start_index, mapping] : sunwarp_mapping_) {
818 if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
819 return GD_GetSunwarpDoors().at(mapping.dots - 1);
820 }
821 }
822 }
823
824 return subway_item.door;
825}
826
765quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { 827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
766 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
767 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
768 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 }
769} 838}
diff --git a/src/subway_map.h b/src/subway_map.h index feee8ff..b04c2fd 100644 --- a/src/subway_map.h +++ b/src/subway_map.h
@@ -15,9 +15,12 @@
15 15
16#include <quadtree/Quadtree.h> 16#include <quadtree/Quadtree.h>
17 17
18#include "ap_state.h"
18#include "game_data.h" 19#include "game_data.h"
19#include "network_set.h" 20#include "network_set.h"
20 21
22class ReportPopup;
23
21class SubwayMap : public wxPanel { 24class SubwayMap : public wxPanel {
22 public: 25 public:
23 SubwayMap(wxWindow *parent); 26 SubwayMap(wxWindow *parent);
@@ -45,15 +48,16 @@ class SubwayMap : public wxPanel {
45 wxPoint RenderPosToMapPos(wxPoint pos) const; 48 wxPoint RenderPosToMapPos(wxPoint pos) const;
46 49
47 void EvaluateScroll(wxPoint pos); 50 void EvaluateScroll(wxPoint pos);
51 void EvaluateHover();
48 52
49 void SetZoomPos(wxPoint pos); 53 void SetZoomPos(wxPoint pos);
50 void SetScrollSpeed(int scroll_x, int scroll_y); 54 void SetScrollSpeed(int scroll_x, int scroll_y);
51 void SetZoom(double zoom, wxPoint static_point); 55 void SetZoom(double zoom, wxPoint static_point);
52 56
57 std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item);
58
53 wxImage map_image_; 59 wxImage map_image_;
54 wxImage owl_image_; 60 wxImage owl_image_;
55 wxBitmap unchecked_eye_;
56 wxBitmap checked_eye_;
57 61
58 wxBitmap rendered_; 62 wxBitmap rendered_;
59 int render_x_ = 0; 63 int render_x_ = 0;
@@ -85,8 +89,13 @@ class SubwayMap : public wxPanel {
85 std::optional<int> actual_hover_; 89 std::optional<int> actual_hover_;
86 bool sticky_hover_ = false; 90 bool sticky_hover_ = false;
87 91
92 ReportPopup *report_popup_;
93
88 NetworkSet networks_; 94 NetworkSet networks_;
89 std::set<std::string> checked_paintings_; 95 std::set<std::string> checked_paintings_;
96
97 // Cached from APState.
98 std::map<int, SunwarpMapping> sunwarp_mapping_;
90}; 99};
91 100
92#endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */ 101#endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */
diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 85164d5..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"]) {
@@ -27,6 +29,11 @@ void TrackerConfig::Load() {
27 }); 29 });
28 } 30 }
29 } 31 }
32
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>());
30 } catch (const std::exception&) { 37 } catch (const std::exception&) {
31 // It's fine if the file can't be loaded. 38 // It's fine if the file can't be loaded.
32 } 39 }
@@ -40,7 +47,6 @@ void TrackerConfig::Save() {
40 output["asked_to_check_for_updates"] = asked_to_check_for_updates; 47 output["asked_to_check_for_updates"] = asked_to_check_for_updates;
41 output["should_check_for_updates"] = should_check_for_updates; 48 output["should_check_for_updates"] = should_check_for_updates;
42 output["hybrid_areas"] = hybrid_areas; 49 output["hybrid_areas"] = hybrid_areas;
43 output["show_hunt_panels"] = show_hunt_panels;
44 50
45 output.remove("connection_history"); 51 output.remove("connection_history");
46 for (const ConnectionDetails& details : connection_history) { 52 for (const ConnectionDetails& details : connection_history) {
@@ -52,6 +58,10 @@ void TrackerConfig::Save() {
52 output["connection_history"].push_back(connection); 58 output["connection_history"].push_back(connection);
53 } 59 }
54 60
61 output["ipc_address"] = ipc_address;
62 output["track_position"] = track_position;
63 output["visible_panels"] = static_cast<int>(visible_panels);
64
55 std::ofstream filewriter(filename_); 65 std::ofstream filewriter(filename_);
56 filewriter << output; 66 filewriter << output;
57} 67}
diff --git a/src/tracker_config.h b/src/tracker_config.h index a1a6c1d..df4105d 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h
@@ -23,12 +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;
37 std::string ipc_address;
38 bool track_position = true;
39 VisiblePanels visible_panels = kLOCATIONS_ONLY;
32 40
33 private: 41 private:
34 std::string filename_; 42 std::string filename_;
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 3b6beda..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -1,37 +1,64 @@
1#include "tracker_frame.h" 1#include "tracker_frame.h"
2 2
3#include <fmt/core.h>
3#include <wx/aboutdlg.h> 4#include <wx/aboutdlg.h>
4#include <wx/choicebk.h> 5#include <wx/choicebk.h>
5#include <wx/filedlg.h> 6#include <wx/filedlg.h>
6#include <wx/notebook.h> 7#include <wx/notebook.h>
8#include <wx/splitter.h>
7#include <wx/stdpaths.h> 9#include <wx/stdpaths.h>
8#include <wx/webrequest.h> 10#include <wx/webrequest.h>
9 11
10#include <fmt/core.h> 12#include <algorithm>
11#include <nlohmann/json.hpp> 13#include <nlohmann/json.hpp>
12#include <sstream> 14#include <sstream>
13 15
14#include "achievements_pane.h" 16#include "achievements_pane.h"
15#include "ap_state.h" 17#include "ap_state.h"
16#include "connection_dialog.h" 18#include "connection_dialog.h"
19#include "ipc_dialog.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"
17#include "settings_dialog.h" 26#include "settings_dialog.h"
18#include "subway_map.h" 27#include "subway_map.h"
19#include "tracker_config.h" 28#include "tracker_config.h"
20#include "tracker_panel.h" 29#include "tracker_panel.h"
21#include "version.h" 30#include "version.h"
22 31
32namespace {
33
34std::string GetStatusMessage() {
35 std::string msg = AP_GetStatusMessage();
36
37 std::optional<std::string> ipc_msg = IPC_GetStatusMessage();
38 if (ipc_msg) {
39 msg += " ";
40 msg += *ipc_msg;
41 }
42
43 return msg;
44}
45
46} // namespace
47
23enum TrackerFrameIds { 48enum TrackerFrameIds {
24 ID_CONNECT = 1, 49 ID_AP_CONNECT = 1,
25 ID_CHECK_FOR_UPDATES = 2, 50 ID_CHECK_FOR_UPDATES = 2,
26 ID_SETTINGS = 3, 51 ID_SETTINGS = 3,
27 ID_ZOOM_IN = 4, 52 ID_ZOOM_IN = 4,
28 ID_ZOOM_OUT = 5, 53 ID_ZOOM_OUT = 5,
29 ID_OPEN_SAVE_FILE = 6, 54 ID_IPC_CONNECT = 7,
55 ID_LOG_DIALOG = 8,
30}; 56};
31 57
32wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); 58wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
33wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 59wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent);
34wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 60wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
61wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent);
35 62
36TrackerFrame::TrackerFrame() 63TrackerFrame::TrackerFrame()
37 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, 64 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
@@ -39,16 +66,24 @@ TrackerFrame::TrackerFrame()
39 ::wxInitAllImageHandlers(); 66 ::wxInitAllImageHandlers();
40 67
41 AP_SetTrackerFrame(this); 68 AP_SetTrackerFrame(this);
69 IPC_SetTrackerFrame(this);
70
71 SetTheIconCache(&icons_);
72
73 updater_ = std::make_unique<Updater>(this);
74 updater_->Cleanup();
42 75
43 wxMenu *menuFile = new wxMenu(); 76 wxMenu *menuFile = new wxMenu();
44 menuFile->Append(ID_CONNECT, "&Connect"); 77 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
45 menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O"); 78 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
46 menuFile->Append(ID_SETTINGS, "&Settings"); 79 menuFile->Append(ID_SETTINGS, "&Settings");
47 menuFile->Append(wxID_EXIT); 80 menuFile->Append(wxID_EXIT);
48 81
49 wxMenu *menuView = new wxMenu(); 82 wxMenu *menuView = new wxMenu();
50 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-+");
51 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");
52 87
53 zoom_in_menu_item_->Enable(false); 88 zoom_in_menu_item_->Enable(false);
54 zoom_out_menu_item_->Enable(false); 89 zoom_out_menu_item_->Enable(false);
@@ -65,38 +100,53 @@ TrackerFrame::TrackerFrame()
65 SetMenuBar(menuBar); 100 SetMenuBar(menuBar);
66 101
67 CreateStatusBar(); 102 CreateStatusBar();
68 SetStatusText("Not connected to Archipelago.");
69 103
70 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); 104 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
71 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); 105 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
72 Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT); 106 Bind(wxEVT_MENU, &TrackerFrame::OnApConnect, this, ID_AP_CONNECT);
107 Bind(wxEVT_MENU, &TrackerFrame::OnIpcConnect, this, ID_IPC_CONNECT);
73 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); 108 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
74 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, 109 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
75 ID_CHECK_FOR_UPDATES); 110 ID_CHECK_FOR_UPDATES);
76 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); 111 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
77 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);
78 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); 114 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
79 Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE); 115 Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged,
116 this);
80 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); 117 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
81 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 118 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
82 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); 119 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
120 Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this);
121
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);
83 128
84 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY);
85 achievements_pane_ = new AchievementsPane(choicebook); 129 achievements_pane_ = new AchievementsPane(choicebook);
86 choicebook->AddPage(achievements_pane_, "Achievements"); 130 choicebook->AddPage(achievements_pane_, "Achievements");
87 131
88 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);
89 tracker_panel_ = new TrackerPanel(notebook_); 142 tracker_panel_ = new TrackerPanel(notebook_);
90 subway_map_ = new SubwayMap(notebook_); 143 subway_map_ = new SubwayMap(notebook_);
91 notebook_->AddPage(tracker_panel_, "Map"); 144 notebook_->AddPage(tracker_panel_, "Map");
92 notebook_->AddPage(subway_map_, "Subway"); 145 notebook_->AddPage(subway_map_, "Subway");
93 146
94 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 147 splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4);
95 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
96 top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3));
97 148
98 SetSizerAndFit(top_sizer); 149 SetSize(logicalSize);
99 SetSize(1280, 728);
100 150
101 if (!GetTrackerConfig().asked_to_check_for_updates) { 151 if (!GetTrackerConfig().asked_to_check_for_updates) {
102 GetTrackerConfig().asked_to_check_for_updates = true; 152 GetTrackerConfig().asked_to_check_for_updates = true;
@@ -113,23 +163,28 @@ TrackerFrame::TrackerFrame()
113 } 163 }
114 164
115 if (GetTrackerConfig().should_check_for_updates) { 165 if (GetTrackerConfig().should_check_for_updates) {
116 CheckForUpdates(/*manual=*/false); 166 updater_->CheckForUpdates(/*invisible=*/true);
117 } 167 }
168
169 SetStatusText(GetStatusMessage());
118} 170}
119 171
120void TrackerFrame::SetStatusMessage(std::string message) { 172void TrackerFrame::ConnectToAp(std::string server, std::string user,
121 wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED); 173 std::string pass) {
122 event->SetString(message.c_str()); 174 QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server),
175 std::move(user), std::move(pass)));
176}
123 177
124 QueueEvent(event); 178void TrackerFrame::UpdateStatusMessage() {
179 QueueEvent(new wxCommandEvent(STATUS_CHANGED));
125} 180}
126 181
127void TrackerFrame::ResetIndicators() { 182void TrackerFrame::ResetIndicators() {
128 QueueEvent(new wxCommandEvent(STATE_RESET)); 183 QueueEvent(new wxCommandEvent(STATE_RESET));
129} 184}
130 185
131void TrackerFrame::UpdateIndicators() { 186void TrackerFrame::UpdateIndicators(StateUpdate state) {
132 QueueEvent(new wxCommandEvent(STATE_CHANGED)); 187 QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state)));
133} 188}
134 189
135void TrackerFrame::OnAbout(wxCommandEvent &event) { 190void TrackerFrame::OnAbout(wxCommandEvent &event) {
@@ -137,6 +192,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
137 about_info.SetName("Lingo Archipelago Tracker"); 192 about_info.SetName("Lingo Archipelago Tracker");
138 about_info.SetVersion(kTrackerVersion.ToString()); 193 about_info.SetVersion(kTrackerVersion.ToString());
139 about_info.AddDeveloper("hatkirby"); 194 about_info.AddDeveloper("hatkirby");
195 about_info.AddDeveloper("art0007i");
140 about_info.AddArtist("Brenton Wildes"); 196 about_info.AddArtist("Brenton Wildes");
141 about_info.AddArtist("kinrah"); 197 about_info.AddArtist("kinrah");
142 198
@@ -145,7 +201,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
145 201
146void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } 202void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
147 203
148void TrackerFrame::OnConnect(wxCommandEvent &event) { 204void TrackerFrame::OnApConnect(wxCommandEvent &event) {
149 ConnectionDialog dlg; 205 ConnectionDialog dlg;
150 206
151 if (dlg.ShowModal() == wxID_OK) { 207 if (dlg.ShowModal() == wxID_OK) {
@@ -175,6 +231,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
175 } 231 }
176} 232}
177 233
234void TrackerFrame::OnIpcConnect(wxCommandEvent &event) {
235 IpcDialog dlg;
236
237 if (dlg.ShowModal() == wxID_OK) {
238 GetTrackerConfig().ipc_address = dlg.GetIpcAddress();
239 GetTrackerConfig().Save();
240
241 IPC_Connect(dlg.GetIpcAddress());
242 }
243}
244
178void TrackerFrame::OnSettings(wxCommandEvent &event) { 245void TrackerFrame::OnSettings(wxCommandEvent &event) {
179 SettingsDialog dlg; 246 SettingsDialog dlg;
180 247
@@ -182,15 +249,18 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
182 GetTrackerConfig().should_check_for_updates = 249 GetTrackerConfig().should_check_for_updates =
183 dlg.GetShouldCheckForUpdates(); 250 dlg.GetShouldCheckForUpdates();
184 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); 251 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
185 GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); 252 GetTrackerConfig().visible_panels = dlg.GetVisiblePanels();
253 GetTrackerConfig().track_position = dlg.GetTrackPosition();
186 GetTrackerConfig().Save(); 254 GetTrackerConfig().Save();
187 255
188 UpdateIndicators(); 256 UpdateIndicators(StateUpdate{.cleared_locations = true,
257 .player_position = true,
258 .changed_settings = true});
189 } 259 }
190} 260}
191 261
192void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 262void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
193 CheckForUpdates(/*manual=*/true); 263 updater_->CheckForUpdates(/*invisible=*/false);
194} 264}
195 265
196void TrackerFrame::OnZoomIn(wxCommandEvent &event) { 266void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
@@ -199,112 +269,99 @@ void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
199 } 269 }
200} 270}
201 271
202void TrackerFrame::OnZoomOut(wxCommandEvent& event) { 272void TrackerFrame::OnZoomOut(wxCommandEvent &event) {
203 if (notebook_->GetSelection() == 1) { 273 if (notebook_->GetSelection() == 1) {
204 subway_map_->Zoom(false); 274 subway_map_->Zoom(false);
205 } 275 }
206} 276}
207 277
208void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { 278void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) {
209 zoom_in_menu_item_->Enable(event.GetSelection() == 1); 279 if (log_dialog_ == nullptr) {
210 zoom_out_menu_item_->Enable(event.GetSelection() == 1); 280 log_dialog_ = new LogDialog(this);
211} 281 log_dialog_->Show();
282 TrackerSetLogDialog(log_dialog_);
212 283
213void TrackerFrame::OnOpenFile(wxCommandEvent& event) { 284 log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow,
214 wxFileDialog open_file_dialog( 285 this);
215 this, "Open Lingo Save File", 286 } else {
216 fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable", 287 log_dialog_->SetFocus();
217 wxStandardPaths::Get().GetUserConfigDir().ToStdString()),
218 AP_GetSaveName(), "Lingo save file (*.save)|*.save",
219 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
220 if (open_file_dialog.ShowModal() == wxID_CANCEL) {
221 return;
222 } 288 }
289}
223 290
224 std::string savedata_path = open_file_dialog.GetPath().ToStdString(); 291void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) {
292 TrackerSetLogDialog(nullptr);
293 log_dialog_ = nullptr;
225 294
226 if (panels_panel_ == nullptr) { 295 event.Skip();
227 panels_panel_ = new TrackerPanel(notebook_); 296}
228 notebook_->AddPage(panels_panel_, "Panels");
229 }
230 297
231 notebook_->SetSelection(notebook_->FindPage(panels_panel_)); 298void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
232 panels_panel_->SetSavedataPath(savedata_path); 299 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
300 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
233} 301}
234 302
235void TrackerFrame::OnStateReset(wxCommandEvent& event) { 303void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) {
236 tracker_panel_->UpdateIndicators(); 304 notebook_->Refresh();
237 achievements_pane_->UpdateIndicators();
238 subway_map_->OnConnect();
239 if (panels_panel_ != nullptr) {
240 notebook_->DeletePage(notebook_->FindPage(panels_panel_));
241 panels_panel_ = nullptr;
242 }
243 Refresh();
244} 305}
245 306
246void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 307void TrackerFrame::OnStateReset(wxCommandEvent &event) {
247 tracker_panel_->UpdateIndicators(); 308 tracker_panel_->UpdateIndicators(/*reset=*/true);
248 achievements_pane_->UpdateIndicators(); 309 achievements_pane_->UpdateIndicators();
249 subway_map_->UpdateIndicators(); 310 items_pane_->ResetIndicators();
250 if (panels_panel_ != nullptr) { 311 options_pane_->OnConnect();
251 panels_panel_->UpdateIndicators(); 312 paintings_pane_->ResetIndicators();
252 } 313 subway_map_->OnConnect();
253 Refresh(); 314 Refresh();
254} 315}
255 316
256void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { 317void TrackerFrame::OnStateChanged(StateChangedEvent &event) {
257 SetStatusText(event.GetString()); 318 const StateUpdate &state = event.GetState();
258} 319
320 bool hunt_panels = false;
321 if (GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) {
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);
336 subway_map_->UpdateIndicators();
337 Refresh();
338 } else if (state.player_position && GetTrackerConfig().track_position) {
339 if (notebook_->GetSelection() == 0) {
340 tracker_panel_->Refresh();
341 }
342 }
259 343
260void TrackerFrame::CheckForUpdates(bool manual) { 344 if (std::any_of(state.panels.begin(), state.panels.end(),
261 wxWebRequest request = wxWebSession::GetDefault().CreateRequest( 345 [](int solve_index) {
262 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); 346 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index))
347 .achievement;
348 })) {
349 achievements_pane_->UpdateIndicators();
350 }
263 351
264 if (!request.IsOk()) { 352 if (!state.items.empty()) {
265 if (manual) { 353 items_pane_->UpdateIndicators(state.items);
266 wxMessageBox("Could not check for updates.", "Error", 354 }
267 wxOK | wxICON_ERROR);
268 } else {
269 SetStatusText("Could not check for updates.");
270 }
271 355
272 return; 356 if (!state.paintings.empty()) {
357 paintings_pane_->UpdateIndicators(state.paintings);
273 } 358 }
359}
274 360
275 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { 361void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
276 if (evt.GetState() == wxWebRequest::State_Completed) { 362 SetStatusText(wxString::FromUTF8(GetStatusMessage()));
277 std::string response = evt.GetResponse().AsString().ToStdString(); 363}
278
279 Version latest_version(response);
280 if (kTrackerVersion < latest_version) {
281 std::ostringstream message_text;
282 message_text << "There is a newer version of Lingo AP Tracker "
283 "available. You have "
284 << kTrackerVersion.ToString()
285 << ", and the latest version is "
286 << latest_version.ToString()
287 << ". Would you like to update?";
288
289 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
290 wxYES) {
291 wxLaunchDefaultBrowser(
292 "https://code.fourisland.com/lingo-ap-tracker/about/"
293 "CHANGELOG.md");
294 }
295 } else if (manual) {
296 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
297 wxOK);
298 }
299 } else if (evt.GetState() == wxWebRequest::State_Failed) {
300 if (manual) {
301 wxMessageBox("Could not check for updates.", "Error",
302 wxOK | wxICON_ERROR);
303 } else {
304 SetStatusText("Could not check for updates.");
305 }
306 }
307 });
308 364
309 request.Start(); 365void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
366 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
310} 367}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index 19bd0b3..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,50 +7,121 @@
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;
28
29class ApConnectEvent : public wxEvent {
30 public:
31 ApConnectEvent(wxEventType eventType, int winid, std::string server,
32 std::string user, std::string pass)
33 : wxEvent(winid, eventType),
34 ap_server_(std::move(server)),
35 ap_user_(std::move(user)),
36 ap_pass_(std::move(pass)) {}
37
38 const std::string &GetServer() const { return ap_server_; }
39
40 const std::string &GetUser() const { return ap_user_; }
41
42 const std::string &GetPass() const { return ap_pass_; }
43
44 virtual wxEvent *Clone() const { return new ApConnectEvent(*this); }
45
46 private:
47 std::string ap_server_;
48 std::string ap_user_;
49 std::string ap_pass_;
50};
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};
15 74
16wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); 75wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
17wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 76wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent);
18wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 77wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
78wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent);
19 79
20class TrackerFrame : public wxFrame { 80class TrackerFrame : public wxFrame {
21 public: 81 public:
22 TrackerFrame(); 82 TrackerFrame();
23 83
24 void SetStatusMessage(std::string message); 84 void ConnectToAp(std::string server, std::string user, std::string pass);
85 void UpdateStatusMessage();
25 86
26 void ResetIndicators(); 87 void ResetIndicators();
27 void UpdateIndicators(); 88 void UpdateIndicators(StateUpdate state);
28 89
29 private: 90 private:
30 void OnExit(wxCommandEvent &event); 91 void OnExit(wxCommandEvent &event);
31 void OnAbout(wxCommandEvent &event); 92 void OnAbout(wxCommandEvent &event);
32 void OnConnect(wxCommandEvent &event); 93 void OnApConnect(wxCommandEvent &event);
94 void OnIpcConnect(wxCommandEvent &event);
33 void OnSettings(wxCommandEvent &event); 95 void OnSettings(wxCommandEvent &event);
34 void OnCheckForUpdates(wxCommandEvent &event); 96 void OnCheckForUpdates(wxCommandEvent &event);
35 void OnZoomIn(wxCommandEvent &event); 97 void OnZoomIn(wxCommandEvent &event);
36 void OnZoomOut(wxCommandEvent &event); 98 void OnZoomOut(wxCommandEvent &event);
99 void OnOpenLogWindow(wxCommandEvent &event);
100 void OnCloseLogWindow(wxCloseEvent &event);
37 void OnChangePage(wxBookCtrlEvent &event); 101 void OnChangePage(wxBookCtrlEvent &event);
38 void OnOpenFile(wxCommandEvent &event); 102 void OnSashPositionChanged(wxSplitterEvent &event);
39 103
40 void OnStateReset(wxCommandEvent &event); 104 void OnStateReset(wxCommandEvent &event);
41 void OnStateChanged(wxCommandEvent &event); 105 void OnStateChanged(StateChangedEvent &event);
42 void OnStatusChanged(wxCommandEvent &event); 106 void OnStatusChanged(wxCommandEvent &event);
107 void OnConnectToAp(ApConnectEvent &event);
108
109 std::unique_ptr<Updater> updater_;
43 110
44 void CheckForUpdates(bool manual); 111 wxSplitterWindow *splitter_window_;
45
46 wxNotebook *notebook_; 112 wxNotebook *notebook_;
47 TrackerPanel *tracker_panel_; 113 TrackerPanel *tracker_panel_;
48 AchievementsPane *achievements_pane_; 114 AchievementsPane *achievements_pane_;
115 ItemsPane *items_pane_;
116 OptionsPane *options_pane_;
117 PaintingsPane *paintings_pane_;
49 SubwayMap *subway_map_; 118 SubwayMap *subway_map_;
50 TrackerPanel *panels_panel_ = nullptr; 119 LogDialog *log_dialog_ = nullptr;
51 120
52 wxMenuItem *zoom_in_menu_item_; 121 wxMenuItem *zoom_in_menu_item_;
53 wxMenuItem *zoom_out_menu_item_; 122 wxMenuItem *zoom_out_menu_item_;
123
124 IconCache icons_;
54}; 125};
55 126
56#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 27e825a..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -9,7 +9,7 @@
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" 12#include "ipc_state.h"
13#include "tracker_config.h" 13#include "tracker_config.h"
14#include "tracker_state.h" 14#include "tracker_state.h"
15 15
@@ -43,67 +43,74 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
43 areas_.push_back(area); 43 areas_.push_back(area);
44 } 44 }
45 45
46 Resize();
46 Redraw(); 47 Redraw();
47 48
48 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); 49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
49 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); 50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
50} 51}
51 52
52void TrackerPanel::UpdateIndicators() { 53void TrackerPanel::UpdateIndicators(bool reset) {
53 for (AreaIndicator &area : areas_) { 54 if (reset) {
54 area.popup->UpdateIndicators(); 55 for (AreaIndicator &area : areas_) {
55 } 56 const MapArea &map_area = GD_GetMapArea(area.area_id);
56 57
57 Redraw(); 58 if ((!AP_IsLocationVisible(map_area.classification) ||
58} 59 IsAreaPostgame(area.area_id)) &&
59 60 !(map_area.hunt &&
60void TrackerPanel::SetSavedataPath(std::string savedata_path) { 61 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
61 if (!panels_mode_) { 62 !(map_area.has_single_panel &&
62 wxButton *refresh_button = new wxButton(this, wxID_ANY, "Refresh", {15, 15}); 63 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) &&
63 refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this); 64 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
64 } 65 area.active = false;
65 66 } else {
66 savedata_path_ = savedata_path; 67 area.active = true;
67 panels_mode_ = true; 68 }
68
69 RefreshSavedata();
70}
71 69
72void TrackerPanel::RefreshSavedata() { 70 area.popup->ResetIndicators();
73 solved_panels_.clear(); 71 }
74 72
75 GodotVariant godot_variant = ParseGodotFile(*savedata_path_); 73 Resize();
76 for (const GodotVariant &panel_node : godot_variant.AsArray()) { 74 } else {
77 const std::vector<GodotVariant> &fields = panel_node.AsArray(); 75 for (AreaIndicator &area : areas_) {
78 if (fields[1].AsBool()) { 76 area.popup->UpdateIndicators();
79 const std::vector<std::string> &nodepath = fields[0].AsNodePath();
80 std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]);
81 solved_panels_.insert(key);
82 } 77 }
83 } 78 }
84 79
85 UpdateIndicators(); 80 Redraw();
86 Refresh();
87} 81}
88 82
89void TrackerPanel::OnPaint(wxPaintEvent &event) { 83void TrackerPanel::OnPaint(wxPaintEvent &event) {
90 if (GetSize() != rendered_.GetSize()) { 84 if (GetSize() != rendered_.GetSize()) {
85 Resize();
91 Redraw(); 86 Redraw();
92 } 87 }
93 88
94 wxBufferedPaintDC dc(this); 89 wxBufferedPaintDC dc(this);
95 dc.DrawBitmap(rendered_, 0, 0); 90 dc.DrawBitmap(rendered_, 0, 0);
96 91
97 if (AP_GetPlayerPosition().has_value()) { 92 std::optional<std::tuple<int, int>> player_position;
93 if (GetTrackerConfig().track_position)
94 {
95 if (IPC_IsConnected()) {
96 player_position = IPC_GetPlayerPosition();
97 } else {
98 player_position = AP_GetPlayerPosition();
99 }
100 }
101
102 if (player_position.has_value()) {
98 // 1588, 1194 103 // 1588, 1194
99 // 14x14 -> 154x154 104 // 14x14 -> 154x154
100 double intended_x = 105 double intended_x =
101 1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 106 1588.0 + (std::get<0>(*player_position) * (154.0 / 14.0));
102 double intended_y = 107 double intended_y =
103 1194.0 + (std::get<1>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 108 1194.0 + (std::get<1>(*player_position) * (154.0 / 14.0));
104 109
105 int real_x = offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; 110 int real_x =
106 int real_y = offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2; 111 offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2;
112 int real_y =
113 offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2;
107 114
108 dc.DrawBitmap(scaled_player_, real_x, real_y); 115 dc.DrawBitmap(scaled_player_, real_x, real_y);
109 } 116 }
@@ -125,12 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
125 event.Skip(); 132 event.Skip();
126} 133}
127 134
128void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { 135void TrackerPanel::Resize() {
129 RefreshSavedata(); 136 wxSize panel_size = GetClientSize();
130}
131
132void TrackerPanel::Redraw() {
133 wxSize panel_size = GetSize();
134 wxSize image_size = map_image_.GetSize(); 137 wxSize image_size = map_image_.GetSize();
135 138
136 int final_x = 0; 139 int final_x = 0;
@@ -149,7 +152,7 @@ void TrackerPanel::Redraw() {
149 final_x = (panel_size.GetWidth() - final_width) / 2; 152 final_x = (panel_size.GetWidth() - final_width) / 2;
150 } 153 }
151 154
152 rendered_ = wxBitmap( 155 scaled_map_ = wxBitmap(
153 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) 156 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
154 .Size(panel_size, {final_x, final_y}, 0, 0, 0)); 157 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
155 158
@@ -164,30 +167,61 @@ void TrackerPanel::Redraw() {
164 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, 167 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1,
165 player_height > 0 ? player_height : 1)); 168 player_height > 0 ? player_height : 1));
166 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
167 wxMemoryDC dc; 212 wxMemoryDC dc;
168 dc.SelectObject(rendered_); 213 dc.SelectObject(rendered_);
169 214
170 int real_area_size =
171 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
172 int actual_border_size = 215 int actual_border_size =
173 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; 216 real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
174 const wxPoint upper_left_triangle[] = { 217 const wxPoint upper_left_triangle[] = {
175 {0, 0}, {0, real_area_size}, {real_area_size, 0}}; 218 {0, 0}, {0, real_area_size_}, {real_area_size_, 0}};
176 const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, 219 const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1},
177 {real_area_size - 1, 0}, 220 {real_area_size_ - 1, 0},
178 {real_area_size, real_area_size}}; 221 {real_area_size_, real_area_size_}};
179 222
180 for (AreaIndicator &area : areas_) { 223 for (AreaIndicator &area : areas_) {
181 const MapArea &map_area = GD_GetMapArea(area.area_id); 224 const MapArea &map_area = GD_GetMapArea(area.area_id);
182 if (panels_mode_) {
183 area.active = map_area.has_single_panel;
184 } else if (!AP_IsLocationVisible(map_area.classification) &&
185 !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
186 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
187 area.active = false;
188 } else {
189 area.active = true;
190 }
191 225
192 if (!area.active) { 226 if (!area.active) {
193 continue; 227 continue;
@@ -199,19 +233,15 @@ void TrackerPanel::Redraw() {
199 bool has_unchecked = false; 233 bool has_unchecked = false;
200 if (IsLocationWinCondition(section)) { 234 if (IsLocationWinCondition(section)) {
201 has_unchecked = !AP_HasReachedGoal(); 235 has_unchecked = !AP_HasReachedGoal();
202 } else if (panels_mode_) { 236 } else if (AP_IsLocationVisible(section.classification) &&
203 if (section.single_panel) { 237 !IsLocationPostgame(section.ap_location_id)) {
204 const Panel &panel = GD_GetPanel(*section.single_panel);
205 if (panel.non_counting) {
206 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
207 } else {
208 has_unchecked = !solved_panels_.contains(panel.nodepath);
209 }
210 }
211 } else if (AP_IsLocationVisible(section.classification)) {
212 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); 238 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
213 } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { 239 } else if ((section.hunt && GetTrackerConfig().visible_panels ==
214 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);
215 } 245 }
216 246
217 if (has_unchecked) { 247 if (has_unchecked) {
@@ -223,8 +253,12 @@ void TrackerPanel::Redraw() {
223 } 253 }
224 } 254 }
225 255
226 if (AP_IsPaintingShuffle() && !panels_mode_) { 256 if (AP_IsPaintingShuffle()) {
227 for (int painting_id : map_area.paintings) { 257 for (int painting_id : map_area.paintings) {
258 if (IsPaintingPostgame(painting_id)) {
259 continue;
260 }
261
228 const PaintingExit &painting = GD_GetPaintingExit(painting_id); 262 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
229 bool reachable = IsPaintingReachable(painting_id); 263 bool reachable = IsPaintingReachable(painting_id);
230 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { 264 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) {
@@ -237,10 +271,8 @@ void TrackerPanel::Redraw() {
237 } 271 }
238 } 272 }
239 273
240 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 274 int real_area_x = area.real_x1;
241 final_width / image_size.GetWidth(); 275 int real_area_y = area.real_y1;
242 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
243 final_width / image_size.GetWidth();
244 276
245 if (has_reachable_unchecked && has_unreachable_unchecked && 277 if (has_reachable_unchecked && has_unreachable_unchecked &&
246 GetTrackerConfig().hybrid_areas) { 278 GetTrackerConfig().hybrid_areas) {
@@ -254,7 +286,7 @@ void TrackerPanel::Redraw() {
254 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
255 dc.SetBrush(*wxTRANSPARENT_BRUSH); 287 dc.SetBrush(*wxTRANSPARENT_BRUSH);
256 dc.DrawRectangle({real_area_x, real_area_y}, 288 dc.DrawRectangle({real_area_x, real_area_y},
257 {real_area_size, real_area_size}); 289 {real_area_size_, real_area_size_});
258 290
259 } else { 291 } else {
260 const wxBrush *brush_color = wxGREY_BRUSH; 292 const wxBrush *brush_color = wxGREY_BRUSH;
@@ -269,30 +301,7 @@ void TrackerPanel::Redraw() {
269 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 301 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
270 dc.SetBrush(*brush_color); 302 dc.SetBrush(*brush_color);
271 dc.DrawRectangle({real_area_x, real_area_y}, 303 dc.DrawRectangle({real_area_x, real_area_y},
272 {real_area_size, real_area_size}); 304 {real_area_size_, real_area_size_});
273 }
274
275 area.real_x1 = real_area_x;
276 area.real_x2 = real_area_x + real_area_size;
277 area.real_y1 = real_area_y;
278 area.real_y2 = real_area_y + real_area_size;
279
280 int popup_x =
281 final_x + map_area.map_x * final_width / image_size.GetWidth();
282 int popup_y =
283 final_y + map_area.map_y * final_width / image_size.GetWidth();
284
285 area.popup->SetClientSize(
286 area.popup->GetVirtualSize().GetWidth(),
287 std::min(panel_size.GetHeight(),
288 area.popup->GetVirtualSize().GetHeight()));
289
290 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
291 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
292 } 305 }
293 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
294 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
295 }
296 area.popup->SetPosition({popup_x, popup_y});
297 } 306 }
298} 307}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index e1f515d..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h
@@ -17,15 +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 SetSavedataPath(std::string savedata_path);
23
24 bool IsPanelsMode() const { return panels_mode_; }
25
26 const std::set<std::string> &GetSolvedPanels() const {
27 return solved_panels_;
28 }
29 21
30 private: 22 private:
31 struct AreaIndicator { 23 struct AreaIndicator {
@@ -40,14 +32,13 @@ class TrackerPanel : public wxPanel {
40 32
41 void OnPaint(wxPaintEvent &event); 33 void OnPaint(wxPaintEvent &event);
42 void OnMouseMove(wxMouseEvent &event); 34 void OnMouseMove(wxMouseEvent &event);
43 void OnRefreshSavedata(wxCommandEvent &event);
44 35
36 void Resize();
45 void Redraw(); 37 void Redraw();
46 38
47 void RefreshSavedata();
48
49 wxImage map_image_; 39 wxImage map_image_;
50 wxImage player_image_; 40 wxImage player_image_;
41 wxBitmap scaled_map_;
51 wxBitmap rendered_; 42 wxBitmap rendered_;
52 wxBitmap scaled_player_; 43 wxBitmap scaled_player_;
53 44
@@ -55,12 +46,9 @@ class TrackerPanel : public wxPanel {
55 int offset_y_ = 0; 46 int offset_y_ = 0;
56 double scale_x_ = 0; 47 double scale_x_ = 0;
57 double scale_y_ = 0; 48 double scale_y_ = 0;
49 int real_area_size_ = 0;
58 50
59 std::vector<AreaIndicator> areas_; 51 std::vector<AreaIndicator> areas_;
60
61 bool panels_mode_ = false;
62 std::optional<std::string> savedata_path_;
63 std::set<std::string> solved_panels_;
64}; 52};
65 53
66#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 2ee705c..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 {
@@ -19,11 +20,13 @@ namespace {
19struct Requirements { 20struct Requirements {
20 bool disabled = false; 21 bool disabled = false;
21 22
22 std::set<int> doors; // non-grouped, handles progressive 23 std::set<int> doors; // non-grouped, handles progressive
23 std::set<int> items; // all other items 24 std::set<int> panel_doors; // non-grouped, handles progressive
24 std::set<int> rooms; // maybe 25 std::set<int> items; // all other items
25 bool mastery = false; // maybe 26 std::set<int> rooms; // maybe
26 bool panel_hunt = false; // maybe 27 bool mastery = false; // maybe
28 bool panel_hunt = false; // maybe
29 bool postgame = false;
27 30
28 void Merge(const Requirements& rhs) { 31 void Merge(const Requirements& rhs) {
29 if (rhs.disabled) { 32 if (rhs.disabled) {
@@ -33,6 +36,9 @@ struct Requirements {
33 for (int id : rhs.doors) { 36 for (int id : rhs.doors) {
34 doors.insert(id); 37 doors.insert(id);
35 } 38 }
39 for (int id : rhs.panel_doors) {
40 panel_doors.insert(id);
41 }
36 for (int id : rhs.items) { 42 for (int id : rhs.items) {
37 items.insert(id); 43 items.insert(id);
38 } 44 }
@@ -41,6 +47,7 @@ struct Requirements {
41 } 47 }
42 mastery = mastery || rhs.mastery; 48 mastery = mastery || rhs.mastery;
43 panel_hunt = panel_hunt || rhs.panel_hunt; 49 panel_hunt = panel_hunt || rhs.panel_hunt;
50 postgame = postgame || rhs.postgame;
44 } 51 }
45}; 52};
46 53
@@ -78,15 +85,12 @@ class RequirementCalculator {
78 requirements.doors.insert(door_obj.id); 85 requirements.doors.insert(door_obj.id);
79 break; 86 break;
80 } 87 }
81 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) { 88 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) {
82 requirements.rooms.insert(door_obj.room);
83
84 for (int panel_id : door_obj.panels) { 89 for (int panel_id : door_obj.panels) {
85 const Requirements& panel_reqs = GetPanel(panel_id); 90 const Requirements& panel_reqs = GetPanel(panel_id);
86 requirements.Merge(panel_reqs); 91 requirements.Merge(panel_reqs);
87 } 92 }
88 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS && 93 } else if (AP_AreDoorsGrouped() && !door_obj.group_name.empty()) {
89 !door_obj.group_name.empty()) {
90 requirements.items.insert(door_obj.group_ap_item_id); 94 requirements.items.insert(door_obj.group_ap_item_id);
91 } else { 95 } else {
92 requirements.doors.insert(door_obj.id); 96 requirements.doors.insert(door_obj.id);
@@ -133,6 +137,21 @@ class RequirementCalculator {
133 requirements.items.insert(GD_GetItemIdForColor(color)); 137 requirements.items.insert(GD_GetItemIdForColor(color));
134 } 138 }
135 } 139 }
140
141 if (panel_obj.panel_door != -1 &&
142 AP_GetDoorShuffleMode() == kPANELS_MODE) {
143 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_obj.panel_door);
144
145 if (panel_door_obj.group_ap_item_id != -1 && AP_AreDoorsGrouped()) {
146 requirements.items.insert(panel_door_obj.group_ap_item_id);
147 } else {
148 requirements.panel_doors.insert(panel_obj.panel_door);
149 }
150 }
151
152 if (panel_obj.location_name == GetWinCondition()) {
153 requirements.postgame = true;
154 }
136 155
137 panels_[panel_id] = requirements; 156 panels_[panel_id] = requirements;
138 } 157 }
@@ -148,11 +167,17 @@ class RequirementCalculator {
148struct TrackerState { 167struct TrackerState {
149 std::map<int, bool> reachability; 168 std::map<int, bool> reachability;
150 std::set<int> reachable_doors; 169 std::set<int> reachable_doors;
170 std::set<int> solveable_panels;
151 std::set<int> reachable_paintings; 171 std::set<int> reachable_paintings;
152 std::mutex reachability_mutex; 172 std::mutex reachability_mutex;
153 RequirementCalculator requirements; 173 RequirementCalculator requirements;
154 std::map<int, std::map<std::string, bool>> door_reports; 174 std::map<int, std::map<std::string, bool>> door_reports;
155 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;
156}; 181};
157 182
158enum Decision { kYes, kNo, kMaybe }; 183enum Decision { kYes, kNo, kMaybe };
@@ -167,6 +192,11 @@ class StateCalculator;
167struct StateCalculatorOptions { 192struct StateCalculatorOptions {
168 int start; 193 int start;
169 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
170 StateCalculator* parent = nullptr; 200 StateCalculator* parent = nullptr;
171}; 201};
172 202
@@ -177,7 +207,21 @@ class StateCalculator {
177 explicit StateCalculator(StateCalculatorOptions options) 207 explicit StateCalculator(StateCalculatorOptions options)
178 : options_(options) {} 208 : options_(options) {}
179 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
180 void Calculate() { 220 void Calculate() {
221 painting_mapping_ = AP_GetPaintingMapping();
222 checked_paintings_ = AP_GetCheckedPaintings();
223 sunwarp_mapping_ = AP_GetSunwarpMapping();
224
181 std::list<int> panel_boundary; 225 std::list<int> panel_boundary;
182 std::list<int> painting_boundary; 226 std::list<int> painting_boundary;
183 std::list<Exit> flood_boundary; 227 std::list<Exit> flood_boundary;
@@ -217,12 +261,13 @@ class StateCalculator {
217 reachable_changed = true; 261 reachable_changed = true;
218 262
219 PaintingExit cur_painting = GD_GetPaintingExit(painting_id); 263 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
220 if (AP_GetPaintingMapping().count(cur_painting.internal_id) && 264 if (painting_mapping_.count(cur_painting.internal_id) &&
221 AP_GetCheckedPaintings().count(cur_painting.internal_id)) { 265 (checked_paintings_.count(cur_painting.internal_id) ||
266 options_.postgame_detection)) {
222 Exit painting_exit; 267 Exit painting_exit;
223 PaintingExit target_painting = 268 PaintingExit target_painting =
224 GD_GetPaintingExit(GD_GetPaintingByName( 269 GD_GetPaintingExit(GD_GetPaintingByName(
225 AP_GetPaintingMapping().at(cur_painting.internal_id))); 270 painting_mapping_.at(cur_painting.internal_id)));
226 painting_exit.source_room = cur_painting.room; 271 painting_exit.source_room = cur_painting.room;
227 painting_exit.destination_room = target_painting.room; 272 painting_exit.destination_room = target_painting.room;
228 painting_exit.type = EntranceType::kPainting; 273 painting_exit.type = EntranceType::kPainting;
@@ -281,8 +326,8 @@ class StateCalculator {
281 326
282 if (AP_IsSunwarpShuffle()) { 327 if (AP_IsSunwarpShuffle()) {
283 for (int index : room_obj.sunwarps) { 328 for (int index : room_obj.sunwarps) {
284 if (AP_GetSunwarpMapping().count(index)) { 329 if (sunwarp_mapping_.count(index)) {
285 const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); 330 const SunwarpMapping& sm = sunwarp_mapping_.at(index);
286 331
287 new_boundary.push_back( 332 new_boundary.push_back(
288 {.source_room = room_exit.destination_room, 333 {.source_room = room_exit.destination_room,
@@ -296,15 +341,14 @@ class StateCalculator {
296 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { 341 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") {
297 new_boundary.push_back( 342 new_boundary.push_back(
298 {.source_room = room_exit.destination_room, 343 {.source_room = room_exit.destination_room,
299 .destination_room = GD_GetRoomByName("Outside The Undeterred"), 344 .destination_room = GD_GetRoomByName("Color Hallways"),
300 .type = EntranceType::kPainting}); 345 .type = EntranceType::kPainting});
301 } 346 }
302 347
303 if (AP_IsPilgrimageEnabled()) { 348 if (AP_IsPilgrimageEnabled()) {
304 int pilgrimage_start_id = GD_GetRoomByName("Hub Room"); 349 int pilgrimage_start_id = GD_GetRoomByName("Hub Room");
305 if (AP_IsSunwarpShuffle()) { 350 if (AP_IsSunwarpShuffle()) {
306 for (const auto& [start_index, mapping] : 351 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
307 AP_GetSunwarpMapping()) {
308 if (mapping.dots == 1) { 352 if (mapping.dots == 1) {
309 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index); 353 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index);
310 } 354 }
@@ -343,6 +387,10 @@ class StateCalculator {
343 // evaluated. 387 // evaluated.
344 for (const Door& door : GD_GetDoors()) { 388 for (const Door& door : GD_GetDoors()) {
345 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]);
346 } 394 }
347 } 395 }
348 396
@@ -378,7 +426,8 @@ class StateCalculator {
378 } 426 }
379 427
380 private: 428 private:
381 Decision IsNonGroupedDoorReachable(const Door& door_obj) { 429 template <typename T>
430 Decision IsNonGroupedDoorReachable(const T& door_obj) {
382 bool has_item = AP_HasItem(door_obj.ap_item_id); 431 bool has_item = AP_HasItem(door_obj.ap_item_id);
383 432
384 if (!has_item) { 433 if (!has_item) {
@@ -399,29 +448,48 @@ class StateCalculator {
399 return kNo; 448 return kNo;
400 } 449 }
401 450
451 if (reqs.postgame && options_.postgame_detection) {
452 return kNo;
453 }
454
402 Decision final_decision = kYes; 455 Decision final_decision = kYes;
403 456
404 for (int door_id : reqs.doors) { 457 if (!options_.postgame_detection) {
405 const Door& door_obj = GD_GetDoor(door_id); 458 for (int door_id : reqs.doors) {
406 Decision decision = IsNonGroupedDoorReachable(door_obj); 459 const Door& door_obj = GD_GetDoor(door_id);
460 Decision decision = IsNonGroupedDoorReachable(door_obj);
407 461
408 if (report) { 462 if (report) {
409 (*report)[door_obj.item_name] = (decision == kYes); 463 (*report)[door_obj.item_name] = (decision == kYes);
410 } 464 }
411 465
412 if (decision != kYes) { 466 if (decision != kYes) {
413 final_decision = decision; 467 final_decision = decision;
468 }
414 } 469 }
415 }
416 470
417 for (int item_id : reqs.items) { 471 for (int panel_door_id : reqs.panel_doors) {
418 bool has_item = AP_HasItem(item_id); 472 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id);
419 if (report) { 473 Decision decision = IsNonGroupedDoorReachable(panel_door_obj);
420 (*report)[AP_GetItemName(item_id)] = has_item; 474
475 if (report) {
476 (*report)[panel_door_obj.item_name] = (decision == kYes);
477 }
478
479 if (decision != kYes) {
480 final_decision = decision;
481 }
421 } 482 }
422 483
423 if (!has_item) { 484 for (int item_id : reqs.items) {
424 final_decision = kNo; 485 bool has_item = AP_HasItem(item_id);
486 if (report) {
487 (*report)[GD_GetItemName(item_id)] = has_item;
488 }
489
490 if (!has_item) {
491 final_decision = kNo;
492 }
425 } 493 }
426 } 494 }
427 495
@@ -490,14 +558,7 @@ class StateCalculator {
490 } 558 }
491 559
492 Decision IsDoorReachable_Helper(int door_id) { 560 Decision IsDoorReachable_Helper(int door_id) {
493 if (door_report_.count(door_id)) { 561 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id));
494 door_report_[door_id].clear();
495 } else {
496 door_report_[door_id] = {};
497 }
498
499 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id),
500 &door_report_[door_id]);
501 } 562 }
502 563
503 Decision IsDoorReachable(int door_id) { 564 Decision IsDoorReachable(int door_id) {
@@ -549,7 +610,7 @@ class StateCalculator {
549 if (AP_IsSunwarpShuffle()) { 610 if (AP_IsSunwarpShuffle()) {
550 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); 611 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5);
551 612
552 for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { 613 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
553 if (mapping.dots > 1) { 614 if (mapping.dots > 1) {
554 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; 615 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index;
555 } 616 }
@@ -620,23 +681,94 @@ class StateCalculator {
620 bool pilgrimage_doable_ = false; 681 bool pilgrimage_doable_ = false;
621 682
622 std::map<int, std::list<int>> paths_; 683 std::map<int, std::list<int>> paths_;
684
685 std::map<std::string, std::string> painting_mapping_;
686 std::set<std::string> checked_paintings_;
687 std::map<int, SunwarpMapping> sunwarp_mapping_;
623}; 688};
624 689
625} // namespace 690} // namespace
626 691
627void ResetReachabilityRequirements() { 692void ResetReachabilityRequirements() {
693 TrackerLog("Resetting tracker state...");
694
628 std::lock_guard reachability_guard(GetState().reachability_mutex); 695 std::lock_guard reachability_guard(GetState().reachability_mutex);
629 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 }
630} 755}
631 756
632void RecalculateReachability() { 757void RecalculateReachability() {
758 TrackerLog("Calculating reachability...");
759
633 std::lock_guard reachability_guard(GetState().reachability_mutex); 760 std::lock_guard reachability_guard(GetState().reachability_mutex);
634 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.
635 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);
636 state_calculator.Calculate(); 768 state_calculator.Calculate();
637 769
638 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); 770 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms();
639 const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); 771 std::set<int> solveable_panels = state_calculator.GetSolveablePanels();
640 772
641 std::map<int, bool> new_reachability; 773 std::map<int, bool> new_reachability;
642 for (const MapArea& map_area : GD_GetMapAreas()) { 774 for (const MapArea& map_area : GD_GetMapAreas()) {
@@ -667,6 +799,7 @@ void RecalculateReachability() {
667 799
668 std::swap(GetState().reachability, new_reachability); 800 std::swap(GetState().reachability, new_reachability);
669 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);
670 std::swap(GetState().reachable_paintings, reachable_paintings); 803 std::swap(GetState().reachable_paintings, reachable_paintings);
671 std::swap(GetState().door_reports, door_reports); 804 std::swap(GetState().door_reports, door_reports);
672 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); 805 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable();
@@ -705,3 +838,33 @@ bool IsPilgrimageDoable() {
705 838
706 return GetState().pilgrimage_doable; 839 return GetState().pilgrimage_doable;
707} 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.cpp b/src/version.cpp new file mode 100644 index 0000000..3b4d5f3 --- /dev/null +++ b/src/version.cpp
@@ -0,0 +1,5 @@
1#include "version.h"
2
3std::ostream& operator<<(std::ostream& out, const Version& ver) {
4 return out << "v" << ver.major << "." << ver.minor << "." << ver.revision;
5}
diff --git a/src/version.h b/src/version.h index 24c04b4..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, 11, 1); 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