13#include <system_error>
24 inline constexpr int SlotTop = 112;
33 bytes.push_back(
static_cast<std::uint8_t
>(
value));
34 bytes.push_back(
static_cast<std::uint8_t
>(
value >> 8U));
44 std::span<const std::uint8_t>
bytes, std::size_t&
offset)
noexcept
46 const std::uint16_t result =
static_cast<std::uint16_t
>(
48 (
static_cast<std::uint16_t
>(
bytes[
offset + 1]) << 8U));
54 std::span<const std::uint8_t>
bytes, std::size_t&
offset)
noexcept
56 std::uint32_t result{};
65 std::vector<std::uint8_t>
bytes;
89 std::span<const std::uint8_t>
bytes)
92 return std::unexpected(
"saved-game metadata is truncated");
108 metadata.description.push_back(
static_cast<char16_t>(
value));
118 if (
value ==
'\0')
break;
125 const std::filesystem::path&
path)
127 std::ifstream
input(
path, std::ios::binary | std::ios::ate);
128 if (!
input)
return std::unexpected(
"unable to open " +
path.string());
129 const auto end =
input.tellg();
130 if (end < 0)
return std::unexpected(
"unable to size " +
path.string());
131 const auto size =
static_cast<std::uint64_t
>(end);
132 if (size >
static_cast<std::uint64_t
>(std::numeric_limits<std::size_t>::max()))
133 return std::unexpected(
"file is too large: " +
path.string());
134 std::vector<std::uint8_t>
bytes(
static_cast<std::size_t
>(size));
135 input.seekg(0, std::ios::beg);
137 reinterpret_cast<char*
>(
bytes.data()),
138 static_cast<std::streamsize
>(
bytes.size())))
139 return std::unexpected(
"unable to read " +
path.string());
144 const std::filesystem::path&
path, std::span<const std::uint8_t>
bytes)
146 std::ofstream
output(
path, std::ios::binary | std::ios::trunc);
147 if (!
output)
return std::unexpected(
"unable to create " +
path.string());
150 output.write(
reinterpret_cast<const char*
>(
bytes.data()),
151 static_cast<std::streamsize
>(
bytes.size()));
152 if (!
output)
return std::unexpected(
"unable to write " +
path.string());
168 else { ++
index;
continue; }
173 const auto byte =
static_cast<std::uint8_t
>(
text[
index +
part]);
174 if ((
byte & 0xC0U) != 0x80U) {
valid =
false;
break; }
188 output.push_back(
static_cast<char16_t>(0xDC00U + (
codepoint & 0x3FFU)));
195 const std::shared_ptr<const data::ResourceSnapshot>& resources)
199 const auto language =
resources->language();
200 if (!language || !language->catalog)
return std::nullopt;
202 if (!
text)
return std::nullopt;
219 if (
value <= 0)
return 0;
220 if (
static_cast<std::uint64_t
>(
value) >
221 std::numeric_limits<std::uint32_t>::max())
222 return std::numeric_limits<std::uint32_t>::max();
223 return static_cast<std::uint32_t
>(
value);
230 if (
base ==
nullptr || *
base ==
'\0')
return {};
231 return std::filesystem::path(
base) /
"savegame";
238 return root / (
"game" + std::to_string(
slot + 1U) +
".msv");
245 return root / (
"game" + std::to_string(
slot + 1U) +
".sgd");
257 return {500, 430, 500 + std::max(
width, 0), 430 + std::max(
height, 0)};
262 return {600, 430, 600 + std::max(
width, 0), 430 + std::max(
height, 0)};
269 return std::unexpected(
"cannot open an empty File dialog");
271 return std::unexpected(
"SDL base path is unavailable for savegame storage");
275 state.selectedSlot = -1;
276 state.draftDescription.clear();
282 std::error_code
error;
283 if (!std::filesystem::is_regular_file(
path,
error) ||
error)
continue;
287 if (!
bytes)
continue;
303 state.selectedSlot = -1;
304 state.draftDescription.clear();
312 std::shared_ptr<const data::ResourceSnapshot> resources)
320 if (
state.selectedSlot < 0 ||
324 result.requestLoad =
true;
326 result.requestSave =
true;
327 result.closeDialog =
true;
328 result.playClick =
true;
341 state.selectedSlot >= 0 && !
state.draftDescription.empty())
343 if (
state.draftDescription.size() >= 2U)
345 const auto last =
static_cast<std::uint16_t
>(
state.draftDescription.back());
346 const auto previous =
static_cast<std::uint16_t
>(
347 state.draftDescription[
state.draftDescription.size() - 2U]);
348 if (
last >= 0xDC00U &&
last <= 0xDFFFU &&
350 state.draftDescription.pop_back();
352 state.draftDescription.pop_back();
361 state.selectedSlot >= 0)
363 std::u16string
added;
365 bool changed =
false;
368 std::size_t
units = 1;
380 if (changed) ++
state.revision;
385 const int x =
static_cast<int>(message.
numberA);
386 const int y =
static_cast<int>(message.
numberB);
392 state.selectedSlot =
static_cast<int>(
slot);
393 state.draftDescription.clear();
407 result.closeDialog =
true;
408 result.playClick =
true;
414 std::expected<std::vector<std::uint8_t>, std::string>
418 state.selectedSlot < 0 ||
420 return std::unexpected(
"no load-game slot is selected");
421 const auto slot =
static_cast<std::size_t
>(
state.selectedSlot);
423 return std::unexpected(
"selected load-game slot is empty");
425 if (!
bytes)
return std::unexpected(
bytes.error());
426 if (
bytes->size() < 12U)
427 return std::unexpected(
"saved game blob is shorter than the retail minimum");
436 std::string customBoardName,
439 if (
state.pendingSaveSlot)
440 return std::unexpected(
"a save-game request is already pending");
442 return std::unexpected(
"no local save-game requester exists");
444 state.selectedSlot < 0 ||
446 return std::unexpected(
"no save-game slot is selected");
448 const auto slot =
static_cast<std::size_t
>(
state.selectedSlot);
457 metadata.customBoardName = std::move(customBoardName);
460 metadata.squareGameEarnings[square] =
471 state.pendingSaveSlot.reset();
472 state.pendingMetadata = {};
478 std::span<const std::uint8_t>
gameBlob)
481 return std::unexpected(
"no pending save-game request exists");
482 const auto slot = *
state.pendingSaveSlot;
486 return std::unexpected(
"RULE returned an empty save-game blob");
490 return std::unexpected(
"SDL base path is unavailable for savegame storage");
491 std::error_code
error;
492 std::filesystem::create_directories(
root,
error);
494 return std::unexpected(
"unable to create savegame directory: " +
error.message());
516 if (
state.selectedSlot < 0 ||
518 return std::unexpected(
"no saved-game metadata slot is selected");
519 const auto slot =
static_cast<std::size_t
>(
state.selectedSlot);
521 return std::unexpected(
"selected saved-game metadata slot is empty");
527 ruleState.
squares[square].gameEarnings =
528 static_cast<std::int64_t
>(
metadata.squareGameEarnings[square]);
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
std::deque< Bytes > output
void closeSaveDialog(SaveRuntimeState &state) noexcept
SaveDialogInput processSaveDialogInput(SaveRuntimeState &state, const uimsg::Message &message, fonts::Runtime *fontRuntime, std::shared_ptr< const data::ResourceSnapshot > resources)
std::expected< void, std::string > persistPendingSave(SaveRuntimeState &state, std::span< const std::uint8_t > gameBlob)
Rect saveSlotRect(std::size_t slot) noexcept
Rect dialogOkayRect(int width, int height) noexcept
constexpr std::size_t SaveSlotCount
std::expected< std::vector< std::uint8_t >, std::string > readSelectedGameBlob(const SaveRuntimeState &state)
void clearPendingSave(SaveRuntimeState &state) noexcept
std::expected< void, std::string > refreshSaveSlots(SaveRuntimeState &state, FileDialogMode mode)
std::filesystem::path gameMetadataPath(std::size_t slot)
std::filesystem::path gameBlobPath(std::size_t slot)
std::expected< void, std::string > applySelectedMetadata(const SaveRuntimeState &state, rules::GameState &ruleState, int &city, int &system)
std::filesystem::path saveGameDirectory()
std::expected< void, std::string > beginPendingSave(SaveRuntimeState &state, const rules::GameState &ruleState, int city, int system, std::string customBoardName, rules::PlayerNumber requester)
Rect dialogCancelRect(int width, int height) noexcept
std::uint8_t PlayerNumber
constexpr std::size_t SquareCount
constexpr PlayerNumber MaxPlayers
constexpr PlayerNumber NobodyPlayer
std::shared_ptr< const data::ResourceSnapshot > resources() noexcept
bool contains(int x, int y) const noexcept
std::array< SquareState, SquareCount > squares