Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
BoardTextureRuntime.cpp
Go to the documentation of this file.
2
3#include "LegacyBitmap.hpp"
4
5#include <algorithm>
6#include <fstream>
7#include <utility>
8
9namespace monopoly::data
10{
11 namespace
12 {
13 struct LoadedTexture
14 {
15 std::vector<std::byte> bytes;
16 LegacyBitmapMetadata metadata;
17 LegacyBitmapRGBA8 image;
18 };
19
20 bool sameAssetPath(std::string_view left, std::string_view right)
21 {
22 const auto fold = [](char c)
23 {
24 return c >= 'A' && c <= 'Z' ? static_cast<char>(c + 'a' - 'A') : c;
25 };
26 return left.size() == right.size() && std::equal(
27 left.begin(), left.end(), right.begin(),
28 [fold](char a, char b) { return fold(a) == fold(b); });
29 }
30
31 std::expected<LoadedTexture, std::string> loadTexture(
32 const ResourcePaths& paths, const BoardTextureRecipe& recipe,
33 const BoardTextureContext& context, TextureLocation location,
34 std::string_view fileName)
35 {
37 recipe.mesh, location, fileName, context);
38 if (!relative)
39 return std::unexpected(std::string(relative.error().detail));
40 const auto resolved = resolveBoardTexturePath(paths,
41 recipe.mesh, location, fileName, context);
42 if (!resolved)
43 return std::unexpected(resolved.error());
44
45 // Stock TexInfo images are at most 256x256 BI_RGB 24-bit.
46 // Bound file allocation as well as the existing decoder's pixels.
47 constexpr std::uint64_t MaximumFileBytes = 1024U * 1024U;
48 std::ifstream input(*resolved, std::ios::binary | std::ios::ate);
49 if (!input)
50 return std::unexpected(*relative + ": cannot open texture BMP");
51 const auto size = input.tellg();
52 if (size < 0 || static_cast<std::uint64_t>(size) > MaximumFileBytes)
53 return std::unexpected(*relative + ": texture BMP exceeds file budget");
54 LoadedTexture result;
55 result.bytes.resize(static_cast<std::size_t>(size));
56 input.seekg(0, std::ios::beg);
57 if (!result.bytes.empty())
58 input.read(reinterpret_cast<char*>(result.bytes.data()),
59 static_cast<std::streamsize>(result.bytes.size()));
60 if (!input)
61 return std::unexpected(*relative + ": cannot read complete texture BMP");
62 const auto metadata = inspectLegacyBitmap(result.bytes);
63 if (!metadata)
64 return std::unexpected(*relative + ": " + metadata.error().detail);
65 auto image = decodeLegacyBitmapRGBA8(result.bytes, 256U * 256U);
66 if (!image)
67 return std::unexpected(*relative + ": " + image.error().detail);
68
69 auto expected = textureDimensions(recipe.resolution);
70 // Custom city name files follow the same source filename anomaly
71 // as retail High/128: CT07/08/09_256 are still 256 pixels wide.
72 if (context.city == -1 && location == TextureLocation::SelectedCityNames &&
73 fileName.find("_256") != std::string_view::npos)
75 // TexInfo's high-detail 128 recipe intentionally references three
76 // 256 city-name files. Preserve their physical corpus dimensions.
77 for (const auto& asset : legacyTextureCorpusManifest())
78 {
79 if (sameAssetPath(asset.relativePath, *relative))
80 {
81 expected = asset.expectedDimensions;
82 break;
83 }
84 }
85 if (image->width != expected.width || image->height != expected.height)
86 return std::unexpected(*relative + ": texture BMP dimensions do not match the catalog");
87 result.metadata = *metadata;
88 result.image = std::move(*image);
89 return result;
90 }
91 }
92
93 std::expected<std::vector<std::shared_ptr<const HmdTextureImage>>, std::string>
95 const ResourcePaths& paths,
97 const BoardTextureContext& context)
98 {
99 std::vector<std::shared_ptr<const HmdTextureImage>> images;
100 if ((recipe.provision != TextureProvision::EmbeddedInMesh &&
103 !isValidTextureResolution(recipe.resolution) ||
104 recipe.meshDataId != boardMeshDataId(recipe.mesh))
105 return std::unexpected("invalid board texture recipe");
107 return images;
108 images.reserve(recipe.textures.size());
109 for (const auto& reference : recipe.textures)
110 {
111 auto base = loadTexture(paths, recipe, context,
112 reference.location, reference.fileName);
113 if (!base)
114 return std::unexpected(base.error());
115 if (reference.overlay)
116 {
117 auto overlay = loadTexture(paths, recipe, context,
118 reference.overlay->location, reference.overlay->fileName);
119 if (!overlay)
120 return std::unexpected(overlay.error());
121 if (base->metadata.bitsPerPixel != 8 || overlay->metadata.bitsPerPixel != 8)
122 return std::unexpected("nonpalettized board overlays are not supported");
123 if (overlay->image.width != base->image.width ||
124 overlay->image.height != base->image.height)
125 return std::unexpected("board overlay dimensions differ from the base texture");
126
127 // UDUtils.cpp uses source palette index zero as DDCOLORKEY,
128 // not an RGB color. Read indices only after decoder validation;
129 // equal colors stored at nonzero indices must remain opaque.
130 const auto width = overlay->image.width;
131 const auto height = overlay->image.height;
132 const std::size_t stride = (static_cast<std::size_t>(width) + 3U) & ~std::size_t{3};
133 for (std::uint32_t y = 0; y < height; ++y)
134 {
135 const auto sourceY = overlay->metadata.topDown() ? y : height - 1U - y;
136 const auto* row = overlay->bytes.data() + overlay->metadata.pixelDataOffset + sourceY * stride;
137 for (std::uint32_t x = 0; x < width; ++x)
138 {
139 if (row[x] == std::byte{0}) continue;
140 const auto pixel = (static_cast<std::size_t>(y) * width + x) * 4U;
141 std::copy_n(overlay->image.pixels.data() + pixel, 4,
142 base->image.pixels.data() + pixel);
143 }
144 }
145 }
146 auto image = std::make_shared<HmdTextureImage>();
147 const auto x = reference.coordinate.x;
148 const auto y = reference.coordinate.y;
149 image->texturePage = static_cast<std::uint16_t>(
150 ((y & 0x200U) << 2U) | ((y & 0x100U) >> 4U) |
151 ((x & 0x3FFU) >> 6U) | (1U << 7U));
152 image->rawX = x;
153 image->rawY = y;
154 image->logicalX = static_cast<std::int32_t>((x & 63U) * 2U);
155 image->logicalY = static_cast<std::int32_t>(y & 255U);
156 image->width = base->image.width;
157 image->height = base->image.height;
158 image->rgba = std::move(base->image.pixels);
159 images.push_back(std::move(image));
160 }
161 return images;
162 }
163}
fs::path relative
std::atomic< float > left
std::atomic< float > right
LegacyBitmapMetadata metadata
LegacyBitmapRGBA8 image
std::vector< std::byte > bytes
std::uint32_t height
std::uint32_t width
unsigned index
Bytes input
constexpr DataId boardMeshDataId(BoardMeshKind mesh) noexcept
constexpr TextureDimensions textureDimensions(TextureResolution resolution) noexcept
constexpr bool isValidTextureResolution(TextureResolution resolution) noexcept
std::expected< LegacyBitmapRGBA8, BitmapError > decodeLegacyBitmapRGBA8(std::span< const std::byte > bytes, std::size_t maxPixels)
constexpr bool isValidBoardMeshKind(BoardMeshKind mesh) noexcept
std::expected< std::string, TextureCatalogError > boardTextureRelativePath(BoardMeshKind mesh, TextureLocation location, std::string_view fileName, const BoardTextureContext &context)
std::expected< std::filesystem::path, std::string > resolveBoardTexturePath(const ResourcePaths &stockPaths, BoardMeshKind mesh, TextureLocation location, std::string_view fileName, const BoardTextureContext &context)
const std::vector< LegacyTextureAsset > & legacyTextureCorpusManifest()
std::expected< std::vector< std::shared_ptr< const HmdTextureImage > >, std::string > loadBoardTextureImages(const ResourcePaths &paths, const BoardTextureRecipe &recipe, const BoardTextureContext &context)
std::expected< LegacyBitmapMetadata, BitmapError > inspectLegacyBitmap(std::span< const std::byte > bytes)
std::size_t size()