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);