Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
StatsAccountRuntime.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <fstream>
6#include <limits>
7
8namespace monopoly::statsui
9{
10 namespace
11 {
12 constexpr std::size_t RecordBytes = 8 + 500 * 2;
13 void put32(std::array<unsigned char, RecordBytes>& bytes, std::size_t at,
14 std::uint32_t value)
15 {
16 for (unsigned i = 0; i < 4; ++i)
17 bytes[at + i] = static_cast<unsigned char>(value >> (8 * i));
18 }
19 std::uint32_t get32(const std::array<unsigned char, RecordBytes>& bytes,
20 std::size_t at)
21 {
22 std::uint32_t value{};
23 for (unsigned i = 0; i < 4; ++i)
24 value |= static_cast<std::uint32_t>(bytes[at + i]) << (8 * i);
25 return value;
26 }
27 }
28
29 std::expected<std::u16string, std::string> statsPropertyName(
30 const data::LanguageCatalog& catalog, data::BoardEdition edition,
31 int city, int square)
32 {
33 if (square < 0 || square >= static_cast<int>(rules::SquareCount))
34 return std::unexpected("Stats property is outside the board");
35 if (city == -1 && edition == data::BoardEdition::Europe)
36 city = static_cast<int>(catalog.language()) - 2;
37 const auto id = 1001ULL + 42ULL * static_cast<unsigned>(std::max(city, 0)) + square;
38 if (id > std::numeric_limits<std::uint32_t>::max())
39 return std::unexpected("Stats city name offset overflows LANG IDs");
40 auto text = catalog.lookup(static_cast<std::uint32_t>(id));
41 if (!text) return std::unexpected(text.error().detail);
42 if (!*text) return std::unexpected("Stats property name is missing from LANG");
43 if (!(**text)->empty() && (**text)->front() == u'*')
44 {
45 text = catalog.lookup(1001U + static_cast<unsigned>(square));
46 if (!text) return std::unexpected(text.error().detail);
47 if (!*text) return std::unexpected("Stats base property name is missing from LANG");
48 }
49 return ***text;
50 }
51
52 std::expected<void, std::string> AccountRuntime::open(std::filesystem::path path)
53 {
54 if (path.empty() || !path.is_absolute())
55 return std::unexpected("account history requires an explicit absolute path");
57 std::error_code error;
58 const bool exists = std::filesystem::exists(path, error);
59 if (error) return std::unexpected(error.message());
60 if (exists)
61 {
62 std::ifstream input(path, std::ios::binary);
63 if (!input) return std::unexpected("cannot open account history");
64 std::array<unsigned char, RecordBytes> bytes{};
65 while (input.read(reinterpret_cast<char*>(bytes.data()), bytes.size()))
66 {
67 const auto player = get32(bytes, 0);
68 if (player >= rules::MaxPlayers)
69 return std::unexpected("account history contains an invalid player");
70 AccountHistoryRow row{static_cast<rules::PlayerNumber>(player), get32(bytes, 4), {}};
71 bool terminated = false;
72 for (std::size_t i = 0; i < 500; ++i)
73 {
74 const auto unit = static_cast<char16_t>(bytes[8 + i * 2] |
75 (static_cast<unsigned>(bytes[9 + i * 2]) << 8));
76 if (unit == 0) { terminated = true; break; }
77 row.description.push_back(unit);
78 }
79 if (!terminated) return std::unexpected("account history description is unterminated");
80 loaded.turn = std::max(loaded.turn, row.turn);
81 loaded.history.push_back(std::move(row));
82 }
83 if (!input.eof() || input.gcount() != 0)
84 return std::unexpected("account history contains a truncated record");
85 }
86 path_ = std::move(path);
87 state_ = std::move(loaded);
88 return {};
89 }
90
91 std::expected<void, std::string> AccountRuntime::resetGame()
92 {
93 if (path_.empty()) return std::unexpected("account history path is not configured");
94 std::ofstream output(path_, std::ios::binary | std::ios::trunc);
95 if (!output) return std::unexpected("cannot reset account history");
96 state_ = {};
97 return {};
98 }
99
100 void AccountRuntime::scroll(int lines) noexcept
101 {
102 state_.scrollLines = static_cast<int>(std::clamp<std::int64_t>(
103 static_cast<std::int64_t>(state_.scrollLines) + lines, 0,
104 state_.scrollLimit));
105 }
106
108 {
109 state_.scrollLimit = std::max(lines, 0);
110 state_.scrollLines = std::min(state_.scrollLines, state_.scrollLimit);
111 }
112
113 std::expected<void, std::string> AccountRuntime::processRuleMessage(
114 const actions::Message& message, const rules::GameState& previous,
115 rules::PlayerNumber iBarPlayer, int city, const data::ResourceSnapshot& resources)
116 {
118 {
119 if (state_.turn == 0)
120 {
121 if (path_.empty()) return std::unexpected("account history path is not configured");
122 std::ofstream output(path_, std::ios::binary | std::ios::trunc);
123 if (!output) return std::unexpected("cannot reset account history for first turn");
124 state_.history.clear();
125 state_.scrollLines = state_.scrollLimit = 0;
126 }
127 if (state_.turn == std::numeric_limits<std::uint32_t>::max())
128 return std::unexpected("account history turn overflow");
129 ++state_.turn;
130 return {};
131 }
134 message.action != actions::Type::NotifySquareHouses) return {};
135 if (message.numberA < 0 || message.numberA >= rules::SquareCount)
136 return std::unexpected("account history notification has invalid square");
137 const int square = static_cast<int>(message.numberA);
138 auto player = iBarPlayer;
140 {
141 if (message.numberB < 0 || message.numberB >= rules::MaxPlayers) return {};
142 player = static_cast<rules::PlayerNumber>(message.numberB);
143 }
144 if (player >= rules::MaxPlayers)
145 return std::unexpected("account history notification has invalid player");
146 const auto language = resources.language();
147 if (!language || !language->catalog)
148 return std::unexpected("account history has no LANG catalog");
149 const auto edition = resources.context().board;
150 AccountHistoryRow row{player, state_.turn, {}};
152 {
153 const auto before = previous.squares[static_cast<std::size_t>(square)].houses;
154 const bool sold = before > message.numberB;
155 const bool hotel = (sold ? before : message.numberB) == message.numberC;
156 if (edition != data::BoardEdition::Usa)
157 return std::unexpected("retail European house-history text IDs are absent from the source catalog");
158 row.description = sold ? (hotel ? u"Sold a hotel. " : u"Sold a house. ") :
159 (hotel ? u"Purchased a hotel. " : u"Purchased a house. ");
160 }
161 else
162 {
163 const auto property = statsPropertyName(*language->catalog, edition, city, square);
164 if (!property) return std::unexpected(property.error());
165 if (edition == data::BoardEdition::Usa)
166 {
167 row.description = message.action == actions::Type::NotifySquareOwnership ?
168 u"Purchased property " : message.numberB ? u"Mortgaged property " : u"Unmortgaged property ";
169 row.description += *property + u". ";
170 }
171 else
172 {
174 return std::unexpected("retail European mortgage-history text IDs are absent from the source catalog");
175 const auto pattern = language->catalog->lookup(3178);
176 if (!pattern) return std::unexpected(pattern.error().detail);
177 if (!*pattern) return std::unexpected("account purchase history is missing LANG ID 3178");
178 for (std::size_t i = 0; i < (***pattern).size(); ++i)
179 {
180 const auto unit = (***pattern)[i];
181 if (unit == u'^' && i + 1 < (***pattern).size() &&
182 ((***pattern)[i + 1] == u'S' || (***pattern)[i + 1] == u's'))
183 { row.description += *property; ++i; }
184 else row.description.push_back(unit);
185 }
186 }
187 }
188 if (path_.empty()) return std::unexpected("account history path is not configured");
189 if (row.description.size() >= 500)
190 {
191 row.description.resize(499);
192 if (row.description.back() >= 0xD800 && row.description.back() <= 0xDBFF)
193 row.description.pop_back();
194 }
195 std::array<unsigned char, RecordBytes> bytes{};
196 put32(bytes, 0, row.player);
197 put32(bytes, 4, row.turn);
198 for (std::size_t i = 0; i < row.description.size(); ++i)
199 {
200 bytes[8 + i * 2] = static_cast<unsigned char>(row.description[i]);
201 bytes[9 + i * 2] = static_cast<unsigned char>(row.description[i] >> 8);
202 }
203 std::ofstream output(path_, std::ios::binary | std::ios::app);
204 if (!output || !output.write(reinterpret_cast<const char*>(bytes.data()), bytes.size()))
205 return std::unexpected("cannot append account history");
206 output.flush();
207 if (!output) return std::unexpected("cannot flush account history");
208 state_.history.push_back(std::move(row));
209 return {};
210 }
211}
std::string_view text
std::vector< std::byte > bytes
unsigned index
std::string path
std::string error
Bytes input
std::deque< Bytes > output
std::size_t at
std::uint16_t value
Definition Timers.cpp:16
std::expected< void, std::string > processRuleMessage(const actions::Message &message, const rules::GameState &previous, rules::PlayerNumber iBarPlayer, int city, const data::ResourceSnapshot &resources)
std::expected< void, std::string > resetGame()
std::expected< void, std::string > open(std::filesystem::path historyPath)
std::uint8_t PlayerNumber
Definition RuleTypes.hpp:12
constexpr std::size_t SquareCount
Definition RuleTypes.hpp:24
constexpr PlayerNumber MaxPlayers
Definition RuleTypes.hpp:14
std::expected< std::u16string, std::string > statsPropertyName(const data::LanguageCatalog &catalog, data::BoardEdition edition, int city, int square)
std::vector< AccountHistoryRow > history