• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Crash] Metamorphosis in spell book

Status
Not open for further replies.
Level 21
Joined
Mar 27, 2012
Messages
3,232
Adding the ability permanently seems to be the best way. It's actually possible for spells in spellbooks to survive morphs, but only if the alternate unit has a different spellbook. The reason seems to be that the game doesn't re-add the spellbook ability upon morphing and thus, forgets to add the abilities in it.
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
Now I don't want the other unit to have a spell book as well since there is no reason for it or is there any way to hide it?

I tried to stop the unit and to remove the abilities, then to add the ability permanently NOT in the spell book and to cast it. The game does still crash and I have no idea why:
JASS:
call DisableTrigger(this.m_channelTrigger)
			call IssueImmediateOrder(this.character().unit(), "stop") // stop spell immediately
			debug call Print("Start for spell: " + GetObjectName(this.ability()))
			// morph
			if (not Character(this.character()).isMorphed()) then
				debug call Print("Is not morphed")
				if (this.canMorph.evaluate()) then
					debug call Print("Can morph")
					
					/*
					 * Now store everything before casting the ability.
					 */
					if (Character(this.character()).morph(this.ability())) then
						debug if (GetUnitTypeId(this.character().unit()) == this.unitTypeId()) then
							debug call Print("Error: Already morphed!")
						debug endif
						debug call Print("Morphed successfully for spell: " + GetObjectName(this.ability()))
						
						// prevent crashes when the morphing ability is removed for example by removing the casting item or grimoire abilities
						// do this before disabling inventory and removing the items
						/*
						*  The ability is removed then made permanent and casted again that it will not be losed by the metamorphosis.
						*/
						// make sure that the ability is not in the spell book
						debug call Print("Removing ability " + GetAbilityName(this.ability()))
						set result = UnitRemoveAbility(this.character().unit(), this.ability())
						debug if (result) then
						debug call Print("Successfully removed: " + GetAbilityName(this.ability()))
						debug else
						debug call Print("Unable to remove: " + GetAbilityName(this.ability()))
						debug endif
						debug call Print("Removing favorite ability " + GetAbilityName(this.favoriteAbility()))
						set result = UnitRemoveAbility(this.character().unit(), this.favoriteAbility())
						debug if (result) then
						debug call Print("Successfully removed favorite: " + GetAbilityName(this.favoriteAbility()))
						debug else
						debug call Print("Unable to remove favorite: " + GetAbilityName(this.favoriteAbility()))
						debug endif
						debug call Print("Adding morph ability to prevent crash: " + GetAbilityName(this.ability()))
						
						call Character(this.character()).grimoire().removeAllSpellsFromUnit()
						
						set result = UnitAddAbility(this.character().unit(), this.ability())
						debug if (result) then
						debug call Print("Successfully added: " + GetAbilityName(this.ability()))
						debug else
						debug call Print("Unable to add: " + GetAbilityName(this.ability()))
						debug endif
						set result = UnitMakeAbilityPermanent(this.character().unit(), true, this.ability())
						debug if (result) then
						debug call Print("Successfully made permanent: " + GetAbilityName(this.ability()))
						debug else
						debug call Print("Unable to make permanent: " + GetAbilityName(this.ability()))
						debug endif
			
						/*
						 * Use the corresponding order string.
						 */
						// TODO Crashes game!
						if (IssueImmediateOrder(this.character().unit(), this.orderString())) then
							debug call Print("Successful morph with spell: " + GetAbilityName(this.ability()))
							// morph spells are expected to morph immediately
							//call this.onMorph.execute()
						else
							debug call Print("Error on calling order " + this.orderString())
						endif
					else
						debug call Print("Error on morphing.")
					endif
					
					// sleep before enabling again otherwise the event will be raised again after morphing -> endless loop
					call TriggerSleepAction(100.0) // TODO wait spell duration + 0.10
					debug call Print("Enabling trigger")
					
				debug else
					debug call Print("Cannot morph for spell: " + GetObjectName(this.ability()))
				endif
			// restore
			else
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
Hey,
my current solution works for the first time the unit morphs and unmorphs.
The second time it morphs the game does crash:
JASS:
/// Metamorphosis
library StructSpellsSpellMetamorphosis requires Asl, StructGameCharacter, StructGameGrimoire

	/**
	 * \brief Generic abstract spell for metamorphosis.
	 * 
	 * Uses \ref EVENT_UNIT_SPELL_CHANNEL to be executed before the unit is morphed to successfully store the inventory items.
	 * 
	 * Besides it uses \ref EVENT_UNIT_HERO_REVIVE_FINISH to unmorph the character when being revived unmorphed.
	 * 
	 * \note All morph spells need a short delay in which \ref Character#morph() can be called which stores spells and items safely!
	 */
	struct SpellMetamorphosis
		private Character m_character
		private integer m_ability
		private integer m_favoriteAbility
		private integer m_unitTypeId
		private string m_orderString
		private string m_unorderString
		private trigger m_channelTrigger
		private trigger m_revivalTrigger
		private boolean m_isMorphed
		
		public method character takes nothing returns Character
			return this.m_character
		endmethod
		
		public method ability takes nothing returns integer
			return this.m_ability
		endmethod
		
		public method setFavoriteAbility takes integer favoriteAbility returns nothing
			set this.m_favoriteAbility = favoriteAbility
		endmethod
		
		public method favoriteAbility takes nothing returns integer
			return this.m_favoriteAbility
		endmethod
		
		public method setUnitTypeId takes integer unitTypeId returns nothing
			set this.m_unitTypeId = unitTypeId
		endmethod
		
		public method unitTypeId takes nothing returns integer
			return this.m_unitTypeId
		endmethod
		
		public method setOrderString takes string orderString returns nothing
			set this.m_orderString = orderString
		endmethod
		
		public method orderString takes nothing returns string
			return this.m_orderString
		endmethod
		
		public method setUnorderString takes string unorderString returns nothing
			set this.m_unorderString = unorderString
		endmethod
		
		public method unorderString takes nothing returns string
			return this.m_unorderString
		endmethod
		
		public method isMorphed takes nothing returns boolean
			return this.m_isMorphed
		endmethod
		
		/*
		 * Overwrite this method to allow the unit to morph only on specific conditions.
		 * If it returns false the unit is stopped immediately when casting the morph ability.
		 * Called with .evaluate()
		 */
		public stub method canMorph takes nothing returns boolean
			return true
		endmethod
		
		/// Called after unit has morphed.
		public stub method onMorph takes nothing returns nothing
		endmethod
		
		// Called with .evaluate()
		public stub method canRestore takes nothing returns boolean
			return true
		endmethod
		
		/// Called after unit has been restored.
		public stub method onRestore takes nothing returns nothing
		endmethod
		
		public static method waitForRestoration takes unit whichUnit, integer unitTypeId returns nothing
			loop
				debug call Print("Checking if unit is no more: " + GetObjectName(unitTypeId) + ": " + I2S(unitTypeId))
				exitwhen (GetUnitTypeId(whichUnit) != unitTypeId)
				call TriggerSleepAction(1.0)
			endloop
		endmethod
		
		public static method waitForMorph takes unit whichUnit, integer unitTypeId returns nothing
			loop
				exitwhen (GetUnitTypeId(whichUnit) == unitTypeId)
				call TriggerSleepAction(1.0)
			endloop
		endmethod
		
		private static method triggerConditionStart takes nothing returns boolean
			local thistype this = AHashTable.global().handleInteger(GetTriggeringTrigger(), "this")
			local boolean result = GetSpellAbilityId() != null and GetSpellAbilityId() == this.ability() and GetTriggerUnit() == this.character().unit()
			debug call Print("Condition start for spell: " + GetAbilityName(this.ability()) + " with casted spell " + GetAbilityName(GetSpellAbilityId()) + " and caster " + GetUnitName(GetTriggerUnit()))
			debug if (GetSpellAbilityId() == null) then
			debug call Print("Spell is null")
			debug endif
			debug if (result) then
			debug call Print("Success")
			debug else
			debug call Print("Fail")
			debug endif
			return result
		endmethod
		
		private static method triggerActionStart takes nothing returns nothing
			local thistype this = AHashTable.global().handleInteger(GetTriggeringTrigger(), "this")
			local boolean result = false
			/*
			 * Disable trigger to make sure that it does not react on manually issued order in this trigger.
			 * Otherwise it would result in an endless loop.
			 */
			call DisableTrigger(this.m_channelTrigger)
			call IssueImmediateOrder(this.character().unit(), "stop") // stop spell immediately
			debug call Print("Start for spell: " + GetObjectName(this.ability()))
			
			// morph
			if (not Character(this.character()).isMorphed()) then
				debug call Print("Is not morphed")
				if (this.canMorph.evaluate()) then
					debug call Print("Can morph")
					
					/*
					 * Now store everything before casting the ability.
					 */
					if (Character(this.character()).morph(this.ability())) then
						debug if (GetUnitTypeId(this.character().unit()) == this.unitTypeId()) then
							debug call Print("Error: Already morphed!")
						debug endif
						debug call Print("Morphed successfully for spell: " + GetObjectName(this.ability()))
						
						// wait until all triggers have been run which have spell events to avoid any null abilities
						// without this call the game crashes since other triggers are called based on an already removed ability
						call TriggerSleepAction(0.0)
						
						/*
						 *  The ability is removed then made permanent and casted again that it will not be losed by the metamorphosis.	
						 * Removing all grimoire abilities including the ability itself is only done for safety to make sure that no grimoire
						 * ability is being cast which is in a spell book.
						 */
						call Character(this.character()).grimoire().removeAllSpellsFromUnit()
						
						/**
						 * Now add a permanent "dummy" ability which is neither in a spell book nor belongs to any item.
						 */
						set result = UnitAddAbility(this.character().unit(), this.ability())
						debug if (result) then
						debug call Print("Successfully added: " + GetAbilityName(this.ability()))
						debug else
						debug call Print("Unable to add: " + GetAbilityName(this.ability()))
						debug endif
						set result = UnitMakeAbilityPermanent(this.character().unit(), true, this.ability())
						debug if (result) then
						debug call Print("Successfully made permanent: " + GetAbilityName(this.ability()))
						debug else
						debug call Print("Unable to make permanent: " + GetAbilityName(this.ability()))
						debug endif
						
						debug call Print("Casting dummy ability with order: " + this.orderString())
			
						/*
						 * Use the corresponding order string to cast the added permanent "dummy" ability.
						 */
						if (IssueImmediateOrder(this.character().unit(), this.orderString())) then
							debug call Print("Successful morph with spell: " + GetAbilityName(this.ability()))
							// morph spells are expected to morph immediately
							call this.onMorph.execute()
						else
							debug call Print("Error on calling order " + this.orderString())
						endif
						
						// wait that this current trigger does not react on the manually issued order.
						call TriggerSleepAction(0.0)
					else
						debug call Print("Error on morphing.")
					endif
					
				debug else
					debug call Print("Cannot morph for spell: " + GetObjectName(this.ability()))
				endif
			// restore
			else
				if (this.canRestore.evaluate()) then
					debug call Print("Restoring from metamorphosis with ability " + GetAbilityName(this.ability()))
					/*
					 * Order ability again.
					 * In this case no replacement dummy ability is required since the morphed unit never has an inventory nor a spell book.
					 * After stopping the unit immediately the ability becomes mysteriosely to the order string as if the unit had been unmorphed successfully.
					 * Therefore the order string must be ordered again.
					 */
					if (IssueImmediateOrder(this.character().unit(), this.orderString())) then
						debug call Print("Waiting for restoration")
						/*
						 * Wait until the unit has unmorphed successfully since the casting time is not known.
						 * This also waits with the removal of the "dummy" ability so it won't be null in any other trigger.
						 */
						call thistype.waitForRestoration(this.character().unit(), this.unitTypeId())
						debug call Print("Restore from morph with spell: "  + GetAbilityName(this.ability()))
						debug call Print("Trigger ability is: "  + GetAbilityName(GetSpellAbilityId()))
						// wait until all triggers have been run which have spell events to avoid any null abilities
						// without this call the game crashes since other triggers are called based on an already removed ability
						call TriggerSleepAction(0.0)
						/*
						 * Remove the permant "dummy" ability".
						 */
						call UnitRemoveAbility(this.character().unit(), this.ability())
						/**
						 * Now readd all removed abilities and restory the inventory.
						 */
						if (Character(this.character()).restoreUnit(this.ability())) then
							debug call Print("Restored successfully for spell: " + GetObjectName(this.ability()))
							set this.m_isMorphed = false
							call this.onRestore.execute()
						endif
					debug else
						debug call Print("Error on calling unorder " + this.orderString())
					endif
				debug else
					debug call Print("Cannot restore for spell: " + GetObjectName(this.ability()))
					
				endif
			endif
			
			debug call Print("Enabling trigger")

			call EnableTrigger(this.m_channelTrigger)
		endmethod
		
		private static method triggerConditionRevival takes nothing returns boolean
			local thistype this = AHashTable.global().handleInteger(GetTriggeringTrigger(), "this")
			debug call Print("Revival: " + GetAbilityName(this.ability()))
			debug if (this.isMorphed()) then
			debug call Print("Is morphed")
			debug endif
			return this.isMorphed()
		endmethod
		
		public static method create takes Character character, integer abilityId returns thistype
			local thistype this = thistype.allocate()
			set this.m_character = character
			set this.m_favoriteAbility = 0
			set this.m_ability = abilityId
			set this.m_unitTypeId = 0
			
			set this.m_channelTrigger = CreateTrigger()
			// register action before cast has finished!
			call TriggerRegisterUnitEvent(this.m_channelTrigger, this.character().unit(), EVENT_UNIT_SPELL_CHANNEL)
			call TriggerAddCondition(this.m_channelTrigger, Condition(function thistype.triggerConditionStart))
			call TriggerAddAction(this.m_channelTrigger, function thistype.triggerActionStart)
			call AHashTable.global().setHandleInteger(this.m_channelTrigger, "this", this)
			
			// unmorph unit if it is being revived and has been morphed
			set this.m_revivalTrigger = CreateTrigger()
			call TriggerRegisterUnitEvent(this.m_revivalTrigger, this.character().unit(), EVENT_UNIT_HERO_REVIVE_FINISH)
			call TriggerAddCondition(this.m_revivalTrigger, Condition(function thistype.triggerConditionRevival))
			call TriggerAddAction(this.m_revivalTrigger, function thistype.triggerActionStart)
			call AHashTable.global().setHandleInteger(this.m_revivalTrigger, "this", this)
			
			return this
		endmethod
		
		public method onDestroy takes nothing returns nothing
			call AHashTable.global().destroyTrigger(this.m_channelTrigger)
			set this.m_channelTrigger = null
			call AHashTable.global().destroyTrigger(this.m_revivalTrigger)
			set this.m_revivalTrigger = null
		endmethod
	endstruct

endlibrary
As you can see I stop the casting unit immediately and then remove the casted ability. Then I add the ability permanently and cast it via the trigger.
The game crashes after the second morph that means the unit is successfully morphed nothing should happen anymore in the trigger function but the game then crashs.
I'd also like to remove the TriggerSleepAction() calls since they might lead to side effects but then I remove the first one other triggers are called with the already removed ability which then is null and it leads to other failures.

Any ideas? I know it doesn't look great but it is a start.

edit:
And there is another annoying bug. After unmorphing I cannot cancel any orders of the unit. If I order the unit to move somewhere I cannot stop it :D
 
Last edited:
Status
Not open for further replies.
Top