载入中...
搜索中...
未找到
HexSerializer.cpp
浏览该文件的文档.
2
3#include "common/Diagnostic.h"
4#include "hexmap/HexMetrics.h"
5
6#include <bit>
7#include <cstddef>
8#include <cstdint>
9#include <string>
10#include <utility>
11#include <vector>
12
13namespace eve::hexmap {
14namespace {
15
17constexpr std::size_t kMagicSize = sizeof(kHexSaveMagic);
18
20constexpr std::size_t kHeaderSize = kMagicSize + 4 + 4 + 4 + 4 + 8;
21
23constexpr std::size_t kCellRecordSize = 8;
24
26constexpr std::size_t kUnitRecordSize = 8;
27
35constexpr std::uint32_t kMaxGridDimension = static_cast<std::uint32_t>(kMaxHexGridDimension);
36
38constexpr std::uint32_t kKnownFlagMask = 0x3fu | (0x3fu << 6) | (0x3fu << 12) | (1u << 18) | (1u << 20) | (1u << 21);
39
49[[nodiscard]] bool isRepresentableCell(const HexCellData& cell) noexcept {
50 const std::int32_t elevation = cell.values.elevation();
51 if (elevation < HexMetrics::kMinElevation || elevation > HexMetrics::kMaxElevation) return false;
52 const std::int32_t water = cell.values.waterLevel();
53 if (water < 0 || water > HexMetrics::kMaxElevation) return false;
54 if (cell.values.terrainType() < 0 || cell.values.terrainType() >= kHexTerrainTypeCount) return false;
55 if (cell.values.specialIndex() < 0 || cell.values.specialIndex() >= 4) return false;
56 // Bits outside the documented layout are unspecified; a payload that sets one was
57 // not written by this version.
58 if ((cell.flags.raw() & ~kKnownFlagMask) != 0u) return false;
59 // The same-edge rule `setOutgoingRiver` enforces: a river may not both enter and
60 // leave through one edge.
61 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
62 const auto direction = static_cast<HexDirection>(i);
63 if (cell.flags.hasRiverIn(direction) && cell.flags.hasRiverOut(direction)) return false;
64 }
65 return true;
66}
67
69[[nodiscard]] Diagnostic invalidArgument(std::string message) {
71}
72
74void pushU32(std::vector<std::uint8_t>& out, std::uint32_t value) {
75 out.push_back(static_cast<std::uint8_t>(value & 0xffu));
76 out.push_back(static_cast<std::uint8_t>((value >> 8) & 0xffu));
77 out.push_back(static_cast<std::uint8_t>((value >> 16) & 0xffu));
78 out.push_back(static_cast<std::uint8_t>((value >> 24) & 0xffu));
79}
80
82void pushU64(std::vector<std::uint8_t>& out, std::uint64_t value) {
83 for (std::uint32_t shift = 0; shift < 64; shift += 8)
84 out.push_back(static_cast<std::uint8_t>((value >> shift) & 0xffu));
85}
86
88void pushI32(std::vector<std::uint8_t>& out, std::int32_t value) { pushU32(out, static_cast<std::uint32_t>(value)); }
89
91void pushF32(std::vector<std::uint8_t>& out, float value) { pushU32(out, std::bit_cast<std::uint32_t>(value)); }
92
100class Reader {
101public:
102 Reader(const std::uint8_t* data, std::size_t size) noexcept : data_(data), size_(size) {}
103
105 [[nodiscard]] bool ok() const noexcept { return ok_; }
107 [[nodiscard]] std::size_t offset() const noexcept { return offset_; }
109 [[nodiscard]] std::size_t remaining() const noexcept { return size_ - offset_; }
110
112 [[nodiscard]] bool seek(std::size_t offset) noexcept {
113 if (offset > size_) {
114 ok_ = false;
115 return false;
116 }
117 offset_ = offset;
118 return true;
119 }
120
122 [[nodiscard]] bool skip(std::size_t count) noexcept { return seek(offset_ + count); }
123
124 [[nodiscard]] std::uint32_t u32() noexcept {
125 if (remaining() < 4) {
126 ok_ = false;
127 return 0u;
128 }
129 std::uint32_t value = 0;
130 for (std::size_t index = 0; index < 4; ++index)
131 value |= static_cast<std::uint32_t>(data_[offset_ + index]) << (8u * static_cast<unsigned>(index));
132 offset_ += 4;
133 return value;
134 }
135
136 [[nodiscard]] std::uint64_t u64() noexcept {
137 if (remaining() < 8) {
138 ok_ = false;
139 return 0u;
140 }
141 std::uint64_t value = 0;
142 for (std::size_t index = 0; index < 8; ++index)
143 value |= static_cast<std::uint64_t>(data_[offset_ + index]) << (8u * static_cast<unsigned>(index));
144 offset_ += 8;
145 return value;
146 }
147
148 [[nodiscard]] std::int32_t i32() noexcept { return static_cast<std::int32_t>(u32()); }
149
150 [[nodiscard]] float f32() noexcept { return std::bit_cast<float>(u32()); }
151
152private:
153 const std::uint8_t* data_ = nullptr;
154 std::size_t size_ = 0;
155 std::size_t offset_ = 0;
156 bool ok_ = true;
157};
158
159} // namespace
160
161// --- writing -----------------------------------------------------------------
162
163Result<void> saveHexMap(const HexMap& map, const std::vector<HexUnitState>& units, std::vector<std::uint8_t>& out) {
164 if (map.empty()) return Result<void>::failure(invalidArgument("cannot serialize an empty hex map"));
165
166 const std::int32_t cellCount = map.cellCount();
167 const std::size_t payload =
168 kHeaderSize + static_cast<std::size_t>(cellCount) * kCellRecordSize + 4 + units.size() * kUnitRecordSize;
169 out.clear();
170 out.reserve(payload);
171
172 for (const std::uint8_t byte : kHexSaveMagic) out.push_back(byte);
173 pushU32(out, kHexSaveVersion);
174 pushU32(out, static_cast<std::uint32_t>(map.cellCountX()));
175 pushU32(out, static_cast<std::uint32_t>(map.cellCountZ()));
176 pushU32(out, map.seed());
177 pushU64(out, map.revision());
178
179 for (std::int32_t index = 0; index < cellCount; ++index) {
180 // `cellAt` is non-null for every index below `cellCount()`, the loop bound.
181 const HexCellData* cell = map.cellAt(index);
182 pushU32(out, cell->values.raw());
183 pushU32(out, cell->flags.raw());
184 }
185
186 pushU32(out, static_cast<std::uint32_t>(units.size()));
187 for (const HexUnitState& unit : units) {
188 pushI32(out, unit.locationIndex);
189 pushF32(out, unit.orientation);
190 }
191
192 return Result<void>::success();
193}
194
195// --- reading -----------------------------------------------------------------
196
197Result<void> loadHexMap(const std::vector<std::uint8_t>& bytes, HexMap& map, std::vector<HexUnitState>& units) {
198 if (bytes.size() < kHeaderSize) return Result<void>::failure(invalidArgument("hex save payload is truncated"));
199
200 for (std::size_t index = 0; index < kMagicSize; ++index) {
202 return Result<void>::failure(invalidArgument("hex save payload has an unknown magic prefix"));
203 }
204
205 Reader reader(bytes.data(), bytes.size());
206 // The payload size check above already covers the magic, so this can only fail
207 // if that check is ever relaxed.
208 if (!reader.skip(kMagicSize)) return Result<void>::failure(invalidArgument("hex save payload is truncated"));
209
210 const std::uint32_t version = reader.u32();
211 if (version != kHexSaveVersion) {
212 return Result<void>::failure(invalidArgument("hex save version " + std::to_string(version) +
213 " is not supported (expected " + std::to_string(kHexSaveVersion) +
214 ")"));
215 }
216 const std::uint32_t cellCountX = reader.u32();
217 const std::uint32_t cellCountZ = reader.u32();
218 const std::uint32_t seed = reader.u32();
219 // Diagnostic only: the restored map's revision is whatever `reset` produces.
220 const std::uint64_t savedRevision = reader.u64();
221 (void)savedRevision;
222
223 if (cellCountX == 0 || cellCountZ == 0)
224 return Result<void>::failure(invalidArgument("hex save grid dimensions must be positive"));
225 if (cellCountX > kMaxGridDimension || cellCountZ > kMaxGridDimension)
226 return Result<void>::failure(invalidArgument("hex save grid dimensions exceed the 512 cell limit"));
227 if (cellCountX % static_cast<std::uint32_t>(HexMetrics::kChunkSizeX) != 0u ||
228 cellCountZ % static_cast<std::uint32_t>(HexMetrics::kChunkSizeZ) != 0u)
230 invalidArgument("hex save grid dimensions must be a multiple of the 5x5 chunk size"));
231
232 const std::size_t cellCount = static_cast<std::size_t>(cellCountX) * static_cast<std::size_t>(cellCountZ);
233 const std::size_t cellsEnd = kHeaderSize + cellCount * kCellRecordSize;
234 const std::size_t unitsStart = cellsEnd + 4;
235 if (bytes.size() < unitsStart)
236 return Result<void>::failure(invalidArgument("hex save payload is truncated before the unit records"));
237
238 if (!reader.seek(cellsEnd)) return Result<void>::failure(invalidArgument("hex save payload is truncated"));
239 const std::uint32_t unitCount = reader.u32();
240 if (static_cast<std::size_t>(unitCount) > (bytes.size() - unitsStart) / kUnitRecordSize)
241 return Result<void>::failure(invalidArgument("hex save unit count exceeds the payload size"));
242 if (unitsStart + static_cast<std::size_t>(unitCount) * kUnitRecordSize != bytes.size())
243 return Result<void>::failure(invalidArgument("hex save payload size does not match its contents"));
244
245 // Decode into locals first: a rejected payload must leave both outputs alone.
246 std::vector<HexCellData> cells(cellCount);
247 if (!reader.seek(kHeaderSize)) return Result<void>::failure(invalidArgument("hex save payload is truncated"));
248 for (std::size_t index = 0; index < cellCount; ++index) {
249 cells[index].values = HexValues{reader.u32()};
250 cells[index].flags = HexFlags{reader.u32()};
251 if (!isRepresentableCell(cells[index]))
252 return Result<void>::failure(invalidArgument("hex save cell record is out of range"));
253 }
254
255 std::vector<HexUnitState> restored(unitCount);
256 if (!reader.seek(unitsStart)) return Result<void>::failure(invalidArgument("hex save payload is truncated"));
257 for (std::uint32_t index = 0; index < unitCount; ++index) {
258 const std::int32_t locationIndex = reader.i32();
259 const float orientation = reader.f32();
260 if (locationIndex < 0 || static_cast<std::size_t>(locationIndex) >= cellCount)
261 return Result<void>::failure(invalidArgument("hex save unit cell is outside the grid"));
262 restored[index].locationIndex = locationIndex;
263 restored[index].orientation = orientation;
264 }
265 if (!reader.ok()) return Result<void>::failure(invalidArgument("hex save payload is truncated"));
266
267 // The unit list must be legal *before* the grid is adopted, because the unit
268 // registry consumes it afterwards and would otherwise reject the whole list
269 // against a grid the caller had already replaced. `canHoldUnit` is the same
270 // terrain rule the registry applies through `isValidDestination`.
271 for (std::size_t index = 0; index < restored.size(); ++index) {
272 const auto cell = static_cast<std::size_t>(restored[index].locationIndex);
273 if (!canHoldUnit(cells[cell]))
274 return Result<void>::failure(invalidArgument("hex save unit stands on an unusable cell"));
275 for (std::size_t other = 0; other < index; ++other) {
276 if (restored[other].locationIndex == restored[index].locationIndex)
277 return Result<void>::failure(invalidArgument("hex save units occupy the same cell"));
278 }
279 }
280
281 // Everything decoded; only now are the outputs rebuilt.
282 auto resized = map.reset(static_cast<std::int32_t>(cellCountX), static_cast<std::int32_t>(cellCountZ), seed);
283 if (!resized.ok()) return Result<void>::failure(resized.status());
284 for (std::size_t index = 0; index < cellCount; ++index) {
285 map.setCellState(map.coordinatesAt(static_cast<std::int32_t>(index)), cells[index].values, cells[index].flags)
286 .ignore("every index below the freshly reset cell count is inside the grid");
287 }
288 map.markAllChunksDirty();
289 units = std::move(restored);
290 return Result<void>::success();
291}
292
293} // namespace eve::hexmap
double value
Value::Object payload
Stable, structured diagnostics shared by engine modules.
std::string message
float u
Definition Grass.cpp:233
Pointy-top hex metrics, directions and vertex helpers.
Versioned payload codec for a hex map and its units.
float elevation
size_t offset
std::uint64_t bytes
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::uint32_t seed
Definition PointSet.cpp:807
RoadLaneDirection direction
std::uint32_t count
Cell cell
TacticalUnit * unit
std::vector< UnitCandidate > units
TerrainWaterField water
float size
Definition TreeMesh.cpp:156
uint32_t index
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Packed boolean cell state (roads, rivers, walls, exploration).
Definition HexCell.h:123
An editable, chunked, pointy-top hex map.
Definition HexMap.h:72
bool empty() const noexcept
Whether the map holds any cell.
Definition HexMap.h:92
HexCoordinates coordinatesAt(std::int32_t index) const noexcept
Coordinates of a linear cell index; out-of-range indices return (0, 0).
Definition HexMap.cpp:69
std::uint64_t revision() const noexcept
Monotonic counter bumped by every mutation; used to detect stale meshes.
Definition HexMap.h:108
std::uint32_t seed() const noexcept
The seed this map was built with.
Definition HexMap.h:106
std::int32_t cellCountX() const noexcept
Number of columns.
Definition HexMap.h:94
void markAllChunksDirty() noexcept
Marks every chunk dirty.
Definition HexMap.cpp:152
Result< void > setCellState(HexCoordinates c, HexValues values, HexFlags flags)
Writes a complete cell record verbatim.
Definition HexMap.cpp:505
std::int32_t cellCount() const noexcept
Total number of cells.
Definition HexMap.h:104
Result< void > reset(std::int32_t cellCountX, std::int32_t cellCountZ, std::uint32_t seed)
Replaces the grid with a new cellCountX * cellCountZ map.
Definition HexMap.cpp:37
std::int32_t cellCountZ() const noexcept
Number of rows.
Definition HexMap.h:96
const HexCellData * cellAt(std::int32_t index) const noexcept
Cell record by linear index, or null when out of range. @ownership Borrowed; the map owns the record....
Definition HexMap.cpp:74
static constexpr int kChunkSizeZ
Chunk size in the Z dimension.
Definition HexMetrics.h:158
static constexpr int kChunkSizeX
Chunk size in the X dimension.
Definition HexMetrics.h:156
static constexpr int kMaxElevation
Maximum editable elevation.
Definition HexMetrics.h:162
Packed numeric cell state.
Definition HexCell.h:38
eve::Diagnostic Diagnostic
constexpr bool canHoldUnit(const HexCellData &cell) noexcept
Whether a cell record is in a state that can hold a unit.
Definition HexCell.h:261
constexpr std::uint8_t kHexSaveMagic[8]
Magic prefix of a hex map payload; ASCII EVEHEX\0\0.
constexpr std::int32_t kMaxHexGridDimension
Largest grid edge, in cells, any entry point may request.
Definition HexMap.h:47
constexpr std::int32_t kHexDirectionCount
Number of hex edges / facing directions.
Definition HexMetrics.h:62
constexpr std::uint32_t kHexSaveVersion
Format version written by saveHexMap.
Result< void > loadHexMap(const std::vector< std::uint8_t > &bytes, HexMap &map, std::vector< HexUnitState > &units)
Restores a map and its units from a payload produced by saveHexMap.
Result< void > saveHexMap(const HexMap &map, const std::vector< HexUnitState > &units, std::vector< std::uint8_t > &out)
Serializes a map and its units into a self-describing byte payload.
constexpr std::int32_t kHexTerrainTypeCount
Number of entries in the terrain palette.
Definition HexCell.h:21
HexDirection
Hex facing directions, counter-clockwise from north-east.
Definition HexMetrics.h:59
Vector2 seek(Vector2 position, Vector2 target, float maxSpeed)
Returns a velocity of at most maxSpeed directed from position to target.
Definition Steering.cpp:149
One cell of the hex map: packed values plus packed flags.
Definition HexCell.h:244
Serializable state of one unit.
Definition HexUnits.h:32