Index: binaries/data/mods/public/simulation/ai/common-api/shared.js =================================================================== --- binaries/data/mods/public/simulation/ai/common-api/shared.js +++ binaries/data/mods/public/simulation/ai/common-api/shared.js @@ -218,9 +218,13 @@ for (let evt of state.events.ConstructionFinished) { - // metada are already moved by EntityRenamed when needed (i.e. construction, not repair) - if (evt.entity != evt.newentity) - foundationFinished[evt.entity] = true; + foundationFinished[evt.entity] = true; + for (let player of this._players) + { + this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity]; + this._entityMetadata[player][evt.entity] = {}; + } + state.entities[evt.newentity].foundationProgress = undefined; } for (let evt of state.events.AIMetadata) Index: binaries/data/mods/public/simulation/ai/petra/basesManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/basesManager.js +++ binaries/data/mods/public/simulation/ai/petra/basesManager.js @@ -200,13 +200,39 @@ for (const evt of events.ConstructionFinished) { - if (evt.newentity == evt.entity) // repaired building - continue; + //if (evt.newentity == evt.entity) // repaired building + //continue; const ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID) continue; if (ent.getMetadata(PlayerID, "base") === undefined) - continue; + { + if (ent.hasClass("Unit")) + PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent); + else if (ent.hasClass("CivCentre")) // build a new base around it + { + let newbase; + if (ent.foundationProgress() !== undefined) + newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_UNCONSTRUCTED); + else + { + newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_CAPTURED); + addBase = true; + } + newbase.assignEntity(gameState, ent); + } + else + { + let base; + // If dropsite on new island, create a base around it + if (!ent.decaying() && ent.resourceDropsiteTypes()) + base = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS); + else + base = PETRA.getBestBase(gameState, ent) || this.noBase; + base.assignEntity(gameState, ent); + } + } + const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); base.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes()) Index: binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- binaries/data/mods/public/simulation/components/AIProxy.js +++ binaries/data/mods/public/simulation/components/AIProxy.js @@ -37,6 +37,12 @@ this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); }; +AIProxy.prototype.Update = function() +{ + this.needsFullGet = true; + this.NotifyChange(); +}; + AIProxy.prototype.Serialize = null; // we have no dynamic state to save AIProxy.prototype.Deserialize = function() Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -207,6 +207,15 @@ { }; +Attack.prototype.Update = function(newTemplate) { + this.template = newTemplate; + + // TODO: this should probably be done in UnitAI directly instead. + let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.UpdateRangeQueries(); +}; + Attack.prototype.GetAttackTypes = function(wantedTypes) { let types = g_AttackTypes.filter(type => !!this.template[type]); Index: binaries/data/mods/public/simulation/components/AttackDetection.js =================================================================== --- binaries/data/mods/public/simulation/components/AttackDetection.js +++ binaries/data/mods/public/simulation/components/AttackDetection.js @@ -22,6 +22,13 @@ this.suppressedList = []; }; +AttackDetection.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.Init(); +}; + + AttackDetection.prototype.ActivateTimer = function() { Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetTimeout(this.entity, IID_AttackDetection, "HandleTimeout", this.suppressionTime); Index: binaries/data/mods/public/simulation/components/Auras.js =================================================================== --- binaries/data/mods/public/simulation/components/Auras.js +++ binaries/data/mods/public/simulation/components/Auras.js @@ -18,6 +18,12 @@ this.Clean(); }; +Auras.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.Init(); +}; + // We can modify identifier if we want stackable auras in some case. Auras.prototype.GetModifierIdentifier = function(name) { Index: binaries/data/mods/public/simulation/components/AutoBuildable.js =================================================================== --- binaries/data/mods/public/simulation/components/AutoBuildable.js +++ binaries/data/mods/public/simulation/components/AutoBuildable.js @@ -5,6 +5,12 @@ this.UpdateRate(); } + Update(newTemplate) + { + this.template = newTemplate; + this.UpdateRate(); + } + /** * @return {number} - The rate with technologies and aura modification applied. */ Index: binaries/data/mods/public/simulation/components/BattleDetection.js =================================================================== --- binaries/data/mods/public/simulation/components/BattleDetection.js +++ binaries/data/mods/public/simulation/components/BattleDetection.js @@ -39,6 +39,11 @@ this.state = "PEACE"; }; +BattleDetection.prototype.Update = function(newTemplate) +{ + warn("BattleDetection.Update() is not implemented"); +}; + BattleDetection.prototype.SetState = function(state) { if (state == this.state) Index: binaries/data/mods/public/simulation/components/Builder.js =================================================================== --- binaries/data/mods/public/simulation/components/Builder.js +++ binaries/data/mods/public/simulation/components/Builder.js @@ -27,6 +27,16 @@ { }; +Builder.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + + // Token changes may require selection updates. + let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + if (cmpPlayer) + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); +}; + Builder.prototype.GetEntitiesList = function() { let string = this.template.Entities._string; Index: binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- binaries/data/mods/public/simulation/components/BuildingAI.js +++ binaries/data/mods/public/simulation/components/BuildingAI.js @@ -30,6 +30,14 @@ this.targetUnits = []; }; +BuildingAI.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.targetUnits = []; + this.SetupRangeQuery(); + this.SetupGaiaRangeQuery(); +}; + BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg) { let classes = this.template.GarrisonArrowClasses; Index: binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- binaries/data/mods/public/simulation/components/Capturable.js +++ binaries/data/mods/public/simulation/components/Capturable.js @@ -19,6 +19,31 @@ this.capturePoints = []; }; +Capturable.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + + if (!this.capturePoints.length) + { + this.UpdateCachedValues(); + let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + + const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + // As a sanity fallback, assign the points to gaia if we don't have an owner. + const to = cmpOwnership ? cmpOwnership.GetOwner() : 0; + for (let i = 0; i < numPlayers; ++i) + { + if (i == to) + this.capturePoints[i] = this.maxCapturePoints; + else + this.capturePoints[i] = 0; + } + Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints }); + this.CheckTimer(); + } else + this.UpdateCachedValuesAndNotify(); +} + // Interface functions /** Index: binaries/data/mods/public/simulation/components/Cost.js =================================================================== --- binaries/data/mods/public/simulation/components/Cost.js +++ binaries/data/mods/public/simulation/components/Cost.js @@ -27,6 +27,12 @@ this.populationCost = +this.template.Population; }; +Cost.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.populationCost = +this.template.Population; +}; + Cost.prototype.GetPopCost = function() { return this.populationCost; Index: binaries/data/mods/public/simulation/components/EntityLimits.js =================================================================== --- binaries/data/mods/public/simulation/components/EntityLimits.js +++ binaries/data/mods/public/simulation/components/EntityLimits.js @@ -101,6 +101,10 @@ } }; +EntityLimits.prototype.Update = function(newTemplate) +{ +} + EntityLimits.prototype.ChangeCount = function(category, value) { if (this.count[category] !== undefined) @@ -298,4 +302,55 @@ this.UpdateLimitRemoval(); }; +EntityLimits.prototype.OnGlobalIdentityClassesChanged = function(msg) +{ + const playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); + const cmpOwnership = Engine.QueryInterface(msg.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() != playerID) + return; + + // foundations shouldn't change the entity limits until they're completed + if (Engine.QueryInterface(msg.entity, IID_Foundation)) + return; + + const oldClasses = msg.from; + const newClasses = msg.to; + + for (const category in this.changers) + for (const cls in this.changers[category]) + { + let inOld = oldClasses.indexOf(cls) !== -1; + let inNew = newClasses.indexOf(cls) !== -1; + // Change limit counts if there is a difference between the old and new classes + if (!inOld && inNew) + { + if (this.limit[category] !== undefined) + this.limit[category] += this.changers[category][cls]; + if (this.removedLimit[category] !== undefined) + this.removedLimit[category] += this.changers[category][cls]; + } + else if (inOld && !inNew) + { + if (this.limit[category] != undefined) + this.limit[category] -= this.changers[category][cls]; + if (this.removedLimit[category] != undefined) + this.removedLimit[category] -= this.changers[category][cls]; + } + } + + for (const category in this.removers) + if ("RequiredClasses" in this.removers[category]) + for (const cls of this.removers[category].RequiredClasses) + { + let inOld = oldClasses.indexOf(cls) >= 0; + let inNew = newClasses.indexOf(cls) >= 0; + if (!inOld && inNew) + this.classCount[cls]++; + else if (inOld && !inNew) + this.classCount[cls]--; + } + + this.UpdateLimitRemoval(); +}; + Engine.RegisterComponentType(IID_EntityLimits, "EntityLimits", EntityLimits); Index: binaries/data/mods/public/simulation/components/Fogging.js =================================================================== --- binaries/data/mods/public/simulation/components/Fogging.js +++ binaries/data/mods/public/simulation/components/Fogging.js @@ -39,6 +39,12 @@ } }; +Fogging.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + // Do we need to do anything ? +}; + Fogging.prototype.Activate = function() { let mustUpdate = !this.activated; Index: binaries/data/mods/public/simulation/components/Formation.js =================================================================== --- binaries/data/mods/public/simulation/components/Formation.js +++ binaries/data/mods/public/simulation/components/Formation.js @@ -159,6 +159,11 @@ .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null); }; +Formation.prototype.Update = function(newTemplate) +{ + warn("Updating Formation is not supported"); +}; + Formation.prototype.Serialize = function() { let result = {}; Index: binaries/data/mods/public/simulation/components/FormationAttack.js =================================================================== --- binaries/data/mods/public/simulation/components/FormationAttack.js +++ binaries/data/mods/public/simulation/components/FormationAttack.js @@ -10,6 +10,12 @@ this.canAttackAsFormation = this.template.CanAttackAsFormation == "true"; }; +FormationAttack.prototype.Update = function(newTemplate) +{ + // TODO + warn("FormationAttack component doesn't support template updates yet"); +}; + FormationAttack.prototype.CanAttackAsFormation = function() { return this.canAttackAsFormation; Index: binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- binaries/data/mods/public/simulation/components/Foundation.js +++ binaries/data/mods/public/simulation/components/Foundation.js @@ -23,6 +23,12 @@ this.previewEntity = INVALID_ENTITY; }; +Foundation.prototype.Update = function(newTemplate) +{ + // TODO + warn("Foundation component doesn't support template updates yet"); +} + Foundation.prototype.Serialize = function() { let ret = Object.assign({}, this); @@ -337,18 +343,6 @@ let building = ChangeEntityTemplate(this.entity, this.finalTemplateName); - const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - const cmpBuildingIdentity = Engine.QueryInterface(building, IID_Identity); - if (cmpIdentity && cmpBuildingIdentity) - { - const oldPhenotype = cmpIdentity.GetPhenotype(); - if (cmpBuildingIdentity.GetPhenotype() !== oldPhenotype) - { - cmpBuildingIdentity.SetPhenotype(oldPhenotype); - Engine.QueryInterface(building, IID_Visual)?.RecomputeActorName(); - } - } - if (cmpPlayerStatisticsTracker) cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building); @@ -357,6 +351,12 @@ Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": building }); + if (this.previewEntity) + { + Engine.DestroyEntity(this.previewEntity); + this.previewEntity = INVALID_ENTITY; + } + for (let builder of this.GetBuilders()) { let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI); Index: binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -49,6 +49,14 @@ this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity); }; + +GarrisonHolder.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity); + this.EjectOrKill(this.entities.filter(entity => !this.IsAllowedToBeGarrisoned(entity))); +}; + /** * @param {number} entity - The entity to verify. * @return {boolean} - Whether the given entity is garrisoned in this GarrisonHolder. Index: binaries/data/mods/public/simulation/components/Garrisonable.js =================================================================== --- binaries/data/mods/public/simulation/components/Garrisonable.js +++ binaries/data/mods/public/simulation/components/Garrisonable.js @@ -13,6 +13,22 @@ { }; +Garrisonable.prototype.Update = function(newTemplate) +{ + if (!this.holder) + return; + + let holder = this.holder; + this.UnGarrison(true, true); + + // Now that we're ungarrisoned, we can update our size. + this.template = newTemplate; + + let cmpGarrisonable = Engine.QueryInterface(msg.newentity, IID_Garrisonable); + if (cmpGarrisonable) + cmpGarrisonable.Garrison(holder, true); +}; + /** * @param {string} type - Unused. * @param {number} target - The entity ID of the target to check. Index: binaries/data/mods/public/simulation/components/Gate.js =================================================================== --- binaries/data/mods/public/simulation/components/Gate.js +++ binaries/data/mods/public/simulation/components/Gate.js @@ -20,6 +20,19 @@ this.locked = false; }; +Gate.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + // We should have an owner, as we had one before the template change. + this.SetupRangeQuery(Engine.QueryInterface(this.entity, IID_Ownership).GetOwner()); + + if (!this.locked) { + // TODO: ordering dependency on cmpObstruction here... + let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); + cmpObstruction.SetDisableBlockMovementPathfinding(this.opened, true, 0); + } +}; + Gate.prototype.OnOwnershipChanged = function(msg) { if (msg.to != INVALID_PLAYER) Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -2051,6 +2051,29 @@ this.renamedEntities.push(msg); }; +GuiInterface.prototype.OnGlobalTemplateChanged = function(msg) +{ + this.renamedEntities.push({ entity: msg.entity, newentity: msg.entity }); + + const cmpOwnership = Engine.QueryInterface(msg.entity, IID_Ownership); + if (cmpOwnership && cmpOwnership.GetOwner()) { + this.templateModified[cmpOwnership.GetOwner()] = true; + this.selectionDirty[cmpOwnership.GetOwner()] = true; + } +}; + +GuiInterface.prototype.OnGlobalConstructionFinished = function(msg) +{ + this.renamedEntities.push({ entity: msg.entity, newentity: msg.entity }); + + const cmpOwnership = Engine.QueryInterface(msg.entity, IID_Ownership); + if (cmpOwnership && cmpOwnership.GetOwner()) { + this.templateModified[cmpOwnership.GetOwner()] = true; + this.selectionDirty[cmpOwnership.GetOwner()] = true; + } +}; + + /** * List the GuiInterface functions that can be safely called by GUI scripts. * (GUI scripts are non-deterministic and untrusted, so these functions must be Index: binaries/data/mods/public/simulation/components/Heal.js =================================================================== --- binaries/data/mods/public/simulation/components/Heal.js +++ binaries/data/mods/public/simulation/components/Heal.js @@ -49,6 +49,17 @@ { }; +Heal.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + // TODO: only do this if something changed + let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); + if (!cmpUnitAI) + return; + + cmpUnitAI.UpdateRangeQueries(); +}; + Heal.prototype.GetTimers = function() { return { Index: binaries/data/mods/public/simulation/components/Health.js =================================================================== --- binaries/data/mods/public/simulation/components/Health.js +++ binaries/data/mods/public/simulation/components/Health.js @@ -64,6 +64,12 @@ this.UpdateActor(); }; +Health.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.RecalculateValues(); +}; + /** * Returns the current hitpoint value. * This is 0 if (and only if) the unit is dead. Index: binaries/data/mods/public/simulation/components/Identity.js =================================================================== --- binaries/data/mods/public/simulation/components/Identity.js +++ binaries/data/mods/public/simulation/components/Identity.js @@ -99,6 +99,25 @@ this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true; }; +Identity.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + + const oldClasses = this.classesList; + this.classesList = GetIdentityClasses(this.template); + this.visibleClassesList = GetVisibleIdentityClasses(this.template); + + if (!this.template.Phenotype) + delete this.phenotype; + else if (this.GetPossiblePhenotypes().indexOf(this.phenotype) === -1) + this.phenotype = pickRandom(this.GetPossiblePhenotypes()); + + this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true; + + Engine.PostMessage(this.entity, MT_IdentityClassesChanged, { "entity": this.entity, "from": oldClasses, "to": this.classesList }); +}; + + Identity.prototype.GetCiv = function() { return this.template.Civ; Index: binaries/data/mods/public/simulation/components/Market.js =================================================================== --- binaries/data/mods/public/simulation/components/Market.js +++ binaries/data/mods/public/simulation/components/Market.js @@ -21,6 +21,12 @@ this.tradeType = new Set(this.template.TradeType.split(/\s+/)); }; +Market.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + warn("Market.Update: not implemented"); +}; + Market.prototype.AddTrader = function(ent) { this.traders.add(ent); Index: binaries/data/mods/public/simulation/components/Mirage.js =================================================================== --- binaries/data/mods/public/simulation/components/Mirage.js +++ binaries/data/mods/public/simulation/components/Mirage.js @@ -16,6 +16,12 @@ this.miragedIids = new Map(); }; +Mirage.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + warn("Mirage.Update: not implemented"); +}; + Mirage.prototype.SetParent = function(ent) { this.parent = ent; Index: binaries/data/mods/public/simulation/components/ModifiersManager.js =================================================================== --- binaries/data/mods/public/simulation/components/ModifiersManager.js +++ binaries/data/mods/public/simulation/components/ModifiersManager.js @@ -254,6 +254,49 @@ Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); }; +ModifiersManager.prototype.OnGlobalIdentityClassesChanged = function(msg) +{ + const entity = msg.entity; + const newclasses = msg.to; + const oldclasses = msg.from; + + // Invalidate all caches. + for (let propName of this.cachedValues.keys()) + this.InvalidateCache(propName, entity); + + // Warn entities that our values have changed. + // Local modifiers will be added by the relevant components, so no need to check for them here. + let modifiedComponents = {}; + let modifs = this.modifiersStorage.GetAllItems(entity); + + let owner = QueryOwnerEntityID(entity); + if (owner) + { + let playerModifs = this.modifiersStorage.GetAllItems(QueryOwnerEntityID(entity)); + for (let propertyName in playerModifs) + modifs[propertyName] = playerModifs[propertyName]; + } + + for (let propertyName in modifs) + { + // We only need to find one one tech per component for a match. + let component = propertyName.split("/")[0]; + // Only inform if the modifier actually applies to the entity as an optimisation. + // TODO: would it be better to call FetchModifiedProperty here and compare values? + modifs[propertyName].forEach(item => item.value.forEach(modif => { + if (!DoesModificationApply(modif, newclasses) && !DoesModificationApply(modif, oldclasses)) + return; + if (!modifiedComponents[component]) + modifiedComponents[component] = []; + modifiedComponents[component].push(propertyName); + })); + } + + for (let component in modifiedComponents) + Engine.PostMessage(entity, MT_ValueModification, { "entities": [entity], "component": component, "valueNames": modifiedComponents[component] }); +}; + + /** * The following functions simply proxy MultiKeyMap's interface. */ Index: binaries/data/mods/public/simulation/components/Pack.js =================================================================== --- binaries/data/mods/public/simulation/components/Pack.js +++ binaries/data/mods/public/simulation/components/Pack.js @@ -24,6 +24,12 @@ this.timer = undefined; }; +Pack.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + warn("Pack.Update: not implemented"); +}; + Pack.prototype.OnDestroy = function() { this.CancelTimer(); Index: binaries/data/mods/public/simulation/components/Player.js =================================================================== --- binaries/data/mods/public/simulation/components/Player.js +++ binaries/data/mods/public/simulation/components/Player.js @@ -111,6 +111,11 @@ }); }; +Player.prototype.Update = function(newTemplate) +{ + warn("Player.Update: not implemented"); +}; + Player.prototype.SetPlayerID = function(id) { this.playerID = id; Index: binaries/data/mods/public/simulation/components/Population.js =================================================================== --- binaries/data/mods/public/simulation/components/Population.js +++ binaries/data/mods/public/simulation/components/Population.js @@ -14,6 +14,12 @@ this.bonus = +this.template.Bonus; }; +Population.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.RecalculateValues(); +}; + /** * @return {number} - The population space provided by this entity. */ Index: binaries/data/mods/public/simulation/components/Promotion.js =================================================================== --- binaries/data/mods/public/simulation/components/Promotion.js +++ binaries/data/mods/public/simulation/components/Promotion.js @@ -19,6 +19,13 @@ this.ComputeTrickleRate(); }; +Promotion.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.ComputeTrickleRate(); + this.IncreaseXp(0); +}; + Promotion.prototype.GetRequiredXp = function() { return ApplyValueModificationsToEntity("Promotion/RequiredXp", +this.template.RequiredXp, this.entity); Index: binaries/data/mods/public/simulation/components/RangeOverlayManager.js =================================================================== --- binaries/data/mods/public/simulation/components/RangeOverlayManager.js +++ binaries/data/mods/public/simulation/components/RangeOverlayManager.js @@ -14,6 +14,13 @@ this.rangeVisualizations = new Map(); }; +RangeOverlayManager.prototype.Update = function() +{ + this.UpdateRangeOverlays("Heal") + this.UpdateRangeOverlays("Attack"); + this.RegenerateRangeOverlays(false); +}; + // The GUI enables visualizations RangeOverlayManager.prototype.Serialize = null; Index: binaries/data/mods/public/simulation/components/Repairable.js =================================================================== --- binaries/data/mods/public/simulation/components/Repairable.js +++ binaries/data/mods/public/simulation/components/Repairable.js @@ -18,6 +18,12 @@ this.repairTimeRatio = +this.template.RepairTimeRatio; }; +Repairable.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.repairTimeRatio = +this.template.RepairTimeRatio; +}; + /** * Returns the current build progress in a [0,1] range. */ Index: binaries/data/mods/public/simulation/components/Researcher.js =================================================================== --- binaries/data/mods/public/simulation/components/Researcher.js +++ binaries/data/mods/public/simulation/components/Researcher.js @@ -147,6 +147,12 @@ this.queue = new Map(); }; +Researcher.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + // TODO: what to do if the queue now contains invalid items ? +}; + Researcher.prototype.Serialize = function() { const queue = []; Index: binaries/data/mods/public/simulation/components/Resistance.js =================================================================== --- binaries/data/mods/public/simulation/components/Resistance.js +++ binaries/data/mods/public/simulation/components/Resistance.js @@ -76,6 +76,11 @@ this.attackers = new Set(); }; +Resistance.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; +}; + Resistance.prototype.IsInvulnerable = function() { return this.invulnerable; Index: binaries/data/mods/public/simulation/components/ResourceDropsite.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceDropsite.js +++ binaries/data/mods/public/simulation/components/ResourceDropsite.js @@ -18,6 +18,16 @@ this.shared = this.sharable; }; +ResourceDropsite.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + + let wasSharable = this.sharable; + this.sharable = this.template.Sharable == "true"; + if (this.sharable && !wasSharable) + this.shared = this.sharable; +}; + /** * Returns the list of resource types accepted by this dropsite, * as defined by it being referred to in the template and the resource being enabled. Index: binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -49,6 +49,20 @@ this.lastCarriedType = undefined; // { generic, specific } }; +ResourceGatherer.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + this.RecalculateGatherRates(); + this.RecalculateCapacities(); + + for (let type in this.carrying) { + if (!(type in this.capacities)) + delete this.carrying[type]; + if (this.carrying[type] > this.capacities[type]) + this.carrying[type] = this.capacities[type]; + } +}; + /** * Returns data about what resources the unit is currently carrying, * in the form [ {"type":"wood", "amount":7, "max":10} ] Index: binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceSupply.js +++ binaries/data/mods/public/simulation/components/ResourceSupply.js @@ -115,6 +115,31 @@ } }; +ResourceSupply.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; + + // TODO: figure out what we want to do with the amount exactly. + let oldAmount = this.amount; + let oldType = this.cachedType; + + this.amount = +(this.template.Initial || this.template.Max); + + let [type, subtype] = this.template.Type.split('.'); + this.cachedType = { "generic": type, "specific": subtype }; + + if (oldType.generic != type || oldType.specific != subtype) { + // TODO: send message. + } else if (oldAmount != this.amount) { + Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { + "from": oldAmount, + "to": this.amount + }); + } + + this.RecalculateValues(); +}; + ResourceSupply.prototype.IsInfinite = function() { return !isFinite(+this.template.Max); Index: binaries/data/mods/public/simulation/components/ResourceTrickle.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceTrickle.js +++ binaries/data/mods/public/simulation/components/ResourceTrickle.js @@ -15,6 +15,11 @@ this.CheckTimer(); }; +ResourceTrickle.prototype.Update = function() +{ + warn("ResourceTrickle.Update not implemented"); +}; + ResourceTrickle.prototype.GetInterval = function() { return this.trickleInterval; Index: binaries/data/mods/public/simulation/components/SkirmishReplacer.js =================================================================== --- binaries/data/mods/public/simulation/components/SkirmishReplacer.js +++ binaries/data/mods/public/simulation/components/SkirmishReplacer.js @@ -13,6 +13,11 @@ { }; +SkirmishReplacer.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; +}; + SkirmishReplacer.prototype.Serialize = null; // We have no dynamic state to save function getReplacementEntities(civ) Index: binaries/data/mods/public/simulation/components/StatisticsTracker.js =================================================================== --- binaries/data/mods/public/simulation/components/StatisticsTracker.js +++ binaries/data/mods/public/simulation/components/StatisticsTracker.js @@ -92,6 +92,10 @@ this.entity, IID_StatisticsTracker, "UpdateSequences", 0, this.UpdateSequenceInterval); }; +StatisticsTracker.prototype.Update = function() { + warn("StatisticsTracker.Update is not implemented") +} + StatisticsTracker.prototype.OnGlobalInitGame = function() { this.sequences = clone(this.GetStatistics()); Index: binaries/data/mods/public/simulation/components/StatusBars.js =================================================================== --- binaries/data/mods/public/simulation/components/StatusBars.js +++ binaries/data/mods/public/simulation/components/StatusBars.js @@ -43,6 +43,13 @@ this.auraSources = new Map(); }; +StatusBars.prototype.Update = function(newTemplate) { + this.template = newTemplate; + + if (this.enabled) + this.RegenerateSprites(); +}; + /** * Don't serialise this.enabled since it's modified by the GUI. */ Index: binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- binaries/data/mods/public/simulation/components/TechnologyManager.js +++ binaries/data/mods/public/simulation/components/TechnologyManager.js @@ -3,6 +3,10 @@ TechnologyManager.prototype.Schema = ""; +TechnologyManager.prototype.Update = function() { + warn("TechnologyManager.Update: not implemented"); +} + /** * This object represents a technology under research. * @param {string} templateName - The name of the template to research. @@ -423,6 +427,79 @@ } }; +TechnologyManager.prototype.OnGlobalIdentityClassesChanged = function(msg) +{ + const playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); + const cmpOwnership = Engine.QueryInterface(msg.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() != playerID) + return; + + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); + + // don't use foundations for the class counts but check if techs apply (e.g. health increase) + if (Engine.QueryInterface(msg.entity, IID_Foundation)) + return; + + let oldClasses = msg.from; + let newClasses = msg.to; + + // Add first so we don't delete the old class if it's the same as the new one + for (let cls of newClasses) + { + this.classCounts[cls] = this.classCounts[cls] || 0; + this.classCounts[cls] += 1; + + this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; + this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; + this.typeCountsByClass[cls][template] += 1; + } + + for (let cls of oldClasses) + { + this.classCounts[cls] -= 1; + if (this.classCounts[cls] <= 0) + delete this.classCounts[cls]; + + this.typeCountsByClass[cls][template] -= 1; + if (this.typeCountsByClass[cls][template] <= 0) + delete this.typeCountsByClass[cls][template]; + } +}; + +TechnologyManager.prototype.OnGlobalTemplateChanged = function(msg) +{ + const playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); + const cmpOwnership = Engine.QueryInterface(msg.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() != playerID) + return; + + // don't use foundations for the class counts but check if techs apply (e.g. health increase) + if (Engine.QueryInterface(msg.entity, IID_Foundation)) + return; + + const cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); + if (!cmpIdentity) + return; + + // We assume that the Identity changes are handled above. + const classes = cmpIdentity.GetClassesList(); + + // Add first so we don't delete the old class if it's the same as the new one + for (let cls of classes) + { + this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; + this.typeCountsByClass[cls][msg.to] = this.typeCountsByClass[cls][msg.to] || 0; + this.typeCountsByClass[cls][msg.to] += 1; + } + for (let cls of classes) + { + this.typeCountsByClass[cls][msg.from] -= 1; + if (this.typeCountsByClass[cls][msg.from] <= 0) + delete this.typeCountsByClass[cls][msg.from]; + } +}; + /** * This does neither apply effects nor verify requirements. * @param {string} tech - The name of the technology to mark as researched. Index: binaries/data/mods/public/simulation/components/TerritoryDecay.js =================================================================== --- binaries/data/mods/public/simulation/components/TerritoryDecay.js +++ binaries/data/mods/public/simulation/components/TerritoryDecay.js @@ -23,6 +23,12 @@ this.territoryOwnership = !isFinite(+this.template.DecayRate); }; +TerritoryDecay.prototype.Update = function(newTemplate) { + this.template = newTemplate; + this.territoryOwnership = !isFinite(+this.template.DecayRate); + this.UpdateDecayState(); +} + TerritoryDecay.prototype.IsConnected = function() { this.connectedNeighbours.fill(0); Index: binaries/data/mods/public/simulation/components/Trader.js =================================================================== --- binaries/data/mods/public/simulation/components/Trader.js +++ binaries/data/mods/public/simulation/components/Trader.js @@ -28,6 +28,10 @@ }; }; +Trader.prototype.Update = function() { + warn("Trader.Update is not implemented") +} + Trader.prototype.CalculateGain = function(currentMarket, nextMarket) { let cmpMarket = QueryMiragedInterface(currentMarket, IID_Market); Index: binaries/data/mods/public/simulation/components/Trainer.js =================================================================== --- binaries/data/mods/public/simulation/components/Trainer.js +++ binaries/data/mods/public/simulation/components/Trainer.js @@ -22,6 +22,20 @@ "" + ""; +Trainer.prototype.Update = function(newTemplate) { + this.template = newTemplate; + + // This also updates the queued production if necessary. + this.CalculateEntitiesMap(); + + // Inform the GUI that it'll need to recompute the selection panel. + // TODO: it would be better to only send the message if something actually changing + // for the current training queue. + const cmpPlayer = QueryOwnerInterface(this.entity); + if (cmpPlayer) + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); +} + /** * This object represents a batch of entities being trained. * @param {string} templateName - The name of the template we ought to train. Index: binaries/data/mods/public/simulation/components/Treasure.js =================================================================== --- binaries/data/mods/public/simulation/components/Treasure.js +++ binaries/data/mods/public/simulation/components/Treasure.js @@ -21,6 +21,10 @@ { }; +Treasure.prototype.Update = function() { + warn("Treasure.Update is not implemented") +}; + Treasure.prototype.ComputeReward = function() { for (let resource in this.template.Resources) Index: binaries/data/mods/public/simulation/components/TreasureCollector.js =================================================================== --- binaries/data/mods/public/simulation/components/TreasureCollector.js +++ binaries/data/mods/public/simulation/components/TreasureCollector.js @@ -13,6 +13,11 @@ { }; +TreasureCollector.prototype.Update = function(newTemplate) +{ + this.template = newTemplate; +}; + /** * @return {Object} - Min/Max range at which this entity can claim a treasure. */ Index: binaries/data/mods/public/simulation/components/TriggerPoint.js =================================================================== --- binaries/data/mods/public/simulation/components/TriggerPoint.js +++ binaries/data/mods/public/simulation/components/TriggerPoint.js @@ -18,6 +18,10 @@ this.triggers = {}; }; +TriggerPoint.prototype.Update = function() { + error("TriggerPoint cannot be upgraded"); +}; + TriggerPoint.prototype.OnDestroy = function() { if (this.template && this.template.EntityReference) Index: binaries/data/mods/public/simulation/components/TurretHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/TurretHolder.js +++ binaries/data/mods/public/simulation/components/TurretHolder.js @@ -25,6 +25,10 @@ }); } + Update() { + warn("TurretHolder.js: Update() is not implemented"); + } + /** * Add a subunit as specified in the template. * This function creates an entity and places it on the turret point. Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -3469,6 +3469,19 @@ this.SetStance(this.template.DefaultStance); }; +UnitAI.prototype.Update = function(newTemplate) { + this.template = newTemplate; + this.cheeringTime = +(this.template.CheeringTime || 0); + + // The current order might not be so achievable anymore, so re-run it. + // TODO: would it be better to send an event to the FSM? Probably + // TODO: this should be done after all components are updated. + if (!this.orderQueue.length) + return; + let order = this.orderQueue.splice(0, 1)[0]; + this.PushOrderFront(order.type, order.data); +}; + /** * @param {cmpTurretable} cmpTurretable - Optionally the component to save a query here. * @return {boolean} - Whether we are occupying a turret point. Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitMotionFlying.js +++ binaries/data/mods/public/simulation/components/UnitMotionFlying.js @@ -63,6 +63,10 @@ this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass); }; +UnitMotionFlying.prototype.Update = function(newTemplate) { + warn("UnitMotionFlying.Update() is not implemented"); +}; + UnitMotionFlying.prototype.OnUpdate = function(msg) { let turnLength = msg.turnLength; Index: binaries/data/mods/public/simulation/components/Upgrade.js =================================================================== --- binaries/data/mods/public/simulation/components/Upgrade.js +++ binaries/data/mods/public/simulation/components/Upgrade.js @@ -55,6 +55,21 @@ this.expendedResources = {}; }; +Upgrade.prototype.Update = function(newTemplate) { + this.template = newTemplate; + + const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) { + warn("Upgrade needs an Ownership component to work - updating template to for " + this.entity); + return; + } + this.owner = cmpOwnership.GetOwner(); + + if (!this.completed) + this.CancelUpgrade(this.owner); + this.DetermineUpgrades(); +}; + // This will also deal with the "OnDestroy" case. Upgrade.prototype.OnOwnershipChanged = function(msg) { Index: binaries/data/mods/public/simulation/components/Upkeep.js =================================================================== --- binaries/data/mods/public/simulation/components/Upkeep.js +++ binaries/data/mods/public/simulation/components/Upkeep.js @@ -15,6 +15,11 @@ this.CheckTimer(); }; +Upkeep.prototype.Update = function(newTemplate) { + this.template = newTemplate; + this.CheckTimer(); +}; + /** * @return {number} - The interval between resource subtractions, in ms. */ Index: binaries/data/mods/public/simulation/components/Visibility.js =================================================================== --- binaries/data/mods/public/simulation/components/Visibility.js +++ binaries/data/mods/public/simulation/components/Visibility.js @@ -18,6 +18,7 @@ "" + ""; + Visibility.prototype.Init = function() { this.retainInFog = this.template.RetainInFog == "true"; @@ -31,6 +32,18 @@ this.SetActivated(true); }; +Visibility.prototype.Update = function(newTemplate) { + this.template = newTemplate; + + this.retainInFog = this.template.RetainInFog == "true"; + this.alwaysVisible = this.template.AlwaysVisible == "true"; + this.corpse = this.template.Corpse == "true"; + this.preview = this.template.Preview == "true"; + + // Notify the range manager the visibility of this entity must be updated. + Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).RequestVisibilityUpdate(this.entity); +}; + /** * Sets the range manager scriptedVisibility flag for this entity. */ Index: binaries/data/mods/public/simulation/components/VisionSharing.js =================================================================== --- binaries/data/mods/public/simulation/components/VisionSharing.js +++ binaries/data/mods/public/simulation/components/VisionSharing.js @@ -23,6 +23,12 @@ this.spies = undefined; }; +VisionSharing.prototype.Update = function(newTemplate) { + this.template = newTemplate; + if (this.activated) + this.CheckVisionSharings(); +}; + /** * As entities have not necessarily the VisionSharing component, it has to be activated * before use so that the rangeManager can register it Index: binaries/data/mods/public/simulation/components/interfaces/Identity.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/Identity.js @@ -0,0 +1,5 @@ +/** + * Message of the form { "entity": entity, "from": oldClasses, "to": newClasses } + * sent from Identity component whenever identity classes changes. + */ +Engine.RegisterMessageType("IdentityClassesChanged"); Index: binaries/data/mods/public/simulation/components/interfaces/Messages.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/Messages.js +++ binaries/data/mods/public/simulation/components/interfaces/Messages.js @@ -11,6 +11,12 @@ */ Engine.RegisterMessageType("EntityRenamed"); +/** + * Message of the form { "entity": number, "from": old, "to": new } + * sent when an entity changes template. + */ +Engine.RegisterMessageType("TemplateChanged"); + /** * Message of the form {} * sent from InitGame for component map-dependent initialization. Index: binaries/data/mods/public/simulation/helpers/Transform.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Transform.js +++ binaries/data/mods/public/simulation/helpers/Transform.js @@ -3,7 +3,14 @@ // returns the ID of the new entity or INVALID_ENTITY. function ChangeEntityTemplate(oldEnt, newTemplate) { - // Done un/packing, copy our parameters to the final entity + const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + const oldTemplate = cmpTemplateManager.GetCurrentTemplateName(oldEnt) + + Engine.UpdateEntityTemplate(newTemplate, oldEnt); + Engine.PostMessage(oldEnt, MT_TemplateChanged, { "entity": oldEnt, "from": oldTemplate, "to": newTemplate }); + + return oldEnt; + var newEnt = Engine.AddEntity(newTemplate); if (newEnt == INVALID_ENTITY) { Index: binaries/data/mods/public/simulation/templates/special/players/maur.xml =================================================================== --- binaries/data/mods/public/simulation/templates/special/players/maur.xml +++ binaries/data/mods/public/simulation/templates/special/players/maur.xml @@ -3,6 +3,14 @@ teambonuses/maur_player_teambonus + + + + Cavalry + + + + maur Mauryas Index: binaries/data/mods/public/simulation/templates/structures/maur/house.xml =================================================================== --- binaries/data/mods/public/simulation/templates/structures/maur/house.xml +++ binaries/data/mods/public/simulation/templates/structures/maur/house.xml @@ -10,6 +10,10 @@ + + structures/maur/outpost + 1 + structures/mauryas/house.xml Index: binaries/data/mods/public/simulation/templates/structures/maur/outpost.xml =================================================================== --- binaries/data/mods/public/simulation/templates/structures/maur/outpost.xml +++ binaries/data/mods/public/simulation/templates/structures/maur/outpost.xml @@ -4,6 +4,10 @@ maur Uparaksana + + structures/maur/house + 1 + structures/mauryas/outpost.xml Index: binaries/data/mods/public/simulation/templates/units/maur/cavalry_javelineer_a.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur/cavalry_javelineer_a.xml +++ binaries/data/mods/public/simulation/templates/units/maur/cavalry_javelineer_a.xml @@ -4,7 +4,7 @@ Advanced - units/maur/cavalry_javelineer_e + units/maur/infantry_archer_e units/mauryas/cavalry_javelinist_a_m.xml Index: binaries/data/mods/public/simulation/templates/units/maur/infantry_archer_b.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur/infantry_archer_b.xml +++ binaries/data/mods/public/simulation/templates/units/maur/infantry_archer_b.xml @@ -14,7 +14,7 @@ units/maur/infantry_archer.png - units/maur/infantry_archer_a + units/maur/cavalry_javelineer_a units/mauryas/infantry_archer_b.xml Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -301,7 +301,9 @@ CVector3D pos1; }; -/*Sent whenever the territory type (neutral,own,enemy) differs from the former type*/ +/** + * Sent whenever the territory type (neutral, own, enemy) under an entity differs from the former type + */ class CMessageTerritoryPositionChanged final : public CMessage { public: @@ -316,6 +318,25 @@ player_id_t newTerritory; }; + +/** + * Sent whenever the territory influence changes. + */ +class CMessageTerritoryInfluenceChanged final : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(TerritoryInfluenceChanged) + + CMessageTerritoryInfluenceChanged(entity_id_t entity) : + entity(entity) + { + } + + entity_id_t entity; +}; + + + /** * Sent by CCmpUnitMotion during Update if an event happened that might interest other components. */ @@ -491,7 +512,7 @@ }; /** - * Sent by aura manager when a value of a certain entity's component is changed + * Sent by techs / aura / ... when the value of a certain modified has changed. */ class CMessageValueModification final : public CMessage { Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -45,6 +45,7 @@ MESSAGE(PositionChanged) MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) +MESSAGE(TerritoryInfluenceChanged) MESSAGE(MotionUpdate) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) @@ -184,12 +185,13 @@ INTERFACE(TerritoryDecayManager) COMPONENT(TerritoryDecayManagerScripted) -INTERFACE(TerritoryInfluence) -COMPONENT(TerritoryInfluence) - INTERFACE(TerritoryManager) COMPONENT(TerritoryManager) +// Must come after the manager +INTERFACE(TerritoryInfluence) +COMPONENT(TerritoryInfluence) + INTERFACE(TurretHolder) COMPONENT(TurretHolderScripted) Index: source/simulation2/components/CCmpDecay.cpp =================================================================== --- source/simulation2/components/CCmpDecay.cpp +++ source/simulation2/components/CCmpDecay.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -115,6 +115,15 @@ { } + void Update(const CParamNode& paramNode) override + { + m_Active = paramNode.GetChild("Active").ToBool(); + m_ShipSink = paramNode.GetChild("SinkingAnim").ToBool(); + m_DelayTime = paramNode.GetChild("DelayTime").ToFixed().ToFloat(); + m_SinkRate = paramNode.GetChild("SinkRate").ToFixed().ToFloat(); + m_SinkAccel = paramNode.GetChild("SinkAccel").ToFixed().ToFloat(); + } + void Serialize(ISerializer& UNUSED(serialize)) override { // This component isn't network-synchronised, so don't serialize anything Index: source/simulation2/components/CCmpFootprint.cpp =================================================================== --- source/simulation2/components/CCmpFootprint.cpp +++ source/simulation2/components/CCmpFootprint.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -125,6 +125,11 @@ { } + void Update(const CParamNode& paramNode) override + { + Init(paramNode); + } + void Serialize(ISerializer& UNUSED(serialize)) override { // No dynamic state to serialize Index: source/simulation2/components/CCmpMinimap.cpp =================================================================== --- source/simulation2/components/CCmpMinimap.cpp +++ source/simulation2/components/CCmpMinimap.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -140,6 +140,36 @@ { } + void Update(const CParamNode& paramNode) override + { + const CParamNode& color = paramNode.GetChild("Color"); + if (color.IsOk()) + { + m_UsePlayerColor = false; + m_R = (u8)color.GetChild("@r").ToInt(); + m_G = (u8)color.GetChild("@g").ToInt(); + m_B = (u8)color.GetChild("@b").ToInt(); + } + else + { + m_UsePlayerColor = true; + UpdateColor(); + } + + const CParamNode& iconNode = paramNode.GetChild("Icon"); + if (iconNode.IsOk()) + { + const CParamNode& iconSizeNode = iconNode.GetChild("@size"); + if (iconSizeNode.IsOk()) + { + m_HasIcon = true; + m_IconPath = "art/textures/ui/session/icons/minimap/" + iconNode.ToString(); + m_IconSize = iconSizeNode.ToFloat(); + } + } else + m_HasIcon = false; + } + template void SerializeCommon(S& serialize) { Index: source/simulation2/components/CCmpObstruction.cpp =================================================================== --- source/simulation2/components/CCmpObstruction.cpp +++ source/simulation2/components/CCmpObstruction.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -275,6 +275,29 @@ void Deinit() override { + m_IsDestroyed = true; + } + + void Update(const CParamNode& paramNode) override + { + bool active = m_Active; + bool moving = m_Moving; + entity_id_t cg1 = m_ControlGroup; + entity_id_t cg2 = m_ControlGroup2; + + bool block_movement = m_Flags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT; + bool block_pathfinding = m_Flags & ICmpObstructionManager::FLAG_BLOCK_PATHFINDING; + + SetActive(false); + + Init(paramNode); + m_Active = false; + + m_Moving = moving; + m_ControlGroup = cg1; + m_ControlGroup2 = cg2; + + SetActive(active); } template Index: source/simulation2/components/CCmpOwnership.cpp =================================================================== --- source/simulation2/components/CCmpOwnership.cpp +++ source/simulation2/components/CCmpOwnership.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -54,6 +54,10 @@ { } + void Update(const CParamNode& UNUSED(paramNode)) override + { + } + void Serialize(ISerializer& serialize) override { serialize.NumberI32_Unbounded("owner", m_Owner); Index: source/simulation2/components/CCmpPosition.cpp =================================================================== --- source/simulation2/components/CCmpPosition.cpp +++ source/simulation2/components/CCmpPosition.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -175,6 +175,29 @@ { } + void Update(const CParamNode& paramNode) override + { + const std::string& anchor = paramNode.GetChild("Anchor").ToString(); + if (anchor == "pitch") + m_AnchorType = PITCH; + else if (anchor == "pitch-roll") + m_AnchorType = PITCH_ROLL; + else if (anchor == "roll") + m_AnchorType = ROLL; + else + m_AnchorType = UPRIGHT; + + if (m_RelativeToGround) + m_Y = paramNode.GetChild("Altitude").ToFixed(); + + m_Floating = paramNode.GetChild("Floating").ToBool(); + m_FloatDepth = paramNode.GetChild("FloatDepth").ToFixed(); + + m_RotYSpeed = paramNode.GetChild("TurnRate").ToFixed(); + + AdvertiseInterpolatedPositionChanges(); + } + void Serialize(ISerializer& serialize) override { serialize.Bool("in world", m_InWorld); @@ -191,7 +214,6 @@ serialize.NumberFixed_Unbounded("rot x", m_RotX); serialize.NumberFixed_Unbounded("rot y", m_RotY); serialize.NumberFixed_Unbounded("rot z", m_RotZ); - serialize.NumberFixed_Unbounded("rot y speed", m_RotYSpeed); serialize.NumberFixed_Unbounded("altitude", m_Y); serialize.Bool("relative", m_RelativeToGround); serialize.Bool("floating", m_Floating); @@ -250,7 +272,6 @@ deserialize.NumberFixed_Unbounded("rot x", m_RotX); deserialize.NumberFixed_Unbounded("rot y", m_RotY); deserialize.NumberFixed_Unbounded("rot z", m_RotZ); - deserialize.NumberFixed_Unbounded("rot y speed", m_RotYSpeed); deserialize.NumberFixed_Unbounded("altitude", m_Y); deserialize.Bool("relative", m_RelativeToGround); deserialize.Bool("floating", m_Floating); Index: source/simulation2/components/CCmpTerritoryInfluence.cpp =================================================================== --- source/simulation2/components/CCmpTerritoryInfluence.cpp +++ source/simulation2/components/CCmpTerritoryInfluence.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -20,6 +20,7 @@ #include "simulation2/system/Component.h" #include "ICmpTerritoryInfluence.h" +#include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpValueModificationManager.h" @@ -27,8 +28,10 @@ class CCmpTerritoryInfluence final : public ICmpTerritoryInfluence { public: - static void ClassInit(CComponentManager& UNUSED(componentManager)) + static void ClassInit(CComponentManager& componentManager) { + componentManager.SubscribeToMessageType(MT_OwnershipChanged); + componentManager.SubscribeToMessageType(MT_ValueModification); } DEFAULT_COMPONENT_ALLOCATOR(TerritoryInfluence) @@ -62,6 +65,14 @@ void Deinit() override { + OnChanged(); + } + + void Update(const CParamNode& paramNode) override + { + // TODO: only update on change + Init(paramNode); + OnChanged(); } void Serialize(ISerializer& UNUSED(serialize)) override @@ -73,6 +84,34 @@ Init(paramNode); } + void HandleMessage(const CMessage& msg, bool UNUSED(global)) override + { + switch (msg.GetType()) + { + case MT_OwnershipChanged: + { + const CMessageOwnershipChanged& msgData = static_cast (msg); + // handled in DeInit(); + if (msgData.to != INVALID_PLAYER) + OnChanged(); + break; + } + case MT_ValueModification: + { + const CMessageValueModification& msgData = static_cast (msg); + if (msgData.component == L"TerritoryInfluence") + OnChanged(); + break; + } + } + } + + void OnChanged() const + { + CMessageTerritoryInfluenceChanged msg(GetEntityId()); + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } + bool IsRoot() const override { CmpPtr cmpValueModificationManager(GetSystemEntity()); Index: source/simulation2/components/CCmpTerritoryManager.cpp =================================================================== --- source/simulation2/components/CCmpTerritoryManager.cpp +++ source/simulation2/components/CCmpTerritoryManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -60,10 +60,10 @@ public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); + componentManager.SubscribeGloballyToMessageType(MT_PlayerColorChanged); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); - componentManager.SubscribeGloballyToMessageType(MT_ValueModification); + componentManager.SubscribeGloballyToMessageType(MT_TerritoryInfluenceChanged); componentManager.SubscribeToMessageType(MT_ObstructionMapShapeChanged); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_WaterChanged); @@ -171,12 +171,6 @@ { switch (msg.GetType()) { - case MT_OwnershipChanged: - { - const CMessageOwnershipChanged& msgData = static_cast (msg); - MakeDirtyIfRelevantEntity(msgData.entity); - break; - } case MT_PlayerColorChanged: { MakeDirty(); @@ -188,11 +182,9 @@ MakeDirtyIfRelevantEntity(msgData.entity); break; } - case MT_ValueModification: + case MT_TerritoryInfluenceChanged: { - const CMessageValueModification& msgData = static_cast (msg); - if (msgData.component == L"TerritoryInfluence") - MakeDirty(); + MakeDirty(); break; } case MT_ObstructionMapShapeChanged: Index: source/simulation2/components/CCmpUnitMotion.h =================================================================== --- source/simulation2/components/CCmpUnitMotion.h +++ source/simulation2/components/CCmpUnitMotion.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -316,6 +316,32 @@ { } + void Update(const CParamNode& paramNode) override + { + m_IsFormationController = paramNode.GetChild("FormationController").ToBool(); + m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); + + if (paramNode.GetChild("RunMultiplier").IsOk()) + m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed(); + else + m_TemplateRunMultiplier = fixed::FromInt(1); + + m_InstantTurnAngle = paramNode.GetChild("InstantTurnAngle").ToFixed(); + m_TemplateAcceleration = paramNode.GetChild("Acceleration").ToFixed(); + m_TemplateWeight = paramNode.GetChild("Weight").ToFixed(); + + m_PassClassName = paramNode.GetChild("PassabilityClass").ToString(); + SetPassabilityData(m_PassClassName); + + CmpPtr cmpObstruction(GetEntityHandle()); + if (cmpObstruction) + m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false); + + SetParticipateInPushing(!paramNode.GetChild("DisablePushing").IsOk() || !paramNode.GetChild("DisablePushing").ToBool()); + + OnValueModification(); + } + template void SerializeCommon(S& serialize) { Index: source/simulation2/components/CCmpVision.cpp =================================================================== --- source/simulation2/components/CCmpVision.cpp +++ source/simulation2/components/CCmpVision.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -72,6 +72,17 @@ { } + void Update(const CParamNode& paramNode) override + { + m_BaseRange = paramNode.GetChild("Range").ToFixed(); + + if (paramNode.GetChild("RevealShore").IsOk() && !m_RevealShore) + // Need to not be lazy and send a message + LOGWARNING("Updating to an entity that reveals the shore is unsupported"); + + ReloadRange(); + } + void Serialize(ISerializer& UNUSED(serialize)) override { // No dynamic state to serialize Index: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ source/simulation2/components/CCmpVisualActor.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -229,6 +229,26 @@ } } + void Update(const CParamNode& paramNode) override + { + m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); + m_IsFoundationActor = paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk(); + + m_BaseActorName = paramNode.GetChild(m_IsFoundationActor ? "FoundationActor" : "Actor").ToWString(); + // Don't parse, we recompute down below + + m_VisibleInAtlasOnly = paramNode.GetChild("VisibleInAtlasOnly").ToBool(); + m_IsActorOnly = paramNode.GetChild("ActorOnly").IsOk(); + + m_SilhouetteDisplay = paramNode.GetChild("SilhouetteDisplay").ToBool(); + m_SilhouetteOccluder = paramNode.GetChild("SilhouetteOccluder").ToBool(); + m_DisableShadows = paramNode.GetChild("DisableShadows").ToBool(); + + InitSelectionShapeDescriptor(paramNode); + + RecomputeActorName(); + } + template void SerializeCommon(S& serialize) { @@ -656,6 +676,7 @@ void CCmpVisualActor::InitSelectionShapeDescriptor(const CParamNode& paramNode) { // by default, we don't need a custom selection shape and we can just keep the default behaviour + // We don't need to delete the descriptor here, because the CModelAbstract holds it. m_ShapeDescriptor = nullptr; const CParamNode& shapeNode = paramNode.GetChild("SelectionShape"); Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -265,6 +265,22 @@ //////////////////////////////// +JS::Value CMessageTerritoryInfluenceChanged::ToJSVal(const ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(entity); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageTerritoryInfluenceChanged::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(entity_id_t, entity); + return new CMessageTerritoryInfluenceChanged(entity); +} + +//////////////////////////////// + const std::array CMessageMotionUpdate::UpdateTypeStr = { { "likelySuccess", "likelyFailure", "obstructed", "veryObstructed" } }; Index: source/simulation2/scripting/ScriptComponent.h =================================================================== --- source/simulation2/scripting/ScriptComponent.h +++ source/simulation2/scripting/ScriptComponent.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -41,6 +41,7 @@ void Init(const CParamNode& paramNode, entity_id_t ent); void Deinit(); + void Update(const CParamNode& paramNode); void HandleMessage(const CMessage& msg, bool global); void Serialize(ISerializer& serialize); @@ -110,6 +111,10 @@ { \ m_Script.Deinit(); \ } \ + void Update(const CParamNode& paramNode) override \ + { \ + m_Script.Update(paramNode); \ + } \ void HandleMessage(const CMessage& msg, bool global) override \ { \ m_Script.HandleMessage(msg, global); \ Index: source/simulation2/scripting/ScriptComponent.cpp =================================================================== --- source/simulation2/scripting/ScriptComponent.cpp +++ source/simulation2/scripting/ScriptComponent.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -36,7 +36,7 @@ { ScriptRequest rq(m_ScriptInterface); Script::SetProperty(rq, m_Instance, "entity", (int)ent, true, false); - Script::SetProperty(rq, m_Instance, "template", paramNode, true, false); + Script::SetProperty(rq, m_Instance, "template", paramNode, false, false); ScriptFunction::CallVoid(rq, m_Instance, "Init"); } @@ -46,6 +46,12 @@ ScriptFunction::CallVoid(rq, m_Instance, "Deinit"); } +void CComponentTypeScript::Update(const CParamNode& paramNode) { + ScriptRequest rq(m_ScriptInterface); + if (!ScriptFunction::CallVoid(rq, m_Instance, "Update", paramNode)) + Script::SetProperty(rq, m_Instance, "template", paramNode, false, false); +} + void CComponentTypeScript::HandleMessage(const CMessage& msg, bool global) { ScriptRequest rq(m_ScriptInterface); Index: source/simulation2/system/Component.h =================================================================== --- source/simulation2/system/Component.h +++ source/simulation2/system/Component.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -55,6 +55,9 @@ void Deinit() override \ { \ } \ + void Update(const CParamNode& paramNode) override \ + { \ + } \ void Serialize(ISerializer& UNUSED(serialize)) override \ { \ } \ Index: source/simulation2/system/ComponentManager.h =================================================================== --- source/simulation2/system/ComponentManager.h +++ source/simulation2/system/ComponentManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -213,6 +213,8 @@ */ entity_id_t AddEntity(const std::wstring& templateName, entity_id_t ent); + bool UpdateEntityTemplate(const std::wstring& templateName, entity_id_t ent); + /** * 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. @@ -286,6 +288,7 @@ void Script_BroadcastMessage(int mtid, JS::HandleValue data); int Script_AddEntity(const std::wstring& templateName); int Script_AddLocalEntity(const std::wstring& templateName); + bool Script_UpdateEntityTemplate(const std::wstring& templateName, entity_id_t ent); const CParamNode& Script_GetTemplate(const std::string& templateName); CMessage* ConstructMessage(int mtid, JS::HandleValue data); @@ -309,6 +312,7 @@ // TODO: some of these should be vectors std::map m_ComponentTypesById; std::vector m_ScriptedSystemComponents; + // TODO: the two containers below must guarantee stability when inserting for now, @see ConstructComponent std::vector > m_ComponentsByInterface; // indexed by InterfaceId std::map > m_ComponentsByTypeId; std::map > m_LocalMessageSubscriptions; Index: source/simulation2/system/ComponentManager.cpp =================================================================== --- source/simulation2/system/ComponentManager.cpp +++ source/simulation2/system/ComponentManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -85,6 +85,7 @@ ScriptFunction::Register<&CComponentManager::Script_BroadcastMessage, Getter>(rq, "BroadcastMessage"); ScriptFunction::Register<&CComponentManager::Script_AddEntity, Getter>(rq, "AddEntity"); ScriptFunction::Register<&CComponentManager::Script_AddLocalEntity, Getter>(rq, "AddLocalEntity"); + ScriptFunction::Register<&CComponentManager::Script_UpdateEntityTemplate, Getter>(rq, "UpdateEntityTemplate"); ScriptFunction::Register<&CComponentManager::QueryInterface, Getter>(rq, "QueryInterface"); ScriptFunction::Register<&CComponentManager::DestroyComponentsSoon, Getter>(rq, "DestroyEntity"); ScriptFunction::Register<&CComponentManager::FlushDestroyedComponents, Getter>(rq, "FlushDestroyedEntities"); @@ -475,6 +476,13 @@ return AddEntity(templateName, AllocateNewLocalEntity()); } +bool CComponentManager::Script_UpdateEntityTemplate(const std::wstring& templateName, entity_id_t ent) +{ + // TODO: should validate the string to make sure it doesn't contain scary characters + // that will let it access non-component-template files + return UpdateEntityTemplate(templateName, ent); +} + void CComponentManager::ResetState() { // Delete all dynamic message subscriptions @@ -488,7 +496,8 @@ std::map::iterator eit = iit->second.begin(); for (; eit != iit->second.end(); ++eit) { - eit->second->Deinit(); + if (eit->second) + eit->second->Deinit(); m_ComponentTypesById[iit->first].dealloc(eit->second); } } @@ -729,7 +738,7 @@ ENSURE((size_t)ct.iid < m_ComponentsByInterface.size()); std::unordered_map& emap1 = m_ComponentsByInterface[ct.iid]; - if (emap1.find(ent.GetId()) != emap1.end()) + if (emap1.find(ent.GetId()) != emap1.end() && emap1.find(ent.GetId())->second) { LOGERROR("Multiple components for interface %d", ct.iid); return NULL; @@ -758,13 +767,11 @@ component->SetSimContext(m_SimContext); // Store a reference to the new component - emap1.insert(std::make_pair(ent.GetId(), component)); - emap2.insert(std::make_pair(ent.GetId(), 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.) + // NB: this requires that insertion does not invalidate iterators, as we might create components + // while iterating over all components. + // TODO: figure out if we want some kind of delayed addition like we have for destruction. + emap1.insert_or_assign(ent.GetId(), component); + emap2.insert_or_assign(ent.GetId(), component); SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && ct.iid < (int)cache->numInterfaces && cache->interfaces[ct.iid] == NULL); @@ -871,6 +878,103 @@ return ent; } +bool CComponentManager::UpdateEntityTemplate(const std::wstring& templateName, entity_id_t ent) +{ + LOGWARNING("Update template : %s, %i", utf8_from_wstring(templateName), ent); + ICmpTemplateManager *cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); + if (!cmpTemplateManager) + { + debug_warn(L"No ICmpTemplateManager loaded"); + return false; + } + + // Don't use loadTemplate, that updates the cache + const CParamNode* newTemplate = cmpTemplateManager->GetTemplate(utf8_from_wstring(templateName)); + if (!newTemplate) + return false; // LoadTemplate will have reported the error + + CEntityHandle handle = LookupEntityHandle(ent, false); + if (handle.GetComponentCache() == nullptr) + { + LOGERROR("Entity %i does not exist", ent); + return false; + } + + const CParamNode* oldTemplate = cmpTemplateManager->GetTemplate(cmpTemplateManager->GetCurrentTemplateName(ent)); + + const CParamNode::ChildrenMap& tmplChilds = newTemplate->GetChildren(); + + // Now pass through the new template and update / create new components + for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) + { + // Ignore attributes on the root element + if (it->first.length() && it->first[0] == '@') + continue; + + CComponentManager::ComponentTypeId cid = LookupCID(it->first); + if (cid == CID__Invalid) + { + LOGERROR("Unrecognized component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); + return false; + } + IComponent* component = handle.GetComponentCache()->interfaces[m_ComponentTypesById[cid].iid]; + if (!component) + { + if (!AddComponent(handle, cid, it->second)) + { + LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); + return false; + } + // TODO: make AddComponent return the component bruh. + component = handle.GetComponentCache()->interfaces[m_ComponentTypesById[cid].iid]; + } + component->Update(it->second); + } + + // Finally pass through existing templates and delete all outstanding components. + // This is the really tricky/hacky bit. + for (CParamNode::ChildrenMap::const_iterator it = oldTemplate->GetChildren().begin(); it != oldTemplate->GetChildren().end(); ++it) { + // Ignore attributes on the root element + if (it->first.length() && it->first[0] == '@') + continue; + CParamNode::ChildrenMap::const_iterator itOld = tmplChilds.find(it->first); + if (itOld != tmplChilds.end()) + continue; + + CComponentManager::ComponentTypeId cid = LookupCID(it->first); + std::map >::iterator iit = m_ComponentsByTypeId.find(cid); + if (iit == m_ComponentsByTypeId.end()) + { + LOGWARNING("Did not find expected component when updating template - %s", it->first); + continue; + } + std::map::iterator eit = iit->second.find(ent); + if (eit == iit->second.end()) + { + LOGWARNING("Did not find expected component when updating template - %s", it->first); + continue; + } + + // Make it inaccessible from outside + handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = nullptr; + m_ComponentsByInterface[m_ComponentTypesById[iit->first].iid][ent] = nullptr; + + // TODO -> should we de-init sooner / later ? + eit->second->Deinit(); + RemoveComponentDynamicSubscriptions(eit->second); + + m_ComponentTypesById[iit->first].dealloc(eit->second); + + // TODO: it would be nice to clean up the null pointers before the entity gets destroyed. + eit->second = nullptr; + } + + // Update the cache + cmpTemplateManager->LoadTemplate(ent, utf8_from_wstring(templateName)); + + return true; +} + bool CComponentManager::EntityExists(entity_id_t ent) const { return m_ComponentCaches.find(ent) != m_ComponentCaches.end(); @@ -917,9 +1021,11 @@ std::map::iterator eit = iit->second.find(ent); if (eit != iit->second.end()) { - eit->second->Deinit(); - RemoveComponentDynamicSubscriptions(eit->second); - m_ComponentTypesById[iit->first].dealloc(eit->second); + if (eit->second) { + eit->second->Deinit(); + RemoveComponentDynamicSubscriptions(eit->second); + m_ComponentTypesById[iit->first].dealloc(eit->second); + } iit->second.erase(ent); handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = NULL; } @@ -970,7 +1076,8 @@ std::unordered_map::const_iterator it = m_ComponentsByInterface[iid].begin(); for (; it != m_ComponentsByInterface[iid].end(); ++it) - ret.push_back(*it); + if (it->second) + ret.push_back(*it); std::sort(ret.begin(), ret.end()); // lexicographic pair comparison means this'll sort by entity ID @@ -1008,7 +1115,7 @@ // Send the message to all of them std::map::const_iterator eit = emap->second.find(ent); - if (eit != emap->second.end()) + if (eit != emap->second.end() && eit->second) eit->second->HandleMessage(msg, false); } } @@ -1034,7 +1141,8 @@ // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) - eit->second->HandleMessage(msg, false); + if (eit->second) + eit->second->HandleMessage(msg, false); } } @@ -1073,7 +1181,8 @@ // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) - eit->second->HandleMessage(msg, true); + if (eit->second) + eit->second->HandleMessage(msg, true); } } Index: source/simulation2/system/IComponent.h =================================================================== --- source/simulation2/system/IComponent.h +++ source/simulation2/system/IComponent.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -46,6 +46,8 @@ virtual void Init(const CParamNode& paramNode) = 0; virtual void Deinit() = 0; + virtual void Update(const CParamNode& paramNode); + virtual void HandleMessage(const CMessage& msg, bool global); CEntityHandle GetEntityHandle() const { return m_EntityHandle; } Index: source/simulation2/system/IComponent.cpp =================================================================== --- source/simulation2/system/IComponent.cpp +++ source/simulation2/system/IComponent.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -56,3 +56,8 @@ { return JS::NullValue(); } + +void IComponent::Update(const CParamNode& paramNode) { + LOGWARNING("Warn - Update for %s not implemented yet", GetSimContext().GetComponentManager().LookupComponentTypeName(GetComponentTypeId())); + // TODO: remove this, this is just a placeholder for compilation. +}