载入中...
搜索中...
未找到
ModuleAssembly.cpp
浏览该文件的文档.
2
3#include "common/Value.h"
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8#include <set>
9#include <utility>
10
11namespace eve::procgen {
12namespace {
13
14Result<void> reject(DiagnosticCode code, std::string message, std::string path) {
16 Diagnostic::error(code, std::move(message), std::move(path), {}, "procgen.moduleAssembly"));
17}
18
19struct Voxel {
20 int x;
21 int z;
22 int level;
23};
24
25int normalizedTurn(int turn) { return ((turn % 4) + 4) % 4; }
26
27Voxel rotateCell(int x, int z, int level, int width, int depth, int turn) {
28 switch (normalizedTurn(turn)) {
29 case 1: return {depth - 1 - z, x, level};
30 case 2: return {width - 1 - x, depth - 1 - z, level};
31 case 3: return {z, width - 1 - x, level};
32 default: return {x, z, level};
33 }
34}
35
36ModuleFacing rotateFacing(ModuleFacing facing, int turn) {
38 return static_cast<ModuleFacing>((static_cast<int>(facing) + normalizedTurn(turn)) % 4);
39}
40
41Voxel facingStep(ModuleFacing facing) {
42 switch (facing) {
43 case ModuleFacing::North: return {0, -1, 0};
44 case ModuleFacing::East: return {1, 0, 0};
45 case ModuleFacing::South: return {0, 1, 0};
46 case ModuleFacing::West: return {-1, 0, 0};
47 case ModuleFacing::Up: return {0, 0, 1};
48 case ModuleFacing::Down: return {0, 0, -1};
49 }
50 return {};
51}
52
56 return static_cast<ModuleFacing>((static_cast<int>(facing) + 2) % 4);
57}
58
59const ModuleDefinition* findDefinition(const std::vector<ModuleDefinition>& definitions, const std::string& id) {
60 const auto it =
61 std::find_if(definitions.begin(), definitions.end(), [&](const auto& item) { return item.id == id; });
62 return it == definitions.end() ? nullptr : &*it;
63}
64
65std::vector<Voxel> occupiedVoxels(const ModuleDefinition& definition, const ModulePlacement& placement) {
66 std::vector<Voxel> result;
67 result.reserve(static_cast<size_t>(definition.widthCells) * definition.depthCells * definition.heightLevels);
68 for (int level = 0; level < definition.heightLevels; ++level)
69 for (int z = 0; z < definition.depthCells; ++z)
70 for (int x = 0; x < definition.widthCells; ++x) {
71 const Voxel local =
72 rotateCell(x, z, level, definition.widthCells, definition.depthCells, placement.quarterTurn);
73 result.push_back({placement.cellX + local.x, placement.cellZ + local.z, placement.level + local.level});
74 }
75 return result;
76}
77
78struct WorldConnector {
79 int x;
80 int z;
81 int level;
83 const std::string* tag;
84 const std::string* accepts;
85};
86
87std::vector<WorldConnector> worldConnectors(const ModuleDefinition& definition, const ModulePlacement& placement) {
88 std::vector<WorldConnector> result;
89 result.reserve(definition.connectors.size());
90 for (const auto& connector : definition.connectors) {
91 const Voxel local = rotateCell(connector.cellX, connector.cellZ, connector.levelOffset, definition.widthCells,
92 definition.depthCells, placement.quarterTurn);
93 result.push_back({placement.cellX + local.x, placement.cellZ + local.z, placement.level + local.level,
94 rotateFacing(connector.facing, placement.quarterTurn), &connector.tag, &connector.accepts});
95 }
96 return result;
97}
98
99bool compatible(const WorldConnector& a, const WorldConnector& b) {
100 return *a.accepts == *b.tag && *b.accepts == *a.tag;
101}
102
103bool exact(const Value::Object& object, std::initializer_list<const char*> fields) {
104 std::set<std::string> expected;
105 for (const char* field : fields) expected.emplace(field);
106 if (object.size() != expected.size()) return false;
107 for (const auto& [key, value] : object) {
108 (void)value;
109 if (!expected.contains(key)) return false;
110 }
111 return true;
112}
113
114const double* number(const Value& value) { return value.getIf<double>(); }
115
116bool integer(const Value& value, int& output) {
117 const auto* encoded = value.getIf<int64_t>();
118 if (!encoded || *encoded < std::numeric_limits<int>::min() || *encoded > std::numeric_limits<int>::max())
119 return false;
120 output = static_cast<int>(*encoded);
121 return true;
122}
123
124int facingFromName(const std::string& name) {
125 static constexpr std::array<const char*, 6> names{"north", "east", "south", "west", "up", "down"};
126 const auto found = std::find(names.begin(), names.end(), name);
127 return found == names.end() ? -1 : static_cast<int>(found - names.begin());
128}
129
130} // namespace
131
132ModuleAssemblyPlan::ModuleAssemblyPlan(ModuleAssemblyConstraints constraints) : constraints_(std::move(constraints)) {}
133
134Result<void> ModuleAssemblyPlan::configure(float cellSize, float floorHeight, int minCellX, int maxCellX, int minCellZ,
135 int maxCellZ, int maxLevels, bool bounded, bool requireConnection) {
136 if (!definitions_.empty() || !placements_.empty())
138 "assembly constraints must be configured before definitions or placements", "constraints");
139 if (!std::isfinite(cellSize) || cellSize <= 0.f || !std::isfinite(floorHeight) || floorHeight <= 0.f ||
140 maxLevels <= 0 || (bounded && (minCellX > maxCellX || minCellZ > maxCellZ)))
141 return reject(DiagnosticCode::InvalidArgument, "assembly dimensions and bounds must be valid", "constraints");
142 constraints_.cellSize = cellSize;
143 constraints_.floorHeight = floorHeight;
144 constraints_.minCellX = minCellX;
145 constraints_.maxCellX = maxCellX;
146 constraints_.minCellZ = minCellZ;
147 constraints_.maxCellZ = maxCellZ;
148 constraints_.maxLevels = maxLevels;
149 constraints_.bounded = bounded;
150 constraints_.requireConnection = requireConnection;
152}
153
154Result<void> ModuleAssemblyPlan::setAllowedQuarterTurns(bool turn0, bool turn90, bool turn180, bool turn270) {
155 if (!definitions_.empty() || !placements_.empty())
157 "allowed rotations must be configured before definitions or placements", "constraints");
158 if (!turn0 && !turn90 && !turn180 && !turn270)
159 return reject(DiagnosticCode::InvalidArgument, "at least one module rotation must be allowed", "constraints");
160 constraints_.allowedQuarterTurns = {turn0, turn90, turn180, turn270};
162}
163
165 if (!definitions_.empty() || !placements_.empty())
167 "minimum support must be configured before definitions or placements", "constraints");
168 if (!std::isfinite(ratio) || ratio < 0.f || ratio > 1.f)
169 return reject(DiagnosticCode::InvalidArgument, "minimum support ratio must be between zero and one",
170 "constraints.minimumSupportRatio");
171 constraints_.minimumSupportRatio = ratio;
173}
174
176 if (!definitions_.empty() || !placements_.empty())
177 return reject(DiagnosticCode::PreconditionViolation, "JSON configuration requires an empty plan", "config");
178 if (json.size() > 1024U * 1024U)
179 return reject(DiagnosticCode::InvalidArgument, "assembly configuration exceeds 1 MiB", "config");
180 auto parsed = Value::fromJson(json);
181 if (!parsed) return Result<void>::failure(parsed.status());
182 const auto* root = parsed.value().getIf<Value::Object>();
183 if (!root || !exact(*root, {"schema", "version", "unknownFields", "constraints", "modules"}))
184 return reject(DiagnosticCode::InvalidArgument, "assembly configuration root fields are invalid", "config");
185 const auto *schema = root->at("schema").getIf<std::string>(),
186 *unknown = root->at("unknownFields").getIf<std::string>();
187 const auto* version = root->at("version").getIf<int64_t>();
188 const auto* constraints = root->at("constraints").getIf<Value::Object>();
189 const auto* modules = root->at("modules").getIf<Value::Array>();
190 if (!schema || *schema != "eve.procgen.module-assembly" || !version || *version != 1 || !unknown ||
191 *unknown != "reject" || !constraints || !modules || modules->empty() || modules->size() > 4096 ||
192 !exact(*constraints, {"cellSize", "floorHeight", "bounds", "maxLevels", "allowedQuarterTurns",
193 "requireConnection", "minimumSupportRatio"}))
194 return reject(DiagnosticCode::InvalidArgument, "assembly configuration schema is invalid", "config");
195
196 const auto *cellSize = number(constraints->at("cellSize")), *floorHeight = number(constraints->at("floorHeight")),
197 *support = number(constraints->at("minimumSupportRatio"));
198 const auto* bounds = constraints->at("bounds").getIf<Value::Object>();
199 const auto* turns = constraints->at("allowedQuarterTurns").getIf<Value::Array>();
200 const auto* requireConnection = constraints->at("requireConnection").getIf<bool>();
201 int maxLevels = 0;
202 if (!cellSize || !floorHeight || !support || !bounds || !turns || turns->size() != 4 || !requireConnection ||
203 !integer(constraints->at("maxLevels"), maxLevels) ||
204 !exact(*bounds, {"enabled", "minCellX", "maxCellX", "minCellZ", "maxCellZ"}))
205 return reject(DiagnosticCode::InvalidArgument, "assembly constraint fields are invalid", "constraints");
206 const auto* bounded = bounds->at("enabled").getIf<bool>();
207 int minX = 0, maxX = 0, minZ = 0, maxZ = 0;
208 std::array<bool, 4> allowed{};
209 for (size_t i = 0; i < allowed.size(); ++i) {
210 const auto* enabled = (*turns)[i].getIf<bool>();
211 if (!enabled)
212 return reject(DiagnosticCode::InvalidArgument, "rotation entries must be booleans", "constraints");
213 allowed[i] = *enabled;
214 }
215 if (!bounded || !integer(bounds->at("minCellX"), minX) || !integer(bounds->at("maxCellX"), maxX) ||
216 !integer(bounds->at("minCellZ"), minZ) || !integer(bounds->at("maxCellZ"), maxZ))
217 return reject(DiagnosticCode::InvalidArgument, "assembly bounds are invalid", "constraints.bounds");
218
219 ModuleAssemblyPlan candidate;
220 auto configured = candidate.configure(float(*cellSize), float(*floorHeight), minX, maxX, minZ, maxZ, maxLevels,
221 *bounded, *requireConnection);
222 if (!configured) return configured;
223 auto rotations = candidate.setAllowedQuarterTurns(allowed[0], allowed[1], allowed[2], allowed[3]);
224 if (!rotations) return rotations;
225 auto supported = candidate.setMinimumSupportRatio(float(*support));
226 if (!supported) return supported;
227 for (const Value& encodedModule : *modules) {
228 const auto* module = encodedModule.getIf<Value::Object>();
229 if (!module || !exact(*module, {"id", "widthCells", "depthCells", "heightLevels", "connectors"}))
230 return reject(DiagnosticCode::InvalidArgument, "module fields are invalid", "modules");
231 const auto* id = module->at("id").getIf<std::string>();
232 const auto* connectors = module->at("connectors").getIf<Value::Array>();
233 int width = 0, depth = 0, height = 0;
234 if (!id || !connectors || connectors->size() > 16384 || !integer(module->at("widthCells"), width) ||
235 !integer(module->at("depthCells"), depth) || !integer(module->at("heightLevels"), height))
236 return reject(DiagnosticCode::InvalidArgument, "module values are invalid", "modules");
237 auto registered = candidate.registerVolumeModuleType(*id, width, depth, height);
238 if (!registered) return registered;
239 for (const Value& encodedConnector : *connectors) {
240 const auto* connector = encodedConnector.getIf<Value::Object>();
241 if (!connector || !exact(*connector, {"cellX", "cellZ", "level", "facing", "tag", "accepts"}))
242 return reject(DiagnosticCode::InvalidArgument, "connector fields are invalid", *id);
243 int x = 0, z = 0, level = 0;
244 const auto *facing = connector->at("facing").getIf<std::string>(),
245 *tag = connector->at("tag").getIf<std::string>(),
246 *accepts = connector->at("accepts").getIf<std::string>();
247 if (!facing || !tag || !accepts || !integer(connector->at("cellX"), x) ||
248 !integer(connector->at("cellZ"), z) || !integer(connector->at("level"), level))
249 return reject(DiagnosticCode::InvalidArgument, "connector values are invalid", *id);
250 auto added = candidate.addVolumeConnector(*id, x, z, level, facingFromName(*facing), *tag, *accepts);
251 if (!added) return added;
252 }
253 }
254 *this = std::move(candidate);
256}
257
258Result<void> ModuleAssemblyPlan::registerModuleType(std::string id, int widthCells, int depthCells) {
259 return registerModule({std::move(id), widthCells, depthCells, {}});
260}
261
262Result<void> ModuleAssemblyPlan::registerVolumeModuleType(std::string id, int widthCells, int depthCells,
263 int heightLevels) {
264 ModuleDefinition definition{std::move(id), widthCells, depthCells, {}};
265 definition.heightLevels = heightLevels;
266 return registerModule(std::move(definition));
267}
268
269Result<void> ModuleAssemblyPlan::addConnector(const std::string& moduleId, int cellX, int cellZ, int facing,
270 std::string tag, std::string accepts) {
271 return addVolumeConnector(moduleId, cellX, cellZ, 0, facing, std::move(tag), std::move(accepts));
272}
273
274Result<void> ModuleAssemblyPlan::addVolumeConnector(const std::string& moduleId, int cellX, int cellZ, int levelOffset,
275 int facing, std::string tag, std::string accepts) {
276 auto definition =
277 std::find_if(definitions_.begin(), definitions_.end(), [&](const auto& item) { return item.id == moduleId; });
278 if (definition == definitions_.end())
279 return reject(DiagnosticCode::NotFound, "module definition is not registered", moduleId);
280 if (std::any_of(placements_.begin(), placements_.end(),
281 [&](const auto& placement) { return placement.moduleId == moduleId; }))
283 "connectors cannot change after the module has been placed", moduleId);
284 if (facing < 0 || facing > 5 || cellX < 0 || cellX >= definition->widthCells || cellZ < 0 ||
285 cellZ >= definition->depthCells || levelOffset < 0 || levelOffset >= definition->heightLevels || tag.empty() ||
286 accepts.empty())
287 return reject(DiagnosticCode::InvalidArgument, "connector fields are outside the module contract", moduleId);
288 definition->connectors.push_back(
289 {cellX, cellZ, static_cast<ModuleFacing>(facing), std::move(tag), std::move(accepts), levelOffset});
291}
292
294 if (definition.id.empty()) return reject(DiagnosticCode::InvalidArgument, "module id must not be empty", "id");
295 if (definition.widthCells <= 0 || definition.depthCells <= 0 || definition.heightLevels <= 0)
296 return reject(DiagnosticCode::InvalidArgument, "module volume dimensions must be positive", definition.id);
297 if (findDefinition(definitions_, definition.id))
298 return reject(DiagnosticCode::AlreadyExists, "module id is already registered", definition.id);
299 for (const auto& connector : definition.connectors) {
300 if (connector.cellX < 0 || connector.cellX >= definition.widthCells || connector.cellZ < 0 ||
301 connector.cellZ >= definition.depthCells || connector.levelOffset < 0 ||
302 connector.levelOffset >= definition.heightLevels || connector.tag.empty() || connector.accepts.empty())
303 return reject(DiagnosticCode::InvalidArgument, "connector must reference a volume cell and typed tags",
304 definition.id);
305 }
306 definitions_.push_back(std::move(definition));
308}
309
311 const ModuleDefinition* definition = findDefinition(definitions_, placement.moduleId);
312 if (!definition) return reject(DiagnosticCode::NotFound, "module definition is not registered", placement.moduleId);
313 placement.quarterTurn = normalizedTurn(placement.quarterTurn);
314 if (!constraints_.allowedQuarterTurns[static_cast<size_t>(placement.quarterTurn)])
315 return reject(DiagnosticCode::PreconditionViolation, "module rotation is disabled by the assembly constraints",
316 placement.moduleId);
317 if (placement.level < 0 || int64_t(placement.level) + definition->heightLevels > constraints_.maxLevels)
318 return reject(DiagnosticCode::PreconditionViolation, "module level is outside the configured range",
319 placement.moduleId);
320
321 const int width = placement.quarterTurn % 2 ? definition->depthCells : definition->widthCells;
322 const int depth = placement.quarterTurn % 2 ? definition->widthCells : definition->depthCells;
323 if (int64_t(placement.cellX) + width - 1 > std::numeric_limits<int>::max() ||
324 int64_t(placement.cellZ) + depth - 1 > std::numeric_limits<int>::max())
325 return reject(DiagnosticCode::InvalidArgument, "module coordinates overflow the grid range",
326 placement.moduleId);
327
328 const auto newVoxels = occupiedVoxels(*definition, placement);
329 std::vector<WorldConnector> existingConnectors;
330 std::vector<Voxel> existingVoxels;
331 bool matchedConnection = false;
332 for (const auto& existing : placements_) {
333 const auto* existingDefinition = findDefinition(definitions_, existing.moduleId);
334 const auto occupied = occupiedVoxels(*existingDefinition, existing);
335 {
336 for (const Voxel& candidate : newVoxels)
337 if (std::any_of(occupied.begin(), occupied.end(), [&](Voxel cell) {
338 return cell.x == candidate.x && cell.z == candidate.z && cell.level == candidate.level;
339 }))
340 return reject(DiagnosticCode::Conflict, "module volume overlaps an existing placement",
341 placement.moduleId);
342 auto connectors = worldConnectors(*existingDefinition, existing);
343 existingConnectors.insert(existingConnectors.end(), connectors.begin(), connectors.end());
344 existingVoxels.insert(existingVoxels.end(), occupied.begin(), occupied.end());
345 }
346 }
347 if (constraints_.bounded)
348 for (const Voxel& cell : newVoxels)
349 if (cell.x < constraints_.minCellX || cell.x > constraints_.maxCellX || cell.z < constraints_.minCellZ ||
350 cell.z > constraints_.maxCellZ)
351 return reject(DiagnosticCode::PreconditionViolation, "module footprint crosses assembly bounds",
352 placement.moduleId);
353
354 if (placement.level > 0 && constraints_.minimumSupportRatio > 0.f) {
355 const size_t bottomCellCount = size_t(definition->widthCells) * definition->depthCells;
356 size_t supportedCellCount = 0;
357 for (const Voxel& cell : newVoxels) {
358 if (cell.level != placement.level) continue;
359 if (std::any_of(existingVoxels.begin(), existingVoxels.end(), [&](Voxel other) {
360 return other.x == cell.x && other.z == cell.z && other.level == cell.level - 1;
361 }))
362 ++supportedCellCount;
363 }
364 const size_t requiredSupport =
365 static_cast<size_t>(std::ceil(double(constraints_.minimumSupportRatio) * double(bottomCellCount)));
366 if (supportedCellCount < requiredSupport)
368 "elevated module does not meet the configured vertical support ratio", placement.moduleId);
369 }
370
371 const auto connectors = worldConnectors(*definition, placement);
372 for (const Voxel& cell : newVoxels) {
373 for (int direction = 0; direction < 6; ++direction) {
374 const auto facing = static_cast<ModuleFacing>(direction);
375 const Voxel step = facingStep(facing);
376 const int64_t neighborX = int64_t(cell.x) + step.x;
377 const int64_t neighborZ = int64_t(cell.z) + step.z;
378 const int64_t neighborLevel = int64_t(cell.level) + step.level;
379 bool neighborOccupied = false;
380 neighborOccupied = std::any_of(existingVoxels.begin(), existingVoxels.end(), [&](Voxel other) {
381 return other.x == neighborX && other.z == neighborZ && other.level == neighborLevel;
382 });
383 if (!neighborOccupied) continue;
384 const auto own = std::find_if(connectors.begin(), connectors.end(), [&](const auto& connector) {
385 return connector.x == cell.x && connector.z == cell.z && connector.level == cell.level &&
386 connector.facing == facing;
387 });
388 const auto other =
389 std::find_if(existingConnectors.begin(), existingConnectors.end(), [&](const auto& connector) {
390 return connector.x == neighborX && connector.z == neighborZ && connector.level == neighborLevel &&
391 connector.facing == opposite(facing);
392 });
393 if (own == connectors.end() || other == existingConnectors.end() || !compatible(*own, *other))
394 return reject(DiagnosticCode::Conflict, "touching module edges do not expose compatible connectors",
395 placement.moduleId);
396 matchedConnection = true;
397 }
398 }
399 if (constraints_.requireConnection && !placements_.empty() && !matchedConnection)
400 return reject(DiagnosticCode::PreconditionViolation, "module is disconnected from the existing assembly",
401 placement.moduleId);
402 placements_.push_back(std::move(placement));
404}
405
407 return index >= 0 && static_cast<size_t>(index) < placements_.size()
408 ? placements_[static_cast<size_t>(index)].moduleId
409 : std::string{};
410}
411
412float ModuleAssemblyPlan::getPlacementX(int index) const noexcept {
413 return index >= 0 && static_cast<size_t>(index) < placements_.size()
414 ? float(placements_[static_cast<size_t>(index)].cellX) * constraints_.cellSize
415 : 0.f;
416}
417
418float ModuleAssemblyPlan::getPlacementY(int index) const noexcept {
419 return index >= 0 && static_cast<size_t>(index) < placements_.size()
420 ? float(placements_[static_cast<size_t>(index)].level) * constraints_.floorHeight
421 : 0.f;
422}
423
424float ModuleAssemblyPlan::getPlacementZ(int index) const noexcept {
425 return index >= 0 && static_cast<size_t>(index) < placements_.size()
426 ? float(placements_[static_cast<size_t>(index)].cellZ) * constraints_.cellSize
427 : 0.f;
428}
429
431 return index >= 0 && static_cast<size_t>(index) < placements_.size()
432 ? float(normalizedTurn(placements_[static_cast<size_t>(index)].quarterTurn)) * 90.f
433 : 0.f;
434}
435
436} // namespace eve::procgen
double value
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::string output
std::string message
DiagnosticCode code
std::uint32_t key
std::uint32_t height
std::uint32_t width
std::string local
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
const std::string * accepts
ModuleFacing facing
int level
const std::string * tag
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
RoadLaneDirection direction
double number
bool found
bool occupied
Cell cell
TacticalUnit::TurnResources turn
Json object
float step
Definition TreeMesh.cpp:314
float size
Definition TreeMesh.cpp:156
uint32_t index
std::uint32_t depth
int turns
Owning, renderer-independent dynamic values.
const std::map< std::string, Value > & definitions
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
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
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Deterministic owner of a validated modular assembly plan.
Result< void > registerVolumeModuleType(std::string id, int widthCells, int depthCells, int heightLevels)
Register a three-dimensional module volume using scalar script-friendly fields.
std::string getPlacementModule(int index) const
Return one accepted placement's module id, or empty text for an invalid index.
const ModuleAssemblyConstraints & constraints() const noexcept
Return the immutable constraint configuration.
Result< void > place(ModulePlacement placement)
Validate and atomically append one placement. @cost Linear in existing occupied cells and connectors.
Result< void > applyConfigJson(const std::string &json)
Atomically replace an empty plan's constraints and module definitions from strict JSON....
Result< void > setAllowedQuarterTurns(bool turn0, bool turn90, bool turn180, bool turn270)
Configure the four permitted quarter-turn rotations before registration or placement.
Result< void > addConnector(const std::string &moduleId, int cellX, int cellZ, int facing, std::string tag, std::string accepts)
Append a typed connector to a registered, not-yet-placed module.
float getPlacementY(int index) const noexcept
Return one placement's world Y coordinate, or zero for an invalid index.
Result< void > registerModuleType(std::string id, int widthCells, int depthCells)
Register a module footprint using scalar script-friendly fields.
ModuleAssemblyPlan()=default
Construct an empty plan with default constraints.
float getPlacementX(int index) const noexcept
Return one placement's world X coordinate, or zero for an invalid index.
float getPlacementZ(int index) const noexcept
Return one placement's world Z coordinate, or zero for an invalid index.
Result< void > configure(float cellSize, float floorHeight, int minCellX, int maxCellX, int minCellZ, int maxCellZ, int maxLevels, bool bounded, bool requireConnection)
Configure an empty plan before definitions or placements are added.
Result< void > addVolumeConnector(const std::string &moduleId, int cellX, int cellZ, int levelOffset, int facing, std::string tag, std::string accepts)
Append a typed connector to one face of a cell in a module volume.
float getPlacementYawDegrees(int index) const noexcept
Return one placement's yaw in degrees, or zero for an invalid index.
Result< void > registerModule(ModuleDefinition definition)
Register one module definition before it is placed.
Result< void > setMinimumSupportRatio(float ratio)
Require this fraction of an elevated module's bottom cells to be supported from below.
constexpr HexDirection opposite(HexDirection d) noexcept
The direction opposite to d.
Definition HexMetrics.h:65
const EditorValue * field(const EditorValue &value, const char *name)
ModuleFacing
Horizontal or vertical face used by grid-module connection constraints.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
bool enabled
Configurable limits shared by automatic modular-map generators.
Immutable footprint and connection contract for one modular asset.
World-grid placement accepted by a ModuleAssemblyPlan.
glm::vec4 bounds