diff --git a/binaries/data/mods/mod/art/actors/actor.rng b/binaries/data/mods/mod/art/actors/actor.rng
index c4216e83ac..8a5d9dd486 100644
--- a/binaries/data/mods/mod/art/actors/actor.rng
+++ b/binaries/data/mods/mod/art/actors/actor.rng
@@ -214,6 +214,16 @@
+
+
+
+ upright
+ pitch
+ roll
+ pitch-roll
+
+
+
diff --git a/source/graphics/ObjectBase.cpp b/source/graphics/ObjectBase.cpp
index 89650a6750..719b675a0f 100644
--- a/source/graphics/ObjectBase.cpp
+++ b/source/graphics/ObjectBase.cpp
@@ -60,6 +60,7 @@ CObjectBase::CObjectBase(CObjectManager& objectManager, CActorDef& actorDef, u8
: m_ObjectManager(objectManager), m_ActorDef(actorDef)
{
m_QualityLevel = qualityLevel;
+ m_Properties.m_AnchorType = "";
m_Properties.m_CastShadows = false;
m_Properties.m_FloatOnWater = false;
@@ -82,6 +83,7 @@ bool CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root)
// Define all the elements used in the XML file
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
+ EL(anchor);
EL(castshadow);
EL(float);
EL(group);
@@ -143,6 +145,8 @@ bool CObjectBase::Load(const CXeromyces& XeroFile, const XMBElement& root)
return false;
}
}
+ else if (child_name == el_anchor)
+ m_Properties.m_AnchorType = child.GetText();
else if (child_name == el_castshadow)
m_Properties.m_CastShadows = true;
else if (child_name == el_float)
diff --git a/source/graphics/ObjectBase.h b/source/graphics/ObjectBase.h
index fecda0cfd0..e0a23d18ca 100644
--- a/source/graphics/ObjectBase.h
+++ b/source/graphics/ObjectBase.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -159,6 +159,8 @@ public:
struct {
+ // whether and how to adapt the rotation to the terrrain slope below
+ CStr m_AnchorType;
// cast shadows from this object
bool m_CastShadows;
// float on top of water
diff --git a/source/simulation2/components/CCmpPosition.cpp b/source/simulation2/components/CCmpPosition.cpp
index 66643ea66a..baeb14b869 100644
--- a/source/simulation2/components/CCmpPosition.cpp
+++ b/source/simulation2/components/CCmpPosition.cpp
@@ -59,13 +59,16 @@ public:
// Template state:
- enum
+ enum class AnchorType
{
+ UNDEFINED = -1, // Used for m_ActorAnchorType only since it's optional.
UPRIGHT = 0,
PITCH = 1,
PITCH_ROLL = 2,
ROLL = 3,
- } m_AnchorType;
+ };
+
+ AnchorType m_AnchorType;
bool m_Floating;
entity_pos_t m_FloatDepth;
@@ -94,6 +97,7 @@ public:
std::set m_Turrets;
// Not serialized:
+ AnchorType m_ActorAnchorType;
float m_InterpolatedRotX, m_InterpolatedRotY, m_InterpolatedRotZ;
float m_LastInterpolatedRotX, m_LastInterpolatedRotZ;
bool m_ActorFloating;
@@ -135,15 +139,8 @@ public:
void Init(const CParamNode& paramNode) override
{
- const std::string& anchor = paramNode.GetChild("Anchor").ToString();
- if (anchor == "pitch")
- m_AnchorType = PITCH;
- else if (anchor == "pitch-roll")
- m_AnchorType = PITCH_ROLL;
- else if (anchor == "roll")
- m_AnchorType = ROLL;
- else
- m_AnchorType = UPRIGHT;
+ m_AnchorType = ParseAnchorString(paramNode.GetChild("Anchor").ToString());
+ m_ActorAnchorType = AnchorType::UNDEFINED;
m_InWorld = false;
@@ -199,19 +196,19 @@ public:
const char* anchor = "???";
switch (m_AnchorType)
{
- case PITCH:
+ case AnchorType::PITCH:
anchor = "pitch";
break;
- case PITCH_ROLL:
+ case AnchorType::PITCH_ROLL:
anchor = "pitch-roll";
break;
- case ROLL:
+ case AnchorType::ROLL:
anchor = "roll";
break;
- case UPRIGHT: // upright is the default
+ case AnchorType::UPRIGHT: // upright is the default
default:
anchor = "upright";
break;
@@ -493,6 +490,11 @@ public:
AdvertiseInterpolatedPositionChanges();
}
+ void SetActorAnchor(const CStr& anchor) override
+ {
+ m_ActorAnchorType = ParseAnchorString(anchor);
+ }
+
void SetConstructionProgress(fixed progress) override
{
m_ConstructionProgress = progress;
@@ -854,6 +856,17 @@ public:
private:
+ AnchorType ParseAnchorString(const CStr& anchor)
+ {
+ if (anchor == "pitch")
+ return AnchorType::PITCH;
+ if (anchor == "roll")
+ return AnchorType::ROLL;
+ if (anchor == "pitch-roll")
+ return AnchorType::PITCH_ROLL;
+ return AnchorType::UPRIGHT;
+ }
+
/*
* Must be called whenever m_RotY or m_InterpolatedRotY change,
* to determine whether we need to call Interpolate to make the unit rotate.
@@ -935,7 +948,8 @@ private:
return;
}
- if (m_AnchorType == UPRIGHT || !m_RotZ.IsZero() || !m_RotX.IsZero())
+ AnchorType anchor = m_ActorAnchorType == AnchorType::UNDEFINED ? m_AnchorType : m_ActorAnchorType;
+ if (anchor == AnchorType::UPRIGHT || !m_RotZ.IsZero() || !m_RotX.IsZero())
{
// set the visual rotations to the ones fixed by the interface
m_InterpolatedRotX = m_RotX.ToFloat();
@@ -961,10 +975,10 @@ private:
normal.Z = projected.Y;
// project and calculate the angles
- if (m_AnchorType == PITCH || m_AnchorType == PITCH_ROLL)
+ if (anchor == AnchorType::PITCH || anchor == AnchorType::PITCH_ROLL)
m_InterpolatedRotX = -atan2(normal.Z, normal.Y);
- if (m_AnchorType == ROLL || m_AnchorType == PITCH_ROLL)
+ if (anchor == AnchorType::ROLL || anchor == AnchorType::PITCH_ROLL)
m_InterpolatedRotZ = atan2(normal.X, normal.Y);
}
};
diff --git a/source/simulation2/components/CCmpVisualActor.cpp b/source/simulation2/components/CCmpVisualActor.cpp
index 1694ce79bf..8cb19c3206 100644
--- a/source/simulation2/components/CCmpVisualActor.cpp
+++ b/source/simulation2/components/CCmpVisualActor.cpp
@@ -464,7 +464,7 @@ public:
if (!m_Unit || !m_Unit->GetAnimation() || !m_Unit->GetID())
return;
- m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str());
+ m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str());
}
void SelectMovementAnimation(const std::string& name, fixed speed) override
@@ -638,10 +638,15 @@ void CCmpVisualActor::InitModel()
model.ToCModelDecal()->RemoveShadowsReceive();
}
+ CStr anchor = m_Unit->GetObject().m_Base->m_Properties.m_AnchorType;
bool floating = m_Unit->GetObject().m_Base->m_Properties.m_FloatOnWater;
CmpPtr cmpPosition(GetEntityHandle());
if (cmpPosition)
+ {
+ if (!anchor.empty())
+ cmpPosition->SetActorAnchor(anchor);
cmpPosition->SetActorFloating(floating);
+ }
if (!m_ModelTag.valid())
{
diff --git a/source/simulation2/components/ICmpPosition.h b/source/simulation2/components/ICmpPosition.h
index 3ed1337a2b..ec7e8f5f19 100644
--- a/source/simulation2/components/ICmpPosition.h
+++ b/source/simulation2/components/ICmpPosition.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -23,6 +23,7 @@
#include "simulation2/helpers/Position.h"
#include "maths/FixedVector3D.h"
#include "maths/FixedVector2D.h"
+#include "ps/CStr.h"
#include
@@ -158,6 +159,13 @@ public:
*/
virtual void SetActorFloating(bool flag) = 0;
+
+ /**
+ * Set the entity's anchor type, in a non-network-synchronised visual-only way.
+ * (This is to support the 'anchor' flag in actor XMLs.)
+ */
+ virtual void SetActorAnchor(const CStr& anchor) = 0;
+
/**
* Set construction progress of the model, this affects the rendered position of the model.
* 0.0 will be fully underground, 1.0 will be fully visible, 0.5 will be half underground.
diff --git a/source/simulation2/components/tests/test_RangeManager.h b/source/simulation2/components/tests/test_RangeManager.h
index 075e656d0e..7001102673 100644
--- a/source/simulation2/components/tests/test_RangeManager.h
+++ b/source/simulation2/components/tests/test_RangeManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2024 Wildfire Games.
+/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -60,6 +60,7 @@ public:
bool CanFloat() const override { return false; }
void SetFloating(bool UNUSED(flag)) override { }
void SetActorFloating(bool UNUSED(flag)) override { }
+ void SetActorAnchor(const CStr& UNUSED(anchor)) override { }
void SetConstructionProgress(fixed UNUSED(progress)) override { }
CFixedVector3D GetPosition() const override { return m_Pos; }
CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); }
diff --git a/source/simulation2/components/tests/test_TerritoryManager.h b/source/simulation2/components/tests/test_TerritoryManager.h
index 5c85ded49b..d50b14d42c 100644
--- a/source/simulation2/components/tests/test_TerritoryManager.h
+++ b/source/simulation2/components/tests/test_TerritoryManager.h
@@ -123,6 +123,7 @@ public:
bool CanFloat() const override { return false; }
void SetFloating(bool) override {}
void SetActorFloating(bool) override {}
+ void SetActorAnchor(const CStr&) override {}
void SetConstructionProgress(fixed) override {}
CFixedVector3D GetPosition() const override { return m_Pos; }
CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); }