/* Copyright (C) 2009 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "EntityTemplate.h" #include "EntityTemplateCollection.h" #include "graphics/ObjectManager.h" #include "ps/CStr.h" #include "ps/Player.h" #include "scripting/ScriptableComplex.inl" #include "ps/XML/Xeromyces.h" #include "sound/SoundGroupMgr.h" #include "ps/CLogger.h" #define LOG_CATEGORY L"entity" STL_HASH_SET CEntityTemplate::scriptsLoaded; // Utility functions used in reading XML namespace { /** * Converts the given string, consisting of underscores and letters, * to camelCase: the first letter of each underscore-separated "word" * is changed to lowercase. For example, the string MiniMap_Colour * is converted to miniMap_colour. * * @param const std::string & str The string to convert. * @return std::string The given string in camelCase. **/ std::string toCamelCase( const std::string& str ) { std::string ret = str; for( size_t i=0; im_bound_type == CBoundingObject::BOUND_CIRCLE ) { m_bound_circle = new CBoundingCircle(); m_bound_circle->SetRadius( m_base->m_bound_circle->m_radius ); m_bound_circle->SetHeight( m_base->m_bound_circle->m_height ); } else if( m_base->m_bound_type == CBoundingObject::BOUND_OABB ) { m_bound_box = new CBoundingBox(); m_bound_box->SetDimensions( m_base->m_bound_box->GetWidth(), m_base->m_bound_box->GetDepth() ); m_bound_box->SetHeight( m_base->m_bound_box->m_height ); } m_bound_type = m_base->m_bound_type; } // Initialize sound group table from the parent's sound group table for(SoundGroupTable::iterator it = m_base->m_SoundGroupTable.begin(); it != m_base->m_SoundGroupTable.end(); ++it) { m_SoundGroupTable[it->first] = it->second; } SetBase( m_base ); m_classes.SetParent( &( m_base->m_classes ) ); SetNextObject( m_base ); } jsval CEntityTemplate::GetClassSet() { CStrW result = m_classes.GetMemberList(); return( ToJSVal( result ) ); } void CEntityTemplate::SetClassSet( jsval value ) { CStr memberCmdList = ToPrimitive( value ); m_classes.SetFromMemberList(memberCmdList); RebuildClassSet(); } void CEntityTemplate::RebuildClassSet() { m_classes.Rebuild(); InheritorsList::iterator it; for( it = m_Inheritors.begin(); it != m_Inheritors.end(); it++ ) (*it)->RebuildClassSet(); } bool CEntityTemplate::LoadXml( const VfsPath& pathname ) { CXeromyces XeroFile; if (XeroFile.Load(pathname) != PSRETURN_OK) // Fail return false; // Define all the elements and attributes used in the XML file #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) // Only the ones we can't load using normal methods. EL(Entity); EL(Script); EL(Event); EL(Traits); EL(Footprint); EL(Depth); EL(Height); EL(Radius); EL(Width); EL(SoundGroups); AT(Parent); AT(On); AT(File); AT(Function); #undef AT #undef EL XMBElement Root = XeroFile.GetRoot(); if( Root.GetNodeName() != el_Entity ) { LOG(CLogger::Error, LOG_CATEGORY, L"CEntityTemplate::LoadXml: XML root was not \"Entity\" in file %ls. Load failed.", pathname.string().c_str() ); return( false ); } XMBElementList RootChildren = Root.GetChildNodes(); m_Tag = CStr(pathname.string()).AfterLast("/").BeforeLast(".xml"); m_Base_Name = Root.GetAttributes().GetNamedItem( at_Parent ); // Load our parent, if we have one if( ! m_Base_Name.empty() ) { CEntityTemplate* base = g_EntityTemplateCollection.GetTemplate( m_Base_Name, m_player ); if( base ) { m_base = base; LoadBase(); } else { LOG(CLogger::Warning, LOG_CATEGORY, L"Parent template \"%ls\" does not exist in template \"%ls\"", m_Base_Name.c_str(), m_Tag.c_str() ); // (The requested entity will still be returned, but with no parent. // Is this a reasonable thing to do?) } } for (int i = 0; i < RootChildren.Count; ++i) { XMBElement Child = RootChildren.Item(i); int ChildName = Child.GetNodeName(); if( ChildName == el_Script ) { CStr Include = Child.GetAttributes().GetNamedItem( at_File ); if( !Include.empty() && scriptsLoaded.find( Include ) == scriptsLoaded.end() ) { scriptsLoaded.insert( Include ); g_ScriptingHost.RunScript( CStrW(Include) ); } CStr Inline = Child.GetText(); if( !Inline.empty() ) { CStr pathname_c = CStr(pathname.string()); g_ScriptingHost.RunMemScript( Inline.c_str(), Inline.length(), pathname_c.c_str(), Child.GetLineNumber() ); } } else if (ChildName == el_Traits) { XMBElementList TraitChildren = Child.GetChildNodes(); for(int j = 0; j < TraitChildren.Count; ++j) { XMBElement TraitChild = TraitChildren.Item(j); int TraitChildName = TraitChild.GetNodeName(); if( TraitChildName == el_Footprint ) { XMBElementList FootprintChildren = TraitChild.GetChildNodes(); float radius=0, height=0, width=0, depth=0; bool hadRadius = false, hadDepth = false; for(int k = 0; k < FootprintChildren.Count; ++k) { XMBElement FootprintChild = FootprintChildren.Item(k); int FootprintChildName = FootprintChild.GetNodeName(); if( FootprintChildName == el_Radius ) { hadRadius = true; radius = CStrW( FootprintChild.GetText() ).ToFloat(); } else if( FootprintChildName == el_Width ) { width = CStrW( FootprintChild.GetText() ).ToFloat(); } else if( FootprintChildName == el_Height ) { height = CStrW( FootprintChild.GetText() ).ToFloat(); } else if( FootprintChildName == el_Depth ) { hadDepth = true; depth = CStrW( FootprintChild.GetText() ).ToFloat(); } } if( hadRadius ) { // Specifying a circular footprint if( !m_bound_circle ) m_bound_circle = new CBoundingCircle(); m_bound_circle->SetRadius( radius ); m_bound_circle->SetHeight( height ); m_bound_type = CBoundingObject::BOUND_CIRCLE; } else if( hadDepth ) { // Specifying a rectangular footprint if( !m_bound_box ) m_bound_box = new CBoundingBox(); m_bound_box->SetDimensions( width, depth ); m_bound_box->SetHeight( height ); m_bound_type = CBoundingObject::BOUND_OABB; } // Else, entity has no footprint. } } // important so that scripts can see traits XMLLoadProperty( XeroFile, Child, CStrW() ); } else if( ChildName == el_Event ) { // Action...On for consistency with the GUI. CStrW EventName = L"on" + (CStrW)Child.GetAttributes().GetNamedItem( at_On ); CStrW Code (Child.GetText()); utf16string ExternalFunction = Child.GetAttributes().GetNamedItem( at_Function ); // Does a property with this name already exist? for( size_t eventID = 0; eventID < EVENT_LAST; eventID++ ) { if( CStrW( EventNames[eventID] ) == EventName ) { if( ExternalFunction != utf16string() ) { jsval fnval; JSBool ret = JS_GetUCProperty( g_ScriptingHost.GetContext(), g_ScriptingHost.GetGlobalObject(), ExternalFunction.c_str(), ExternalFunction.size(), &fnval ); debug_assert( ret ); JSFunction* fn = JS_ValueToFunction( g_ScriptingHost.GetContext(), fnval ); if( !fn ) { LOG(CLogger::Error, LOG_CATEGORY, L"CEntityTemplate::LoadXml: Function does not exist for event %ls in file %ls. Load failed.", EventName.c_str(), pathname.string().c_str() ); break; } m_EventHandlers[eventID].SetFunction( fn ); } else m_EventHandlers[eventID].Compile( pathname.string() + L"::" + EventName + L" (" + CStrW( Child.GetLineNumber() ) + L")", Code ); HasProperty( EventName )->m_Inherited = false; break; } } } else if( ChildName == el_SoundGroups ) { // Read every child element's value into m_SoundGroupTable with its tag as the key XMBElementList children = Child.GetChildNodes(); for(int j = 0; j < children.Count; ++j) { XMBElement child = children.Item(j); CStr8 name = toCamelCase( XeroFile.GetElementString( child.GetNodeName() ) ); VfsPath soundGroupFilename = CStrW(child.GetText()); const size_t soundGroupIndex = g_soundGroupMgr->AddGroup(soundGroupFilename); m_SoundGroupTable[name] = soundGroupIndex; } } else { XMLLoadProperty( XeroFile, Child, CStrW() ); } } if( m_player == 0 ) { m_unmodified = this; } else { m_unmodified = g_EntityTemplateCollection.GetTemplate( m_Tag, 0 ); } return true; } void CEntityTemplate::XMLLoadProperty( const CXeromyces& XeroFile, const XMBElement& Source, const CStrW& BasePropertyName ) { // Add a property, put the node text into it. CStrW PropertyName = BasePropertyName + CStrW( toCamelCase( XeroFile.GetElementString( Source.GetNodeName() ) ) ); IJSComplexProperty* Existing = HasProperty( PropertyName ); if( Existing ) { if( !Existing->m_Intrinsic ) LOG(CLogger::Warning, LOG_CATEGORY, L"CEntityTemplate::XMLAddProperty: %ls already defined for %ls. Property trees will be merged.", PropertyName.c_str(), m_Tag.c_str() ); Existing->Set( this, JSParseString( Source.GetText() ) ); //Existing->m_Inherited = false; } else { if( !Source.GetText().length() ) { // Arbitrarily say that if a node has no other value, define it to be 'true'. // Appears to be the most convenient thing to do in most circumstances. AddProperty( PropertyName, JSVAL_TRUE ); } else { AddProperty( PropertyName, Source.GetText() ); } } PropertyName += L"."; // Retrieve any attributes it has and add them as subproperties. XMBAttributeList AttributeSet = Source.GetAttributes(); for( int AttributeID = 0; AttributeID < AttributeSet.Count; AttributeID++ ) { XMBAttribute Attribute = AttributeSet.Item( AttributeID ); CStrW AttributeName = PropertyName + CStr8( toCamelCase( XeroFile.GetAttributeString( Attribute.Name ) ) ); Existing = HasProperty( AttributeName ); if( Existing ) { Existing->Set( this, JSParseString( Attribute.Value ) ); Existing->m_Inherited = false; } else AddProperty( AttributeName, Attribute.Value ); } // Retrieve any child nodes the property has and, similarly, add them as subproperties. XMBElementList NodeSet = Source.GetChildNodes(); for( int NodeID = 0; NodeID < NodeSet.Count; NodeID++ ) { XMBElement Node = NodeSet.Item( NodeID ); XMLLoadProperty( XeroFile, Node, PropertyName ); } } /* Scripting Interface */ // Scripting initialization void CEntityTemplate::ScriptingInit() { AddMethod( "toString", 0 ); AddClassProperty( L"traits.id.classes", static_cast(&CEntityTemplate::GetClassSet), static_cast(&CEntityTemplate::SetClassSet) ); AddClassProperty( L"actions.move.speed", &CEntityTemplate::m_speed ); AddClassProperty( L"actions.move.turningRadius", &CEntityTemplate::m_turningRadius ); AddClassProperty( L"actions.move.run.speed", &CEntityTemplate::m_runSpeed ); AddClassProperty( L"actions.move.run.rangeMin", &CEntityTemplate::m_runMinRange ); AddClassProperty( L"actions.move.run.range", &CEntityTemplate::m_runMaxRange ); AddClassProperty( L"actions.move.run.regenRate", &CEntityTemplate::m_runRegenRate ); AddClassProperty( L"actions.move.run.decayRate", &CEntityTemplate::m_runDecayRate ); AddClassProperty( L"actions.move.passThroughAllies", &CEntityTemplate::m_passThroughAllies ); AddClassProperty( L"actor", &CEntityTemplate::m_actorName ); AddClassProperty( L"traits.health.max", &CEntityTemplate::m_healthMax ); AddClassProperty( L"traits.health.barHeight", &CEntityTemplate::m_healthBarHeight ); AddClassProperty( L"traits.health.barSize", &CEntityTemplate::m_healthBarSize ); AddClassProperty( L"traits.health.barWidth", &CEntityTemplate::m_healthBarWidth ); AddClassProperty( L"traits.health.borderHeight", &CEntityTemplate::m_healthBorderHeight); AddClassProperty( L"traits.health.borderWidth", &CEntityTemplate::m_healthBorderWidth ); AddClassProperty( L"traits.health.borderName", &CEntityTemplate::m_healthBorderName ); AddClassProperty( L"traits.health.regenRate", &CEntityTemplate::m_healthRegenRate ); AddClassProperty( L"traits.health.regenStart", &CEntityTemplate::m_healthRegenStart ); AddClassProperty( L"traits.health.decayRate", &CEntityTemplate::m_healthDecayRate ); AddClassProperty( L"traits.stamina.max", &CEntityTemplate::m_staminaMax ); AddClassProperty( L"traits.stamina.barHeight", &CEntityTemplate::m_staminaBarHeight ); AddClassProperty( L"traits.stamina.barSize", &CEntityTemplate::m_staminaBarSize ); AddClassProperty( L"traits.stamina.barWidth", &CEntityTemplate::m_staminaBarWidth ); AddClassProperty( L"traits.stamina.borderHeight", &CEntityTemplate::m_staminaBorderHeight); AddClassProperty( L"traits.stamina.borderWidth", &CEntityTemplate::m_staminaBorderWidth ); AddClassProperty( L"traits.stamina.borderName", &CEntityTemplate::m_staminaBorderName ); AddClassProperty( L"traits.rally.name", &CEntityTemplate::m_rallyName ); AddClassProperty( L"traits.rally.width", &CEntityTemplate::m_rallyWidth ); AddClassProperty( L"traits.rally.height", &CEntityTemplate::m_rallyHeight ); AddClassProperty( L"traits.flankPenalty.sectors", &CEntityTemplate::m_sectorDivs ); AddClassProperty( L"traits.pitch.sectors", &CEntityTemplate::m_pitchDivs ); AddClassProperty( L"traits.pitch.value", &CEntityTemplate::m_pitchValue ); AddClassProperty( L"traits.rank.width", &CEntityTemplate::m_rankWidth ); AddClassProperty( L"traits.rank.height", &CEntityTemplate::m_rankHeight ); AddClassProperty( L"traits.rank.name", &CEntityTemplate::m_rankName ); AddClassProperty( L"traits.ai.stance.curr", &CEntityTemplate::m_stanceName ); AddClassProperty( L"traits.miniMap.type", &CEntityTemplate::m_minimapType ); AddClassProperty( L"traits.miniMap.red", &CEntityTemplate::m_minimapR ); AddClassProperty( L"traits.miniMap.green", &CEntityTemplate::m_minimapG ); AddClassProperty( L"traits.miniMap.blue", &CEntityTemplate::m_minimapB ); AddClassProperty( L"traits.anchor.type", &CEntityTemplate::m_anchorType ); AddClassProperty( L"traits.anchor.conformX", &CEntityTemplate::m_anchorConformX ); AddClassProperty( L"traits.anchor.conformZ", &CEntityTemplate::m_anchorConformZ ); AddClassProperty( L"traits.vision.los", &CEntityTemplate::m_los ); AddClassProperty( L"traits.vision.permanent", &CEntityTemplate::m_visionPermanent ); AddClassProperty( L"traits.display.bars.enabled", &CEntityTemplate::m_barsEnabled ); AddClassProperty( L"traits.display.bars.offset", &CEntityTemplate::m_barOffset ); AddClassProperty( L"traits.display.bars.height", &CEntityTemplate::m_barHeight ); AddClassProperty( L"traits.display.bars.width", &CEntityTemplate::m_barWidth ); AddClassProperty( L"traits.display.bars.border", &CEntityTemplate::m_barBorder ); AddClassProperty( L"traits.display.bars.borderSize", &CEntityTemplate::m_barBorderSize ); AddClassProperty( L"traits.isTerritoryCentre", &CEntityTemplate::m_isTerritoryCentre ); AddClassProperty( L"traits.creation.foundation", &CEntityTemplate::m_foundation ); AddClassProperty( L"traits.creation.socket", &CEntityTemplate::m_socket ); AddClassProperty( L"traits.creation.territoryRestriction", &CEntityTemplate::m_territoryRestriction ); AddClassProperty( L"traits.creation.buildingLimitCategory", &CEntityTemplate::m_buildingLimitCategory ); CJSComplex::ScriptingInit( "EntityTemplate" ); } // Script-bound functions JSObject* CEntityTemplate::GetScriptExecContext( IEventTarget* target ) { return( target->GetScriptExecContext( target ) ); } CStr CEntityTemplate::ToString( JSContext* UNUSED(cx), uintN UNUSED(argc), jsval* UNUSED(argv) ) { if( m_player == 0 ) return "[object EntityTemplate: " + CStr(m_Tag) + " base]"; else return "[object EntityTemplate: " + CStr(m_Tag) + " for player " + CStr((unsigned)m_player->GetPlayerID()) + "]"; }