diff --git a/binaries/data/mods/public/simulation/components/Armour.js b/binaries/data/mods/public/simulation/components/Armour.js
index 904e3c3d23..f64b49d8b9 100644
--- a/binaries/data/mods/public/simulation/components/Armour.js
+++ b/binaries/data/mods/public/simulation/components/Armour.js
@@ -1,5 +1,16 @@
function Armour() {}
+Armour.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Armour.prototype.Init = function()
{
};
@@ -7,10 +18,9 @@ Armour.prototype.Init = function()
Armour.prototype.TakeDamage = function(hack, pierce, crush)
{
// Adjust damage values based on armour
- // (Default armour values to 0 if undefined)
- var adjHack = Math.max(0, hack - (this.template.Hack || 0));
- var adjPierce = Math.max(0, pierce - (this.template.Pierce || 0));
- var adjCrush = Math.max(0, crush - (this.template.Crush || 0));
+ var adjHack = Math.max(0, hack - this.template.Hack);
+ var adjPierce = Math.max(0, pierce - this.template.Pierce);
+ var adjCrush = Math.max(0, crush - this.template.Crush);
// Total is sum of individual damages, with minimum damage 1
var total = Math.max(1, adjHack + adjPierce + adjCrush);
@@ -22,11 +32,11 @@ Armour.prototype.TakeDamage = function(hack, pierce, crush)
Armour.prototype.GetArmourStrengths = function()
{
- // Convert attack values to numbers, default 0 if unspecified
+ // Convert attack values to numbers
return {
- hack: +(this.template.Hack || 0),
- pierce: +(this.template.Pierce || 0),
- crush: +(this.template.Crush || 0)
+ hack: +this.template.Hack,
+ pierce: +this.template.Pierce,
+ crush: +this.template.Crush
};
};
diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js
index 9e51cae7df..98f993ca36 100644
--- a/binaries/data/mods/public/simulation/components/Attack.js
+++ b/binaries/data/mods/public/simulation/components/Attack.js
@@ -1,5 +1,39 @@
function Attack() {}
+Attack.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Attack.prototype.Init = function()
{
};
diff --git a/binaries/data/mods/public/simulation/components/Builder.js b/binaries/data/mods/public/simulation/components/Builder.js
index 9fb6b1b831..9bd114fcee 100644
--- a/binaries/data/mods/public/simulation/components/Builder.js
+++ b/binaries/data/mods/public/simulation/components/Builder.js
@@ -1,5 +1,14 @@
function Builder() {}
+Builder.prototype.Schema =
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Builder.prototype.Init = function()
{
};
diff --git a/binaries/data/mods/public/simulation/components/Cost.js b/binaries/data/mods/public/simulation/components/Cost.js
index 5d1aabf62f..93cc7f6bd7 100644
--- a/binaries/data/mods/public/simulation/components/Cost.js
+++ b/binaries/data/mods/public/simulation/components/Cost.js
@@ -1,5 +1,30 @@
function Cost() {}
+Cost.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Cost.prototype.Init = function()
{
};
@@ -26,10 +51,10 @@ Cost.prototype.GetBuildTime = function()
Cost.prototype.GetResourceCosts = function()
{
return {
- "food": +(this.template.Resources.food || 0),
- "wood": +(this.template.Resources.wood || 0),
- "stone": +(this.template.Resources.stone || 0),
- "metal": +(this.template.Resources.metal || 0)
+ "food": +this.template.Resources.food,
+ "wood": +this.template.Resources.wood,
+ "stone": +this.template.Resources.stone,
+ "metal": +this.template.Resources.metal
};
};
diff --git a/binaries/data/mods/public/simulation/components/Health.js b/binaries/data/mods/public/simulation/components/Health.js
index 9b739521e7..14e2a4497e 100644
--- a/binaries/data/mods/public/simulation/components/Health.js
+++ b/binaries/data/mods/public/simulation/components/Health.js
@@ -1,5 +1,25 @@
function Health() {}
+Health.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "corpse" +
+ "" +
+ "";
+
Health.prototype.Init = function()
{
// Default to , but use if it's undefined or zero
diff --git a/binaries/data/mods/public/simulation/components/Identity.js b/binaries/data/mods/public/simulation/components/Identity.js
index 9127bb7c3c..f448a123eb 100644
--- a/binaries/data/mods/public/simulation/components/Identity.js
+++ b/binaries/data/mods/public/simulation/components/Identity.js
@@ -1,5 +1,24 @@
function Identity() {}
+Identity.prototype.Schema =
+ "" +
+ "" + // TODO: ?
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Identity.prototype.Init = function()
{
};
diff --git a/binaries/data/mods/public/simulation/components/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/ResourceGatherer.js
index 6ec33a945b..3af00175f8 100644
--- a/binaries/data/mods/public/simulation/components/ResourceGatherer.js
+++ b/binaries/data/mods/public/simulation/components/ResourceGatherer.js
@@ -1,5 +1,23 @@
function ResourceGatherer() {}
+ResourceGatherer.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
ResourceGatherer.prototype.Init = function()
{
};
diff --git a/binaries/data/mods/public/simulation/components/ResourceSupply.js b/binaries/data/mods/public/simulation/components/ResourceSupply.js
index 4fdcb97fae..21189c81f6 100644
--- a/binaries/data/mods/public/simulation/components/ResourceSupply.js
+++ b/binaries/data/mods/public/simulation/components/ResourceSupply.js
@@ -1,5 +1,35 @@
function ResourceSupply() {}
+ResourceSupply.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "food" +
+ "fish" +
+ "" +
+ "" +
+ "food" +
+ "fruit" +
+ "" +
+ "" +
+ "food" +
+ "grain" +
+ "" +
+ "" +
+ "food" +
+ "meat" +
+ "" +
+ "" +
+ "food" +
+ "milk" +
+ "" +
+ "wood" +
+ "stone" +
+ "metal" +
+ "";
+
ResourceSupply.prototype.Init = function()
{
// Current resource amount (non-negative; can be a fractional amount)
diff --git a/binaries/data/mods/public/simulation/components/Sound.js b/binaries/data/mods/public/simulation/components/Sound.js
index d3e8215daa..2518b7fb45 100644
--- a/binaries/data/mods/public/simulation/components/Sound.js
+++ b/binaries/data/mods/public/simulation/components/Sound.js
@@ -1,5 +1,15 @@
function Sound() {}
+Sound.prototype.Schema =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
Sound.prototype.Init = function()
{
};
diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp
index 70471eebc2..d401b9d5db 100644
--- a/source/ps/GameSetup/GameSetup.cpp
+++ b/source/ps/GameSetup/GameSetup.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2009 Wildfire Games.
+/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -856,6 +856,19 @@ void Init(const CmdLineArgs& args, int flags)
// (required for finding our output log files).
g_Logger = new CLogger;
+ // Special command-line mode to dump the entity schemas instead of running the game.
+ // (This must be done after loading VFS etc, but should be done before wasting time
+ // on anything else.)
+ if (args.Has("dumpSchema"))
+ {
+ CSimulation2 sim(NULL, NULL);
+ sim.LoadDefaultScripts();
+ std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
+ f << sim.GenerateSchema();
+ std::cout << "Generated entity.rng\n";
+ exit(0);
+ }
+
// Call LoadLanguage(NULL) to initialize the I18n system, but
// without loading an actual language file - translate() will
// just show the English key text, which is better than crashing
diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp
index c352c973fc..714dd988ea 100644
--- a/source/simulation2/Simulation2.cpp
+++ b/source/simulation2/Simulation2.cpp
@@ -306,3 +306,8 @@ bool CSimulation2::DeserializeState(std::istream& stream)
// TODO: need to make sure the required SYSTEM_ENTITY components get constructed
return m->m_ComponentManager.DeserializeState(stream);
}
+
+std::string CSimulation2::GenerateSchema()
+{
+ return m->m_ComponentManager.GenerateSchema();
+}
diff --git a/source/simulation2/Simulation2.h b/source/simulation2/Simulation2.h
index df1ea2e902..3bcd3014f2 100644
--- a/source/simulation2/Simulation2.h
+++ b/source/simulation2/Simulation2.h
@@ -128,6 +128,8 @@ public:
bool SerializeState(std::ostream& stream);
bool DeserializeState(std::istream& stream);
+ std::string GenerateSchema();
+
private:
CSimulation2Impl* m;
diff --git a/source/simulation2/components/CCmpFootprint.cpp b/source/simulation2/components/CCmpFootprint.cpp
index 1222c1e31d..0227f9e641 100644
--- a/source/simulation2/components/CCmpFootprint.cpp
+++ b/source/simulation2/components/CCmpFootprint.cpp
@@ -36,6 +36,23 @@ public:
CFixed_23_8 m_Size1; // height/radius
CFixed_23_8 m_Height;
+ static std::string GetSchema()
+ {
+ return
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ "";
+ }
+
virtual void Init(const CSimContext& UNUSED(context), const CParamNode& paramNode)
{
if (paramNode.GetChild("Square").IsOk())
diff --git a/source/simulation2/components/CCmpObstruction.cpp b/source/simulation2/components/CCmpObstruction.cpp
index 2caf7d918c..b5a1f3d5fb 100644
--- a/source/simulation2/components/CCmpObstruction.cpp
+++ b/source/simulation2/components/CCmpObstruction.cpp
@@ -46,6 +46,13 @@ public:
ICmpObstructionManager::tag_t m_Tag;
+ static std::string GetSchema()
+ {
+ return
+ ""
+ ""
+ "";
+ }
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_Context = &context;
diff --git a/source/simulation2/components/CCmpPosition.cpp b/source/simulation2/components/CCmpPosition.cpp
index 233455a6e1..663f426cf8 100644
--- a/source/simulation2/components/CCmpPosition.cpp
+++ b/source/simulation2/components/CCmpPosition.cpp
@@ -75,27 +75,24 @@ public:
bool m_Dirty; // true if position/rotation has changed since last TurnStart
- /*
- * Schema: (untested)
- *
- *
- *
- *
- *
- * upright
- * pitch
- * pitch-roll
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- */
+ static std::string GetSchema()
+ {
+ return
+ ""
+ ""
+ "upright"
+ "pitch"
+ "pitch-roll"
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ "";
+ }
+
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_Context = &context;
diff --git a/source/simulation2/components/CCmpUnitMotion.cpp b/source/simulation2/components/CCmpUnitMotion.cpp
index 4e17c12d06..a19017239f 100644
--- a/source/simulation2/components/CCmpUnitMotion.cpp
+++ b/source/simulation2/components/CCmpUnitMotion.cpp
@@ -56,6 +56,13 @@ public:
};
int m_State;
+ static std::string GetSchema()
+ {
+ return
+ ""
+ ""
+ "";
+ }
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_Context = &context;
diff --git a/source/simulation2/components/CCmpVisualActor.cpp b/source/simulation2/components/CCmpVisualActor.cpp
index eae6959f62..f372e6939e 100644
--- a/source/simulation2/components/CCmpVisualActor.cpp
+++ b/source/simulation2/components/CCmpVisualActor.cpp
@@ -60,6 +60,17 @@ public:
{
}
+ static std::string GetSchema()
+ {
+ return
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ "";
+ }
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
if (!context.HasUnitManager())
@@ -68,7 +79,7 @@ public:
// TODO: we should do some fancy animation of under-construction buildings rising from the ground,
// but for now we'll just use the foundation actor and ignore the normal one
std::string name;
- if (paramNode.GetChild("Foundation").IsOk())
+ if (paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk())
name = utf8_from_wstring(paramNode.GetChild("FoundationActor").ToString());
else
name = utf8_from_wstring(paramNode.GetChild("Actor").ToString());
diff --git a/source/simulation2/system/Component.h b/source/simulation2/system/Component.h
index a647f14053..831319a6ad 100644
--- a/source/simulation2/system/Component.h
+++ b/source/simulation2/system/Component.h
@@ -30,14 +30,14 @@
#define REGISTER_COMPONENT_TYPE(cname) \
void RegisterComponentType_##cname(CComponentManager& mgr) \
{ \
- mgr.RegisterComponentType(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname); \
+ mgr.RegisterComponentType(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \
CCmp##cname::ClassInit(mgr); \
}
#define REGISTER_COMPONENT_SCRIPT_WRAPPER(cname) \
void RegisterComponentType_##cname(CComponentManager& mgr) \
{ \
- mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname); \
+ mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \
CCmp##cname::ClassInit(mgr); \
}
diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp
index fca8261d87..19a7039f81 100644
--- a/source/simulation2/system/ComponentManager.cpp
+++ b/source/simulation2/system/ComponentManager.cpp
@@ -185,8 +185,18 @@ void CComponentManager::Script_RegisterComponentType(void* cbdata, int iid, std:
mustReloadComponents = true;
}
+ std::string schema = "";
+ {
+ CScriptValRooted prototype;
+ if (componentManager->m_ScriptInterface.GetProperty(ctor.get(), "prototype", prototype) &&
+ componentManager->m_ScriptInterface.HasProperty(prototype.get(), "Schema"))
+ {
+ componentManager->m_ScriptInterface.GetProperty(prototype.get(), "Schema", schema);
+ }
+ }
+
// Construct a new ComponentType, using the wrapper's alloc functions
- ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, ctor.get() };
+ ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, ctor.get() };
componentManager->m_ComponentTypesById[cid] = ct;
componentManager->m_CurrentComponent = cid; // needed by Subscribe
@@ -365,17 +375,17 @@ void CComponentManager::ResetState()
}
void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc,
- const char* name)
+ const char* name, const std::string& schema)
{
- ComponentType c = { CT_Native, iid, alloc, dealloc, name, 0 };
+ ComponentType c = { CT_Native, iid, alloc, dealloc, name, schema, 0 };
m_ComponentTypesById.insert(std::make_pair(cid, c));
m_ComponentTypeIdsByName[name] = cid;
}
void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc,
- DeallocFunc dealloc, const char* name)
+ DeallocFunc dealloc, const char* name, const std::string& schema)
{
- ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, 0 };
+ ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, schema, 0 };
m_ComponentTypesById.insert(std::make_pair(cid, c));
m_ComponentTypeIdsByName[name] = cid;
// TODO: merge with RegisterComponentType
@@ -715,3 +725,68 @@ void CComponentManager::SendGlobalMessage(const CMessage& msg) const
}
}
}
+
+
+std::string CComponentManager::GenerateSchema()
+{
+ std::string schema =
+ ""
+ ""
+ "0"
+ ""
+ ""
+ "0"
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ ""
+ "";
+
+ std::map > interfaceComponentTypes;
+
+ for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it)
+ {
+ schema +=
+ ""
+ ""
+ "" + it->second.schema + ""
+ ""
+ "";
+
+ interfaceComponentTypes[it->second.iid].push_back(it->second.name);
+ }
+
+ for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it)
+ {
+ schema += "";
+ std::vector& cts = interfaceComponentTypes[it->second];
+ for (size_t i = 0; i < cts.size(); ++i)
+ schema += "";
+ schema += "";
+ }
+
+ schema +=
+ ""
+ ""
+ ""
+ "";
+ for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it)
+ schema += "";
+ schema +=
+ ""
+ ""
+ "";
+
+ schema += "";
+
+ // TODO: pretty-print
+ return schema;
+}
diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h
index 454eb883fb..52e94ba6a0 100644
--- a/source/simulation2/system/ComponentManager.h
+++ b/source/simulation2/system/ComponentManager.h
@@ -63,6 +63,7 @@ private:
AllocFunc alloc;
DeallocFunc dealloc;
std::string name;
+ std::string schema; // RelaxNG fragment
jsval ctor; // only valid if type == CT_Script
};
@@ -82,8 +83,8 @@ public:
void RegisterMessageType(MessageTypeId mtid, const char* name);
- void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*);
- void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*);
+ void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema);
+ void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema);
void SubscribeToMessageType(MessageTypeId);
void SubscribeGloballyToMessageType(MessageTypeId);
@@ -191,6 +192,8 @@ public:
bool SerializeState(std::ostream& stream);
bool DeserializeState(std::istream& stream);
+ std::string GenerateSchema();
+
ScriptInterface& GetScriptInterface() { return m_ScriptInterface; }
private:
diff --git a/source/simulation2/system/IComponent.cpp b/source/simulation2/system/IComponent.cpp
index a49b68137a..195b6982b2 100644
--- a/source/simulation2/system/IComponent.cpp
+++ b/source/simulation2/system/IComponent.cpp
@@ -23,6 +23,12 @@ IComponent::~IComponent()
{
}
+std::string IComponent::GetSchema()
+{
+ // No schema specified -> allow only empty elements
+ return "";
+}
+
void IComponent::HandleMessage(const CSimContext& UNUSED(context), const CMessage& UNUSED(msg), bool UNUSED(global))
{
}
diff --git a/source/simulation2/system/IComponent.h b/source/simulation2/system/IComponent.h
index b41cc58aa9..381fa63fcc 100644
--- a/source/simulation2/system/IComponent.h
+++ b/source/simulation2/system/IComponent.h
@@ -35,6 +35,8 @@ class IComponent
public:
virtual ~IComponent();
+ static std::string GetSchema();
+
virtual void Init(const CSimContext& context, const CParamNode& paramNode) = 0;
virtual void Deinit(const CSimContext& context) = 0;
diff --git a/source/tools/entvalidate/entvalidate.pl b/source/tools/entvalidate/entvalidate.pl
new file mode 100644
index 0000000000..286e461409
--- /dev/null
+++ b/source/tools/entvalidate/entvalidate.pl
@@ -0,0 +1,154 @@
+use strict;
+use warnings;
+
+use XML::Parser;
+use XML::LibXML;
+use Data::Dumper;
+use Storable qw(dclone);
+use File::Find;
+
+my $root = '../../../binaries/data/mods/public/simulation/templates';
+my $rngschema = XML::LibXML::RelaxNG->new(location =>'../../../binaries/system/entity.rng');
+
+sub get_file
+{
+ my ($vfspath) = @_;
+ my $fn = "$root/$vfspath.xml";
+ open my $f, $fn or die "Error loading $fn: $!";
+ local $/;
+ return <$f>;
+}
+
+sub trim
+{
+ my ($t) = @_;
+ return '' if not defined $t;
+ $t =~ /^\s*(.*?)\s*$/s;
+ return $1;
+}
+
+sub load_xml
+{
+ my ($file) = @_;
+ my $root = {};
+ my @stack = ($root);
+ my $p = new XML::Parser(Handlers => {
+ Start => sub {
+ my ($e, $n, %a) = @_;
+ my $t = {};
+ die "Duplicate child node '$n'" if exists $stack[-1]{$n};
+ $stack[-1]{$n} = $t;
+ for (keys %a) {
+ $t->{'@'.$_}{' content'} = trim($a{$_});
+ }
+ push @stack, $t;
+ },
+ End => sub {
+ my ($e, $n) = @_;
+ $stack[-1]{' content'} = trim($stack[-1]{' content'});
+ pop @stack;
+ },
+ Char => sub {
+ my ($e, $str) = @_;
+ $stack[-1]{' content'} .= $str;
+ },
+ });
+ $p->parse($file);
+
+ return $root;
+}
+
+sub apply_layer
+{
+ my ($base, $new) = @_;
+ $base->{' content'} = $new->{' content'};
+ for my $k (grep $_ ne ' content', keys %$new) {
+ if ($new->{$k}{'@disable'}) {
+ delete $base->{$k};
+ } else {
+ if ($new->{$k}{'@replace'}) {
+ delete $base->{$k};
+ }
+ $base->{$k} ||= {};
+ apply_layer($base->{$k}, $new->{$k});
+ delete $base->{$k}{'@replace'};
+ }
+ }
+}
+
+sub load_inherited
+{
+ my ($vfspath) = @_;
+ my $layer = load_xml(get_file($vfspath));
+
+ if ($layer->{Entity}{'@parent'}) {
+ my $parent = load_inherited($layer->{Entity}{'@parent'}{' content'});
+ apply_layer($parent->{Entity}, $layer->{Entity});
+ return $parent;
+ } else {
+ return $layer;
+ }
+}
+
+sub escape_xml
+{
+ my ($t) = @_;
+ $t =~ s/&/&/g;
+ $t =~ s/</g;
+ $t =~ s/>/>/g;
+ $t =~ s/"/"/g;
+ $t =~ s/\t/ /g;
+ $t =~ s/\n/
/g;
+ $t =~ s/\r/
/g;
+ $t;
+}
+
+sub to_xml
+{
+ my ($e) = @_;
+ my $r = $e->{' content'};
+ $r = '' if not defined $r;
+ for my $k (sort grep !/^[\@ ]/, keys %$e) {
+ $r .= "<$k";
+ for my $a (sort grep /^\@/, keys %{$e->{$k}}) {
+ $a =~ /^\@(.*)/;
+ $r .= " $1=\"".escape_xml($e->{$k}{$a}{' content'})."\"";
+ }
+ $r .= ">";
+ $r .= to_xml($e->{$k});
+ $r .= "$k>";
+ }
+ return $r;
+}
+
+sub validate
+{
+ my ($vfspath) = @_;
+ my $xml = to_xml(load_inherited($vfspath));
+ my $doc = XML::LibXML->new->parse_string($xml);
+ $rngschema->validate($doc);
+}
+
+my @files;
+sub find_process {
+ return $File::Find::prune = 1 if $_ eq '.svn';
+ my $n = $File::Find::name;
+ return if /~$/;
+ return unless -f $_;
+ $n =~ s/\Q$root\///;
+ $n =~ s/\.xml$//;
+ push @files, $n;
+}
+find({ wanted => \&find_process }, $root);
+
+for my $f (sort @files) {
+ next if $f =~ /^template_/;
+ print "# $f...\n";
+ eval {
+ validate($f);
+ };
+ if ($@) {
+ print $@;
+ eval { print to_xml(load_inherited($f)), "\n"; }
+ }
+}