14float wrapDeg(
float deg) {
15 deg = std::fmod(deg, 360.f);
16 if (deg < 0.f) deg += 360.f;
20int cardinalQuarter(
float rotationDeg) {
21 const float d = wrapDeg(rotationDeg);
22 int q = int(std::lround(
d / 90.f)) % 4;
34std::unordered_map<std::string, PlacementSystem::ValidateFn> &PlacementSystem::validateRules() {
35 return inst().validateRules_;
38std::unordered_map<std::string, PlacementSystem::SnapFn> &PlacementSystem::snapRules() {
39 return inst().snapRules_;
42std::unordered_map<std::string, PlacementSystem::ChangeHook> &PlacementSystem::changeHooks() {
43 return inst().changeHooks_;
46std::unordered_map<std::string, PlacementSystem::SurfaceFn> &PlacementSystem::surfaces() {
47 return inst().surfaces_;
50std::vector<BuildingChangeEvent> &PlacementSystem::eventQueue() {
51 return inst().eventQueue_;
54int &PlacementSystem::instanceCounter() {
55 return inst().instanceCounter_;
58bool &PlacementSystem::builtinsReady() {
59 return inst().builtinsReady_;
63 if (
name.empty() || !
fn)
return;
64 validateRules()[
name] = std::move(
fn);
68 validateRules().erase(
name);
73 return validateRules().count(
name) > 0;
77 if (
name.empty() || !
fn)
return;
78 snapRules()[
name] = std::move(
fn);
85 return snapRules().count(
name) > 0;
89 if (
name.empty() || !
fn)
return;
90 changeHooks()[
name] = std::move(
fn);
96 return changeHooks().count(
name) > 0;
100 if (
name.empty() || !
fn)
return;
101 surfaces()[
name] = std::move(
fn);
108 return surfaces().count(
name) > 0;
114 auto &rules = surfaces();
115 auto it = rules.find(
name.empty() ?
"plane" :
name);
116 if (it == rules.end() || !it->second)
return false;
117 return it->second(world,
x,
y, hit);
122 std::vector<std::string> names;
123 names.reserve(surfaces().size());
124 for (
const auto &kv : surfaces()) names.push_back(kv.first);
132float &PlacementSystem::planeSurfaceHeight() {
133 return inst().planeSurfaceHeight_;
137 if (builtinsReady())
return;
138 builtinsReady() =
true;
153 r.
cellX = int(std::floor(worldX + 0.5f));
154 r.
cellY = int(std::floor(worldY + 0.5f));
172 std::string *reason) {
175 if (reason) *reason =
"unknown_building";
178 if (!checkBoundsAndOccupancy(world, *def, q,
true, reason))
return false;
179 if (!checkTerrain(world, *def, q, reason))
return false;
180 if (!checkAdjacency(world, *def, q, reason))
return false;
185 std::string *reason) {
188 if (reason) *reason =
"unknown_building";
191 return checkBoundsAndOccupancy(world, *def, q,
false, reason);
195 std::string *reason) {
198 if (reason) *reason =
"unknown_building";
201 if (!checkBoundsAndOccupancy(world, *def, q,
false, reason))
return false;
202 if (!checkTerrain(world, *def, q, reason))
return false;
203 if (!checkAdjacency(world, *def, q, reason))
return false;
209 if (!hit)
return false;
210 const float h = planeSurfaceHeight();
230 eventQueue().clear();
238 eventQueue().push_back(ev);
239 for (
auto &kv : changeHooks()) {
240 if (kv.second) kv.second(ev);
245 float worldX,
float worldY) {
247 auto &rules = snapRules();
248 auto it = rules.find(mode.empty() ?
"grid" : mode);
249 if (it == rules.end() || !it->second) {
250 it = rules.find(
"grid");
252 if (it != rules.end() && it->second)
return it->second(world, worldX, worldY);
262 float worldX,
float worldY) {
266 if (!def->snapMode.empty()) mode = def->snapMode;
272 float worldX,
float worldY,
float worldZ) {
288 const std::string mode = def ? def->
rotationMode :
"cardinal";
289 if (mode ==
"none")
return 0.f;
290 if (mode ==
"free")
return wrapDeg(rotationDeg);
292 const float d = wrapDeg(rotationDeg);
293 const int steps = int(std::lround(
d / 60.f)) % 6;
294 return float((steps < 0 ? steps + 6 : steps) * 60);
297 return float(cardinalQuarter(rotationDeg) * 90);
305 const int steps = int(std::lround(wrapDeg(rotationDeg) / 60.f)) % 6;
309 const int q = cardinalQuarter(rotationDeg);
310 if (q == 1 || q == 3) std::swap(
w,
h);
317 int originCellY,
float rotationDeg,
318 const std::function<
bool(
int cx,
int cy)> &
fn) {
319 if (!
fn)
return false;
322 hex ? int(std::lround(wrapDeg(rotationDeg) / 60.f)) % 6 : cardinalQuarter(rotationDeg);
325 [&](
int lx,
int ly) {
326 if (!fn(originCellX + lx, originCellY + ly)) ok = false;
331bool PlacementSystem::checkBoundsAndOccupancy(
const PlacementWorld &world,
334 std::string *reason) {
337 if (!world.inBounds(cx, cy)) {
338 if (reason) *reason =
"out_of_bounds";
342 if (checkOccupancy) {
345 if (reason) *reason =
"occupied";
355bool PlacementSystem::checkTerrain(
const PlacementWorld &world,
const BuildingDefinition &def,
356 const PlacementQuery &q, std::string *reason) {
357 if (def.requireTerrain.empty() && def.forbidTerrain.empty())
return true;
359 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](
int cx,
int cy) {
360 const int sem = world.getTerrain(cx, cy);
361 if (!def.forbidTerrain.empty()) {
362 if (std::find(def.forbidTerrain.begin(), def.forbidTerrain.end(), sem) !=
363 def.forbidTerrain.end()) {
364 if (reason) *reason =
"terrain_forbidden";
369 if (!def.requireTerrain.empty()) {
370 if (std::find(def.requireTerrain.begin(), def.requireTerrain.end(), sem) ==
371 def.requireTerrain.end()) {
372 if (reason) *reason =
"terrain_mismatch";
382bool PlacementSystem::checkAdjacency(
const PlacementWorld &world,
const BuildingDefinition &def,
383 const PlacementQuery &q, std::string *reason) {
384 const bool needTag = !def.requireAdjacentTag.empty();
385 const bool needTerrain = def.requireAdjacentTerrain >= 0;
386 if (!needTag && !needTerrain)
return true;
389 std::vector<std::pair<int, int>> cells;
390 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](
int cx,
int cy) {
391 cells.emplace_back(cx, cy);
395 auto isFootprint = [&](
int x,
int y) {
396 return std::find(cells.begin(), cells.end(), std::make_pair(
x,
y)) != cells.end();
399 bool tagOk = !needTag;
400 bool terrainOk = !needTerrain;
401 static const int dx[4] = {1, -1, 0, 0};
402 static const int dy[4] = {0, 0, 1, -1};
404 for (
const auto &
c : cells) {
405 for (
int i = 0; i < 4; ++i) {
406 const int nx =
c.first + dx[i];
407 const int ny =
c.second + dy[i];
408 if (isFootprint(nx, ny))
continue;
409 if (!world.inBounds(nx, ny))
continue;
411 if (needTag && !tagOk) {
412 const int occ = world.getAnyOccupant(nx, ny);
413 if (occ != 0 && occ != q.excludeInstanceId) {
414 auto it = world.buildings().find(occ);
415 if (it != world.buildings().end()) {
416 const PlacedBuilding &pb = it->second;
417 bool hit = pb.hasTag(def.requireAdjacentTag);
419 if (
const BuildingDefinition *odef =
420 BuildingRegistry::find(pb.buildingId)) {
421 hit = odef->hasTag(def.requireAdjacentTag);
424 if (hit) tagOk =
true;
428 if (needTerrain && !terrainOk) {
429 if (world.getTerrain(nx, ny) == def.requireAdjacentTerrain) terrainOk =
true;
431 if (tagOk && terrainOk)
return true;
435 if (needTag && !tagOk) {
436 if (reason) *reason =
"adjacency_tag";
439 if (needTerrain && !terrainOk) {
440 if (reason) *reason =
"adjacency_terrain";
446bool PlacementSystem::runValidate(
const PlacementWorld &world,
const BuildingDefinition &def,
447 const PlacementQuery &q, std::string *reason) {
449 std::string rule = def.validateRule.empty() ? world.getValidateRule() : def.validateRule;
450 if (rule.empty()) rule =
"default";
451 auto &rules = validateRules();
452 auto it = rules.find(rule);
453 if (it == rules.end() || !it->second) {
454 it = rules.find(
"default");
456 if (it == rules.end() || !it->second) {
457 if (reason) *reason =
"validate_rejected";
460 return it->second(world, q, reason);
463bool PlacementSystem::canPlaceElev(
PlacementWorld *world,
const std::string &buildingId,
int cellX,
464 int cellY,
float elevation,
float rotationDeg,
465 int excludeInstanceId, std::string *reason) {
468 if (reason) *reason =
"no_world";
473 if (reason) *reason =
"unknown_building";
480 q.
rotationDeg = normalizeRotation(buildingId, rotationDeg);
484 return runValidate(*world, *def, q, reason);
487bool PlacementSystem::canPlace(
PlacementWorld *world,
const std::string &buildingId,
int cellX,
488 int cellY,
float rotationDeg,
int excludeInstanceId,
489 std::string *reason) {
490 return canPlaceElev(world, buildingId, cellX, cellY, 0.f, rotationDeg, excludeInstanceId,
498 [&](
int cx,
int cy) {
499 if (world.inBounds(cx, cy)) {
501 size_t(cy) * size_t(world.getWidth()) + size_t(cx);
502 if (idx < occ.size()) occ[idx] = instanceId;
508void PlacementSystem::clearOccupancy(PlacementWorld &world,
int instanceId) {
510 for (
int &
v : world.occupancy()) {
511 if (
v == instanceId)
v = 0;
513 for (
auto &kv : world.allChannels()) {
514 for (
int &
v : kv.second) {
515 if (
v == instanceId)
v = 0;
520int PlacementSystem::placeAt(
PlacementWorld *world,
const std::string &buildingId,
int cellX,
521 int cellY,
float rotationDeg) {
523 if (!world)
return 0;
526 const float rot = normalizeRotation(buildingId, rotationDeg);
527 if (!canPlace(world, buildingId, cellX, cellY, rot, 0,
nullptr))
return 0;
540 writeOccupancy(*world, *def, pb, pb.
instanceId);
542 world->instanceOrder_.push_back(pb.
instanceId);
560int PlacementSystem::placeAtWorld(
PlacementWorld *world,
const std::string &buildingId,
561 float worldX,
float worldY,
float rotationDeg) {
563 if (!world)
return 0;
564 const SnapResult s = snap(*world, buildingId, worldX, worldY);
565 const int id = placeAt(world, buildingId,
s.cellX,
s.cellY, rotationDeg);
566 if (
id <= 0)
return 0;
570 it->second.worldX =
s.worldX;
571 it->second.worldY =
s.worldY;
576int PlacementSystem::placeAtWorld3D(
PlacementWorld *world,
const std::string &buildingId,
577 float worldX,
float worldY,
float worldZ,
580 if (!world)
return 0;
581 const SnapResult s = snap3D(*world, buildingId, worldX, worldY, worldZ);
582 const int id = placeAt(world, buildingId,
s.cellX,
s.cellY, rotationDeg);
583 if (
id <= 0)
return 0;
586 it->second.worldX =
s.worldX;
587 it->second.worldY =
s.worldY;
588 it->second.elevation =
s.elevation;
594 if (!world || !ghost)
return 0;
595 if (!ghost->
validate(world))
return 0;
598 if (
id <= 0)
return 0;
610 if (!world || instanceId <= 0)
return false;
611 auto it = world->
buildings().find(instanceId);
612 if (it == world->
buildings().end())
return false;
619 ev.
cellX = it->second.originCellX;
620 ev.
cellY = it->second.originCellY;
622 ev.
worldX = it->second.worldX;
623 ev.
worldY = it->second.worldY;
625 ev.
channel = it->second.channel;
627 clearOccupancy(*world, instanceId);
629 auto &order = world->instanceOrder_;
630 order.erase(std::remove(order.begin(), order.end(), instanceId), order.end());
635bool PlacementSystem::moveBuilding(
PlacementWorld *world,
int instanceId,
int cellX,
int cellY,
637 if (!world || instanceId <= 0)
return false;
638 auto it = world->
buildings().find(instanceId);
639 if (it == world->
buildings().end())
return false;
644 if (!canPlace(world, pb.
buildingId, cellX, cellY, rot, instanceId,
nullptr))
return false;
647 if (!def)
return false;
659 float px = 0.f, py = 0.f;
666 clearOccupancy(*world, instanceId);
671 writeOccupancy(*world, *def, pb, instanceId);
679 std::vector<int> ids = world->instanceOrder_;
680 for (
int id : ids) removeBuilding(world,
id);
682 world->instanceOrder_.clear();
684 std::fill(kv.second.begin(), kv.second.end(), 0);
SettlementPipeline::Stage fn
static const BuildingDefinition * find(const std::string &id)
float getElevation() const
bool validate(PlacementWorld *world)
对当前姿态做校验,写入 valid_/reason_。
float getRotationDeg() const
std::string getBuildingId() const
std::function< bool(const PlacementWorld &world, const PlacementQuery &q, std::string *reason)> ValidateFn
static SnapResult snap(const PlacementWorld &world, const std::string &buildingId, float worldX, float worldY)
static void effectiveFootprint(const BuildingDefinition &def, float rotationDeg, int *outW, int *outH)
旋转后的占地宽高(cardinal 90/270 交换)。
std::function< bool(const PlacementWorld &world, float x, float y, PlacementHit *hit)> SurfaceFn
static std::vector< std::string > surfaceNames()
static void registerSurface(const std::string &name, SurfaceFn fn)
static SnapResult snap3D(const PlacementWorld &world, const std::string &buildingId, float worldX, float worldY, float worldZ)
static PlacementSystem & inst()
Process singleton holding all PlacementSystem state.
static const std::vector< BuildingChangeEvent > & events()
static bool surfaceHit(const PlacementWorld &world, const std::string &name, float x, float y, PlacementHit *hit)
std::function< SnapResult(const PlacementWorld &world, float worldX, float worldY)> SnapFn
static float getPlaneSurfaceHeight()
static void unregisterChangeHook(const std::string &name)
static void setPlaneSurfaceHeight(float h)
static void ensureBuiltins()
确保内置规则已注册(模块首次使用时自动调用)。
static bool hasSurface(const std::string &name)
std::function< void(const BuildingChangeEvent &ev)> ChangeHook
static void pushEvent(BuildingChangeEvent ev)
static bool foreachFootprintCell(const BuildingDefinition &def, int originCellX, int originCellY, float rotationDeg, const std::function< bool(int cx, int cy)> &fn)
枚举占地格子(旋转后局部 → 世界格子)。返回 false 若定义未知。
static float normalizeRotation(const std::string &buildingId, float rotationDeg)
规范化旋转角(cardinal → 0/90/180/270;none → 0)。
static bool hasSnapRule(const std::string &name)
static void clearEvents()
static void unregisterSurface(const std::string &name)
static bool hasChangeHook(const std::string &name)
static void registerSnapRule(const std::string &name, SnapFn fn)
static bool hasValidateRule(const std::string &name)
static void pollEvents(std::vector< BuildingChangeEvent > &out)
static void unregisterValidateRule(const std::string &name)
static int nextInstanceId()
static void unregisterSnapRule(const std::string &name)
static void registerChangeHook(const std::string &name, ChangeHook fn)
static SnapResult snapWithMode(const PlacementWorld &world, const std::string &mode, float worldX, float worldY)
static void registerValidateRule(const std::string &name, ValidateFn fn)
int worldToCellX(float worldX) const
世界像素 ↔ 格子坐标。
int getWidth() const
尺寸 / 格子大小 / 原点。
const std::unordered_map< int, PlacedBuilding > & buildings() const
const grid::GridConfig & getGrid() const
std::string getSnapMode() const
放置策略:吸附模式 / 校验规则。
std::string getId() const
世界 id(用于变更事件定位)。
float cellToWorldX(int cellX) const
float cellToWorldY(int cellY) const
int worldToCellY(float worldY) const
void cellToWorldPlane(int cellX, int cellY, float &px, float &py) const
const std::unordered_map< std::string, std::vector< int > > & allChannels() const
int getOccupantInChannel(const std::string &channel, int cellX, int cellY) const
std::vector< int > & channelOccupancy(const std::string &channel)
建筑放置模块入口:定义 / 放置世界 / 鬼影 / 变更事件的脚本绑定点。 设计文档:docs/dev/建筑放置系统设计.md
void worldToCell(const GridConfig &cfg, float px, float py, int &cx, int &cy, int mapW, int mapH)
void foreachRotatedFootprint(int w, int h, const std::vector< uint8_t > &mask, int steps, bool hexMode, const std::function< void(int lx, int ly)> &fn)
void rotatedFootprintSize(int w, int h, const std::vector< uint8_t > &mask, int steps, bool hexMode, int &outW, int &outH)
一次成功放置变更的事件(供脚本 poll / C++ hook)。
std::string action
place / remove / move / rotate
std::string channel
占地格子尺寸。
std::string rotationMode
none | cardinal | free。
std::vector< uint8_t > footprintMask
可选占地掩码,长度 footprintW*footprintH;空表示实心矩形。行主序、原点在最小 x/y。
std::vector< std::string > tags
std::vector< std::string > tags
int excludeInstanceId
move 时排除自身占用