Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 21644) +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 21645) @@ -1,702 +1,725 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + "" + ""; /** * Initialize GarrisonHolder Component * Garrisoning when loading a map is set in the script of the map, by setting initGarrison * which should contain the array of garrisoned entities. */ GarrisonHolder.prototype.Init = function() { // Garrisoned Units this.entities = []; this.timer = undefined; this.allowGarrisoning = new Map(); this.visibleGarrisonPoints = []; if (this.template.VisibleGarrisonPoints) { let points = this.template.VisibleGarrisonPoints; for (let point in points) this.visibleGarrisonPoints.push({ "offset": { "x": +points[point].X, "y": +points[point].Y, "z": +points[point].Z }, + "angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null, "entity": null }); } }; /** * @return {Object} max and min range at which entities can garrison the holder. */ GarrisonHolder.prototype.GetLoadingRange = function() { return { "max": +this.template.LoadingRange, "min": 0 }; }; GarrisonHolder.prototype.CanPickup = function(ent) { if (!this.template.Pickup || this.IsFull()) return false; let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership); return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent); }; GarrisonHolder.prototype.GetEntities = function() { return this.entities; }; /** * @return {Array} unit classes which can be garrisoned inside this * particular entity. Obtained from the entity's template. */ GarrisonHolder.prototype.GetAllowedClasses = function() { return this.template.List._string; }; GarrisonHolder.prototype.GetCapacity = function() { return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity); }; GarrisonHolder.prototype.IsFull = function() { return this.GetGarrisonedEntitiesCount() >= this.GetCapacity(); }; GarrisonHolder.prototype.GetHealRate = function() { return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity); }; /** * Set this entity to allow or disallow garrisoning in the entity. * Every component calling this function should do it with its own ID, and as long as one * component doesn't allow this entity to garrison, it can't be garrisoned * When this entity already contains garrisoned soldiers, * these will not be able to ungarrison until the flag is set to true again. * * This more useful for modern-day features. For example you can't garrison or ungarrison * a driving vehicle or plane. * @param {boolean} allow - Whether the entity should be garrisonable. */ GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID) { this.allowGarrisoning.set(callerID, allow); }; GarrisonHolder.prototype.IsGarrisoningAllowed = function() { return Array.from(this.allowGarrisoning.values()).every(allow => allow); }; GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function() { let count = this.entities.length; for (let ent of this.entities) { let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) count += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); } return count; }; GarrisonHolder.prototype.IsAllowedToGarrison = function(ent) { if (!this.IsGarrisoningAllowed()) return false; if (!IsOwnedByMutualAllyOfEntity(ent, this.entity)) return false; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return false; let entityClasses = cmpIdentity.GetClassesList(); return MatchesClassList(entityClasses, this.template.List._string) && !!Engine.QueryInterface(ent, IID_Garrisonable); }; /** * Garrison a unit inside. The timer for AutoHeal is started here. * @param {number} vgpEntity - The visual garrison point that will be used. * If vgpEntity is given, this visualGarrisonPoint will be used for the entity. * @return {boolean} Whether the entity was garrisonned. */ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity) { let cmpPosition = Engine.QueryInterface(entity, IID_Position); if (!cmpPosition) return false; if (!this.PerformGarrison(entity)) return false; let visibleGarrisonPoint = vgpEntity; if (!visibleGarrisonPoint) for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity) continue; visibleGarrisonPoint = vgp; break; } if (visibleGarrisonPoint) { visibleGarrisonPoint.entity = entity; - cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset); + // Angle of turrets: + // Renamed entities (vgpEntity != undefined) should keep their angle. + // Otherwise if an angle is given in the visibleGarrisonPoint, use it. + // If no such angle given (usually walls for which outside/inside not well defined), we keep + // the current angle as it was used for garrisoning and thus quite often was from inside to + // outside, except when garrisoning from outWorld where we take as default PI. + let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!vgpEntity && visibleGarrisonPoint.angle != null) + cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle); + else if (!vgpEntity && !cmpPosition.IsInWorld()) + cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI); + let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion); + if (cmpUnitMotion) + cmpUnitMotion.SetFacePointAfterMove(false); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SetTurretStance(); + cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset); } else cmpPosition.MoveOutOfWorld(); return true; }; /** * @return {boolean} Whether the entity was garrisonned. */ GarrisonHolder.prototype.PerformGarrison = function(entity) { if (!this.HasEnoughHealth()) return false; // Check if the unit is allowed to be garrisoned inside the building if (!this.IsAllowedToGarrison(entity)) return false; // Check the capacity let extraCount = 0; let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder); if (cmpGarrisonHolder) extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity()) return false; if (!this.timer && this.GetHealRate() > 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } // Actual garrisoning happens here this.entities.push(entity); this.UpdateGarrisonFlag(); let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.PauseProduction(); let cmpAura = Engine.QueryInterface(entity, IID_Auras); if (cmpAura && cmpAura.HasGarrisonAura()) cmpAura.ApplyGarrisonBonus(this.entity); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] }); return true; }; /** * Simply eject the unit from the garrisoning entity without moving it * @param {number} entity - Id of the entity to be ejected. * @param {boolean} forced - Whether eject is forced (i.e. if building is destroyed). * @return {boolean} Whether the entity was ejected. */ GarrisonHolder.prototype.Eject = function(entity, forced) { let entityIndex = this.entities.indexOf(entity); // Error: invalid entity ID, usually it's already been ejected if (entityIndex == -1) return false; // Find spawning location let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); // If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities // TODO: should use passability classes to be more generic let pos; if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship")) pos = cmpFootprint.PickSpawnPointBothPass(entity); else pos = cmpFootprint.PickSpawnPoint(entity); if (pos.y < 0) { // Error: couldn't find space satisfying the unit's passability criteria if (!forced) return false; // If ejection is forced, we need to continue, so use center of the building let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); pos = cmpPosition.GetPosition(); } - let cmpNewPosition = Engine.QueryInterface(entity, IID_Position); this.entities.splice(entityIndex, 1); + let cmpEntPosition = Engine.QueryInterface(entity, IID_Position); + let cmpEntUnitAI = Engine.QueryInterface(entity, IID_UnitAI); - let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != entity) continue; - cmpNewPosition.SetTurretParent(INVALID_ENTITY, new Vector3D()); - if (cmpUnitAI) - cmpUnitAI.ResetTurretStance(); + cmpEntPosition.SetTurretParent(INVALID_ENTITY, new Vector3D()); + let cmpEntUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion); + if (cmpEntUnitMotion) + cmpEntUnitMotion.SetFacePointAfterMove(true); + if (cmpEntUnitAI) + cmpEntUnitAI.ResetTurretStance(); vgp.entity = null; break; } - if (cmpUnitAI) - cmpUnitAI.Ungarrison(); - - let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); - if (cmpProductionQueue) - cmpProductionQueue.UnpauseProduction(); + if (cmpEntUnitAI) + cmpEntUnitAI.Ungarrison(); - let cmpAura = Engine.QueryInterface(entity, IID_Auras); - if (cmpAura && cmpAura.HasGarrisonAura()) - cmpAura.RemoveGarrisonBonus(this.entity); + let cmpEntProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); + if (cmpEntProductionQueue) + cmpEntProductionQueue.UnpauseProduction(); + + let cmpEntAura = Engine.QueryInterface(entity, IID_Auras); + if (cmpEntAura && cmpEntAura.HasGarrisonAura()) + cmpEntAura.RemoveGarrisonBonus(this.entity); - cmpNewPosition.JumpTo(pos.x, pos.z); - cmpNewPosition.SetHeightOffset(0); + cmpEntPosition.JumpTo(pos.x, pos.z); + cmpEntPosition.SetHeightOffset(0); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) - cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); + cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] }); return true; }; /** * Ejects units and orders them to move to the rally point. If an ejection * with a given obstruction radius has failed, we won't try anymore to eject * entities with a bigger obstruction as that is compelled to also fail. * @param {Array} entities - An array containing the ids of the entities to eject. * @param {boolean} forced - Whether eject is forced (ie: if building is destroyed). * @return {boolean} Whether the entities were ejected. */ GarrisonHolder.prototype.PerformEject = function(entities, forced) { if (!this.IsGarrisoningAllowed() && !forced) return false; let ejectedEntities = []; let success = true; let failedRadius; let radius; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); for (let entity of entities) { if (failedRadius !== undefined) { let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); radius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0; if (radius >= failedRadius) continue; } if (this.Eject(entity, forced)) { let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner()) ejectedEntities.push(entity); } else { success = false; if (failedRadius !== undefined) failedRadius = Math.min(failedRadius, radius); else { let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); failedRadius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0; } } } this.OrderWalkToRallyPoint(ejectedEntities); this.UpdateGarrisonFlag(); return success; }; /** * Order entities to walk to the rally point. * @param {Array} entities - An array containing all the ids of the entities. */ GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0]) return; let commands = GetRallyPointCommands(cmpRallyPoint, entities); // Ignore the rally point if it is autogarrison if (commands[0].type == "garrison" && commands[0].target == this.entity) return; for (let command of commands) ProcessCommand(cmpOwnership.GetOwner(), command); }; /** * Unload unit from the garrisoning entity and order them * to move to the rally point. * @return {boolean} Whether the command was successful. */ GarrisonHolder.prototype.Unload = function(entity, forced) { return this.PerformEject([entity], forced); }; /** * Unload one or all units that match a template and owner from * the garrisoning entity and order them to move to the rally point. * @param {string} template - Type of units that should be ejected. * @param {number} owner - Id of the player whose units should be ejected. * @param {boolean} all - Whether all units should be ejected. * @param {boolean} forced - Whether unload is forced. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all, forced) { let entities = []; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for (let entity of this.entities) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); // Units with multiple ranks are grouped together. let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity); if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner()) continue; entities.push(entity); // If 'all' is false, only ungarrison the first matched unit. if (!all) break; } return this.PerformEject(entities, forced); }; /** * Unload all units, that belong to certain player * and order all own units to move to the rally point. * @param {boolean} forced - Whether unload is forced. * @param {number} owner - Id of the player whose units should be ejected. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAllByOwner = function(owner, forced) { let entities = this.entities.filter(ent => { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); return cmpOwnership && cmpOwnership.GetOwner() == owner; }); return this.PerformEject(entities, forced); }; /** * Unload all units from the entity and order them to move to the rally point. * @param {boolean} forced - Whether unload is forced. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAll = function(forced) { return this.PerformEject(this.entities.slice(), forced); }; /** * Used to check if the garrisoning entity's health has fallen below * a certain limit after which all garrisoned units are unloaded. */ GarrisonHolder.prototype.OnHealthChanged = function(msg) { if (!this.HasEnoughHealth() && this.entities.length) this.EjectOrKill(this.entities.slice()); }; GarrisonHolder.prototype.HasEnoughHealth = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); return cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints()); }; /** * Called every second. Heals garrisoned units. */ GarrisonHolder.prototype.HealTimeout = function(data) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (!this.entities.length) { cmpTimer.CancelTimer(this.timer); this.timer = undefined; return; } for (let entity of this.entities) { let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth && !cmpHealth.IsUnhealable()) cmpHealth.Increase(this.GetHealRate()); } this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); }; /** * Updates the garrison flag depending whether something is garrisoned in the entity. */ GarrisonHolder.prototype.UpdateGarrisonFlag = function() { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned"); }; /** * Cancel timer when destroyed. */ GarrisonHolder.prototype.OnDestroy = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /** * If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'), * remove it from the building so we only ever contain valid entities. */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { // The ownership change may be on the garrisonholder if (this.entity == msg.entity) { let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent)); if (entities.length) this.EjectOrKill(entities); return; } // or on some of its garrisoned units let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { // If the entity is dead, remove it directly instead of ejecting the corpse let cmpHealth = Engine.QueryInterface(msg.entity, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() == 0) { this.entities.splice(entityIndex, 1); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [msg.entity] }); this.UpdateGarrisonFlag(); for (let point of this.visibleGarrisonPoints) if (point.entity == msg.entity) point.entity = null; } else if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity)) this.EjectOrKill([msg.entity]); } }; /** * Update list of garrisoned entities if one gets renamed (e.g. by promotion). */ GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg) { let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { let vgpRenamed; for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != msg.entity) continue; vgpRenamed = vgp; break; } this.Eject(msg.entity, true); this.Garrison(msg.newentity, vgpRenamed); } if (!this.initGarrison) return; // Update the pre-game garrison because of SkirmishReplacement if (msg.entity == this.entity) { let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder); if (cmpGarrisonHolder) cmpGarrisonHolder.initGarrison = this.initGarrison; } else { entityIndex = this.initGarrison.indexOf(msg.entity); if (entityIndex != -1) this.initGarrison[entityIndex] = msg.newentity; } }; /** * Eject all foreign garrisoned entities which are no more allied. */ GarrisonHolder.prototype.OnDiplomacyChanged = function() { this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent))); }; /** * Eject or kill a garrisoned unit which can no more be garrisoned * (garrisonholder's health too small or ownership changed). */ GarrisonHolder.prototype.EjectOrKill = function(entities) { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); // Eject the units which can be ejected (if not in world, it generally means this holder // is inside a holder which kills its entities, so do not eject) if (cmpPosition && cmpPosition.IsInWorld()) { let ejectables = entities.filter(ent => this.IsEjectable(ent)); if (ejectables.length) this.PerformEject(ejectables, false); } // And destroy all remaining entities let killedEntities = []; for (let entity of entities) { let entityIndex = this.entities.indexOf(entity); if (entityIndex == -1) continue; let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) cmpHealth.Kill(); this.entities.splice(entityIndex, 1); killedEntities.push(entity); } if (killedEntities.length) Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities }); this.UpdateGarrisonFlag(); }; GarrisonHolder.prototype.IsEjectable = function(entity) { if (!this.entities.find(ent => ent == entity)) return false; let ejectableClasses = this.template.EjectClassesOnDestroy._string; ejectableClasses = ejectableClasses ? ejectableClasses.split(/\s+/) : []; let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList(); return ejectableClasses.some(ejectableClass => entityClasses.indexOf(ejectableClass) != -1); }; /** * Initialise the garrisoned units. */ GarrisonHolder.prototype.OnGlobalInitGame = function(msg) { if (!this.initGarrison) return; for (let ent of this.initGarrison) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.CanGarrison(this.entity) && this.Garrison(ent)) cmpUnitAI.Autogarrison(this.entity); } this.initGarrison = undefined; }; GarrisonHolder.prototype.OnValueModification = function(msg) { if (msg.component != "GarrisonHolder" || msg.valueNames.indexOf("GarrisonHolder/BuffHeal") == -1) return; if (this.timer && this.GetHealRate() == 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } else if (!this.timer && this.GetHealRate() > 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } }; Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder); Index: ps/trunk/source/simulation2/components/CCmpPosition.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpPosition.cpp (revision 21644) +++ ps/trunk/source/simulation2/components/CCmpPosition.cpp (revision 21645) @@ -1,979 +1,981 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpPosition.h" #include "simulation2/MessageTypes.h" #include "ICmpTerrain.h" #include "ICmpTerritoryManager.h" #include "ICmpVisual.h" #include "ICmpWaterManager.h" #include "graphics/Terrain.h" #include "lib/rand.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Vector3D.h" #include "maths/Vector2D.h" #include "ps/CLogger.h" #include "ps/Profile.h" /** * Basic ICmpPosition implementation. */ class CCmpPosition : public ICmpPosition { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_WaterChanged); componentManager.SubscribeToMessageType(MT_Deserialized); // TODO: if this component turns out to be a performance issue, it should // be optimised by creating a new PositionStatic component that doesn't subscribe // to messages and doesn't store LastX/LastZ, and that should be used for all // entities that don't move } DEFAULT_COMPONENT_ALLOCATOR(Position) // Template state: enum { UPRIGHT = 0, PITCH = 1, PITCH_ROLL = 2, ROLL = 3, } m_AnchorType; bool m_Floating; entity_pos_t m_FloatDepth; float m_RotYSpeed; // maximum radians per second, used by InterpolatedRotY to follow RotY // Dynamic state: bool m_InWorld; // m_LastX/Z contain the position from the start of the most recent turn // m_PrevX/Z conatain the position from the turn before that entity_pos_t m_X, m_Z, m_LastX, m_LastZ, m_PrevX, m_PrevZ; // these values contain undefined junk if !InWorld entity_pos_t m_Y, m_LastYDifference; // either the relative or the absolute Y coordinate bool m_RelativeToGround; // whether m_Y is relative to terrain/water plane, or an absolute height fixed m_ConstructionProgress; // when the entity is a turret, only m_RotY is used, and this is the rotation // relative to the parent entity entity_angle_t m_RotX, m_RotY, m_RotZ; player_id_t m_Territory; entity_id_t m_TurretParent; CFixedVector3D m_TurretPosition; std::set m_Turrets; // Not serialized: float m_InterpolatedRotX, m_InterpolatedRotY, m_InterpolatedRotZ; float m_LastInterpolatedRotX, m_LastInterpolatedRotZ; bool m_ActorFloating; bool m_EnabledMessageInterpolate; static std::string GetSchema() { return "Allows this entity to exist at a location (and orientation) in the world, and defines some details of the positioning." "" "upright" "0.0" "false" "0.0" "6.0" "" "" "" "upright" "pitch" "roll" "pitch-roll" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { std::wstring anchor = paramNode.GetChild("Anchor").ToString(); if (anchor == L"pitch") m_AnchorType = PITCH; else if (anchor == L"pitch-roll") m_AnchorType = PITCH_ROLL; else if (anchor == L"roll") m_AnchorType = ROLL; else m_AnchorType = UPRIGHT; m_InWorld = false; m_LastYDifference = entity_pos_t::Zero(); m_Y = paramNode.GetChild("Altitude").ToFixed(); m_RelativeToGround = true; m_Floating = paramNode.GetChild("Floating").ToBool(); m_FloatDepth = paramNode.GetChild("FloatDepth").ToFixed(); m_RotYSpeed = paramNode.GetChild("TurnRate").ToFixed().ToFloat(); m_RotX = m_RotY = m_RotZ = entity_angle_t::FromInt(0); m_InterpolatedRotX = m_InterpolatedRotY = m_InterpolatedRotZ = 0.f; m_LastInterpolatedRotX = m_LastInterpolatedRotZ = 0.f; m_Territory = INVALID_PLAYER; m_TurretParent = INVALID_ENTITY; m_TurretPosition = CFixedVector3D(); m_ActorFloating = false; m_EnabledMessageInterpolate = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.Bool("in world", m_InWorld); if (m_InWorld) { serialize.NumberFixed_Unbounded("x", m_X); serialize.NumberFixed_Unbounded("y", m_Y); serialize.NumberFixed_Unbounded("z", m_Z); serialize.NumberFixed_Unbounded("last x", m_LastX); serialize.NumberFixed_Unbounded("last y diff", m_LastYDifference); serialize.NumberFixed_Unbounded("last z", m_LastZ); } serialize.NumberI32_Unbounded("territory", m_Territory); serialize.NumberFixed_Unbounded("rot x", m_RotX); serialize.NumberFixed_Unbounded("rot y", m_RotY); serialize.NumberFixed_Unbounded("rot z", m_RotZ); serialize.NumberFixed_Unbounded("altitude", m_Y); serialize.Bool("relative", m_RelativeToGround); serialize.Bool("floating", m_Floating); serialize.NumberFixed_Unbounded("float depth", m_FloatDepth); serialize.NumberFixed_Unbounded("constructionprogress", m_ConstructionProgress); if (serialize.IsDebug()) { const char* anchor = "???"; switch (m_AnchorType) { case PITCH: anchor = "pitch"; break; case PITCH_ROLL: anchor = "pitch-roll"; break; case ROLL: anchor = "roll"; break; case UPRIGHT: // upright is the default default: anchor = "upright"; break; } serialize.StringASCII("anchor", anchor, 0, 16); } serialize.NumberU32_Unbounded("turret parent", m_TurretParent); if (m_TurretParent != INVALID_ENTITY) { serialize.NumberFixed_Unbounded("x", m_TurretPosition.X); serialize.NumberFixed_Unbounded("y", m_TurretPosition.Y); serialize.NumberFixed_Unbounded("z", m_TurretPosition.Z); } } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); deserialize.Bool("in world", m_InWorld); if (m_InWorld) { deserialize.NumberFixed_Unbounded("x", m_X); deserialize.NumberFixed_Unbounded("y", m_Y); deserialize.NumberFixed_Unbounded("z", m_Z); deserialize.NumberFixed_Unbounded("last x", m_LastX); deserialize.NumberFixed_Unbounded("last y diff", m_LastYDifference); deserialize.NumberFixed_Unbounded("last z", m_LastZ); } deserialize.NumberI32_Unbounded("territory", m_Territory); deserialize.NumberFixed_Unbounded("rot x", m_RotX); deserialize.NumberFixed_Unbounded("rot y", m_RotY); deserialize.NumberFixed_Unbounded("rot z", m_RotZ); deserialize.NumberFixed_Unbounded("altitude", m_Y); deserialize.Bool("relative", m_RelativeToGround); deserialize.Bool("floating", m_Floating); deserialize.NumberFixed_Unbounded("float depth", m_FloatDepth); deserialize.NumberFixed_Unbounded("constructionprogress", m_ConstructionProgress); // TODO: should there be range checks on all these values? m_InterpolatedRotY = m_RotY.ToFloat(); deserialize.NumberU32_Unbounded("turret parent", m_TurretParent); if (m_TurretParent != INVALID_ENTITY) { deserialize.NumberFixed_Unbounded("x", m_TurretPosition.X); deserialize.NumberFixed_Unbounded("y", m_TurretPosition.Y); deserialize.NumberFixed_Unbounded("z", m_TurretPosition.Z); } if (m_InWorld) UpdateXZRotation(); UpdateMessageSubscriptions(); } void Deserialized() { AdvertiseInterpolatedPositionChanges(); } virtual void UpdateTurretPosition() { if (m_TurretParent == INVALID_ENTITY) return; CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (!cmpPosition) { LOGERROR("Turret with parent without position component"); return; } if (!cmpPosition->IsInWorld()) MoveOutOfWorld(); else { CFixedVector2D rotatedPosition = CFixedVector2D(m_TurretPosition.X, m_TurretPosition.Z); rotatedPosition = rotatedPosition.Rotate(cmpPosition->GetRotation().Y); CFixedVector2D rootPosition = cmpPosition->GetPosition2D(); entity_pos_t x = rootPosition.X + rotatedPosition.X; entity_pos_t z = rootPosition.Y + rotatedPosition.Y; if (!m_InWorld || m_X != x || m_Z != z) MoveTo(x, z); entity_pos_t y = cmpPosition->GetHeightOffset() + m_TurretPosition.Y; if (!m_InWorld || GetHeightOffset() != y) SetHeightOffset(y); m_InWorld = true; } } virtual std::set* GetTurrets() { return &m_Turrets; } virtual void SetTurretParent(entity_id_t id, const CFixedVector3D& offset) { + entity_angle_t angle = GetRotation().Y; if (m_TurretParent != INVALID_ENTITY) { CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (cmpPosition) cmpPosition->GetTurrets()->erase(GetEntityId()); } m_TurretParent = id; m_TurretPosition = offset; if (m_TurretParent != INVALID_ENTITY) { CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (cmpPosition) cmpPosition->GetTurrets()->insert(GetEntityId()); } + SetYRotation(angle); UpdateTurretPosition(); } virtual entity_id_t GetTurretParent() const { return m_TurretParent; } virtual bool IsInWorld() const { return m_InWorld; } virtual void MoveOutOfWorld() { m_InWorld = false; AdvertisePositionChanges(); AdvertiseInterpolatedPositionChanges(); } virtual void MoveTo(entity_pos_t x, entity_pos_t z) { m_X = x; m_Z = z; if (!m_InWorld) { m_InWorld = true; m_LastX = m_PrevX = m_X; m_LastZ = m_PrevZ = m_Z; m_LastYDifference = entity_pos_t::Zero(); } AdvertisePositionChanges(); AdvertiseInterpolatedPositionChanges(); } virtual void MoveAndTurnTo(entity_pos_t x, entity_pos_t z, entity_angle_t ry) { m_X = x; m_Z = z; if (!m_InWorld) { m_InWorld = true; m_LastX = m_PrevX = m_X; m_LastZ = m_PrevZ = m_Z; m_LastYDifference = entity_pos_t::Zero(); } // TurnTo will advertise the position changes TurnTo(ry); AdvertiseInterpolatedPositionChanges(); } virtual void JumpTo(entity_pos_t x, entity_pos_t z) { m_LastX = m_PrevX = m_X = x; m_LastZ = m_PrevZ = m_Z = z; m_InWorld = true; UpdateXZRotation(); m_LastInterpolatedRotX = m_InterpolatedRotX; m_LastInterpolatedRotZ = m_InterpolatedRotZ; AdvertisePositionChanges(); AdvertiseInterpolatedPositionChanges(); } virtual void SetHeightOffset(entity_pos_t dy) { // subtract the offset and replace with a new offset m_LastYDifference = dy - GetHeightOffset(); m_Y += m_LastYDifference; AdvertiseInterpolatedPositionChanges(); } virtual entity_pos_t GetHeightOffset() const { if (m_RelativeToGround) return m_Y; // not relative to the ground, so the height offset is m_Y - ground height // except when floating, when the height offset is m_Y - water level + float depth entity_pos_t baseY; CmpPtr cmpTerrain(GetSystemEntity()); if (cmpTerrain) baseY = cmpTerrain->GetGroundLevel(m_X, m_Z); if (m_Floating) { CmpPtr cmpWaterManager(GetSystemEntity()); if (cmpWaterManager) baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(m_X, m_Z) - m_FloatDepth); } return m_Y - baseY; } virtual void SetHeightFixed(entity_pos_t y) { // subtract the absolute height and replace it with a new absolute height m_LastYDifference = y - GetHeightFixed(); m_Y += m_LastYDifference; AdvertiseInterpolatedPositionChanges(); } virtual entity_pos_t GetHeightFixed() const { if (!m_RelativeToGround) return m_Y; // relative to the ground, so the fixed height = ground height + m_Y // except when floating, when the fixed height = water level - float depth + m_Y entity_pos_t baseY; CmpPtr cmpTerrain(GetSystemEntity()); if (cmpTerrain) baseY = cmpTerrain->GetGroundLevel(m_X, m_Z); if (m_Floating) { CmpPtr cmpWaterManager(GetSystemEntity()); if (cmpWaterManager) baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(m_X, m_Z) - m_FloatDepth); } return m_Y + baseY; } virtual bool IsHeightRelative() const { return m_RelativeToGround; } virtual void SetHeightRelative(bool relative) { // move y to use the right offset (from terrain or from map origin) m_Y = relative ? GetHeightOffset() : GetHeightFixed(); m_RelativeToGround = relative; m_LastYDifference = entity_pos_t::Zero(); AdvertiseInterpolatedPositionChanges(); } virtual bool CanFloat() const { return m_Floating; } virtual void SetFloating(bool flag) { m_Floating = flag; AdvertiseInterpolatedPositionChanges(); } virtual void SetActorFloating(bool flag) { m_ActorFloating = flag; AdvertiseInterpolatedPositionChanges(); } virtual void SetConstructionProgress(fixed progress) { m_ConstructionProgress = progress; AdvertiseInterpolatedPositionChanges(); } virtual CFixedVector3D GetPosition() const { if (!m_InWorld) { LOGERROR("CCmpPosition::GetPosition called on entity when IsInWorld is false"); return CFixedVector3D(); } return CFixedVector3D(m_X, GetHeightFixed(), m_Z); } virtual CFixedVector2D GetPosition2D() const { if (!m_InWorld) { LOGERROR("CCmpPosition::GetPosition2D called on entity when IsInWorld is false"); return CFixedVector2D(); } return CFixedVector2D(m_X, m_Z); } virtual CFixedVector3D GetPreviousPosition() const { if (!m_InWorld) { LOGERROR("CCmpPosition::GetPreviousPosition called on entity when IsInWorld is false"); return CFixedVector3D(); } return CFixedVector3D(m_PrevX, GetHeightFixed(), m_PrevZ); } virtual CFixedVector2D GetPreviousPosition2D() const { if (!m_InWorld) { LOGERROR("CCmpPosition::GetPreviousPosition2D called on entity when IsInWorld is false"); return CFixedVector2D(); } return CFixedVector2D(m_PrevX, m_PrevZ); } virtual void TurnTo(entity_angle_t y) { if (m_TurretParent != INVALID_ENTITY) { CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (cmpPosition) y -= cmpPosition->GetRotation().Y; } m_RotY = y; AdvertisePositionChanges(); UpdateMessageSubscriptions(); } virtual void SetYRotation(entity_angle_t y) { if (m_TurretParent != INVALID_ENTITY) { CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (cmpPosition) y -= cmpPosition->GetRotation().Y; } m_RotY = y; m_InterpolatedRotY = m_RotY.ToFloat(); if (m_InWorld) { UpdateXZRotation(); m_LastInterpolatedRotX = m_InterpolatedRotX; m_LastInterpolatedRotZ = m_InterpolatedRotZ; } AdvertisePositionChanges(); UpdateMessageSubscriptions(); } virtual void SetXZRotation(entity_angle_t x, entity_angle_t z) { m_RotX = x; m_RotZ = z; if (m_InWorld) { UpdateXZRotation(); m_LastInterpolatedRotX = m_InterpolatedRotX; m_LastInterpolatedRotZ = m_InterpolatedRotZ; } } virtual CFixedVector3D GetRotation() const { entity_angle_t y = m_RotY; if (m_TurretParent != INVALID_ENTITY) { CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (cmpPosition) y += cmpPosition->GetRotation().Y; } return CFixedVector3D(m_RotX, y, m_RotZ); } virtual fixed GetDistanceTravelled() const { if (!m_InWorld) { LOGERROR("CCmpPosition::GetDistanceTravelled called on entity when IsInWorld is false"); return fixed::Zero(); } return CFixedVector2D(m_X - m_LastX, m_Z - m_LastZ).Length(); } float GetConstructionProgressOffset(const CVector3D& pos) const { if (m_ConstructionProgress.IsZero()) return 0.0f; CmpPtr cmpVisual(GetEntityHandle()); if (!cmpVisual) return 0.0f; // We use selection boxes to calculate the model size, since the model could be offset // TODO: this annoyingly shows decals, would be nice to hide them CBoundingBoxOriented bounds = cmpVisual->GetSelectionBox(); if (bounds.IsEmpty()) return 0.0f; float dy = 2.0f * bounds.m_HalfSizes.Y; // If this is a floating unit, we want it to start all the way under the terrain, // so find the difference between its current position and the terrain CmpPtr cmpTerrain(GetSystemEntity()); if (cmpTerrain && (m_Floating || m_ActorFloating)) { float ground = cmpTerrain->GetExactGroundLevel(pos.X, pos.Z); dy += std::max(0.f, pos.Y - ground); } return (m_ConstructionProgress.ToFloat() - 1.0f) * dy; } virtual void GetInterpolatedPosition2D(float frameOffset, float& x, float& z, float& rotY) const { if (!m_InWorld) { LOGERROR("CCmpPosition::GetInterpolatedPosition2D called on entity when IsInWorld is false"); return; } x = Interpolate(m_LastX.ToFloat(), m_X.ToFloat(), frameOffset); z = Interpolate(m_LastZ.ToFloat(), m_Z.ToFloat(), frameOffset); rotY = m_InterpolatedRotY; } virtual CMatrix3D GetInterpolatedTransform(float frameOffset) const { if (m_TurretParent != INVALID_ENTITY) { CmpPtr cmpPosition(GetSimContext(), m_TurretParent); if (!cmpPosition) { LOGERROR("Turret with parent without position component"); CMatrix3D m; m.SetIdentity(); return m; } if (!cmpPosition->IsInWorld()) { LOGERROR("CCmpPosition::GetInterpolatedTransform called on turret entity when IsInWorld is false"); CMatrix3D m; m.SetIdentity(); return m; } else { CMatrix3D parentTransformMatrix = cmpPosition->GetInterpolatedTransform(frameOffset); CMatrix3D ownTransformation = CMatrix3D(); ownTransformation.SetYRotation(m_InterpolatedRotY); ownTransformation.Translate(-m_TurretPosition.X.ToFloat(), m_TurretPosition.Y.ToFloat(), -m_TurretPosition.Z.ToFloat()); return parentTransformMatrix * ownTransformation; } } if (!m_InWorld) { LOGERROR("CCmpPosition::GetInterpolatedTransform called on entity when IsInWorld is false"); CMatrix3D m; m.SetIdentity(); return m; } float x, z, rotY; GetInterpolatedPosition2D(frameOffset, x, z, rotY); float baseY = 0; if (m_RelativeToGround) { CmpPtr cmpTerrain(GetSystemEntity()); if (cmpTerrain) baseY = cmpTerrain->GetExactGroundLevel(x, z); if (m_Floating || m_ActorFloating) { CmpPtr cmpWaterManager(GetSystemEntity()); if (cmpWaterManager) baseY = std::max(baseY, cmpWaterManager->GetExactWaterLevel(x, z) - m_FloatDepth.ToFloat()); } } float y = baseY + m_Y.ToFloat() + Interpolate(-1 * m_LastYDifference.ToFloat(), 0.f, frameOffset); CMatrix3D m; // linear interpolation is good enough (for RotX/Z). // As you always stay close to zero angle. m.SetXRotation(Interpolate(m_LastInterpolatedRotX, m_InterpolatedRotX, frameOffset)); m.RotateZ(Interpolate(m_LastInterpolatedRotZ, m_InterpolatedRotZ, frameOffset)); CVector3D pos(x, y, z); pos.Y += GetConstructionProgressOffset(pos); m.RotateY(rotY + (float)M_PI); m.Translate(pos); return m; } void GetInterpolatedPositions(CVector3D& pos0, CVector3D& pos1) const { float baseY0 = 0; float baseY1 = 0; float x0 = m_LastX.ToFloat(); float z0 = m_LastZ.ToFloat(); float x1 = m_X.ToFloat(); float z1 = m_Z.ToFloat(); if (m_RelativeToGround) { CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); if (cmpTerrain) { baseY0 = cmpTerrain->GetExactGroundLevel(x0, z0); baseY1 = cmpTerrain->GetExactGroundLevel(x1, z1); } if (m_Floating || m_ActorFloating) { CmpPtr cmpWaterManager(GetSimContext(), SYSTEM_ENTITY); if (cmpWaterManager) { baseY0 = std::max(baseY0, cmpWaterManager->GetExactWaterLevel(x0, z0) - m_FloatDepth.ToFloat()); baseY1 = std::max(baseY1, cmpWaterManager->GetExactWaterLevel(x1, z1) - m_FloatDepth.ToFloat()); } } } float y0 = baseY0 + m_Y.ToFloat() + m_LastYDifference.ToFloat(); float y1 = baseY1 + m_Y.ToFloat(); pos0 = CVector3D(x0, y0, z0); pos1 = CVector3D(x1, y1, z1); pos0.Y += GetConstructionProgressOffset(pos0); pos1.Y += GetConstructionProgressOffset(pos1); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Interpolate: { PROFILE("Position::Interpolate"); const CMessageInterpolate& msgData = static_cast (msg); float rotY = m_RotY.ToFloat(); if (rotY != m_InterpolatedRotY) { float delta = rotY - m_InterpolatedRotY; // Wrap delta to -M_PI..M_PI delta = fmodf(delta + (float)M_PI, 2*(float)M_PI); // range -2PI..2PI if (delta < 0) delta += 2*(float)M_PI; // range 0..2PI delta -= (float)M_PI; // range -M_PI..M_PI // Clamp to max rate float deltaClamped = clamp(delta, -m_RotYSpeed*msgData.deltaSimTime, +m_RotYSpeed*msgData.deltaSimTime); // Calculate new orientation, in a peculiar way in order to make sure the // result gets close to m_orientation (rather than being n*2*M_PI out) m_InterpolatedRotY = rotY + deltaClamped - delta; // update the visual XZ rotation if (m_InWorld) { m_LastInterpolatedRotX = m_InterpolatedRotX; m_LastInterpolatedRotZ = m_InterpolatedRotZ; UpdateXZRotation(); } UpdateMessageSubscriptions(); } break; } case MT_TurnStart: { m_LastInterpolatedRotX = m_InterpolatedRotX; m_LastInterpolatedRotZ = m_InterpolatedRotZ; if (m_InWorld && (m_LastX != m_X || m_LastZ != m_Z)) UpdateXZRotation(); // Store the positions from the turn before m_PrevX = m_LastX; m_PrevZ = m_LastZ; m_LastX = m_X; m_LastZ = m_Z; m_LastYDifference = entity_pos_t::Zero(); // warn when a position change also causes a territory change under the entity if (m_InWorld) { player_id_t newTerritory; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager) newTerritory = cmpTerritoryManager->GetOwner(m_X, m_Z); else newTerritory = INVALID_PLAYER; if (newTerritory != m_Territory) { m_Territory = newTerritory; CMessageTerritoryPositionChanged msg(GetEntityId(), m_Territory); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } } else if (m_Territory != INVALID_PLAYER) { m_Territory = INVALID_PLAYER; CMessageTerritoryPositionChanged msg(GetEntityId(), m_Territory); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } break; } case MT_TerrainChanged: case MT_WaterChanged: { AdvertiseInterpolatedPositionChanges(); break; } case MT_Deserialized: { Deserialized(); break; } } } private: /* * Must be called whenever m_RotY or m_InterpolatedRotY change, * to determine whether we need to call Interpolate to make the unit rotate. */ void UpdateMessageSubscriptions() { bool needInterpolate = false; float rotY = m_RotY.ToFloat(); if (rotY != m_InterpolatedRotY) needInterpolate = true; if (needInterpolate != m_EnabledMessageInterpolate) { GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_Interpolate, this, needInterpolate); m_EnabledMessageInterpolate = needInterpolate; } } /** * This must be called after changing anything that will affect the * return value of GetPosition2D() or GetRotation().Y: * - m_InWorld * - m_X, m_Z * - m_RotY */ void AdvertisePositionChanges() const { for (std::set::const_iterator it = m_Turrets.begin(); it != m_Turrets.end(); ++it) { CmpPtr cmpPosition(GetSimContext(), *it); if (cmpPosition) cmpPosition->UpdateTurretPosition(); } if (m_InWorld) { CMessagePositionChanged msg(GetEntityId(), true, m_X, m_Z, m_RotY); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } else { CMessagePositionChanged msg(GetEntityId(), false, entity_pos_t::Zero(), entity_pos_t::Zero(), entity_angle_t::Zero()); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } } /** * This must be called after changing anything that will affect the * return value of GetInterpolatedPositions(): * - m_InWorld * - m_X, m_Z * - m_LastX, m_LastZ * - m_Y, m_LastYDifference, m_RelativeToGround * - If m_RelativeToGround, then the ground under this unit * - If m_RelativeToGround && m_Float, then the water level */ void AdvertiseInterpolatedPositionChanges() const { if (m_InWorld) { CVector3D pos0, pos1; GetInterpolatedPositions(pos0, pos1); CMessageInterpolatedPositionChanged msg(GetEntityId(), true, pos0, pos1); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } else { CMessageInterpolatedPositionChanged msg(GetEntityId(), false, CVector3D(), CVector3D()); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } } void UpdateXZRotation() { if (!m_InWorld) { LOGERROR("CCmpPosition::UpdateXZRotation called on entity when IsInWorld is false"); return; } if (m_AnchorType == UPRIGHT || !m_RotZ.IsZero() || !m_RotX.IsZero()) { // set the visual rotations to the ones fixed by the interface m_InterpolatedRotX = m_RotX.ToFloat(); m_InterpolatedRotZ = m_RotZ.ToFloat(); return; } CmpPtr cmpTerrain(GetSystemEntity()); if (!cmpTerrain || !cmpTerrain->IsLoaded()) { LOGERROR("Terrain not loaded"); return; } // TODO: average normal (average all the tiles?) for big units or for buildings? CVector3D normal = cmpTerrain->CalcExactNormal(m_X.ToFloat(), m_Z.ToFloat()); // rotate the normal so the positive x direction is in the direction of the unit CVector2D projected = CVector2D(normal.X, normal.Z); projected.Rotate(m_InterpolatedRotY); normal.X = projected.X; normal.Z = projected.Y; // project and calculate the angles if (m_AnchorType == PITCH || m_AnchorType == PITCH_ROLL) m_InterpolatedRotX = -atan2(normal.Z, normal.Y); if (m_AnchorType == ROLL || m_AnchorType == PITCH_ROLL) m_InterpolatedRotZ = atan2(normal.X, normal.Y); } }; REGISTER_COMPONENT_TYPE(Position)