Index: ps/trunk/source/renderer/ModelRenderer.cpp =================================================================== --- ps/trunk/source/renderer/ModelRenderer.cpp (revision 24486) +++ ps/trunk/source/renderer/ModelRenderer.cpp (revision 24487) @@ -1,773 +1,776 @@ /* Copyright (C) 2020 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 "graphics/Color.h" #include "graphics/LightEnv.h" #include "graphics/Material.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "lib/allocators/allocator_adapters.h" #include "lib/allocators/arena.h" #include "lib/hash.h" #include "lib/ogl.h" #include "maths/Vector3D.h" #include "maths/Vector4D.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/MikktspaceWrap.h" #include "renderer/ModelRenderer.h" #include "renderer/ModelVertexRenderer.h" #include "renderer/Renderer.h" #include "renderer/RenderModifiers.h" #include "renderer/SkyManager.h" #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" #if ARCH_X86_X64 # include "lib/sysdep/arch/x86_x64/x86_x64.h" #endif /////////////////////////////////////////////////////////////////////////////////////////////// // ModelRenderer implementation #if ARCH_X86_X64 static bool g_EnableSSE = false; #endif void ModelRenderer::Init() { #if ARCH_X86_X64 if (x86_x64::Cap(x86_x64::CAP_SSE)) g_EnableSSE = true; #endif } // Helper function to copy object-space position and normal vectors into arrays. void ModelRenderer::CopyPositionAndNormals( const CModelDefPtr& mdef, const VertexArrayIterator& Position, const VertexArrayIterator& Normal) { size_t numVertices = mdef->GetNumVertices(); SModelVertex* vertices = mdef->GetVertices(); for (size_t j = 0; j < numVertices; ++j) { Position[j] = vertices[j].m_Coords; Normal[j] = vertices[j].m_Norm; } } // Helper function to transform position and normal vectors into world-space. void ModelRenderer::BuildPositionAndNormals( CModel* model, const VertexArrayIterator& Position, const VertexArrayIterator& Normal) { CModelDefPtr mdef = model->GetModelDef(); size_t numVertices = mdef->GetNumVertices(); SModelVertex* vertices = mdef->GetVertices(); if (model->IsSkinned()) { // boned model - calculate skinned vertex positions/normals // Avoid the noisy warnings that occur inside SkinPoint/SkinNormal in // some broken situations if (numVertices && vertices[0].m_Blend.m_Bone[0] == 0xff) { LOGERROR("Model %s is boned with unboned animation", mdef->GetName().string8()); return; } #if HAVE_SSE if (g_EnableSSE) { CModelDef::SkinPointsAndNormals_SSE(numVertices, Position, Normal, vertices, mdef->GetBlendIndices(), model->GetAnimatedBoneMatrices()); } else #endif { CModelDef::SkinPointsAndNormals(numVertices, Position, Normal, vertices, mdef->GetBlendIndices(), model->GetAnimatedBoneMatrices()); } } else { PROFILE("software transform"); // just copy regular positions, transform normals to world space const CMatrix3D& transform = model->GetTransform(); const CMatrix3D& invtransform = model->GetInvTransform(); for (size_t j = 0; j < numVertices; ++j) { transform.Transform(vertices[j].m_Coords, Position[j]); invtransform.RotateTransposed(vertices[j].m_Norm, Normal[j]); } } } // Helper function for lighting void ModelRenderer::BuildColor4ub( CModel* model, const VertexArrayIterator& Normal, const VertexArrayIterator& Color) { PROFILE("lighting vertices"); CModelDefPtr mdef = model->GetModelDef(); size_t numVertices = mdef->GetNumVertices(); const CLightEnv& lightEnv = g_Renderer.GetLightEnv(); CColor shadingColor = model->GetShadingColor(); for (size_t j = 0; j < numVertices; ++j) { RGBColor tempcolor = lightEnv.EvaluateUnitScaled(Normal[j]); tempcolor.X *= shadingColor.r; tempcolor.Y *= shadingColor.g; tempcolor.Z *= shadingColor.b; Color[j] = ConvertRGBColorTo4ub(tempcolor); } } void ModelRenderer::GenTangents(const CModelDefPtr& mdef, std::vector& newVertices, bool gpuSkinning) { MikkTSpace ms(mdef, newVertices, gpuSkinning); ms.Generate(); } // Copy UV coordinates void ModelRenderer::BuildUV( const CModelDefPtr& mdef, const VertexArrayIterator& UV, int UVset) { size_t numVertices = mdef->GetNumVertices(); SModelVertex* vertices = mdef->GetVertices(); for (size_t j = 0; j < numVertices; ++j) { UV[j][0] = vertices[j].m_UVs[UVset * 2]; UV[j][1] = 1.0 - vertices[j].m_UVs[UVset * 2 + 1]; } } // Build default indices array. void ModelRenderer::BuildIndices( const CModelDefPtr& mdef, const VertexArrayIterator& Indices) { size_t idxidx = 0; SModelFace* faces = mdef->GetFaces(); for (size_t j = 0; j < mdef->GetNumFaces(); ++j) { SModelFace& face = faces[j]; Indices[idxidx++] = face.m_Verts[0]; Indices[idxidx++] = face.m_Verts[1]; Indices[idxidx++] = face.m_Verts[2]; } } /////////////////////////////////////////////////////////////////////////////////////////////// // ShaderModelRenderer implementation /** * Internal data of the ShaderModelRenderer. * * Separated into the source file to increase implementation hiding (and to * avoid some causes of recompiles). */ struct ShaderModelRenderer::ShaderModelRendererInternals { ShaderModelRendererInternals(ShaderModelRenderer* r) : m_Renderer(r) { } /// Back-link to "our" renderer ShaderModelRenderer* m_Renderer; /// ModelVertexRenderer used for vertex transformations ModelVertexRendererPtr vertexRenderer; /// List of submitted models for rendering in this frame std::vector submissions[CRenderer::CULL_MAX]; }; // Construction/Destruction ShaderModelRenderer::ShaderModelRenderer(ModelVertexRendererPtr vertexrenderer) { m = new ShaderModelRendererInternals(this); m->vertexRenderer = vertexrenderer; } ShaderModelRenderer::~ShaderModelRenderer() { delete m; } // Submit one model. void ShaderModelRenderer::Submit(int cullGroup, CModel* model) { CModelRData* rdata = (CModelRData*)model->GetRenderData(); // Ensure model data is valid const void* key = m->vertexRenderer.get(); if (!rdata || rdata->GetKey() != key) { rdata = m->vertexRenderer->CreateModelData(key, model); model->SetRenderData(rdata); model->SetDirty(~0u); } m->submissions[cullGroup].push_back(model); } // Call update for all submitted models and enter the rendering phase void ShaderModelRenderer::PrepareModels() { for (int cullGroup = 0; cullGroup < CRenderer::CULL_MAX; ++cullGroup) { for (size_t i = 0; i < m->submissions[cullGroup].size(); ++i) { CModel* model = m->submissions[cullGroup][i]; model->ValidatePosition(); CModelRData* rdata = static_cast(model->GetRenderData()); ENSURE(rdata->GetKey() == m->vertexRenderer.get()); m->vertexRenderer->UpdateModelData(model, rdata, rdata->m_UpdateFlags); rdata->m_UpdateFlags = 0; } } } // Clear the submissions list void ShaderModelRenderer::EndFrame() { for (int cullGroup = 0; cullGroup < CRenderer::CULL_MAX; ++cullGroup) m->submissions[cullGroup].clear(); } // Helper structs for ShaderModelRenderer::Render(): struct SMRSortByDistItem { size_t techIdx; CModel* model; float dist; }; struct SMRBatchModel { bool operator()(CModel* a, CModel* b) { if (a->GetModelDef() < b->GetModelDef()) return true; if (b->GetModelDef() < a->GetModelDef()) return false; if (a->GetMaterial().GetDiffuseTexture() < b->GetMaterial().GetDiffuseTexture()) return true; if (b->GetMaterial().GetDiffuseTexture() < a->GetMaterial().GetDiffuseTexture()) return false; return a->GetMaterial().GetStaticUniforms() < b->GetMaterial().GetStaticUniforms(); } }; struct SMRCompareSortByDistItem { bool operator()(const SMRSortByDistItem& a, const SMRSortByDistItem& b) { // Prefer items with greater distance, so we draw back-to-front return (a.dist > b.dist); // (Distances will almost always be distinct, so we don't need to bother // tie-breaking on modeldef/texture/etc) } }; -struct SMRMaterialBucketKey +class SMRMaterialBucketKey { +public: SMRMaterialBucketKey(CStrIntern effect, const CShaderDefines& defines) : effect(effect), defines(defines) { } + SMRMaterialBucketKey(const SMRMaterialBucketKey& entity) = default; + CStrIntern effect; CShaderDefines defines; bool operator==(const SMRMaterialBucketKey& b) const { return (effect == b.effect && defines == b.defines); } private: SMRMaterialBucketKey& operator=(const SMRMaterialBucketKey&); }; struct SMRMaterialBucketKeyHash { size_t operator()(const SMRMaterialBucketKey& key) const { size_t hash = 0; hash_combine(hash, key.effect.GetHash()); hash_combine(hash, key.defines.GetHash()); return hash; } }; struct SMRTechBucket { CShaderTechniquePtr tech; CModel** models; size_t numModels; // Model list is stored as pointers, not as a std::vector, // so that sorting lists of this struct is fast }; struct SMRCompareTechBucket { bool operator()(const SMRTechBucket& a, const SMRTechBucket& b) { return a.tech < b.tech; } }; void ShaderModelRenderer::Render(const RenderModifierPtr& modifier, const CShaderDefines& context, int cullGroup, int flags) { if (m->submissions[cullGroup].empty()) return; CMatrix3D worldToCam; g_Renderer.GetViewCamera().GetOrientation().GetInverse(worldToCam); /* * Rendering approach: * * m->submissions contains the list of CModels to render. * * The data we need to render a model is: * - CShaderTechnique * - CTexture * - CShaderUniforms * - CModelDef (mesh data) * - CModel (model instance data) * * For efficient rendering, we need to batch the draw calls to minimise state changes. * (Uniform and texture changes are assumed to be cheaper than binding new mesh data, * and shader changes are assumed to be most expensive.) * First, group all models that share a technique to render them together. * Within those groups, sub-group by CModelDef. * Within those sub-groups, sub-sub-group by CTexture. * Within those sub-sub-groups, sub-sub-sub-group by CShaderUniforms. * * Alpha-blended models have to be sorted by distance from camera, * then we can batch as long as the order is preserved. * Non-alpha-blended models can be arbitrarily reordered to maximise batching. * * For each model, the CShaderTechnique is derived from: * - The current global 'context' defines * - The CModel's material's defines * - The CModel's material's shader effect name * * There are a smallish number of materials, and a smaller number of techniques. * * To minimise technique lookups, we first group models by material, * in 'materialBuckets' (a hash table). * * For each material bucket we then look up the appropriate shader technique. * If the technique requires sort-by-distance, the model is added to the * 'sortByDistItems' list with its computed distance. * Otherwise, the bucket's list of models is sorted by modeldef+texture+uniforms, * then the technique and model list is added to 'techBuckets'. * * 'techBuckets' is then sorted by technique, to improve batching when multiple * materials map onto the same technique. * * (Note that this isn't perfect batching: we don't sort across models in * multiple buckets that share a technique. In practice that shouldn't reduce * batching much (we rarely have one mesh used with multiple materials), * and it saves on copying and lets us sort smaller lists.) * * Extra tech buckets are added for the sorted-by-distance models without reordering. * Finally we render by looping over each tech bucket, then looping over the model * list in each, rebinding the GL state whenever it changes. */ Allocators::DynamicArena arena(256 * KiB); using ModelListAllocator = ProxyAllocator; using ModelList_t = std::vector; using MaterialBuckets_t = std::unordered_map< SMRMaterialBucketKey, ModelList_t, SMRMaterialBucketKeyHash, std::equal_to, ProxyAllocator< std::pair, Allocators::DynamicArena> >; MaterialBuckets_t materialBuckets((MaterialBuckets_t::allocator_type(arena))); { PROFILE3("bucketing by material"); for (size_t i = 0; i < m->submissions[cullGroup].size(); ++i) { CModel* model = m->submissions[cullGroup][i]; uint32_t condFlags = 0; const CShaderConditionalDefines& condefs = model->GetMaterial().GetConditionalDefines(); for (size_t j = 0; j < condefs.GetSize(); ++j) { const CShaderConditionalDefines::CondDefine& item = condefs.GetItem(j); int type = item.m_CondType; switch (type) { case DCOND_DISTANCE: { CVector3D modelpos = model->GetTransform().GetTranslation(); float dist = worldToCam.Transform(modelpos).Z; float dmin = item.m_CondArgs[0]; float dmax = item.m_CondArgs[1]; if ((dmin < 0 || dist >= dmin) && (dmax < 0 || dist < dmax)) condFlags |= (1 << j); break; } } } CShaderDefines defs = model->GetMaterial().GetShaderDefines(condFlags); SMRMaterialBucketKey key(model->GetMaterial().GetShaderEffect(), defs); MaterialBuckets_t::iterator it = materialBuckets.find(key); if (it == materialBuckets.end()) { std::pair inserted = materialBuckets.insert( std::make_pair(key, ModelList_t(ModelList_t::allocator_type(arena)))); inserted.first->second.reserve(32); inserted.first->second.push_back(model); } else { it->second.push_back(model); } } } typedef ProxyAllocator SortByDistItemsAllocator; std::vector sortByDistItems((SortByDistItemsAllocator(arena))); typedef ProxyAllocator SortByTechItemsAllocator; std::vector sortByDistTechs((SortByTechItemsAllocator(arena))); // indexed by sortByDistItems[i].techIdx // (which stores indexes instead of CShaderTechniquePtr directly // to avoid the shared_ptr copy cost when sorting; maybe it'd be better // if we just stored raw CShaderTechnique* and assumed the shader manager // will keep it alive long enough) typedef ProxyAllocator TechBucketsAllocator; std::vector techBuckets((TechBucketsAllocator(arena))); { PROFILE3("processing material buckets"); for (MaterialBuckets_t::iterator it = materialBuckets.begin(); it != materialBuckets.end(); ++it) { CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(it->first.effect, context, it->first.defines); // Skip invalid techniques (e.g. from data file errors) if (!tech) continue; if (tech->GetSortByDistance()) { // Add the tech into a vector so we can index it // (There might be duplicates in this list, but that doesn't really matter) if (sortByDistTechs.empty() || sortByDistTechs.back() != tech) sortByDistTechs.push_back(tech); size_t techIdx = sortByDistTechs.size() - 1; // Add each model into sortByDistItems for (size_t i = 0; i < it->second.size(); ++i) { SMRSortByDistItem itemWithDist; itemWithDist.techIdx = techIdx; CModel* model = it->second[i]; itemWithDist.model = model; CVector3D modelpos = model->GetTransform().GetTranslation(); itemWithDist.dist = worldToCam.Transform(modelpos).Z; sortByDistItems.push_back(itemWithDist); } } else { // Sort model list by modeldef+texture, for batching // TODO: This only sorts by base texture. While this is an OK approximation // for most cases (as related samplers are usually used together), it would be better // to take all the samplers into account when sorting here. std::sort(it->second.begin(), it->second.end(), SMRBatchModel()); // Add a tech bucket pointing at this model list SMRTechBucket techBucket = { tech, &it->second[0], it->second.size() }; techBuckets.push_back(techBucket); } } } { PROFILE3("sorting tech buckets"); // Sort by technique, for better batching std::sort(techBuckets.begin(), techBuckets.end(), SMRCompareTechBucket()); } // List of models corresponding to sortByDistItems[i].model // (This exists primarily because techBuckets wants a CModel**; // we could avoid the cost of copying into this list by adding // a stride length into techBuckets and not requiring contiguous CModel*s) std::vector sortByDistModels((ModelListAllocator(arena))); if (!sortByDistItems.empty()) { { PROFILE3("sorting items by dist"); std::sort(sortByDistItems.begin(), sortByDistItems.end(), SMRCompareSortByDistItem()); } { PROFILE3("batching dist-sorted items"); sortByDistModels.reserve(sortByDistItems.size()); // Find runs of distance-sorted models that share a technique, // and create a new tech bucket for each run size_t start = 0; // start of current run size_t currentTechIdx = sortByDistItems[start].techIdx; for (size_t end = 0; end < sortByDistItems.size(); ++end) { sortByDistModels.push_back(sortByDistItems[end].model); size_t techIdx = sortByDistItems[end].techIdx; if (techIdx != currentTechIdx) { // Start of a new run - push the old run into a new tech bucket SMRTechBucket techBucket = { sortByDistTechs[currentTechIdx], &sortByDistModels[start], end - start }; techBuckets.push_back(techBucket); start = end; currentTechIdx = techIdx; } } // Add the tech bucket for the final run SMRTechBucket techBucket = { sortByDistTechs[currentTechIdx], &sortByDistModels[start], sortByDistItems.size() - start }; techBuckets.push_back(techBucket); } } { PROFILE3("rendering bucketed submissions"); size_t idxTechStart = 0; // This vector keeps track of texture changes during rendering. It is kept outside the // loops to avoid excessive reallocations. The token allocation of 64 elements // should be plenty, though it is reallocated below (at a cost) if necessary. typedef ProxyAllocator TextureListAllocator; std::vector currentTexs((TextureListAllocator(arena))); currentTexs.reserve(64); // texBindings holds the identifier bindings in the shader, which can no longer be defined // statically in the ShaderRenderModifier class. texBindingNames uses interned strings to // keep track of when bindings need to be reevaluated. typedef ProxyAllocator BindingListAllocator; std::vector texBindings((BindingListAllocator(arena))); texBindings.reserve(64); typedef ProxyAllocator BindingNamesListAllocator; std::vector texBindingNames((BindingNamesListAllocator(arena))); texBindingNames.reserve(64); while (idxTechStart < techBuckets.size()) { CShaderTechniquePtr currentTech = techBuckets[idxTechStart].tech; // Find runs [idxTechStart, idxTechEnd) in techBuckets of the same technique size_t idxTechEnd; for (idxTechEnd = idxTechStart + 1; idxTechEnd < techBuckets.size(); ++idxTechEnd) { if (techBuckets[idxTechEnd].tech != currentTech) break; } // For each of the technique's passes, render all the models in this run for (int pass = 0; pass < currentTech->GetNumPasses(); ++pass) { currentTech->BeginPass(pass); const CShaderProgramPtr& shader = currentTech->GetShader(pass); int streamflags = shader->GetStreamFlags(); modifier->BeginPass(shader); m->vertexRenderer->BeginPass(streamflags); // When the shader technique changes, textures need to be // rebound, so ensure there are no remnants from the last pass. // (the vector size is set to 0, but memory is not freed) currentTexs.clear(); texBindings.clear(); texBindingNames.clear(); CModelDef* currentModeldef = NULL; CShaderUniforms currentStaticUniforms; for (size_t idx = idxTechStart; idx < idxTechEnd; ++idx) { CModel** models = techBuckets[idx].models; size_t numModels = techBuckets[idx].numModels; for (size_t i = 0; i < numModels; ++i) { CModel* model = models[i]; if (flags && !(model->GetFlags() & flags)) continue; const CMaterial::SamplersVector& samplers = model->GetMaterial().GetSamplers(); size_t samplersNum = samplers.size(); // make sure the vectors are the right virtual sizes, and also // reallocate if there are more samplers than expected. if (currentTexs.size() != samplersNum) { currentTexs.resize(samplersNum, NULL); texBindings.resize(samplersNum, CShaderProgram::Binding()); texBindingNames.resize(samplersNum, CStrIntern()); // ensure they are definitely empty std::fill(texBindings.begin(), texBindings.end(), CShaderProgram::Binding()); std::fill(currentTexs.begin(), currentTexs.end(), (CTexture*)NULL); std::fill(texBindingNames.begin(), texBindingNames.end(), CStrIntern()); } // bind the samplers to the shader for (size_t s = 0; s < samplersNum; ++s) { const CMaterial::TextureSampler& samp = samplers[s]; // check that the handles are current // and reevaluate them if necessary if (texBindingNames[s] != samp.Name || !texBindings[s].Active()) { texBindings[s] = shader->GetTextureBinding(samp.Name); texBindingNames[s] = samp.Name; } // same with the actual sampler bindings CTexture* newTex = samp.Sampler.get(); if (texBindings[s].Active() && newTex != currentTexs[s]) { shader->BindTexture(texBindings[s], newTex->GetHandle()); currentTexs[s] = newTex; } } // Bind modeldef when it changes CModelDef* newModeldef = model->GetModelDef().get(); if (newModeldef != currentModeldef) { currentModeldef = newModeldef; m->vertexRenderer->PrepareModelDef(shader, streamflags, *currentModeldef); } // Bind all uniforms when any change CShaderUniforms newStaticUniforms = model->GetMaterial().GetStaticUniforms(); if (newStaticUniforms != currentStaticUniforms) { currentStaticUniforms = newStaticUniforms; currentStaticUniforms.BindUniforms(shader); } const CShaderRenderQueries& renderQueries = model->GetMaterial().GetRenderQueries(); for (size_t q = 0; q < renderQueries.GetSize(); ++q) { CShaderRenderQueries::RenderQuery rq = renderQueries.GetItem(q); if (rq.first == RQUERY_TIME) { CShaderProgram::Binding binding = shader->GetUniformBinding(rq.second); if (binding.Active()) { double time = g_Renderer.GetTimeManager().GetGlobalTime(); shader->Uniform(binding, time, 0.0f, 0.0f, 0.0f); } } else if (rq.first == RQUERY_WATER_TEX) { WaterManager* WaterMgr = g_Renderer.GetWaterManager(); double time = WaterMgr->m_WaterTexTimer; double period = 1.6; int curTex = static_cast(time * 60.0 / period) % 60; if (WaterMgr->m_RenderWater && WaterMgr->WillRenderFancyWater()) shader->BindTexture(str_waterTex, WaterMgr->m_NormalMap[curTex]); else shader->BindTexture(str_waterTex, g_Renderer.GetTextureManager().GetErrorTexture()); } else if (rq.first == RQUERY_SKY_CUBE) { shader->BindTexture(str_skyCube, g_Renderer.GetSkyManager()->GetSkyCube()); } } modifier->PrepareModel(shader, model); CModelRData* rdata = static_cast(model->GetRenderData()); ENSURE(rdata->GetKey() == m->vertexRenderer.get()); m->vertexRenderer->RenderModel(shader, streamflags, model, rdata); } } m->vertexRenderer->EndPass(streamflags); currentTech->EndPass(pass); } idxTechStart = idxTechEnd; } } } Index: ps/trunk/source/renderer/PatchRData.cpp =================================================================== --- ps/trunk/source/renderer/PatchRData.cpp (revision 24486) +++ ps/trunk/source/renderer/PatchRData.cpp (revision 24487) @@ -1,1522 +1,1522 @@ /* Copyright (C) 2020 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 "renderer/PatchRData.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/Patch.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TextRenderer.h" #include "lib/alignment.h" #include "lib/allocators/arena.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/World.h" #include "renderer/AlphaMapCalculator.h" #include "renderer/Renderer.h" #include "renderer/TerrainRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" #include #include #include const ssize_t BlendOffsets[9][2] = { { 0, -1 }, { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 }, { 1, 0 }, { 1, -1 }, { 0, 0 } }; /////////////////////////////////////////////////////////////////// // CPatchRData constructor CPatchRData::CPatchRData(CPatch* patch, CSimulation2* simulation) : m_Patch(patch), m_VBSides(0), m_VBBase(0), m_VBBaseIndices(0), m_VBBlends(0), m_VBBlendIndices(0), m_VBWater(0), m_VBWaterIndices(0), m_VBWaterShore(0), m_VBWaterIndicesShore(0), m_Simulation(simulation) { ENSURE(patch); Build(); } /////////////////////////////////////////////////////////////////// // CPatchRData destructor CPatchRData::~CPatchRData() { // release vertex buffer chunks if (m_VBSides) g_VBMan.Release(m_VBSides); if (m_VBBase) g_VBMan.Release(m_VBBase); if (m_VBBaseIndices) g_VBMan.Release(m_VBBaseIndices); if (m_VBBlends) g_VBMan.Release(m_VBBlends); if (m_VBBlendIndices) g_VBMan.Release(m_VBBlendIndices); if (m_VBWater) g_VBMan.Release(m_VBWater); if (m_VBWaterIndices) g_VBMan.Release(m_VBWaterIndices); if (m_VBWaterShore) g_VBMan.Release(m_VBWaterShore); if (m_VBWaterIndicesShore) g_VBMan.Release(m_VBWaterIndicesShore); } /** * Represents a blend for a single tile, texture and shape. */ struct STileBlend { CTerrainTextureEntry* m_Texture; int m_Priority; u16 m_TileMask; // bit n set if this blend contains neighbour tile BlendOffsets[n] struct DecreasingPriority { bool operator()(const STileBlend& a, const STileBlend& b) const { if (a.m_Priority > b.m_Priority) return true; if (a.m_Priority < b.m_Priority) return false; if (a.m_Texture && b.m_Texture) return a.m_Texture->GetTag() > b.m_Texture->GetTag(); return false; } }; struct CurrentTile { bool operator()(const STileBlend& a) const { return (a.m_TileMask & (1 << 8)) != 0; } }; }; /** * Represents the ordered collection of blends drawn on a particular tile. */ struct STileBlendStack { u8 i, j; std::vector blends; // back of vector is lowest-priority texture }; /** * Represents a batched collection of blends using the same texture. */ struct SBlendLayer { struct Tile { u8 i, j; u8 shape; }; CTerrainTextureEntry* m_Texture; std::vector m_Tiles; }; void CPatchRData::BuildBlends() { PROFILE3("build blends"); m_BlendSplats.clear(); std::vector blendVertices; std::vector blendIndices; CTerrain* terrain = m_Patch->m_Parent; std::vector blendStacks; blendStacks.reserve(PATCH_SIZE*PATCH_SIZE); // For each tile in patch .. for (ssize_t j = 0; j < PATCH_SIZE; ++j) { for (ssize_t i = 0; i < PATCH_SIZE; ++i) { ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; std::vector blends; blends.reserve(9); // Compute a blend for every tile in the 3x3 square around this tile for (size_t n = 0; n < 9; ++n) { ssize_t ox = gx + BlendOffsets[n][1]; ssize_t oz = gz + BlendOffsets[n][0]; CMiniPatch* nmp = terrain->GetTile(ox, oz); if (!nmp) continue; STileBlend blend; blend.m_Texture = nmp->GetTextureEntry(); blend.m_Priority = nmp->GetPriority(); blend.m_TileMask = 1 << n; blends.push_back(blend); } // Sort the blends, highest priority first std::sort(blends.begin(), blends.end(), STileBlend::DecreasingPriority()); STileBlendStack blendStack; blendStack.i = i; blendStack.j = j; // Put the blends into the tile's stack, merging any adjacent blends with the same texture for (size_t k = 0; k < blends.size(); ++k) { if (!blendStack.blends.empty() && blendStack.blends.back().m_Texture == blends[k].m_Texture) blendStack.blends.back().m_TileMask |= blends[k].m_TileMask; else blendStack.blends.push_back(blends[k]); } // Remove blends that are after (i.e. lower priority than) the current tile // (including the current tile), since we don't want to render them on top of // the tile's base texture blendStack.blends.erase( std::find_if(blendStack.blends.begin(), blendStack.blends.end(), STileBlend::CurrentTile()), blendStack.blends.end()); blendStacks.push_back(blendStack); } } // Given the blend stack per tile, we want to batch together as many blends as possible. // Group them into a series of layers (each of which has a single texture): // (This is effectively a topological sort / linearisation of the partial order induced // by the per-tile stacks, preferring to make tiles with equal textures adjacent.) std::vector blendLayers; while (true) { if (!blendLayers.empty()) { // Try to grab as many tiles as possible that match our current layer, // from off the blend stacks of all the tiles CTerrainTextureEntry* tex = blendLayers.back().m_Texture; for (size_t k = 0; k < blendStacks.size(); ++k) { if (!blendStacks[k].blends.empty() && blendStacks[k].blends.back().m_Texture == tex) { SBlendLayer::Tile t = { blendStacks[k].i, blendStacks[k].j, (u8)blendStacks[k].blends.back().m_TileMask }; blendLayers.back().m_Tiles.push_back(t); blendStacks[k].blends.pop_back(); } // (We've already merged adjacent entries of the same texture in each stack, // so we don't need to bother looping to check the next entry in this stack again) } } // We've grabbed as many tiles as possible; now we need to start a new layer. // The new layer's texture could come from the back of any non-empty stack; // choose the longest stack as a heuristic to reduce the number of layers CTerrainTextureEntry* bestTex = NULL; size_t bestStackSize = 0; for (size_t k = 0; k < blendStacks.size(); ++k) { if (blendStacks[k].blends.size() > bestStackSize) { bestStackSize = blendStacks[k].blends.size(); bestTex = blendStacks[k].blends.back().m_Texture; } } // If all our stacks were empty, we're done if (bestStackSize == 0) break; // Otherwise add the new layer, then loop back and start filling it in SBlendLayer layer; layer.m_Texture = bestTex; blendLayers.push_back(layer); } // Now build outgoing splats m_BlendSplats.resize(blendLayers.size()); for (size_t k = 0; k < blendLayers.size(); ++k) { SSplat& splat = m_BlendSplats[k]; splat.m_IndexStart = blendIndices.size(); splat.m_Texture = blendLayers[k].m_Texture; for (size_t t = 0; t < blendLayers[k].m_Tiles.size(); ++t) { SBlendLayer::Tile& tile = blendLayers[k].m_Tiles[t]; AddBlend(blendVertices, blendIndices, tile.i, tile.j, tile.shape, splat.m_Texture); } splat.m_IndexCount = blendIndices.size() - splat.m_IndexStart; } // Release existing vertex buffer chunks if (m_VBBlends) { g_VBMan.Release(m_VBBlends); m_VBBlends = 0; } if (m_VBBlendIndices) { g_VBMan.Release(m_VBBlendIndices); m_VBBlendIndices = 0; } if (blendVertices.size()) { // Construct vertex buffer m_VBBlends = g_VBMan.Allocate(sizeof(SBlendVertex), blendVertices.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER); m_VBBlends->m_Owner->UpdateChunkVertices(m_VBBlends, &blendVertices[0]); // Update the indices to include the base offset of the vertex data for (size_t k = 0; k < blendIndices.size(); ++k) blendIndices[k] += static_cast(m_VBBlends->m_Index); m_VBBlendIndices = g_VBMan.Allocate(sizeof(u16), blendIndices.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER); m_VBBlendIndices->m_Owner->UpdateChunkVertices(m_VBBlendIndices, &blendIndices[0]); } } void CPatchRData::AddBlend(std::vector& blendVertices, std::vector& blendIndices, u16 i, u16 j, u8 shape, CTerrainTextureEntry* texture) { CTerrain* terrain = m_Patch->m_Parent; ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; // uses the current neighbour texture BlendShape8 shape8; for (size_t m = 0; m < 8; ++m) shape8[m] = (shape & (1 << m)) ? 0 : 1; // calculate the required alphamap and the required rotation of the alphamap from blendshape unsigned int alphamapflags; int alphamap = CAlphaMapCalculator::Calculate(shape8, alphamapflags); // now actually render the blend tile (if we need one) if (alphamap == -1) return; float u0 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].u0; float u1 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].u1; float v0 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].v0; float v1 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].v1; if (alphamapflags & BLENDMAP_FLIPU) std::swap(u0, u1); if (alphamapflags & BLENDMAP_FLIPV) std::swap(v0, v1); int base = 0; if (alphamapflags & BLENDMAP_ROTATE90) base = 1; else if (alphamapflags & BLENDMAP_ROTATE180) base = 2; else if (alphamapflags & BLENDMAP_ROTATE270) base = 3; SBlendVertex vtx[4]; vtx[(base + 0) % 4].m_AlphaUVs[0] = u0; vtx[(base + 0) % 4].m_AlphaUVs[1] = v0; vtx[(base + 1) % 4].m_AlphaUVs[0] = u1; vtx[(base + 1) % 4].m_AlphaUVs[1] = v0; vtx[(base + 2) % 4].m_AlphaUVs[0] = u1; vtx[(base + 2) % 4].m_AlphaUVs[1] = v1; vtx[(base + 3) % 4].m_AlphaUVs[0] = u0; vtx[(base + 3) % 4].m_AlphaUVs[1] = v1; SBlendVertex dst; CVector3D normal; u16 index = static_cast(blendVertices.size()); terrain->CalcPosition(gx, gz, dst.m_Position); terrain->CalcNormal(gx, gz, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[0].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[0].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx + 1, gz, dst.m_Position); terrain->CalcNormal(gx + 1, gz, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[1].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[1].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx + 1, gz + 1, dst.m_Position); terrain->CalcNormal(gx + 1, gz + 1, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[2].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[2].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx, gz + 1, dst.m_Position); terrain->CalcNormal(gx, gz + 1, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[3].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[3].m_AlphaUVs[1]; blendVertices.push_back(dst); bool dir = terrain->GetTriangulationDir(gx, gz); if (dir) { blendIndices.push_back(index+0); blendIndices.push_back(index+1); blendIndices.push_back(index+3); blendIndices.push_back(index+1); blendIndices.push_back(index+2); blendIndices.push_back(index+3); } else { blendIndices.push_back(index+0); blendIndices.push_back(index+1); blendIndices.push_back(index+2); blendIndices.push_back(index+2); blendIndices.push_back(index+3); blendIndices.push_back(index+0); } } void CPatchRData::BuildIndices() { PROFILE3("build indices"); CTerrain* terrain = m_Patch->m_Parent; ssize_t px = m_Patch->m_X * PATCH_SIZE; ssize_t pz = m_Patch->m_Z * PATCH_SIZE; // must have allocated some vertices before trying to build corresponding indices ENSURE(m_VBBase); // number of vertices in each direction in each patch ssize_t vsize=PATCH_SIZE+1; // PATCH_SIZE must be 2^8-2 or less to not overflow u16 indices buffer. Thankfully this is always true. ENSURE(vsize*vsize < 65536); std::vector indices; indices.reserve(PATCH_SIZE * PATCH_SIZE * 4); // release existing splats m_Splats.clear(); // build grid of textures on this patch std::vector textures; CTerrainTextureEntry* texgrid[PATCH_SIZE][PATCH_SIZE]; for (ssize_t j=0;jm_MiniPatches[j][i].GetTextureEntry(); texgrid[j][i]=tex; if (std::find(textures.begin(),textures.end(),tex)==textures.end()) { textures.push_back(tex); } } } // now build base splats from interior textures m_Splats.resize(textures.size()); // build indices for base splats size_t base=m_VBBase->m_Index; for (size_t k = 0; k < m_Splats.size(); ++k) { CTerrainTextureEntry* tex = textures[k]; SSplat& splat=m_Splats[k]; splat.m_Texture=tex; splat.m_IndexStart=indices.size(); for (ssize_t j = 0; j < PATCH_SIZE; j++) { for (ssize_t i = 0; i < PATCH_SIZE; i++) { if (texgrid[j][i] == tex) { bool dir = terrain->GetTriangulationDir(px+i, pz+j); if (dir) { indices.push_back(u16(((j+0)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); } else { indices.push_back(u16(((j+0)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+0))+base)); } } } } splat.m_IndexCount=indices.size()-splat.m_IndexStart; } // Release existing vertex buffer chunk if (m_VBBaseIndices) { g_VBMan.Release(m_VBBaseIndices); m_VBBaseIndices = 0; } ENSURE(indices.size()); // Construct vertex buffer m_VBBaseIndices = g_VBMan.Allocate(sizeof(u16), indices.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER); m_VBBaseIndices->m_Owner->UpdateChunkVertices(m_VBBaseIndices, &indices[0]); } void CPatchRData::BuildVertices() { PROFILE3("build vertices"); // create both vertices and lighting colors // number of vertices in each direction in each patch ssize_t vsize = PATCH_SIZE + 1; std::vector vertices; vertices.resize(vsize * vsize); // get index of this patch ssize_t px = m_Patch->m_X; ssize_t pz = m_Patch->m_Z; CTerrain* terrain = m_Patch->m_Parent; // build vertices for (ssize_t j = 0; j < vsize; ++j) { for (ssize_t i = 0; i < vsize; ++i) { ssize_t ix = px * PATCH_SIZE + i; ssize_t iz = pz * PATCH_SIZE + j; ssize_t v = j * vsize + i; // calculate vertex data terrain->CalcPosition(ix, iz, vertices[v].m_Position); CVector3D normal; terrain->CalcNormal(ix, iz, normal); vertices[v].m_Normal = normal; } } // upload to vertex buffer if (!m_VBBase) m_VBBase = g_VBMan.Allocate(sizeof(SBaseVertex), vsize * vsize, GL_STATIC_DRAW, GL_ARRAY_BUFFER); m_VBBase->m_Owner->UpdateChunkVertices(m_VBBase, &vertices[0]); } void CPatchRData::BuildSide(std::vector& vertices, CPatchSideFlags side) { ssize_t vsize = PATCH_SIZE + 1; CTerrain* terrain = m_Patch->m_Parent; CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); for (ssize_t k = 0; k < vsize; k++) { ssize_t gx = m_Patch->m_X * PATCH_SIZE; ssize_t gz = m_Patch->m_Z * PATCH_SIZE; switch (side) { case CPATCH_SIDE_NEGX: gz += k; break; case CPATCH_SIDE_POSX: gx += PATCH_SIZE; gz += PATCH_SIZE-k; break; case CPATCH_SIDE_NEGZ: gx += PATCH_SIZE-k; break; case CPATCH_SIDE_POSZ: gz += PATCH_SIZE; gx += k; break; } CVector3D pos; terrain->CalcPosition(gx, gz, pos); // Clamp the height to the water level float waterHeight = 0.f; if (cmpWaterManager) waterHeight = cmpWaterManager->GetExactWaterLevel(pos.X, pos.Z); pos.Y = std::max(pos.Y, waterHeight); SSideVertex v0, v1; v0.m_Position = pos; v1.m_Position = pos; v1.m_Position.Y = 0; // If this is the start of this tristrip, but we've already got a partial // tristrip, add a couple of degenerate triangles to join the strips properly if (k == 0 && !vertices.empty()) { vertices.push_back(vertices.back()); vertices.push_back(v1); } // Now add the new triangles vertices.push_back(v1); vertices.push_back(v0); } } void CPatchRData::BuildSides() { PROFILE3("build sides"); std::vector sideVertices; int sideFlags = m_Patch->GetSideFlags(); // If no sides are enabled, we don't need to do anything if (!sideFlags) return; // For each side, generate a tristrip by adding a vertex at ground/water // level and a vertex underneath at height 0. if (sideFlags & CPATCH_SIDE_NEGX) BuildSide(sideVertices, CPATCH_SIDE_NEGX); if (sideFlags & CPATCH_SIDE_POSX) BuildSide(sideVertices, CPATCH_SIDE_POSX); if (sideFlags & CPATCH_SIDE_NEGZ) BuildSide(sideVertices, CPATCH_SIDE_NEGZ); if (sideFlags & CPATCH_SIDE_POSZ) BuildSide(sideVertices, CPATCH_SIDE_POSZ); if (sideVertices.empty()) return; if (!m_VBSides) m_VBSides = g_VBMan.Allocate(sizeof(SSideVertex), sideVertices.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER); m_VBSides->m_Owner->UpdateChunkVertices(m_VBSides, &sideVertices[0]); } void CPatchRData::Build() { BuildVertices(); BuildSides(); BuildIndices(); BuildBlends(); BuildWater(); } void CPatchRData::Update(CSimulation2* simulation) { m_Simulation = simulation; if (m_UpdateFlags!=0) { // TODO,RC 11/04/04 - need to only rebuild necessary bits of renderdata rather // than everything; it's complicated slightly because the blends are dependent // on both vertex and index data BuildVertices(); BuildSides(); BuildIndices(); BuildBlends(); BuildWater(); m_UpdateFlags=0; } } // Types used for glMultiDrawElements batching: // To minimise the cost of memory allocations, everything used for computing // batches uses a arena allocator. (All allocations are short-lived so we can // just throw away the whole arena at the end of each frame.) // std::map types with appropriate arena allocators and default comparison operator #define POOLED_BATCH_MAP(Key, Value) \ std::map, ProxyAllocator, Allocators::DynamicArena > > // Equivalent to "m[k]", when it returns a arena-allocated std::map (since we can't // use the default constructor in that case) template typename M::mapped_type& PooledMapGet(M& m, const typename M::key_type& k, Allocators::DynamicArena& arena) { return m.insert(std::make_pair(k, typename M::mapped_type(typename M::mapped_type::key_compare(), typename M::mapped_type::allocator_type(arena)) )).first->second; } // Equivalent to "m[k]", when it returns a std::pair of arena-allocated std::vectors template typename M::mapped_type& PooledPairGet(M& m, const typename M::key_type& k, Allocators::DynamicArena& arena) { return m.insert(std::make_pair(k, std::make_pair( typename M::mapped_type::first_type(typename M::mapped_type::first_type::allocator_type(arena)), typename M::mapped_type::second_type(typename M::mapped_type::second_type::allocator_type(arena)) ))).first->second; } // Each multidraw batch has a list of index counts, and a list of pointers-to-first-indexes typedef std::pair >, std::vector > > BatchElements; // Group batches by index buffer typedef POOLED_BATCH_MAP(CVertexBuffer*, BatchElements) IndexBufferBatches; // Group batches by vertex buffer typedef POOLED_BATCH_MAP(CVertexBuffer*, IndexBufferBatches) VertexBufferBatches; // Group batches by texture typedef POOLED_BATCH_MAP(CTerrainTextureEntry*, VertexBufferBatches) TextureBatches; void CPatchRData::RenderBases(const std::vector& patches, const CShaderDefines& context, ShadowMap* shadow, bool isDummyShader, const CShaderProgramPtr& dummy) { Allocators::DynamicArena arena(1 * MiB); TextureBatches batches (TextureBatches::key_compare(), (TextureBatches::allocator_type(arena))); PROFILE_START("compute batches"); // Collect all the patches' base splats into their appropriate batches for (size_t i = 0; i < patches.size(); ++i) { CPatchRData* patch = patches[i]; for (size_t j = 0; j < patch->m_Splats.size(); ++j) { SSplat& splat = patch->m_Splats[j]; BatchElements& batch = PooledPairGet( PooledMapGet( PooledMapGet(batches, splat.m_Texture, arena), patch->m_VBBase->m_Owner, arena ), patch->m_VBBaseIndices->m_Owner, arena ); batch.first.push_back(splat.m_IndexCount); u8* indexBase = patch->m_VBBaseIndices->m_Owner->GetBindAddress(); batch.second.push_back(indexBase + sizeof(u16)*(patch->m_VBBaseIndices->m_Index + splat.m_IndexStart)); } } PROFILE_END("compute batches"); // Render each batch for (TextureBatches::iterator itt = batches.begin(); itt != batches.end(); ++itt) { int numPasses = 1; CShaderTechniquePtr techBase; if (!isDummyShader) { if (itt->first->GetMaterial().GetShaderEffect().length() == 0) { LOGERROR("Terrain renderer failed to load shader effect.\n"); continue; } techBase = g_Renderer.GetShaderManager().LoadEffect(itt->first->GetMaterial().GetShaderEffect(), context, itt->first->GetMaterial().GetShaderDefines(0)); numPasses = techBase->GetNumPasses(); } for (int pass = 0; pass < numPasses; ++pass) { if (!isDummyShader) { techBase->BeginPass(pass); TerrainRenderer::PrepareShader(techBase->GetShader(), shadow); } const CShaderProgramPtr& shader = isDummyShader ? dummy : techBase->GetShader(pass); if (itt->first->GetMaterial().GetSamplers().size() != 0) { const CMaterial::SamplersVector& samplers = itt->first->GetMaterial().GetSamplers(); size_t samplersNum = samplers.size(); for (size_t s = 0; s < samplersNum; ++s) { const CMaterial::TextureSampler& samp = samplers[s]; shader->BindTexture(samp.Name, samp.Sampler); } itt->first->GetMaterial().GetStaticUniforms().BindUniforms(shader); #if !CONFIG2_GLES if (isDummyShader) { glMatrixMode(GL_TEXTURE); glLoadMatrixf(itt->first->GetTextureMatrix()); glMatrixMode(GL_MODELVIEW); } else #endif { float c = itt->first->GetTextureMatrix()[0]; float ms = itt->first->GetTextureMatrix()[8]; shader->Uniform(str_textureTransform, c, ms, -ms, 0.f); } } else { shader->BindTexture(str_baseTex, g_Renderer.GetTextureManager().GetErrorTexture()); } for (VertexBufferBatches::iterator itv = itt->second.begin(); itv != itt->second.end(); ++itv) { GLsizei stride = sizeof(SBaseVertex); SBaseVertex *base = (SBaseVertex *)itv->first->Bind(); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position[0]); shader->NormalPointer(GL_FLOAT, stride, &base->m_Normal[0]); shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position[0]); shader->AssertPointersBound(); for (IndexBufferBatches::iterator it = itv->second.begin(); it != itv->second.end(); ++it) { it->first->Bind(); BatchElements& batch = it->second; if (!g_Renderer.m_SkipSubmit) { // Don't use glMultiDrawElements here since it doesn't have a significant // performance impact and it suffers from various driver bugs (e.g. it breaks // in Mesa 7.10 swrast with index VBOs) for (size_t i = 0; i < batch.first.size(); ++i) glDrawElements(GL_TRIANGLES, batch.first[i], GL_UNSIGNED_SHORT, batch.second[i]); } g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } if (!isDummyShader) techBase->EndPass(); } } #if !CONFIG2_GLES if (isDummyShader) { glMatrixMode(GL_TEXTURE); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } #endif CVertexBuffer::Unbind(); } /** * Helper structure for RenderBlends. */ struct SBlendBatch { SBlendBatch(Allocators::DynamicArena& arena) : m_Batches(VertexBufferBatches::key_compare(), VertexBufferBatches::allocator_type(arena)) { } CTerrainTextureEntry* m_Texture; VertexBufferBatches m_Batches; }; /** * Helper structure for RenderBlends. */ struct SBlendStackItem { SBlendStackItem(CVertexBuffer::VBChunk* v, CVertexBuffer::VBChunk* i, const std::vector& s, Allocators::DynamicArena& arena) : vertices(v), indices(i), splats(s.begin(), s.end(), SplatStack::allocator_type(arena)) { } typedef std::vector > SplatStack; CVertexBuffer::VBChunk* vertices; CVertexBuffer::VBChunk* indices; SplatStack splats; }; void CPatchRData::RenderBlends(const std::vector& patches, const CShaderDefines& context, ShadowMap* shadow, bool isDummyShader, const CShaderProgramPtr& dummy) { Allocators::DynamicArena arena(1 * MiB); typedef std::vector > BatchesStack; BatchesStack batches((BatchesStack::allocator_type(arena))); CShaderDefines contextBlend = context; contextBlend.Add(str_BLEND, str_1); PROFILE_START("compute batches"); // Reserve an arbitrary size that's probably big enough in most cases, // to avoid heavy reallocations batches.reserve(256); typedef std::vector > BlendStacks; BlendStacks blendStacks((BlendStacks::allocator_type(arena))); blendStacks.reserve(patches.size()); // Extract all the blend splats from each patch for (size_t i = 0; i < patches.size(); ++i) { CPatchRData* patch = patches[i]; if (!patch->m_BlendSplats.empty()) { blendStacks.push_back(SBlendStackItem(patch->m_VBBlends, patch->m_VBBlendIndices, patch->m_BlendSplats, arena)); // Reverse the splats so the first to be rendered is at the back of the list std::reverse(blendStacks.back().splats.begin(), blendStacks.back().splats.end()); } } // Rearrange the collection of splats to be grouped by texture, preserving // order of splats within each patch: // (This is exactly the same algorithm used in CPatchRData::BuildBlends, // but applied to patch-sized splats rather than to tile-sized splats; // see that function for comments on the algorithm.) while (true) { if (!batches.empty()) { CTerrainTextureEntry* tex = batches.back().m_Texture; for (size_t k = 0; k < blendStacks.size(); ++k) { SBlendStackItem::SplatStack& splats = blendStacks[k].splats; if (!splats.empty() && splats.back().m_Texture == tex) { CVertexBuffer::VBChunk* vertices = blendStacks[k].vertices; CVertexBuffer::VBChunk* indices = blendStacks[k].indices; BatchElements& batch = PooledPairGet(PooledMapGet(batches.back().m_Batches, vertices->m_Owner, arena), indices->m_Owner, arena); batch.first.push_back(splats.back().m_IndexCount); u8* indexBase = indices->m_Owner->GetBindAddress(); batch.second.push_back(indexBase + sizeof(u16)*(indices->m_Index + splats.back().m_IndexStart)); splats.pop_back(); } } } CTerrainTextureEntry* bestTex = NULL; size_t bestStackSize = 0; for (size_t k = 0; k < blendStacks.size(); ++k) { SBlendStackItem::SplatStack& splats = blendStacks[k].splats; if (splats.size() > bestStackSize) { bestStackSize = splats.size(); bestTex = splats.back().m_Texture; } } if (bestStackSize == 0) break; SBlendBatch layer(arena); layer.m_Texture = bestTex; batches.push_back(layer); } PROFILE_END("compute batches"); CVertexBuffer* lastVB = NULL; for (BatchesStack::iterator itt = batches.begin(); itt != batches.end(); ++itt) { if (itt->m_Texture->GetMaterial().GetSamplers().size() == 0) continue; int numPasses = 1; CShaderTechniquePtr techBase; if (!isDummyShader) { techBase = g_Renderer.GetShaderManager().LoadEffect(itt->m_Texture->GetMaterial().GetShaderEffect(), contextBlend, itt->m_Texture->GetMaterial().GetShaderDefines(0)); numPasses = techBase->GetNumPasses(); } CShaderProgramPtr previousShader; for (int pass = 0; pass < numPasses; ++pass) { if (!isDummyShader) { techBase->BeginPass(pass); TerrainRenderer::PrepareShader(techBase->GetShader(), shadow); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } const CShaderProgramPtr& shader = isDummyShader ? dummy : techBase->GetShader(pass); if (itt->m_Texture) { const CMaterial::SamplersVector& samplers = itt->m_Texture->GetMaterial().GetSamplers(); size_t samplersNum = samplers.size(); for (size_t s = 0; s < samplersNum; ++s) { const CMaterial::TextureSampler& samp = samplers[s]; shader->BindTexture(samp.Name, samp.Sampler); } shader->BindTexture(str_blendTex, itt->m_Texture->m_TerrainAlpha->second.m_hCompositeAlphaMap); itt->m_Texture->GetMaterial().GetStaticUniforms().BindUniforms(shader); #if !CONFIG2_GLES if (isDummyShader) { pglClientActiveTextureARB(GL_TEXTURE0); glMatrixMode(GL_TEXTURE); glLoadMatrixf(itt->m_Texture->GetTextureMatrix()); glMatrixMode(GL_MODELVIEW); } else #endif { float c = itt->m_Texture->GetTextureMatrix()[0]; float ms = itt->m_Texture->GetTextureMatrix()[8]; shader->Uniform(str_textureTransform, c, ms, -ms, 0.f); } } else { shader->BindTexture(str_baseTex, g_Renderer.GetTextureManager().GetErrorTexture()); } for (VertexBufferBatches::iterator itv = itt->m_Batches.begin(); itv != itt->m_Batches.end(); ++itv) { // Rebind the VB only if it changed since the last batch if (itv->first != lastVB || shader != previousShader) { lastVB = itv->first; previousShader = shader; GLsizei stride = sizeof(SBlendVertex); SBlendVertex *base = (SBlendVertex *)itv->first->Bind(); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position[0]); shader->NormalPointer(GL_FLOAT, stride, &base->m_Normal[0]); shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position[0]); shader->TexCoordPointer(GL_TEXTURE1, 2, GL_FLOAT, stride, &base->m_AlphaUVs[0]); } shader->AssertPointersBound(); for (IndexBufferBatches::iterator it = itv->second.begin(); it != itv->second.end(); ++it) { it->first->Bind(); BatchElements& batch = it->second; if (!g_Renderer.m_SkipSubmit) { for (size_t i = 0; i < batch.first.size(); ++i) glDrawElements(GL_TRIANGLES, batch.first[i], GL_UNSIGNED_SHORT, batch.second[i]); } g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_BlendSplats++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } if (!isDummyShader) { glDisable(GL_BLEND); techBase->EndPass(); } } } #if !CONFIG2_GLES if (isDummyShader) { pglClientActiveTextureARB(GL_TEXTURE0); glMatrixMode(GL_TEXTURE); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } #endif CVertexBuffer::Unbind(); } void CPatchRData::RenderStreams(const std::vector& patches, const CShaderProgramPtr& shader, int streamflags) { // Each batch has a list of index counts, and a list of pointers-to-first-indexes using StreamBatchElements = std::pair, std::vector > ; // Group batches by index buffer using StreamIndexBufferBatches = std::map ; // Group batches by vertex buffer using StreamVertexBufferBatches = std::map ; StreamVertexBufferBatches batches; PROFILE_START("compute batches"); // Collect all the patches into their appropriate batches for (const CPatchRData* patch : patches) { StreamBatchElements& batch = batches[patch->m_VBBase->m_Owner][patch->m_VBBaseIndices->m_Owner]; batch.first.push_back(patch->m_VBBaseIndices->m_Count); u8* indexBase = patch->m_VBBaseIndices->m_Owner->GetBindAddress(); batch.second.push_back(indexBase + sizeof(u16)*(patch->m_VBBaseIndices->m_Index)); } PROFILE_END("compute batches"); ENSURE(!(streamflags & ~(STREAM_POS|STREAM_POSTOUV0|STREAM_POSTOUV1))); // Render each batch - for (const std::pair& streamBatch : batches) + for (const std::pair& streamBatch : batches) { GLsizei stride = sizeof(SBaseVertex); SBaseVertex *base = (SBaseVertex *)streamBatch.first->Bind(); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position); if (streamflags & STREAM_POSTOUV0) shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position); if (streamflags & STREAM_POSTOUV1) shader->TexCoordPointer(GL_TEXTURE1, 3, GL_FLOAT, stride, &base->m_Position); shader->AssertPointersBound(); - for (const std::pair& batchIndexBuffer : streamBatch.second) + for (const std::pair& batchIndexBuffer : streamBatch.second) { batchIndexBuffer.first->Bind(); const StreamBatchElements& batch = batchIndexBuffer.second; if (!g_Renderer.m_SkipSubmit) { for (size_t i = 0; i < batch.first.size(); ++i) glDrawElements(GL_TRIANGLES, batch.first[i], GL_UNSIGNED_SHORT, batch.second[i]); } g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } CVertexBuffer::Unbind(); } void CPatchRData::RenderOutline() { CTerrain* terrain = m_Patch->m_Parent; ssize_t gx = m_Patch->m_X * PATCH_SIZE; ssize_t gz = m_Patch->m_Z * PATCH_SIZE; CVector3D pos; std::vector line; for (ssize_t i = 0, j = 0; i <= PATCH_SIZE; ++i) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = PATCH_SIZE, j = 1; j <= PATCH_SIZE; ++j) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = PATCH_SIZE-1, j = PATCH_SIZE; i >= 0; --i) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = 0, j = PATCH_SIZE-1; j >= 0; --j) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } #if CONFIG2_GLES #warning TODO: implement CPatchRData::RenderOutlines for GLES #else glVertexPointer(3, GL_FLOAT, sizeof(CVector3D), &line[0]); glDrawArrays(GL_LINE_STRIP, 0, line.size()); #endif } void CPatchRData::RenderSides(CShaderProgramPtr& shader) { ENSURE(m_UpdateFlags==0); if (!m_VBSides) return; glDisable(GL_CULL_FACE); SSideVertex *base = (SSideVertex *)m_VBSides->m_Owner->Bind(); // setup data pointers GLsizei stride = sizeof(SSideVertex); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_TRIANGLE_STRIP, m_VBSides->m_Index, (GLsizei)m_VBSides->m_Count); // bump stats g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += m_VBSides->m_Count - 2; CVertexBuffer::Unbind(); glEnable(GL_CULL_FACE); } void CPatchRData::RenderPriorities(CTextRenderer& textRenderer) { CTerrain* terrain = m_Patch->m_Parent; const CCamera& camera = *(g_Game->GetView()->GetCamera()); for (ssize_t j = 0; j < PATCH_SIZE; ++j) { for (ssize_t i = 0; i < PATCH_SIZE; ++i) { ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; CVector3D pos; terrain->CalcPosition(gx, gz, pos); // Move a bit towards the center of the tile pos.X += TERRAIN_TILE_SIZE/4.f; pos.Z += TERRAIN_TILE_SIZE/4.f; float x, y; camera.GetScreenCoordinates(pos, x, y); textRenderer.PrintfAt(x, y, L"%d", m_Patch->m_MiniPatches[j][i].Priority); } } } // // Water build and rendering // // Build vertex buffer for water vertices over our patch void CPatchRData::BuildWater() { PROFILE3("build water"); // Number of vertices in each direction in each patch ENSURE(PATCH_SIZE % water_cell_size == 0); if (m_VBWater) { g_VBMan.Release(m_VBWater); m_VBWater = nullptr; } if (m_VBWaterIndices) { g_VBMan.Release(m_VBWaterIndices); m_VBWaterIndices = nullptr; } if (m_VBWaterShore) { g_VBMan.Release(m_VBWaterShore); m_VBWaterShore = nullptr; } if (m_VBWaterIndicesShore) { g_VBMan.Release(m_VBWaterIndicesShore); m_VBWaterIndicesShore = nullptr; } m_WaterBounds.SetEmpty(); // We need to use this to access the water manager or we may not have the // actual values but some compiled-in defaults CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); if (!cmpWaterManager) return; // Build data for water std::vector water_vertex_data; std::vector water_indices; u16 water_index_map[PATCH_SIZE+1][PATCH_SIZE+1]; memset(water_index_map, 0xFF, sizeof(water_index_map)); // Build data for shore std::vector water_vertex_data_shore; std::vector water_indices_shore; u16 water_shore_index_map[PATCH_SIZE+1][PATCH_SIZE+1]; memset(water_shore_index_map, 0xFF, sizeof(water_shore_index_map)); WaterManager* WaterMgr = g_Renderer.GetWaterManager(); CPatch* patch = m_Patch; CTerrain* terrain = patch->m_Parent; ssize_t mapSize = terrain->GetVerticesPerSide(); // Top-left coordinates of our patch. ssize_t px = m_Patch->m_X * PATCH_SIZE; ssize_t pz = m_Patch->m_Z * PATCH_SIZE; // To whoever implements different water heights, this is a TODO: water height) float waterHeight = cmpWaterManager->GetExactWaterLevel(0.0f,0.0f); // The 4 points making a water tile. int moves[4][2] = { {0, 0}, {water_cell_size, 0}, {0, water_cell_size}, {water_cell_size, water_cell_size} }; // Where to look for when checking for water for shore tiles. int check[10][2] = { {0, 0}, {water_cell_size, 0}, {water_cell_size*2, 0}, {0, water_cell_size}, {0, water_cell_size*2}, {water_cell_size, water_cell_size}, {water_cell_size*2, water_cell_size*2}, {-water_cell_size, 0}, {0, -water_cell_size}, {-water_cell_size, -water_cell_size} }; // build vertices, uv, and shader varying for (ssize_t z = 0; z < PATCH_SIZE; z += water_cell_size) { for (ssize_t x = 0; x < PATCH_SIZE; x += water_cell_size) { // Check that this tile is close to water bool nearWater = false; for (size_t test = 0; test < 10; ++test) if (terrain->GetVertexGroundLevel(x + px + check[test][0], z + pz + check[test][1]) < waterHeight) nearWater = true; if (!nearWater) continue; // This is actually lying and I should call CcmpTerrain /*if (!terrain->IsOnMap(x+x1, z+z1) && !terrain->IsOnMap(x+x1, z+z1 + water_cell_size) && !terrain->IsOnMap(x+x1 + water_cell_size, z+z1) && !terrain->IsOnMap(x+x1 + water_cell_size, z+z1 + water_cell_size)) continue;*/ for (int i = 0; i < 4; ++i) { if (water_index_map[z+moves[i][1]][x+moves[i][0]] != 0xFFFF) continue; ssize_t xx = x + px + moves[i][0]; ssize_t zz = z + pz + moves[i][1]; SWaterVertex vertex; terrain->CalcPosition(xx,zz, vertex.m_Position); float depth = waterHeight - vertex.m_Position.Y; vertex.m_Position.Y = waterHeight; m_WaterBounds += vertex.m_Position; vertex.m_WaterData = CVector2D(WaterMgr->m_WindStrength[xx + zz*mapSize], depth); water_index_map[z+moves[i][1]][x+moves[i][0]] = static_cast(water_vertex_data.size()); water_vertex_data.push_back(vertex); } water_indices.push_back(water_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices.push_back(water_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices.push_back(water_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices.push_back(water_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices.push_back(water_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices.push_back(water_index_map[z + moves[2][1]][x + moves[2][0]]); // Check id this tile is partly over land. // If so add a square over the terrain. This is necessary to render waves that go on shore. if (terrain->GetVertexGroundLevel(x+px, z+pz) < waterHeight && terrain->GetVertexGroundLevel(x+px + water_cell_size, z+pz) < waterHeight && terrain->GetVertexGroundLevel(x+px, z+pz+water_cell_size) < waterHeight && terrain->GetVertexGroundLevel(x+px + water_cell_size, z+pz+water_cell_size) < waterHeight) continue; for (int i = 0; i < 4; ++i) { if (water_shore_index_map[z+moves[i][1]][x+moves[i][0]] != 0xFFFF) continue; ssize_t xx = x + px + moves[i][0]; ssize_t zz = z + pz + moves[i][1]; SWaterVertex vertex; terrain->CalcPosition(xx,zz, vertex.m_Position); vertex.m_Position.Y += 0.02f; m_WaterBounds += vertex.m_Position; vertex.m_WaterData = CVector2D(0.0f, -5.0f); water_shore_index_map[z+moves[i][1]][x+moves[i][0]] = static_cast(water_vertex_data_shore.size()); water_vertex_data_shore.push_back(vertex); } if (terrain->GetTriangulationDir(x + px, z + pz)) { water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); } else { water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); } } } // No vertex buffers if no data generated if (!water_indices.empty()) { m_VBWater = g_VBMan.Allocate(sizeof(SWaterVertex), water_vertex_data.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER); m_VBWater->m_Owner->UpdateChunkVertices(m_VBWater, &water_vertex_data[0]); m_VBWaterIndices = g_VBMan.Allocate(sizeof(GLushort), water_indices.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER); m_VBWaterIndices->m_Owner->UpdateChunkVertices(m_VBWaterIndices, &water_indices[0]); } if (!water_indices_shore.empty()) { m_VBWaterShore = g_VBMan.Allocate(sizeof(SWaterVertex), water_vertex_data_shore.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER); m_VBWaterShore->m_Owner->UpdateChunkVertices(m_VBWaterShore, &water_vertex_data_shore[0]); // Construct indices buffer m_VBWaterIndicesShore = g_VBMan.Allocate(sizeof(GLushort), water_indices_shore.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER); m_VBWaterIndicesShore->m_Owner->UpdateChunkVertices(m_VBWaterIndicesShore, &water_indices_shore[0]); } } void CPatchRData::RenderWater(CShaderProgramPtr& shader, bool onlyShore, bool fixedPipeline) { ASSERT(m_UpdateFlags==0); if (g_Renderer.m_SkipSubmit || (!m_VBWater && !m_VBWaterShore)) return; #if !CONFIG2_GLES if (g_Renderer.GetWaterRenderMode() == WIREFRAME) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); #endif if (m_VBWater != 0x0 && !onlyShore) { SWaterVertex *base=(SWaterVertex *)m_VBWater->m_Owner->Bind(); // setup data pointers GLsizei stride = sizeof(SWaterVertex); shader->VertexPointer(3, GL_FLOAT, stride, &base[m_VBWater->m_Index].m_Position); if (!fixedPipeline) shader->VertexAttribPointer(str_a_waterInfo, 2, GL_FLOAT, false, stride, &base[m_VBWater->m_Index].m_WaterData); shader->AssertPointersBound(); u8* indexBase = m_VBWaterIndices->m_Owner->Bind(); glDrawElements(GL_TRIANGLES, (GLsizei) m_VBWaterIndices->m_Count, GL_UNSIGNED_SHORT, indexBase + sizeof(u16)*(m_VBWaterIndices->m_Index)); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_WaterTris += m_VBWaterIndices->m_Count / 3; } if (m_VBWaterShore != 0x0 && g_Renderer.GetWaterManager()->m_WaterEffects && g_Renderer.GetWaterManager()->m_WaterFancyEffects) { SWaterVertex *base=(SWaterVertex *)m_VBWaterShore->m_Owner->Bind(); GLsizei stride = sizeof(SWaterVertex); shader->VertexPointer(3, GL_FLOAT, stride, &base[m_VBWaterShore->m_Index].m_Position); if (!fixedPipeline) shader->VertexAttribPointer(str_a_waterInfo, 2, GL_FLOAT, false, stride, &base[m_VBWaterShore->m_Index].m_WaterData); shader->AssertPointersBound(); u8* indexBase = m_VBWaterIndicesShore->m_Owner->Bind(); glDrawElements(GL_TRIANGLES, (GLsizei) m_VBWaterIndicesShore->m_Count, GL_UNSIGNED_SHORT, indexBase + sizeof(u16)*(m_VBWaterIndicesShore->m_Index)); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_WaterTris += m_VBWaterIndicesShore->m_Count / 3; } CVertexBuffer::Unbind(); #if !CONFIG2_GLES if (g_Renderer.GetWaterRenderMode() == WIREFRAME) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #endif } Index: ps/trunk/source/simulation2/components/CCmpCinemaManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpCinemaManager.cpp (revision 24486) +++ ps/trunk/source/simulation2/components/CCmpCinemaManager.cpp (revision 24487) @@ -1,366 +1,366 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 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 "ICmpCinemaManager.h" #include "ps/CLogger.h" #include "simulation2/components/ICmpOverlayRenderer.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/Simulation2.h" class CCmpCinemaManager : public ICmpCinemaManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update); } DEFAULT_COMPONENT_ALLOCATOR(CinemaManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_Enabled = false; m_MapRevealed = false; m_ElapsedTime = fixed::Zero(); m_TotalTime = fixed::Zero(); m_CurrentPathElapsedTime = fixed::Zero(); } virtual void Deinit() { } virtual void Serialize(ISerializer& serializer) { serializer.Bool("Enabled", m_Enabled); serializer.NumberFixed_Unbounded("ElapsedTime", m_ElapsedTime); serializer.NumberFixed_Unbounded("CurrentPathElapsedTime", m_CurrentPathElapsedTime); serializer.Bool("MapRevealed", m_MapRevealed); serializer.NumberU32_Unbounded("NumberOfPaths", m_Paths.size()); - for (const std::pair& it : m_Paths) + for (const std::pair& it : m_Paths) SerializePath(it.second, serializer); serializer.NumberU32_Unbounded("NumberOfQueuedPaths", m_PathQueue.size()); for (const CCinemaPath& path : m_PathQueue) serializer.String("PathName", path.GetName(), 1, 128); } virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserializer) { deserializer.Bool("Enabled", m_Enabled); deserializer.NumberFixed_Unbounded("ElapsedTime", m_ElapsedTime); deserializer.NumberFixed_Unbounded("CurrentPathElapsedTime", m_CurrentPathElapsedTime); deserializer.Bool("MapRevealed", m_MapRevealed); uint32_t numberOfPaths = 0; deserializer.NumberU32_Unbounded("NumberOfPaths", numberOfPaths); for (uint32_t i = 0; i < numberOfPaths; ++i) { CCinemaPath path = DeserializePath(deserializer); m_Paths[path.GetName()] = path; } uint32_t numberOfQueuedPaths = 0; deserializer.NumberU32_Unbounded("NumberOfQueuedPaths", numberOfQueuedPaths); for (uint32_t i = 0; i < numberOfQueuedPaths; ++i) { CStrW pathName; deserializer.String("PathName", pathName, 1, 128); ENSURE(HasPath(pathName)); AddCinemaPathToQueue(pathName); } if (!m_PathQueue.empty()) { m_PathQueue.front().m_TimeElapsed = m_CurrentPathElapsedTime.ToFloat(); m_PathQueue.front().Validate(); } SetEnabled(m_Enabled); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update: { const CMessageUpdate &msgData = static_cast(msg); if (!m_Enabled) break; m_ElapsedTime += msgData.turnLength; m_CurrentPathElapsedTime += msgData.turnLength; if (m_CurrentPathElapsedTime >= m_PathQueue.front().GetDuration()) { CMessageCinemaPathEnded msgCinemaPathEnded(m_PathQueue.front().GetName()); m_PathQueue.pop_front(); GetSimContext().GetComponentManager().PostMessage(SYSTEM_ENTITY, msgCinemaPathEnded); m_CurrentPathElapsedTime = fixed::Zero(); if (!m_PathQueue.empty()) m_PathQueue.front().Reset(); } if (m_ElapsedTime >= m_TotalTime) { m_CurrentPathElapsedTime = fixed::Zero(); m_ElapsedTime = fixed::Zero(); m_TotalTime = fixed::Zero(); SetEnabled(false); GetSimContext().GetComponentManager().PostMessage(SYSTEM_ENTITY, CMessageCinemaQueueEnded()); } break; } default: break; } } virtual void AddPath(const CCinemaPath& path) { if (m_Paths.find(path.GetName()) != m_Paths.end()) { LOGWARNING("Path with name '%s' already exists", path.GetName().ToUTF8()); return; } m_Paths[path.GetName()] = path; } virtual void AddCinemaPathToQueue(const CStrW& name) { if (!HasPath(name)) { LOGWARNING("Path with name '%s' doesn't exist", name.ToUTF8()); return; } m_PathQueue.push_back(m_Paths[name]); if (m_PathQueue.size() == 1) m_PathQueue.front().Reset(); m_TotalTime += m_Paths[name].GetDuration(); } virtual void Play() { SetEnabled(true); } virtual void Stop() { SetEnabled(false); } virtual bool HasPath(const CStrW& name) const { return m_Paths.find(name) != m_Paths.end(); } virtual void ClearQueue() { m_PathQueue.clear(); } virtual void DeletePath(const CStrW& name) { if (!HasPath(name)) { LOGWARNING("Path with name '%s' doesn't exist", name.ToUTF8()); return; } m_PathQueue.remove_if([name](const CCinemaPath& path) { return path.GetName() == name; }); m_Paths.erase(name); } virtual const std::map& GetPaths() const { return m_Paths; } virtual void SetPaths(const std::map& newPaths) { m_Paths = newPaths; } virtual const std::list& GetQueue() const { return m_PathQueue; } virtual bool IsEnabled() const { return m_Enabled; } virtual void SetEnabled(bool enabled) { if (m_PathQueue.empty() && enabled) enabled = false; if (m_Enabled == enabled) return; CmpPtr cmpRangeManager(GetSimContext().GetSystemEntity()); CmpPtr cmpTerritoryManager(GetSimContext().GetSystemEntity()); if (cmpRangeManager) { if (enabled) m_MapRevealed = cmpRangeManager->GetLosRevealAll(-1); // TODO: improve m_MapRevealed state and without fade in cmpRangeManager->SetLosRevealAll(-1, enabled); } if (cmpTerritoryManager) cmpTerritoryManager->SetVisibility(!enabled); ICmpSelectable::SetOverrideVisibility(!enabled); ICmpOverlayRenderer::SetOverrideVisibility(!enabled); m_Enabled = enabled; } virtual void PlayQueue(const float deltaRealTime, CCamera* camera) { if (m_PathQueue.empty()) return; m_PathQueue.front().Play(deltaRealTime, camera); } private: void SerializePath(const CCinemaPath& path, ISerializer& serializer) { const CCinemaData* data = path.GetData(); serializer.String("PathName", data->m_Name, 1, 128); serializer.String("PathOrientation", data->m_Orientation, 1, 128); serializer.String("PathMode", data->m_Mode, 1, 128); serializer.String("PathStyle", data->m_Style, 1, 128); serializer.NumberFixed_Unbounded("PathTimescale", data->m_Timescale); serializer.Bool("LookAtTarget", data->m_LookAtTarget); serializer.NumberU32("NumberOfNodes", path.GetAllNodes().size(), 1, MAX_SPLINE_NODES); const std::vector& nodes = path.GetAllNodes(); for (size_t i = 0; i < nodes.size(); ++i) { if (i > 0) serializer.NumberFixed_Unbounded("NodeDeltaTime", nodes[i - 1].Distance); else serializer.NumberFixed_Unbounded("NodeDeltaTime", fixed::Zero()); serializer.NumberFixed_Unbounded("PositionX", nodes[i].Position.X); serializer.NumberFixed_Unbounded("PositionY", nodes[i].Position.Y); serializer.NumberFixed_Unbounded("PositionZ", nodes[i].Position.Z); serializer.NumberFixed_Unbounded("RotationX", nodes[i].Rotation.X); serializer.NumberFixed_Unbounded("RotationY", nodes[i].Rotation.Y); serializer.NumberFixed_Unbounded("RotationZ", nodes[i].Rotation.Z); } if (!data->m_LookAtTarget) return; const std::vector& targetNodes = path.GetTargetSpline().GetAllNodes(); serializer.NumberU32("NumberOfTargetNodes", targetNodes.size(), 1, MAX_SPLINE_NODES); for (size_t i = 0; i < targetNodes.size(); ++i) { if (i > 0) serializer.NumberFixed_Unbounded("NodeDeltaTime", targetNodes[i - 1].Distance); else serializer.NumberFixed_Unbounded("NodeDeltaTime", fixed::Zero()); serializer.NumberFixed_Unbounded("PositionX", targetNodes[i].Position.X); serializer.NumberFixed_Unbounded("PositionY", targetNodes[i].Position.Y); serializer.NumberFixed_Unbounded("PositionZ", targetNodes[i].Position.Z); } } CCinemaPath DeserializePath(IDeserializer& deserializer) { CCinemaData data; deserializer.String("PathName", data.m_Name, 1, 128); deserializer.String("PathOrientation", data.m_Orientation, 1, 128); deserializer.String("PathMode", data.m_Mode, 1, 128); deserializer.String("PathStyle", data.m_Style, 1, 128); deserializer.NumberFixed_Unbounded("PathTimescale", data.m_Timescale); deserializer.Bool("LookAtTarget", data.m_LookAtTarget); TNSpline pathSpline, targetSpline; uint32_t numberOfNodes = 0; deserializer.NumberU32("NumberOfNodes", numberOfNodes, 1, MAX_SPLINE_NODES); for (uint32_t j = 0; j < numberOfNodes; ++j) { SplineData node; deserializer.NumberFixed_Unbounded("NodeDeltaTime", node.Distance); deserializer.NumberFixed_Unbounded("PositionX", node.Position.X); deserializer.NumberFixed_Unbounded("PositionY", node.Position.Y); deserializer.NumberFixed_Unbounded("PositionZ", node.Position.Z); deserializer.NumberFixed_Unbounded("RotationX", node.Rotation.X); deserializer.NumberFixed_Unbounded("RotationY", node.Rotation.Y); deserializer.NumberFixed_Unbounded("RotationZ", node.Rotation.Z); pathSpline.AddNode(node.Position, node.Rotation, node.Distance); } if (data.m_LookAtTarget) { uint32_t numberOfTargetNodes = 0; deserializer.NumberU32("NumberOfTargetNodes", numberOfTargetNodes, 1, MAX_SPLINE_NODES); for (uint32_t j = 0; j < numberOfTargetNodes; ++j) { SplineData node; deserializer.NumberFixed_Unbounded("NodeDeltaTime", node.Distance); deserializer.NumberFixed_Unbounded("PositionX", node.Position.X); deserializer.NumberFixed_Unbounded("PositionY", node.Position.Y); deserializer.NumberFixed_Unbounded("PositionZ", node.Position.Z); targetSpline.AddNode(node.Position, CFixedVector3D(), node.Distance); } } return CCinemaPath(data, pathSpline, targetSpline); } bool m_Enabled; std::map m_Paths; std::list m_PathQueue; // States before playing bool m_MapRevealed; fixed m_ElapsedTime; fixed m_TotalTime; fixed m_CurrentPathElapsedTime; }; REGISTER_COMPONENT_TYPE(CinemaManager) Index: ps/trunk/source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpPathfinder.cpp (revision 24486) +++ ps/trunk/source/simulation2/components/CCmpPathfinder.cpp (revision 24487) @@ -1,988 +1,988 @@ /* Copyright (C) 2020 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 . */ /** * @file * Common code and setup code for CCmpPathfinder. */ #include "precompiled.h" #include "CCmpPathfinder_Common.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/HierarchicalPathfinder.h" #include "simulation2/helpers/LongPathfinder.h" #include "simulation2/helpers/MapEdgeTiles.h" #include "simulation2/helpers/Rasterize.h" #include "simulation2/helpers/VertexPathfinder.h" #include "simulation2/serialization/SerializedPathfinder.h" #include "simulation2/serialization/SerializedTypes.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "renderer/Scene.h" REGISTER_COMPONENT_TYPE(Pathfinder) void CCmpPathfinder::Init(const CParamNode& UNUSED(paramNode)) { m_MapSize = 0; m_Grid = NULL; m_TerrainOnlyGrid = NULL; FlushAIPathfinderDirtinessInformation(); m_NextAsyncTicket = 1; m_AtlasOverlay = NULL; m_VertexPathfinder = std::unique_ptr(new VertexPathfinder(m_MapSize, m_TerrainOnlyGrid)); m_LongPathfinder = std::unique_ptr(new LongPathfinder()); m_PathfinderHier = std::unique_ptr(new HierarchicalPathfinder()); // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng"); // Since this is used as a system component (not loaded from an entity template), // we can't use the real paramNode (it won't get handled properly when deserializing), // so load the data from a special XML file. CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); // Previously all move commands during a turn were // queued up and processed asynchronously at the start // of the next turn. Now we are processing queued up // events several times duing the turn. This improves // responsiveness and units move more smoothly especially. // when in formation. There is still a call at the // beginning of a turn to process all outstanding moves - // this will handle any moves above the MaxSameTurnMoves // threshold. // // TODO - The moves processed at the beginning of the // turn do not count against the maximum moves per turn // currently. The thinking is that this will eventually // happen in another thread. Either way this probably // will require some adjustment and rethinking. const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder"); m_MaxSameTurnMoves = (u16)pathingSettings.GetChild("MaxSameTurnMoves").ToInt(); const CParamNode::ChildrenMap& passClasses = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses").GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = passClasses.begin(); it != passClasses.end(); ++it) { std::string name = it->first; ENSURE((int)m_PassClasses.size() <= PASS_CLASS_BITS); pass_class_t mask = PASS_CLASS_MASK_FROM_INDEX(m_PassClasses.size()); m_PassClasses.push_back(PathfinderPassability(mask, it->second)); m_PassClassMasks[name] = mask; } m_Workers.emplace_back(PathfinderWorker{}); } CCmpPathfinder::~CCmpPathfinder() {}; void CCmpPathfinder::Deinit() { m_Workers.clear(); SetDebugOverlay(false); // cleans up memory SAFE_DELETE(m_AtlasOverlay); SAFE_DELETE(m_Grid); SAFE_DELETE(m_TerrainOnlyGrid); } template<> struct SerializeHelper { template void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify value) { serialize.NumberU32_Unbounded("ticket", value.ticket); serialize.NumberFixed_Unbounded("x0", value.x0); serialize.NumberFixed_Unbounded("z0", value.z0); Serializer(serialize, "goal", value.goal); serialize.NumberU16_Unbounded("pass class", value.passClass); serialize.NumberU32_Unbounded("notify", value.notify); } }; template<> struct SerializeHelper { template void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify value) { serialize.NumberU32_Unbounded("ticket", value.ticket); serialize.NumberFixed_Unbounded("x0", value.x0); serialize.NumberFixed_Unbounded("z0", value.z0); serialize.NumberFixed_Unbounded("clearance", value.clearance); serialize.NumberFixed_Unbounded("range", value.range); Serializer(serialize, "goal", value.goal); serialize.NumberU16_Unbounded("pass class", value.passClass); serialize.Bool("avoid moving units", value.avoidMovingUnits); serialize.NumberU32_Unbounded("group", value.group); serialize.NumberU32_Unbounded("notify", value.notify); } }; template void CCmpPathfinder::SerializeCommon(S& serialize) { Serializer(serialize, "long requests", m_LongPathRequests); Serializer(serialize, "short requests", m_ShortPathRequests); serialize.NumberU32_Unbounded("next ticket", m_NextAsyncTicket); serialize.NumberU16_Unbounded("map size", m_MapSize); } void CCmpPathfinder::Serialize(ISerializer& serialize) { SerializeCommon(serialize); } void CCmpPathfinder::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); } void CCmpPathfinder::HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } case MT_TerrainChanged: { const CMessageTerrainChanged& msgData = static_cast(msg); m_TerrainDirty = true; MinimalTerrainUpdate(msgData.i0, msgData.j0, msgData.i1, msgData.j1); break; } case MT_WaterChanged: case MT_ObstructionMapShapeChanged: m_TerrainDirty = true; UpdateGrid(); break; case MT_Deserialized: UpdateGrid(); // In case we were serialised with requests pending, we need to process them. if (!m_ShortPathRequests.empty() || !m_LongPathRequests.empty()) { ENSURE(CmpPtr(GetSystemEntity())); StartProcessingMoves(false); } break; } } void CCmpPathfinder::RenderSubmit(SceneCollector& collector) { m_VertexPathfinder->RenderSubmit(collector); m_PathfinderHier->RenderSubmit(collector); } void CCmpPathfinder::SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) { m_LongPathfinder->SetDebugPath(*m_PathfinderHier, x0, z0, goal, passClass); } void CCmpPathfinder::SetDebugOverlay(bool enabled) { m_VertexPathfinder->SetDebugOverlay(enabled); m_LongPathfinder->SetDebugOverlay(enabled); } void CCmpPathfinder::SetHierDebugOverlay(bool enabled) { m_PathfinderHier->SetDebugOverlay(enabled, &GetSimContext()); } void CCmpPathfinder::GetDebugData(u32& steps, double& time, Grid& grid) const { m_LongPathfinder->GetDebugData(steps, time, grid); } void CCmpPathfinder::SetAtlasOverlay(bool enable, pass_class_t passClass) { if (enable) { if (!m_AtlasOverlay) m_AtlasOverlay = new AtlasOverlay(this, passClass); m_AtlasOverlay->m_PassClass = passClass; } else SAFE_DELETE(m_AtlasOverlay); } pass_class_t CCmpPathfinder::GetPassabilityClass(const std::string& name) const { std::map::const_iterator it = m_PassClassMasks.find(name); if (it == m_PassClassMasks.end()) { LOGERROR("Invalid passability class name '%s'", name.c_str()); return 0; } return it->second; } void CCmpPathfinder::GetPassabilityClasses(std::map& passClasses) const { passClasses = m_PassClassMasks; } void CCmpPathfinder::GetPassabilityClasses(std::map& nonPathfindingPassClasses, std::map& pathfindingPassClasses) const { - for (const std::pair& pair : m_PassClassMasks) + for (const std::pair& pair : m_PassClassMasks) { if ((GetPassabilityFromMask(pair.second)->m_Obstructions == PathfinderPassability::PATHFINDING)) pathfindingPassClasses[pair.first] = pair.second; else nonPathfindingPassClasses[pair.first] = pair.second; } } const PathfinderPassability* CCmpPathfinder::GetPassabilityFromMask(pass_class_t passClass) const { for (const PathfinderPassability& passability : m_PassClasses) { if (passability.m_Mask == passClass) return &passability; } return NULL; } const Grid& CCmpPathfinder::GetPassabilityGrid() { if (!m_Grid) UpdateGrid(); return *m_Grid; } /** * Given a grid of passable/impassable navcells (based on some passability mask), * computes a new grid where a navcell is impassable (per that mask) if * it is <=clearance navcells away from an impassable navcell in the original grid. * The results are ORed onto the original grid. * * This is used for adding clearance onto terrain-based navcell passability. * * TODO PATHFINDER: might be nicer to get rounded corners by measuring clearances as * Euclidean distances; currently it effectively does dist=max(dx,dy) instead. * This would only really be a problem for big clearances. */ static void ExpandImpassableCells(Grid& grid, u16 clearance, pass_class_t mask) { PROFILE3("ExpandImpassableCells"); u16 w = grid.m_W; u16 h = grid.m_H; // First expand impassable cells horizontally into a temporary 1-bit grid Grid tempGrid(w, h); for (u16 j = 0; j < h; ++j) { // New cell (i,j) is blocked if (i',j) blocked for any i-clearance <= i' <= i+clearance // Count the number of blocked cells around i=0 u16 numBlocked = 0; for (u16 i = 0; i <= clearance && i < w; ++i) if (!IS_PASSABLE(grid.get(i, j), mask)) ++numBlocked; for (u16 i = 0; i < w; ++i) { // Store a flag if blocked by at least one nearby cell if (numBlocked) tempGrid.set(i, j, 1); // Slide the numBlocked window along: // remove the old i-clearance value, add the new (i+1)+clearance // (avoiding overflowing the grid) if (i >= clearance && !IS_PASSABLE(grid.get(i-clearance, j), mask)) --numBlocked; if (i+1+clearance < w && !IS_PASSABLE(grid.get(i+1+clearance, j), mask)) ++numBlocked; } } for (u16 i = 0; i < w; ++i) { // New cell (i,j) is blocked if (i,j') blocked for any j-clearance <= j' <= j+clearance // Count the number of blocked cells around j=0 u16 numBlocked = 0; for (u16 j = 0; j <= clearance && j < h; ++j) if (tempGrid.get(i, j)) ++numBlocked; for (u16 j = 0; j < h; ++j) { // Add the mask if blocked by at least one nearby cell if (numBlocked) grid.set(i, j, grid.get(i, j) | mask); // Slide the numBlocked window along: // remove the old j-clearance value, add the new (j+1)+clearance // (avoiding overflowing the grid) if (j >= clearance && tempGrid.get(i, j-clearance)) --numBlocked; if (j+1+clearance < h && tempGrid.get(i, j+1+clearance)) ++numBlocked; } } } Grid CCmpPathfinder::ComputeShoreGrid(bool expandOnWater) { PROFILE3("ComputeShoreGrid"); CmpPtr cmpWaterManager(GetSystemEntity()); // TODO: these bits should come from ICmpTerrain CTerrain& terrain = GetSimContext().GetTerrain(); // avoid integer overflow in intermediate calculation const u16 shoreMax = 32767; // First pass - find underwater tiles Grid waterGrid(m_MapSize, m_MapSize); for (u16 j = 0; j < m_MapSize; ++j) { for (u16 i = 0; i < m_MapSize; ++i) { fixed x, z; Pathfinding::TileCenter(i, j, x, z); bool underWater = cmpWaterManager && (cmpWaterManager->GetWaterLevel(x, z) > terrain.GetExactGroundLevelFixed(x, z)); waterGrid.set(i, j, underWater ? 1 : 0); } } // Second pass - find shore tiles Grid shoreGrid(m_MapSize, m_MapSize); for (u16 j = 0; j < m_MapSize; ++j) { for (u16 i = 0; i < m_MapSize; ++i) { // Find a land tile if (!waterGrid.get(i, j)) { // If it's bordered by water, it's a shore tile if ((i > 0 && waterGrid.get(i-1, j)) || (i > 0 && j < m_MapSize-1 && waterGrid.get(i-1, j+1)) || (i > 0 && j > 0 && waterGrid.get(i-1, j-1)) || (i < m_MapSize-1 && waterGrid.get(i+1, j)) || (i < m_MapSize-1 && j < m_MapSize-1 && waterGrid.get(i+1, j+1)) || (i < m_MapSize-1 && j > 0 && waterGrid.get(i+1, j-1)) || (j > 0 && waterGrid.get(i, j-1)) || (j < m_MapSize-1 && waterGrid.get(i, j+1)) ) shoreGrid.set(i, j, 0); else shoreGrid.set(i, j, shoreMax); } // If we want to expand on water, we want water tiles not to be shore tiles else if (expandOnWater) shoreGrid.set(i, j, shoreMax); } } // Expand influences on land to find shore distance for (u16 y = 0; y < m_MapSize; ++y) { u16 min = shoreMax; for (u16 x = 0; x < m_MapSize; ++x) { if (!waterGrid.get(x, y) || expandOnWater) { u16 g = shoreGrid.get(x, y); if (g > min) shoreGrid.set(x, y, min); else if (g < min) min = g; ++min; } } for (u16 x = m_MapSize; x > 0; --x) { if (!waterGrid.get(x-1, y) || expandOnWater) { u16 g = shoreGrid.get(x-1, y); if (g > min) shoreGrid.set(x-1, y, min); else if (g < min) min = g; ++min; } } } for (u16 x = 0; x < m_MapSize; ++x) { u16 min = shoreMax; for (u16 y = 0; y < m_MapSize; ++y) { if (!waterGrid.get(x, y) || expandOnWater) { u16 g = shoreGrid.get(x, y); if (g > min) shoreGrid.set(x, y, min); else if (g < min) min = g; ++min; } } for (u16 y = m_MapSize; y > 0; --y) { if (!waterGrid.get(x, y-1) || expandOnWater) { u16 g = shoreGrid.get(x, y-1); if (g > min) shoreGrid.set(x, y-1, min); else if (g < min) min = g; ++min; } } } return shoreGrid; } void CCmpPathfinder::UpdateGrid() { PROFILE3("UpdateGrid"); CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); if (!cmpTerrain) return; // error u16 terrainSize = cmpTerrain->GetTilesPerSide(); if (terrainSize == 0) return; // If the terrain was resized then delete the old grid data if (m_Grid && m_MapSize != terrainSize) { SAFE_DELETE(m_Grid); SAFE_DELETE(m_TerrainOnlyGrid); } // Initialise the terrain data when first needed if (!m_Grid) { m_MapSize = terrainSize; m_Grid = new Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE); SAFE_DELETE(m_TerrainOnlyGrid); m_TerrainOnlyGrid = new Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE); m_DirtinessInformation = { true, true, Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE) }; m_AIPathfinderDirtinessInformation = m_DirtinessInformation; m_TerrainDirty = true; } // The grid should be properly initialized and clean. Checking the latter is expensive so do it only for debugging. #ifdef NDEBUG ENSURE(m_DirtinessInformation.dirtinessGrid.compare_sizes(m_Grid)); #else ENSURE(m_DirtinessInformation.dirtinessGrid == Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE)); #endif CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); cmpObstructionManager->UpdateInformations(m_DirtinessInformation); if (!m_DirtinessInformation.dirty && !m_TerrainDirty) return; // If the terrain has changed, recompute m_Grid // Else, use data from m_TerrainOnlyGrid and add obstructions if (m_TerrainDirty) { TerrainUpdateHelper(); *m_Grid = *m_TerrainOnlyGrid; m_TerrainDirty = false; m_DirtinessInformation.globallyDirty = true; } else if (m_DirtinessInformation.globallyDirty) { ENSURE(m_Grid->compare_sizes(m_TerrainOnlyGrid)); memcpy(m_Grid->m_Data, m_TerrainOnlyGrid->m_Data, (m_Grid->m_W)*(m_Grid->m_H)*sizeof(NavcellData)); } else { ENSURE(m_Grid->compare_sizes(m_TerrainOnlyGrid)); for (u16 j = 0; j < m_DirtinessInformation.dirtinessGrid.m_H; ++j) for (u16 i = 0; i < m_DirtinessInformation.dirtinessGrid.m_W; ++i) if (m_DirtinessInformation.dirtinessGrid.get(i, j) == 1) m_Grid->set(i, j, m_TerrainOnlyGrid->get(i, j)); } // Add obstructions onto the grid cmpObstructionManager->Rasterize(*m_Grid, m_PassClasses, m_DirtinessInformation.globallyDirty); // Update the long-range and hierarchical pathfinders. if (m_DirtinessInformation.globallyDirty) { std::map nonPathfindingPassClasses, pathfindingPassClasses; GetPassabilityClasses(nonPathfindingPassClasses, pathfindingPassClasses); m_LongPathfinder->Reload(m_Grid); m_PathfinderHier->Recompute(m_Grid, nonPathfindingPassClasses, pathfindingPassClasses); } else { m_LongPathfinder->Update(m_Grid); m_PathfinderHier->Update(m_Grid, m_DirtinessInformation.dirtinessGrid); } // Remember the necessary updates that the AI pathfinder will have to perform as well m_AIPathfinderDirtinessInformation.MergeAndClear(m_DirtinessInformation); } void CCmpPathfinder::MinimalTerrainUpdate(int itile0, int jtile0, int itile1, int jtile1) { TerrainUpdateHelper(false, itile0, jtile0, itile1, jtile1); } void CCmpPathfinder::TerrainUpdateHelper(bool expandPassability, int itile0, int jtile0, int itile1, int jtile1) { PROFILE3("TerrainUpdateHelper"); CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY); CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); CTerrain& terrain = GetSimContext().GetTerrain(); if (!cmpTerrain || !cmpObstructionManager) return; u16 terrainSize = cmpTerrain->GetTilesPerSide(); if (terrainSize == 0) return; const bool needsNewTerrainGrid = !m_TerrainOnlyGrid || m_MapSize != terrainSize; if (needsNewTerrainGrid) { m_MapSize = terrainSize; SAFE_DELETE(m_TerrainOnlyGrid); m_TerrainOnlyGrid = new Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE); // If this update comes from a map resizing, we must reinitialize the other grids as well if (!m_TerrainOnlyGrid->compare_sizes(m_Grid)) { SAFE_DELETE(m_Grid); m_Grid = new Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE); m_DirtinessInformation = { true, true, Grid(m_MapSize * Pathfinding::NAVCELLS_PER_TILE, m_MapSize * Pathfinding::NAVCELLS_PER_TILE) }; m_AIPathfinderDirtinessInformation = m_DirtinessInformation; } } Grid shoreGrid = ComputeShoreGrid(); const bool partialTerrainGridUpdate = !expandPassability && !needsNewTerrainGrid && itile0 != -1 && jtile0 != -1 && itile1 != -1 && jtile1 != -1; int istart = 0, iend = m_MapSize * Pathfinding::NAVCELLS_PER_TILE; int jstart = 0, jend = m_MapSize * Pathfinding::NAVCELLS_PER_TILE; if (partialTerrainGridUpdate) { // We need to extend the boundaries by 1 tile, because slope and ground // level are calculated by multiple neighboring tiles. // TODO: add CTerrain constant instead of 1. istart = Clamp(itile0 - 1, 0, static_cast(m_MapSize)) * Pathfinding::NAVCELLS_PER_TILE; iend = Clamp(itile1 + 1, 0, static_cast(m_MapSize)) * Pathfinding::NAVCELLS_PER_TILE; jstart = Clamp(jtile0 - 1, 0, static_cast(m_MapSize)) * Pathfinding::NAVCELLS_PER_TILE; jend = Clamp(jtile1 + 1, 0, static_cast(m_MapSize)) * Pathfinding::NAVCELLS_PER_TILE; } // Compute initial terrain-dependent passability for (int j = jstart; j < jend; ++j) { for (int i = istart; i < iend; ++i) { // World-space coordinates for this navcell fixed x, z; Pathfinding::NavcellCenter(i, j, x, z); // Terrain-tile coordinates for this navcell int itile = i / Pathfinding::NAVCELLS_PER_TILE; int jtile = j / Pathfinding::NAVCELLS_PER_TILE; // Gather all the data potentially needed to determine passability: fixed height = terrain.GetExactGroundLevelFixed(x, z); fixed water; if (cmpWaterManager) water = cmpWaterManager->GetWaterLevel(x, z); fixed depth = water - height; // Exact slopes give kind of weird output, so just use rough tile-based slopes fixed slope = terrain.GetSlopeFixed(itile, jtile); // Get world-space coordinates from shoreGrid (which uses terrain tiles) fixed shoredist = fixed::FromInt(shoreGrid.get(itile, jtile)).MultiplyClamp(TERRAIN_TILE_SIZE); // Compute the passability for every class for this cell NavcellData t = 0; for (const PathfinderPassability& passability : m_PassClasses) if (!passability.IsPassable(depth, slope, shoredist)) t |= passability.m_Mask; m_TerrainOnlyGrid->set(i, j, t); } } // Compute off-world passability const int edgeSize = MAP_EDGE_TILES * Pathfinding::NAVCELLS_PER_TILE; NavcellData edgeMask = 0; for (const PathfinderPassability& passability : m_PassClasses) edgeMask |= passability.m_Mask; int w = m_TerrainOnlyGrid->m_W; int h = m_TerrainOnlyGrid->m_H; if (cmpObstructionManager->GetPassabilityCircular()) { for (int j = jstart; j < jend; ++j) { for (int i = istart; i < iend; ++i) { // Based on CCmpRangeManager::LosIsOffWorld // but tweaked since it's tile-based instead. // (We double all the values so we can handle half-tile coordinates.) // This needs to be slightly tighter than the LOS circle, // else units might get themselves lost in the SoD around the edge. int dist2 = (i*2 + 1 - w)*(i*2 + 1 - w) + (j*2 + 1 - h)*(j*2 + 1 - h); if (dist2 >= (w - 2*edgeSize) * (h - 2*edgeSize)) m_TerrainOnlyGrid->set(i, j, m_TerrainOnlyGrid->get(i, j) | edgeMask); } } } else { for (u16 j = 0; j < h; ++j) for (u16 i = 0; i < edgeSize; ++i) m_TerrainOnlyGrid->set(i, j, m_TerrainOnlyGrid->get(i, j) | edgeMask); for (u16 j = 0; j < h; ++j) for (u16 i = w-edgeSize+1; i < w; ++i) m_TerrainOnlyGrid->set(i, j, m_TerrainOnlyGrid->get(i, j) | edgeMask); for (u16 j = 0; j < edgeSize; ++j) for (u16 i = edgeSize; i < w-edgeSize+1; ++i) m_TerrainOnlyGrid->set(i, j, m_TerrainOnlyGrid->get(i, j) | edgeMask); for (u16 j = h-edgeSize+1; j < h; ++j) for (u16 i = edgeSize; i < w-edgeSize+1; ++i) m_TerrainOnlyGrid->set(i, j, m_TerrainOnlyGrid->get(i, j) | edgeMask); } if (!expandPassability) return; // Expand the impassability grid, for any class with non-zero clearance, // so that we can stop units getting too close to impassable navcells. // Note: It's not possible to perform this expansion once for all passabilities // with the same clearance, because the impassable cells are not necessarily the // same for all these passabilities. for (PathfinderPassability& passability : m_PassClasses) { if (passability.m_Clearance == fixed::Zero()) continue; int clearance = (passability.m_Clearance / Pathfinding::NAVCELL_SIZE).ToInt_RoundToInfinity(); ExpandImpassableCells(*m_TerrainOnlyGrid, clearance, passability.m_Mask); } } ////////////////////////////////////////////////////////// // Async pathfinder workers CCmpPathfinder::PathfinderWorker::PathfinderWorker() {} template void CCmpPathfinder::PathfinderWorker::PushRequests(std::vector&, ssize_t) { static_assert(sizeof(T) == 0, "Only specializations can be used"); } template<> void CCmpPathfinder::PathfinderWorker::PushRequests(std::vector& from, ssize_t amount) { m_LongRequests.insert(m_LongRequests.end(), std::make_move_iterator(from.end() - amount), std::make_move_iterator(from.end())); } template<> void CCmpPathfinder::PathfinderWorker::PushRequests(std::vector& from, ssize_t amount) { m_ShortRequests.insert(m_ShortRequests.end(), std::make_move_iterator(from.end() - amount), std::make_move_iterator(from.end())); } void CCmpPathfinder::PathfinderWorker::Work(const CCmpPathfinder& pathfinder) { while (!m_LongRequests.empty()) { const LongPathRequest& req = m_LongRequests.back(); WaypointPath path; pathfinder.m_LongPathfinder->ComputePath(*pathfinder.m_PathfinderHier, req.x0, req.z0, req.goal, req.passClass, path); m_Results.emplace_back(req.ticket, req.notify, path); m_LongRequests.pop_back(); } while (!m_ShortRequests.empty()) { const ShortPathRequest& req = m_ShortRequests.back(); WaypointPath path = pathfinder.m_VertexPathfinder->ComputeShortPath(req, CmpPtr(pathfinder.GetSystemEntity())); m_Results.emplace_back(req.ticket, req.notify, path); m_ShortRequests.pop_back(); } } u32 CCmpPathfinder::ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, entity_id_t notify) { LongPathRequest req = { m_NextAsyncTicket++, x0, z0, goal, passClass, notify }; m_LongPathRequests.push_back(req); return req.ticket; } u32 CCmpPathfinder::ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t clearance, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify) { ShortPathRequest req = { m_NextAsyncTicket++, x0, z0, clearance, range, goal, passClass, avoidMovingUnits, group, notify }; m_ShortPathRequests.push_back(req); return req.ticket; } void CCmpPathfinder::ComputePathImmediate(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret) const { m_LongPathfinder->ComputePath(*m_PathfinderHier, x0, z0, goal, passClass, ret); } WaypointPath CCmpPathfinder::ComputeShortPathImmediate(const ShortPathRequest& request) const { return m_VertexPathfinder->ComputeShortPath(request, CmpPtr(GetSystemEntity())); } void CCmpPathfinder::FetchAsyncResultsAndSendMessages() { PROFILE2("FetchAsyncResults"); // We may now clear existing requests. m_ShortPathRequests.clear(); m_LongPathRequests.clear(); // WARNING: the order in which moves are pulled must be consistent when using 1 or n workers. // We fetch in the same order we inserted in, but we push moves backwards, so this works. std::vector results; for (PathfinderWorker& worker : m_Workers) { results.insert(results.end(), std::make_move_iterator(worker.m_Results.begin()), std::make_move_iterator(worker.m_Results.end())); worker.m_Results.clear(); } { PROFILE2("PostMessages"); for (PathResult& path : results) { CMessagePathResult msg(path.ticket, path.path); GetSimContext().GetComponentManager().PostMessage(path.notify, msg); } } } void CCmpPathfinder::StartProcessingMoves(bool useMax) { std::vector longRequests = GetMovesToProcess(m_LongPathRequests, useMax, m_MaxSameTurnMoves); std::vector shortRequests = GetMovesToProcess(m_ShortPathRequests, useMax, m_MaxSameTurnMoves - longRequests.size()); PushRequestsToWorkers(longRequests); PushRequestsToWorkers(shortRequests); for (PathfinderWorker& worker : m_Workers) worker.Work(*this); } template std::vector CCmpPathfinder::GetMovesToProcess(std::vector& requests, bool useMax, size_t maxMoves) { // Keep the original requests in which we need to serialize. std::vector copiedRequests; if (useMax) { size_t amount = std::min(requests.size(), maxMoves); if (amount > 0) copiedRequests.insert(copiedRequests.begin(), requests.end() - amount, requests.end()); } else copiedRequests = requests; return copiedRequests; } template void CCmpPathfinder::PushRequestsToWorkers(std::vector& from) { if (from.empty()) return; // Trivial load-balancing, / rounds towards zero so add 1 to ensure we do push all requests. size_t amount = from.size() / m_Workers.size() + 1; // WARNING: the order in which moves are pushed must be consistent when using 1 or n workers. // In this instance, work is distributed in a strict LIFO order, effectively reversing tickets. for (PathfinderWorker& worker : m_Workers) { amount = std::min(amount, from.size()); // Since we are rounding up before, ensure we aren't pushing beyond the end. worker.PushRequests(from, amount); from.erase(from.end() - amount, from.end()); } } ////////////////////////////////////////////////////////// bool CCmpPathfinder::IsGoalReachable(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) { PROFILE2("IsGoalReachable"); u16 i, j; Pathfinding::NearestNavcell(x0, z0, i, j, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); if (!IS_PASSABLE(m_Grid->get(i, j), passClass)) m_PathfinderHier->FindNearestPassableNavcell(i, j, passClass); return m_PathfinderHier->IsGoalReachable(i, j, goal, passClass); } bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) const { PROFILE2_IFSPIKE("Check Movement", 0.001); // Test against obstructions first. filter may discard pathfinding-blocking obstructions. // Use more permissive version of TestLine to allow unit-unit collisions to overlap slightly. // This makes movement smoother and more natural for units, overall. CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager || cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r, true)) return false; // Then test against the terrain grid. This should not be necessary // But in case we allow terrain to change it will become so. return Pathfinding::CheckLineMovement(x0, z0, x1, z1, passClass, *m_TerrainOnlyGrid); } ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool UNUSED(onlyCenterPoint)) const { // Check unit obstruction CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return ICmpObstruction::FOUNDATION_CHECK_FAIL_ERROR; if (cmpObstructionManager->TestUnitShape(filter, x, z, r, NULL)) return ICmpObstruction::FOUNDATION_CHECK_FAIL_OBSTRUCTS_FOUNDATION; // Test against terrain and static obstructions: u16 i, j; Pathfinding::NearestNavcell(x, z, i, j, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); if (!IS_PASSABLE(m_Grid->get(i, j), passClass)) return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS; // (Static obstructions will be redundantly tested against in both the // obstruction-shape test and navcell-passability test, which is slightly // inefficient but shouldn't affect behaviour) return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckBuildingPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, entity_id_t id, pass_class_t passClass) const { return CCmpPathfinder::CheckBuildingPlacement(filter, x, z, a, w, h, id, passClass, false); } ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckBuildingPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, entity_id_t id, pass_class_t passClass, bool UNUSED(onlyCenterPoint)) const { // Check unit obstruction CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return ICmpObstruction::FOUNDATION_CHECK_FAIL_ERROR; if (cmpObstructionManager->TestStaticShape(filter, x, z, a, w, h, NULL)) return ICmpObstruction::FOUNDATION_CHECK_FAIL_OBSTRUCTS_FOUNDATION; // Test against terrain: ICmpObstructionManager::ObstructionSquare square; CmpPtr cmpObstruction(GetSimContext(), id); if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(square)) return ICmpObstruction::FOUNDATION_CHECK_FAIL_NO_OBSTRUCTION; entity_pos_t expand; const PathfinderPassability* passability = GetPassabilityFromMask(passClass); if (passability) expand = passability->m_Clearance; SimRasterize::Spans spans; SimRasterize::RasterizeRectWithClearance(spans, square, expand, Pathfinding::NAVCELL_SIZE); for (const SimRasterize::Span& span : spans) { i16 i0 = span.i0; i16 i1 = span.i1; i16 j = span.j; // Fail if any span extends outside the grid if (i0 < 0 || i1 > m_TerrainOnlyGrid->m_W || j < 0 || j > m_TerrainOnlyGrid->m_H) return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS; // Fail if any span includes an impassable tile for (i16 i = i0; i < i1; ++i) if (!IS_PASSABLE(m_TerrainOnlyGrid->get(i, j), passClass)) return ICmpObstruction::FOUNDATION_CHECK_FAIL_TERRAIN_CLASS; } return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 24486) +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 24487) @@ -1,2482 +1,2485 @@ /* Copyright (C) 2020 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 "ICmpRangeManager.h" #include "ICmpTerrain.h" #include "simulation2/system/EntityMap.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpFogging.h" #include "simulation2/components/ICmpMirage.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/components/ICmpVisibility.h" #include "simulation2/components/ICmpVision.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Los.h" #include "simulation2/helpers/MapEdgeTiles.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Spatial.h" #include "simulation2/serialization/SerializedTypes.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Scene.h" #define LOS_TILES_RATIO 8 #define DEBUG_RANGE_MANAGER_BOUNDS 0 /** * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) * into a 32-bit mask for quick set-membership tests. */ static inline u32 CalcOwnerMask(player_id_t owner) { if (owner >= -1 && owner < 31) return 1 << (1+owner); else return 0; // owner was invalid } /** * Returns LOS mask for given player. */ static inline u32 CalcPlayerLosMask(player_id_t player) { if (player > 0 && player <= 16) return (u32)LosState::MASK << (2*(player-1)); return 0; } /** * Returns shared LOS mask for given list of players. */ static u32 CalcSharedLosMask(std::vector players) { u32 playerMask = 0; for (size_t i = 0; i < players.size(); i++) playerMask |= CalcPlayerLosMask(players[i]); return playerMask; } /** * Add/remove a player to/from mask, which is a 1-bit mask representing a list of players. * Returns true if the mask is modified. */ static bool SetPlayerSharedDirtyVisibilityBit(u16& mask, player_id_t player, bool enable) { if (player <= 0 || player > 16) return false; u16 oldMask = mask; if (enable) mask |= (0x1 << (player - 1)); else mask &= ~(0x1 << (player - 1)); return oldMask != mask; } /** * Computes the 2-bit visibility for one player, given the total 32-bit visibilities */ static inline LosVisibility GetPlayerVisibility(u32 visibilities, player_id_t player) { if (player > 0 && player <= 16) return static_cast( (visibilities >> (2 *(player-1))) & 0x3 ); return LosVisibility::HIDDEN; } /** * Test whether the visibility is dirty for a given LoS tile and a given player */ static inline bool IsVisibilityDirty(u16 dirty, player_id_t player) { if (player > 0 && player <= 16) return (dirty >> (player - 1)) & 0x1; return false; } /** * Test whether a player share this vision */ static inline bool HasVisionSharing(u16 visionSharing, player_id_t player) { return (visionSharing & (1 << (player - 1))) != 0; } /** * Computes the shared vision mask for the player */ static inline u16 CalcVisionSharingMask(player_id_t player) { return 1 << (player-1); } /** * Representation of a range query. */ struct Query { std::vector lastMatch; CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it entity_pos_t minRange; entity_pos_t maxRange; entity_pos_t elevationBonus; // Used for parabolas only. u32 ownersMask; i32 interface; u8 flagsMask; bool enabled; bool parabolic; }; /** * Checks whether v is in a parabolic range of (0,0,0) * The highest point of the paraboloid is (0,range/2,0) * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid * This equates to computing f(x, z) = y = -(xx + zz)/(2*range) + range/2 > 0, * or alternatively √(xx+zz) <= √(range^2 - 2range*y). * * Avoids sqrting and overflowing. */ static bool InParabolicRange(CFixedVector3D v, fixed range) { u64 xx = SQUARE_U64_FIXED(v.X); // xx <= 2^62 u64 zz = SQUARE_U64_FIXED(v.Z); i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow) i32 y = v.Y.GetInternalValue(); i32 c = range.GetInternalValue(); i32 c_2 = c >> 1; i64 c2 = MUL_I64_I32_I32(c_2 - y, c); return d2 <= c2; } struct EntityParabolicRangeOutline { entity_id_t source; CFixedVector3D position; entity_pos_t range; std::vector outline; }; static std::map ParabolicRangesOutlines; /** * Representation of an entity, with the data needed for queries. */ enum FlagMasks { // flags used for queries None = 0x00, Normal = 0x01, Injured = 0x02, AllQuery = Normal | Injured, // 0x04 reserved for future use // general flags InWorld = 0x08, RetainInFog = 0x10, RevealShore = 0x20, ScriptedVisibility = 0x40, SharedVision = 0x80 }; struct EntityData { EntityData() : visibilities(0), size(0), visionSharing(0), owner(-1), flags(FlagMasks::Normal) { } entity_pos_t x, z; entity_pos_t visionRange; u32 visibilities; // 2-bit visibility, per player u32 size; u16 visionSharing; // 1-bit per player i8 owner; u8 flags; // See the FlagMasks enum template inline bool HasFlag() const { return (flags & mask) != 0; } template inline void SetFlag(bool val) { flags = val ? (flags | mask) : (flags & ~mask); } inline void SetFlag(u8 mask, bool val) { flags = val ? (flags | mask) : (flags & ~mask); } }; cassert(sizeof(EntityData) == 24); /** * Serialization helper template for Query */ template<> struct SerializeHelper { template void Common(S& serialize, const char* UNUSED(name), Serialize::qualify value) { serialize.Bool("enabled", value.enabled); serialize.Bool("parabolic",value.parabolic); serialize.NumberFixed_Unbounded("min range", value.minRange); serialize.NumberFixed_Unbounded("max range", value.maxRange); serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus); serialize.NumberU32_Unbounded("owners mask", value.ownersMask); serialize.NumberI32_Unbounded("interface", value.interface); Serializer(serialize, "last match", value.lastMatch); serialize.NumberU8_Unbounded("flagsMask", value.flagsMask); } void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext& UNUSED(context)) { Common(serialize, name, value); uint32_t id = value.source.GetId(); serialize.NumberU32_Unbounded("source", id); } void operator()(IDeserializer& deserialize, const char* name, Query& value, const CSimContext& context) { Common(deserialize, name, value); uint32_t id; deserialize.NumberU32_Unbounded("source", id); value.source = context.GetComponentManager().LookupEntityHandle(id, true); // the referenced entity might not have been deserialized yet, // so tell LookupEntityHandle to allocate the handle if necessary } }; /** * Serialization helper template for EntityData */ template<> struct SerializeHelper { template void operator()(S& serialize, const char* UNUSED(name), Serialize::qualify value) { serialize.NumberFixed_Unbounded("x", value.x); serialize.NumberFixed_Unbounded("z", value.z); serialize.NumberFixed_Unbounded("vision", value.visionRange); serialize.NumberU32_Unbounded("visibilities", value.visibilities); serialize.NumberU32_Unbounded("size", value.size); serialize.NumberU16_Unbounded("vision sharing", value.visionSharing); serialize.NumberI8_Unbounded("owner", value.owner); serialize.NumberU8_Unbounded("flags", value.flags); } }; /** * Functor for sorting entities by distance from a source point. * It must only be passed entities that are in 'entities' * and are currently in the world. */ -struct EntityDistanceOrdering +class EntityDistanceOrdering { +public: EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source) : m_EntityData(entities), m_Source(source) { } + EntityDistanceOrdering(const EntityDistanceOrdering& entity) = default; + bool operator()(entity_id_t a, entity_id_t b) const { const EntityData& da = m_EntityData.find(a)->second; const EntityData& db = m_EntityData.find(b)->second; CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source; CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source; return (vecA.CompareLength(vecB) < 0); } const EntityMap& m_EntityData; CFixedVector2D m_Source; private: EntityDistanceOrdering& operator=(const EntityDistanceOrdering&); }; /** * Range manager implementation. * Maintains a list of all entities (and their positions and owners), which is used for * queries. * * LOS implementation is based on the model described in GPG2. * (TODO: would be nice to make it cleverer, so e.g. mountains and walls * can block vision) */ class CCmpRangeManager : public ICmpRangeManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Create); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_Destroy); componentManager.SubscribeGloballyToMessageType(MT_VisionRangeChanged); componentManager.SubscribeGloballyToMessageType(MT_VisionSharingChanged); componentManager.SubscribeToMessageType(MT_Deserialized); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays } DEFAULT_COMPONENT_ALLOCATOR(RangeManager) bool m_DebugOverlayEnabled; bool m_DebugOverlayDirty; std::vector m_DebugOverlayLines; // Deserialization flag. A lot of different functions are called by Deserialize() // and we don't want to pass isDeserializing bool arguments to all of them... bool m_Deserializing; // World bounds (entities are expected to be within this range) entity_pos_t m_WorldX0; entity_pos_t m_WorldZ0; entity_pos_t m_WorldX1; entity_pos_t m_WorldZ1; // Range query state: tag_t m_QueryNext; // next allocated id std::map m_Queries; EntityMap m_EntityData; FastSpatialSubdivision m_Subdivision; // spatial index of m_EntityData std::vector m_SubdivisionResults; // LOS state: static const player_id_t MAX_LOS_PLAYER_ID = 16; using LosTile = std::pair; std::array m_LosRevealAll; bool m_LosCircular; i32 m_TerrainVerticesPerSide; // Cache for visibility tracking i32 m_LosTilesPerSide; bool m_GlobalVisibilityUpdate; std::array m_GlobalPlayerVisibilityUpdate; Grid m_DirtyVisibility; Grid> m_LosTiles; // List of entities that must be updated, regardless of the status of their tile std::vector m_ModifiedEntities; // Counts of units seeing vertex, per vertex, per player (starting with player 0). // Use u16 to avoid overflows when we have very large (but not infeasibly large) numbers // of units in a very small area. // (Note we use vertexes, not tiles, to better match the renderer.) // Lazily constructed when it's needed, to save memory in smaller games. std::array, MAX_LOS_PLAYER_ID> m_LosPlayerCounts; // 2-bit LosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) Grid m_LosState; // Special static visibility data for the "reveal whole map" mode // (TODO: this is usually a waste of memory) Grid m_LosStateRevealed; // Shared LOS masks, one per player. std::array m_SharedLosMasks; // Shared dirty visibility masks, one per player. std::array m_SharedDirtyVisibilityMasks; // Cache explored vertices per player (not serialized) u32 m_TotalInworldVertices; std::vector m_ExploredVertices; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_QueryNext = 1; m_DebugOverlayEnabled = false; m_DebugOverlayDirty = true; m_Deserializing = false; m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero(); // Initialise with bogus values (these will get replaced when // SetBounds is called) ResetSubdivisions(entity_pos_t::FromInt(1024), entity_pos_t::FromInt(1024)); m_SubdivisionResults.reserve(4096); // The whole map should be visible to Gaia by default, else e.g. animals // will get confused when trying to run from enemies m_LosRevealAll[0] = true; m_GlobalVisibilityUpdate = true; m_LosCircular = false; m_TerrainVerticesPerSide = 0; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("world x0", m_WorldX0); serialize.NumberFixed_Unbounded("world z0", m_WorldZ0); serialize.NumberFixed_Unbounded("world x1", m_WorldX1); serialize.NumberFixed_Unbounded("world z1", m_WorldZ1); serialize.NumberU32_Unbounded("query next", m_QueryNext); Serializer(serialize, "queries", m_Queries, GetSimContext()); Serializer(serialize, "entity data", m_EntityData); Serializer(serialize, "los reveal all", m_LosRevealAll); serialize.Bool("los circular", m_LosCircular); serialize.NumberI32_Unbounded("terrain verts per side", m_TerrainVerticesPerSide); serialize.Bool("global visibility update", m_GlobalVisibilityUpdate); Serializer(serialize, "global player visibility update", m_GlobalPlayerVisibilityUpdate); Serializer(serialize, "dirty visibility", m_DirtyVisibility); Serializer(serialize, "modified entities", m_ModifiedEntities); // We don't serialize m_Subdivision, m_LosPlayerCounts or m_LosTiles // since they can be recomputed from the entity data when deserializing; // m_LosState must be serialized since it depends on the history of exploration Serializer(serialize, "los state", m_LosState); Serializer(serialize, "shared los masks", m_SharedLosMasks); Serializer(serialize, "shared dirty visibility masks", m_SharedDirtyVisibilityMasks); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Deserialized: { // Reinitialize subdivisions and LOS data after all // other components have been deserialized. m_Deserializing = true; ResetDerivedData(); m_Deserializing = false; break; } case MT_Create: { const CMessageCreate& msgData = static_cast (msg); entity_id_t ent = msgData.entity; // Ignore local entities - we shouldn't let them influence anything if (ENTITY_IS_LOCAL(ent)) break; // Ignore non-positional entities CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition) break; // The newly-created entity will have owner -1 and position out-of-world // (any initialisation of those values will happen later), so we can just // use the default-constructed EntityData here EntityData entdata; // Store the LOS data, if any CmpPtr cmpVision(GetSimContext(), ent); if (cmpVision) { entdata.visionRange = cmpVision->GetRange(); entdata.SetFlag(cmpVision->GetRevealShore()); } CmpPtr cmpVisibility(GetSimContext(), ent); if (cmpVisibility) entdata.SetFlag(cmpVisibility->GetRetainInFog()); // Store the size CmpPtr cmpObstruction(GetSimContext(), ent); if (cmpObstruction) entdata.size = cmpObstruction->GetSize().ToInt_RoundToInfinity(); // Remember this entity m_EntityData.insert(ent, entdata); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (msgData.inWorld) { if (it->second.HasFlag()) { CFixedVector2D from(it->second.x, it->second.z); CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Move(ent, from, to, it->second.size); if (it->second.HasFlag()) SharingLosMove(it->second.visionSharing, it->second.visionRange, from, to); else LosMove(it->second.owner, it->second.visionRange, from, to); LosTile oldLosTile = PosToLosTilesHelper(it->second.x, it->second.z); LosTile newLosTile = PosToLosTilesHelper(msgData.x, msgData.z); if (oldLosTile != newLosTile) { RemoveFromTile(oldLosTile, ent); AddToTile(newLosTile, ent); } } else { CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Add(ent, to, it->second.size); if (it->second.HasFlag()) SharingLosAdd(it->second.visionSharing, it->second.visionRange, to); else LosAdd(it->second.owner, it->second.visionRange, to); AddToTile(PosToLosTilesHelper(msgData.x, msgData.z), ent); } it->second.SetFlag(true); it->second.x = msgData.x; it->second.z = msgData.z; } else { if (it->second.HasFlag()) { CFixedVector2D from(it->second.x, it->second.z); m_Subdivision.Remove(ent, from, it->second.size); if (it->second.HasFlag()) SharingLosRemove(it->second.visionSharing, it->second.visionRange, from); else LosRemove(it->second.owner, it->second.visionRange, from); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } it->second.SetFlag(false); it->second.x = entity_pos_t::Zero(); it->second.z = entity_pos_t::Zero(); } RequestVisibilityUpdate(ent); break; } case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.HasFlag()) { // Entity vision is taken into account in VisionSharingChanged // when sharing component activated if (!it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); LosRemove(it->second.owner, it->second.visionRange, pos); LosAdd(msgData.to, it->second.visionRange, pos); } if (it->second.HasFlag()) { RevealShore(it->second.owner, false); RevealShore(msgData.to, true); } } ENSURE(-128 <= msgData.to && msgData.to <= 127); it->second.owner = (i8)msgData.to; break; } case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.HasFlag()) { m_Subdivision.Remove(ent, CFixedVector2D(it->second.x, it->second.z), it->second.size); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } // This will be called after Ownership's OnDestroy, so ownership will be set // to -1 already and we don't have to do a LosRemove here ENSURE(it->second.owner == -1); m_EntityData.erase(it); break; } case MT_VisionRangeChanged: { const CMessageVisionRangeChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; CmpPtr cmpVision(GetSimContext(), ent); if (!cmpVision) break; entity_pos_t oldRange = it->second.visionRange; entity_pos_t newRange = msgData.newRange; // If the range changed and the entity's in-world, we need to manually adjust it // but if it's not in-world, we only need to set the new vision range it->second.visionRange = newRange; if (it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); if (it->second.HasFlag()) { SharingLosRemove(it->second.visionSharing, oldRange, pos); SharingLosAdd(it->second.visionSharing, newRange, pos); } else { LosRemove(it->second.owner, oldRange, pos); LosAdd(it->second.owner, newRange, pos); } } break; } case MT_VisionSharingChanged: { const CMessageVisionSharingChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; ENSURE(msgData.player > 0 && msgData.player < MAX_LOS_PLAYER_ID+1); u16 visionChanged = CalcVisionSharingMask(msgData.player); if (!it->second.HasFlag()) { // Activation of the Vision Sharing ENSURE(it->second.owner == (i8)msgData.player); it->second.visionSharing = visionChanged; it->second.SetFlag(true); break; } if (it->second.HasFlag()) { entity_pos_t range = it->second.visionRange; CFixedVector2D pos(it->second.x, it->second.z); if (msgData.add) LosAdd(msgData.player, range, pos); else LosRemove(msgData.player, range, pos); } if (msgData.add) it->second.visionSharing |= visionChanged; else it->second.visionSharing &= ~visionChanged; break; } case MT_Update: { m_DebugOverlayDirty = true; ExecuteActiveQueries(); UpdateVisibilityData(); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) { m_WorldX0 = x0; m_WorldZ0 = z0; m_WorldX1 = x1; m_WorldZ1 = z1; m_TerrainVerticesPerSide = (i32)vertices; ResetDerivedData(); } virtual void Verify() { // Ignore if map not initialised yet if (m_WorldX1.IsZero()) return; // Check that calling ResetDerivedData (i.e. recomputing all the state from scratch) // does not affect the incrementally-computed state std::array, MAX_LOS_PLAYER_ID> oldPlayerCounts = m_LosPlayerCounts; Grid oldStateRevealed = m_LosStateRevealed; FastSpatialSubdivision oldSubdivision = m_Subdivision; Grid > oldLosTiles = m_LosTiles; m_Deserializing = true; ResetDerivedData(); m_Deserializing = false; if (oldPlayerCounts != m_LosPlayerCounts) { for (size_t id = 0; id < m_LosPlayerCounts.size(); ++id) { debug_printf("player %zu\n", id); for (size_t i = 0; i < oldPlayerCounts[id].width(); ++i) { for (size_t j = 0; j < oldPlayerCounts[id].height(); ++j) debug_printf("%i ", oldPlayerCounts[id].get(i,j)); debug_printf("\n"); } } for (size_t id = 0; id < m_LosPlayerCounts.size(); ++id) { debug_printf("player %zu\n", id); for (size_t i = 0; i < m_LosPlayerCounts[id].width(); ++i) { for (size_t j = 0; j < m_LosPlayerCounts[id].height(); ++j) debug_printf("%i ", m_LosPlayerCounts[id].get(i,j)); debug_printf("\n"); } } debug_warn(L"inconsistent player counts"); } if (oldStateRevealed != m_LosStateRevealed) debug_warn(L"inconsistent revealed"); if (oldSubdivision != m_Subdivision) debug_warn(L"inconsistent subdivs"); if (oldLosTiles != m_LosTiles) debug_warn(L"inconsistent los tiles"); } FastSpatialSubdivision* GetSubdivision() { return &m_Subdivision; } // Reinitialise subdivisions and LOS data, based on entity data void ResetDerivedData() { ENSURE(m_WorldX0.IsZero() && m_WorldZ0.IsZero()); // don't bother implementing non-zero offsets yet ResetSubdivisions(m_WorldX1, m_WorldZ1); m_LosTilesPerSide = (m_TerrainVerticesPerSide - 1)/LOS_TILES_RATIO; for (size_t player_id = 0; player_id < m_LosPlayerCounts.size(); ++player_id) m_LosPlayerCounts[player_id].clear(); m_ExploredVertices.clear(); m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0); if (m_Deserializing) { // recalc current exploration stats. for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) if (!LosIsOffWorld(i, j)) for (u8 k = 1; k < MAX_LOS_PLAYER_ID+1; ++k) m_ExploredVertices.at(k) += ((m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(k-1)))) > 0); } else m_LosState.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); m_LosStateRevealed.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); if (!m_Deserializing) { m_DirtyVisibility.resize(m_LosTilesPerSide, m_LosTilesPerSide); } ENSURE(m_DirtyVisibility.width() == m_LosTilesPerSide); ENSURE(m_DirtyVisibility.height() == m_LosTilesPerSide); m_LosTiles.resize(m_LosTilesPerSide, m_LosTilesPerSide); for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) if (it->second.HasFlag()) { if (it->second.HasFlag()) SharingLosAdd(it->second.visionSharing, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); else LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); AddToTile(PosToLosTilesHelper(it->second.x, it->second.z), it->first); if (it->second.HasFlag()) RevealShore(it->second.owner, true); } m_TotalInworldVertices = 0; for (ssize_t j = 0; j < m_TerrainVerticesPerSide; ++j) for (ssize_t i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) m_LosStateRevealed.get(i, j) = 0; else { m_LosStateRevealed.get(i, j) = 0xFFFFFFFFu; m_TotalInworldVertices++; } } } void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1) { m_Subdivision.Reset(x1, z1); for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) if (it->second.HasFlag()) m_Subdivision.Add(it->first, CFixedVector2D(it->second.x, it->second.z), it->second.size); } virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags); return id; } virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags); return id; } virtual void DestroyActiveQuery(tag_t tag) { if (m_Queries.find(tag) == m_Queries.end()) { LOGERROR("CCmpRangeManager: DestroyActiveQuery called with invalid tag %u", tag); return; } m_Queries.erase(tag); } virtual void EnableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: EnableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = true; } virtual void DisableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: DisableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = false; } virtual bool IsActiveQueryEnabled(tag_t tag) const { std::map::const_iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: IsActiveQueryEnabled called with invalid tag %u", tag); return false; } const Query& q = it->second; return q.enabled; } virtual std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) { Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; PerformQuery(q, r, pos); // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) { PROFILE("ExecuteQuery"); Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list return r; } CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); PerformQuery(q, r, pos); // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ResetActiveQuery(tag_t tag) { PROFILE("ResetActiveQuery"); std::vector r; std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: ResetActiveQuery called with invalid tag %u", tag); return r; } Query& q = it->second; q.enabled = true; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list q.lastMatch = r; return r; } CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); PerformQuery(q, r, pos); q.lastMatch = r; // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector GetEntitiesByPlayer(player_id_t player) const { return GetEntitiesByMask(CalcOwnerMask(player)); } virtual std::vector GetNonGaiaEntities() const { return GetEntitiesByMask(~3u); // bit 0 for owner=-1 and bit 1 for gaia } virtual std::vector GetGaiaAndNonGaiaEntities() const { return GetEntitiesByMask(~1u); // bit 0 for owner=-1 } std::vector GetEntitiesByMask(u32 ownerMask) const { std::vector entities; for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { // Check owner and add to list if it matches if (CalcOwnerMask(it->second.owner) & ownerMask) entities.push_back(it->first); } return entities; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; m_DebugOverlayDirty = true; if (!enabled) m_DebugOverlayLines.clear(); } /** * Update all currently-enabled active queries. */ void ExecuteActiveQueries() { PROFILE3("ExecuteActiveQueries"); // Store a queue of all messages before sending any, so we can assume // no entities will move until we've finished checking all the ranges std::vector > messages; std::vector results; std::vector added; std::vector removed; for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& query = it->second; if (!query.enabled) continue; results.clear(); CmpPtr cmpSourcePosition(query.source); if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) { results.reserve(query.lastMatch.size()); PerformQuery(query, results, cmpSourcePosition->GetPosition2D()); } // Compute the changes vs the last match added.clear(); removed.clear(); // Return the 'added' list sorted by distance from the entity // (Don't bother sorting 'removed' because they might not even have positions or exist any more) std::set_difference(results.begin(), results.end(), query.lastMatch.begin(), query.lastMatch.end(), std::back_inserter(added)); std::set_difference(query.lastMatch.begin(), query.lastMatch.end(), results.begin(), results.end(), std::back_inserter(removed)); if (added.empty() && removed.empty()) continue; if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D())); messages.resize(messages.size() + 1); std::pair& back = messages.back(); back.first = query.source.GetId(); back.second.tag = it->first; back.second.added.swap(added); back.second.removed.swap(removed); query.lastMatch.swap(results); } CComponentManager& cmpMgr = GetSimContext().GetComponentManager(); for (size_t i = 0; i < messages.size(); ++i) cmpMgr.PostMessage(messages[i].first, messages[i].second); } /** * Returns whether the given entity matches the given query (ignoring maxRange) */ bool TestEntityQuery(const Query& q, entity_id_t id, const EntityData& entity) const { // Quick filter to ignore entities with the wrong owner if (!(CalcOwnerMask(entity.owner) & q.ownersMask)) return false; // Ignore entities not present in the world if (!entity.HasFlag()) return false; // Ignore entities that don't match the current flags if (!((entity.flags & FlagMasks::AllQuery) & q.flagsMask)) return false; // Ignore self if (id == q.source.GetId()) return false; // Ignore if it's missing the required interface if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface)) return false; return true; } /** * Returns a list of distinct entity IDs that match the given query, sorted by ID. */ void PerformQuery(const Query& q, std::vector& r, CFixedVector2D pos) { // Special case: range -1.0 means check all entities ignoring distance if (q.maxRange == entity_pos_t::FromInt(-1)) { for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (!TestEntityQuery(q, it->first, it->second)) continue; r.push_back(it->first); } } // Not the entire world, so check a parabolic range, or a regular range else if (q.parabolic) { // elevationBonus is part of the 3D position, as the source is really that much heigher CmpPtr cmpSourcePosition(q.source); CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange * 2); for (size_t i = 0; i < m_SubdivisionResults.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(m_SubdivisionResults[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; CmpPtr cmpSecondPosition(GetSimContext(), m_SubdivisionResults[i]); if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld()) continue; CFixedVector3D secondPosition = cmpSecondPosition->GetPosition(); // Doing an exact check for parabolas with obstruction sizes is not really possible. // However, we can prove that InParabolicRange(d, range + size) > InParabolicRange(d, range) // in the sense that it always returns true when the latter would, which is enough. // To do so, compute the derivative with respect to distance, and notice that // they have an intersection after which the former grows slower, and then use that to prove the above. // Note that this is only true because we do not account for vertical size here, // if we did, we would also need to artificially 'raise' the source over the target. if (!InParabolicRange(CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - pos3d, q.maxRange + fixed::FromInt(it->second.size))) continue; if (!q.minRange.IsZero()) if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0) continue; r.push_back(it->first); } std::sort(r.begin(), r.end()); } // check a regular range (i.e. not the entire world, and not parabolic) else { // Get a quick list of entities that are potentially in range m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange); for (size_t i = 0; i < m_SubdivisionResults.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(m_SubdivisionResults[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; // Restrict based on approximate circle-circle distance. if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange + fixed::FromInt(it->second.size)) > 0) continue; if (!q.minRange.IsZero()) if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0) continue; r.push_back(it->first); } std::sort(r.begin(), r.end()); } } virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos1, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const { entity_pos_t r = entity_pos_t::Zero(); CFixedVector3D pos(pos1); pos.Y += elevationBonus; entity_pos_t orientation = rot.Y; entity_pos_t maxAngle = orientation + angle/2; entity_pos_t minAngle = orientation - angle/2; int numberOfSteps = 16; if (angle == entity_pos_t::Zero()) numberOfSteps = 1; std::vector coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps); entity_pos_t part = entity_pos_t::FromInt(numberOfSteps); for (int i = 0; i < numberOfSteps; ++i) r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part; return r; } virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) const { std::vector r; CmpPtr cmpTerrain(GetSystemEntity()); if (!cmpTerrain) return r; // angle = 0 goes in the positive Z direction u64 precisionSquared = SQUARE_U64_FIXED(entity_pos_t::FromInt(static_cast(TERRAIN_TILE_SIZE)) / 8); CmpPtr cmpWaterManager(GetSystemEntity()); entity_pos_t waterLevel = cmpWaterManager ? cmpWaterManager->GetWaterLevel(pos.X, pos.Z) : entity_pos_t::Zero(); entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel; for (int i = 0; i < numberOfSteps; ++i) { entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i; entity_pos_t sin; entity_pos_t cos; entity_pos_t minDistance = entity_pos_t::Zero(); entity_pos_t maxDistance = cutoff; sincos_approx(angle, sin, cos); CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(), entity_pos_t::Zero()); CFixedVector2D maxVector = CFixedVector2D(sin, cos).Multiply(cutoff); entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X, pos.Z+maxVector.Y); // use water level to display range on water targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; if (InParabolicRange(CFixedVector3D(maxVector.X, targetHeight-thisHeight, maxVector.Y), maxRange)) { r.push_back(maxVector.X); r.push_back(maxVector.Y); continue; } // Loop until vectors come close enough while ((maxVector - minVector).CompareLengthSquared(precisionSquared) > 0) { // difference still bigger than precision, bisect to get smaller difference entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2); CFixedVector2D newVector = CFixedVector2D(sin, cos).Multiply(newDistance); // get the height of the ground targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X, pos.Z+newVector.Y); targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; if (InParabolicRange(CFixedVector3D(newVector.X, targetHeight-thisHeight, newVector.Y), maxRange)) { // new vector is in parabolic range, so this is a new minVector minVector = newVector; minDistance = newDistance; } else { // new vector is out parabolic range, so this is a new maxVector maxVector = newVector; maxDistance = newDistance; } } r.push_back(maxVector.X); r.push_back(maxVector.Y); } r.push_back(r[0]); r.push_back(r[1]); return r; } Query ConstructQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flagsMask) const { // Min range must be non-negative if (minRange < entity_pos_t::Zero()) LOGWARNING("CCmpRangeManager: Invalid min range %f in query for entity %u", minRange.ToDouble(), source); // Max range must be non-negative, or else -1 if (maxRange < entity_pos_t::Zero() && maxRange != entity_pos_t::FromInt(-1)) LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source); Query q; q.enabled = false; q.parabolic = false; q.source = GetSimContext().GetComponentManager().LookupEntityHandle(source); q.minRange = minRange; q.maxRange = maxRange; q.elevationBonus = entity_pos_t::Zero(); if (q.source.GetId() != INVALID_ENTITY && q.maxRange != entity_pos_t::FromInt(-1)) { u32 size = 0; if (ENTITY_IS_LOCAL(q.source.GetId())) { CmpPtr cmpObstruction(GetSimContext(), q.source.GetId()); if (cmpObstruction) size = cmpObstruction->GetSize().ToInt_RoundToInfinity(); } else { EntityMap::const_iterator it = m_EntityData.find(q.source.GetId()); if (it != m_EntityData.end()) size = it->second.size; } // Adjust the range query based on the querier's obstruction radius. // The smallest side of the obstruction isn't known here, so we can't safely adjust the min-range, only the max. // 'size' is the diagonal size rounded up so this will cover all possible rotations of the querier. q.maxRange += fixed::FromInt(size); } q.ownersMask = 0; for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); if (q.ownersMask == 0) LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source); q.interface = requiredInterface; q.flagsMask = flagsMask; return q; } Query ConstructParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flagsMask) const { Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask); q.parabolic = true; q.elevationBonus = elevationBonus; return q; } void RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; static CColor disabledRingColor(1, 0, 0, 1); // red static CColor enabledRingColor(0, 1, 0, 1); // green static CColor subdivColor(0, 0, 1, 1); // blue static CColor rayColor(1, 1, 0, 0.2f); if (m_DebugOverlayDirty) { m_DebugOverlayLines.clear(); for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) continue; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Draw the max range circle if (!q.parabolic) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColor : disabledRingColor); SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); } else { // elevation bonus is part of the 3D position. As if the unit is really that much higher CFixedVector3D pos3D = cmpSourcePosition->GetPosition(); pos3D.Y += q.elevationBonus; std::vector coords; // Get the outline from cache if possible if (ParabolicRangesOutlines.find(q.source.GetId()) != ParabolicRangesOutlines.end()) { EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source.GetId()]; if (e.position == pos3D && e.range == q.maxRange) { // outline is cached correctly, use it coords = e.outline; } else { // outline was cached, but important parameters changed // (position, elevation, range) // update it coords = getParabolicRangeForm(pos3D,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); e.outline = coords; e.range = q.maxRange; e.position = pos3D; ParabolicRangesOutlines[q.source.GetId()] = e; } } else { // outline wasn't cached (first time you enable the range overlay // or you created a new entiy) // cache a new outline coords = getParabolicRangeForm(pos3D,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); EntityParabolicRangeOutline e; e.source = q.source.GetId(); e.range = q.maxRange; e.position = pos3D; e.outline = coords; ParabolicRangesOutlines[q.source.GetId()] = e; } CColor thiscolor = q.enabled ? enabledRingColor : disabledRingColor; // draw the outline (piece by piece) for (size_t i = 3; i < coords.size(); i += 2) { std::vector c; c.push_back((coords[i - 3] + pos3D.X).ToFloat()); c.push_back((coords[i - 2] + pos3D.Z).ToFloat()); c.push_back((coords[i - 1] + pos3D.X).ToFloat()); c.push_back((coords[i] + pos3D.Z).ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = thiscolor; SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true); } } // Draw the min range circle if (!q.minRange.IsZero()) SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true); // Draw a ray from the source to each matched entity for (size_t i = 0; i < q.lastMatch.size(); ++i) { CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]); if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) continue; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); std::vector coords; coords.push_back(pos.X.ToFloat()); coords.push_back(pos.Y.ToFloat()); coords.push_back(targetPos.X.ToFloat()); coords.push_back(targetPos.Y.ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = rayColor; SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true); } } // render subdivision grid float divSize = m_Subdivision.GetDivisionSize(); int size = m_Subdivision.GetWidth(); for (int x = 0; x < size; ++x) { for (int y = 0; y < size; ++y) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = subdivColor; float xpos = x*divSize + divSize/2; float zpos = y*divSize + divSize/2; SimRender::ConstructSquareOnGround(GetSimContext(), xpos, zpos, divSize, divSize, 0.0f, m_DebugOverlayLines.back(), false, 1.0f); } } m_DebugOverlayDirty = false; } for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); } virtual u8 GetEntityFlagMask(const std::string& identifier) const { if (identifier == "normal") return FlagMasks::Normal; if (identifier == "injured") return FlagMasks::Injured; LOGWARNING("CCmpRangeManager: Invalid flag identifier %s", identifier.c_str()); return FlagMasks::None; } virtual void SetEntityFlag(entity_id_t ent, const std::string& identifier, bool value) { EntityMap::iterator it = m_EntityData.find(ent); // We don't have this entity if (it == m_EntityData.end()) return; u8 flag = GetEntityFlagMask(identifier); if (flag == FlagMasks::None) LOGWARNING("CCmpRangeManager: Invalid flag identifier %s for entity %u", identifier.c_str(), ent); else it->second.SetFlag(flag, value); } // **************************************************************** // LOS implementation: virtual CLosQuerier GetLosQuerier(player_id_t player) const { if (GetLosRevealAll(player)) return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_TerrainVerticesPerSide); else return CLosQuerier(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); } virtual void ActivateScriptedVisibility(entity_id_t ent, bool status) { EntityMap::iterator it = m_EntityData.find(ent); if (it != m_EntityData.end()) it->second.SetFlag(status); } LosVisibility ComputeLosVisibility(CEntityHandle ent, player_id_t player) const { // Entities not with positions in the world are never visible if (ent.GetId() == INVALID_ENTITY) return LosVisibility::HIDDEN; CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return LosVisibility::HIDDEN; // Mirage entities, whatever the situation, are visible for one specific player CmpPtr cmpMirage(ent); if (cmpMirage && cmpMirage->GetPlayer() != player) return LosVisibility::HIDDEN; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j) || cmpMirage) return LosVisibility::HIDDEN; return LosVisibility::VISIBLE; } // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); CmpPtr cmpVisibility(ent); // Possibly ask the scripted Visibility component EntityMap::const_iterator it = m_EntityData.find(ent.GetId()); if (it != m_EntityData.end()) { if (it->second.HasFlag() && cmpVisibility) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } else { if (cmpVisibility && cmpVisibility->IsActivated()) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } // Else, default behavior if (los.IsVisible(i, j)) { if (cmpMirage) return LosVisibility::HIDDEN; return LosVisibility::VISIBLE; } if (!los.IsExplored(i, j)) return LosVisibility::HIDDEN; // Invisible if the 'retain in fog' flag is not set, and in a non-visible explored region // Try using the 'retainInFog' flag in m_EntityData to save a script call if (it != m_EntityData.end()) { if (!it->second.HasFlag()) return LosVisibility::HIDDEN; } else { if (!(cmpVisibility && cmpVisibility->GetRetainInFog())) return LosVisibility::HIDDEN; } if (cmpMirage) return LosVisibility::FOGGED; CmpPtr cmpOwnership(ent); if (!cmpOwnership) return LosVisibility::FOGGED; if (cmpOwnership->GetOwner() == player) { CmpPtr cmpFogging(ent); if (!(cmpFogging && cmpFogging->IsMiraged(player))) return LosVisibility::FOGGED; return LosVisibility::HIDDEN; } // Fogged entities are hidden in two cases: // - They were not scouted // - A mirage replaces them CmpPtr cmpFogging(ent); if (cmpFogging && cmpFogging->IsActivated() && (!cmpFogging->WasSeen(player) || cmpFogging->IsMiraged(player))) return LosVisibility::HIDDEN; return LosVisibility::FOGGED; } LosVisibility ComputeLosVisibility(entity_id_t ent, player_id_t player) const { CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); return ComputeLosVisibility(handle, player); } virtual LosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const { entity_id_t entId = ent.GetId(); // Entities not with positions in the world are never visible if (entId == INVALID_ENTITY) return LosVisibility::HIDDEN; CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return LosVisibility::HIDDEN; // Gaia and observers do not have a visibility cache if (player <= 0) return ComputeLosVisibility(ent, player); CFixedVector2D pos = cmpPosition->GetPosition2D(); if (IsVisibilityDirty(m_DirtyVisibility[PosToLosTilesHelper(pos.X, pos.Y)], player)) return ComputeLosVisibility(ent, player); if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end()) return ComputeLosVisibility(ent, player); EntityMap::const_iterator it = m_EntityData.find(entId); if (it == m_EntityData.end()) return ComputeLosVisibility(ent, player); return static_cast(GetPlayerVisibility(it->second.visibilities, player)); } virtual LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const { CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); return GetLosVisibility(handle, player); } virtual LosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const { int i = (x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j)) return LosVisibility::HIDDEN; else return LosVisibility::VISIBLE; } // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); if (los.IsVisible(i,j)) return LosVisibility::VISIBLE; if (los.IsExplored(i,j)) return LosVisibility::FOGGED; return LosVisibility::HIDDEN; } LosTile PosToLosTilesHelper(u16 x, u16 z) const { return LosTile{ Clamp(x/LOS_TILES_RATIO, 0, m_LosTilesPerSide - 1), Clamp(z/LOS_TILES_RATIO, 0, m_LosTilesPerSide - 1) }; } LosTile PosToLosTilesHelper(entity_pos_t x, entity_pos_t z) const { i32 i = Clamp( (x/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(), 0, m_LosTilesPerSide - 1); i32 j = Clamp( (z/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(), 0, m_LosTilesPerSide - 1); return std::make_pair(i, j); } void AddToTile(LosTile tile, entity_id_t ent) { m_LosTiles[tile].insert(ent); } void RemoveFromTile(LosTile tile, entity_id_t ent) { std::set::const_iterator tileIt = m_LosTiles[tile].find(ent); if (tileIt != m_LosTiles[tile].end()) m_LosTiles[tile].erase(tileIt); } void UpdateVisibilityData() { PROFILE("UpdateVisibilityData"); for (u16 i = 0; i < m_LosTilesPerSide; ++i) for (u16 j = 0; j < m_LosTilesPerSide; ++j) { LosTile pos{i, j}; for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) if (IsVisibilityDirty(m_DirtyVisibility[pos], player) || m_GlobalPlayerVisibilityUpdate[player-1] == 1 || m_GlobalVisibilityUpdate) for (const entity_id_t& ent : m_LosTiles[pos]) UpdateVisibility(ent, player); m_DirtyVisibility[pos] = 0; } std::fill(m_GlobalPlayerVisibilityUpdate.begin(), m_GlobalPlayerVisibilityUpdate.end(), false); m_GlobalVisibilityUpdate = false; // Calling UpdateVisibility can modify m_ModifiedEntities, so be careful: // infinite loops could be triggered by feedback between entities and their mirages. std::map attempts; while (!m_ModifiedEntities.empty()) { entity_id_t ent = m_ModifiedEntities.back(); m_ModifiedEntities.pop_back(); ++attempts[ent]; ENSURE(attempts[ent] < 100 && "Infinite loop in UpdateVisibilityData"); UpdateVisibility(ent); } } virtual void RequestVisibilityUpdate(entity_id_t ent) { if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), ent) == m_ModifiedEntities.end()) m_ModifiedEntities.push_back(ent); } void UpdateVisibility(entity_id_t ent, player_id_t player) { EntityMap::iterator itEnts = m_EntityData.find(ent); if (itEnts == m_EntityData.end()) return; LosVisibility oldVis = GetPlayerVisibility(itEnts->second.visibilities, player); LosVisibility newVis = ComputeLosVisibility(itEnts->first, player); if (oldVis == newVis) return; itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2 * (player - 1))) | ((u8)newVis << 2 * (player - 1)); CMessageVisibilityChanged msg(player, ent, static_cast(oldVis), static_cast(newVis)); GetSimContext().GetComponentManager().PostMessage(ent, msg); } void UpdateVisibility(entity_id_t ent) { for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) UpdateVisibility(ent, player); } virtual void SetLosRevealAll(player_id_t player, bool enabled) { if (player == -1) m_LosRevealAll[MAX_LOS_PLAYER_ID+1] = enabled; else { ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID); m_LosRevealAll[player] = enabled; } // On next update, update the visibility of every entity in the world m_GlobalVisibilityUpdate = true; } virtual bool GetLosRevealAll(player_id_t player) const { // Special player value can force reveal-all for every player if (m_LosRevealAll[MAX_LOS_PLAYER_ID+1] || player == -1) return true; ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID+1); // Otherwise check the player-specific flag if (m_LosRevealAll[player]) return true; return false; } virtual void SetLosCircular(bool enabled) { m_LosCircular = enabled; ResetDerivedData(); } virtual bool GetLosCircular() const { return m_LosCircular; } virtual void SetSharedLos(player_id_t player, const std::vector& players) { m_SharedLosMasks[player] = CalcSharedLosMask(players); // Units belonging to any of 'players' can now trigger visibility updates for 'player'. // If shared LOS partners have been removed, we disable visibility updates from them // in order to improve performance. That also allows us to properly determine whether // 'player' needs a global visibility update for this turn. bool modified = false; for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) { bool inList = std::find(players.begin(), players.end(), p) != players.end(); if (SetPlayerSharedDirtyVisibilityBit(m_SharedDirtyVisibilityMasks[p], player, inList)) modified = true; } if (modified && (size_t)player <= m_GlobalPlayerVisibilityUpdate.size()) m_GlobalPlayerVisibilityUpdate[player-1] = 1; } virtual u32 GetSharedLosMask(player_id_t player) const { return m_SharedLosMasks[player]; } void ExploreAllTiles(player_id_t p) { for (u16 j = 0; j < m_TerrainVerticesPerSide; ++j) for (u16 i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) continue; u32 &explored = m_ExploredVertices.at(p); explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(p-1)))); m_LosState.get(i, j) |= ((u32)LosState::EXPLORED << (2*(p-1))); } SeeExploredEntities(p); } virtual void ExploreTerritories() { PROFILE3("ExploreTerritories"); CmpPtr cmpTerritoryManager(GetSystemEntity()); const Grid& grid = cmpTerritoryManager->GetTerritoryGrid(); // Territory data is stored per territory-tile (typically a multiple of terrain-tiles). // LOS data is stored per terrain-tile vertex. // For each territory-tile, if it is owned by a valid player then update the LOS // for every vertex inside/around that tile, to mark them as explored. // Currently this code doesn't support territory-tiles smaller than terrain-tiles // (it will get scale==0 and break), or a non-integer multiple, so check that first cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE >= Pathfinding::NAVCELLS_PER_TILE); cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE % Pathfinding::NAVCELLS_PER_TILE == 0); int scale = ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / Pathfinding::NAVCELLS_PER_TILE; ENSURE(grid.m_W*scale == m_TerrainVerticesPerSide-1 && grid.m_H*scale == m_TerrainVerticesPerSide-1); for (u16 j = 0; j < grid.m_H; ++j) for (u16 i = 0; i < grid.m_W; ++i) { u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK; if (p > 0 && p <= MAX_LOS_PLAYER_ID) { u32& explored = m_ExploredVertices.at(p); for (int tj = j * scale; tj <= (j+1) * scale; ++tj) for (int ti = i * scale; ti <= (i+1) * scale; ++ti) { if (LosIsOffWorld(ti, tj)) continue; u32& losState = m_LosState.get(ti, tj); if (!(losState & ((u32)LosState::EXPLORED << (2*(p-1))))) { ++explored; losState |= ((u32)LosState::EXPLORED << (2*(p-1))); } } } } for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) SeeExploredEntities(p); } /** * Force any entity in explored territory to appear for player p. * This is useful for miraging entities inside the territory borders at the beginning of a game, * or if the "Explore Map" option has been set. */ void SeeExploredEntities(player_id_t p) const { // Warning: Code related to fogging (like ForceMiraging) shouldn't be // invoked while iterating through m_EntityData. // Otherwise, by deleting mirage entities and so on, that code will // change the indexes in the map, leading to segfaults. // So we just remember what entities to mirage and do that later. std::vector miragableEntities; for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { CmpPtr cmpPosition(GetSimContext(), it->first); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); CLosQuerier los(GetSharedLosMask(p), m_LosState, m_TerrainVerticesPerSide); if (!los.IsExplored(i,j) || los.IsVisible(i,j)) continue; CmpPtr cmpFogging(GetSimContext(), it->first); if (cmpFogging) miragableEntities.push_back(it->first); } for (std::vector::iterator it = miragableEntities.begin(); it != miragableEntities.end(); ++it) { CmpPtr cmpFogging(GetSimContext(), *it); ENSURE(cmpFogging && "Impossible to retrieve Fogging component, previously achieved"); cmpFogging->ForceMiraging(p); } } virtual void RevealShore(player_id_t p, bool enable) { if (p <= 0 || p > MAX_LOS_PLAYER_ID) return; // Maximum distance to the shore const u16 maxdist = 10; CmpPtr cmpPathfinder(GetSystemEntity()); const Grid& shoreGrid = cmpPathfinder->ComputeShoreGrid(true); ENSURE(shoreGrid.m_W == m_TerrainVerticesPerSide-1 && shoreGrid.m_H == m_TerrainVerticesPerSide-1); Grid& counts = m_LosPlayerCounts.at(p); ENSURE(!counts.blank()); for (u16 j = 0; j < shoreGrid.m_H; ++j) for (u16 i = 0; i < shoreGrid.m_W; ++i) { u16 shoredist = shoreGrid.get(i, j); if (shoredist > maxdist) continue; // Maybe we could be more clever and don't add dummy strips of one tile if (enable) LosAddStripHelper(p, i, i, j, counts); else LosRemoveStripHelper(p, i, i, j, counts); } } /** * Returns whether the given vertex is outside the normal bounds of the world * (i.e. outside the range of a circular map) */ inline bool LosIsOffWorld(ssize_t i, ssize_t j) const { if (m_LosCircular) { // With a circular map, vertex is off-world if hypot(i - size/2, j - size/2) >= size/2: ssize_t dist2 = (i - m_TerrainVerticesPerSide/2)*(i - m_TerrainVerticesPerSide/2) + (j - m_TerrainVerticesPerSide/2)*(j - m_TerrainVerticesPerSide/2); ssize_t r = m_TerrainVerticesPerSide / 2 - MAP_EDGE_TILES + 1; // subtract a bit from the radius to ensure nice // SoD blurring around the edges of the map return (dist2 >= r*r); } else { // With a square map, the outermost edge of the map should be off-world, // so the SoD texture blends out nicely return i < MAP_EDGE_TILES || j < MAP_EDGE_TILES || i >= m_TerrainVerticesPerSide - MAP_EDGE_TILES || j >= m_TerrainVerticesPerSide - MAP_EDGE_TILES; } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; u32 &explored = m_ExploredVertices.at(owner); for (i32 i = i0; i <= i1; ++i) { // Increasing from zero to non-zero - move from unexplored/explored to visible+explored if (counts.get(i, j) == 0) { if (!LosIsOffWorld(i, j)) { explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(owner-1)))); m_LosState.get(i, j) |= (((int)LosState::VISIBLE | (u32)LosState::EXPLORED) << (2*(owner-1))); } MarkVisibilityDirtyAroundTile(owner, i, j); } ENSURE(counts.get(i, j) < std::numeric_limits::max()); counts.get(i, j) = (u16)(counts.get(i, j) + 1); // ignore overflow; the player should never have 64K units } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; for (i32 i = i0; i <= i1; ++i) { ASSERT(counts.get(i, j) > 0); counts.get(i, j) = (u16)(counts.get(i, j) - 1); // Decreasing from non-zero to zero - move from visible+explored to explored if (counts.get(i, j) == 0) { // (If LosIsOffWorld then this is a no-op, so don't bother doing the check) m_LosState.get(i, j) &= ~((int)LosState::VISIBLE << (2*(owner-1))); MarkVisibilityDirtyAroundTile(owner, i, j); } } } inline void MarkVisibilityDirtyAroundTile(u8 owner, i32 i, i32 j) { // If we're still in the deserializing process, we must not modify m_DirtyVisibility if (m_Deserializing) return; // Mark the LoS tiles around the updated vertex // 1: left-up, 2: right-up, 3: left-down, 4: right-down LosTile n1 = PosToLosTilesHelper(i-1, j-1); LosTile n2 = PosToLosTilesHelper(i-1, j); LosTile n3 = PosToLosTilesHelper(i, j-1); LosTile n4 = PosToLosTilesHelper(i, j); u16 sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner]; if (j > 0 && i > 0) m_DirtyVisibility[n1] |= sharedDirtyVisibilityMask; if (n2 != n1 && j > 0 && i < m_TerrainVerticesPerSide) m_DirtyVisibility[n2] |= sharedDirtyVisibilityMask; if (n3 != n1 && j < m_TerrainVerticesPerSide && i > 0) m_DirtyVisibility[n3] |= sharedDirtyVisibilityMask; if (n4 != n1 && j < m_TerrainVerticesPerSide && i < m_TerrainVerticesPerSide) m_DirtyVisibility[n4] |= sharedDirtyVisibilityMask; } /** * Update the LOS state of tiles within a given circular range, * either adding or removing visibility depending on the template parameter. * Assumes owner is in the valid range. */ template void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelper"); Grid& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.blank()) counts.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); // Compute the circular region as a series of strips. // Rather than quantise pos to vertexes, we do more precise sub-tile computations // to get smoother behaviour as a unit moves rather than jumping a whole tile // at once. // To avoid the cost of sqrt when computing the outline of the circle, // we loop from the bottom to the top and estimate the width of the current // strip based on the previous strip, then adjust each end of the strip // inwards or outwards until it's the widest that still falls within the circle. // Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map // (so that we never render the sharp edge of the map) i32 j0 = ((pos.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1 = ((pos.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(j0, 1); i32 j1clamp = std::min(j1, m_TerrainVerticesPerSide-2); // Translate world coordinates into fractional tile-space coordinates entity_pos_t x = pos.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y = pos.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); // Compute the integers on either side of x i32 xfloor = (x - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil = (x + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); // Initialise the strip (i0, i1) to a rough guess i32 i0 = xfloor; i32 i1 = xceil; for (i32 j = j0clamp; j <= j1clamp; ++j) { // Adjust i0 and i1 to be the outermost values that don't exceed // the circle's radius (i.e. require dy^2 + dx^2 <= r^2). // When moving the points inwards, clamp them to xceil+1 or xfloor-1 // so they don't accidentally shoot off in the wrong direction forever. entity_pos_t dy = entity_pos_t::FromInt(j) - y; entity_pos_t dy2 = dy.Square(); while (dy2 + (entity_pos_t::FromInt(i0-1) - x).Square() <= r2) --i0; while (i0 < xceil && dy2 + (entity_pos_t::FromInt(i0) - x).Square() > r2) ++i0; while (dy2 + (entity_pos_t::FromInt(i1+1) - x).Square() <= r2) ++i1; while (i1 > xfloor && dy2 + (entity_pos_t::FromInt(i1) - x).Square() > r2) --i1; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0 <= i1) { ENSURE(dy2 + (entity_pos_t::FromInt(i0) - x).Square() <= r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1) - x).Square() <= r2); } ENSURE(dy2 + (entity_pos_t::FromInt(i0 - 1) - x).Square() > r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1 + 1) - x).Square() > r2); #endif // Clamp the strip to exclude the 1-tile border, // then add or remove the strip as requested i32 i0clamp = std::max(i0, 1); i32 i1clamp = std::min(i1, m_TerrainVerticesPerSide-2); if (adding) LosAddStripHelper(owner, i0clamp, i1clamp, j, counts); else LosRemoveStripHelper(owner, i0clamp, i1clamp, j, counts); } } /** * Update the LOS state of tiles within a given circular range, * by removing visibility around the 'from' position * and then adding visibility around the 'to' position. */ void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelperIncremental"); Grid& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.blank()) counts.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); // See comments in LosUpdateHelper. // This does exactly the same, except computing the strips for // both circles simultaneously. // (The idea is that the circles will be heavily overlapping, // so we can compute the difference between the removed/added strips // and only have to touch tiles that have a net change.) i32 j0_from = ((from.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_from = ((from.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0_to = ((to.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_to = ((to.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(std::min(j0_from, j0_to), 1); i32 j1clamp = std::min(std::max(j1_from, j1_to), m_TerrainVerticesPerSide-2); entity_pos_t x_from = from.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_from = from.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t x_to = to.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_to = to.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); i32 xfloor_from = (x_from - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_from = (x_from + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 xfloor_to = (x_to - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_to = (x_to + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 i0_from = xfloor_from; i32 i1_from = xceil_from; i32 i0_to = xfloor_to; i32 i1_to = xceil_to; for (i32 j = j0clamp; j <= j1clamp; ++j) { entity_pos_t dy_from = entity_pos_t::FromInt(j) - y_from; entity_pos_t dy2_from = dy_from.Square(); while (dy2_from + (entity_pos_t::FromInt(i0_from-1) - x_from).Square() <= r2) --i0_from; while (i0_from < xceil_from && dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() > r2) ++i0_from; while (dy2_from + (entity_pos_t::FromInt(i1_from+1) - x_from).Square() <= r2) ++i1_from; while (i1_from > xfloor_from && dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() > r2) --i1_from; entity_pos_t dy_to = entity_pos_t::FromInt(j) - y_to; entity_pos_t dy2_to = dy_to.Square(); while (dy2_to + (entity_pos_t::FromInt(i0_to-1) - x_to).Square() <= r2) --i0_to; while (i0_to < xceil_to && dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() > r2) ++i0_to; while (dy2_to + (entity_pos_t::FromInt(i1_to+1) - x_to).Square() <= r2) ++i1_to; while (i1_to > xfloor_to && dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() > r2) --i1_to; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0_from <= i1_from) { ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() <= r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() <= r2); } ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from - 1) - x_from).Square() > r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from + 1) - x_from).Square() > r2); if (i0_to <= i1_to) { ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() <= r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() <= r2); } ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to - 1) - x_to).Square() > r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to + 1) - x_to).Square() > r2); #endif // Check whether this strip moved at all if (!(i0_to == i0_from && i1_to == i1_from)) { i32 i0clamp_from = std::max(i0_from, 1); i32 i1clamp_from = std::min(i1_from, m_TerrainVerticesPerSide-2); i32 i0clamp_to = std::max(i0_to, 1); i32 i1clamp_to = std::min(i1_to, m_TerrainVerticesPerSide-2); // Check whether one strip is negative width, // and we can just add/remove the entire other strip if (i1clamp_from < i0clamp_from) { LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, counts); } else if (i1clamp_to < i0clamp_to) { LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, counts); } else { // There are four possible regions of overlap between the two strips // (remove before add, remove after add, add before remove, add after remove). // Process each of the regions as its own strip. // (If this produces negative-width strips then they'll just get ignored // which is fine.) // (If the strips don't actually overlap (which is very rare with normal unit // movement speeds), the region between them will be both added and removed, // so we have to do the add first to avoid overflowing to -1 and triggering // assertion failures.) LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, counts); LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, counts); LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, counts); LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, counts); } } } } void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosAdd(i, visionRange, pos); } void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosRemove(i, visionRange, pos); } void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; if ((from - to).CompareLength(visionRange) > 0) { // If it's a very large move, then simply remove and add to the new position LosUpdateHelper((u8)owner, visionRange, from); LosUpdateHelper((u8)owner, visionRange, to); } else // Otherwise use the version optimised for mostly-overlapping circles LosUpdateHelperIncremental((u8)owner, visionRange, from, to); } void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosMove(i, visionRange, from, to); } virtual u8 GetPercentMapExplored(player_id_t player) const { return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices; } virtual u8 GetUnionPercentMapExplored(const std::vector& players) const { u32 exploredVertices = 0; std::vector::const_iterator playerIt; for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) { if (LosIsOffWorld(i, j)) continue; for (playerIt = players.begin(); playerIt != players.end(); ++playerIt) if (m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*((*playerIt)-1)))) { exploredVertices += 1; break; } } return exploredVertices * 100 / m_TotalInworldVertices; } }; REGISTER_COMPONENT_TYPE(RangeManager) #undef LOS_TILES_RATIO #undef DEBUG_RANGE_MANAGER_BOUNDS Index: ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 24486) +++ ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 24487) @@ -1,238 +1,238 @@ /* Copyright (C) 2020 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 "ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializedTypes.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/TemplateLoader.h" #include "ps/XML/RelaxNG.h" class CCmpTemplateManager : public ICmpTemplateManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(TemplateManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_DisableValidation = false; m_Validator.LoadGrammar(GetSimContext().GetComponentManager().GenerateSchema()); // TODO: handle errors loading the grammar here? // TODO: support hotloading changes to the grammar } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { - std::map> templateMap; + std::map> templateMap; - for (const std::pair& templateEnt : m_LatestTemplates) + for (const std::pair& templateEnt : m_LatestTemplates) if (!ENTITY_IS_LOCAL(templateEnt.first)) templateMap[templateEnt.second].push_back(templateEnt.first); Serializer(serialize, "templates", templateMap); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); - std::map> templateMap; + std::map> templateMap; Serializer(deserialize, "templates", templateMap); - for (const std::pair>& mapEl : templateMap) + for (const std::pair>& mapEl : templateMap) for (entity_id_t id : mapEl.second) m_LatestTemplates[id] = mapEl.first; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); // Clean up m_LatestTemplates so it doesn't record any data for destroyed entities m_LatestTemplates.erase(msgData.entity); break; } } } virtual void DisableValidation() { m_DisableValidation = true; } virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::string& templateName); virtual const CParamNode* GetTemplate(const std::string& templateName); virtual const CParamNode* GetTemplateWithoutValidation(const std::string& templateName); virtual bool TemplateExists(const std::string& templateName) const; virtual const CParamNode* LoadLatestTemplate(entity_id_t ent); virtual std::string GetCurrentTemplateName(entity_id_t ent) const; virtual std::vector FindAllTemplates(bool includeActors) const; virtual std::vector FindUsedTemplates() const; virtual std::vector GetEntitiesUsingTemplate(const std::string& templateName) const; private: // Template loader CTemplateLoader m_templateLoader; // Entity template XML validator RelaxNGValidator m_Validator; // Disable validation, for test cases bool m_DisableValidation; // Map from template name to schema validation status. // (Some files, e.g. inherited parent templates, may not be valid themselves but we still need to load // them and use them; we only reject invalid templates that were requested directly by GetTemplate/etc) std::map m_TemplateSchemaValidity; // Remember the template used by each entity, so we can return them // again for deserialization. std::map m_LatestTemplates; }; REGISTER_COMPONENT_TYPE(TemplateManager) const CParamNode* CCmpTemplateManager::LoadTemplate(entity_id_t ent, const std::string& templateName) { m_LatestTemplates[ent] = templateName; return GetTemplate(templateName); } const CParamNode* CCmpTemplateManager::GetTemplate(const std::string& templateName) { const CParamNode& fileData = m_templateLoader.GetTemplateFileData(templateName); if (!fileData.IsOk()) return NULL; if (!m_DisableValidation) { // Compute validity, if it's not computed before if (m_TemplateSchemaValidity.find(templateName) == m_TemplateSchemaValidity.end()) { m_TemplateSchemaValidity[templateName] = m_Validator.Validate(wstring_from_utf8(templateName), fileData.ToXML()); // Show error on the first failure to validate the template if (!m_TemplateSchemaValidity[templateName]) LOGERROR("Failed to validate entity template '%s'", templateName.c_str()); } // Refuse to return invalid templates if (!m_TemplateSchemaValidity[templateName]) return NULL; } const CParamNode& templateRoot = fileData.GetChild("Entity"); if (!templateRoot.IsOk()) { // The validator should never let this happen LOGERROR("Invalid root element in entity template '%s'", templateName.c_str()); return NULL; } return &templateRoot; } const CParamNode* CCmpTemplateManager::GetTemplateWithoutValidation(const std::string& templateName) { const CParamNode& templateRoot = m_templateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) return NULL; return &templateRoot; } bool CCmpTemplateManager::TemplateExists(const std::string& templateName) const { return m_templateLoader.TemplateExists(templateName); } const CParamNode* CCmpTemplateManager::LoadLatestTemplate(entity_id_t ent) { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return NULL; return LoadTemplate(ent, it->second); } std::string CCmpTemplateManager::GetCurrentTemplateName(entity_id_t ent) const { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return ""; return it->second; } std::vector CCmpTemplateManager::FindAllTemplates(bool includeActors) const { ETemplatesType templatesType = includeActors ? ALL_TEMPLATES : SIMULATION_TEMPLATES; return m_templateLoader.FindTemplates("", true, templatesType); } std::vector CCmpTemplateManager::FindUsedTemplates() const { std::vector usedTemplates; - for (const std::pair& p : m_LatestTemplates) + for (const std::pair& p : m_LatestTemplates) if (std::find(usedTemplates.begin(), usedTemplates.end(), p.second) == usedTemplates.end()) usedTemplates.push_back(p.second); return usedTemplates; } /** * Get the list of entities using the specified template */ std::vector CCmpTemplateManager::GetEntitiesUsingTemplate(const std::string& templateName) const { std::vector entities; - for (const std::pair& p : m_LatestTemplates) + for (const std::pair& p : m_LatestTemplates) if (p.second == templateName) entities.push_back(p.first); return entities; } Index: ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 24486) +++ ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 24487) @@ -1,858 +1,858 @@ /* Copyright (C) 2020 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 "ICmpTerritoryManager.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "graphics/TerritoryBoundary.h" #include "maths/MathUtil.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/TerrainOverlay.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerritoryDecayManager.h" #include "simulation2/components/ICmpTerritoryInfluence.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Render.h" #include class CCmpTerritoryManager; class TerritoryOverlay : public TerrainTextureOverlay { NONCOPYABLE(TerritoryOverlay); public: CCmpTerritoryManager& m_TerritoryManager; TerritoryOverlay(CCmpTerritoryManager& manager); virtual void BuildTextureRGBA(u8* data, size_t w, size_t h); }; class CCmpTerritoryManager : public ICmpTerritoryManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_PlayerColorChanged); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_ObstructionMapShapeChanged); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_WaterChanged); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(TerritoryManager) static std::string GetSchema() { return ""; } u8 m_ImpassableCost; float m_BorderThickness; float m_BorderSeparation; // Player ID in bits 0-4 (TERRITORY_PLAYER_MASK) // connected flag in bit 5 (TERRITORY_CONNECTED_MASK) // blinking flag in bit 6 (TERRITORY_BLINKING_MASK) // processed flag in bit 7 (TERRITORY_PROCESSED_MASK) Grid* m_Territories; std::vector m_TerritoryCellCounts; u16 m_TerritoryTotalPassableCellCount; // Saves the cost per tile (to stop territory on impassable tiles) Grid* m_CostGrid; // Set to true when territories change; will send a TerritoriesChanged message // during the Update phase bool m_TriggerEvent; struct SBoundaryLine { bool blinking; player_id_t owner; CColor color; SOverlayTexturedLine overlay; }; std::vector m_BoundaryLines; bool m_BoundaryLinesDirty; double m_AnimTime; // time since start of rendering, in seconds TerritoryOverlay* m_DebugOverlay; bool m_EnableLineDebugOverlays; ///< Enable node debugging overlays for boundary lines? std::vector m_DebugBoundaryLineNodes; virtual void Init(const CParamNode& UNUSED(paramNode)) { m_Territories = NULL; m_CostGrid = NULL; m_DebugOverlay = NULL; // m_DebugOverlay = new TerritoryOverlay(*this); m_BoundaryLinesDirty = true; m_TriggerEvent = true; m_EnableLineDebugOverlays = false; m_DirtyID = 1; m_DirtyBlinkingID = 1; m_Visible = true; m_ColorChanged = false; m_AnimTime = 0.0; m_TerritoryTotalPassableCellCount = 0; // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "territorymanager", "simulation/data/territorymanager.rng"); CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/territorymanager.xml", "territorymanager"); int impassableCost = externalParamNode.GetChild("TerritoryManager").GetChild("ImpassableCost").ToInt(); ENSURE(0 <= impassableCost && impassableCost <= 255); m_ImpassableCost = (u8)impassableCost; m_BorderThickness = externalParamNode.GetChild("TerritoryManager").GetChild("BorderThickness").ToFixed().ToFloat(); m_BorderSeparation = externalParamNode.GetChild("TerritoryManager").GetChild("BorderSeparation").ToFixed().ToFloat(); } virtual void Deinit() { SAFE_DELETE(m_Territories); SAFE_DELETE(m_CostGrid); SAFE_DELETE(m_DebugOverlay); } virtual void Serialize(ISerializer& serialize) { // Territory state can be recomputed as required, so we don't need to serialize any of it. serialize.Bool("trigger event", m_TriggerEvent); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); deserialize.Bool("trigger event", m_TriggerEvent); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_PlayerColorChanged: { MakeDirty(); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component == L"TerritoryInfluence") MakeDirty(); break; } case MT_ObstructionMapShapeChanged: case MT_TerrainChanged: case MT_WaterChanged: { // also recalculate the cost grid to support atlas changes SAFE_DELETE(m_CostGrid); MakeDirty(); break; } case MT_Update: { if (m_TriggerEvent) { m_TriggerEvent = false; GetSimContext().GetComponentManager().BroadcastMessage(CMessageTerritoriesChanged()); } break; } case MT_Interpolate: { const CMessageInterpolate& msgData = static_cast (msg); Interpolate(msgData.deltaSimTime, msgData.offset); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector, msgData.frustum, msgData.culling); break; } } } // Check whether the entity is either a settlement or territory influence; // ignore any others void MakeDirtyIfRelevantEntity(entity_id_t ent) { CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); if (cmpTerritoryInfluence) MakeDirty(); } virtual const Grid& GetTerritoryGrid() { CalculateTerritories(); ENSURE(m_Territories); return *m_Territories; } virtual player_id_t GetOwner(entity_pos_t x, entity_pos_t z); virtual std::vector GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected); virtual bool IsConnected(entity_pos_t x, entity_pos_t z); virtual void SetTerritoryBlinking(entity_pos_t x, entity_pos_t z, bool enable); virtual bool IsTerritoryBlinking(entity_pos_t x, entity_pos_t z); // To support lazy updates of territory render data, // we maintain a DirtyID here and increment it whenever territories change; // if a caller has a lower DirtyID then it needs to be updated. // We also do the same thing for blinking updates using DirtyBlinkingID. size_t m_DirtyID; size_t m_DirtyBlinkingID; bool m_ColorChanged; void MakeDirty() { SAFE_DELETE(m_Territories); ++m_DirtyID; m_BoundaryLinesDirty = true; m_TriggerEvent = true; } virtual bool NeedUpdateTexture(size_t* dirtyID) { if (*dirtyID == m_DirtyID && !m_ColorChanged) return false; *dirtyID = m_DirtyID; m_ColorChanged = false; return true; } virtual bool NeedUpdateAI(size_t* dirtyID, size_t* dirtyBlinkingID) const { if (*dirtyID == m_DirtyID && *dirtyBlinkingID == m_DirtyBlinkingID) return false; *dirtyID = m_DirtyID; *dirtyBlinkingID = m_DirtyBlinkingID; return true; } void CalculateCostGrid(); void CalculateTerritories(); u8 GetTerritoryPercentage(player_id_t player); std::vector ComputeBoundaries(); void UpdateBoundaryLines(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); void SetVisibility(bool visible) { m_Visible = visible; } void UpdateColors(); private: bool m_Visible; }; REGISTER_COMPONENT_TYPE(TerritoryManager) // Tile data type, for easier accessing of coordinates struct Tile { Tile(u16 i, u16 j) : x(i), z(j) { } u16 x, z; }; // Floodfill templates that expand neighbours from a certain source onwards // (posX, posZ) are the coordinates of the currently expanded tile // (nx, nz) are the coordinates of the current neighbour handled // The user of this floodfill should use "continue" on every neighbour that // shouldn't be expanded on its own. (without continue, an infinite loop will happen) # define FLOODFILL(i, j, code)\ do {\ const int NUM_NEIGHBOURS = 8;\ const int NEIGHBOURS_X[NUM_NEIGHBOURS] = {1,-1, 0, 0, 1,-1, 1,-1};\ const int NEIGHBOURS_Z[NUM_NEIGHBOURS] = {0, 0, 1,-1, 1,-1,-1, 1};\ std::queue openTiles;\ openTiles.emplace(i, j);\ while (!openTiles.empty())\ {\ u16 posX = openTiles.front().x;\ u16 posZ = openTiles.front().z;\ openTiles.pop();\ for (int n = 0; n < NUM_NEIGHBOURS; ++n)\ {\ u16 nx = posX + NEIGHBOURS_X[n];\ u16 nz = posZ + NEIGHBOURS_Z[n];\ /* Check the bounds, underflow will cause the values to be big again */\ if (nx >= tilesW || nz >= tilesH)\ continue;\ code\ openTiles.emplace(nx, nz);\ }\ }\ }\ while (false) /** * Compute the tile indexes on the grid nearest to a given point */ static void NearestTerritoryTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h) { entity_pos_t scale = Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE; i = Clamp((x / scale).ToInt_RoundToNegInfinity(), 0, w - 1); j = Clamp((z / scale).ToInt_RoundToNegInfinity(), 0, h - 1); } void CCmpTerritoryManager::CalculateCostGrid() { if (m_CostGrid) return; CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; pass_class_t passClassTerritory = cmpPathfinder->GetPassabilityClass("default-terrain-only"); pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted"); const Grid& passGrid = cmpPathfinder->GetPassabilityGrid(); int tilesW = passGrid.m_W / NAVCELLS_PER_TERRITORY_TILE; int tilesH = passGrid.m_H / NAVCELLS_PER_TERRITORY_TILE; m_CostGrid = new Grid(tilesW, tilesH); m_TerritoryTotalPassableCellCount = 0; for (int i = 0; i < tilesW; ++i) { for (int j = 0; j < tilesH; ++j) { NavcellData c = 0; for (u16 di = 0; di < NAVCELLS_PER_TERRITORY_TILE; ++di) for (u16 dj = 0; dj < NAVCELLS_PER_TERRITORY_TILE; ++dj) c |= passGrid.get( i * NAVCELLS_PER_TERRITORY_TILE + di, j * NAVCELLS_PER_TERRITORY_TILE + dj); if (!IS_PASSABLE(c, passClassTerritory)) m_CostGrid->set(i, j, m_ImpassableCost); else if (!IS_PASSABLE(c, passClassUnrestricted)) m_CostGrid->set(i, j, 255); // off the world; use maximum cost else { m_CostGrid->set(i, j, 1); ++m_TerritoryTotalPassableCellCount; } } } } void CCmpTerritoryManager::CalculateTerritories() { if (m_Territories) return; PROFILE("CalculateTerritories"); // If the pathfinder hasn't been loaded (e.g. this is called during map initialisation), // abort the computation (and assume callers can cope with m_Territories == NULL) CalculateCostGrid(); if (!m_CostGrid) return; const u16 tilesW = m_CostGrid->m_W; const u16 tilesH = m_CostGrid->m_H; m_Territories = new Grid(tilesW, tilesH); // Reset territory counts for all players CmpPtr cmpPlayerManager(GetSystemEntity()); if (cmpPlayerManager && (size_t)cmpPlayerManager->GetNumPlayers() != m_TerritoryCellCounts.size()) m_TerritoryCellCounts.resize(cmpPlayerManager->GetNumPlayers()); for (u16& count : m_TerritoryCellCounts) count = 0; // Find all territory influence entities CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); // Split influence entities into per-player lists, ignoring any with invalid properties std::map > influenceEntities; for (const CComponentManager::InterfacePair& pair : influences) { entity_id_t ent = pair.first; CmpPtr cmpOwnership(GetSimContext(), ent); if (!cmpOwnership) continue; // Ignore Gaia and unassigned or players we can't represent player_id_t owner = cmpOwnership->GetOwner(); if (owner <= 0 || owner > TERRITORY_PLAYER_MASK) continue; influenceEntities[owner].push_back(ent); } // Store the overall best weight for comparison Grid bestWeightGrid(tilesW, tilesH); // store the root influences to mark territory as connected std::vector rootInfluenceEntities; - for (const std::pair >& pair : influenceEntities) + for (const std::pair>& pair : influenceEntities) { // entityGrid stores the weight for a single entity, and is reset per entity Grid entityGrid(tilesW, tilesH); // playerGrid stores the combined weight of all entities for this player Grid playerGrid(tilesW, tilesH); - u8 owner = (u8)pair.first; + u8 owner = static_cast(pair.first); const std::vector& ents = pair.second; // With 2^16 entities, we're safe against overflows as the weight is also limited to 2^16 ENSURE(ents.size() < 1 << 16); // Compute the influence map of the current entity, then add it to the player grid for (entity_id_t ent : ents) { CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); u32 weight = cmpTerritoryInfluence->GetWeight(); u32 radius = cmpTerritoryInfluence->GetRadius(); if (weight == 0 || radius == 0) continue; u32 falloff = weight * (Pathfinding::NAVCELL_SIZE * NAVCELLS_PER_TERRITORY_TILE).ToInt_RoundToNegInfinity() / radius; CFixedVector2D pos = cmpPosition->GetPosition2D(); u16 i, j; NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH); if (cmpTerritoryInfluence->IsRoot()) rootInfluenceEntities.push_back(ent); // Initialise the tile under the entity entityGrid.set(i, j, weight); if (weight > bestWeightGrid.get(i, j)) { bestWeightGrid.set(i, j, weight); m_Territories->set(i, j, owner); } // Expand influences outwards FLOODFILL(i, j, u32 dg = falloff * m_CostGrid->get(nx, nz); // diagonal neighbour -> multiply with approx sqrt(2) if (nx != posX && nz != posZ) dg = (dg * 362) / 256; // Don't expand if new cost is not better than previous value for that tile // (arranged to avoid underflow if entityGrid.get(x, z) < dg) if (entityGrid.get(posX, posZ) <= entityGrid.get(nx, nz) + dg) continue; // weight of this tile = weight of predecessor - falloff from predecessor u32 newWeight = entityGrid.get(posX, posZ) - dg; u32 totalWeight = playerGrid.get(nx, nz) - entityGrid.get(nx, nz) + newWeight; playerGrid.set(nx, nz, totalWeight); entityGrid.set(nx, nz, newWeight); // if this weight is better than the best thus far, set the owner if (totalWeight > bestWeightGrid.get(nx, nz)) { bestWeightGrid.set(nx, nz, totalWeight); m_Territories->set(nx, nz, owner); } ); entityGrid.reset(); } } // Detect territories connected to a 'root' influence (typically a civ center) // belonging to their player, and mark them with the connected flag for (entity_id_t ent : rootInfluenceEntities) { // (These components must be valid else the entities wouldn't be added to this list) CmpPtr cmpOwnership(GetSimContext(), ent); CmpPtr cmpPosition(GetSimContext(), ent); CFixedVector2D pos = cmpPosition->GetPosition2D(); u16 i, j; NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH); u8 owner = (u8)cmpOwnership->GetOwner(); if (m_Territories->get(i, j) != owner) continue; m_Territories->set(i, j, owner | TERRITORY_CONNECTED_MASK); FLOODFILL(i, j, // Don't expand non-owner tiles, or tiles that already have a connected mask if (m_Territories->get(nx, nz) != owner) continue; m_Territories->set(nx, nz, owner | TERRITORY_CONNECTED_MASK); if (m_CostGrid->get(nx, nz) < m_ImpassableCost) ++m_TerritoryCellCounts[owner]; ); } // Then recomputes the blinking tiles CmpPtr cmpTerritoryDecayManager(GetSystemEntity()); if (cmpTerritoryDecayManager) { size_t dirtyBlinkingID = m_DirtyBlinkingID; cmpTerritoryDecayManager->SetBlinkingEntities(); m_DirtyBlinkingID = dirtyBlinkingID; } } std::vector CCmpTerritoryManager::ComputeBoundaries() { PROFILE("ComputeBoundaries"); CalculateTerritories(); ENSURE(m_Territories); return CTerritoryBoundaryCalculator::ComputeBoundaries(m_Territories); } u8 CCmpTerritoryManager::GetTerritoryPercentage(player_id_t player) { if (player <= 0 || static_cast(player) >= m_TerritoryCellCounts.size()) return 0; CalculateTerritories(); // Territories may have been recalculated, check whether player is still there. if (m_TerritoryTotalPassableCellCount == 0 || static_cast(player) >= m_TerritoryCellCounts.size()) return 0; u8 percentage = (m_TerritoryCellCounts[player] * 100) / m_TerritoryTotalPassableCellCount; ENSURE(percentage <= 100); return percentage; } void CCmpTerritoryManager::UpdateBoundaryLines() { PROFILE("update boundary lines"); m_BoundaryLines.clear(); m_DebugBoundaryLineNodes.clear(); if (!CRenderer::IsInitialised()) return; std::vector boundaries = ComputeBoundaries(); CTextureProperties texturePropsBase("art/textures/misc/territory_border.png"); texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsBase.SetMaxAnisotropy(2.f); CTexturePtr textureBase = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask("art/textures/misc/territory_border_mask.png"); texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsMask.SetMaxAnisotropy(2.f); CTexturePtr textureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; for (size_t i = 0; i < boundaries.size(); ++i) { if (boundaries[i].points.empty()) continue; CColor color(1, 0, 1, 1); CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaries[i].owner)); if (cmpPlayer) color = cmpPlayer->GetDisplayedColor(); m_BoundaryLines.push_back(SBoundaryLine()); m_BoundaryLines.back().blinking = boundaries[i].blinking; m_BoundaryLines.back().owner = boundaries[i].owner; m_BoundaryLines.back().color = color; m_BoundaryLines.back().overlay.m_SimContext = &GetSimContext(); m_BoundaryLines.back().overlay.m_TextureBase = textureBase; m_BoundaryLines.back().overlay.m_TextureMask = textureMask; m_BoundaryLines.back().overlay.m_Color = color; m_BoundaryLines.back().overlay.m_Thickness = m_BorderThickness; m_BoundaryLines.back().overlay.m_Closed = true; SimRender::SmoothPointsAverage(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed); SimRender::InterpolatePointsRNS(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed, m_BorderSeparation); std::vector& points = m_BoundaryLines.back().overlay.m_Coords; for (size_t j = 0; j < boundaries[i].points.size(); ++j) { points.push_back(boundaries[i].points[j]); if (m_EnableLineDebugOverlays) { const size_t numHighlightNodes = 7; // highlight the X last nodes on either end to see where they meet (if closed) SOverlayLine overlayNode; if (j > boundaries[i].points.size() - 1 - numHighlightNodes) overlayNode.m_Color = CColor(1.f, 0.f, 0.f, 1.f); else if (j < numHighlightNodes) overlayNode.m_Color = CColor(0.f, 1.f, 0.f, 1.f); else overlayNode.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); overlayNode.m_Thickness = 1; SimRender::ConstructCircleOnGround(GetSimContext(), boundaries[i].points[j].X, boundaries[i].points[j].Y, 0.1f, overlayNode, true); m_DebugBoundaryLineNodes.push_back(overlayNode); } } } } void CCmpTerritoryManager::Interpolate(float frameTime, float UNUSED(frameOffset)) { m_AnimTime += frameTime; if (m_BoundaryLinesDirty) { UpdateBoundaryLines(); m_BoundaryLinesDirty = false; } for (size_t i = 0; i < m_BoundaryLines.size(); ++i) { if (m_BoundaryLines[i].blinking) { CColor c = m_BoundaryLines[i].color; c.a *= 0.2f + 0.8f * fabsf((float)cos(m_AnimTime * M_PI)); // TODO: should let artists tweak this m_BoundaryLines[i].overlay.m_Color = c; } } } void CCmpTerritoryManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { if (!m_Visible) return; for (size_t i = 0; i < m_BoundaryLines.size(); ++i) { if (culling && !m_BoundaryLines[i].overlay.IsVisibleInFrustum(frustum)) continue; collector.Submit(&m_BoundaryLines[i].overlay); } for (size_t i = 0; i < m_DebugBoundaryLineNodes.size(); ++i) collector.Submit(&m_DebugBoundaryLineNodes[i]); } player_id_t CCmpTerritoryManager::GetOwner(entity_pos_t x, entity_pos_t z) { u16 i, j; if (!m_Territories) { CalculateTerritories(); if (!m_Territories) return 0; } NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; } std::vector CCmpTerritoryManager::GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) { CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return std::vector(); std::vector ret(cmpPlayerManager->GetNumPlayers(), 0); CalculateTerritories(); if (!m_Territories) return ret; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); // calculate the neighbours player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; u16 tilesW = m_Territories->m_W; u16 tilesH = m_Territories->m_H; // use a flood-fill algorithm that fills up to the borders and remembers the owners Grid markerGrid(tilesW, tilesH); markerGrid.set(i, j, true); FLOODFILL(i, j, if (markerGrid.get(nx, nz)) continue; // mark the tile as visited in any case markerGrid.set(nx, nz, true); int owner = m_Territories->get(nx, nz) & TERRITORY_PLAYER_MASK; if (owner != thisOwner) { if (owner == 0 || !filterConnected || (m_Territories->get(nx, nz) & TERRITORY_CONNECTED_MASK) != 0) ret[owner]++; // add player to the neighbour list when requested continue; // don't expand non-owner tiles further } ); return ret; } bool CCmpTerritoryManager::IsConnected(entity_pos_t x, entity_pos_t z) { u16 i, j; CalculateTerritories(); if (!m_Territories) return false; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return (m_Territories->get(i, j) & TERRITORY_CONNECTED_MASK) != 0; } void CCmpTerritoryManager::SetTerritoryBlinking(entity_pos_t x, entity_pos_t z, bool enable) { CalculateTerritories(); if (!m_Territories) return; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); u16 tilesW = m_Territories->m_W; u16 tilesH = m_Territories->m_H; player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; FLOODFILL(i, j, u8 bitmask = m_Territories->get(nx, nz); if ((bitmask & TERRITORY_PLAYER_MASK) != thisOwner) continue; u8 blinking = bitmask & TERRITORY_BLINKING_MASK; if (enable && !blinking) m_Territories->set(nx, nz, bitmask | TERRITORY_BLINKING_MASK); else if (!enable && blinking) m_Territories->set(nx, nz, bitmask & ~TERRITORY_BLINKING_MASK); else continue; ); ++m_DirtyBlinkingID; m_BoundaryLinesDirty = true; } bool CCmpTerritoryManager::IsTerritoryBlinking(entity_pos_t x, entity_pos_t z) { CalculateTerritories(); if (!m_Territories) return false; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return (m_Territories->get(i, j) & TERRITORY_BLINKING_MASK) != 0; } void CCmpTerritoryManager::UpdateColors() { m_ColorChanged = true; CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; for (SBoundaryLine& boundaryLine : m_BoundaryLines) { CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaryLine.owner)); if (!cmpPlayer) continue; boundaryLine.color = cmpPlayer->GetDisplayedColor(); boundaryLine.overlay.m_Color = boundaryLine.color; } } TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager) : TerrainTextureOverlay((float)Pathfinding::NAVCELLS_PER_TILE / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE), m_TerritoryManager(manager) { } void TerritoryOverlay::BuildTextureRGBA(u8* data, size_t w, size_t h) { for (size_t j = 0; j < h; ++j) { for (size_t i = 0; i < w; ++i) { SColor4ub color; u8 id = (m_TerritoryManager.m_Territories->get((int)i, (int)j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK); color = GetColor(id, 64); *data++ = color.R; *data++ = color.G; *data++ = color.B; *data++ = color.A; } } } #undef FLOODFILL Index: ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.cpp =================================================================== --- ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.cpp (revision 24486) +++ ps/trunk/source/simulation2/helpers/HierarchicalPathfinder.cpp (revision 24487) @@ -1,850 +1,852 @@ /* Copyright (C) 2020 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 "HierarchicalPathfinder.h" #include "graphics/Overlay.h" #include "ps/Profile.h" #include "renderer/Scene.h" #include "simulation2/helpers/Grid.h" // Find the root ID of a region, used by InitRegions inline u16 RootID(u16 x, const std::vector& v) { while (v[x] < x) x = v[x]; return x; } void HierarchicalPathfinder::Chunk::InitRegions(int ci, int cj, Grid* grid, pass_class_t passClass) { ENSURE(ci < 256 && cj < 256); // avoid overflows m_ChunkI = ci; m_ChunkJ = cj; memset(m_Regions, 0, sizeof(m_Regions)); int i0 = ci * CHUNK_SIZE; int j0 = cj * CHUNK_SIZE; int i1 = std::min(i0 + CHUNK_SIZE, (int)grid->m_W); int j1 = std::min(j0 + CHUNK_SIZE, (int)grid->m_H); // Efficiently flood-fill the m_Regions grid int regionID = 0; std::vector connect; u16* pCurrentID = NULL; u16 LeftID = 0; u16 DownID = 0; bool Checked = false; // prevent some unneccessary RootID calls connect.reserve(32); // TODO: What's a sensible number? connect.push_back(0); // connect[0] = 0 // Start by filling the grid with 0 for blocked, // and regionID for unblocked for (int j = j0; j < j1; ++j) { for (int i = i0; i < i1; ++i) { pCurrentID = &m_Regions[j-j0][i-i0]; if (!IS_PASSABLE(grid->get(i, j), passClass)) { *pCurrentID = 0; continue; } if (j > j0) DownID = m_Regions[j-1-j0][i-i0]; if (i == i0) LeftID = 0; else LeftID = m_Regions[j-j0][i-1-i0]; if (LeftID > 0) { *pCurrentID = LeftID; if (*pCurrentID != DownID && DownID > 0 && !Checked) { u16 id0 = RootID(DownID, connect); u16 id1 = RootID(LeftID, connect); Checked = true; // this avoids repeatedly connecting the same IDs if (id0 < id1) connect[id1] = id0; else if (id0 > id1) connect[id0] = id1; } else if (DownID == 0) Checked = false; } else if (DownID > 0) { *pCurrentID = DownID; Checked = false; } else { // New ID *pCurrentID = ++regionID; connect.push_back(regionID); Checked = false; } } } // Mark connected regions as being the same ID (i.e. the lowest) m_RegionsID.clear(); for (u16 i = 1; i < regionID+1; ++i) { if (connect[i] != i) connect[i] = RootID(i, connect); else m_RegionsID.push_back(connect[i]); } // Replace IDs by the root ID for (int j = 0; j < CHUNK_SIZE; ++j) for (int i = 0; i < CHUNK_SIZE; ++i) m_Regions[j][i] = connect[m_Regions[j][i]]; } /** * Returns a RegionID for the given global navcell coords * (which must be inside this chunk); */ HierarchicalPathfinder::RegionID HierarchicalPathfinder::Chunk::Get(int i, int j) const { ENSURE(i < CHUNK_SIZE && j < CHUNK_SIZE); return RegionID(m_ChunkI, m_ChunkJ, m_Regions[j][i]); } /** * Return the global navcell coords that correspond roughly to the * center of the given region in this chunk. * (This is not guaranteed to be actually inside the region.) */ void HierarchicalPathfinder::Chunk::RegionCenter(u16 r, int& i_out, int& j_out) const { // Find the mean of i,j coords of navcells in this region: u32 si = 0, sj = 0; // sum of i,j coords u32 n = 0; // number of navcells in region cassert(CHUNK_SIZE < 256); // conservative limit to ensure si and sj don't overflow for (int j = 0; j < CHUNK_SIZE; ++j) { for (int i = 0; i < CHUNK_SIZE; ++i) { if (m_Regions[j][i] == r) { si += i; sj += j; n += 1; } } } // Avoid divide-by-zero if (n == 0) n = 1; i_out = m_ChunkI * CHUNK_SIZE + si / n; j_out = m_ChunkJ * CHUNK_SIZE + sj / n; } /** * Returns the global navcell coords, and the squared distance to the goal * navcell, of whichever navcell inside the given region is closest to * that goal. */ void HierarchicalPathfinder::Chunk::RegionNavcellNearest(u16 r, int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const { iBest = 0; jBest = 0; dist2Best = std::numeric_limits::max(); for (int j = 0; j < CHUNK_SIZE; ++j) { for (int i = 0; i < CHUNK_SIZE; ++i) { if (m_Regions[j][i] != r) continue; u32 dist2 = (i + m_ChunkI*CHUNK_SIZE - iGoal)*(i + m_ChunkI*CHUNK_SIZE - iGoal) + (j + m_ChunkJ*CHUNK_SIZE - jGoal)*(j + m_ChunkJ*CHUNK_SIZE - jGoal); if (dist2 < dist2Best) { iBest = i + m_ChunkI*CHUNK_SIZE; jBest = j + m_ChunkJ*CHUNK_SIZE; dist2Best = dist2; } } } } /** * Gives the global navcell coords, and the squared distance to the (i0,j0) * navcell, of whichever navcell inside the given region and inside the given goal * is closest to (i0,j0) * Returns true if the goal is inside the region, false otherwise. */ bool HierarchicalPathfinder::Chunk::RegionNearestNavcellInGoal(u16 r, u16 i0, u16 j0, const PathGoal& goal, u16& iOut, u16& jOut, u32& dist2Best) const { // TODO: this should be optimized further. // Most used cases empirically seem to be SQUARE, INVERTED_CIRCLE and then POINT and CIRCLE somehwat equally iOut = 0; jOut = 0; dist2Best = std::numeric_limits::max(); // Calculate the navcell that contains the center of the goal. int gi = (goal.x >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity(); int gj = (goal.z >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToNegInfinity(); switch(goal.type) { case PathGoal::POINT: { // i and j can be equal to CHUNK_SIZE on the top and right borders of the map, // specially when mapSize is a multiple of CHUNK_SIZE int i = std::min((int)CHUNK_SIZE - 1, gi - m_ChunkI * CHUNK_SIZE); int j = std::min((int)CHUNK_SIZE - 1, gj - m_ChunkJ * CHUNK_SIZE); if (m_Regions[j][i] == r) { iOut = gi; jOut = gj; dist2Best = (gi - i0)*(gi - i0) + (gj - j0)*(gj - j0); return true; } return false; } case PathGoal::CIRCLE: case PathGoal::SQUARE: { // restrict ourselves to a square surrounding the goal. int radius = (std::max(goal.hw*3/2,goal.hh*3/2) >> Pathfinding::NAVCELL_SIZE_LOG2).ToInt_RoundToInfinity(); int imin = std::max(0, gi-m_ChunkI*CHUNK_SIZE-radius); int imax = std::min((int)CHUNK_SIZE, gi-m_ChunkI*CHUNK_SIZE+radius+1); int jmin = std::max(0, gj-m_ChunkJ*CHUNK_SIZE-radius); int jmax = std::min((int)CHUNK_SIZE, gj-m_ChunkJ*CHUNK_SIZE+radius+1); bool found = false; u32 dist2 = std::numeric_limits::max(); for (u16 j = jmin; j < jmax; ++j) { for (u16 i = imin; i < imax; ++i) { if (m_Regions[j][i] != r) continue; if (found) { dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0) + (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0); if (dist2 >= dist2Best) continue; } if (goal.NavcellContainsGoal(m_ChunkI * CHUNK_SIZE + i, m_ChunkJ * CHUNK_SIZE + j)) { if (!found) { found = true; dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0) + (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0); } iOut = i + m_ChunkI*CHUNK_SIZE; jOut = j + m_ChunkJ*CHUNK_SIZE; dist2Best = dist2; } } } return found; } case PathGoal::INVERTED_CIRCLE: case PathGoal::INVERTED_SQUARE: { bool found = false; u32 dist2 = std::numeric_limits::max(); // loop over all navcells. for (u16 j = 0; j < CHUNK_SIZE; ++j) { for (u16 i = 0; i < CHUNK_SIZE; ++i) { if (m_Regions[j][i] != r) continue; if (found) { dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0) + (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0); if (dist2 >= dist2Best) continue; } if (goal.NavcellContainsGoal(m_ChunkI * CHUNK_SIZE + i, m_ChunkJ * CHUNK_SIZE + j)) { if (!found) { found = true; dist2 = (i + m_ChunkI*CHUNK_SIZE - i0)*(i + m_ChunkI*CHUNK_SIZE - i0) + (j + m_ChunkJ*CHUNK_SIZE - j0)*(j + m_ChunkJ*CHUNK_SIZE - j0); } iOut = i + m_ChunkI*CHUNK_SIZE; jOut = j + m_ChunkJ*CHUNK_SIZE; dist2Best = dist2; } } } return found; } } return false; } HierarchicalPathfinder::HierarchicalPathfinder() : m_DebugOverlay(NULL) { } HierarchicalPathfinder::~HierarchicalPathfinder() { SAFE_DELETE(m_DebugOverlay); } void HierarchicalPathfinder::SetDebugOverlay(bool enabled, const CSimContext* simContext) { if (enabled && !m_DebugOverlay) { m_DebugOverlay = new HierarchicalOverlay(*this); m_DebugOverlayLines.clear(); m_SimContext = simContext; AddDebugEdges(GetPassabilityClass("default")); } else if (!enabled && m_DebugOverlay) { SAFE_DELETE(m_DebugOverlay); m_DebugOverlayLines.clear(); m_SimContext = NULL; } } void HierarchicalPathfinder::RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlay) return; for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); } void HierarchicalPathfinder::Recompute(Grid* grid, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { PROFILE2("Hierarchical Recompute"); m_PassClassMasks = pathfindingPassClassMasks; std::map allPassClasses = m_PassClassMasks; allPassClasses.insert(nonPathfindingPassClassMasks.begin(), nonPathfindingPassClassMasks.end()); m_W = grid->m_W; m_H = grid->m_H; ENSURE((grid->m_W + CHUNK_SIZE - 1) / CHUNK_SIZE < 256 && (grid->m_H + CHUNK_SIZE - 1) / CHUNK_SIZE < 256); // else the u8 Chunk::m_ChunkI will overflow // Divide grid into chunks with round-to-positive-infinity m_ChunksW = (grid->m_W + CHUNK_SIZE - 1) / CHUNK_SIZE; m_ChunksH = (grid->m_H + CHUNK_SIZE - 1) / CHUNK_SIZE; m_Chunks.clear(); m_Edges.clear(); // Reset global regions. m_NextGlobalRegionID = 1; for (auto& passClassMask : allPassClasses) { pass_class_t passClass = passClassMask.second; // Compute the regions within each chunk m_Chunks[passClass].resize(m_ChunksW*m_ChunksH); for (int cj = 0; cj < m_ChunksH; ++cj) { for (int ci = 0; ci < m_ChunksW; ++ci) { m_Chunks[passClass].at(cj*m_ChunksW + ci).InitRegions(ci, cj, grid, passClass); } } // Construct the search graph over the regions. EdgesMap& edges = m_Edges[passClass]; RecomputeAllEdges(passClass, edges); // Spread global regions. std::map& globalRegion = m_GlobalRegions[passClass]; globalRegion.clear(); for (u8 cj = 0; cj < m_ChunksH; ++cj) for (u8 ci = 0; ci < m_ChunksW; ++ci) for (u16 rid : GetChunk(ci, cj, passClass).m_RegionsID) { RegionID reg{ci,cj,rid}; if (globalRegion.find(reg) == globalRegion.end()) { GlobalRegionID ID = m_NextGlobalRegionID++; globalRegion.insert({ reg, ID }); // Avoid creating an empty link if possible, FindReachableRegions uses [] which calls the default constructor. if (edges.find(reg) != edges.end()) { std::set reachable; FindReachableRegions(reg, reachable, passClass); for (const RegionID& region : reachable) globalRegion.insert({ region, ID }); } } } } if (m_DebugOverlay) { m_DebugOverlayLines.clear(); AddDebugEdges(GetPassabilityClass("default")); } } void HierarchicalPathfinder::Update(Grid* grid, const Grid& dirtinessGrid) { PROFILE3("Hierarchical Update"); ASSERT(m_NextGlobalRegionID < std::numeric_limits::max()); std::map > needNewGlobalRegionMap; // Algorithm for the partial update: // 1. Loop over chunks. // 2. For any dirty chunk: // - remove all regions from the global region map // - remove all edges, by removing the neighbor connection with them and then deleting us // - recreate regions inside the chunk // - reconnect the regions. We may do too much work if we reconnect with a dirty chunk, but that's fine. // 3. Recreate global regions. // This means that if any chunk changes, we may need to flood (at most once) the whole map. // That's quite annoying, but I can't think of an easy way around it. // If we could be sure that a region's topology hasn't changed, we could skip removing its global region // but that's non trivial as we have no easy way to determine said topology (regions could "switch" IDs on update for now). for (u8 cj = 0; cj < m_ChunksH; ++cj) { int j0 = cj * CHUNK_SIZE; int j1 = std::min(j0 + CHUNK_SIZE, (int)dirtinessGrid.m_H); for (u8 ci = 0; ci < m_ChunksW; ++ci) { // Skip chunks where no navcells are dirty. int i0 = ci * CHUNK_SIZE; int i1 = std::min(i0 + CHUNK_SIZE, (int)dirtinessGrid.m_W); if (!dirtinessGrid.any_set_in_square(i0, j0, i1, j1)) continue; - for (const std::pair& passClassMask : m_PassClassMasks) + for (const std::pair& passClassMask : m_PassClassMasks) { pass_class_t passClass = passClassMask.second; Chunk& a = m_Chunks[passClass].at(ci + cj*m_ChunksW); // Clean up edges and global region ID EdgesMap& edgeMap = m_Edges[passClass]; for (u16 i : a.m_RegionsID) { RegionID reg{ci, cj, i}; m_GlobalRegions[passClass].erase(reg); for (const RegionID& neighbor : edgeMap[reg]) { edgeMap[neighbor].erase(reg); if (edgeMap[neighbor].empty()) edgeMap.erase(neighbor); } edgeMap.erase(reg); } // Recompute regions inside this chunk. a.InitRegions(ci, cj, grid, passClass); for (u16 i : a.m_RegionsID) needNewGlobalRegionMap[passClass].push_back(RegionID{ci, cj, i}); UpdateEdges(ci, cj, passClass, edgeMap); } } } UpdateGlobalRegions(needNewGlobalRegionMap); if (m_DebugOverlay) { m_DebugOverlayLines.clear(); AddDebugEdges(GetPassabilityClass("default")); } } void HierarchicalPathfinder::ComputeNeighbors(EdgesMap& edges, Chunk& a, Chunk& b, bool transpose, bool opposite) const { // For each edge between chunks, we loop over every adjacent pair of // navcells in the two chunks. If they are both in valid regions // (i.e. are passable navcells) then add a graph edge between those regions. // (We don't need to test for duplicates since EdgesMap already uses a // std::set which will drop duplicate entries.) // But as set.insert can be quite slow on large collection, and that we usually // try to insert the same values, we cache the previous one for a fast test. RegionID raPrev(0,0,0); RegionID rbPrev(0,0,0); for (int k = 0; k < CHUNK_SIZE; ++k) { u8 aSide = opposite ? CHUNK_SIZE - 1 : 0; u8 bSide = CHUNK_SIZE - 1 - aSide; RegionID ra = transpose ? a.Get(k, aSide) : a.Get(aSide, k); RegionID rb = transpose ? b.Get(k, bSide) : b.Get(bSide, k); if (ra.r && rb.r) { if (ra == raPrev && rb == rbPrev) continue; edges[ra].insert(rb); edges[rb].insert(ra); raPrev = ra; rbPrev = rb; } } } /** * Connect a chunk's regions to their neighbors. Not optimised for global recomputing. */ void HierarchicalPathfinder::UpdateEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges) { std::vector& chunks = m_Chunks[passClass]; Chunk& a = chunks.at(cj*m_ChunksW + ci); if (ci > 0) ComputeNeighbors(edges, a, chunks.at(cj*m_ChunksW + (ci-1)), false, false); if (ci < m_ChunksW-1) ComputeNeighbors(edges, a, chunks.at(cj*m_ChunksW + (ci+1)), false, true); if (cj > 0) ComputeNeighbors(edges, a, chunks.at((cj-1)*m_ChunksW + ci), true, false); if (cj < m_ChunksH - 1) ComputeNeighbors(edges, a, chunks.at((cj+1)*m_ChunksW + ci), true, true); } /** * Find edges between regions in all chunks, in an optimised manner (only look at top/left) */ void HierarchicalPathfinder::RecomputeAllEdges(pass_class_t passClass, EdgesMap& edges) { std::vector& chunks = m_Chunks[passClass]; edges.clear(); for (int cj = 0; cj < m_ChunksH; ++cj) { for (int ci = 0; ci < m_ChunksW; ++ci) { Chunk& a = chunks.at(cj*m_ChunksW + ci); if (ci > 0) ComputeNeighbors(edges, a, chunks.at(cj*m_ChunksW + (ci-1)), false, false); if (cj > 0) ComputeNeighbors(edges, a, chunks.at((cj-1)*m_ChunksW + ci), true, false); } } } /** * Debug visualisation of graph edges between regions. */ void HierarchicalPathfinder::AddDebugEdges(pass_class_t passClass) { const EdgesMap& edges = m_Edges[passClass]; const std::vector& chunks = m_Chunks[passClass]; for (auto& edge : edges) { for (const RegionID& region: edge.second) { // Draw a line between the two regions' centers int i0, j0, i1, j1; chunks[edge.first.cj * m_ChunksW + edge.first.ci].RegionCenter(edge.first.r, i0, j0); chunks[region.cj * m_ChunksW + region.ci].RegionCenter(region.r, i1, j1); CFixedVector2D a, b; Pathfinding::NavcellCenter(i0, j0, a.X, a.Y); Pathfinding::NavcellCenter(i1, j1, b.X, b.Y); // Push the endpoints inwards a little to avoid overlaps CFixedVector2D d = b - a; d.Normalize(entity_pos_t::FromInt(1)); a += d; b -= d; std::vector xz; xz.push_back(a.X.ToFloat()); xz.push_back(a.Y.ToFloat()); xz.push_back(b.X.ToFloat()); xz.push_back(b.Y.ToFloat()); m_DebugOverlayLines.emplace_back(); m_DebugOverlayLines.back().m_Color = CColor(1.0, 1.0, 1.0, 1.0); SimRender::ConstructLineOnGround(*m_SimContext, xz, m_DebugOverlayLines.back(), true); } } } void HierarchicalPathfinder::UpdateGlobalRegions(const std::map >& needNewGlobalRegionMap) { // Use FindReachableRegions because we cannot be sure, even if we find a non-dirty chunk nearby, // that we weren't the only bridge connecting that chunk to the rest of the global region. - for (const std::pair >& regionsInNeed : needNewGlobalRegionMap) + for (const std::pair>& regionsInNeed : needNewGlobalRegionMap) + { for (const RegionID& reg : regionsInNeed.second) { std::map& globalRegions = m_GlobalRegions[regionsInNeed.first]; // If we have already been given a region, skip us. if (globalRegions.find(reg) != globalRegions.end()) continue; std::set reachable; FindReachableRegions(reg, reachable, regionsInNeed.first); GlobalRegionID ID = m_NextGlobalRegionID++; for (const RegionID& regionId : reachable) globalRegions[regionId] = ID; } + } } HierarchicalPathfinder::RegionID HierarchicalPathfinder::Get(u16 i, u16 j, pass_class_t passClass) const { int ci = i / CHUNK_SIZE; int cj = j / CHUNK_SIZE; ENSURE(ci < m_ChunksW && cj < m_ChunksH); return m_Chunks.at(passClass)[cj*m_ChunksW + ci].Get(i % CHUNK_SIZE, j % CHUNK_SIZE); } HierarchicalPathfinder::GlobalRegionID HierarchicalPathfinder::GetGlobalRegion(u16 i, u16 j, pass_class_t passClass) const { return GetGlobalRegion(Get(i, j, passClass), passClass); } HierarchicalPathfinder::GlobalRegionID HierarchicalPathfinder::GetGlobalRegion(RegionID region, pass_class_t passClass) const { return region.r == 0 ? GlobalRegionID(0) : m_GlobalRegions.at(passClass).at(region); } void CreatePointGoalAt(u16 i, u16 j, PathGoal& goal) { PathGoal newGoal; newGoal.type = PathGoal::POINT; Pathfinding::NavcellCenter(i, j, newGoal.x, newGoal.z); goal = newGoal; } bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass) const { PROFILE2("MakeGoalReachable"); u16 iGoal, jGoal; Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H); std::set goalRegions(SortByBestToPoint(i0, j0)); // This returns goal regions ordered by distance from the best navcell in each region. FindGoalRegionsAndBestNavcells(i0, j0, iGoal, jGoal, goal, goalRegions, passClass); // Because of the sorting above, we can stop as soon as the first reachable goal region is found. for (const InterestingRegion& region : goalRegions) if (GetGlobalRegion(region.region, passClass) == GetGlobalRegion(i0, j0, passClass)) { iGoal = region.bestI; jGoal = region.bestJ; // No need to move reachable point goals. if (goal.type != PathGoal::POINT) CreatePointGoalAt(iGoal, jGoal, goal); return true; } // Goal wasn't reachable - get the closest navcell in the nearest reachable region. std::set reachableRegions(SortByCenterToPoint(iGoal, jGoal)); FindReachableRegions(Get(i0, j0, passClass), reachableRegions, passClass); FindNearestNavcellInRegions(reachableRegions, iGoal, jGoal, passClass); CreatePointGoalAt(iGoal, jGoal, goal); return false; } bool HierarchicalPathfinder::IsGoalReachable(u16 i0, u16 j0, const PathGoal& goal, pass_class_t passClass) const { PROFILE2("IsGoalReachable"); u16 iGoal, jGoal; Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H); std::set goalRegions(SortByBestToPoint(i0, j0)); // This returns goal regions ordered by distance from the best navcell in each region. FindGoalRegionsAndBestNavcells(i0, j0, iGoal, jGoal, goal, goalRegions, passClass); // Because of the sorting above, we can stop as soon as the first reachable goal region is found. for (const InterestingRegion& region : goalRegions) if (GetGlobalRegion(region.region, passClass) == GetGlobalRegion(i0, j0, passClass)) return true; return false; } void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) const { std::set regions(SortByCenterToPoint(i, j)); // Construct a set of all regions of all chunks for this pass class for (const Chunk& chunk : m_Chunks.at(passClass)) for (int r : chunk.m_RegionsID) regions.insert(RegionID(chunk.m_ChunkI, chunk.m_ChunkJ, r)); FindNearestNavcellInRegions(regions, i, j, passClass); } void HierarchicalPathfinder::FindNearestNavcellInRegions(const std::set& regions, u16& iGoal, u16& jGoal, pass_class_t passClass) const { u16 bestI = iGoal, bestJ = jGoal; // Somewhat sensible default-values should regions() be passed empty. u32 bestDist = std::numeric_limits::max(); // Because regions are sorted by increasing distance, we can ignore regions that are obviously farther than the current best point. // Since regions are squares, that happens when the center of a region is at least √2 * CHUNK_SIZE farther than the current best point. // Add one to avoid cases where the center navcell is actually slightly off-center (= CHUNK_SIZE is even) u32 maxDistFromBest = (fixed::FromInt(3) / 2 * CHUNK_SIZE).ToInt_RoundToInfinity() + 1; // TODO: update to static_assert with constexpr ENSURE(maxDistFromBest < std::numeric_limits::max()); maxDistFromBest *= maxDistFromBest; for (const RegionID& region : regions) { u32 chunkDist = region.DistanceTo(iGoal, jGoal); // This might overflow, but only if we are already close to the maximal possible distance, so the condition would probably be false anyways. // It's also a bit pessimistic, so we'll still consider a few too many regions. if (bestDist < std::numeric_limits::max() && chunkDist > maxDistFromBest + bestDist) break; // Break, the set is ordered by increased distance so a closer region will not be found. int ri, rj; u32 dist; GetChunk(region.ci, region.cj, passClass).RegionNavcellNearest(region.r, iGoal, jGoal, ri, rj, dist); if (dist < bestDist) { bestI = ri; bestJ = rj; bestDist = dist; } } iGoal = bestI; jGoal = bestJ; } void HierarchicalPathfinder::FindGoalRegionsAndBestNavcells(u16 i0, u16 j0, u16 gi, u16 gj, const PathGoal& goal, std::set& regions, pass_class_t passClass) const { if (goal.type == PathGoal::POINT) { RegionID region = Get(gi, gj, passClass); if (region.r > 0) regions.insert({region, gi, gj}); return; } // For non-point cases, we'll test each region inside the bounds of the goal. // we might occasionally test a few too many for circles but it's not too bad. // Note that this also works in the Inverse-circle / Inverse-square case // Since our ranges are inclusive, we will necessarily test at least the perimeter/outer bound of the goal. // If we find a navcell, great, if not, well then we'll be surrounded by an impassable barrier. // Since in the Inverse-XX case we're supposed to start inside, then we can't ever reach the goal so it's good enough. // It's not worth it to skip the "inner" regions since we'd need ranges above CHUNK_SIZE for that to start mattering // (and even then not always) and that just doesn't happen for Inverse-XX goals int size = (std::max(goal.hh, goal.hw) * 3 / 2).ToInt_RoundToInfinity(); u16 bestI, bestJ; u32 c; // Unused. for (u8 sz = std::max(0,(gj - size) / CHUNK_SIZE); sz <= std::min(m_ChunksH-1, (gj + size + 1) / CHUNK_SIZE); ++sz) for (u8 sx = std::max(0,(gi - size) / CHUNK_SIZE); sx <= std::min(m_ChunksW-1, (gi + size + 1) / CHUNK_SIZE); ++sx) { const Chunk& chunk = GetChunk(sx, sz, passClass); for (u16 i : chunk.m_RegionsID) if (chunk.RegionNearestNavcellInGoal(i, i0, j0, goal, bestI, bestJ, c)) regions.insert({RegionID{sx, sz, i}, bestI, bestJ}); } } void HierarchicalPathfinder::FillRegionOnGrid(const RegionID& region, pass_class_t passClass, u16 value, Grid& grid) const { ENSURE(grid.m_W == m_W && grid.m_H == m_H); int i0 = region.ci * CHUNK_SIZE; int j0 = region.cj * CHUNK_SIZE; const Chunk& c = m_Chunks.at(passClass)[region.cj * m_ChunksW + region.ci]; for (int j = 0; j < CHUNK_SIZE; ++j) for (int i = 0; i < CHUNK_SIZE; ++i) if (c.m_Regions[j][i] == region.r) grid.set(i0 + i, j0 + j, value); } Grid HierarchicalPathfinder::GetConnectivityGrid(pass_class_t passClass) const { Grid connectivityGrid(m_W, m_H); connectivityGrid.reset(); u16 idx = 1; for (u16 i = 0; i < m_W; ++i) { for (u16 j = 0; j < m_H; ++j) { if (connectivityGrid.get(i, j) != 0) continue; RegionID from = Get(i, j, passClass); if (from.r == 0) continue; std::set reachable; FindReachableRegions(from, reachable, passClass); for (const RegionID& region : reachable) FillRegionOnGrid(region, passClass, idx, connectivityGrid); ++idx; } } return connectivityGrid; } Index: ps/trunk/source/soundmanager/SoundManager.cpp =================================================================== --- ps/trunk/source/soundmanager/SoundManager.cpp (revision 24486) +++ ps/trunk/source/soundmanager/SoundManager.cpp (revision 24487) @@ -1,866 +1,866 @@ /* Copyright (C) 2020 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 "ISoundManager.h" #include "SoundManager.h" #include "data/SoundData.h" #include "items/CBufferItem.h" #include "items/CSoundItem.h" #include "items/CStreamItem.h" #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Profiler2.h" #include "ps/XML/Xeromyces.h" #include ISoundManager* g_SoundManager = NULL; #define SOURCE_NUM 64 #if CONFIG2_AUDIO class CSoundManagerWorker { NONCOPYABLE(CSoundManagerWorker); public: CSoundManagerWorker() { m_Items = new ItemsList; m_DeadItems = new ItemsList; m_Shutdown = false; m_WorkerThread = std::thread(RunThread, this); } ~CSoundManagerWorker() { delete m_Items; CleanupItems(); delete m_DeadItems; } bool Shutdown() { { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; ItemsList::iterator lstr = m_Items->begin(); while (lstr != m_Items->end()) { delete *lstr; ++lstr; } } m_WorkerThread.join(); return true; } void addItem(ISoundItem* anItem) { std::lock_guard lock(m_WorkerMutex); m_Items->push_back(anItem); } void CleanupItems() { std::lock_guard lock(m_DeadItemsMutex); AL_CHECK; ItemsList::iterator deadItems = m_DeadItems->begin(); while (deadItems != m_DeadItems->end()) { delete *deadItems; ++deadItems; AL_CHECK; } m_DeadItems->clear(); } private: static void RunThread(CSoundManagerWorker* data) { debug_SetThreadName("CSoundManagerWorker"); g_Profiler2.RegisterCurrentThread("soundmanager"); data->Run(); } void Run() { while (true) { // Handle shutdown requests as soon as possible if (GetShutdown()) return; int pauseTime = 500; if (g_SoundManager->InDistress()) pauseTime = 50; { std::lock_guard workerLock(m_WorkerMutex); ItemsList::iterator lstr = m_Items->begin(); ItemsList* nextItemList = new ItemsList; while (lstr != m_Items->end()) { AL_CHECK; if ((*lstr)->IdleTask()) { if ((pauseTime == 500) && (*lstr)->IsFading()) pauseTime = 100; nextItemList->push_back(*lstr); } else { std::lock_guard deadItemsLock(m_DeadItemsMutex); m_DeadItems->push_back(*lstr); } ++lstr; AL_CHECK; } delete m_Items; m_Items = nextItemList; AL_CHECK; } SDL_Delay(pauseTime); } } bool GetShutdown() { std::lock_guard lock(m_WorkerMutex); return m_Shutdown; } private: // Thread-related members: std::thread m_WorkerThread; std::mutex m_WorkerMutex; std::mutex m_DeadItemsMutex; // Shared by main thread and worker thread: // These variables are all protected by a mutexes ItemsList* m_Items; ItemsList* m_DeadItems; bool m_Shutdown; CSoundManagerWorker(ISoundManager* UNUSED(other)){}; }; void ISoundManager::CreateSoundManager() { if (!g_SoundManager) { g_SoundManager = new CSoundManager(); g_SoundManager->StartWorker(); } } void ISoundManager::SetEnabled(bool doEnable) { if (g_SoundManager && !doEnable) SAFE_DELETE(g_SoundManager); else if (!g_SoundManager && doEnable) ISoundManager::CreateSoundManager(); } void ISoundManager::CloseGame() { if (CSoundManager* aSndMgr = (CSoundManager*)g_SoundManager) aSndMgr->SetAmbientItem(NULL); } void CSoundManager::al_ReportError(ALenum err, const char* caller, int line) { LOGERROR("OpenAL error: %s; called from %s (line %d)\n", alGetString(err), caller, line); } void CSoundManager::al_check(const char* caller, int line) { ALenum err = alGetError(); if (err != AL_NO_ERROR) al_ReportError(err, caller, line); } Status CSoundManager::ReloadChangedFiles(const VfsPath& UNUSED(path)) { // TODO implement sound file hotloading return INFO::OK; } /*static*/ Status CSoundManager::ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFiles(path); } CSoundManager::CSoundManager() : m_Context(nullptr), m_Device(nullptr), m_ALSourceBuffer(nullptr), m_CurrentTune(nullptr), m_CurrentEnvirons(nullptr), m_Worker(nullptr), m_DistressMutex(), m_PlayListItems(nullptr), m_SoundGroups(), m_Gain(.5f), m_MusicGain(.5f), m_AmbientGain(.5f), m_ActionGain(.5f), m_UIGain(.5f), m_Enabled(false), m_BufferSize(98304), m_BufferCount(50), m_SoundEnabled(true), m_MusicEnabled(true), m_MusicPaused(false), m_AmbientPaused(false), m_ActionPaused(false), m_RunningPlaylist(false), m_PlayingPlaylist(false), m_LoopingPlaylist(false), m_PlaylistGap(0), m_DistressErrCount(0), m_DistressTime(0) { CFG_GET_VAL("sound.mastergain", m_Gain); CFG_GET_VAL("sound.musicgain", m_MusicGain); CFG_GET_VAL("sound.ambientgain", m_AmbientGain); CFG_GET_VAL("sound.actiongain", m_ActionGain); CFG_GET_VAL("sound.uigain", m_UIGain); AlcInit(); if (m_Enabled) { SetMasterGain(m_Gain); InitListener(); m_PlayListItems = new PlayList; } if (!CXeromyces::AddValidator(g_VFS, "sound_group", "audio/sound_group.rng")) LOGERROR("CSoundManager: failed to load grammar file 'audio/sound_group.rng'"); RegisterFileReloadFunc(ReloadChangedFileCB, this); RunHardwareDetection(); } CSoundManager::~CSoundManager() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); if (m_Worker) { AL_CHECK; m_Worker->Shutdown(); AL_CHECK; m_Worker->CleanupItems(); AL_CHECK; delete m_Worker; } AL_CHECK; - for (const std::pair& p : m_SoundGroups) + for (const std::pair& p : m_SoundGroups) delete p.second; m_SoundGroups.clear(); if (m_PlayListItems) delete m_PlayListItems; if (m_ALSourceBuffer != NULL) delete[] m_ALSourceBuffer; if (m_Context) alcDestroyContext(m_Context); if (m_Device) alcCloseDevice(m_Device); } void CSoundManager::StartWorker() { if (m_Enabled) m_Worker = new CSoundManagerWorker(); } Status CSoundManager::AlcInit() { Status ret = INFO::OK; m_Device = alcOpenDevice(NULL); if (m_Device) { ALCint attribs[] = {ALC_STEREO_SOURCES, 16, 0}; m_Context = alcCreateContext(m_Device, &attribs[0]); if (m_Context) { alcMakeContextCurrent(m_Context); m_ALSourceBuffer = new ALSourceHolder[SOURCE_NUM]; ALuint* sourceList = new ALuint[SOURCE_NUM]; alGenSources(SOURCE_NUM, sourceList); ALCenum err = alcGetError(m_Device); if (err == ALC_NO_ERROR) { for (int x = 0; x < SOURCE_NUM; x++) { m_ALSourceBuffer[x].ALSource = sourceList[x]; m_ALSourceBuffer[x].SourceItem = NULL; } m_Enabled = true; } else { LOGERROR("error in gensource = %d", err); } delete[] sourceList; } } // check if init succeeded. // some OpenAL implementations don't indicate failure here correctly; // we need to check if the device and context pointers are actually valid. ALCenum err = alcGetError(m_Device); const char* dev_name = (const char*)alcGetString(m_Device, ALC_DEVICE_SPECIFIER); if (err == ALC_NO_ERROR && m_Device && m_Context) debug_printf("Sound: AlcInit success, using %s\n", dev_name); else { LOGERROR("Sound: AlcInit failed, m_Device=%p m_Context=%p dev_name=%s err=%x\n", (void *)m_Device, (void *)m_Context, dev_name, err); // FIXME Hack to get around exclusive access to the sound device #if OS_UNIX ret = INFO::OK; #else ret = ERR::FAIL; #endif // !OS_UNIX } return ret; } bool CSoundManager::InDistress() { std::lock_guard lock(m_DistressMutex); if (m_DistressTime == 0) return false; else if ((timer_Time() - m_DistressTime) > 10) { m_DistressTime = 0; // Coming out of distress mode m_DistressErrCount = 0; return false; } return true; } void CSoundManager::SetDistressThroughShortage() { std::lock_guard lock(m_DistressMutex); // Going into distress for normal reasons m_DistressTime = timer_Time(); } void CSoundManager::SetDistressThroughError() { std::lock_guard lock(m_DistressMutex); // Going into distress due to unknown error m_DistressTime = timer_Time(); m_DistressErrCount++; } ALuint CSoundManager::GetALSource(ISoundItem* anItem) { for (int x = 0; x < SOURCE_NUM; x++) { if (!m_ALSourceBuffer[x].SourceItem) { m_ALSourceBuffer[x].SourceItem = anItem; return m_ALSourceBuffer[x].ALSource; } } SetDistressThroughShortage(); return 0; } void CSoundManager::ReleaseALSource(ALuint theSource) { for (int x = 0; x < SOURCE_NUM; x++) { if (m_ALSourceBuffer[x].ALSource == theSource) { m_ALSourceBuffer[x].SourceItem = NULL; return; } } } long CSoundManager::GetBufferCount() { return m_BufferCount; } long CSoundManager::GetBufferSize() { return m_BufferSize; } void CSoundManager::AddPlayListItem(const VfsPath& itemPath) { if (m_Enabled) m_PlayListItems->push_back(itemPath); } void CSoundManager::ClearPlayListItems() { if (m_Enabled) { if (m_PlayingPlaylist) SetMusicItem(NULL); m_PlayingPlaylist = false; m_LoopingPlaylist = false; m_RunningPlaylist = false; m_PlayListItems->clear(); } } void CSoundManager::StartPlayList(bool doLoop) { if (m_Enabled && m_MusicEnabled) { if (m_PlayListItems->size() > 0) { m_PlayingPlaylist = true; m_LoopingPlaylist = doLoop; m_RunningPlaylist = false; ISoundItem* aSnd = LoadItem((m_PlayListItems->at(0))); if (aSnd) SetMusicItem(aSnd); else SetMusicItem(NULL); } } } void CSoundManager::SetMasterGain(float gain) { if (m_Enabled) { m_Gain = gain; alListenerf(AL_GAIN, m_Gain); AL_CHECK; } } void CSoundManager::SetMusicGain(float gain) { m_MusicGain = gain; if (m_CurrentTune) m_CurrentTune->SetGain(m_MusicGain); } void CSoundManager::SetAmbientGain(float gain) { m_AmbientGain = gain; } void CSoundManager::SetActionGain(float gain) { m_ActionGain = gain; } void CSoundManager::SetUIGain(float gain) { m_UIGain = gain; } ISoundItem* CSoundManager::LoadItem(const VfsPath& itemPath) { AL_CHECK; if (m_Enabled) { CSoundData* itemData = CSoundData::SoundDataFromFile(itemPath); AL_CHECK; if (itemData) return CSoundManager::ItemForData(itemData); } return NULL; } ISoundItem* CSoundManager::ItemForData(CSoundData* itemData) { AL_CHECK; ISoundItem* answer = NULL; AL_CHECK; if (m_Enabled && (itemData != NULL)) { if (itemData->IsOneShot()) { if (itemData->GetBufferCount() == 1) answer = new CSoundItem(itemData); else answer = new CBufferItem(itemData); } else { answer = new CStreamItem(itemData); } if (answer && m_Worker) m_Worker->addItem(answer); } return answer; } void CSoundManager::IdleTask() { if (m_Enabled) { if (m_CurrentTune) { m_CurrentTune->EnsurePlay(); if (m_PlayingPlaylist && m_RunningPlaylist) { if (m_CurrentTune->Finished()) { if (m_PlaylistGap == 0) { m_PlaylistGap = timer_Time() + 15; } else if (m_PlaylistGap < timer_Time()) { m_PlaylistGap = 0; PlayList::iterator it = find(m_PlayListItems->begin(), m_PlayListItems->end(), m_CurrentTune->GetName()); if (it != m_PlayListItems->end()) { ++it; Path nextPath; if (it == m_PlayListItems->end()) nextPath = m_PlayListItems->at(0); else nextPath = *it; ISoundItem* aSnd = LoadItem(nextPath); if (aSnd) SetMusicItem(aSnd); } } } } } if (m_CurrentEnvirons) m_CurrentEnvirons->EnsurePlay(); if (m_Worker) m_Worker->CleanupItems(); } } ISoundItem* CSoundManager::ItemForEntity(entity_id_t UNUSED(source), CSoundData* sndData) { ISoundItem* currentItem = NULL; if (m_Enabled) currentItem = ItemForData(sndData); return currentItem; } void CSoundManager::InitListener() { ALfloat listenerPos[] = {0.0, 0.0, 0.0}; ALfloat listenerVel[] = {0.0, 0.0, 0.0}; ALfloat listenerOri[] = {0.0, 0.0, -1.0, 0.0, 1.0, 0.0}; alListenerfv(AL_POSITION, listenerPos); alListenerfv(AL_VELOCITY, listenerVel); alListenerfv(AL_ORIENTATION, listenerOri); alDistanceModel(AL_LINEAR_DISTANCE); } void CSoundManager::PlayGroupItem(ISoundItem* anItem, ALfloat groupGain) { if (anItem) { if (m_Enabled && (m_ActionGain > 0)) { anItem->SetGain(m_ActionGain * groupGain); anItem->PlayAndDelete(); AL_CHECK; } } } void CSoundManager::SetMusicEnabled(bool isEnabled) { if (m_CurrentTune && !isEnabled) { m_CurrentTune->FadeAndDelete(1.00); m_CurrentTune = NULL; } m_MusicEnabled = isEnabled; } void CSoundManager::PlayAsGroup(const VfsPath& groupPath, const CVector3D& sourcePos, entity_id_t source, bool ownedSound) { // Make sure the sound group is loaded CSoundGroup* group; if (m_SoundGroups.find(groupPath.string()) == m_SoundGroups.end()) { group = new CSoundGroup(); if (!group->LoadSoundGroup(L"audio/" + groupPath.string())) { LOGERROR("Failed to load sound group '%s'", groupPath.string8()); delete group; group = NULL; } // Cache the sound group (or the null, if it failed) m_SoundGroups[groupPath.string()] = group; } else { group = m_SoundGroups[groupPath.string()]; } // Failed to load group -> do nothing if (group && (ownedSound || !group->TestFlag(eOwnerOnly))) group->PlayNext(sourcePos, source); } void CSoundManager::PlayAsMusic(const VfsPath& itemPath, bool looping) { if (m_Enabled) { UNUSED2(looping); ISoundItem* aSnd = LoadItem(itemPath); if (aSnd != NULL) SetMusicItem(aSnd); } } void CSoundManager::PlayAsAmbient(const VfsPath& itemPath, bool looping) { if (m_Enabled) { UNUSED2(looping); ISoundItem* aSnd = LoadItem(itemPath); if (aSnd != NULL) SetAmbientItem(aSnd); } } void CSoundManager::PlayAsUI(const VfsPath& itemPath, bool looping) { if (m_Enabled) { IdleTask(); if (ISoundItem* anItem = LoadItem(itemPath)) { if (m_UIGain > 0) { anItem->SetGain(m_UIGain); anItem->SetLooping(looping); anItem->PlayAndDelete(); } } AL_CHECK; } } void CSoundManager::Pause(bool pauseIt) { PauseMusic(pauseIt); PauseAmbient(pauseIt); PauseAction(pauseIt); } void CSoundManager::PauseMusic(bool pauseIt) { if (m_CurrentTune && pauseIt && !m_MusicPaused) { m_CurrentTune->FadeAndPause(1.0); } else if (m_CurrentTune && m_MusicPaused && !pauseIt && m_MusicEnabled) { m_CurrentTune->SetGain(0); m_CurrentTune->Resume(); m_CurrentTune->FadeToIn(m_MusicGain, 1.0); } m_MusicPaused = pauseIt; } void CSoundManager::PauseAmbient(bool pauseIt) { if (m_CurrentEnvirons && pauseIt) m_CurrentEnvirons->Pause(); else if (m_CurrentEnvirons) m_CurrentEnvirons->Resume(); m_AmbientPaused = pauseIt; } void CSoundManager::PauseAction(bool pauseIt) { m_ActionPaused = pauseIt; } void CSoundManager::SetMusicItem(ISoundItem* anItem) { if (m_Enabled) { AL_CHECK; if (m_CurrentTune) { m_CurrentTune->FadeAndDelete(2.00); m_CurrentTune = NULL; } IdleTask(); if (anItem) { if (m_MusicEnabled) { m_CurrentTune = anItem; m_CurrentTune->SetGain(0); if (m_PlayingPlaylist) { m_RunningPlaylist = true; m_CurrentTune->Play(); } else m_CurrentTune->PlayLoop(); m_MusicPaused = false; m_CurrentTune->FadeToIn(m_MusicGain, 1.00); } else { anItem->StopAndDelete(); } } AL_CHECK; } } void CSoundManager::SetAmbientItem(ISoundItem* anItem) { if (m_Enabled) { if (m_CurrentEnvirons) { m_CurrentEnvirons->FadeAndDelete(3.00); m_CurrentEnvirons = NULL; } IdleTask(); if (anItem) { if (m_AmbientGain > 0) { m_CurrentEnvirons = anItem; m_CurrentEnvirons->SetGain(0); m_CurrentEnvirons->PlayLoop(); m_CurrentEnvirons->FadeToIn(m_AmbientGain, 2.00); } } AL_CHECK; } } void CSoundManager::RunHardwareDetection() { // OpenAL alGetString might not return anything interesting on certain platforms // (see https://stackoverflow.com/questions/28960638 for an example). // However our previous code supported only Windows, and alGetString does work on // Windows, so this is an improvement. // Sound cards const ALCchar* devices = nullptr; if (alcIsExtensionPresent(nullptr, "ALC_enumeration_EXT") == AL_TRUE) { if (alcIsExtensionPresent(nullptr, "ALC_enumerate_all_EXT") == AL_TRUE) devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); else devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); } WARN_IF_FALSE(devices); m_SoundCardNames.clear(); do { m_SoundCardNames += devices; devices += strlen(devices) + 1; m_SoundCardNames += "; "; } while (*devices); // Driver version const ALCchar* al_version = alGetString(AL_VERSION); if (al_version) m_OpenALVersion = al_version; } CStr8 CSoundManager::GetOpenALVersion() const { return m_OpenALVersion; } CStr8 CSoundManager::GetSoundCardNames() const { return m_SoundCardNames; } #else // CONFIG2_AUDIO void ISoundManager::CreateSoundManager(){} void ISoundManager::SetEnabled(bool UNUSED(doEnable)){} void ISoundManager::CloseGame(){} void ISoundManager::RunHardwareDetection() {} CStr8 ISoundManager::GetSoundCardNames() const { return CStr8(); }; CStr8 ISoundManager::GetOpenALVersion() const { return CStr8(); }; #endif // CONFIG2_AUDIO Index: ps/trunk/source/third_party/jsonspirit/json_spirit_value.h =================================================================== --- ps/trunk/source/third_party/jsonspirit/json_spirit_value.h (revision 24486) +++ ps/trunk/source/third_party/jsonspirit/json_spirit_value.h (revision 24487) @@ -1,606 +1,606 @@ #ifndef JSON_SPIRIT_VALUE #define JSON_SPIRIT_VALUE // Copyright John W. Wilkinson 2007 - 2014 // Distributed under the MIT License, see accompanying file LICENSE.txt // json spirit version 4.08 #if defined(_MSC_VER) && (_MSC_VER >= 1020) # pragma once # pragma warning(disable: 4505) // Unreferenced function has been removed. #endif #include #include #include #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include // comment out the value types you don't need to reduce build times and intermediate file sizes #define JSON_SPIRIT_VALUE_ENABLED //#define JSON_SPIRIT_WVALUE_ENABLED //#define JSON_SPIRIT_MVALUE_ENABLED //#define JSON_SPIRIT_WMVALUE_ENABLED namespace json_spirit { enum Value_type{ obj_type, array_type, str_type, bool_type, int_type, real_type, null_type }; - static std::string value_type_to_string( Value_type vtype ); + static inline std::string value_type_to_string(const Value_type vtype ); struct Null{}; - + template< class Config > // Config determines whether the value uses std::string or std::wstring and // whether JSON Objects are represented as vectors or maps class Value_impl { public: typedef Config Config_type; typedef typename Config::String_type String_type; typedef typename Config::Object_type Object; typedef typename Config::Array_type Array; typedef typename String_type::const_pointer Const_str_ptr; // eg const char* Value_impl(); // creates null value - Value_impl( Const_str_ptr value ); + Value_impl( Const_str_ptr value ); Value_impl( const String_type& value ); Value_impl( const Object& value ); Value_impl( const Array& value ); Value_impl( bool value ); Value_impl( int value ); Value_impl( boost::int64_t value ); Value_impl( boost::uint64_t value ); Value_impl( double value ); template< class Iter > Value_impl( Iter first, Iter last ); // constructor from containers, e.g. std::vector or std::list template< BOOST_VARIANT_ENUM_PARAMS( typename T ) > Value_impl( const boost::variant< BOOST_VARIANT_ENUM_PARAMS(T) >& variant ); // constructor for compatible variant types Value_impl( const Value_impl& other ); bool operator==( const Value_impl& lhs ) const; Value_impl& operator=( const Value_impl& lhs ); Value_type type() const; bool is_uint64() const; bool is_null() const; const String_type& get_str() const; const Object& get_obj() const; const Array& get_array() const; bool get_bool() const; int get_int() const; boost::int64_t get_int64() const; boost::uint64_t get_uint64() const; double get_real() const; Object& get_obj(); Array& get_array(); template< typename T > T get_value() const; // example usage: int i = value.get_value< int >(); // or double d = value.get_value< double >(); static const Value_impl null; private: void check_type( const Value_type vtype ) const; - typedef boost::variant< boost::recursive_wrapper< Object >, boost::recursive_wrapper< Array >, + typedef boost::variant< boost::recursive_wrapper< Object >, boost::recursive_wrapper< Array >, String_type, bool, boost::int64_t, double, Null, boost::uint64_t > Variant; Variant v_; - class Variant_converter_visitor : public boost::static_visitor< Variant > + class Variant_converter_visitor : public boost::static_visitor< Variant > { public: - + template< typename T, typename A, template< typename, typename > class Cont > - Variant operator()( const Cont< T, A >& cont ) const + Variant operator()( const Cont< T, A >& cont ) const { return Array( cont.begin(), cont.end() ); } - - Variant operator()( int i ) const + + Variant operator()( int i ) const { return static_cast< boost::int64_t >( i ); } - + template - Variant operator()( const T& t ) const + Variant operator()( const T& t ) const { return t; } }; }; // vector objects template< class Config > struct Pair_impl { typedef typename Config::String_type String_type; typedef typename Config::Value_type Value_type; Pair_impl() { } Pair_impl( const String_type& name, const Value_type& value ); bool operator==( const Pair_impl& lhs ) const; String_type name_; Value_type value_; }; #if defined( JSON_SPIRIT_VALUE_ENABLED ) || defined( JSON_SPIRIT_WVALUE_ENABLED ) template< class String > struct Config_vector { typedef String String_type; typedef Value_impl< Config_vector > Value_type; typedef Pair_impl < Config_vector > Pair_type; typedef std::vector< Value_type > Array_type; typedef std::vector< Pair_type > Object_type; static Value_type& add( Object_type& obj, const String_type& name, const Value_type& value ) { obj.push_back( Pair_type( name , value ) ); return obj.back().value_; } - + static const String_type& get_name( const Pair_type& pair ) { return pair.name_; } - + static const Value_type& get_value( const Pair_type& pair ) { return pair.value_; } }; #endif // typedefs for ASCII #ifdef JSON_SPIRIT_VALUE_ENABLED typedef Config_vector< std::string > Config; typedef Config::Value_type Value; typedef Config::Pair_type Pair; typedef Config::Object_type Object; typedef Config::Array_type Array; #endif // typedefs for Unicode #if defined( JSON_SPIRIT_WVALUE_ENABLED ) && !defined( BOOST_NO_STD_WSTRING ) typedef Config_vector< std::wstring > wConfig; typedef wConfig::Value_type wValue; typedef wConfig::Pair_type wPair; typedef wConfig::Object_type wObject; typedef wConfig::Array_type wArray; #endif // map objects #if defined( JSON_SPIRIT_MVALUE_ENABLED ) || defined( JSON_SPIRIT_WMVALUE_ENABLED ) template< class String > struct Config_map { typedef String String_type; typedef Value_impl< Config_map > Value_type; typedef std::vector< Value_type > Array_type; typedef std::map< String_type, Value_type > Object_type; typedef std::pair< const String_type, Value_type > Pair_type; static Value_type& add( Object_type& obj, const String_type& name, const Value_type& value ) { return obj[ name ] = value; } - + static const String_type& get_name( const Pair_type& pair ) { return pair.first; } - + static const Value_type& get_value( const Pair_type& pair ) { return pair.second; } }; #endif // typedefs for ASCII #ifdef JSON_SPIRIT_MVALUE_ENABLED typedef Config_map< std::string > mConfig; typedef mConfig::Value_type mValue; typedef mConfig::Object_type mObject; typedef mConfig::Array_type mArray; #endif // typedefs for Unicode #if defined( JSON_SPIRIT_WMVALUE_ENABLED ) && !defined( BOOST_NO_STD_WSTRING ) typedef Config_map< std::wstring > wmConfig; typedef wmConfig::Value_type wmValue; typedef wmConfig::Object_type wmObject; typedef wmConfig::Array_type wmArray; #endif /////////////////////////////////////////////////////////////////////////////////////////////// // // implementation inline bool operator==( const Null&, const Null& ) { return true; } template< class Config > const Value_impl< Config > Value_impl< Config >::null; template< class Config > Value_impl< Config >::Value_impl() : v_( Null() ) { } template< class Config > Value_impl< Config >::Value_impl( const Const_str_ptr value ) : v_( String_type( value ) ) { } template< class Config > Value_impl< Config >::Value_impl( const String_type& value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( const Object& value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( const Array& value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( bool value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( int value ) : v_( static_cast< boost::int64_t >( value ) ) { } template< class Config > Value_impl< Config >::Value_impl( boost::int64_t value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( boost::uint64_t value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( double value ) : v_( value ) { } template< class Config > Value_impl< Config >::Value_impl( const Value_impl< Config >& other ) : v_( other.v_ ) { } template< class Config > template< class Iter > Value_impl< Config >::Value_impl( Iter first, Iter last ) : v_( Array( first, last ) ) { } template< class Config > template< BOOST_VARIANT_ENUM_PARAMS( typename T ) > Value_impl< Config >::Value_impl( const boost::variant< BOOST_VARIANT_ENUM_PARAMS(T) >& variant ) : v_( boost::apply_visitor( Variant_converter_visitor(), variant) ) { } template< class Config > Value_impl< Config >& Value_impl< Config >::operator=( const Value_impl& lhs ) { Value_impl tmp( lhs ); std::swap( v_, tmp.v_ ); return *this; } template< class Config > bool Value_impl< Config >::operator==( const Value_impl& lhs ) const { if( this == &lhs ) return true; if( type() != lhs.type() ) return false; - return v_ == lhs.v_; + return v_ == lhs.v_; } template< class Config > Value_type Value_impl< Config >::type() const { if( is_uint64() ) { return int_type; } return static_cast< Value_type >( v_.which() ); } template< class Config > bool Value_impl< Config >::is_uint64() const { return v_.which() == null_type + 1; } template< class Config > bool Value_impl< Config >::is_null() const { return type() == null_type; } template< class Config > void Value_impl< Config >::check_type( const Value_type vtype ) const { - if( type() != vtype ) + if( type() != vtype ) { std::ostringstream os; os << "get_value< " << value_type_to_string( vtype ) << " > called on " << value_type_to_string( type() ) << " Value"; throw std::runtime_error( os.str() ); } } template< class Config > const typename Config::String_type& Value_impl< Config >::get_str() const { check_type( str_type ); return *boost::get< String_type >( &v_ ); } template< class Config > const typename Value_impl< Config >::Object& Value_impl< Config >::get_obj() const { check_type( obj_type ); return *boost::get< Object >( &v_ ); } - + template< class Config > const typename Value_impl< Config >::Array& Value_impl< Config >::get_array() const { check_type( array_type ); return *boost::get< Array >( &v_ ); } - + template< class Config > bool Value_impl< Config >::get_bool() const { check_type( bool_type ); return boost::get< bool >( v_ ); } - + template< class Config > int Value_impl< Config >::get_int() const { check_type( int_type ); return static_cast< int >( get_int64() ); } - + template< class Config > boost::int64_t Value_impl< Config >::get_int64() const { check_type( int_type ); if( is_uint64() ) { return static_cast< boost::int64_t >( get_uint64() ); } return boost::get< boost::int64_t >( v_ ); } - + template< class Config > boost::uint64_t Value_impl< Config >::get_uint64() const { check_type( int_type ); if( !is_uint64() ) { return static_cast< boost::uint64_t >( get_int64() ); } return boost::get< boost::uint64_t >( v_ ); } template< class Config > double Value_impl< Config >::get_real() const { if( type() == int_type ) { return is_uint64() ? static_cast< double >( get_uint64() ) : static_cast< double >( get_int64() ); } check_type( real_type ); return boost::get< double >( v_ ); } template< class Config > typename Value_impl< Config >::Object& Value_impl< Config >::get_obj() { check_type( obj_type ); return *boost::get< Object >( &v_ ); } template< class Config > typename Value_impl< Config >::Array& Value_impl< Config >::get_array() { check_type( array_type ); return *boost::get< Array >( &v_ ); } template< class Config > Pair_impl< Config >::Pair_impl( const String_type& name, const Value_type& value ) : name_( name ) , value_( value ) { } template< class Config > bool Pair_impl< Config >::operator==( const Pair_impl< Config >& lhs ) const { if( this == &lhs ) return true; return ( name_ == lhs.name_ ) && ( value_ == lhs.value_ ); } // converts a C string, ie. 8 bit char array, to a string object // template < class String_type > String_type to_str( const char* c_str ) { String_type result; for( const char* p = c_str; *p != 0; ++p ) { result += *p; } return result; } // namespace internal_ { template< typename T > struct Type_to_type { }; - template< class Value > + template< class Value > int get_value( const Value& value, Type_to_type< int > ) { return value.get_int(); } - - template< class Value > + + template< class Value > boost::int64_t get_value( const Value& value, Type_to_type< boost::int64_t > ) { return value.get_int64(); } - - template< class Value > + + template< class Value > boost::uint64_t get_value( const Value& value, Type_to_type< boost::uint64_t > ) { return value.get_uint64(); } - - template< class Value > + + template< class Value > double get_value( const Value& value, Type_to_type< double > ) { return value.get_real(); } - - template< class Value > + + template< class Value > typename Value::String_type get_value( const Value& value, Type_to_type< typename Value::String_type > ) { return value.get_str(); } - - template< class Value > + + template< class Value > typename Value::Array get_value( const Value& value, Type_to_type< typename Value::Array > ) { return value.get_array(); } - - template< class Value > + + template< class Value > typename Value::Object get_value( const Value& value, Type_to_type< typename Value::Object > ) { return value.get_obj(); } - - template< class Value > + + template< class Value > bool get_value( const Value& value, Type_to_type< bool > ) { return value.get_bool(); } } template< class Config > - template< typename T > + template< typename T > T Value_impl< Config >::get_value() const { return internal_::get_value( *this, internal_::Type_to_type< T >() ); } - static std::string value_type_to_string( const Value_type vtype ) + static inline std::string value_type_to_string( const Value_type vtype ) { switch( vtype ) { case obj_type: return "Object"; case array_type: return "Array"; case str_type: return "string"; case bool_type: return "boolean"; case int_type: return "integer"; case real_type: return "real"; case null_type: return "null"; } assert( false ); return "unknown type"; } } #endif Index: ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp =================================================================== --- ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 24486) +++ ps/trunk/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Map/Map.cpp (revision 24487) @@ -1,747 +1,747 @@ /* Copyright (C) 2020 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 "Map.h" #include "AtlasObject/AtlasObject.h" #include "AtlasObject/JSONSpiritInclude.h" #include "GameInterface/Messages.h" #include "MapResizeDialog/MapResizeDialog.h" #include "ScenarioEditor/ScenarioEditor.h" #include "ScenarioEditor/Tools/Common/Tools.h" #include #include #include #define CREATE_CHECKBOX(window, parentSizer, name, description, ID) \ parentSizer->Add(new wxStaticText(window, wxID_ANY, _(name)), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); \ parentSizer->Add(Tooltipped(new wxCheckBox(window, ID, wxEmptyString), _(description))); enum { ID_MapName, ID_MapDescription, ID_MapReveal, ID_MapType, ID_MapPreview, ID_MapTeams, ID_MapKW_Demo, ID_MapKW_Naval, ID_MapKW_New, ID_MapKW_Trigger, ID_RandomScript, ID_RandomSize, ID_RandomNomad, ID_RandomSeed, ID_RandomReseed, ID_RandomGenerate, ID_ResizeMap, ID_SimPlay, ID_SimFast, ID_SimSlow, ID_SimPause, ID_SimReset, ID_OpenPlayerPanel }; enum { SimInactive, SimPlaying, SimPlayingFast, SimPlayingSlow, SimPaused }; bool IsPlaying(int s) { return (s == SimPlaying || s == SimPlayingFast || s == SimPlayingSlow); } // TODO: Some of these helper things should be moved out of this file // and into shared locations // Helper function for adding tooltips static wxWindow* Tooltipped(wxWindow* window, const wxString& tip) { window->SetToolTip(tip); return window; } // Helper class for storing AtObjs class AtObjClientData : public wxClientData { public: AtObjClientData(const AtObj& obj) : obj(obj) {} virtual ~AtObjClientData() {} AtObj GetValue() { return obj; } private: AtObj obj; }; class MapSettingsControl : public wxPanel { public: MapSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor); void CreateWidgets(); void ReadFromEngine(); void SetMapSettings(const AtObj& obj); AtObj UpdateSettingsObject(); private: void SendToEngine(); void OnVictoryConditionChanged(long controlId); void OnEdit(wxCommandEvent& evt) { long id = static_cast(evt.GetId()); if (std::any_of(m_VictoryConditions.begin(), m_VictoryConditions.end(), [id](const std::pair& vc) { return vc.first == id; })) OnVictoryConditionChanged(id); SendToEngine(); } std::map m_VictoryConditions; std::set m_MapSettingsKeywords; std::set m_MapSettingsVictoryConditions; std::vector m_PlayerCivChoices; Observable& m_MapSettings; DECLARE_EVENT_TABLE(); }; BEGIN_EVENT_TABLE(MapSettingsControl, wxPanel) EVT_TEXT(ID_MapName, MapSettingsControl::OnEdit) EVT_TEXT(ID_MapDescription, MapSettingsControl::OnEdit) EVT_TEXT(ID_MapPreview, MapSettingsControl::OnEdit) EVT_CHECKBOX(wxID_ANY, MapSettingsControl::OnEdit) EVT_CHOICE(wxID_ANY, MapSettingsControl::OnEdit) END_EVENT_TABLE(); MapSettingsControl::MapSettingsControl(wxWindow* parent, ScenarioEditor& scenarioEditor) : wxPanel(parent, wxID_ANY), m_MapSettings(scenarioEditor.GetMapSettings()) { wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Map settings")); SetSizer(sizer); } void MapSettingsControl::CreateWidgets() { wxSizer* sizer = GetSizer(); ///////////////////////////////////////////////////////////////////////// // Map settings wxBoxSizer* nameSizer = new wxBoxSizer(wxHORIZONTAL); nameSizer->Add(new wxStaticText(this, wxID_ANY, _("Name")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL)); nameSizer->Add(8, 0); nameSizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapName), _("Displayed name of the map")), wxSizerFlags().Proportion(1)); sizer->Add(nameSizer, wxSizerFlags().Expand()); sizer->Add(0, 2); sizer->Add(new wxStaticText(this, wxID_ANY, _("Description"))); sizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapDescription, wxEmptyString, wxDefaultPosition, wxSize(-1, 100), wxTE_MULTILINE), _("Short description used on the map selection screen")), wxSizerFlags().Expand()); sizer->AddSpacer(5); wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5); gridSizer->AddGrowableCol(1); // TODO: have preview selector tool? gridSizer->Add(new wxStaticText(this, wxID_ANY, _("Preview")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(Tooltipped(new wxTextCtrl(this, ID_MapPreview, wxEmptyString), _("Texture used for map preview")), wxSizerFlags().Expand()); CREATE_CHECKBOX(this, gridSizer, "Reveal map", "If checked, players won't need to explore", ID_MapReveal); CREATE_CHECKBOX(this, gridSizer, "Lock teams", "If checked, teams will be locked", ID_MapTeams); sizer->Add(gridSizer, wxSizerFlags().Expand()); sizer->AddSpacer(5); wxStaticBoxSizer* victoryConditionSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Victory Conditions")); wxFlexGridSizer* vcGridSizer = new wxFlexGridSizer(2, 0, 5); vcGridSizer->AddGrowableCol(1); AtlasMessage::qGetVictoryConditionData qryVictoryCondition; qryVictoryCondition.Post(); std::vector victoryConditionData = *qryVictoryCondition.data; for (const std::string& victoryConditionJson : victoryConditionData) { AtObj victoryCondition = AtlasObject::LoadFromJSON(victoryConditionJson); long index = wxWindow::NewControlId(); wxString title = wxString::FromUTF8(victoryCondition["Data"]["Title"]); std::string escapedTitle = wxString::FromUTF8(title).Lower().ToStdString(); std::replace(escapedTitle.begin(), escapedTitle.end(), ' ', '_'); AtObj updateCondition = *(victoryCondition["Data"]["Title"]); updateCondition.setString(escapedTitle.c_str()); m_VictoryConditions.insert(std::pair(index, victoryCondition)); CREATE_CHECKBOX(this, vcGridSizer, title, "Select " + title + " victory condition.", index); } victoryConditionSizer->Add(vcGridSizer); sizer->Add(victoryConditionSizer, wxSizerFlags().Expand()); sizer->AddSpacer(5); wxStaticBoxSizer* keywordsSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Keywords")); wxFlexGridSizer* kwGridSizer = new wxFlexGridSizer(4, 5, 15); CREATE_CHECKBOX(this, kwGridSizer, "Demo", "If checked, map will only be visible using filters in game setup", ID_MapKW_Demo); CREATE_CHECKBOX(this, kwGridSizer, "Naval", "If checked, map will only be visible using filters in game setup", ID_MapKW_Naval); CREATE_CHECKBOX(this, kwGridSizer, "New", "If checked, the map will appear in the list of new maps", ID_MapKW_New); CREATE_CHECKBOX(this, kwGridSizer, "Trigger", "If checked, the map will appear in the list of maps with trigger scripts", ID_MapKW_Trigger); keywordsSizer->Add(kwGridSizer); sizer->Add(keywordsSizer, wxSizerFlags().Expand()); } void MapSettingsControl::ReadFromEngine() { AtlasMessage::qGetMapSettings qry; qry.Post(); if (!(*qry.settings).empty()) { // Prevent error if there's no map settings to parse m_MapSettings = AtlasObject::LoadFromJSON(*qry.settings); } // map name wxDynamicCast(FindWindow(ID_MapName), wxTextCtrl)->ChangeValue(wxString::FromUTF8(m_MapSettings["Name"])); // map description wxDynamicCast(FindWindow(ID_MapDescription), wxTextCtrl)->ChangeValue(wxString::FromUTF8(m_MapSettings["Description"])); // map preview wxDynamicCast(FindWindow(ID_MapPreview), wxTextCtrl)->ChangeValue(wxString::FromUTF8(m_MapSettings["Preview"])); // reveal map wxDynamicCast(FindWindow(ID_MapReveal), wxCheckBox)->SetValue(wxString::FromUTF8(m_MapSettings["RevealMap"]) == "true"); // victory conditions m_MapSettingsVictoryConditions.clear(); for (AtIter victoryCondition = m_MapSettings["VictoryConditions"]["item"]; victoryCondition.defined(); ++victoryCondition) m_MapSettingsVictoryConditions.insert(std::string(victoryCondition)); // Clear Checkboxes before loading data. We don't update victory condition just yet because it might reenable some of the checkboxes. - for (const std::pair& vc : m_VictoryConditions) + for (const std::pair& vc : m_VictoryConditions) { wxCheckBox* checkBox = wxDynamicCast(FindWindow(vc.first), wxCheckBox); if (!checkBox) continue; checkBox->SetValue(false); checkBox->Enable(true); } - for (const std::pair& vc : m_VictoryConditions) + for (const std::pair& vc : m_VictoryConditions) { std::string escapedTitle = wxString::FromUTF8(vc.second["Data"]["Title"]).Lower().ToStdString(); std::replace(escapedTitle.begin(), escapedTitle.end(), ' ', '_'); if (m_MapSettingsVictoryConditions.find(escapedTitle) == m_MapSettingsVictoryConditions.end()) continue; wxCheckBox* checkBox = wxDynamicCast(FindWindow(vc.first), wxCheckBox); if (!checkBox) continue; checkBox->SetValue(true); OnVictoryConditionChanged(vc.first); } // lock teams wxDynamicCast(FindWindow(ID_MapTeams), wxCheckBox)->SetValue(wxString::FromUTF8(m_MapSettings["LockTeams"]) == "true"); // keywords { m_MapSettingsKeywords.clear(); for (AtIter keyword = m_MapSettings["Keywords"]["item"]; keyword.defined(); ++keyword) m_MapSettingsKeywords.insert(std::string(keyword)); wxWindow* window; #define INIT_CHECKBOX(ID, mapSettings, value) \ window = FindWindow(ID); \ if (window != nullptr) \ wxDynamicCast(window, wxCheckBox)->SetValue(mapSettings.count(value) != 0); INIT_CHECKBOX(ID_MapKW_Demo, m_MapSettingsKeywords, "demo"); INIT_CHECKBOX(ID_MapKW_Naval, m_MapSettingsKeywords, "naval"); INIT_CHECKBOX(ID_MapKW_New, m_MapSettingsKeywords, "new"); INIT_CHECKBOX(ID_MapKW_Trigger, m_MapSettingsKeywords, "trigger"); #undef INIT_CHECKBOX } } void MapSettingsControl::SetMapSettings(const AtObj& obj) { m_MapSettings = obj; m_MapSettings.NotifyObservers(); SendToEngine(); } void MapSettingsControl::OnVictoryConditionChanged(long controlId) { AtObj victoryCondition; wxCheckBox* modifiedCheckbox = wxDynamicCast(FindWindow(controlId), wxCheckBox); - for (const std::pair& vc : m_VictoryConditions) + for (const std::pair& vc : m_VictoryConditions) { - if(vc.first != controlId) + if (vc.first != controlId) continue; - victoryCondition = vc.second; + victoryCondition = vc.second; break; } - if(modifiedCheckbox->GetValue()) + if (modifiedCheckbox->GetValue()) { for (AtIter victoryConditionPair = victoryCondition["Data"]["ChangeOnChecked"]; victoryConditionPair.defined(); ++victoryConditionPair) { - for (const std::pair& vc : m_VictoryConditions) + for (const std::pair& vc : m_VictoryConditions) { std::string escapedTitle = wxString::FromUTF8(vc.second["Data"]["Title"]).Lower().ToStdString(); std::replace(escapedTitle.begin(), escapedTitle.end(), ' ', '_'); if (victoryConditionPair[escapedTitle.c_str()].defined()) { wxCheckBox* victoryConditionCheckBox = wxDynamicCast(FindWindow(vc.first), wxCheckBox); victoryConditionCheckBox->SetValue(wxString::FromUTF8(victoryConditionPair[escapedTitle.c_str()]).Lower().ToStdString() == "true"); } } } } - for (const std::pair& vc : m_VictoryConditions) + for (const std::pair& vc : m_VictoryConditions) { if (vc.first == controlId) continue; wxCheckBox* otherCheckbox = wxDynamicCast(FindWindow(vc.first), wxCheckBox); otherCheckbox->Enable(true); - for (const std::pair& vc2 : m_VictoryConditions) + for (const std::pair& vc2 : m_VictoryConditions) { for (AtIter victoryConditionTitle = vc2.second["Data"]["DisabledWhenChecked"]; victoryConditionTitle.defined(); ++victoryConditionTitle) { std::string escapedTitle = wxString::FromUTF8(vc.second["Data"]["Title"]).Lower().ToStdString(); std::replace(escapedTitle.begin(), escapedTitle.end(), ' ', '_'); if (escapedTitle == wxString::FromUTF8(victoryConditionTitle["item"]).ToStdString() && wxDynamicCast(FindWindow(vc2.first), wxCheckBox)->GetValue()) { otherCheckbox->Enable(false); otherCheckbox->SetValue(false); break; } } } } } AtObj MapSettingsControl::UpdateSettingsObject() { // map name m_MapSettings.set("Name", wxDynamicCast(FindWindow(ID_MapName), wxTextCtrl)->GetValue().utf8_str()); // map description m_MapSettings.set("Description", wxDynamicCast(FindWindow(ID_MapDescription), wxTextCtrl)->GetValue().utf8_str()); // map preview m_MapSettings.set("Preview", wxDynamicCast(FindWindow(ID_MapPreview), wxTextCtrl)->GetValue().utf8_str()); // reveal map m_MapSettings.setBool("RevealMap", wxDynamicCast(FindWindow(ID_MapReveal), wxCheckBox)->GetValue()); // victory conditions #define INSERT_VICTORY_CONDITION_CHECKBOX(name, ID) \ if (wxDynamicCast(FindWindow(ID), wxCheckBox)->GetValue()) \ m_MapSettingsVictoryConditions.insert(name); \ else \ m_MapSettingsVictoryConditions.erase(name); - for (const std::pair& vc : m_VictoryConditions) + for (const std::pair& vc : m_VictoryConditions) { std::string escapedTitle = wxString::FromUTF8(vc.second["Data"]["Title"]).Lower().ToStdString(); std::replace(escapedTitle.begin(), escapedTitle.end(), ' ', '_'); INSERT_VICTORY_CONDITION_CHECKBOX(escapedTitle, vc.first) } #undef INSERT_VICTORY_CONDITION_CHECKBOX AtObj victoryConditions; victoryConditions.set("@array", ""); for (const std::string& victoryCondition : m_MapSettingsVictoryConditions) victoryConditions.add("item", victoryCondition.c_str()); m_MapSettings.set("VictoryConditions", victoryConditions); // keywords { #define INSERT_KEYWORDS_CHECKBOX(name, ID) \ if (wxDynamicCast(FindWindow(ID), wxCheckBox)->GetValue()) \ m_MapSettingsKeywords.insert(name); \ else \ m_MapSettingsKeywords.erase(name); INSERT_KEYWORDS_CHECKBOX("demo", ID_MapKW_Demo); INSERT_KEYWORDS_CHECKBOX("naval", ID_MapKW_Naval); INSERT_KEYWORDS_CHECKBOX("new", ID_MapKW_New); INSERT_KEYWORDS_CHECKBOX("trigger", ID_MapKW_Trigger); #undef INSERT_KEYWORDS_CHECKBOX AtObj keywords; keywords.set("@array", ""); for (const std::string& keyword : m_MapSettingsKeywords) keywords.add("item", keyword.c_str()); m_MapSettings.set("Keywords", keywords); } // teams locked m_MapSettings.setBool("LockTeams", wxDynamicCast(FindWindow(ID_MapTeams), wxCheckBox)->GetValue()); // default AI RNG seed m_MapSettings.setInt("AISeed", 0); return m_MapSettings; } void MapSettingsControl::SendToEngine() { UpdateSettingsObject(); std::string json = AtlasObject::SaveToJSON(m_MapSettings); // TODO: would be nice if we supported undo for settings changes POST_COMMAND(SetMapSettings, (json)); } MapSidebar::MapSidebar(ScenarioEditor& scenarioEditor, wxWindow* sidebarContainer, wxWindow* bottomBarContainer) : Sidebar(scenarioEditor, sidebarContainer, bottomBarContainer), m_SimState(SimInactive) { wxSizer* scrollSizer = new wxBoxSizer(wxVERTICAL); wxScrolledWindow* scrolledWindow = new wxScrolledWindow(this); scrolledWindow->SetScrollRate(10, 10); scrolledWindow->SetSizer(scrollSizer); m_MainSizer->Add(scrolledWindow, wxSizerFlags().Expand().Proportion(1)); m_MapSettingsCtrl = new MapSettingsControl(scrolledWindow, m_ScenarioEditor); scrollSizer->Add(m_MapSettingsCtrl, wxSizerFlags().Expand()); { ///////////////////////////////////////////////////////////////////////// // Random map settings wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Random map")); scrollSizer->Add(sizer, wxSizerFlags().Expand()); sizer->Add(new wxChoice(scrolledWindow, ID_RandomScript), wxSizerFlags().Expand()); sizer->AddSpacer(5); sizer->Add(new wxButton(scrolledWindow, ID_OpenPlayerPanel, _T("Change players")), wxSizerFlags().Expand()); sizer->AddSpacer(5); wxFlexGridSizer* gridSizer = new wxFlexGridSizer(2, 5, 5); gridSizer->AddGrowableCol(1); wxChoice* sizeChoice = new wxChoice(scrolledWindow, ID_RandomSize); gridSizer->Add(new wxStaticText(scrolledWindow, wxID_ANY, _("Map size")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); gridSizer->Add(sizeChoice, wxSizerFlags().Expand()); CREATE_CHECKBOX(scrolledWindow, gridSizer, "Nomad", "Place only some units instead of starting bases.", ID_RandomNomad); gridSizer->Add(new wxStaticText(scrolledWindow, wxID_ANY, _("Random seed")), wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); wxBoxSizer* seedSizer = new wxBoxSizer(wxHORIZONTAL); seedSizer->Add(Tooltipped(new wxTextCtrl(scrolledWindow, ID_RandomSeed, _T("0"), wxDefaultPosition, wxDefaultSize, 0, wxTextValidator(wxFILTER_NUMERIC)), _("Seed value for random map")), wxSizerFlags(1).Expand()); seedSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_RandomReseed, _("R"), wxDefaultPosition, wxSize(40, -1)), _("New random seed"))); gridSizer->Add(seedSizer, wxSizerFlags().Expand()); sizer->Add(gridSizer, wxSizerFlags().Expand()); sizer->AddSpacer(5); sizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_RandomGenerate, _("Generate map")), _("Run selected random map script")), wxSizerFlags().Expand()); } { ///////////////////////////////////////////////////////////////////////// // Misc tools wxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Misc tools")); sizer->Add(new wxButton(scrolledWindow, ID_ResizeMap, _("Resize/Recenter map")), wxSizerFlags().Expand()); scrollSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 10)); } { ///////////////////////////////////////////////////////////////////////// // Simulation buttons wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, scrolledWindow, _("Simulation test")); scrollSizer->Add(sizer, wxSizerFlags().Expand().Border(wxTOP, 8)); wxGridSizer* gridSizer = new wxGridSizer(5); gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimPlay, _("Play"), wxDefaultPosition, wxSize(48, -1)), _("Run the simulation at normal speed")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimFast, _("Fast"), wxDefaultPosition, wxSize(48, -1)), _("Run the simulation at 8x speed")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimSlow, _("Slow"), wxDefaultPosition, wxSize(48, -1)), _("Run the simulation at 1/8x speed")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimPause, _("Pause"), wxDefaultPosition, wxSize(48, -1)), _("Pause the simulation")), wxSizerFlags().Expand()); gridSizer->Add(Tooltipped(new wxButton(scrolledWindow, ID_SimReset, _("Reset"), wxDefaultPosition, wxSize(48, -1)), _("Reset the editor to initial state")), wxSizerFlags().Expand()); sizer->Add(gridSizer, wxSizerFlags().Expand()); UpdateSimButtons(); } } void MapSidebar::OnCollapse(wxCollapsiblePaneEvent& WXUNUSED(evt)) { Freeze(); // Toggling the collapsing doesn't seem to update the sidebar layout // automatically, so do it explicitly here Layout(); Refresh(); // fixes repaint glitch on Windows Thaw(); } void MapSidebar::OnFirstDisplay() { // We do this here becase messages are used which requires simulation to be init'd m_MapSettingsCtrl->CreateWidgets(); m_MapSettingsCtrl->ReadFromEngine(); // Load the map sizes list AtlasMessage::qGetMapSizes qrySizes; qrySizes.Post(); AtObj sizes = AtlasObject::LoadFromJSON(*qrySizes.sizes); wxChoice* sizeChoice = wxDynamicCast(FindWindow(ID_RandomSize), wxChoice); for (AtIter s = sizes["Data"]["item"]; s.defined(); ++s) sizeChoice->Append(wxString::FromUTF8(s["Name"]), reinterpret_cast((*s["Tiles"]).getLong())); sizeChoice->SetSelection(0); // Load the RMS script list AtlasMessage::qGetRMSData qry; qry.Post(); std::vector scripts = *qry.data; wxChoice* scriptChoice = wxDynamicCast(FindWindow(ID_RandomScript), wxChoice); scriptChoice->Clear(); for (size_t i = 0; i < scripts.size(); ++i) { AtObj data = AtlasObject::LoadFromJSON(scripts[i]); wxString name = wxString::FromUTF8(data["settings"]["Name"]); if (!name.IsEmpty()) scriptChoice->Append(name, new AtObjClientData(*data["settings"])); } scriptChoice->SetSelection(0); Layout(); } void MapSidebar::OnMapReload() { m_MapSettingsCtrl->ReadFromEngine(); // Reset sim test buttons POST_MESSAGE(SimPlay, (0.f, false)); POST_MESSAGE(SimStopMusic, ()); POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml")); m_SimState = SimInactive; UpdateSimButtons(); } void MapSidebar::UpdateSimButtons() { wxButton* button; button = wxDynamicCast(FindWindow(ID_SimPlay), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimPlaying); button = wxDynamicCast(FindWindow(ID_SimFast), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimPlayingFast); button = wxDynamicCast(FindWindow(ID_SimSlow), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimPlayingSlow); button = wxDynamicCast(FindWindow(ID_SimPause), wxButton); wxCHECK(button, ); button->Enable(IsPlaying(m_SimState)); button = wxDynamicCast(FindWindow(ID_SimReset), wxButton); wxCHECK(button, ); button->Enable(m_SimState != SimInactive); } void MapSidebar::OnSimPlay(wxCommandEvent& event) { float speed = 1.f; int newState = SimPlaying; if (event.GetId() == ID_SimFast) { speed = 8.f; newState = SimPlayingFast; } else if (event.GetId() == ID_SimSlow) { speed = 0.125f; newState = SimPlayingSlow; } if (m_SimState == SimInactive) { // Force update of player settings POST_MESSAGE(LoadPlayerSettings, (false)); POST_MESSAGE(SimStateSave, (L"default")); POST_MESSAGE(GuiSwitchPage, (L"page_session.xml")); POST_MESSAGE(SimPlay, (speed, true)); m_SimState = newState; } else // paused or already playing at a different speed { POST_MESSAGE(SimPlay, (speed, true)); m_SimState = newState; } UpdateSimButtons(); } void MapSidebar::OnSimPause(wxCommandEvent& WXUNUSED(event)) { if (IsPlaying(m_SimState)) { POST_MESSAGE(SimPlay, (0.f, true)); m_SimState = SimPaused; } UpdateSimButtons(); } void MapSidebar::OnSimReset(wxCommandEvent& WXUNUSED(event)) { if (IsPlaying(m_SimState)) { POST_MESSAGE(SimPlay, (0.f, true)); POST_MESSAGE(SimStateRestore, (L"default")); POST_MESSAGE(SimStopMusic, ()); POST_MESSAGE(SimPlay, (0.f, false)); POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml")); m_SimState = SimInactive; } else if (m_SimState == SimPaused) { POST_MESSAGE(SimPlay, (0.f, true)); POST_MESSAGE(SimStateRestore, (L"default")); POST_MESSAGE(SimStopMusic, ()); POST_MESSAGE(SimPlay, (0.f, false)); POST_MESSAGE(GuiSwitchPage, (L"page_atlas.xml")); m_SimState = SimInactive; } UpdateSimButtons(); } void MapSidebar::OnRandomReseed(wxCommandEvent& WXUNUSED(evt)) { // Pick a shortish randomish value wxString seed; seed << (int)floor((rand() / (float)RAND_MAX) * 10000.f); wxDynamicCast(FindWindow(ID_RandomSeed), wxTextCtrl)->SetValue(seed); } void MapSidebar::OnRandomGenerate(wxCommandEvent& WXUNUSED(evt)) { if (m_ScenarioEditor.DiscardChangesDialog()) return; wxChoice* scriptChoice = wxDynamicCast(FindWindow(ID_RandomScript), wxChoice); if (scriptChoice->GetSelection() < 0) return; // TODO: this settings thing seems a bit of a mess, // since it's mixing data from three different sources AtObj settings = m_MapSettingsCtrl->UpdateSettingsObject(); AtObj scriptSettings = dynamic_cast(scriptChoice->GetClientObject(scriptChoice->GetSelection()))->GetValue(); settings.addOverlay(scriptSettings); wxChoice* sizeChoice = wxDynamicCast(FindWindow(ID_RandomSize), wxChoice); wxString size; size << (intptr_t)sizeChoice->GetClientData(sizeChoice->GetSelection()); settings.setInt("Size", wxAtoi(size)); settings.setBool("Nomad", wxDynamicCast(FindWindow(ID_RandomNomad), wxCheckBox)->GetValue()); settings.setInt("Seed", wxAtoi(wxDynamicCast(FindWindow(ID_RandomSeed), wxTextCtrl)->GetValue())); std::string json = AtlasObject::SaveToJSON(settings); wxBusyInfo busy(_("Generating map")); wxBusyCursor busyc; wxString scriptName = wxString::FromUTF8(settings["Script"]); // Copy the old map settings, so we don't lose them if the map generation fails AtObj oldSettings = settings; AtlasMessage::qGenerateMap qry((std::wstring)scriptName.wc_str(), json); qry.Post(); if (qry.status < 0) { // Display error message and revert to old map settings wxLogError(_("Random map script '%s' failed"), scriptName.c_str()); m_MapSettingsCtrl->SetMapSettings(oldSettings); } m_ScenarioEditor.NotifyOnMapReload(); } void MapSidebar::OnOpenPlayerPanel(wxCommandEvent& WXUNUSED(evt)) { m_ScenarioEditor.SelectPage(_T("PlayerSidebar")); } void MapSidebar::OnResizeMap(wxCommandEvent& WXUNUSED(evt)) { MapResizeDialog dlg(this); if (dlg.ShowModal() != wxID_OK) return; wxPoint offset = dlg.GetOffset(); POST_COMMAND(ResizeMap, (dlg.GetNewSize(), offset.x, offset.y)); } BEGIN_EVENT_TABLE(MapSidebar, Sidebar) EVT_COLLAPSIBLEPANE_CHANGED(wxID_ANY, MapSidebar::OnCollapse) EVT_BUTTON(ID_SimPlay, MapSidebar::OnSimPlay) EVT_BUTTON(ID_SimFast, MapSidebar::OnSimPlay) EVT_BUTTON(ID_SimSlow, MapSidebar::OnSimPlay) EVT_BUTTON(ID_SimPause, MapSidebar::OnSimPause) EVT_BUTTON(ID_SimReset, MapSidebar::OnSimReset) EVT_BUTTON(ID_RandomReseed, MapSidebar::OnRandomReseed) EVT_BUTTON(ID_RandomGenerate, MapSidebar::OnRandomGenerate) EVT_BUTTON(ID_ResizeMap, MapSidebar::OnResizeMap) EVT_BUTTON(ID_OpenPlayerPanel, MapSidebar::OnOpenPlayerPanel) END_EVENT_TABLE(); Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/CinemaHandler.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/CinemaHandler.cpp (revision 24486) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/CinemaHandler.cpp (revision 24487) @@ -1,544 +1,544 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 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 "../GameLoop.h" #include "../View.h" #include "graphics/Camera.h" #include "graphics/CinemaManager.h" #include "graphics/GameView.h" #include "ps/Game.h" #include "ps/CStr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "maths/Vector2D.h" #include "maths/Vector3D.h" #include "lib/res/graphics/ogl_tex.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpCinemaManager.h" namespace AtlasMessage { const float MINIMAL_SCREEN_DISTANCE = 5.f; sCinemaPath ConstructCinemaPath(const CCinemaPath* source) { sCinemaPath path; const CCinemaData* data = source->GetData(); //path.mode = data->m_Mode; //path.style = data->m_Style; path.growth = data->m_Growth; path.timescale = data->m_Timescale.ToFloat(); path.change = data->m_Switch; return path; } CCinemaData ConstructCinemaData(const sCinemaPath& path) { CCinemaData data; data.m_Growth = data.m_GrowthCount = path.growth; data.m_Switch = path.change; //data.m_Mode = path.mode; //data.m_Style = path.style; return data; } sCinemaSplineNode ConstructCinemaNode(const SplineData& data) { sCinemaSplineNode node; node.px = data.Position.X.ToFloat(); node.py = data.Position.Y.ToFloat(); node.pz = data.Position.Z.ToFloat(); node.rx = data.Rotation.X.ToFloat(); node.ry = data.Rotation.Y.ToFloat(); node.rz = data.Rotation.Z.ToFloat(); node.t = data.Distance.ToFloat(); return node; } std::vector GetCurrentPaths() { std::vector atlasPaths; CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return atlasPaths; const std::map& paths = cmpCinemaManager->GetPaths(); for ( std::map::const_iterator it=paths.begin(); it!=paths.end(); ++it ) { sCinemaPath path = ConstructCinemaPath(&it->second); path.name = it->first; const std::vector& nodes = it->second.GetAllNodes(); std::vector atlasNodes; for ( size_t i=0; i 2 ) { for ( size_t i=atlasNodes.size()-2; i>0; --i ) atlasNodes[i].t = atlasNodes[i-1].t; } atlasNodes.back().t = atlasNodes.front().t; atlasNodes.front().t = back; } path.nodes = atlasNodes; atlasPaths.push_back(path); } return atlasPaths; } void SetCurrentPaths(const std::vector& atlasPaths) { std::map paths; for ( std::vector::const_iterator it=atlasPaths.begin(); it!=atlasPaths.end(); ++it ) { CStrW pathName(*it->name); paths[pathName] = CCinemaPath(); paths[pathName].SetTimescale(fixed::FromFloat(it->timescale)); const sCinemaPath& atlasPath = *it; const std::vector nodes = *atlasPath.nodes; TNSpline spline; CCinemaData data = ConstructCinemaData(atlasPath); for ( size_t j=0; j cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpCinemaManager) cmpCinemaManager->SetPaths(paths); } QUERYHANDLER(GetCameraInfo) { sCameraInfo info; const CMatrix3D& cameraOrientation = g_Game->GetView()->GetCamera()->GetOrientation(); CQuaternion quatRot = cameraOrientation.GetRotation(); quatRot.Normalize(); CVector3D rotation = quatRot.ToEulerAngles(); rotation.X = RADTODEG(rotation.X); rotation.Y = RADTODEG(rotation.Y); rotation.Z = RADTODEG(rotation.Z); CVector3D translation = cameraOrientation.GetTranslation(); info.pX = translation.X; info.pY = translation.Y; info.pZ = translation.Z; info.rX = rotation.X; info.rY = rotation.Y; info.rZ = rotation.Z; msg->info = info; } MESSAGEHANDLER(CinemaEvent) { CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return; if (msg->mode == eCinemaEventMode::SMOOTH) { cmpCinemaManager->AddCinemaPathToQueue(*msg->path); } else if ( msg->mode == eCinemaEventMode::RESET ) { // g_Game->GetView()->ResetCamera(); } else ENSURE(false); } BEGIN_COMMAND(AddCinemaPath) { void Do() { CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return; CCinemaData pathData; pathData.m_Name = *msg->pathName; pathData.m_Timescale = fixed::FromInt(1); pathData.m_Orientation = L"target"; pathData.m_Mode = L"ease_inout"; pathData.m_Style = L"default"; CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); CFixedVector3D target( fixed::FromFloat(focus.X), fixed::FromFloat(focus.Y), fixed::FromFloat(focus.Z) ); CVector3D camera = g_Game->GetView()->GetCamera()->GetOrientation().GetTranslation(); CFixedVector3D position( fixed::FromFloat(camera.X), fixed::FromFloat(camera.Y), fixed::FromFloat(camera.Z) ); TNSpline positionSpline; positionSpline.AddNode(position, CFixedVector3D(), fixed::FromInt(0)); TNSpline targetSpline; targetSpline.AddNode(target, CFixedVector3D(), fixed::FromInt(0)); cmpCinemaManager->AddPath(CCinemaPath(pathData, positionSpline, targetSpline)); } void Redo() { } void Undo() { } }; END_COMMAND(AddCinemaPath) BEGIN_COMMAND(DeleteCinemaPath) { void Do() { CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return; cmpCinemaManager->DeletePath(*msg->pathName); } void Redo() { } void Undo() { } }; END_COMMAND(DeleteCinemaPath) BEGIN_COMMAND(SetCinemaPaths) { std::vector m_oldPaths, m_newPaths; void Do() { m_oldPaths = GetCurrentPaths(); m_newPaths = *msg->paths; Redo(); } void Redo() { SetCurrentPaths(m_newPaths); } void Undo() { SetCurrentPaths(m_oldPaths); } }; END_COMMAND(SetCinemaPaths) BEGIN_COMMAND(SetCinemaPathsDrawing) { void Do() { if (g_Game && g_Game->GetView() && g_Game->GetView()->GetCinema()) g_Game->GetView()->GetCinema()->SetPathsDrawing(msg->drawPaths); } void Redo() { } void Undo() { } }; END_COMMAND(SetCinemaPathsDrawing) static CVector3D GetNearestPointToScreenCoords(const CVector3D& base, const CVector3D& dir, const CVector2D& screen, float lower = -1e5, float upper = 1e5) { // It uses a ternary search, because an intersection of cylinders is the complex task for (int i = 0; i < 64; ++i) { float delta = (upper - lower) / 3.0; float middle1 = lower + delta, middle2 = lower + 2.0f * delta; CVector3D p1 = base + dir * middle1, p2 = base + dir * middle2; CVector2D s1, s2; g_Game->GetView()->GetCamera()->GetScreenCoordinates(p1, s1.X, s1.Y); g_Game->GetView()->GetCamera()->GetScreenCoordinates(p2, s2.X, s2.Y); if ((s1 - screen).Length() < (s2 - screen).Length()) upper = middle2; else lower = middle1; } return base + dir * upper; } #define GET_PATH_NODE_WITH_VALIDATION() \ int index = msg->node->index; \ if (index < 0) \ return; \ CStrW name = *msg->node->name; \ CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); \ if (!cmpCinemaManager || !cmpCinemaManager->HasPath(name)) \ return; \ const CCinemaPath& path = cmpCinemaManager->GetPaths().find(name)->second; \ if (!msg->node->targetNode) \ { \ if (index >= (int)path.GetAllNodes().size()) \ return; \ } \ else \ { \ if (index >= (int)path.GetTargetSpline().GetAllNodes().size()) \ return; \ } BEGIN_COMMAND(AddPathNode) { void Do() { GET_PATH_NODE_WITH_VALIDATION(); CCinemaData data = *path.GetData(); TNSpline positionSpline = path; TNSpline targetSpline = path.GetTargetSpline(); TNSpline& spline = msg->node->targetNode ? targetSpline : positionSpline; CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); CFixedVector3D target( fixed::FromFloat(focus.X), fixed::FromFloat(focus.Y), fixed::FromFloat(focus.Z) ); spline.InsertNode(index + 1, target, CFixedVector3D(), fixed::FromInt(1)); spline.BuildSpline(); cmpCinemaManager->DeletePath(name); cmpCinemaManager->AddPath(CCinemaPath(data, positionSpline, targetSpline)); } void Redo() { } void Undo() { } }; END_COMMAND(AddPathNode) BEGIN_COMMAND(DeletePathNode) { void Do() { GET_PATH_NODE_WITH_VALIDATION(); CCinemaData data = *path.GetData(); TNSpline positionSpline = path; TNSpline targetSpline = path.GetTargetSpline(); TNSpline& spline = msg->node->targetNode ? targetSpline : positionSpline; if (spline.GetAllNodes().size() <= 1) return; spline.RemoveNode(index); spline.BuildSpline(); cmpCinemaManager->DeletePath(name); cmpCinemaManager->AddPath(CCinemaPath(data, positionSpline, targetSpline)); g_AtlasGameLoop->view->SetParam(L"movetool", false); } void Redo() { } void Undo() { } }; END_COMMAND(DeletePathNode) BEGIN_COMMAND(MovePathNode) { void Do() { int axis = msg->axis; if (axis == AXIS_INVALID) return; GET_PATH_NODE_WITH_VALIDATION(); CCinemaData data = *path.GetData(); TNSpline positionSpline = path; TNSpline targetSpline = path.GetTargetSpline(); TNSpline& spline = msg->node->targetNode ? targetSpline : positionSpline; // Get shift of the tool by the cursor movement CFixedVector3D pos = spline.GetAllNodes()[index].Position; CVector3D position( pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat() ); CVector3D axisDirection(axis & AXIS_X, axis & AXIS_Y, axis & AXIS_Z); CVector2D from, to; msg->from->GetScreenSpace(from.X, from.Y); msg->to->GetScreenSpace(to.X, to.Y); CVector3D shift( GetNearestPointToScreenCoords(position, axisDirection, to) - GetNearestPointToScreenCoords(position, axisDirection, from) ); // Change, rebuild and update the path position += shift; pos += CFixedVector3D( fixed::FromFloat(shift.X), fixed::FromFloat(shift.Y), fixed::FromFloat(shift.Z) ); spline.UpdateNodePos(index, pos); spline.BuildSpline(); cmpCinemaManager->DeletePath(name); cmpCinemaManager->AddPath(CCinemaPath(data, positionSpline, targetSpline)); // Update visual tool coordinates g_AtlasGameLoop->view->SetParam(L"movetool_x", position.X); g_AtlasGameLoop->view->SetParam(L"movetool_y", position.Y); g_AtlasGameLoop->view->SetParam(L"movetool_z", position.Z); } void Redo() { } void Undo() { } }; END_COMMAND(MovePathNode) QUERYHANDLER(GetCinemaPaths) { msg->paths = GetCurrentPaths(); } static bool isPathNodePicked(const TNSpline& spline, const CVector2D& cursor, AtlasMessage::sCinemaPathNode& node, bool targetNode) { for (size_t i = 0; i < spline.GetAllNodes().size(); ++i) { const SplineData& data = spline.GetAllNodes()[i]; CVector3D position( data.Position.X.ToFloat(), data.Position.Y.ToFloat(), data.Position.Z.ToFloat() ); CVector2D screen_pos; g_Game->GetView()->GetCamera()->GetScreenCoordinates(position, screen_pos.X, screen_pos.Y); if ((screen_pos - cursor).Length() < MINIMAL_SCREEN_DISTANCE) { node.index = i; node.targetNode = targetNode; g_AtlasGameLoop->view->SetParam(L"movetool", true); g_AtlasGameLoop->view->SetParam(L"movetool_x", position.X); g_AtlasGameLoop->view->SetParam(L"movetool_y", position.Y); g_AtlasGameLoop->view->SetParam(L"movetool_z", position.Z); return true; } } return false; } QUERYHANDLER(PickPathNode) { AtlasMessage::sCinemaPathNode node; CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) { msg->node = node; return; } CVector2D cursor; msg->pos->GetScreenSpace(cursor.X, cursor.Y); - for (const std::pair& p : cmpCinemaManager->GetPaths()) + for (const std::pair& p : cmpCinemaManager->GetPaths()) { - const CCinemaPath& path = p.second; + const CCinemaPath& path = p.second; if (isPathNodePicked(path, cursor, node, false) || isPathNodePicked(path.GetTargetSpline(), cursor, node, true)) { node.name = path.GetName(); msg->node = node; return; } } msg->node = node; g_AtlasGameLoop->view->SetParam(L"movetool", false); } static bool isAxisPicked(const CVector3D& base, const CVector3D& direction, float length, const CVector2D& cursor) { CVector3D position = GetNearestPointToScreenCoords(base, direction, cursor, 0, length); CVector2D screen_position; g_Game->GetView()->GetCamera()->GetScreenCoordinates(position, screen_position.X, screen_position.Y); return (cursor - screen_position).Length() < MINIMAL_SCREEN_DISTANCE; } QUERYHANDLER(PickAxis) { msg->axis = AXIS_INVALID; GET_PATH_NODE_WITH_VALIDATION(); const TNSpline& spline = msg->node->targetNode ? path.GetTargetSpline() : path; CFixedVector3D pos = spline.GetAllNodes()[index].Position; CVector3D position(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()); CVector3D camera = g_Game->GetView()->GetCamera()->GetOrientation().GetTranslation(); float scale = (position - camera).Length() / 10.0; CVector2D cursor; msg->pos->GetScreenSpace(cursor.X, cursor.Y); if (isAxisPicked(position, CVector3D(1, 0, 0), scale, cursor)) msg->axis = AXIS_X; else if (isAxisPicked(position, CVector3D(0, 1, 0), scale, cursor)) msg->axis = AXIS_Y; else if (isAxisPicked(position, CVector3D(0, 0, 1), scale, cursor)) msg->axis = AXIS_Z; } MESSAGEHANDLER(ClearPathNodePreview) { UNUSED2(msg); g_AtlasGameLoop->view->SetParam(L"movetool", false); } } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 24486) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 24487) @@ -1,643 +1,642 @@ /* Copyright (C) 2020 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 "../GameLoop.h" #include "../MessagePasser.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MapIO.h" #include "graphics/MapWriter.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "gui/ObjectTypes/CMiniMap.h" #include "lib/bits.h" #include "lib/file/vfs/vfs_path.h" #include "lib/status.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Loader.h" #include "ps/Shapes.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/system/ParamNode.h" #ifdef _MSC_VER # pragma warning(disable: 4458) // Declaration hides class member. #endif namespace { void InitGame() { if (g_Game) { delete g_Game; g_Game = NULL; } g_Game = new CGame(false); // Default to player 1 for playtesting g_Game->SetPlayerID(1); } void StartGame(JS::MutableHandleValue attrs) { g_Game->StartGame(attrs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); // Disable fog-of-war - this must be done before starting the game, // as visual actors cache their visibility state on first render. CmpPtr cmpRangeManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(-1, true); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } } namespace AtlasMessage { QUERYHANDLER(GenerateMap) { try { InitGame(); // Random map const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); scriptInterface.ParseJSON(*msg->settings, &settings); scriptInterface.SetProperty(settings, "mapType", "random"); JS::RootedValue attrs(rq.cx); ScriptInterface::CreateObject( rq, &attrs, "mapType", "random", "script", *msg->filename, "settings", settings); StartGame(&attrs); msg->status = 0; } catch (PSERROR_Game_World_MapLoadFailed&) { // Cancel loading LDR_Cancel(); // Since map generation failed and we don't know why, use the blank map as a fallback InitGame(); const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); // Set up 8-element array of empty objects to satisfy init JS::RootedValue playerData(rq.cx); ScriptInterface::CreateArray(rq, &playerData); for (int i = 0; i < 8; ++i) { JS::RootedValue player(rq.cx); ScriptInterface::CreateObject(rq, &player); scriptInterface.SetPropertyInt(playerData, i, player); } JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "mapType", "scenario", "PlayerData", playerData); JS::RootedValue attrs(rq.cx); ScriptInterface::CreateObject( rq, &attrs, "mapType", "scenario", "map", "maps/scenarios/_default", "settings", settings); StartGame(&attrs); msg->status = -1; } } MESSAGEHANDLER(LoadMap) { InitGame(); const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); // Scenario CStrW map = *msg->filename; CStrW mapBase = map.BeforeLast(L".pmp"); // strip the file extension, if any JS::RootedValue attrs(rq.cx); ScriptInterface::CreateObject( rq, &attrs, "mapType", "scenario", "map", mapBase); StartGame(&attrs); } MESSAGEHANDLER(ImportHeightmap) { std::vector heightmap_source; if (LoadHeightmapImageOs(*msg->filename, heightmap_source) != INFO::OK) { LOGERROR("Failed to decode heightmap."); return; } // resize terrain to heightmap size // Notice that the number of tiles/pixels per side of the heightmap image is // one less than the number of vertices per side of the heightmap. CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); const ssize_t newSize = (sqrt(heightmap_source.size()) - 1) / PATCH_SIZE; const ssize_t offset = (newSize - terrain->GetPatchesPerSide()) / 2; terrain->ResizeAndOffset(newSize, offset, offset); // copy heightmap data into map u16* heightmap = g_Game->GetWorld()->GetTerrain()->GetHeightMap(); ENSURE(heightmap_source.size() == (std::size_t) SQR(g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide())); std::copy(heightmap_source.begin(), heightmap_source.end(), heightmap); // update simulation CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); g_Game->GetView()->GetLOSTexture().MakeDirty(); } MESSAGEHANDLER(SaveMap) { CMapWriter writer; VfsPath pathname = VfsPath(*msg->filename).ChangeExtension(L".pmp"); writer.SaveMap(pathname, g_Game->GetWorld()->GetTerrain(), g_Renderer.GetWaterManager(), g_Renderer.GetSkyManager(), &g_LightEnv, g_Game->GetView()->GetCamera(), g_Game->GetView()->GetCinema(), &g_Renderer.GetPostprocManager(), g_Game->GetSimulation2()); } QUERYHANDLER(GetMapSettings) { msg->settings = g_Game->GetSimulation2()->GetMapSettingsString(); } BEGIN_COMMAND(SetMapSettings) { std::string m_OldSettings, m_NewSettings; void SetSettings(const std::string& settings) { g_Game->GetSimulation2()->SetMapSettings(settings); } void Do() { m_OldSettings = g_Game->GetSimulation2()->GetMapSettingsString(); m_NewSettings = *msg->settings; SetSettings(m_NewSettings); } // TODO: we need some way to notify the Atlas UI when the settings are changed // externally, otherwise this will have no visible effect void Undo() { // SetSettings(m_OldSettings); } void Redo() { // SetSettings(m_NewSettings); } void MergeIntoPrevious(cSetMapSettings* prev) { prev->m_NewSettings = m_NewSettings; } }; END_COMMAND(SetMapSettings) MESSAGEHANDLER(LoadPlayerSettings) { g_Game->GetSimulation2()->LoadPlayerSettings(msg->newplayers); } QUERYHANDLER(GetMapSizes) { msg->sizes = g_Game->GetSimulation2()->GetMapSizes(); } QUERYHANDLER(RasterizeMinimap) { // TODO: remove the code duplication of the rasterization algorithm, using // CMinimap version. const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); const ssize_t dimension = terrain->GetVerticesPerSide() - 1; const ssize_t bpp = 24; const ssize_t imageDataSize = dimension * dimension * (bpp / 8); std::vector imageBytes(imageDataSize); float shallowPassageHeight = CMiniMap::GetShallowPassageHeight(); ssize_t w = dimension; ssize_t h = dimension; float waterHeight = g_Renderer.GetWaterManager()->m_WaterHeight; for (ssize_t j = 0; j < h; ++j) { // Work backwards to vertically flip the image. ssize_t position = 3 * (h - j - 1) * dimension; for (ssize_t i = 0; i < w; ++i) { float avgHeight = (terrain->GetVertexGroundLevel(i, j) + terrain->GetVertexGroundLevel(i + 1, j) + terrain->GetVertexGroundLevel(i, j + 1) + terrain->GetVertexGroundLevel(i + 1, j + 1) ) / 4.0f; if (avgHeight < waterHeight && avgHeight > waterHeight - shallowPassageHeight) { // shallow water imageBytes[position++] = 0x70; imageBytes[position++] = 0x98; imageBytes[position++] = 0xc0; } else if (avgHeight < waterHeight) { // Set water as constant color for consistency on different maps imageBytes[position++] = 0x50; imageBytes[position++] = 0x78; imageBytes[position++] = 0xa0; } else { u32 color = std::numeric_limits::max(); u32 hmap = static_cast(terrain->GetHeightMap()[j * dimension + i]) >> 8; float scale = hmap / 3.0f + 170.0f / 255.0f; CMiniPatch* mp = terrain->GetTile(i, j); if (mp) { CTerrainTextureEntry* tex = mp->GetTextureEntry(); if (tex) color = tex->GetBaseColor(); } // Convert imageBytes[position++] = static_cast(static_cast(color & 0xff) * scale); imageBytes[position++] = static_cast(static_cast((color >> 8) & 0xff) * scale); imageBytes[position++] = static_cast(static_cast((color >> 16) & 0xff) * scale); } } } msg->imageBytes = std::move(imageBytes); msg->dimension = dimension; } QUERYHANDLER(GetRMSData) { msg->data = g_Game->GetSimulation2()->GetRMSData(); } QUERYHANDLER(GetCurrentMapSize) { msg->size = g_Game->GetWorld()->GetTerrain()->GetTilesPerSide(); } BEGIN_COMMAND(ResizeMap) { bool Within(const CFixedVector3D& pos, const int centerX, const int centerZ, const int radius) { int dx = abs(pos.X.ToInt_RoundToZero() - centerX); if (dx > radius) return false; int dz = abs(pos.Z.ToInt_RoundToZero() - centerZ); if (dz > radius) return false; if (dx + dz <= radius) return true; return dx * dx + dz * dz <= radius * radius; } struct DeletedObject { entity_id_t entityId; CStr templateName; player_id_t owner; CFixedVector3D pos; CFixedVector3D rot; u32 actorSeed; }; ssize_t m_OldPatches, m_NewPatches; int m_OffsetX, m_OffsetY; u16* m_Heightmap; CPatch* m_Patches; std::vector m_DeletedObjects; std::vector> m_OldPositions; std::vector> m_NewPositions; cResizeMap() : m_Heightmap(nullptr), m_Patches(nullptr) { } ~cResizeMap() { delete[] m_Heightmap; delete[] m_Patches; } void MakeDirty() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); // The LOS texture won't normally get updated when running Atlas // (since there's no simulation updates), so explicitly dirty it g_Game->GetView()->GetLOSTexture().MakeDirty(); } void ResizeTerrain(ssize_t patches, int offsetX, int offsetY) { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); terrain->ResizeAndOffset(patches, -offsetX, -offsetY); } void DeleteObjects(const std::vector& deletedObjects) { for (const DeletedObject& deleted : deletedObjects) g_Game->GetSimulation2()->DestroyEntity(deleted.entityId); g_Game->GetSimulation2()->FlushDestroyedEntities(); } void RestoreObjects(const std::vector& deletedObjects) { CSimulation2& sim = *g_Game->GetSimulation2(); for (const DeletedObject& deleted : deletedObjects) { entity_id_t ent = sim.AddEntity(deleted.templateName.FromUTF8(), deleted.entityId); if (ent == INVALID_ENTITY) { LOGERROR("Failed to load entity template '%s'", deleted.templateName.c_str()); } else { CmpPtr cmpPosition(sim, deleted.entityId); if (cmpPosition) { cmpPosition->JumpTo(deleted.pos.X, deleted.pos.Z); cmpPosition->SetXZRotation(deleted.rot.X, deleted.rot.Z); cmpPosition->SetYRotation(deleted.rot.Y); } CmpPtr cmpOwnership(sim, deleted.entityId); if (cmpOwnership) cmpOwnership->SetOwner(deleted.owner); CmpPtr cmpVisual(sim, deleted.entityId); if (cmpVisual) cmpVisual->SetActorSeed(deleted.actorSeed); } } } void SetMovedEntitiesPosition(const std::vector>& movedObjects) { for (const std::pair& obj : movedObjects) { const entity_id_t id = obj.first; const CFixedVector3D position = obj.second; CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); ENSURE(cmpPosition); cmpPosition->JumpTo(position.X, position.Z); } } void Do() { CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); ENSURE(cmpTemplateManager); CmpPtr cmpTerrain(sim, SYSTEM_ENTITY); if (!cmpTerrain) { m_OldPatches = m_NewPatches = 0; m_OffsetX = m_OffsetY = 0; } else { m_OldPatches = static_cast(cmpTerrain->GetTilesPerSide() / PATCH_SIZE); m_NewPatches = msg->tiles / PATCH_SIZE; m_OffsetX = msg->offsetX / PATCH_SIZE; // Need to flip direction of vertical offset, due to screen mapping order. m_OffsetY = -(msg->offsetY / PATCH_SIZE); CTerrain* terrain = cmpTerrain->GetCTerrain(); m_Heightmap = new u16[(m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1)]; std::copy_n(terrain->GetHeightMap(), (m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1), m_Heightmap); m_Patches = new CPatch[m_OldPatches * m_OldPatches]; for (ssize_t j = 0; j < m_OldPatches; ++j) for (ssize_t i = 0; i < m_OldPatches; ++i) { CPatch& src = *(terrain->GetPatch(i, j)); CPatch& dst = m_Patches[j * m_OldPatches + i]; std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); } } const int radiusInTerrainUnits = m_NewPatches * PATCH_SIZE * TERRAIN_TILE_SIZE / 2 * (1.f - 1e-6f); // Opposite direction offset, as we move the destination onto the source, not the source into the destination. const int mapCenterX = (m_OldPatches / 2 - m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE; const int mapCenterZ = (m_OldPatches / 2 - m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE; // The offset to move units by is opposite the direction the map is moved, and from the corner. const int offsetX = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE; const int offsetZ = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE; const CFixedVector3D offset = CFixedVector3D(fixed::FromInt(offsetX), fixed::FromInt(0), fixed::FromInt(offsetZ)); const CSimulation2::InterfaceListUnordered& ents = sim.GetEntitiesWithInterfaceUnordered(IID_Selectable); - for (const std::pair& ent : ents) + for (const std::pair& ent : ents) { const entity_id_t entityId = ent.first; - CmpPtr cmpPosition(sim, entityId); if (cmpPosition && cmpPosition->IsInWorld() && Within(cmpPosition->GetPosition(), mapCenterX, mapCenterZ, radiusInTerrainUnits)) { CFixedVector3D position = cmpPosition->GetPosition(); m_NewPositions.emplace_back(entityId, position + offset); m_OldPositions.emplace_back(entityId, position); } else { DeletedObject deleted; deleted.entityId = entityId; deleted.templateName = cmpTemplateManager->GetCurrentTemplateName(entityId); // If the entity has a position, but the ending position is not valid; if (cmpPosition) { deleted.pos = cmpPosition->GetPosition(); deleted.rot = cmpPosition->GetRotation(); } CmpPtr cmpOwnership(sim, entityId); if (cmpOwnership) deleted.owner = cmpOwnership->GetOwner(); CmpPtr cmpVisual(sim, deleted.entityId); if (cmpVisual) deleted.actorSeed = cmpVisual->GetActorSeed(); m_DeletedObjects.push_back(deleted); } } DeleteObjects(m_DeletedObjects); ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY); SetMovedEntitiesPosition(m_NewPositions); MakeDirty(); } void Undo() { if (m_Heightmap == nullptr || m_Patches == nullptr) { // If there previously was no data, just resize to old (probably not originally valid). ResizeTerrain(m_OldPatches, -m_OffsetX, -m_OffsetY); } else { CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpTerrain(sim, SYSTEM_ENTITY); CTerrain* terrain = cmpTerrain->GetCTerrain(); terrain->Initialize(m_OldPatches, m_Heightmap); // Copy terrain data back. for (ssize_t j = 0; j < m_OldPatches; ++j) for (ssize_t i = 0; i < m_OldPatches; ++i) { CPatch& src = m_Patches[j * m_OldPatches + i]; CPatch& dst = *(terrain->GetPatch(i, j)); std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); } } RestoreObjects(m_DeletedObjects); SetMovedEntitiesPosition(m_OldPositions); MakeDirty(); } void Redo() { DeleteObjects(m_DeletedObjects); ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY); SetMovedEntitiesPosition(m_NewPositions); MakeDirty(); } }; END_COMMAND(ResizeMap) QUERYHANDLER(VFSFileExists) { msg->exists = VfsFileExists(*msg->path); } QUERYHANDLER(VFSFileRealPath) { VfsPath pathname(*msg->path); if (pathname.empty()) return; OsPath realPathname; if (g_VFS->GetRealPath(pathname, realPathname) == INFO::OK) msg->realPath = realPathname.string(); } static Status AddToFilenames(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& filenames = *(std::vector*)cbData; filenames.push_back(pathname.string().c_str()); return INFO::OK; } QUERYHANDLER(GetMapList) { #define GET_FILE_LIST(path, list) \ std::vector list; \ vfs::ForEachFile(g_VFS, path, AddToFilenames, (uintptr_t)&list, L"*.xml", vfs::DIR_RECURSIVE); \ msg->list = list; GET_FILE_LIST(L"maps/scenarios/", scenarioFilenames); GET_FILE_LIST(L"maps/skirmishes/", skirmishFilenames); GET_FILE_LIST(L"maps/tutorials/", tutorialFilenames); #undef GET_FILE_LIST } QUERYHANDLER(GetVictoryConditionData) { msg->data = g_Game->GetSimulation2()->GetVictoryConditiondData(); } } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp (revision 24486) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp (revision 24487) @@ -1,1131 +1,1131 @@ /* Copyright (C) 2020 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 #include #include "MessageHandler.h" #include "../CommandProc.h" #include "../SimState.h" #include "../View.h" #include "graphics/GameView.h" #include "graphics/Model.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "graphics/ObjectManager.h" #include "graphics/Terrain.h" #include "graphics/Unit.h" #include "lib/ogl.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Selection.h" #include "ps/XML/XMLWriter.h" namespace AtlasMessage { namespace { bool SortObjectsList(const sObjectsListItem& a, const sObjectsListItem& b) { return wcscmp(a.name.c_str(), b.name.c_str()) < 0; } } // Helpers for object constraints bool CheckEntityObstruction(entity_id_t ent) { CmpPtr cmpObstruction(*g_Game->GetSimulation2(), ent); if (cmpObstruction) { ICmpObstruction::EFoundationCheck result = cmpObstruction->CheckFoundation("default"); if (result != ICmpObstruction::FOUNDATION_CHECK_SUCCESS) return false; } return true; } void CheckObstructionAndUpdateVisual(entity_id_t id) { CmpPtr cmpVisual(*g_Game->GetSimulation2(), id); if (cmpVisual) { if (!CheckEntityObstruction(id)) cmpVisual->SetShadingColor(fixed::FromDouble(1.4), fixed::FromDouble(0.4), fixed::FromDouble(0.4), fixed::FromDouble(1)); else cmpVisual->SetShadingColor(fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1)); } } QUERYHANDLER(GetObjectsList) { std::vector objects; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTemplateManager) { std::vector names = cmpTemplateManager->FindAllTemplates(true); for (std::vector::iterator it = names.begin(); it != names.end(); ++it) { std::wstring name(it->begin(), it->end()); sObjectsListItem e; e.id = name; if (name.substr(0, 6) == L"actor|") { e.name = name.substr(6); e.type = 1; } else { e.name = name; e.type = 0; } objects.push_back(e); } } std::sort(objects.begin(), objects.end(), SortObjectsList); msg->objects = objects; } static std::vector g_Selection; typedef std::map PlayerColorMap; // Helper function to find color of player owning the given entity, // returns white if entity has no owner. Uses caching to avoid // expensive script calls. static CColor GetOwnerPlayerColor(PlayerColorMap& colorMap, entity_id_t id) { // Default color - white CColor color(1.0f, 1.0f, 1.0f, 1.0f); CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpOwnership(sim, id); if (cmpOwnership) { player_id_t owner = cmpOwnership->GetOwner(); if (colorMap.find(owner) != colorMap.end()) return colorMap[owner]; else { CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY); entity_id_t playerEnt = cmpPlayerManager->GetPlayerByID(owner); CmpPtr cmpPlayer(sim, playerEnt); if (cmpPlayer) { colorMap[owner] = cmpPlayer->GetDisplayedColor(); color = colorMap[owner]; } } } return color; } MESSAGEHANDLER(SetSelectionPreview) { CSimulation2& sim = *g_Game->GetSimulation2(); // Cache player colors for performance PlayerColorMap playerColors; // Clear old selection rings for (size_t i = 0; i < g_Selection.size(); ++i) { // We can't set only alpha here, because that won't trigger desaturation // so we set the complete color (not too evil since it's cached) CmpPtr cmpSelectable(sim, g_Selection[i]); if (cmpSelectable) { CColor color = GetOwnerPlayerColor(playerColors, g_Selection[i]); color.a = 0.0f; cmpSelectable->SetSelectionHighlight(color, false); } } g_Selection = *msg->ids; // Set new selection rings for (size_t i = 0; i < g_Selection.size(); ++i) { CmpPtr cmpSelectable(sim, g_Selection[i]); if (cmpSelectable) cmpSelectable->SetSelectionHighlight(GetOwnerPlayerColor(playerColors, g_Selection[i]), true); } } QUERYHANDLER(GetObjectSettings) { AtlasView* view = AtlasView::GetView(msg->view); CSimulation2* simulation = view->GetSimulation2(); sObjectSettings settings; settings.player = 0; CmpPtr cmpOwnership(*simulation, view->GetEntityId(msg->id)); if (cmpOwnership) { int32_t player = cmpOwnership->GetOwner(); if (player != -1) settings.player = player; } // TODO: selections /* // Get the unit's possible variants and selected variants std::vector > groups = unit->GetObject().m_Base->GetVariantGroups(); const std::set& selections = unit->GetActorSelections(); // Iterate over variant groups std::vector > variantgroups; std::set selections_set; variantgroups.reserve(groups.size()); for (size_t i = 0; i < groups.size(); ++i) { // Copy variants into output structure std::vector group; group.reserve(groups[i].size()); int choice = -1; for (size_t j = 0; j < groups[i].size(); ++j) { group.push_back(CStrW(groups[i][j])); // Find the first string in 'selections' that matches one of this // group's variants if (choice == -1) if (selections.find(groups[i][j]) != selections.end()) choice = (int)j; } // Assuming one of the variants was selected (which it really ought // to be), remember that one's name if (choice != -1) selections_set.insert(CStrW(groups[i][choice])); variantgroups.push_back(group); } settings.variantgroups = variantgroups; settings.selections = std::vector (selections_set.begin(), selections_set.end()); // convert set->vector */ msg->settings = settings; } QUERYHANDLER(GetObjectMapSettings) { std::vector ids = *msg->ids; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); XMLWriter_File exampleFile; { XMLWriter_Element entitiesTag(exampleFile, "Entities"); { for (entity_id_t id : ids) { XMLWriter_Element entityTag(exampleFile, "Entity"); { //Template name entityTag.Setting("Template", cmpTemplateManager->GetCurrentTemplateName(id)); //Player CmpPtr cmpOwnership(*g_Game->GetSimulation2(), id); if (cmpOwnership) entityTag.Setting("Player", static_cast(cmpOwnership->GetOwner())); //Adding position to make some relative position later CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (cmpPosition) { CFixedVector3D pos = cmpPosition->GetPosition(); CFixedVector3D rot = cmpPosition->GetRotation(); { XMLWriter_Element positionTag(exampleFile, "Position"); positionTag.Attribute("x", pos.X); positionTag.Attribute("z", pos.Z); // TODO: height offset etc } { XMLWriter_Element orientationTag(exampleFile, "Orientation"); orientationTag.Attribute("y", rot.Y); // TODO: X, Z maybe } } // Adding actor seed CmpPtr cmpVisual(*g_Game->GetSimulation2(), id); if (cmpVisual) entityTag.Setting("ActorSeed", static_cast(cmpVisual->GetActorSeed())); } } } } const CStr& data = exampleFile.GetOutput(); msg->xmldata = data.FromUTF8(); } BEGIN_COMMAND(SetObjectSettings) { player_id_t m_PlayerOld, m_PlayerNew; std::set m_SelectionsOld, m_SelectionsNew; void Do() { sObjectSettings settings = msg->settings; AtlasView* view = AtlasView::GetView(msg->view); CSimulation2* simulation = view->GetSimulation2(); CmpPtr cmpOwnership(*simulation, view->GetEntityId(msg->id)); m_PlayerOld = 0; if (cmpOwnership) { int32_t player = cmpOwnership->GetOwner(); if (player != -1) m_PlayerOld = player; } // TODO: selections // m_SelectionsOld = unit->GetActorSelections(); m_PlayerNew = (player_id_t)settings.player; std::vector selections = *settings.selections; for (std::vector::iterator it = selections.begin(); it != selections.end(); ++it) { m_SelectionsNew.insert(CStrW(*it).ToUTF8()); } Redo(); } void Redo() { Set(m_PlayerNew, m_SelectionsNew); } void Undo() { Set(m_PlayerOld, m_SelectionsOld); } private: void Set(player_id_t player, const std::set& UNUSED(selections)) { AtlasView* view = AtlasView::GetView(msg->view); CSimulation2* simulation = view->GetSimulation2(); CmpPtr cmpOwnership(*simulation, view->GetEntityId(msg->id)); if (cmpOwnership) cmpOwnership->SetOwner(player); // TODO: selections // unit->SetActorSelections(selections); } }; END_COMMAND(SetObjectSettings); ////////////////////////////////////////////////////////////////////////// static CStrW g_PreviewUnitName; static entity_id_t g_PreviewEntityID = INVALID_ENTITY; static std::vector g_PreviewEntitiesID; static CVector3D GetUnitPos(const Position& pos, bool floating) { static CVector3D vec; vec = pos.GetWorldSpace(vec, floating); // if msg->pos is 'Unchanged', use the previous pos // Clamp the position to the edges of the world: // Use 'Clamp' with a value slightly less than the width, so that converting // to integer (rounding towards zero) will put it on the tile inside the edge // instead of just outside float mapWidth = (g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide()-1)*TERRAIN_TILE_SIZE; float delta = 1e-6f; // fraction of map width - must be > FLT_EPSILON float xOnMap = Clamp(vec.X, 0.f, mapWidth * (1.f - delta)); float zOnMap = Clamp(vec.Z, 0.f, mapWidth * (1.f - delta)); // Don't waste time with GetExactGroundLevel unless we've changed if (xOnMap != vec.X || zOnMap != vec.Z) { vec.X = xOnMap; vec.Z = zOnMap; vec.Y = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(xOnMap, zOnMap); } return vec; } QUERYHANDLER(GetCurrentSelection) { msg->ids = g_Selection; } MESSAGEHANDLER(ObjectPreviewToEntity) { UNUSED2(msg); if (g_PreviewEntitiesID.size() == 0) return; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); PlayerColorMap playerColor; //I need to re create the objects finally delete preview objects for (entity_id_t ent : g_PreviewEntitiesID) { //Get template name (without the "preview|" prefix) std::wstring wTemplateName = wstring_from_utf8(cmpTemplateManager->GetCurrentTemplateName(ent).substr(8)); //Create new entity entity_id_t new_ent = g_Game->GetSimulation2()->AddEntity(wTemplateName); if (new_ent == INVALID_ENTITY) continue; //get position, get rotation CmpPtr cmpPositionNew(*g_Game->GetSimulation2(), new_ent); CmpPtr cmpPositionOld(*g_Game->GetSimulation2(), ent); if (cmpPositionNew && cmpPositionOld) { CVector3D pos = cmpPositionOld->GetPosition(); cmpPositionNew->JumpTo(entity_pos_t::FromFloat(pos.X), entity_pos_t::FromFloat(pos.Z)); //now rotate CFixedVector3D rotation = cmpPositionOld->GetRotation(); cmpPositionNew->SetYRotation(rotation.Y); } //get owner CmpPtr cmpOwnershipNew(*g_Game->GetSimulation2(), new_ent); CmpPtr cmpOwnershipOld(*g_Game->GetSimulation2(), ent); if (cmpOwnershipNew && cmpOwnershipOld) cmpOwnershipNew->SetOwner(cmpOwnershipOld->GetOwner()); //getVisual CmpPtr cmpVisualNew(*g_Game->GetSimulation2(), new_ent); CmpPtr cmpVisualOld(*g_Game->GetSimulation2(), ent); if (cmpVisualNew && cmpVisualOld) cmpVisualNew->SetActorSeed(cmpVisualOld->GetActorSeed()); //Update g_selectedObject and higligth g_Selection.push_back(new_ent); CmpPtr cmpSelectable(*g_Game->GetSimulation2(), new_ent); if (cmpSelectable) cmpSelectable->SetSelectionHighlight(GetOwnerPlayerColor(playerColor, new_ent), true); g_Game->GetSimulation2()->DestroyEntity(ent); } g_PreviewEntitiesID.clear(); } MESSAGEHANDLER(MoveObjectPreview) { if (g_PreviewEntitiesID.size()==0) return; //TODO:Change pivot entity_id_t referenceEntity = *g_PreviewEntitiesID.begin(); // All selected objects move relative to a pivot object, // so get its position and whether it's floating CFixedVector3D referencePos; CmpPtr cmpPosition(*g_Game->GetSimulation2(), referenceEntity); if (cmpPosition && cmpPosition->IsInWorld()) referencePos = cmpPosition->GetPosition(); // Calculate directional vector of movement for pivot object, // we apply the same movement to all objects CVector3D targetPos = GetUnitPos(msg->pos, true); CFixedVector3D fTargetPos(entity_pos_t::FromFloat(targetPos.X), entity_pos_t::FromFloat(targetPos.Y), entity_pos_t::FromFloat(targetPos.Z)); CFixedVector3D dir = fTargetPos - referencePos; for (const entity_id_t id : g_PreviewEntitiesID) { CmpPtr cmpPreviewPosition(*g_Game->GetSimulation2(), id); if (cmpPreviewPosition) { CFixedVector3D posFinal; if (cmpPreviewPosition->IsInWorld()) { // Calculate this object's position CFixedVector3D posFixed = cmpPreviewPosition->GetPosition(); posFinal = posFixed + dir; } cmpPreviewPosition->JumpTo(posFinal.X, posFinal.Z); } CheckObstructionAndUpdateVisual(id); } } MESSAGEHANDLER(ObjectPreview) { // If the selection has changed... if (*msg->id != g_PreviewUnitName || (!msg->cleanObjectPreviews)) { // Delete old entity if (g_PreviewEntityID != INVALID_ENTITY && msg->cleanObjectPreviews) { //Time to delete all preview objects for (entity_id_t ent : g_PreviewEntitiesID) g_Game->GetSimulation2()->DestroyEntity(ent); g_PreviewEntitiesID.clear(); } // Create the new entity if ((*msg->id).empty()) g_PreviewEntityID = INVALID_ENTITY; else { g_PreviewEntityID = g_Game->GetSimulation2()->AddLocalEntity(L"preview|" + *msg->id); g_PreviewEntitiesID.push_back(g_PreviewEntityID); } g_PreviewUnitName = *msg->id; } if (g_PreviewEntityID != INVALID_ENTITY) { // Update the unit's position and orientation: CmpPtr cmpPosition(*g_Game->GetSimulation2(), g_PreviewEntityID); if (cmpPosition) { CVector3D pos = GetUnitPos(msg->pos, cmpPosition->CanFloat()); cmpPosition->JumpTo(entity_pos_t::FromFloat(pos.X), entity_pos_t::FromFloat(pos.Z)); float angle; if (msg->usetarget) { // Aim from pos towards msg->target CVector3D target = msg->target->GetWorldSpace(pos.Y); angle = atan2(target.X-pos.X, target.Z-pos.Z); } else { angle = msg->angle; } cmpPosition->SetYRotation(entity_angle_t::FromFloat(angle)); } // TODO: handle random variations somehow CmpPtr cmpVisual(*g_Game->GetSimulation2(), g_PreviewEntityID); if (cmpVisual) cmpVisual->SetActorSeed(msg->actorseed); CmpPtr cmpOwnership(*g_Game->GetSimulation2(), g_PreviewEntityID); if (cmpOwnership) cmpOwnership->SetOwner((player_id_t)msg->settings->player); CheckObstructionAndUpdateVisual(g_PreviewEntityID); } } BEGIN_COMMAND(CreateObject) { CVector3D m_Pos; float m_Angle; player_id_t m_Player; entity_id_t m_EntityID; u32 m_ActorSeed; void Do() { // Calculate the position/orientation to create this unit with m_Pos = GetUnitPos(msg->pos, true); // don't really care about floating if (msg->usetarget) { // Aim from m_Pos towards msg->target CVector3D target = msg->target->GetWorldSpace(m_Pos.Y); m_Angle = atan2(target.X-m_Pos.X, target.Z-m_Pos.Z); } else { m_Angle = msg->angle; } m_Player = (player_id_t)msg->settings->player; m_ActorSeed = msg->actorseed; // TODO: variation/selection strings Redo(); } void Redo() { m_EntityID = g_Game->GetSimulation2()->AddEntity(*msg->id); if (m_EntityID == INVALID_ENTITY) return; CmpPtr cmpPosition(*g_Game->GetSimulation2(), m_EntityID); if (cmpPosition) { cmpPosition->JumpTo(entity_pos_t::FromFloat(m_Pos.X), entity_pos_t::FromFloat(m_Pos.Z)); cmpPosition->SetYRotation(entity_angle_t::FromFloat(m_Angle)); } CmpPtr cmpOwnership(*g_Game->GetSimulation2(), m_EntityID); if (cmpOwnership) cmpOwnership->SetOwner(m_Player); CmpPtr cmpVisual(*g_Game->GetSimulation2(), m_EntityID); if (cmpVisual) { cmpVisual->SetActorSeed(m_ActorSeed); // TODO: variation/selection strings } } void Undo() { if (m_EntityID != INVALID_ENTITY) { g_Game->GetSimulation2()->DestroyEntity(m_EntityID); m_EntityID = INVALID_ENTITY; } } }; END_COMMAND(CreateObject) QUERYHANDLER(PickObject) { float x, y; msg->pos->GetScreenSpace(x, y); // Normally this function would be called with a player ID to check LOS, // but in Atlas the entire map is revealed, so just pass INVALID_PLAYER entity_id_t ent = EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, INVALID_PLAYER, msg->selectActors);; if (ent == INVALID_ENTITY) msg->id = INVALID_ENTITY; else { msg->id = ent; // Calculate offset of object from original mouse click position // so it gets moved by that offset CmpPtr cmpPosition(*g_Game->GetSimulation2(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) { // error msg->offsetx = msg->offsety = 0; } else { CFixedVector3D fixed = cmpPosition->GetPosition(); CVector3D centre = CVector3D(fixed.X.ToFloat(), fixed.Y.ToFloat(), fixed.Z.ToFloat()); float cx, cy; g_Game->GetView()->GetCamera()->GetScreenCoordinates(centre, cx, cy); msg->offsetx = (int)(cx - x); msg->offsety = (int)(cy - y); } } } QUERYHANDLER(PickObjectsInRect) { float x0, y0, x1, y1; msg->start->GetScreenSpace(x0, y0); msg->end->GetScreenSpace(x1, y1); // Since owner selections are meaningless in Atlas, use INVALID_PLAYER msg->ids = EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, INVALID_PLAYER, msg->selectActors); } QUERYHANDLER(PickSimilarObjects) { CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); entity_id_t ent = msg->id; std::string templateName = cmpTemplateManager->GetCurrentTemplateName(ent); // If unit has ownership, only pick units from the same player player_id_t owner = INVALID_PLAYER; CmpPtr cmpOwnership(*g_Game->GetSimulation2(), ent); if (cmpOwnership) owner = cmpOwnership->GetOwner(); msg->ids = EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, owner, false, true, true, false); } MESSAGEHANDLER(ResetSelectionColor) { UNUSED2(msg); for (entity_id_t ent : g_Selection) { CmpPtr cmpVisual(*g_Game->GetSimulation2(), ent); if (cmpVisual) cmpVisual->SetShadingColor(fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1)); } } BEGIN_COMMAND(MoveObjects) { // Mapping from object to position std::map m_PosOld, m_PosNew; void Do() { std::vector ids = *msg->ids; // All selected objects move relative to a pivot object, // so get its position and whether it's floating CVector3D pivotPos(0, 0, 0); bool pivotFloating = false; CmpPtr cmpPositionPivot(*g_Game->GetSimulation2(), (entity_id_t)msg->pivot); if (cmpPositionPivot && cmpPositionPivot->IsInWorld()) { pivotFloating = cmpPositionPivot->CanFloat(); CFixedVector3D pivotFixed = cmpPositionPivot->GetPosition(); pivotPos = CVector3D(pivotFixed.X.ToFloat(), pivotFixed.Y.ToFloat(), pivotFixed.Z.ToFloat()); } // Calculate directional vector of movement for pivot object, // we apply the same movement to all objects CVector3D targetPos = GetUnitPos(msg->pos, pivotFloating); CVector3D dir = targetPos - pivotPos; for (entity_id_t id : ids) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (!cmpPosition || !cmpPosition->IsInWorld()) { // error m_PosOld[id] = m_PosNew[id] = CVector3D(0, 0, 0); } else { // Calculate this object's position CFixedVector3D posFixed = cmpPosition->GetPosition(); CVector3D pos = CVector3D(posFixed.X.ToFloat(), posFixed.Y.ToFloat(), posFixed.Z.ToFloat()); m_PosNew[id] = pos + dir; m_PosOld[id] = pos; } } SetPos(m_PosNew); } void SetPos(const std::map& map) { - for (const std::pair& p : map) + for (const std::pair& p : map) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), p.first); if (!cmpPosition) return; // Set 2D position, ignoring height cmpPosition->JumpTo(entity_pos_t::FromFloat(p.second.X), entity_pos_t::FromFloat(p.second.Z)); CheckObstructionAndUpdateVisual(p.first); } } void Redo() { SetPos(m_PosNew); } void Undo() { SetPos(m_PosOld); } void MergeIntoPrevious(cMoveObjects* prev) { // TODO: do something valid if prev selection != this selection ENSURE(*(prev->msg->ids) == *(msg->ids)); prev->m_PosNew = m_PosNew; } }; END_COMMAND(MoveObjects) BEGIN_COMMAND(RotateObjectsFromCenterPoint) { std::map m_PosOld, m_PosNew; std::map m_AngleOld, m_AngleNew; CVector3D m_CenterPoint; float m_AngleInitialRotation; void Do() { std::vector ids = *msg->ids; CVector3D minPos; CVector3D maxPos; bool first = true; // Compute min position and max position for (entity_id_t id : ids) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (!cmpPosition) continue; CVector3D pos = cmpPosition->GetPosition(); m_PosOld[id] = cmpPosition->GetPosition(); m_AngleOld[id] = cmpPosition->GetRotation().Y.ToFloat(); if (first) { first = false; minPos = pos; maxPos = pos; m_CenterPoint.Y = pos.Y; continue; } if (pos.X < minPos.X) minPos.X = pos.X; if (pos.X > maxPos.X) maxPos.X = pos.X; if (pos.Z < minPos.Z) minPos.Z = pos.Z; if (pos.Z > maxPos.Z) maxPos.Z = pos.Z; } // Calculate objects center point m_CenterPoint.X = minPos.X + ((maxPos.X - minPos.X) * 0.5); m_CenterPoint.Z = minPos.Z + ((maxPos.Z - minPos.Z) * 0.5); CVector3D target = msg->target->GetWorldSpace(m_CenterPoint.Y); m_AngleInitialRotation = atan2(target.X-m_CenterPoint.X, target.Z-m_CenterPoint.Z); } void SetPos(const std::map& position, const std::map& angle) { - for (const std::pair& p : position) + for (const std::pair& p : position) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), p.first); if (!cmpPosition) return; // Set 2D position, ignoring height cmpPosition->JumpTo(entity_pos_t::FromFloat(p.second.X), entity_pos_t::FromFloat(p.second.Z)); if (msg->rotateObject) cmpPosition->SetYRotation(entity_angle_t::FromFloat(angle.at(p.first))); } - for (const std::pair& p: position) + for (const std::pair& p : position) CheckObstructionAndUpdateVisual(p.first); } void Redo() { SetPos(m_PosNew, m_AngleNew); } void RecalculateRotation(Position newPoint) { std::vector ids = *msg->ids; CVector3D target = newPoint.GetWorldSpace(m_CenterPoint.Y); float newAngle = atan2(target.X-m_CenterPoint.X, target.Z-m_CenterPoint.Z); float globalAngle = m_AngleInitialRotation - newAngle; // Recalculate positions for (entity_id_t id : ids) { CVector3D pos = m_PosOld[id]; float angle = atan2(pos.X - m_CenterPoint.X, pos.Z - m_CenterPoint.Z); float localAngle = angle + (globalAngle - angle); float xCos = cosf(localAngle); float xSin = sinf(localAngle); pos.X -= m_CenterPoint.X; pos.Z -= m_CenterPoint.Z; float newX = pos.X * xCos - pos.Z * xSin; float newZ = pos.X * xSin + pos.Z * xCos; pos.X = newX + m_CenterPoint.X; pos.Z = newZ + m_CenterPoint.Z; m_PosNew[id] = pos; m_AngleNew[id] = m_AngleOld[id] - globalAngle; } SetPos(m_PosNew, m_AngleNew); } void Undo() { SetPos(m_PosOld, m_AngleOld); } void MergeIntoPrevious(cRotateObjectsFromCenterPoint* prev) { // TODO: do something valid if prev unit != this unit ENSURE(*prev->msg->ids == *msg->ids); m_PosOld = prev->m_PosOld; m_AngleInitialRotation = prev->m_AngleInitialRotation; m_AngleOld = prev->m_AngleOld; m_CenterPoint = prev->m_CenterPoint; RecalculateRotation(msg->target); } }; END_COMMAND(RotateObjectsFromCenterPoint) BEGIN_COMMAND(RotateObject) { std::map m_AngleOld, m_AngleNew; void Do() { std::vector ids = *msg->ids; for (entity_id_t id : ids) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (!cmpPosition) return; m_AngleOld[id] = cmpPosition->GetRotation().Y.ToFloat(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(0.f); CVector3D pos = transform.GetTranslation(); CVector3D target = msg->target->GetWorldSpace(pos.Y); m_AngleNew[id] = atan2(target.X-pos.X, target.Z-pos.Z); } SetAngle(m_AngleNew); } void SetAngle(const std::map& angles) { - for (const std::pair& p : angles) + for (const std::pair& p : angles) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), p.first); if (!cmpPosition) return; cmpPosition->SetYRotation(entity_angle_t::FromFloat(p.second)); } } void Redo() { SetAngle(m_AngleNew); } void Undo() { SetAngle(m_AngleOld); } void MergeIntoPrevious(cRotateObject* prev) { // TODO: do something valid if prev unit != this unit ENSURE(*prev->msg->ids == *msg->ids); prev->m_AngleNew = m_AngleNew; } }; END_COMMAND(RotateObject) BEGIN_COMMAND(DeleteObjects) { // Saved copy of the important aspects of a unit, to allow undo struct OldObject { entity_id_t entityID; CStr templateName; player_id_t owner; CFixedVector3D pos; CFixedVector3D rot; u32 actorSeed; }; std::vector oldObjects; cDeleteObjects() { } void Do() { Redo(); } void Redo() { CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); ENSURE(cmpTemplateManager); std::vector ids = *msg->ids; for (size_t i = 0; i < ids.size(); ++i) { OldObject obj; obj.entityID = (entity_id_t)ids[i]; obj.templateName = cmpTemplateManager->GetCurrentTemplateName(obj.entityID); CmpPtr cmpOwnership(sim, obj.entityID); if (cmpOwnership) obj.owner = cmpOwnership->GetOwner(); CmpPtr cmpPosition(sim, obj.entityID); if (cmpPosition) { obj.pos = cmpPosition->GetPosition(); obj.rot = cmpPosition->GetRotation(); } CmpPtr cmpVisual(sim, obj.entityID); if (cmpVisual) obj.actorSeed = cmpVisual->GetActorSeed(); oldObjects.push_back(obj); g_Game->GetSimulation2()->DestroyEntity(obj.entityID); } g_Game->GetSimulation2()->FlushDestroyedEntities(); } void Undo() { CSimulation2& sim = *g_Game->GetSimulation2(); for (size_t i = 0; i < oldObjects.size(); ++i) { entity_id_t ent = sim.AddEntity(oldObjects[i].templateName.FromUTF8(), oldObjects[i].entityID); if (ent == INVALID_ENTITY) { LOGERROR("Failed to load entity template '%s'", oldObjects[i].templateName.c_str()); } else { CmpPtr cmpPosition(sim, oldObjects[i].entityID); if (cmpPosition) { cmpPosition->JumpTo(oldObjects[i].pos.X, oldObjects[i].pos.Z); cmpPosition->SetXZRotation(oldObjects[i].rot.X, oldObjects[i].rot.Z); cmpPosition->SetYRotation(oldObjects[i].rot.Y); } CmpPtr cmpOwnership(sim, oldObjects[i].entityID); if (cmpOwnership) cmpOwnership->SetOwner(oldObjects[i].owner); CmpPtr cmpVisual(sim, oldObjects[i].entityID); if (cmpVisual) cmpVisual->SetActorSeed(oldObjects[i].actorSeed); } } oldObjects.clear(); } }; END_COMMAND(DeleteObjects) QUERYHANDLER(GetPlayerObjects) { std::vector ids; player_id_t playerID = msg->player; const CSimulation2::InterfaceListUnordered& cmps = g_Game->GetSimulation2()->GetEntitiesWithInterfaceUnordered(IID_Ownership); for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) { if (static_cast(eit->second)->GetOwner() == playerID) { ids.push_back(eit->first); } } msg->ids = ids; } MESSAGEHANDLER(SetBandbox) { AtlasView::GetView_Game()->SetBandbox(msg->show, (float)msg->sx0, (float)msg->sy0, (float)msg->sx1, (float)msg->sy1); } QUERYHANDLER(GetSelectedObjectsTemplateNames) { std::vector ids = *msg->ids; std::vector names; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); for (size_t i = 0; i < ids.size(); ++i) { entity_id_t id = (entity_id_t)ids[i]; std::string templateName = cmpTemplateManager->GetCurrentTemplateName(id); names.push_back(templateName); } std::sort(names.begin(), names.end()); msg->names = names; } } Index: ps/trunk/source/tools/atlas/GameInterface/View.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/View.cpp (revision 24486) +++ ps/trunk/source/tools/atlas/GameInterface/View.cpp (revision 24487) @@ -1,517 +1,517 @@ /* Copyright (C) 2020 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 "View.h" #include "ActorViewer.h" #include "GameLoop.h" #include "Messages.h" #include "SimState.h" #include "graphics/CinemaManager.h" #include "graphics/GameView.h" #include "graphics/ParticleManager.h" #include "graphics/SColor.h" #include "graphics/UnitManager.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "ps/Game.h" #include "ps/GameSetup/GameSetup.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpParticleManager.h" #include "simulation2/components/ICmpPathfinder.h" #include "soundmanager/ISoundManager.h" extern void (*Atlas_GLSwapBuffers)(void* context); extern int g_xres, g_yres; ////////////////////////////////////////////////////////////////////////// void AtlasView::SetParam(const std::wstring& UNUSED(name), bool UNUSED(value)) { } void AtlasView::SetParam(const std::wstring& UNUSED(name), const AtlasMessage::Color& UNUSED(value)) { } void AtlasView::SetParam(const std::wstring& UNUSED(name), const std::wstring& UNUSED(value)) { } void AtlasView::SetParam(const std::wstring& UNUSED(name), int UNUSED(value)) { } ////////////////////////////////////////////////////////////////////////// AtlasViewActor::AtlasViewActor() : m_SpeedMultiplier(1.f), m_ActorViewer(new ActorViewer()) { } AtlasViewActor::~AtlasViewActor() { delete m_ActorViewer; } void AtlasViewActor::Update(float realFrameLength) { m_ActorViewer->Update(realFrameLength * m_SpeedMultiplier, realFrameLength); } void AtlasViewActor::Render() { SViewPort vp = { 0, 0, g_xres, g_yres }; CCamera& camera = GetCamera(); camera.SetViewPort(vp); camera.SetPerspectiveProjection(2.f, 512.f, DEGTORAD(20.f)); camera.UpdateFrustum(); m_ActorViewer->Render(); Atlas_GLSwapBuffers((void*)g_AtlasGameLoop->glCanvas); } CCamera& AtlasViewActor::GetCamera() { return m_Camera; } CSimulation2* AtlasViewActor::GetSimulation2() { return m_ActorViewer->GetSimulation2(); } entity_id_t AtlasViewActor::GetEntityId(AtlasMessage::ObjectID UNUSED(obj)) { return m_ActorViewer->GetEntity(); } bool AtlasViewActor::WantsHighFramerate() { if (m_SpeedMultiplier != 0.f) return true; return false; } void AtlasViewActor::SetEnabled(bool enabled) { m_ActorViewer->SetEnabled(enabled); } void AtlasViewActor::SetSpeedMultiplier(float speedMultiplier) { m_SpeedMultiplier = speedMultiplier; } ActorViewer& AtlasViewActor::GetActorViewer() { return *m_ActorViewer; } void AtlasViewActor::SetParam(const std::wstring& name, bool value) { if (name == L"wireframe") g_Renderer.SetModelRenderMode(value ? WIREFRAME : SOLID); else if (name == L"walk") m_ActorViewer->SetWalkEnabled(value); else if (name == L"ground") m_ActorViewer->SetGroundEnabled(value); // TODO: this causes corruption of WaterManager's global state // which should be asociated with terrain or simulation instead // see http://trac.wildfiregames.com/ticket/2692 //else if (name == L"water") //m_ActorViewer->SetWaterEnabled(value); else if (name == L"shadows") m_ActorViewer->SetShadowsEnabled(value); else if (name == L"stats") m_ActorViewer->SetStatsEnabled(value); else if (name == L"bounding_box") m_ActorViewer->SetBoundingBoxesEnabled(value); else if (name == L"axes_marker") m_ActorViewer->SetAxesMarkerEnabled(value); } void AtlasViewActor::SetParam(const std::wstring& name, int value) { if (name == L"prop_points") m_ActorViewer->SetPropPointsMode(value); } void AtlasViewActor::SetParam(const std::wstring& name, const AtlasMessage::Color& value) { if (name == L"background") { m_ActorViewer->SetBackgroundColor(SColor4ub(value.r, value.g, value.b, 255)); } } ////////////////////////////////////////////////////////////////////////// AtlasViewGame::AtlasViewGame() : m_SpeedMultiplier(0.f), m_IsTesting(false), m_DrawMoveTool(false) { ENSURE(g_Game); } AtlasViewGame::~AtlasViewGame() { - for (const std::pair& p : m_SavedStates) + for (const std::pair& p : m_SavedStates) delete p.second; } CSimulation2* AtlasViewGame::GetSimulation2() { return g_Game->GetSimulation2(); } void AtlasViewGame::Update(float realFrameLength) { const float actualFrameLength = realFrameLength * m_SpeedMultiplier; // Clean up any entities destroyed during UI message processing g_Game->GetSimulation2()->FlushDestroyedEntities(); if (m_SpeedMultiplier == 0.f) { // Update unit interpolation g_Game->Interpolate(0.0, realFrameLength); } else { // Update the whole world // (Tell the game update not to interpolate graphics - we'll do that // ourselves) g_Game->Update(actualFrameLength, false); // Interpolate the graphics - we only want to do this once per visual frame, // not in every call to g_Game->Update g_Game->Interpolate(actualFrameLength, realFrameLength); } // Run sound idle tasks every frame. if (g_SoundManager) g_SoundManager->IdleTask(); // Cinematic motion should be independent of simulation update, so we can // preview the cinematics by themselves g_Game->GetView()->GetCinema()->Update(realFrameLength); } void AtlasViewGame::Render() { SViewPort vp = { 0, 0, g_xres, g_yres }; CCamera& camera = GetCamera(); camera.SetViewPort(vp); camera.SetProjectionFromCamera(*g_Game->GetView()->GetCamera()); camera.UpdateFrustum(); ::Render(); Atlas_GLSwapBuffers((void*)g_AtlasGameLoop->glCanvas); g_Renderer.OnSwapBuffers(); } void AtlasViewGame::DrawCinemaPathTool() { if (!m_DrawMoveTool) return; #if CONFIG2_GLES #warning TODO : implement Atlas cinema path tool for GLES #else CVector3D focus = m_MoveTool; CVector3D camera = GetCamera().GetOrientation().GetTranslation(); float scale = (focus - camera).Length() / 10.0; glDisable(GL_DEPTH_TEST); glLineWidth(1.6f); glEnable(GL_LINE_SMOOTH); glColor3f(1.0f, 0.0f, 0.0f); glBegin(GL_LINE_STRIP); glVertex3fv(focus.GetFloatArray()); glVertex3fv((focus + CVector3D(scale, 0, 0)).GetFloatArray()); glEnd(); glColor3f(0.0f, 1.0f, 0.0f); glBegin(GL_LINE_STRIP); glVertex3fv(focus.GetFloatArray()); glVertex3fv((focus + CVector3D(0, scale, 0)).GetFloatArray()); glEnd(); glColor3f(0.0f, 0.0f, 1.0f); glBegin(GL_LINE_STRIP); glVertex3fv(focus.GetFloatArray()); glVertex3fv((focus + CVector3D(0, 0, scale)).GetFloatArray()); glEnd(); glDisable(GL_LINE_SMOOTH); glLineWidth(1.0f); glEnable(GL_DEPTH_TEST); #endif } void AtlasViewGame::DrawOverlays() { #if CONFIG2_GLES #warning TODO: implement Atlas game overlays for GLES #else // Set up transform for overlays glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetIdentity(); transform.Scale(1.0f, -1.f, 1.0f); transform.Translate(0.0f, (float)g_yres, -1000.0f); CMatrix3D proj; proj.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); transform = proj * transform; glLoadMatrixf(&transform._11); if (m_BandboxArray.size() > 0) { glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_VERTEX_ARRAY); // Render bandbox as array of lines glVertexPointer(2, GL_FLOAT, sizeof(SBandboxVertex), &m_BandboxArray[0].x); glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(SBandboxVertex), &m_BandboxArray[0].r); glDrawArrays(GL_LINES, 0, m_BandboxArray.size()); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY); } glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } void AtlasViewGame::SetParam(const std::wstring& name, bool value) { if (name == L"priorities") g_Renderer.SetDisplayTerrainPriorities(value); else if (name == L"movetool") m_DrawMoveTool = value; else if (name == L"preferGLSL") g_RenderingOptions.SetPreferGLSL(value); } void AtlasViewGame::SetParam(const std::wstring& name, float value) { if (name == L"movetool_x") m_MoveTool.X = value; else if (name == L"movetool_y") m_MoveTool.Y = value; else if (name == L"movetool_z") m_MoveTool.Z = value; } void AtlasViewGame::SetParam(const std::wstring& name, const std::wstring& value) { if (name == L"passability") { m_DisplayPassability = CStrW(value).ToUTF8(); CmpPtr cmpPathfinder(*GetSimulation2(), SYSTEM_ENTITY); if (cmpPathfinder) { if (!value.empty()) cmpPathfinder->SetAtlasOverlay(true, cmpPathfinder->GetPassabilityClass(m_DisplayPassability)); else cmpPathfinder->SetAtlasOverlay(false); } } else if (name == L"renderpath") { g_RenderingOptions.SetRenderPath(RenderPathEnum::FromString(CStrW(value).ToUTF8())); } } CCamera& AtlasViewGame::GetCamera() { return *g_Game->GetView()->GetCamera(); } bool AtlasViewGame::WantsHighFramerate() { if (g_Game->GetView()->GetCinema()->IsPlaying()) return true; if (m_SpeedMultiplier != 0.f) return true; return false; } void AtlasViewGame::SetSpeedMultiplier(float speed) { m_SpeedMultiplier = speed; } void AtlasViewGame::SetTesting(bool testing) { m_IsTesting = testing; // If we're testing, particles should freeze on pause (like in-game), otherwise they keep going CmpPtr cmpParticleManager(*GetSimulation2(), SYSTEM_ENTITY); if (cmpParticleManager) cmpParticleManager->SetUseSimTime(m_IsTesting); } void AtlasViewGame::SaveState(const std::wstring& label) { delete m_SavedStates[label]; // in case it already exists m_SavedStates[label] = SimState::Freeze(); } void AtlasViewGame::RestoreState(const std::wstring& label) { SimState* simState = m_SavedStates[label]; if (! simState) return; simState->Thaw(); } std::wstring AtlasViewGame::DumpState(bool binary) { std::stringstream stream; if (binary) { if (! g_Game->GetSimulation2()->SerializeState(stream)) return L"(internal error)"; // We can't return raw binary data, because we want to handle it with wxJS which // doesn't like \0 bytes in strings, so return it as hex static const char digits[] = "0123456789abcdef"; std::string str = stream.str(); std::wstring ret; ret.reserve(str.length()*3); for (size_t i = 0; i < str.length(); ++i) { ret += digits[(unsigned char)str[i] >> 4]; ret += digits[(unsigned char)str[i] & 0x0f]; ret += ' '; } return ret; } else { if (! g_Game->GetSimulation2()->DumpDebugState(stream)) return L"(internal error)"; return wstring_from_utf8(stream.str()); } } void AtlasViewGame::SetBandbox(bool visible, float x0, float y0, float x1, float y1) { m_BandboxArray.clear(); if (visible) { // Make sure corners are arranged in correct order if (x0 > x1) std::swap(x0, x1); if (y0 > y1) std::swap(y0, y1); // Bandbox is draw as lines comprising two rectangles SBandboxVertex vert[] = { // Black - outer rectangle SBandboxVertex(x0, y0, 0, 0, 0, 255), SBandboxVertex(x1, y0, 0, 0, 0, 255), SBandboxVertex(x1, y1, 0, 0, 0, 255), SBandboxVertex(x0, y1, 0, 0, 0, 255), // White - inner rectangle SBandboxVertex(x0+1.0f, y0+1.0f, 255, 255, 255, 255), SBandboxVertex(x1-1.0f, y0+1.0f, 255, 255, 255, 255), SBandboxVertex(x1-1.0f, y1-1.0f, 255, 255, 255, 255), SBandboxVertex(x0+1.0f, y1-1.0f, 255, 255, 255, 255) }; for (size_t i = 0; i < 4; ++i) { m_BandboxArray.push_back(vert[i]); m_BandboxArray.push_back(vert[(i+1)%4]); } for (size_t i = 0; i < 4; ++i) { m_BandboxArray.push_back(vert[i+4]); m_BandboxArray.push_back(vert[(i+1)%4+4]); } } } ////////////////////////////////////////////////////////////////////////// AtlasViewNone* view_None = NULL; AtlasViewGame* view_Game = NULL; AtlasViewActor* view_Actor = NULL; AtlasView::~AtlasView() { } AtlasView* AtlasView::GetView(int /*eRenderView*/ view) { switch (view) { case AtlasMessage::eRenderView::NONE: return AtlasView::GetView_None(); case AtlasMessage::eRenderView::GAME: return AtlasView::GetView_Game(); case AtlasMessage::eRenderView::ACTOR: return AtlasView::GetView_Actor(); default: debug_warn(L"Invalid view type"); return AtlasView::GetView_None(); } } AtlasView* AtlasView::GetView_None() { if (! view_None) view_None = new AtlasViewNone(); return view_None; } AtlasViewGame* AtlasView::GetView_Game() { if (! view_Game) view_Game = new AtlasViewGame(); return view_Game; } AtlasViewActor* AtlasView::GetView_Actor() { if (! view_Actor) view_Actor = new AtlasViewActor(); return view_Actor; } void AtlasView::DestroyViews() { delete view_None; view_None = NULL; delete view_Game; view_Game = NULL; delete view_Actor; view_Actor = NULL; }