Index: ps/trunk/source/graphics/UnitManager.cpp =================================================================== --- ps/trunk/source/graphics/UnitManager.cpp (revision 26269) +++ ps/trunk/source/graphics/UnitManager.cpp (revision 26270) @@ -1,99 +1,107 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Container that owns all units */ #include "precompiled.h" #include #include "Model.h" #include "UnitManager.h" #include "Unit.h" #include "ObjectManager.h" #include "ObjectEntry.h" #include "ps/Game.h" #include "ps/World.h" #include /////////////////////////////////////////////////////////////////////////////// // CUnitManager constructor CUnitManager::CUnitManager() : m_ObjectManager(NULL) { } /////////////////////////////////////////////////////////////////////////////// // CUnitManager destructor CUnitManager::~CUnitManager() { DeleteAll(); } /////////////////////////////////////////////////////////////////////////////// // AddUnit: add given unit to world void CUnitManager::AddUnit(CUnit* unit) { m_Units.push_back(unit); } /////////////////////////////////////////////////////////////////////////////// // RemoveUnit: remove given unit from world, but don't delete it void CUnitManager::RemoveUnit(CUnit* unit) { // find entry in list typedef std::vector::iterator Iter; Iter i=std::find(m_Units.begin(),m_Units.end(),unit); if (i!=m_Units.end()) { m_Units.erase(i); } } /////////////////////////////////////////////////////////////////////////////// // DeleteUnit: remove given unit from world and delete it void CUnitManager::DeleteUnit(CUnit* unit) { RemoveUnit(unit); delete unit; } /////////////////////////////////////////////////////////////////////////////// // DeleteAll: remove and delete all units void CUnitManager::DeleteAll() { for (size_t i=0;iGetModel().SetTerrainDirty(i0, j0, i1, j1); +} Index: ps/trunk/source/graphics/UnitManager.h =================================================================== --- ps/trunk/source/graphics/UnitManager.h (revision 26269) +++ ps/trunk/source/graphics/UnitManager.h (revision 26270) @@ -1,66 +1,72 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Container that owns all units */ #ifndef INCLUDED_UNITMANAGER #define INCLUDED_UNITMANAGER #include "ps/CStrForward.h" #include #include class CUnit; class CObjectManager; /////////////////////////////////////////////////////////////////////////////// // CUnitManager: simple container class holding all units within the world class CUnitManager { public: // constructor, destructor CUnitManager(); ~CUnitManager(); // add given unit to world void AddUnit(CUnit* unit); // remove given unit from world, but don't delete it void RemoveUnit(CUnit* unit); // remove given unit from world and delete it void DeleteUnit(CUnit* unit); // remove and delete all units void DeleteAll(); // creates a new unit and adds it to the world CUnit* CreateUnit(const CStrW& actorName, uint32_t seed); // return the units const std::vector& GetUnits() const { return m_Units; } void SetObjectManager(CObjectManager& objectManager) { m_ObjectManager = &objectManager; } + /** + * Mark a specific region of the terrain as dirty. + * Coordinates are in terrain tiles, lower inclusive, upper exclusive. + */ + void MakeTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags); + private: // list of all known units std::vector m_Units; // graphical object manager; may be NULL if not set up CObjectManager* m_ObjectManager; }; #endif Index: ps/trunk/source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 26269) +++ ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 26270) @@ -1,799 +1,789 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpVisual.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializedTypes.h" #include "ICmpFootprint.h" #include "ICmpIdentity.h" #include "ICmpMirage.h" #include "ICmpOwnership.h" #include "ICmpPosition.h" #include "ICmpTemplateManager.h" #include "ICmpTerrain.h" #include "ICmpUnitMotion.h" #include "ICmpUnitRenderer.h" #include "ICmpValueModificationManager.h" #include "ICmpVisibility.h" #include "ICmpSound.h" #include "graphics/Decal.h" #include "graphics/Model.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "graphics/Unit.h" #include "graphics/UnitAnimation.h" #include "graphics/UnitManager.h" #include "maths/BoundingSphere.h" #include "maths/Frustum.h" #include "maths/Matrix3D.h" #include "maths/Vector3D.h" #include "ps/CLogger.h" #include "ps/GameSetup/Config.h" #include "renderer/Scene.h" class CCmpVisualActor : public ICmpVisual { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); - componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_Create); componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(VisualActor) private: std::wstring m_BaseActorName, m_ActorName; bool m_IsFoundationActor; // Not initialized in non-visual mode CUnit* m_Unit; CModelAbstract::CustomSelectionShape* m_ShapeDescriptor = nullptr; fixed m_R, m_G, m_B; // shading color // Current animation state std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; std::wstring m_SoundGroup; fixed m_AnimDesync; fixed m_AnimSyncRepeatTime; // 0.0 if not synced fixed m_AnimSyncOffsetTime; std::map m_VariantSelections; u32 m_Seed; // seed used for random variations bool m_ConstructionPreview; bool m_VisibleInAtlasOnly; bool m_IsActorOnly; // an in-world entity should not have this or it might not be rendered. bool m_SilhouetteDisplay; bool m_SilhouetteOccluder; bool m_DisableShadows; ICmpUnitRenderer::tag_t m_ModelTag; public: static std::string GetSchema() { return "Display the unit using the engine's actor system." "" "units/hellenes/infantry_spearman_b.xml" "" "" "structures/hellenes/barracks.xml" "structures/fndn_4x4.xml" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { m_Unit = NULL; m_R = m_G = m_B = fixed::FromInt(1); m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); m_Seed = GetEntityId(); m_IsFoundationActor = paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk(); m_BaseActorName = paramNode.GetChild(m_IsFoundationActor ? "FoundationActor" : "Actor").ToWString(); ParseActorName(m_BaseActorName); m_VisibleInAtlasOnly = paramNode.GetChild("VisibleInAtlasOnly").ToBool(); m_IsActorOnly = paramNode.GetChild("ActorOnly").IsOk(); m_SilhouetteDisplay = paramNode.GetChild("SilhouetteDisplay").ToBool(); m_SilhouetteOccluder = paramNode.GetChild("SilhouetteOccluder").ToBool(); m_DisableShadows = paramNode.GetChild("DisableShadows").ToBool(); // Initialize the model's selection shape descriptor. This currently relies on the component initialization order; the // Footprint component must be initialized before this component (VisualActor) to support the ability to use the footprint // shape for the selection box (instead of the default recursive bounding box). See TypeList.h for the order in // which components are initialized; if for whatever reason you need to get rid of this dependency, you can always just // initialize the selection shape descriptor on-demand. InitSelectionShapeDescriptor(paramNode); } virtual void Deinit() { if (m_Unit) { GetSimContext().GetUnitManager().DeleteUnit(m_Unit); m_Unit = NULL; } } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("r", m_R); serialize.NumberFixed_Unbounded("g", m_G); serialize.NumberFixed_Unbounded("b", m_B); serialize.StringASCII("anim name", m_AnimName, 0, 256); serialize.Bool("anim once", m_AnimOnce); serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed); serialize.String("sound group", m_SoundGroup, 0, 256); serialize.NumberFixed_Unbounded("anim desync", m_AnimDesync); serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime); serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime); Serializer(serialize, "variation", m_VariantSelections); serialize.NumberU32_Unbounded("seed", m_Seed); serialize.String("actor", m_ActorName, 0, 256); // TODO: store actor variables? } virtual void Serialize(ISerializer& serialize) { // TODO: store the actor name, if !debug and it differs from the template if (serialize.IsDebug()) { serialize.String("base actor", m_BaseActorName, 0, 256); } SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 oldSeed = GetActorSeed(); SerializeCommon(deserialize); InitModel(); // If we serialized a different seed or different actor, reload actor if (oldSeed != GetActorSeed() || m_BaseActorName != m_ActorName) ReloadActor(); else ReloadUnitAnimation(); if (m_Unit) { CmpPtr cmpOwnership(GetEntityHandle()); if (cmpOwnership) m_Unit->GetModel().SetPlayerID(cmpOwnership->GetOwner()); } } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_OwnershipChanged: { RecomputeActorName(); if (!m_Unit) break; const CMessageOwnershipChanged& msgData = static_cast (msg); m_Unit->GetModel().SetPlayerID(msgData.to); break; } - case MT_TerrainChanged: - { - if (!m_Unit) - break; - - const CMessageTerrainChanged& msgData = static_cast (msg); - m_Unit->GetModel().SetTerrainDirty(msgData.i0, msgData.j0, msgData.i1, msgData.j1); - break; - } case MT_ValueModification: { // Mirages don't respond to technology modifications. CmpPtr cmpMirage(GetEntityHandle()); if (cmpMirage) return; const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"VisualActor") break; RecomputeActorName(); break; } case MT_InterpolatedPositionChanged: { const CMessageInterpolatedPositionChanged& msgData = static_cast (msg); if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); cmpModelRenderer->UpdateUnitPos(m_ModelTag, msgData.inWorld, msgData.pos0, msgData.pos1); } break; } case MT_Create: { InitModel(); SelectAnimation("idle"); break; } case MT_Destroy: { if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); cmpModelRenderer->RemoveUnit(m_ModelTag); m_ModelTag = ICmpUnitRenderer::tag_t(); } break; } } } virtual CBoundingBoxAligned GetBounds() const { if (!m_Unit) return CBoundingBoxAligned::EMPTY; return m_Unit->GetModel().GetWorldBounds(); } virtual CUnit* GetUnit() { return m_Unit; } virtual CBoundingBoxOriented GetSelectionBox() const { if (!m_Unit) return CBoundingBoxOriented::EMPTY; return m_Unit->GetModel().GetSelectionBox(); } virtual CVector3D GetPosition() const { if (!m_Unit) return CVector3D(0, 0, 0); return m_Unit->GetModel().GetTransform().GetTranslation(); } virtual std::wstring GetProjectileActor() const { if (!m_Unit) return L""; return m_Unit->GetObject().m_ProjectileModelName; } virtual CFixedVector3D GetProjectileLaunchPoint() const { if (!m_Unit) return CFixedVector3D(); if (m_Unit->GetModel().ToCModel()) { // Ensure the prop transforms are correct CmpPtr cmpUnitRenderer(GetSystemEntity()); CmpPtr cmpPosition(GetEntityHandle()); if (cmpUnitRenderer && cmpPosition) { float frameOffset = cmpUnitRenderer->GetFrameOffset(); CMatrix3D transform(cmpPosition->GetInterpolatedTransform(frameOffset)); m_Unit->GetModel().SetTransform(transform); m_Unit->GetModel().ValidatePosition(); } CModelAbstract* ammo = m_Unit->GetModel().ToCModel()->FindFirstAmmoProp(); if (ammo) { CVector3D vector = ammo->GetTransform().GetTranslation(); return CFixedVector3D(fixed::FromFloat(vector.X), fixed::FromFloat(vector.Y), fixed::FromFloat(vector.Z)); } } return CFixedVector3D(); } virtual void SetVariant(const CStr& key, const CStr& selection) { if (m_VariantSelections[key] == selection) return; m_VariantSelections[key] = selection; if (m_Unit) { m_Unit->SetEntitySelection(key, selection); if (m_Unit->GetAnimation()) m_Unit->GetAnimation()->ReloadAnimation(); } } virtual std::string GetAnimationName() const { return m_AnimName; } virtual void SelectAnimation(const std::string& name, bool once = false, fixed speed = fixed::FromInt(1)) { m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; m_SoundGroup = L""; m_AnimDesync = fixed::FromInt(1)/20; // TODO: make this an argument m_AnimSyncRepeatTime = fixed::Zero(); m_AnimSyncOffsetTime = fixed::Zero(); SetVariant("animation", m_AnimName); CmpPtr cmpSound(GetEntityHandle()); if (cmpSound) m_SoundGroup = cmpSound->GetSoundGroup(wstring_from_utf8(m_AnimName)); 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()); } virtual void SelectMovementAnimation(const std::string& name, fixed speed) { ENSURE(name == "idle" || name == "walk" || name == "run"); if (m_AnimName != "idle" && m_AnimName != "walk" && m_AnimName != "run") return; if (m_AnimName == name && speed == m_AnimSpeed) return; SelectAnimation(name, false, speed); } virtual void SetAnimationSyncRepeat(fixed repeattime) { m_AnimSyncRepeatTime = repeattime; if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat()); } virtual void SetAnimationSyncOffset(fixed actiontime) { m_AnimSyncOffsetTime = actiontime; if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } virtual void SetShadingColor(fixed r, fixed g, fixed b, fixed a) { m_R = r; m_G = g; m_B = b; UNUSED2(a); // TODO: why is this even an argument? if (m_Unit) { CModelAbstract& model = m_Unit->GetModel(); model.SetShadingColor(CColor(m_R.ToFloat(), m_G.ToFloat(), m_B.ToFloat(), 1.0f)); } } virtual void SetVariable(const std::string& name, float value) { if (m_Unit) m_Unit->GetModel().SetEntityVariable(name, value); } virtual u32 GetActorSeed() const { return m_Seed; } virtual void SetActorSeed(u32 seed) { if (seed == m_Seed) return; m_Seed = seed; ReloadActor(); } virtual void RecomputeActorName() { CmpPtr cmpValueModificationManager(GetSystemEntity()); std::wstring newActorName; if (m_IsFoundationActor) newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/FoundationActor", m_BaseActorName, GetEntityId()); else newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/Actor", m_BaseActorName, GetEntityId()); if (newActorName != m_ActorName) { ParseActorName(newActorName); ReloadActor(); } } virtual bool HasConstructionPreview() const { return m_ConstructionPreview; } virtual void Hotload(const VfsPath& name) { if (!m_Unit) return; if (!name.empty() && name != m_ActorName) return; ReloadActor(); } private: // Replace {phenotype} with the correct value in m_ActorName void ParseActorName(std::wstring base); /// Helper function shared by component init and actor reloading void InitModel(); /// Helper method; initializes the model selection shape descriptor from XML. Factored out for readability of @ref Init. void InitSelectionShapeDescriptor(const CParamNode& paramNode); // ReloadActor is used when the actor or seed changes. void ReloadActor(); // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical. // It is also used by ReloadActor. void ReloadUnitAnimation(); }; REGISTER_COMPONENT_TYPE(VisualActor) // ------------------------------------------------------------------------------------------------------------------ void CCmpVisualActor::ParseActorName(std::wstring base) { CmpPtr cmpIdentity(GetEntityHandle()); const std::wstring pattern = L"{phenotype}"; if (cmpIdentity) { size_t pos = base.find(pattern); while (pos != std::string::npos) { base.replace(pos, pattern.size(), cmpIdentity->GetPhenotype()); pos = base.find(pattern, pos + pattern.size()); } } m_ActorName = base; } void CCmpVisualActor::InitModel() { if (!GetSimContext().HasUnitManager()) return; std::wstring actorName = m_ActorName; if (actorName.find(L".xml") == std::wstring::npos) actorName += L".xml"; m_Unit = GetSimContext().GetUnitManager().CreateUnit(actorName, GetActorSeed()); if (!m_Unit) return; CModelAbstract& model = m_Unit->GetModel(); if (model.ToCModel()) { u32 modelFlags = 0; if (m_SilhouetteDisplay) modelFlags |= MODELFLAG_SILHOUETTE_DISPLAY; if (m_SilhouetteOccluder) modelFlags |= MODELFLAG_SILHOUETTE_OCCLUDER; CmpPtr cmpVisibility(GetEntityHandle()); if (cmpVisibility && cmpVisibility->GetAlwaysVisible()) modelFlags |= MODELFLAG_IGNORE_LOS; model.ToCModel()->AddFlagsRec(modelFlags); } if (m_DisableShadows) { if (model.ToCModel()) model.ToCModel()->RemoveShadowsRec(); else if (model.ToCModelDecal()) model.ToCModelDecal()->RemoveShadows(); } m_Unit->SetID(GetEntityId()); bool floating = m_Unit->GetObject().m_Base->m_Properties.m_FloatOnWater; CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition) cmpPosition->SetActorFloating(floating); if (!m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); if (cmpModelRenderer) { // TODO: this should account for all possible props, animations, etc, // else we might accidentally cull the unit when it should be visible CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec(); CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds); int flags = 0; if (m_IsActorOnly) flags |= ICmpUnitRenderer::ACTOR_ONLY; if (m_VisibleInAtlasOnly) flags |= ICmpUnitRenderer::VISIBLE_IN_ATLAS_ONLY; m_ModelTag = cmpModelRenderer->AddUnit(GetEntityHandle(), m_Unit, boundSphere, flags); } } // the model is now responsible for cleaning up the descriptor if (m_ShapeDescriptor != nullptr) m_Unit->GetModel().SetCustomSelectionShape(m_ShapeDescriptor); } void CCmpVisualActor::InitSelectionShapeDescriptor(const CParamNode& paramNode) { // by default, we don't need a custom selection shape and we can just keep the default behaviour m_ShapeDescriptor = nullptr; const CParamNode& shapeNode = paramNode.GetChild("SelectionShape"); if (shapeNode.IsOk()) { if (shapeNode.GetChild("Bounds").IsOk()) { // default; no need to take action } else if (shapeNode.GetChild("Footprint").IsOk()) { CmpPtr cmpFootprint(GetEntityHandle()); if (cmpFootprint) { ICmpFootprint::EShape fpShape; // fp stands for "footprint" entity_pos_t fpSize0, fpSize1, fpHeight; // fp stands for "footprint" cmpFootprint->GetShape(fpShape, fpSize0, fpSize1, fpHeight); float size0 = fpSize0.ToFloat(); float size1 = fpSize1.ToFloat(); // TODO: we should properly distinguish between CIRCLE and SQUARE footprint shapes here, but since cylinders // aren't implemented yet and are almost indistinguishable from boxes for small enough sizes anyway, // we'll just use boxes for either case. However, for circular footprints the size0 and size1 values both // represent the radius, so we do have to adjust them to match the size1 and size0's of square footprints // (which represent the full width and depth). if (fpShape == ICmpFootprint::CIRCLE) { size0 *= 2; size1 *= 2; } m_ShapeDescriptor = new CModelAbstract::CustomSelectionShape; m_ShapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX; m_ShapeDescriptor->m_Size0 = size0; m_ShapeDescriptor->m_Size1 = size1; m_ShapeDescriptor->m_Height = fpHeight.ToFloat(); } else { LOGERROR("[VisualActor] Cannot apply footprint-based SelectionShape; Footprint component not initialized."); } } else if (shapeNode.GetChild("Box").IsOk()) { // TODO: we might need to support the ability to specify a different box center in the future m_ShapeDescriptor = new CModelAbstract::CustomSelectionShape; m_ShapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX; m_ShapeDescriptor->m_Size0 = shapeNode.GetChild("Box").GetChild("@width").ToFixed().ToFloat(); m_ShapeDescriptor->m_Size1 = shapeNode.GetChild("Box").GetChild("@depth").ToFixed().ToFloat(); m_ShapeDescriptor->m_Height = shapeNode.GetChild("Box").GetChild("@height").ToFixed().ToFloat(); } else if (shapeNode.GetChild("Cylinder").IsOk()) { LOGWARNING("[VisualActor] TODO: Cylinder selection shapes are not yet implemented; defaulting to recursive bounding boxes"); } else { // shouldn't happen by virtue of validation against schema LOGERROR("[VisualActor] No selection shape specified"); } } } void CCmpVisualActor::ReloadActor() { if (!m_Unit) return; // Save some data from the old unit CColor shading = m_Unit->GetModel().GetShadingColor(); player_id_t playerID = m_Unit->GetModel().GetPlayerID(); // Replace with the new unit GetSimContext().GetUnitManager().DeleteUnit(m_Unit); // HACK: selection shape needs template data, but rather than storing all that data // in the component, we load the template here and pass it into a helper function CmpPtr cmpTemplateManager(GetSystemEntity()); const CParamNode* node = cmpTemplateManager->LoadLatestTemplate(GetEntityId()); ENSURE(node && node->GetChild("VisualActor").IsOk()); InitSelectionShapeDescriptor(node->GetChild("VisualActor")); InitModel(); if (!m_Unit) { if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); if (cmpModelRenderer) cmpModelRenderer->RemoveUnit(m_ModelTag); m_ModelTag = ICmpUnitRenderer::tag_t{}; } return; } ReloadUnitAnimation(); m_Unit->GetModel().SetShadingColor(shading); m_Unit->GetModel().SetPlayerID(playerID); if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec(); CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds); cmpModelRenderer->UpdateUnit(m_ModelTag, m_Unit, boundSphere); } } void CCmpVisualActor::ReloadUnitAnimation() { if (!m_Unit) return; m_Unit->SetEntitySelection(m_VariantSelections); if (!m_Unit->GetAnimation()) return; m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); // We'll lose the exact synchronisation but we should at least make sure it's going at the correct rate if (!m_AnimSyncRepeatTime.IsZero()) m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat()); if (!m_AnimSyncOffsetTime.IsZero()) m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/ElevationHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/ElevationHandlers.cpp (revision 26269) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/ElevationHandlers.cpp (revision 26270) @@ -1,454 +1,459 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MessageHandler.h" #include "../CommandProc.h" +#include "graphics/RenderableObject.h" #include "graphics/Terrain.h" +#include "graphics/UnitManager.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/World.h" #include "maths/MathUtil.h" -#include "graphics/RenderableObject.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpTerrain.h" #include "../Brushes.h" #include "../DeltaArray.h" namespace AtlasMessage { class TerrainArray : public DeltaArray2D { public: void Init() { m_Heightmap = g_Game->GetWorld()->GetTerrain()->GetHeightMap(); m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide(); } void RaiseVertex(ssize_t x, ssize_t y, int amount) { // Ignore out-of-bounds vertices if (size_t(x) >= size_t(m_VertsPerSide) || size_t(y) >= size_t(m_VertsPerSide)) return; set(x, y, static_cast(Clamp(get(x,y) + amount, 0, 65535))); } void MoveVertexTowards(ssize_t x, ssize_t y, int target, int amount) { if (size_t(x) >= size_t(m_VertsPerSide) || size_t(y) >= size_t(m_VertsPerSide)) return; int h = get(x,y); if (h < target) h = std::min(target, h + amount); else if (h > target) h = std::max(target, h - amount); else return; set(x, y, static_cast(Clamp(h, 0, 65535))); } void SetVertex(ssize_t x, ssize_t y, u16 value) { if (size_t(x) >= size_t(m_VertsPerSide) || size_t(y) >= size_t(m_VertsPerSide)) return; set(x,y, value); } u16 GetVertex(ssize_t x, ssize_t y) { return get(Clamp(x, 0, m_VertsPerSide - 1), Clamp(y, 0, m_VertsPerSide - 1)); } protected: u16 getOld(ssize_t x, ssize_t y) { return m_Heightmap[y*m_VertsPerSide + x]; } void setNew(ssize_t x, ssize_t y, const u16& val) { m_Heightmap[y*m_VertsPerSide + x] = val; } u16* m_Heightmap; ssize_t m_VertsPerSide; }; ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(AlterElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cAlterElevation() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; // If the framerate is very high, 'amount' is often very // small (even zero) so the integer truncation is significant static float roundingError = 0.0; roundingError += msg->amount - (float)amount; if (roundingError >= 1.f) { amount += (int)roundingError; roundingError -= (float)(int)roundingError; } static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { // TODO: proper variable raise amount (store floats in terrain delta array?) float b = g_CurrentBrush.Get(dx, dy); if (b) m_TerrainDelta.RaiseVertex(x0+dx, y0+dy, (int)(amount*b)); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W; m_j1 = y0 + g_CurrentBrush.m_H; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cAlterElevation* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(AlterElevation) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(SmoothElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cSmoothElevation() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; // If the framerate is very high, 'amount' is often very // small (even zero) so the integer truncation is significant static float roundingError = 0.0; roundingError += msg->amount - (float)amount; if (roundingError >= 1.f) { amount += (int)roundingError; roundingError -= (float)(int)roundingError; } static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); if (g_CurrentBrush.m_H > 2) { std::vector terrainDeltas; ssize_t num = (g_CurrentBrush.m_H - 2) * (g_CurrentBrush.m_W - 2); terrainDeltas.resize(num); // For each vertex, compute the average of the 9 adjacent vertices for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { float delta = m_TerrainDelta.GetVertex(x0+dx, y0+dy) / 9.0f; ssize_t x1_min = std::max((ssize_t)1, dx - 1); ssize_t x1_max = std::min(dx + 1, g_CurrentBrush.m_W - 2); ssize_t y1_min = std::max((ssize_t)1, dy - 1); ssize_t y1_max = std::min(dy + 1, g_CurrentBrush.m_H - 2); for (ssize_t yy = y1_min; yy <= y1_max; ++yy) { for (ssize_t xx = x1_min; xx <= x1_max; ++xx) { ssize_t index = (yy-1)*(g_CurrentBrush.m_W-2) + (xx-1); terrainDeltas[index] += delta; } } } } // Move each vertex towards the computed average of its neighbours for (ssize_t dy = 1; dy < g_CurrentBrush.m_H - 1; ++dy) { for (ssize_t dx = 1; dx < g_CurrentBrush.m_W - 1; ++dx) { ssize_t index = (dy-1)*(g_CurrentBrush.m_W-2) + (dx-1); float b = g_CurrentBrush.Get(dx, dy); if (b) m_TerrainDelta.MoveVertexTowards(x0+dx, y0+dy, (int)terrainDeltas[index], (int)(amount*b)); } } } m_i0 = x0; m_j0 = y0; m_i1 = x0 + g_CurrentBrush.m_W - 1; m_j1 = y0 + g_CurrentBrush.m_H - 1; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cSmoothElevation* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(SmoothElevation) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(FlattenElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cFlattenElevation() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t xc, yc; g_CurrentBrush.GetCentre(xc, yc); u16 height = m_TerrainDelta.GetVertex(xc, yc); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { float b = g_CurrentBrush.Get(dx, dy); if (b) m_TerrainDelta.MoveVertexTowards(x0+dx, y0+dy, height, 1 + (int)(b*amount)); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W; m_j1 = y0 + g_CurrentBrush.m_H; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cFlattenElevation* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(FlattenElevation) BEGIN_COMMAND(PikeElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cPikeElevation() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; // If the framerate is very high, 'amount' is often very // small (even zero) so the integer truncation is significant static float roundingError = 0.0; roundingError += msg->amount - (float)amount; if (roundingError >= 1.f) { amount += (int)roundingError; roundingError -= (float)(int)roundingError; } static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); float h = ((float) g_CurrentBrush.m_H - 1) / 2.f; for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { float b = g_CurrentBrush.Get(dx, dy); if (b) { float x = (float)dx - ((float)g_CurrentBrush.m_H - 1) / 2.f; float y = (float)dy - ((float)g_CurrentBrush.m_W - 1) / 2.f; float distance = Clamp(1 - static_cast(sqrt(x * x + y * y)) / h, 0.01f, 1.0f); distance *= distance; m_TerrainDelta.RaiseVertex(x0 + dx, y0 + dy, (int)(amount * distance)); } } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W; m_j1 = y0 + g_CurrentBrush.m_H; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cPikeElevation* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(PikeElevation) } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 26269) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 26270) @@ -1,581 +1,575 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MessageHandler.h" #include "../CommandProc.h" #include "graphics/Patch.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "ps/Game.h" #include "ps/World.h" #include "lib/tex/tex.h" #include "ps/Filesystem.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/helpers/Grid.h" #include "../Brushes.h" #include "../DeltaArray.h" #include "../View.h" #include namespace AtlasMessage { namespace { sTerrainTexturePreview MakeEmptyTerrainTexturePreview() { sTerrainTexturePreview preview{}; preview.name = std::wstring(); preview.loaded = false; preview.imageHeight = 0; preview.imageWidth = 0; preview.imageData = {}; return preview; } bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b) { return (wcscmp(a.name.c_str(), b.name.c_str()) < 0); } sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, size_t width, size_t height) { sTerrainTexturePreview preview; preview.name = tex->GetTag().FromUTF8(); const size_t previewBPP = 3; std::vector buffer(width * height * previewBPP); // It's not good to shrink the entire texture to fit the small preview // window, since it's the fine details in the texture that are // interesting; so just go down one mipmap level, then crop a chunk // out of the middle. std::shared_ptr fileData; size_t fileSize; Tex texture; const bool canUsePreview = !tex->GetDiffuseTexturePath().empty() && g_VFS->LoadFile(tex->GetDiffuseTexturePath(), fileData, fileSize) == INFO::OK && texture.decode(fileData, fileSize) == INFO::OK && // Check that we can fit the texture into the preview size before any transform. texture.m_Width >= width && texture.m_Height >= height && // Transform to a single format that we can process. texture.transform_to((texture.m_Flags | TEX_MIPMAPS) & ~(TEX_DXT | TEX_GREY | TEX_BGR)) == INFO::OK && (texture.m_Bpp == 24 || texture.m_Bpp == 32); if (canUsePreview) { size_t level = 0; while ((texture.m_Width >> (level + 1)) >= width && (texture.m_Height >> (level + 1)) >= height) ++level; // Extract the middle section (as a representative preview), // and copy into buffer. u8* data = texture.GetMipLevelData(level); ENSURE(data); const size_t levelWidth = texture.m_Width >> level; const size_t levelHeight = texture.m_Height >> level; const size_t dataShiftX = (levelWidth - width) / 2; const size_t dataShiftY = (levelHeight - height) / 2; for (size_t y = 0; y < height; ++y) for (size_t x = 0; x < width; ++x) { const size_t bufferOffset = (y * width + x) * previewBPP; const size_t dataOffset = ((y + dataShiftY) * levelWidth + x + dataShiftX) * texture.m_Bpp / 8; buffer[bufferOffset + 0] = data[dataOffset + 0]; buffer[bufferOffset + 1] = data[dataOffset + 1]; buffer[bufferOffset + 2] = data[dataOffset + 2]; } preview.loaded = true; } else { // Too small to preview. Just use a flat color instead. const u32 baseColor = tex->GetBaseColor(); for (size_t i = 0; i < width * height; ++i) { buffer[i * previewBPP + 0] = (baseColor >> 16) & 0xff; buffer[i * previewBPP + 1] = (baseColor >> 8) & 0xff; buffer[i * previewBPP + 2] = (baseColor >> 0) & 0xff; } preview.loaded = tex->GetTexture()->IsLoaded(); } preview.imageWidth = width; preview.imageHeight = height; preview.imageData = buffer; return preview; } } // anonymous namespace QUERYHANDLER(GetTerrainGroups) { const CTerrainTextureManager::TerrainGroupMap &groups = g_TexMan.GetGroups(); std::vector groupNames; for (CTerrainTextureManager::TerrainGroupMap::const_iterator it = groups.begin(); it != groups.end(); ++it) groupNames.push_back(it->first.FromUTF8()); msg->groupNames = groupNames; } QUERYHANDLER(GetTerrainGroupTextures) { std::vector names; CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8()); if (group) { for (std::vector::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it) names.emplace_back((*it)->GetTag().FromUTF8()); } std::sort(names.begin(), names.end()); msg->names = names; } QUERYHANDLER(GetTerrainGroupPreviews) { std::vector previews; CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8()); for (std::vector::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it) { previews.push_back(GetPreview(*it, msg->imageWidth, msg->imageHeight)); } // Sort the list alphabetically by name std::sort(previews.begin(), previews.end(), CompareTerrain); msg->previews = previews; } QUERYHANDLER(GetTerrainPassabilityClasses) { CmpPtr cmpPathfinder(*AtlasView::GetView_Game()->GetSimulation2(), SYSTEM_ENTITY); if (cmpPathfinder) { std::map nonPathfindingClasses, pathfindingClasses; cmpPathfinder->GetPassabilityClasses(nonPathfindingClasses, pathfindingClasses); std::vector classNames; for (std::map::iterator it = nonPathfindingClasses.begin(); it != nonPathfindingClasses.end(); ++it) classNames.push_back(CStr(it->first).FromUTF8()); msg->classNames = classNames; } } QUERYHANDLER(GetTerrainTexture) { ssize_t x, y; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); g_CurrentBrush.GetCentre(x, y); CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CMiniPatch* tile = terrain->GetTile(x, y); if (tile) { CTerrainTextureEntry* tex = tile->GetTextureEntry(); msg->texture = tex->GetTag().FromUTF8(); } else { msg->texture = std::wstring(); } } QUERYHANDLER(GetTerrainTexturePreview) { CTerrainTextureEntry* tex = g_TexMan.FindTexture(CStrW(*msg->name).ToUTF8()); if (tex) msg->preview = GetPreview(tex, msg->imageWidth, msg->imageHeight); else msg->preview = MakeEmptyTerrainTexturePreview(); } ////////////////////////////////////////////////////////////////////////// namespace { struct TerrainTile { TerrainTile(CTerrainTextureEntry* t, ssize_t p) : tex(t), priority(p) {} CTerrainTextureEntry* tex; ssize_t priority; }; class TerrainArray : public DeltaArray2D { public: void Init() { m_Terrain = g_Game->GetWorld()->GetTerrain(); m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide(); } void UpdatePriority(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priorityScale, ssize_t& priority) { CMiniPatch* tile = m_Terrain->GetTile(x, y); if (!tile) return; // tile was out-of-bounds // If this tile matches the current texture, we just want to match its // priority; otherwise we want to exceed its priority if (tile->GetTextureEntry() == tex) priority = std::max(priority, tile->GetPriority()*priorityScale); else priority = std::max(priority, tile->GetPriority()*priorityScale + 1); } CTerrainTextureEntry* GetTexEntry(ssize_t x, ssize_t y) { if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return NULL; return get(x, y).tex; } ssize_t GetPriority(ssize_t x, ssize_t y) { if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return 0; return get(x, y).priority; } void PaintTile(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priority) { // Ignore out-of-bounds tiles if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return; set(x,y, TerrainTile(tex, priority)); } ssize_t GetTilesPerSide() { return m_VertsPerSide-1; } protected: TerrainTile getOld(ssize_t x, ssize_t y) { CMiniPatch* mp = m_Terrain->GetTile(x, y); ENSURE(mp); return TerrainTile(mp->Tex, mp->Priority); } void setNew(ssize_t x, ssize_t y, const TerrainTile& val) { CMiniPatch* mp = m_Terrain->GetTile(x, y); ENSURE(mp); mp->Tex = val.tex; mp->Priority = val.priority; } CTerrain* m_Terrain; ssize_t m_VertsPerSide; }; } BEGIN_COMMAND(PaintTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cPaintTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } // Priority system: If the new tile should have a high priority, // set it to one plus the maximum priority of all surrounding tiles // that aren't included in the brush (so that it's definitely the highest). // Similar for low priority. ssize_t priorityScale = (msg->priority == ePaintTerrainPriority::HIGH ? +1 : -1); ssize_t priority = 0; for (ssize_t dy = -1; dy < g_CurrentBrush.m_H+1; ++dy) { for (ssize_t dx = -1; dx < g_CurrentBrush.m_W+1; ++dx) { if (!(g_CurrentBrush.Get(dx, dy) > 0.5f)) // ignore tiles that will be painted over m_TerrainDelta.UpdatePriority(x0+dx, y0+dy, texentry, priorityScale, priority); } } for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { if (g_CurrentBrush.Get(dx, dy) > 0.5f) // TODO: proper solid brushes m_TerrainDelta.PaintTile(x0+dx, y0+dy, texentry, priority*priorityScale); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W + 1; m_j1 = y0 + g_CurrentBrush.m_H + 1; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cPaintTerrain* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(PaintTerrain) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(ReplaceTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cReplaceTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); - CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); - if (cmpTerrain) - cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); m_i0 = m_i1 = x0; m_j0 = m_j1 = y0; CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0); // Don't bother if we're not making a change if (texentry == replacedTex) { return; } ssize_t tiles = m_TerrainDelta.GetTilesPerSide(); for (ssize_t j = 0; j < tiles; ++j) { for (ssize_t i = 0; i < tiles; ++i) { if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex) { m_i0 = std::min(m_i0, i-1); m_j0 = std::min(m_j0, j-1); m_i1 = std::max(m_i1, i+2); m_j1 = std::max(m_j1, j+2); m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j)); } } } MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } }; END_COMMAND(ReplaceTerrain) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(FillTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cFillTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); - CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); - if (cmpTerrain) - cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); m_i0 = m_i1 = x0; m_j0 = m_j1 = y0; CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0); // Don't bother if we're not making a change if (texentry == replacedTex) { return; } ssize_t tiles = m_TerrainDelta.GetTilesPerSide(); // Simple 4-way flood fill algorithm using queue and a grid to keep track of visited tiles, // almost as fast as loop for filling whole map, much faster for small patches SparseGrid visited(tiles, tiles); std::queue > queue; // Initial tile queue.push(std::make_pair((u16)x0, (u16)y0)); visited.set(x0, y0, true); while(!queue.empty()) { // Check front of queue std::pair t = queue.front(); queue.pop(); u16 i = t.first; u16 j = t.second; if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex) { // Found a tile to replace: adjust bounds and paint it m_i0 = std::min(m_i0, (ssize_t)i-1); m_j0 = std::min(m_j0, (ssize_t)j-1); m_i1 = std::max(m_i1, (ssize_t)i+2); m_j1 = std::max(m_j1, (ssize_t)j+2); m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j)); // Visit 4 adjacent tiles (could visit 8 if we want to count diagonal adjacency) if (i > 0 && !visited.get(i-1, j)) { visited.set(i-1, j, true); queue.push(std::make_pair(i-1, j)); } if (i < (tiles-1) && !visited.get(i+1, j)) { visited.set(i+1, j, true); queue.push(std::make_pair(i+1, j)); } if (j > 0 && !visited.get(i, j-1)) { visited.set(i, j-1, true); queue.push(std::make_pair(i, j-1)); } if (j < (tiles-1) && !visited.get(i, j+1)) { visited.set(i, j+1, true); queue.push(std::make_pair(i, j+1)); } } } MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } }; END_COMMAND(FillTerrain) } // namespace AtlasMessage