Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
OptionsCustomBoardRuntime.cpp
Go to the documentation of this file.
3#include "LegacyBitmap.hpp"
4#include <algorithm>
5#include <fstream>
6#ifdef _WIN32
7#ifndef NOMINMAX
8#define NOMINMAX
9#endif
10#include <windows.h>
11#endif
12namespace monopoly::optionsui
13{
14 namespace
15 {
16 bool boardExtension(const std::filesystem::path& path)
17 {
18 auto extension = path.extension().u8string();
19 for (auto& c : extension) if (c >= u8'A' && c <= u8'Z') c += u8'a' - u8'A';
20 return extension == u8".brd";
21 }
22 bool within(const std::filesystem::path& root, const std::filesystem::path& child)
23 {
24 const auto relative = child.lexically_relative(root);
25 if (relative.empty() || relative.is_absolute()) return false;
26 for (const auto& part : relative) if (part == "..") return false;
27 return relative != ".";
28 }
29 std::string utf8Name(const std::filesystem::path& path)
30 {
31 const auto value = path.u8string();
32 return {reinterpret_cast<const char*>(value.data()), value.size()};
33 }
34 }
35 Rect customBoardSlotRect(std::size_t slot) noexcept
36 {
37 if (slot >= CustomBoardPageSize) return {};
38 const int top = 112 + static_cast<int>(slot) * 68;
39 return {152, top, 648, top + 34};
40 }
42 { return state.pageOffset >= CustomBoardPageSize; }
44 { return state.pageOffset < state.entries.size() && state.entries.size() - state.pageOffset > CustomBoardPageSize; }
45
46 std::expected<void, std::string> openCustomBoardDialog(CustomBoardState& state,
47 const std::filesystem::path& moduleDirectory, display::Screen2D previousView)
48 {
49 if (!moduleDirectory.is_absolute()) return std::unexpected("custom boards require an absolute executable directory");
50 std::error_code error;
51 const auto module = std::filesystem::canonical(moduleDirectory, error);
52 if (error || !std::filesystem::is_directory(module, error))
53 return std::unexpected("custom-board executable directory is unavailable");
55 next.previousView = previousView;
56 next.directory = std::filesystem::weakly_canonical(module / "custbrds", error);
57 if (error || !within(module, next.directory))
58 return std::unexpected("custom-board directory escapes the executable directory");
59 const bool exists = std::filesystem::exists(next.directory, error);
60 if (error) return std::unexpected("cannot inspect custom-board directory: " + error.message());
61 if (exists)
62 {
63 std::filesystem::directory_iterator iterator(next.directory, error), end;
64 if (error) return std::unexpected("cannot enumerate custom boards: " + error.message());
65 for (; iterator != end && next.entries.size() < CustomBoardLimit; iterator.increment(error))
66 {
67 if (error) return std::unexpected("cannot enumerate custom boards: " + error.message());
68 const auto& entry = *iterator;
69 if (!boardExtension(entry.path())) continue;
70 const bool regular = entry.is_regular_file(error);
71 if (error) return std::unexpected("cannot inspect custom-board file: " + error.message());
72 if (!regular) continue;
73 const auto canonical = std::filesystem::canonical(entry.path(), error);
74 if (error || !within(next.directory, canonical))
75 return std::unexpected("custom-board file escapes its directory");
76 const auto name = entry.path().filename();
77 next.entries.push_back({name, utf8Name(name)});
78 }
79 if (error) return std::unexpected("cannot enumerate custom boards: " + error.message());
80 std::sort(next.entries.begin(), next.entries.end(), [](const auto& a, const auto& b)
81 { return a.fileName < b.fileName; });
82 }
83 next.active = true;
84 next.selectedIndex = next.entries.empty() ? -1 : 0;
85 next.revision = state.revision + 1;
86 state = std::move(next);
87 return {};
88 }
90 {
91 state.active = false;
92 state.buttonRects = {};
93 ++state.revision;
94 }
96 {
97 CustomBoardInput result;
98 if (!state.active) return result;
99 result.consumed = true;
100 if (message.type != uimsg::Type::MouseLeftDown) return result;
101 const int x = static_cast<int>(message.numberA), y = static_cast<int>(message.numberB);
102 for (std::size_t slot = 0; slot < CustomBoardPageSize && state.pageOffset + slot < state.entries.size(); ++slot)
103 {
104 if (!customBoardSlotRect(slot).contains(x, y)) continue;
105 state.selectedIndex = static_cast<int>(state.pageOffset + slot);
106 ++state.revision;
107 return result; // Retail slot selection has no click sound.
108 }
109 for (std::size_t button = 0; button < state.buttonRects.size(); ++button)
110 {
111 if (!state.buttonRects[button].contains(x, y)) continue;
112 if ((button == 2 && !customBoardHasPrevious(state)) ||
113 (button == 3 && !customBoardHasNext(state))) return result;
114 result.playClick = true;
115 if (button == 0)
116 result.requestLoad = state.selectedIndex >= 0 && static_cast<std::size_t>(state.selectedIndex) < state.entries.size();
117 else if (button == 1) result.closeDialog = true;
118 else
119 {
120 if (button == 2) state.pageOffset -= CustomBoardPageSize;
121 else state.pageOffset += CustomBoardPageSize;
122 state.selectedIndex = static_cast<int>(state.pageOffset);
123 ++state.revision;
124 }
125 return result;
126 }
127 return result;
128 }
129 std::expected<std::uint32_t, std::string> readCustomBoardSecurityVersion()
130 {
131#ifdef _WIN32
132 HKEY key{};
134 L"Software\\Hasbro Interactive\\Monopoly\\2.00.101", 0,
136 if (opened != ERROR_SUCCESS)
137 return std::unexpected("custom-board editor registry key is unavailable (Windows error " + std::to_string(opened) + ")");
138 DWORD value{}, type{}, bytes = sizeof(value);
139 const auto queried = RegQueryValueExW(key, L"Version", nullptr, &type,
140 reinterpret_cast<BYTE*>(&value), &bytes);
142 if (queried != ERROR_SUCCESS || type != REG_BINARY || bytes != sizeof(value))
143 return std::unexpected("custom-board editor Version must be an installed four-byte REG_BINARY value");
144 return static_cast<std::uint32_t>(value);
145#else
146 return std::unexpected("custom-board editor ownership validation requires the Windows registry");
147#endif
148 }
149 std::expected<CustomBoardSelection, std::string> validateCustomBoardFile(
150 const std::filesystem::path& directory, const std::filesystem::path& fileName,
151 std::uint32_t installedSecurityVersion)
152 {
153 if (!directory.is_absolute() || fileName.empty() || fileName.has_parent_path() ||
154 !boardExtension(fileName) || fileName.stem().empty() || fileName.stem() == "." || fileName.stem() == "..")
155 return std::unexpected("invalid custom-board filename or directory");
156 std::error_code error;
157 const auto root = std::filesystem::canonical(directory, error);
158 if (error) return std::unexpected("custom-board directory is unavailable");
159 const auto file = std::filesystem::canonical(root / fileName, error);
160 if (error || !within(root, file) || !std::filesystem::is_regular_file(file, error))
161 return std::unexpected("custom-board file is unavailable or escapes its directory");
162 const auto assets = std::filesystem::canonical(root / fileName.stem(), error);
163 if (error || !within(root, assets) || !std::filesystem::is_directory(assets, error))
164 return std::unexpected("custom-board asset directory is unavailable or escapes its directory");
165 std::ifstream input(file, std::ios::binary);
166 std::array<unsigned char, 4> bytes{};
167 if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size()))
168 return std::unexpected("custom-board file lacks its four-byte ownership code");
169 const auto version = static_cast<std::uint32_t>(bytes[0]) |
170 (static_cast<std::uint32_t>(bytes[1]) << 8U) |
171 (static_cast<std::uint32_t>(bytes[2]) << 16U) |
172 (static_cast<std::uint32_t>(bytes[3]) << 24U);
173 if (version != installedSecurityVersion)
174 return std::unexpected("custom board was not created by this installed board editor");
175 return CustomBoardSelection{file, assets};
176 }
177 std::expected<CustomBoardSelection, std::string> validateSelectedCustomBoard(const CustomBoardState& state)
178 {
179 if (!state.active || state.selectedIndex < 0 || static_cast<std::size_t>(state.selectedIndex) >= state.entries.size())
180 return std::unexpected("no custom board is selected");
181 const auto version = readCustomBoardSecurityVersion();
182 if (!version) return std::unexpected(version.error());
183 return validateCustomBoardFile(state.directory, state.entries[static_cast<std::size_t>(state.selectedIndex)].fileName, *version);
184 }
185
186 std::expected<std::optional<std::filesystem::path>, std::string>
188 const data::ResourceSnapshot& resources, int monetarySystem) try
189 {
190 if (savedAssetRoot.empty()) return std::optional<std::filesystem::path>{};
191 if (savedAssetRoot.find('\0') != std::string_view::npos)
192 return std::unexpected("saved custom-board path contains NUL");
193 const std::filesystem::path savedPath(
194 std::u8string(savedAssetRoot.begin(), savedAssetRoot.end()));
195 auto paths = savedPath.is_absolute()
197 : data::ResourcePaths::create(resources.paths().roots());
198 if (!paths) return std::unexpected(paths.error().detail);
199 std::string firstName = "2dboards/2dview01.bmp";
200 if (!savedPath.is_absolute())
202 const auto first = paths->resolve(firstName);
203 if (!first)
204 {
206 return std::optional<std::filesystem::path>{};
207 return std::unexpected(first.error().detail);
208 }
209 const auto root = first->parent_path().parent_path();
210 const data::BoardTextureContext context{resources.context().board,
211 resources.context().language, -1, monetarySystem, root};
212 // UDUTILS_Load2DBoardSet expects all 39 cameras. Decode them before
213 // accepting the save, including views other than the current camera.
215 {
216 const auto path = data::resolveBoardTexturePath(resources.paths(),
218 name, context);
219 if (!path) return std::unexpected(path.error());
220 std::ifstream input(*path, std::ios::binary | std::ios::ate);
221 if (!input) return std::unexpected("cannot open saved custom-board camera");
222 const auto size = input.tellg();
223 if (size < 0 || static_cast<std::uint64_t>(size) > 4U * 1024U * 1024U)
224 return std::unexpected("saved custom-board camera exceeds file budget");
225 std::vector<std::byte> bytes(static_cast<std::size_t>(size));
226 input.seekg(0, std::ios::beg);
227 if (!bytes.empty()) input.read(reinterpret_cast<char*>(bytes.data()),
228 static_cast<std::streamsize>(bytes.size()));
229 if (!input) return std::unexpected("cannot read saved custom-board camera");
230 const auto decoded = data::decodeLegacyBitmapRGBA8(bytes, 800U * 450U);
231 if (!decoded) return std::unexpected(decoded.error().detail);
232 }
233 const auto recipe = context.edition == data::BoardEdition::Usa
236 if (!recipe) return std::unexpected(std::string(recipe.error().detail));
237 const auto textures = data::loadBoardTextureImages(resources.paths(), *recipe, context);
238 if (!textures) return std::unexpected(textures.error());
239 return std::optional<std::filesystem::path>{root};
240 }
241 catch (const std::filesystem::filesystem_error& error)
242 {
243 return std::unexpected(std::string("invalid saved custom-board path: ") + error.what());
244 }
245}
const char * name
fs::path relative
std::size_t root
std::vector< std::byte > bytes
int top
PlayerNumber slot
std::uint64_t key
unsigned state
unsigned index
data::DataTag first
std::filesystem::path directory
Button button
std::string path
std::string error
Bytes input
std::uint16_t value
Definition Timers.cpp:16
static std::expected< ResourcePaths, DataError > create(std::span< const std::filesystem::path > roots)
BoardTextureRecipeResult buildUsaTextureRecipe(BoardMeshKind mesh, TextureResolution resolution)
std::span< const std::string_view > twoDimensionalBoardTextureNames() noexcept
std::expected< LegacyBitmapRGBA8, BitmapError > decodeLegacyBitmapRGBA8(std::span< const std::byte > bytes, std::size_t maxPixels)
std::expected< std::filesystem::path, std::string > resolveBoardTexturePath(const ResourcePaths &stockPaths, BoardMeshKind mesh, TextureLocation location, std::string_view fileName, const BoardTextureContext &context)
std::expected< std::vector< std::shared_ptr< const HmdTextureImage > >, std::string > loadBoardTextureImages(const ResourcePaths &paths, const BoardTextureRecipe &recipe, const BoardTextureContext &context)
BoardTextureRecipeResult buildEuropeanTextureRecipe(BoardMeshKind mesh, TextureResolution resolution)
Rect customBoardSlotRect(std::size_t slot) noexcept
CustomBoardInput processCustomBoardInput(CustomBoardState &state, const uimsg::Message &message) noexcept
void closeCustomBoardDialog(CustomBoardState &state) noexcept
constexpr std::size_t CustomBoardLimit
std::expected< std::optional< std::filesystem::path >, std::string > restoreSavedCustomBoard(std::string_view savedAssetRoot, const data::ResourceSnapshot &resources, int monetarySystem)
constexpr std::size_t CustomBoardPageSize
std::expected< void, std::string > openCustomBoardDialog(CustomBoardState &state, const std::filesystem::path &moduleDirectory, display::Screen2D previousView)
bool customBoardHasNext(const CustomBoardState &state) noexcept
bool customBoardHasPrevious(const CustomBoardState &state) noexcept
std::expected< CustomBoardSelection, std::string > validateCustomBoardFile(const std::filesystem::path &directory, const std::filesystem::path &fileName, std::uint32_t installedSecurityVersion)
std::expected< CustomBoardSelection, std::string > validateSelectedCustomBoard(const CustomBoardState &state)
std::expected< std::uint32_t, std::string > readCustomBoardSecurityVersion()
bool contains(int x, int y) const noexcept
Definition OptionsUI.hpp:80