diff --git a/binaries/data/mods/_test.sim/simulation/templates/inherit-special.xml b/binaries/data/mods/_test.sim/simulation/templates/inherit-special.xml new file mode 100644 index 0000000000..e103d1ae2b --- /dev/null +++ b/binaries/data/mods/_test.sim/simulation/templates/inherit-special.xml @@ -0,0 +1,2 @@ + +test diff --git a/binaries/data/mods/_test.sim/simulation/templates/unit.xml b/binaries/data/mods/_test.sim/simulation/templates/unit.xml new file mode 100644 index 0000000000..d1c587102e --- /dev/null +++ b/binaries/data/mods/_test.sim/simulation/templates/unit.xml @@ -0,0 +1,13 @@ + + + + example + + + + 0 + upright + false + + + diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index 9c657ff6a8..ee2009152f 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -44,16 +44,13 @@ public: m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); - m_NextId = SYSTEM_ENTITY + 1; - m_DeltaTime = 0.0; // (can't call ResetState here since the scripts haven't been loaded yet) } void ResetState(bool skipGui) { - m_ComponentManager.DestroyAllComponents(); + m_ComponentManager.ResetState(); - m_NextId = SYSTEM_ENTITY + 1; m_DeltaTime = 0.0; CParamNode noParam; @@ -77,17 +74,15 @@ public: bool LoadScripts(const VfsPath& path); LibError ReloadChangedFile(const VfsPath& path); - entity_id_t AllocateNewEntity(); void AddComponent(entity_id_t ent, EComponentTypeId cid, const CParamNode& paramNode); - entity_id_t AddEntity(const std::wstring& templateName, entity_id_t preferredId); + entity_id_t AddEntity(const std::wstring& templateName, entity_id_t ent); void Update(float frameTime); void Interpolate(float frameTime); CSimContext m_SimContext; CComponentManager m_ComponentManager; - entity_id_t m_NextId; double m_DeltaTime; std::set m_LoadedScripts; @@ -134,28 +129,17 @@ LibError CSimulation2Impl::ReloadChangedFile(const VfsPath& path) return INFO::OK; } -entity_id_t CSimulation2Impl::AllocateNewEntity() -{ - return m_NextId++; -} - void CSimulation2Impl::AddComponent(entity_id_t ent, EComponentTypeId cid, const CParamNode& paramNode) { m_ComponentManager.AddComponent(ent, cid, paramNode); } -entity_id_t CSimulation2Impl::AddEntity(const std::wstring& templateName, entity_id_t preferredId) +entity_id_t CSimulation2Impl::AddEntity(const std::wstring& templateName, entity_id_t ent) { CmpPtr tempMan(m_SimContext, SYSTEM_ENTITY); debug_assert(!tempMan.null()); - entity_id_t ent = preferredId; - // TODO: should check if this entity is already defined (might happen with bogus map files) - // and choose a new ID in that case - - // Make sure any newly allocated IDs won't conflict with ones that are already added - if (m_NextId <= ent) - m_NextId = ent + 1; + // TODO: should assert that ent doesn't exist const CParamNode* tmpl = tempMan->LoadTemplate(ent, templateName, -1); if (!tmpl) @@ -230,12 +214,27 @@ CSimulation2::~CSimulation2() entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { - return m->AddEntity(templateName, m->AllocateNewEntity()); + return m->AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId) { - return m->AddEntity(templateName, preferredId); + return m->AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId)); +} + +entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName) +{ + return m->AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity()); +} + +void CSimulation2::DestroyEntity(entity_id_t ent) +{ + m->m_ComponentManager.DestroyComponentsSoon(ent); +} + +void CSimulation2::FlushDestroyedEntities() +{ + m->m_ComponentManager.FlushDestroyedComponents(); } IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const @@ -258,11 +257,6 @@ const CSimulation2::InterfaceList& CSimulation2::GetEntitiesWithInterface(int ii return m->m_ComponentManager.GetEntitiesWithInterface(iid); } -entity_id_t CSimulation2::AllocateNewEntity() -{ - return m->AllocateNewEntity(); -} - const CSimContext& CSimulation2::GetSimContext() const { return m->m_SimContext; @@ -310,13 +304,11 @@ bool CSimulation2::DumpDebugState(std::ostream& stream) bool CSimulation2::SerializeState(std::ostream& stream) { - // TODO: need to save m->m_NextId return m->m_ComponentManager.SerializeState(stream); } bool CSimulation2::DeserializeState(std::istream& stream) { - // TODO: need to update m->m_NextId // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } diff --git a/source/simulation2/Simulation2.h b/source/simulation2/Simulation2.h index 66c1d061c6..6a6692db09 100644 --- a/source/simulation2/Simulation2.h +++ b/source/simulation2/Simulation2.h @@ -71,8 +71,27 @@ public: void Update(float frameTime); void Interpolate(float frameTime); + /** + * Construct a new entity and add it to the world. + * @param templateName see ICmpTemplateManager for syntax + * @return the new entity ID, or INVALID_ENTITY on error + */ entity_id_t AddEntity(const std::wstring& templateName); entity_id_t AddEntity(const std::wstring& templateName, entity_id_t preferredId); + entity_id_t AddLocalEntity(const std::wstring& templateName); + + /** + * Destroys the specified entity, once FlushDestroyedEntities is called. + * Has no effect if the entity does not exist, or has already been added to the destruction queue. + */ + void DestroyEntity(entity_id_t ent); + + /** + * Does the actual destruction of entities from DestroyEntity. + * This should be called at the beginning of each frame or after an Update message. + */ + void FlushDestroyedEntities(); + IComponent* QueryInterface(entity_id_t ent, int iid) const; void PostMessage(entity_id_t ent, const CMessage& msg) const; void BroadcastMessage(const CMessage& msg) const; @@ -88,8 +107,6 @@ public: bool SerializeState(std::ostream& stream); bool DeserializeState(std::istream& stream); - entity_id_t AllocateNewEntity(); - private: CSimulation2Impl* m; diff --git a/source/simulation2/components/CCmpTemplateManager.cpp b/source/simulation2/components/CCmpTemplateManager.cpp index d507bd4c7f..c5e8c6b6dc 100644 --- a/source/simulation2/components/CCmpTemplateManager.cpp +++ b/source/simulation2/components/CCmpTemplateManager.cpp @@ -88,8 +88,10 @@ public: virtual std::vector FindAllTemplates(); private: - // Map from template XML filename to last loaded valid template data - // (We store "last loaded valid" to behave more nicely when hotloading broken files) + // Map from template name (XML filename or special |-separated string) to the most recently + // loaded valid template data. + // (Failed loads won't remove valid entries under the same name, so we behave more nicely + // when hotloading broken files) std::map m_TemplateFileData; // Remember the template used by each entity, so we can return them @@ -100,10 +102,15 @@ private: // (Re)loads the given template, regardless of whether it exists already, // and saves into m_TemplateFileData. Also loads any parents that are not yet // loaded. Returns false on error. + // @param templateName XML filename to load (not a |-separated string) bool LoadTemplateFile(const std::wstring& templateName, int depth); // Constructs a standard static-decorative-object template for the given actor void ConstructTemplateActor(const std::wstring& actorName, CParamNode& out); + + // Copy the non-interactive components of an entity template (position, actor, etc) into + // a new entity template + void CopyPreviewSubset(CParamNode& out, const CParamNode& in); }; REGISTER_COMPONENT_TYPE(TemplateManager) @@ -112,24 +119,11 @@ const CParamNode* CCmpTemplateManager::LoadTemplate(entity_id_t ent, const std:: { m_LatestTemplates[ent] = templateName; - bool isNew = (m_TemplateFileData.find(templateName) == m_TemplateFileData.end()); - - if (isNew) + // Load the template if necessary + if (!LoadTemplateFile(templateName, 0)) { - // Handle special case "actor|foo" - if (templateName.find(L"actor|") == 0) - { - ConstructTemplateActor(templateName.substr(6), m_TemplateFileData[templateName]); - } - else - { - // Load it as a plain XML file - if (!LoadTemplateFile(templateName, 0)) - { - LOGERROR(L"Failed to load entity template '%ls'", templateName.c_str()); - return NULL; - } - } + LOGERROR(L"Failed to load entity template '%ls'", templateName.c_str()); + return NULL; } // TODO: Eventually we need to support techs in here, and return a different template per playerID @@ -164,6 +158,10 @@ std::wstring CCmpTemplateManager::GetCurrentTemplateName(entity_id_t ent) bool CCmpTemplateManager::LoadTemplateFile(const std::wstring& templateName, int depth) { + // If this file was already loaded, we don't need to do anything + if (m_TemplateFileData.find(templateName) != m_TemplateFileData.end()) + return true; + // Handle infinite loops more gracefully than running out of stack space and crashing if (depth > 100) { @@ -171,6 +169,30 @@ bool CCmpTemplateManager::LoadTemplateFile(const std::wstring& templateName, int return false; } + // Handle special case "actor|foo" + if (templateName.find(L"actor|") == 0) + { + ConstructTemplateActor(templateName.substr(6), m_TemplateFileData[templateName]); + return true; + } + + // Handle special case "preview|foo" + if (templateName.find(L"preview|") == 0) + { + // Load the base entity template, if it wasn't already loaded + std::wstring baseName = templateName.substr(8); + if (!LoadTemplateFile(baseName, depth+1)) + { + LOGERROR(L"Failed to load entity template '%ls'", baseName.c_str()); + return NULL; + } + // Copy a subset to the requested template + CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]); + return true; + } + + // Normal case: templateName is an XML file: + VfsPath path = VfsPath(TEMPLATE_ROOT) / (templateName + L".xml"); CXeromyces xero; PSRETURN ok = xero.Load(path); @@ -183,14 +205,18 @@ bool CCmpTemplateManager::LoadTemplateFile(const std::wstring& templateName, int { std::wstring parentName(parentStr.begin(), parentStr.end()); - // If the parent wasn't already loaded, then load it. - if (m_TemplateFileData.find(parentName) == m_TemplateFileData.end()) + // To prevent needless complexity in template design, we don't allow |-separated strings as parents + if (parentName.find('|') != parentName.npos) { - if (!LoadTemplateFile(parentName, depth+1)) - { - LOGERROR(L"Failed to load parent '%ls' of entity template '%ls'", parentName.c_str(), templateName.c_str()); - return false; - } + LOGERROR(L"Invalid parent '%ls' in entity template '%ls'", parentName.c_str(), templateName.c_str()); + return false; + } + + // Ensure the parent is loaded + if (!LoadTemplateFile(parentName, depth+1)) + { + LOGERROR(L"Failed to load parent '%ls' of entity template '%ls'", parentName.c_str(), templateName.c_str()); + return false; } CParamNode& parentData = m_TemplateFileData[parentName]; @@ -228,8 +254,10 @@ static LibError AddToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(f { std::vector& templates = *(std::vector*)cbData; + // Strip the .xml extension + VfsPath pathstem = change_extension(pathname, L""); // Strip the root from the path - std::wstring name = pathname.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); + std::wstring name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); // We want to ignore template_*.xml templates, since they should never be built in the editor if (name.substr(0, 9) == L"template_") @@ -270,3 +298,19 @@ std::vector CCmpTemplateManager::FindAllTemplates() return templates; } + +void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& in) +{ + // We only want to include components which are necessary (for the visual previewing of an entity) + // and safe (i.e. won't do anything that affects the synchronised simulation state), so additions + // to this list should be carefully considered + std::set permittedComponentTypes; + permittedComponentTypes.insert("Position"); + permittedComponentTypes.insert("VisualActor"); + // (This could be initialised once and reused, but it's not worth the effort) + + CParamNode::LoadXMLString(out, ""); + out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes); + // In the future, we might want to add some extra flags to certain components, to indicate they're + // running in 'preview' mode and should not e.g. register with global managers +} diff --git a/source/simulation2/components/ICmpTemplateManager.h b/source/simulation2/components/ICmpTemplateManager.h index 64701c91ff..e0d539933a 100644 --- a/source/simulation2/components/ICmpTemplateManager.h +++ b/source/simulation2/components/ICmpTemplateManager.h @@ -34,8 +34,15 @@ public: * from parent XML files), and applies the techs that are currently active for * player 'playerID', for use with a new entity 'ent'. * The returned CParamNode must not be used for any entities other than 'ent'. - * Alternatively, if templateName is of the form "actor|foo" then it will load a - * default stationary entity template that uses actor "foo". + * + * If templateName is of the form "actor|foo" then it will load a default + * stationary entity template that uses actor "foo". (This is a convenience to + * avoid the need for hundreds of tiny decorative-object entity templates.) + * + * If templateName is of the form "preview|foo" then it will load a template + * based on entity template "foo" with the non-graphical components removed. + * (This is for previewing construction/placement of units.) + * * @return NULL on error */ virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::wstring& templateName, int playerID) = 0; @@ -53,7 +60,7 @@ public: /** * Returns a list of strings that could be validly passed as @c templateName to LoadTemplate. - * (This includes "actor|foo" names). + * (This includes "actor|foo" etc names). * Intended for use by the map editor. This is likely to be quite slow. */ virtual std::vector FindAllTemplates() = 0; diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp index 812376edfa..cbd63678a7 100644 --- a/source/simulation2/system/ComponentManager.cpp +++ b/source/simulation2/system/ComponentManager.cpp @@ -48,11 +48,13 @@ CComponentManager::CComponentManager(const CSimContext& context, bool skipScript #undef COMPONENT m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY); + + ResetState(); } CComponentManager::~CComponentManager() { - DestroyAllComponents(); + ResetState(); // Release GC roots std::map::iterator it = m_ComponentTypesById.begin(); @@ -234,7 +236,7 @@ void CComponentManager::Script_BroadcastMessage(void* cbdata, int mtid, CScriptV delete msg; } -void CComponentManager::DestroyAllComponents() +void CComponentManager::ResetState() { // Delete all IComponents std::map >::iterator iit = m_ComponentsByTypeId.begin(); @@ -250,6 +252,12 @@ void CComponentManager::DestroyAllComponents() m_ComponentsByInterface.clear(); m_ComponentsByTypeId.clear(); + + m_DestructionQueue.clear(); + + // Reset IDs + m_NextEntityId = SYSTEM_ENTITY + 1; + m_NextLocalEntityId = FIRST_LOCAL_ENTITY; } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, @@ -309,6 +317,34 @@ CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(Interface return CID__Invalid; } +entity_id_t CComponentManager::AllocateNewEntity() +{ + entity_id_t id = m_NextEntityId++; + // TODO: check for overflow + return id; +} + +entity_id_t CComponentManager::AllocateNewLocalEntity() +{ + entity_id_t id = m_NextLocalEntityId++; + // TODO: check for overflow + return id; +} + +entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId) +{ + // TODO: ensure this ID hasn't been allocated before + // (this might occur with broken map files) + entity_id_t id = preferredId; + + // Ensure this ID won't be allocated again + if (id >= m_NextEntityId) + m_NextEntityId = id+1; + // TODO: check for overflow + + return id; +} + bool CComponentManager::AddComponent(entity_id_t ent, ComponentTypeId cid, const CParamNode& paramNode) { IComponent* component = ConstructComponent(ent, cid); @@ -360,6 +396,11 @@ IComponent* CComponentManager::ConstructComponent(entity_id_t ent, ComponentType // Store a reference to the new component emap1.insert(std::make_pair(ent, component)); emap2.insert(std::make_pair(ent, component)); + // TODO: We need to more careful about this - if an entity is constructed by a component + // while we're iterating over all components, this will invalidate the iterators and everything + // will break. + // We probably need some kind of delayed addition, so they get pushed onto a queue and then + // inserted into the world later on. (Be careful about immediation deletion in that case, too.) return component; } @@ -375,9 +416,44 @@ void CComponentManager::AddMockComponent(entity_id_t ent, InterfaceId iid, IComp emap1.insert(std::make_pair(ent, &component)); } +void CComponentManager::DestroyComponentsSoon(entity_id_t ent) +{ + m_DestructionQueue.push_back(ent); +} + +void CComponentManager::FlushDestroyedComponents() +{ + for (std::vector::iterator it = m_DestructionQueue.begin(); it != m_DestructionQueue.end(); ++it) + { + entity_id_t ent = *it; + + // Destroy the components, and remove from m_ComponentsByTypeId: + std::map >::iterator iit = m_ComponentsByTypeId.begin(); + for (; iit != m_ComponentsByTypeId.end(); ++iit) + { + std::map::iterator eit = iit->second.find(ent); + if (eit != iit->second.end()) + { + eit->second->Deinit(m_SimContext); + m_ComponentTypesById[iit->first].dealloc(eit->second); + iit->second.erase(ent); + } + } + + // Remove from m_ComponentsByInterface + std::map >::iterator ifcit = m_ComponentsByInterface.begin(); + for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) + { + ifcit->second.erase(ent); + } + } + + m_DestructionQueue.clear(); +} + IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const { - std::map >::const_iterator iit = m_ComponentsByInterface.find(iid); + std::map >::const_iterator iit = m_ComponentsByInterface.find(iid); if (iit == m_ComponentsByInterface.end()) { // Invalid iid, or no entities implement this interface @@ -397,7 +473,7 @@ IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) static std::map g_EmptyEntityMap; const std::map& CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const { - std::map >::const_iterator iit = m_ComponentsByInterface.find(iid); + std::map >::const_iterator iit = m_ComponentsByInterface.find(iid); if (iit == m_ComponentsByInterface.end()) { // Invalid iid, or no entities implement this interface diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h index abf3470f68..ebb3930e03 100644 --- a/source/simulation2/system/ComponentManager.h +++ b/source/simulation2/system/ComponentManager.h @@ -97,6 +97,25 @@ public: */ std::string LookupComponentTypeName(ComponentTypeId cid) const; + /** + * Returns a new entity ID that has never been used before. + * This affects the simulation state so it must only be called in network-synchronised ways. + */ + entity_id_t AllocateNewEntity(); + + /** + * Returns a new local entity ID that has never been used before. + * This entity will not be synchronised over the network, stored in saved games, etc. + */ + entity_id_t AllocateNewLocalEntity(); + + /** + * Returns a new entity ID that has never been used before. + * If possible, returns preferredId, and ensures this ID won't be allocated again. + * This affects the simulation state so it must only be called in network-synchronised ways. + */ + entity_id_t AllocateNewEntity(entity_id_t preferredId); + /** * Constructs a component of type 'cid', initialised with data 'paramNode', * and attaches it to entity 'ent'. @@ -120,16 +139,36 @@ public: */ IComponent* ConstructComponent(entity_id_t ent, ComponentTypeId cid); + /** + * Destroys all the components belonging to the specified entity when FlushDestroyedComponents is called. + * Has no effect if the entity does not exist, or has already been added to the destruction queue. + */ + void DestroyComponentsSoon(entity_id_t ent); + + /** + * Does the actual destruction of components from DestroyComponentsSoon. + * This must not be called if the component manager is on the call stack (since it + * will break internal iterators). + */ + void FlushDestroyedComponents(); + IComponent* QueryInterface(entity_id_t ent, InterfaceId iid) const; const std::map& GetEntitiesWithInterface(InterfaceId iid) const; void PostMessage(entity_id_t ent, const CMessage& msg) const; void BroadcastMessage(const CMessage& msg) const; - void DestroyAllComponents(); + /** + * Resets the dynamic simulation state (deletes all entities, resets entity ID counters; + * doesn't unload/reload component scripts). + */ + void ResetState(); + // Various state serialization functions: bool ComputeStateHash(std::string& outHash); bool DumpDebugState(std::ostream& stream); + // FlushDestroyedComponents must be called before SerializeState (since the destruction queue + // won't get serialized) bool SerializeState(std::ostream& stream); bool DeserializeState(std::istream& stream); @@ -161,7 +200,11 @@ private: // TODO: maintaining both ComponentsBy* is nasty; can we get rid of one, // while keeping QueryInterface and PostMessage sufficiently efficient? + std::vector m_DestructionQueue; + ComponentTypeId m_NextScriptComponentTypeId; + entity_id_t m_NextEntityId; + entity_id_t m_NextLocalEntityId; friend class TestComponentManager; }; diff --git a/source/simulation2/system/ComponentManagerSerialization.cpp b/source/simulation2/system/ComponentManagerSerialization.cpp index 1315306ad9..150c5c4b1f 100644 --- a/source/simulation2/system/ComponentManagerSerialization.cpp +++ b/source/simulation2/system/ComponentManagerSerialization.cpp @@ -55,6 +55,9 @@ bool CComponentManager::DumpDebugState(std::ostream& stream) n << "- id: " << cit->first; serializer.TextLine(n.str()); + if (ENTITY_IS_LOCAL(cit->first)) + serializer.TextLine(" type: local"); + std::map::const_iterator ctit = cit->second.begin(); for (; ctit != cit->second.end(); ++ctit) { @@ -88,6 +91,10 @@ bool CComponentManager::ComputeStateHash(std::string& outHash) std::map::const_iterator eit = cit->second.begin(); for (; eit != cit->second.end(); ++eit) { + // Don't hash local entities + if (ENTITY_IS_LOCAL(eit->first)) + continue; + serializer.NumberU32_Unbounded("entity id", eit->first); eit->second->Serialize(serializer); } @@ -125,6 +132,12 @@ bool CComponentManager::SerializeState(std::ostream& stream) { CStdSerializer serializer(m_ScriptInterface, stream); + // We don't serialize the destruction queue, since we'd have to be careful to skip local entities etc + // and it's (hopefully) easier to just expect callers to flush the queue before serializing + debug_assert(m_DestructionQueue.empty()); + + serializer.NumberU32_Unbounded("next entity id", m_NextEntityId); + uint32_t numComponentTypes = 0; std::map >::const_iterator cit; @@ -153,12 +166,29 @@ bool CComponentManager::SerializeState(std::ostream& stream) serializer.StringASCII("name", ctit->second.name, 0, 255); - uint32_t numComponents = cit->second.size(); + std::map::const_iterator eit; + + // Count the components before serializing any of them + uint32_t numComponents = 0; + for (eit = cit->second.begin(); eit != cit->second.end(); ++eit) + { + // Don't serialize local entities + if (ENTITY_IS_LOCAL(eit->first)) + continue; + + numComponents++; + } + + // Emit the count serializer.NumberU32_Unbounded("num components", numComponents); - std::map::const_iterator eit = cit->second.begin(); - for (; eit != cit->second.end(); ++eit) + // Serialize the components now + for (eit = cit->second.begin(); eit != cit->second.end(); ++eit) { + // Don't serialize local entities + if (ENTITY_IS_LOCAL(eit->first)) + continue; + serializer.NumberU32_Unbounded("entity id", eit->first); eit->second->Serialize(serializer); } @@ -172,7 +202,9 @@ bool CComponentManager::DeserializeState(std::istream& stream) { CStdDeserializer deserializer(m_ScriptInterface, stream); - DestroyAllComponents(); + ResetState(); + + deserializer.NumberU32_Unbounded(m_NextEntityId); // TODO: use sensible bounds uint32_t numComponentTypes; deserializer.NumberU32_Unbounded(numComponentTypes); diff --git a/source/simulation2/system/Entity.h b/source/simulation2/system/Entity.h index eb3d9a1a6d..e455b84910 100644 --- a/source/simulation2/system/Entity.h +++ b/source/simulation2/system/Entity.h @@ -41,4 +41,22 @@ const entity_id_t INVALID_ENTITY = 0; */ const entity_id_t SYSTEM_ENTITY = 1; +// Entities are split into two kinds: +// "Normal" (for most entities) +// "Local" (for entities that only exist on the local machine, aren't synchronised across the network, +// aren't retained in saved games, etc) +// The distinction is encoded in the entity ID, so that they're easily distinguished. +// +// We want all entity_id_ts to fit in jsval ints, i.e. 1-2^30 .. 2^30-1 (inclusive) +// We want them to be unsigned ints (actually it shouldn't matter but unsigned seems simpler) +// We want 1 tag bit +// So we have 1 JS-reserved bit, 1 unused sign bit, 1 local tag bit, 29 counter bits +// (0.5B entities should be plenty) + +#define ENTITY_TAGMASK (1 << 29) +#define ENTITY_IS_NORMAL(id) (((id) & ENTITY_TAGMASK) == 0) +#define ENTITY_IS_LOCAL(id) (((id) & ENTITY_TAGMASK) == ENTITY_TAGMASK) +const entity_id_t FIRST_LOCAL_ENTITY = ENTITY_TAGMASK; + + #endif // INCLUDED_SIM2_ENTITY diff --git a/source/simulation2/system/ParamNode.cpp b/source/simulation2/system/ParamNode.cpp index 49b6906037..9c8d227c30 100644 --- a/source/simulation2/system/ParamNode.cpp +++ b/source/simulation2/system/ParamNode.cpp @@ -66,6 +66,19 @@ void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element) // TODO: support some kind of 'delete' marker, for use with inheritance } +void CParamNode::CopyFilteredChildrenOfChild(const CParamNode& src, const char* name, const std::set& permitted) +{ + ChildrenMap::iterator dstChild = m_Childs.find(name); + ChildrenMap::const_iterator srcChild = src.m_Childs.find(name); + if (dstChild == m_Childs.end() || srcChild == src.m_Childs.end()) + return; // error + + ChildrenMap::const_iterator it = srcChild->second.m_Childs.begin(); + for (; it != srcChild->second.m_Childs.end(); ++it) + if (permitted.count(it->first)) + dstChild->second.m_Childs[it->first] = it->second; +} + const CParamNode* CParamNode::GetChild(const char* name) const { ChildrenMap::const_iterator it = m_Childs.find(name); diff --git a/source/simulation2/system/ParamNode.h b/source/simulation2/system/ParamNode.h index 50003a93e4..5298d9ebde 100644 --- a/source/simulation2/system/ParamNode.h +++ b/source/simulation2/system/ParamNode.h @@ -32,22 +32,32 @@ public: typedef std::map ChildrenMap; /** - * Loads the XML data specified by 'file' into the node 'ret'. - * Any existing data in 'ret' will be overwritten or else kept, so this + * Loads the XML data specified by @a file into the node @a ret. + * Any existing data in @a ret will be overwritten or else kept, so this * can be called multiple times to build up a node from multiple inputs. */ static void LoadXML(CParamNode& ret, const XMBFile& file); /** - * See LoadXML, but parses the XML string 'xml'. - * @return error code if parsing failed, else PSRETURN_OK + * See LoadXML, but parses the XML string @a xml. + * @return error code if parsing failed, else @c PSRETURN_OK */ static PSRETURN LoadXMLString(CParamNode& ret, const char* xml); + /** + * Finds the childs named @a name from @a src and from @a this, and copies the source child's children + * which are in the @a permitted set into this node's child. + * Intended for use as a filtered clone of XML files. + * @a this and @a src must have childs named @a name. + */ + void CopyFilteredChildrenOfChild(const CParamNode& src, const char* name, const std::set& permitted); + /** * Returns the (unique) child node with the given name, or NULL if there is none. */ const CParamNode* GetChild(const char* name) const; + // (Children are returned as const in order to allow future optimisations, where we assume + // a node is always modified explicitly and not indirectly via its children, e.g. to cache jsvals) /** * Returns the content of this node as a string diff --git a/source/simulation2/tests/test_CmpTemplateManager.h b/source/simulation2/tests/test_CmpTemplateManager.h index 2bb2978c23..e3bd656245 100644 --- a/source/simulation2/tests/test_CmpTemplateManager.h +++ b/source/simulation2/tests/test_CmpTemplateManager.h @@ -74,7 +74,15 @@ public: const CParamNode* actor = tempMan->LoadTemplate(ent2, L"actor|example", -1); TS_ASSERT(actor != NULL); TS_ASSERT_WSTR_EQUALS(actor->ToXML(), L"0uprightfalseexample"); - } + + const CParamNode* preview = tempMan->LoadTemplate(ent2, L"preview|unit", -1); + TS_ASSERT(preview != NULL); + TS_ASSERT_WSTR_EQUALS(preview->ToXML(), L"0uprightfalseexample"); + + const CParamNode* previewactor = tempMan->LoadTemplate(ent2, L"preview|actor|example2", -1); + TS_ASSERT(previewactor != NULL); + TS_ASSERT_WSTR_EQUALS(previewactor->ToXML(), L"0uprightfalseexample2"); +} void test_LoadTemplate_errors() { @@ -94,11 +102,13 @@ public: TS_ASSERT(tempMan->LoadTemplate(ent2, L"nonexistent", -1) == NULL); - const CParamNode* inherit_loop = tempMan->LoadTemplate(ent2, L"inherit-loop", -1); - TS_ASSERT(inherit_loop == NULL); + TS_ASSERT(tempMan->LoadTemplate(ent2, L"inherit-loop", -1) == NULL); - const CParamNode* inherit_broken = tempMan->LoadTemplate(ent2, L"inherit-broken", -1); - TS_ASSERT(inherit_broken == NULL); + TS_ASSERT(tempMan->LoadTemplate(ent2, L"inherit-broken", -1) == NULL); + + TS_ASSERT(tempMan->LoadTemplate(ent2, L"inherit-special", -1) == NULL); + + TS_ASSERT(tempMan->LoadTemplate(ent2, L"preview|nonexistent", -1) == NULL); } void test_LoadTemplate_multiple() diff --git a/source/simulation2/tests/test_ComponentManager.h b/source/simulation2/tests/test_ComponentManager.h index 46bed3dda0..d3b7b801db 100644 --- a/source/simulation2/tests/test_ComponentManager.h +++ b/source/simulation2/tests/test_ComponentManager.h @@ -71,6 +71,29 @@ public: TS_ASSERT_EQUALS(man.LookupCID("Test1B"), (int)CID_Test1B); } + void test_AllocateNewEntity() + { + CSimContext context; + CComponentManager man(context); + + TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); + TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)3); + TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)4); + TS_ASSERT_EQUALS(man.AllocateNewEntity(100), (u32)100); + TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)101); + // TODO: + // TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)102); + + TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); + TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY+1); + + man.ResetState(); + + TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); + TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)3); + TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); + } + void test_AddComponent_errors() { CSimContext context; @@ -403,7 +426,7 @@ public: CComponentManager man(context); man.LoadComponentTypes(); - entity_id_t ent1 = 1, ent2 = 2; + entity_id_t ent1 = 1, ent2 = 2, ent3 = FIRST_LOCAL_ENTITY; CParamNode noParam; CParamNode testParam; @@ -412,10 +435,12 @@ public: man.AddComponent(ent1, CID_Test1A, noParam); man.AddComponent(ent1, CID_Test2A, noParam); man.AddComponent(ent2, CID_Test1A, testParam); + man.AddComponent(ent3, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); + TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream)); @@ -430,6 +455,11 @@ public: " Test1A:\n" " x: 1234\n" "\n" + "- id: 536870912\n" + " type: local\n" + " Test2A:\n" + " x: 21000\n" + "\n" ); std::string hash; @@ -441,7 +471,8 @@ public: std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); - TS_ASSERT_STREAM(stateStream, 56, + TS_ASSERT_STREAM(stateStream, 60, + "\x02\x00\x00\x00" // next entity ID "\x02\x00\x00\x00" // num component types "\x06\x00\x00\x00Test1A" "\x02\x00\x00\x00" // num ents @@ -462,12 +493,14 @@ public: TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man2.QueryInterface(ent2, IID_Test1) == NULL); + TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 1234); + TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); } void test_script_serialization() diff --git a/source/simulation2/tests/test_Simulation2.h b/source/simulation2/tests/test_Simulation2.h index 53e96feb5d..4d22d7d33f 100644 --- a/source/simulation2/tests/test_Simulation2.h +++ b/source/simulation2/tests/test_Simulation2.h @@ -18,6 +18,7 @@ #include "lib/self_test.h" #include "simulation2/Simulation2.h" +#include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpTest.h" #include "graphics/Terrain.h" @@ -51,15 +52,6 @@ public: g_VFS.reset(); } - void test_AllocateNewEntity() - { - CSimulation2 sim(NULL, &m_Terrain); - sim.ResetState(true); - TS_ASSERT_EQUALS(sim.AllocateNewEntity(), (u32)2); - TS_ASSERT_EQUALS(sim.AllocateNewEntity(), (u32)3); - TS_ASSERT_EQUALS(sim.AllocateNewEntity(), (u32)4); - } - void test_AddEntity() { CSimulation2 sim(NULL, &m_Terrain); @@ -80,6 +72,57 @@ public: TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } + void test_DestroyEntity() + { + CSimulation2 sim(NULL, &m_Terrain); + TS_ASSERT(sim.LoadScripts(L"simulation/components/addentity/")); + + sim.ResetState(true); + + entity_id_t ent1 = sim.AddEntity(L"test1"); + entity_id_t ent2 = sim.AddEntity(L"test1"); + entity_id_t ent3 = sim.AddEntity(L"test1"); + + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); + + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 999); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); + + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test1))->GetX(), 999); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test2))->GetX(), 12345); + + sim.DestroyEntity(ent2); // mark it for deletion + + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 999); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); + + sim.FlushDestroyedEntities(); // actually delete it + + TS_ASSERT(sim.QueryInterface(ent2, IID_Test1) == NULL); + TS_ASSERT(sim.QueryInterface(ent2, IID_Test2) == NULL); + + sim.FlushDestroyedEntities(); // nothing in the queue + + sim.DestroyEntity(ent2); + sim.FlushDestroyedEntities(); // already deleted + + // Other entities weren't affected + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test1))->GetX(), 999); + TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test2))->GetX(), 12345); + + sim.DestroyEntity(ent3); // mark it for deletion twice + sim.DestroyEntity(ent3); + sim.FlushDestroyedEntities(); + TS_ASSERT(sim.QueryInterface(ent3, IID_Test1) == NULL); + TS_ASSERT(sim.QueryInterface(ent3, IID_Test2) == NULL); + + // Messages mustn't get sent to the destroyed components (else we'll crash) + sim.BroadcastMessage(CMessageTurnStart()); + } + void test_hotload_scripts() { CSimulation2 sim(NULL, &m_Terrain); diff --git a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp index dc699f37b7..27321b03da 100644 --- a/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp @@ -289,9 +289,9 @@ END_COMMAND(SetObjectSettings); ////////////////////////////////////////////////////////////////////////// - -static size_t g_PreviewUnitID = invalidUnitId; static CStrW g_PreviewUnitName; +static entity_id_t g_PreviewEntityID = INVALID_ENTITY; // used if g_UseSimulation2 +static size_t g_PreviewUnitID = invalidUnitId; // old simulation static bool g_PreviewUnitFloating; static CVector3D GetUnitPos(const Position& pos, bool floating) @@ -343,6 +343,58 @@ static bool ParseObjectName(const CStrW& obj, bool& isEntity, CStrW& name) MESSAGEHANDLER(ObjectPreview) { + if (g_UseSimulation2) + { + // If the selection has changed... + if (*msg->id != g_PreviewUnitName) + { + // Delete old entity + if (g_PreviewEntityID != INVALID_ENTITY) + g_Game->GetSimulation2()->DestroyEntity(g_PreviewEntityID); + + // Create the new entity + if ((*msg->id).empty()) + g_PreviewEntityID = INVALID_ENTITY; + else + g_PreviewEntityID = g_Game->GetSimulation2()->AddLocalEntity(*msg->id); + + g_PreviewUnitName = *msg->id; + } + + if (g_PreviewEntityID != INVALID_ENTITY) + { + // Update the unit's position and orientation: + + CmpPtr cmpPos (*g_Game->GetSimulation2(), g_PreviewEntityID); + if (!cmpPos.null()) + { + CVector3D pos = GetUnitPos(msg->pos, false); + cmpPos->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; + } + + cmpPos->SetYRotation(entity_angle_t::FromFloat(angle)); + } + + // TODO: handle random variations somehow + // TODO: set player colour + } + + return; + } + + // Old simulation system: + CUnit* previewUnit = GetUnitManager().FindByID(g_PreviewUnitID); // Don't recreate the unit unless it's changed @@ -433,8 +485,9 @@ BEGIN_COMMAND(CreateObject) { CVector3D m_Pos; float m_Angle; - size_t m_ID; + size_t m_ID; // old simulation system size_t m_Player; + entity_id_t m_EntityID; // new simulation system void Do() { @@ -446,8 +499,7 @@ BEGIN_COMMAND(CreateObject) { // Aim from m_Pos towards msg->target CVector3D target = msg->target->GetWorldSpace(m_Pos.Y); - CVector2D dir(target.X-m_Pos.X, target.Z-m_Pos.Z); - m_Angle = atan2(dir.x, dir.y); + m_Angle = atan2(target.X-m_Pos.X, target.Z-m_Pos.Z); } else { @@ -457,14 +509,38 @@ BEGIN_COMMAND(CreateObject) // TODO: variations too m_Player = msg->settings->player; - // Get a new ID, for future reference to this unit - m_ID = GetUnitManager().GetNewID(); + if (!g_UseSimulation2) + { + // Get a new ID, for future reference to this unit + m_ID = GetUnitManager().GetNewID(); + } Redo(); } void Redo() { + if (g_UseSimulation2) + { + m_EntityID = g_Game->GetSimulation2()->AddEntity(*msg->id); + if (m_EntityID == INVALID_ENTITY) + return; + + CmpPtr cmpPos (*g_Game->GetSimulation2(), m_EntityID); + if (!cmpPos.null()) + { + cmpPos->JumpTo(entity_pos_t::FromFloat(m_Pos.X), entity_pos_t::FromFloat(m_Pos.Z)); + cmpPos->SetYRotation(entity_angle_t::FromFloat(m_Angle)); + } + + // TODO: handle random variations somehow + // TODO: set player colour + + return; + } + + // Old simulation system: + bool isEntity; CStrW name; if (ParseObjectName(*msg->id, isEntity, name)) @@ -532,6 +608,19 @@ BEGIN_COMMAND(CreateObject) void Undo() { + if (g_UseSimulation2) + { + if (m_EntityID != INVALID_ENTITY) + { + g_Game->GetSimulation2()->DestroyEntity(m_EntityID); + m_EntityID = INVALID_ENTITY; + } + + return; + } + + // Old simulation system: + CUnit* unit = GetUnitManager().FindByID(m_ID); if (unit) { diff --git a/source/tools/atlas/GameInterface/View.cpp b/source/tools/atlas/GameInterface/View.cpp index 7721602c5f..27c11690fe 100644 --- a/source/tools/atlas/GameInterface/View.cpp +++ b/source/tools/atlas/GameInterface/View.cpp @@ -159,6 +159,10 @@ void ViewGame::Update(float frameLength) { float actualFrameLength = frameLength * m_SpeedMultiplier; + // Clean up any entities destroyed during UI message processing + if (g_UseSimulation2) + g_Game->GetSimulation2()->FlushDestroyedEntities(); + if (m_SpeedMultiplier == 0.f) { // Update unit interpolation @@ -184,6 +188,10 @@ void ViewGame::Update(float frameLength) } } + // Clean up any entities destroyed during simulation update + if (g_UseSimulation2) + g_Game->GetSimulation2()->FlushDestroyedEntities(); + // Interpolate the graphics - we only want to do this once per visual frame, // not in every call to g_Game->Update g_Game->GetSimulation()->Interpolate(actualFrameLength);