As you may know, charging the large firestorm does not increase the damage caused by the AoE. I set out to try and fix this for my mod some week ago. This seems to be because the spell information transmitted by the VisualFX is lost somewhere along the way. I couldn't find much in the way of solutions or fixes for this, and so far I have only been able to find a rather roundabout way to get the missing spell ID (code below) using the "OnDMG" scripts originally created by Lehona. I thought I would just share what I found here, perhaps someone has some good ideas or even knows a much better approach to fixing this issue, here's hoping ...
Here are some things that I found out:
This seems to only be an issue where AOE/spread collision VFX is created by a projectile (with base game spells only small/large firestorm do this)
If the projectile VFX has static collision (ground/world), then the OnDMG for NPCs hit by the AOE has lost the spell level / damage information.
If the projectile VFX has dynamic collision (e.g. direct hit on npc), then the OnDMG for other NPCs hit by the AOE has lost all spell information.
In both cases (static/dynamic collision) there seems to be absolutely no way to figure out the original spell level / charged damage from the OnDMG event or VFX associated with the OnDMG event.
Below is an adjusted version of the "OnDMG" script left by Kirides here: link. With this the damage can be calculated using the correct spell ID, which is enough for small firestorm, but in both cases (static and dynamic collision) the spell level / charged damage information is lost. This is problematic for large firestorm and any similar custom spells, causing them to deal charged damage only on direct hit.
Spoiler:(zum lesen bitte Text markieren)
For the sake of conciseness, I left out *DMG_Calculate*. Lets just say it simply returns some integer.
Code:
func int DMG_OnDmg(var int victimPtr, var int attackerPtr, var int dmg, var int dmgDescriptorPtr, var int bHasHit) {
// ???
if (!victimPtr) { return dmg; };
var oSDamageDescriptor dmgDesc; dmgDesc = _^(dmgDescriptorPtr);
// If there is hitPfx but no spellId, probably AoE spell _SPREAD hitting other NPCs after a direct hit on NPC with the original projectile
if (dmgDesc.hitPfx && dmgDesc.spellId <= 0) {
var oCVisualFX hitPfx; hitPfx = _^(dmgDesc.hitPfx);
// If we still have no attacker, try to take him from PFX inflictor (e.g. AoE spells)
if (!attackerPtr) {
attackerPtr = hitPfx.inflictor;
};
// Set lost spell ID for AoE spells, based on hitPfx.
if (Hlp_StrCmp(hitPfx.fxName, "spellFX_Firestorm_SPREAD")) {
dmgDesc.spellId = SPL_Firestorm;
} else if (Hlp_StrCmp(hitPfx.fxName, "spellFX_Pyrokinesis_SPREAD")) {
dmgDesc.spellId = SPL_Pyrokinesis;
};
};
if (attackerPtr) {
var c_npc att; att = _^(attackerPtr);
var c_npc vic; vic = _^(victimPtr);
return DMG_Calculate(att, vic, dmgDesc, bHasHit);
};
return dmg;
};
func void _DMG_OnDmg_Post() {
const int dmgDesc = 0; dmgDesc = MEM_ReadInt((ESP + 640) + 4/* &oSDamageDescriptor */); // oCNpc :: OnDamage_Hit ( oSDamageDescriptor& descDamage )
const int bHasHit = 0; bHasHit = MEM_ReadInt((ESP + 640) - 356/* zBOOL bHasHit */);
EDI = DMG_OnDmg(EBP, MEM_ReadInt(dmgDesc+8), EDI, dmgDesc, bHasHit);
};
func void InitCustomDamageHook() {
HookEngineF(6736583/*0x66CAC7*/, 5, _DMG_OnDmg_Post);
};
PS. Is there some good way to copy/paste code into the messages here? Tabs get all messed up and some spaces are lost, I have to go through the code and try to make it readable each time.
Thanks, for some reason, my previous attempts failed with something like that.
Worked now though.
This is code for Gothic 2 Addon that sets the SpellLevel for the particleFx correctly.
The Engine sets the level in "CreateAndPlay" at "pfx->SetLevel(lvl, 0)", but resets it in the next line "Init(org, target, 0)"
This code hooks right after "Init()" and before "Cast()" and sets the level to the provided spellLevel (which is passed as function argument to "CreateAndPlay")
I use this code for my custom damage script to properly calculate damage and additional effects based on the spellLevel.
you can read the level using "(visFx.bitfield & oCVisualFX_bitfield_level) >> 13;" where visFX is and instance of oCVisualFx.
Spoiler:(zum lesen bitte Text markieren)
Code:
func void _Hook_oCVisualFX__CreateAndPlay_PostInit() {
const int stack_offset = 172;
const int param_lvl_offset = 16;
var int childPtr; childPtr = ESI;
var int level; level = MEM_ReadInt(ESP + stack_offset + param_lvl_offset);
var oCVisualFX _current; _current = _^(childPtr);
if (Hlp_StrCmp(_current.fxName, "spellFX_Firestorm_SPREAD"))
|| (Hlp_StrCmp(_current.fxName, "spellFX_Pyrokinesis_SPREAD"))
{
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
};
};
func void InitHook() {
HookEngineF(4778368/*0048e980*/, 8, _Hook_oCVisualFX__CreateAndPlay_PostInit);
};
oCVisualFX-class:
Spoiler:(zum lesen bitte Text markieren)
Code:
const int oCVisualFX_bitfield_offset = 1372;
const int oCVisualFX_bitfield_collisionOccured = ((1 << 1) - 1) << 0;
const int oCVisualFX_bitfield_showVisual = ((1 << 1) - 1) << 1;
const int oCVisualFX_bitfield_isChild = ((1 << 1) - 1) << 2;
const int oCVisualFX_bitfield_isDeleted = ((1 << 1) - 1) << 3;
const int oCVisualFX_bitfield_initialized = ((1 << 1) - 1) << 4;
const int oCVisualFX_bitfield_shouldDelete = ((1 << 1) - 1) << 5;
const int oCVisualFX_bitfield_lightning = ((1 << 1) - 1) << 6;
const int oCVisualFX_bitfield_fxInvestOriginInitialized = ((1 << 1) - 1) << 7;
const int oCVisualFX_bitfield_fxInvestTargetInitialized = ((1 << 1) - 1) << 8;
const int oCVisualFX_bitfield_fxInvestStopped = ((1 << 1) - 1) << 9;
const int oCVisualFX_bitfield_timeScaled = ((1 << 1) - 1) << 10;
const int oCVisualFX_bitfield_fovMorph = ((1 << 2) - 1) << 11;
const int oCVisualFX_bitfield_level = ((1 << 5) - 1) << 13;
const int oCVisualFX_bitfield_collisionCtr = ((1 << 3) - 1) << 18;
const int oCVisualFX_bitfield_queueSetLevel = ((1 << 5) - 1) << 21;
class oCVisualFX
{
//zCVob {
//zCObject {
var int _vtbl;
var int _zCObject_refCtr;
var int _zCObject_hashIndex;
var int _zCObject_hashNext;
var string _zCObject_objectName; // zSTRING
//}
var int _zCVob_globalVobTreeNode;
var int _zCVob_lastTimeDrawn;
var int _zCVob_lastTimeCollected;
var int _zCVob_vobLeafList_array;
var int _zCVob_vobLeafList_numAlloc;
var int _zCVob_vobLeafList_numInArray;
var int _zCVob_trafoObjToWorld[16];
var int _zCVob_bbox3D_mins[3];
var int _zCVob_bbox3D_maxs[3];
var int _zCVob_bsphere3D_center[3];
var int _zCVob_bsphere3D_radius;
var int _zCVob_touchVobList_array;
var int _zCVob_touchVobList_numAlloc;
var int _zCVob_touchVobList_numInArray;
var int _zCVob_type;
var int _zCVob_groundShadowSizePacked;
var int _zCVob_homeWorld;
var int _zCVob_groundPoly;
var int _zCVob_callback_ai;
var int _zCVob_trafo;
var int _zCVob_visual;
var int _zCVob_visualAlpha;
var int _zCVob_m_fVobFarClipZScale;
var int _zCVob_m_AniMode;
var int _zCVob_m_aniModeStrength;
var int _zCVob_m_zBias;
var int _zCVob_rigidBody;
var int _zCVob_lightColorStat;
var int _zCVob_lightColorDyn;
var int _zCVob_lightDirectionStat[3];
var int _zCVob_vobPresetName;
var int _zCVob_eventManager;
var int _zCVob_nextOnTimer;
var int _zCVob_bitfield[5];
var int _zCVob_m_poCollisionObjectClass;
var int _zCVob_m_poCollisionObject;
//}
// public:
var string visName_S; // zSTRING
var string visSize_S; // zSTRING
var int visAlpha; // float
var string visAlphaBlendFunc_S; // zSTRING
var int visTexAniFPS; // float
var int visTexAniIsLooping; // int
var string emTrjMode_S; // zSTRING
var string emTrjOriginNode_S; // zSTRING
var string emTrjTargetNode_S; // zSTRING
var int emTrjTargetRange; // float
var int emTrjTargetAzi; // float
var int emTrjTargetElev; // float
var int emTrjNumKeys;
var int emTrjNumKeysVar;
var int emTrjAngleElevVar; // float
var int emTrjAngleHeadVar; // float
var int emTrjKeyDistVar; // float
var string emTrjLoopMode_S; // zSTRING
var string emTrjEaseFunc_S; // zSTRING
var int emTrjEaseVel; // float
var int emTrjDynUpdateDelay; // float
var int emTrjDynUpdateTargetOnly;
var string emFXCreate_S; // zSTRING
var string emFXInvestOrigin_S; // zSTRING
var string emFXInvestTarget_S; // zSTRING
var int emFXTriggerDelay; // float
var int emFXCreatedOwnTrj; // int
var string emActionCollDyn_S; // zSTRING // CREATE, BOUNCE, COLLIDE
var string emActionCollStat_S; // zSTRING // CREATE, CREATEONCE, BOUNCE, COLLIDE, CREATEQUAD
var string emFXCollStat_S; // zSTRING
var string emFXCollDyn_S; // zSTRING
var string emFXCollDynPerc_S; // zSTRING
var string emFXCollStatAlign_S; // zSTRING // TRAJECTORY, COLLISIONNORMAL
var string emFXCollDynAlign_S; // zSTRING
var int emFXLifeSpan; // float
var int emCheckCollision;
var int emAdjustShpToOrigin;
var int emInvestNextKeyDuration; // float
var int emFlyGravity; // float
var string emSelfRotVel_S; // zSTRING
var string userString[5]; // zSTRING
var string lightPresetName; // zSTRING
var string sfxID; // zSTRING
var int sfxIsAmbient; // zBOOL
var int sendAssessMagic; // int
var int secsPerDamage; // float
var int dScriptEnd; // zBYTE
var int visSize[3]; // zVEC3
var int emTrjMode;
var int emActionCollDyn;
var int emActionCollStat;
var int emSelfRotVel[3]; // zVEC3
var int emTrjEaseFunc; // enum TEaseFunc
var int emTrjLoopMode; // enum TTrjLoopMode
var int fxState; // enum zTVFXState
// static zCParser* fxParser;
// static oCVisualFX* actFX;
var int root; // oCVisualFX*
var int parent; // oCVisualFX*
var int fxInvestOrigin; // oCVisualFX*
var int fxInvestTarget; // oCVisualFX*
var int ai; // oCVisualFXAI*
//zCArray <oCVisualFX *> fxList {
var int fxList_array;
var int fxList_numAlloc;
var int fxList_numInArray;
// }
//zCArray <oCVisualFX *> childList {
var int childList_array;
var int childList_numAlloc;
var int childList_numInArray;
// }
//zCArray <oCEmitterKey *> emKeyList {
var int emKeyList_array;
var int emKeyList_numAlloc;
var int emKeyList_numInArray;
// }
//zCArray <zCVob *> vobList {
var int vobList_array;
var int vobList_numAlloc;
var int vobList_numInArray;
// }
//zCArray <zCVob *> ignoreVobList {
var int ignoreVobList_array;
var int ignoreVobList_numAlloc;
var int ignoreVobList_numInArray;
// }
//zCArray <zCVob *> allowedCollisionVobList {
var int allowedCollisionVobList_array;
var int allowedCollisionVobList_numAlloc;
var int allowedCollisionVobList_numInArray;
// }
//zCArray <zCVob *> collidedVobs {
var int collidedVobs_array;
var int collidedVobs_numAlloc;
var int collidedVobs_numInArray;
// }
//zCArray <zSVisualFXColl> queuedCollisions {
var int queuedCollisions_array;
var int queuedCollisions_numAlloc;
var int queuedCollisions_numInArray;
// }
// oCTrajectory trajectory {
//zCArray <zCPositionKey *> keyList {
var int trajectory_keyList_array;
var int trajectory_keyList_numAlloc;
var int trajectory_keyList_numInArray;
// }
var int spl; // zCKBSpline*
var int mode;
var int length; // float
// zMAT4 res {
var int trajectory_res_v0[4]; //zREAL[4]
var int trajectory_res_v1[4]; //zREAL[4]
var int trajectory_res_v2[4]; //zREAL[4]
var int trajectory_res_v3[4]; //zREAL[4]
// }
var int lastKey;
// }
var int earthQuake; // zCEarthquake*
var int screenFX; // zCVobScreenFX*
var int screenFXTime; // float
var int screenFXDir;
var int orgNode; // zCModelNodeInst*
var int targetNode; // zCModelNodeInst*
var int lastSetVisual; // zCVisual*
var int origin; // zCVob*
var int inflictor; // zCVob*
var int target; // zCVob*
var int light; // zCVobLight*
var int lightRange; // float
var int sfx; // zCSoundFX*
var int sfxHnd; // zTSoundHandle
var string fxName; // zSTRING
var int fxBackup; // oCEmitterKey*
var int lastSetKey; // oCEmitterKey*
var int actKey; // oCEmitterKey*
var int frameTime; // float
var int collisionTime; // float
var int deleteTime; // float
var int damageTime; // float
var int targetPos[3]; // zPOINT3
var int lastTrjDir[3]; // zVEC3
var int keySize[3]; // zVEC3
var int actSize[3]; // zVEC3
var int castEndSize[3]; // zVEC3
var int nextLevelTime; // float
var int easeTime; // float
var int age; // float
var int trjUpdateTime; // float
var int emTrjDist; // float
var int trjSign; // float
var int levelTime; // float
var int lifeSpanTimer; // float
var int fxStartTime; // float
var int oldFovX; // float
var int oldFovY; // float - 0x0558
// { - 0x055C
// collisionOccured; // zBOOL : 1
// showVisual; // zBOOL : 1
// isChild; // zBOOL : 1
// isDeleted; // zBOOL : 1
// initialized; // zBOOL : 1
// shouldDelete; // zBOOL : 1
// lightning; // zBOOL : 1
// fxInvestOriginInitialized; // zBOOL : 1
// fxInvestTargetInitialized; // zBOOL : 1
// fxInvestStopped; // zBOOL : 1
// timeScaled; // zBOOL : 1
// fovMorph; // : 2
// level; // : 5
// collisionCtr; // : 3
// queueSetLevel; // : 5
var int bitfield;
// }
// protected:
var int damage; // float - 0x0560
var int damageType;
// private:
var int spellType;
var int spellCat;
var int spellTargetTypes;
var int savePpsValue; // float
var int saveVisSizeStart[2]; // zVEC2
var int transRing_v0[3]; // zPOINT3
var int transRing_v1[3]; // zPOINT3
var int transRing_v2[3]; // zPOINT3
var int transRing_v3[3]; // zPOINT3
var int transRing_v4[3]; // zPOINT3
var int transRing_v5[3]; // zPOINT3
var int transRing_v6[3]; // zPOINT3
var int transRing_v7[3]; // zPOINT3
var int transRing_v8[3]; // zPOINT3
var int transRing_v9[3]; // zPOINT3
var int ringPos;
var int emTrjFollowHitLastCheck; // zBOOL
var int bIsProjectile; // zBOOL
var int bPfxMeshSetByVisualFX; // zBOOL
var int m_bAllowMovement; // zBOOL
var int m_fSleepTimer; // zREAL
};
I did explore this bitfield, the values did not seem to change with different charge levels of Large Firestorm (when NPCs get hit by the "wave").
EDIT: did not see Kirides' new post before posting this, will check later today...
EDIT2: the solution from Kirides works perfectly with the dynamic collision case. I adjusted it slightly, so that spell ID is likewise corrected for the "OnDMG" hook:
Spoiler:(zum lesen bitte Text markieren)
Code:
func void _Hook_oCVisualFX__CreateAndPlay_PostInit() {
const int stack_offset = 172;
const int param_lvl_offset = 16;
var int childPtr; childPtr = ESI;
var int level; level = MEM_ReadInt(ESP + stack_offset + param_lvl_offset);
var oCVisualFX _current; _current = _^(childPtr);
if (Hlp_StrCmp(_current.fxName, "spellFX_Firestorm_SPREAD")) {
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
_current.spellType = SPL_Firestorm;
} else if (Hlp_StrCmp(_current.fxName, "spellFX_Pyrokinesis_SPREAD")) {
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
_current.spellType = SPL_Pyrokinesis;
};
};
However, this does not work for the the static collision case (e.g. hit ground next to NPC). The VFX that deals the damage seems to be different, "VOG_MAGICBURN" instead of the "_SPREAD" vfx. Changing the condition to include that won't help, as when this effect is created in the static collision case the level at ESP + stack_offset + param_lvl_offset seems to always be 1.
I did explore this bitfield, the values did not seem to change with different charge levels of Large Firestorm (when NPCs get hit by the "wave").
EDIT: did not see Kirides' new post before posting this, will check later today...
EDIT2: the solution from Kirides works perfectly with the dynamic collision case. I adjusted it slightly, so that spell ID is likewise corrected for the "OnDMG" hook:
Spoiler:(zum lesen bitte Text markieren)
Code:
func void _Hook_oCVisualFX__CreateAndPlay_PostInit() {
const int stack_offset = 172;
const int param_lvl_offset = 16;
var int childPtr; childPtr = ESI;
var int level; level = MEM_ReadInt(ESP + stack_offset + param_lvl_offset);
var oCVisualFX _current; _current = _^(childPtr);
if (Hlp_StrCmp(_current.fxName, "spellFX_Firestorm_SPREAD")) {
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
_current.spellType = SPL_Firestorm;
} else if (Hlp_StrCmp(_current.fxName, "spellFX_Pyrokinesis_SPREAD")) {
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
_current.spellType = SPL_Pyrokinesis;
};
};
However, this does not work for the the static collision case (e.g. hit ground next to NPC). The VFX that deals the damage seems to be different, "VOG_MAGICBURN" instead of the "_SPREAD" vfx. Changing the condition to include that won't help, as when this effect is created in the static collision case the level at ESP + stack_offset + param_lvl_offset seems to always be 1.
i imagine this is because i just hook the "vob" case of this function, and not the "position" case. I'll test it and update this post once i figured the neccessery stuff out.
EDIT: now i see...
The spell consists of three major parts (and a few others)
1. The main Attack
2. The Area of Effect damage attack
3. The additional burn effect
1 + 2 are mutually exclusive and 3 is a bonus on-top of either.
2 has a very short range, about 2-3 characters wide, while 3 has a much bigger range, about 4-5 characters wide.
idk if i can or want to make 3 count as a real hit.
I think I was wrong in my previous post about the VOB_MAGICBURN, which is the "part 3" in your categorization. This does not seem to be the vfx that deals damage.
However, I'm not sure we are on the same page still. I don't have the means to explore the engine code, I don't quite understand everything you mentioned, but do you mean to say that your solution works in both dynamic and static collision case? It does not seem to be the case for me...
I will attach 3 screenshots that hopefully explain what I mean better. In the screenshots I am printing the _current.fxName and level in your provided CreateAndPlay hook. In each screenshot I have cast a fully charged Large Firestorm (spell level 4).
Additionally, in the static collision case the CreateAndPlay hook never triggers for the _SPREAD VisualFX. In this way, the fix not having an effect for the static collision case makes sense; the spell level is not corrected at the "main attack" stage since the hook does not trigger for the _SPREAD VisualFX and is therefore still wrong in the "area of effect" attack stage.
So, I finally took some time and installed IDA. I set out to try and fix this issue for the static collision case.
I think I managed to find a similar point near the end of oCVisualFX::CreateAndCastFX at 0x0048f50f. At least it triggers when the ground is hit with a firestorm spell and its creating the correct _SPREAD VFX which can be found in ESI. The other version of CreateAndPlay method was not triggering at all at this point.
The problem I am facing is that I don't really know what I am doing. I think I am missing something obvious, I don't see the engine code as clearly as Kirides' makes it seem like... e.g. "pfx->SetLevel(lvl, 0)" or anything like that, its just all assembly with very sparse hints of what names things might have. I'm completely new to IDA... Perhaps I am missing some symbol table available somewhere, or maybe just some setting? I don't see how I can find the address where the level might be stored in this function, as its not given as a function parameter.
Here is my current code (WIP, includes Kirides' fix for the dynamic collision case):
Spoiler:(zum lesen bitte Text markieren)
Code:
func void _Hook_oCVisualFX__CreateAndPlay_PostInit() {
const int stack_offset = 172;
const int param_lvl_offset = 16;
var int childPtr; childPtr = ESI;
var int level; level = MEM_ReadInt(ESP + stack_offset + param_lvl_offset);
var oCVisualFX _current; _current = _^(childPtr);
if (Hlp_StrCmp(_current.fxName, "spellFX_Firestorm_SPREAD")) {
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
_current.spellType = SPL_Firestorm;
} else if (Hlp_StrCmp(_current.fxName, "spellFX_Pyrokinesis_SPREAD")) {
_current.bitfield = _current.bitfield & ~oCVisualFX_bitfield_level;
_current.bitfield = _current.bitfield | (level << 13);
_current.spellType = SPL_Pyrokinesis;
};
};
func void _Hook_oCVisualFX__CreateAndCastFX_PostInit() {
var int childPtr; childPtr = ESI;
var oCVisualFX _current; _current = _^(childPtr);
var int level; level = (_current.bitfield & oCVisualFX_bitfield_level) >> 13;
Print (ConcatStrings ("_current:", ConcatStrings(_current.fxName,ConcatStrings(" level:",IntToString(level)))));
};
func void oCVisualFX_CreateAndPlay_Init() {
HookEngineF(4778368/*0048e980*/, 8, _Hook_oCVisualFX__CreateAndPlay_PostInit);
HookEngineF(4781327/*0048f50f*/, 8, _Hook_oCVisualFX__CreateAndCastFX_PostInit);
};
So, I finally took some time and installed IDA. I set out to try and fix this issue for the static collision case.
I think I managed to find a similar point near the end of oCVisualFX::CreateAndCastFX at 0x0048f50f. At least it triggers when the ground is hit with a firestorm spell and its creating the correct _SPREAD VFX which can be found in ESI. The other version of CreateAndPlay method was not triggering at all at this point.
The problem I am facing is that I don't really know what I am doing. I think I am missing something obvious, I don't see the engine code as clearly as Kirides' makes it seem like... e.g. "pfx->SetLevel(lvl, 0)" or anything like that, its just all assembly with very sparse hints of what names things might have. I'm completely new to IDA... Perhaps I am missing some symbol table available somewhere, or maybe just some setting? I don't see how I can find the address where the level might be stored in this function, as its not given as a function parameter.
Here is my current code (WIP, includes Kirides' fix for the dynamic collision case):
...
i use IDA and Ghidra aswell as zEngine sourcecode material from Union.
This way i can use IDA to find proper stack-offsets and with Ghidra i can take a look at decompiled assembly in C++ from the game.
That's ... not clearly seeing it and also very time consuming to fill out every single class that i want to look at using the zEngine data.
From the looks of it, i don't see any valid "level" value anywhere in that path :/
maybe by hooking somewhere earlier and providing the "level" all the way down, or looking at every parent-oCVisualFx there is a way to find the proper value...
In the static collision (I hit the ground), the level is always 1
It it set to 1 in oCVisualFX::Init(const zCVob *orgVob, const zCVob *trgtVob, const zCVob *inflictorVob).
I dont know if it has impact on other spells/effects and we need to rewrite the caller function (maybe just reorder SetLevel and Init calls), but you can just disable this assignment:
So, I tested that overriding the spell level in the bitfield at 0x0048f50f for the VFX stored in ESI does affect the ultimate damage dealt to the NPCs hit by the _SPREAD VFX (in the static collision case). So it does seem to be the right place to set the level.
I tried to find a previous address where I could get the spell level. I did trial&error with the assembly code, but as I didn't really know what any of the code was about I didn't find a solution or really any useful information this way.
I also explored the VFX stored in ESI, but there appears to be no link to the projectile VFX shot from the player/caster, where I could potentially copy the level from. Root and parent both seem to point at the same VFX stored in ESI.
TopLayers suggestion seems plausible, but I think this is a bit beyond my abilities.
TopLayers suggestion seems plausible, but I think this is a bit beyond my abilities.
It is just about writing bytes in memory. The solution I gave can be easy applied if you rely on Union: just add .patch file with the text I provided. If you dont rely on Union you have Ikarus methods such MEM_WriteByte or something.
More complete solution:
-fixed spell level reset
-fixed C_CanCollideWith argument (was focus vob instead of collided vob)
-dynamic collision effects created the same way as static
It is just about writing bytes in memory. The solution I gave can be easy applied if you rely on Union: just add .patch file with the text I provided. If you dont rely on Union you have Ikarus methods such MEM_WriteByte or something.
More complete solution:
-fixed spell level reset
-fixed C_CanCollideWith argument (was focus vob instead of collided vob)
-dynamic collision effects created the same way as static
....
For the Firestorm AoE, it seems that just overriding the "level = 1" assignment in oCVisualFx::Init fixes the issue for Static aswell as dynamic collision.
This is the daedalus code for that: (I do some sanity check at the beginning)
This can be used instead of hooking the engine. It should also be way more performant that way, since you only override the engine once at the start.
EDIT:
Regarding the "C_CanNpcCollideWithSpell" wrong target:
@mud-freak already wrote a fix for the GothicFreeAim Patch/Scripts.
I took the code and modified it to work without GFA (only a few checks).
EDIT2: And removed a check for "only do it if spell is casted from the player"
It properly checks for the collided vob to be oCNpc before setting "target = collidedVob"
Spoiler:(zum lesen bitte Text markieren)
Code:
func void _Hook_Fix_oCVisualFX_SpellTarget_CollisionTarget() {
var int visualFX; visualFX = EBP;
const int oCVisualFX_targetVob_offset = 1200; //0x04B0
const int oCVisualFX_originVob_offset = 1192; //0x04A8
const int oCVisualFX__SetTarget = 4788960; //0x4912E0
const int oCNpc__player = 11216516; // 00ab2684
// Only if player is caster and free aiming is enabled
if (MEM_ReadInt(visualFX+oCVisualFX_originVob_offset) != MEM_ReadInt(oCNpc__player)) {
return;
};
// Get collision vob and target vob
var int collisionVob; collisionVob = MEM_ReadInt(MEM_ReadInt(ESP+360)); // esp+164h+4h
var int target; target = MEM_ReadInt(visualFX+oCVisualFX_targetVob_offset);
// Update target (increase/decrease reference counters properly)
if (Hlp_Is_oCNpc(collisionVob)) && (collisionVob != target) {
const int call = 0; var int zero;
if (CALL_Begin(call)) {
if (GOTHIC_BASE_VERSION == 2) {
CALL_IntParam(_@(zero)); // Do not re-calculate new trajectory
};
CALL_PtrParam(_@(collisionVob));
CALL__thiscall(_@(visualFX), oCVisualFX__SetTarget);
call = CALL_End();
};
};
};
Superb solution(s)! Thank you both for the help/info. I tested the solution from Kirides' quite thoroughly (as I don't use Union) and nothing seems to break due to skipping that setting of level to 1.
Regarding the additional fix for C_CanNpcCollideWithSpell; I understand what the fix does, but can you specify what exactly is the reason to do this, when is the target not correct in the collide check function? Do I need to modify GFA code to make this work with it?
I should also point out that if damage calculation in the OnDMG hook require the spell ID, the dynamic collision case will have lost it in the damage calculation hook. Since this is not an issue in the static collision case, the previous solution can be used to fix this issue as well.
Regarding the additional fix for C_CanNpcCollideWithSpell; I understand what the fix does, but can you specify what exactly is the reason to do this, when is the target not correct in the collide check function? Do I need to modify GFA code to make this work with it?
For the player, the target of a spell corresponds to the focused NPC. In the context of free aiming in GFA, there may not necessarily be any NPC in focus when firing off a spell. On collision, C_CanNpcCollideWithSpell would then give incorrect results as the collision NPC is not passed to the function. Under normal circumstances (without GFA), this happens only rarely.
The source of the code snippet can be found here. As posted here, it is already adjusted that you do not need to worry about GFA.
However, I would suggest to leave the check for the player in there. I remember there was infact a good reason for this check, although I don't recall what it was exactly. If I had to guess, it had to do something with ensuring that NPCs only damage the NPCs they specifically target. This is found throughout all of Gothic (ranged and melee combat). It is important to avoid, for instance, that NPCs would accidentally attack their allies resulting in random quarrels/killings between allied NPCs while hostile NPCs are no longer attacked.
...
However, I would suggest to leave the check for the player in there. I remember there was infact a good reason for this check, although I don't recall what it was exactly. If I had to guess, it had to do something with ensuring that NPCs only damage the NPCs they specifically target. This is found throughout all of Gothic (ranged and melee combat). It is important to avoid, for instance, that NPCs would accidentally attack their allies resulting in random quarrels/killings between allied NPCs while hostile NPCs are no longer attacked.
Well that explains the angry orc shaman attacking his Elite brother
totally forgot that enemy mages actually exist. ( seekers, shamans anyone? )
I modified the snippet to include the check again.
So, what is the reason to change oCVisualFX::target property? Why not just push collided NPC in C_CanNpcCollideWithSpell function instead of target?
I hadn't read the earlier replies to this thread. The solution you suggest here seems to be complete and addressing the original issue of this thread well. I was only commenting on the code snippet from GFA that showed up here. The intention of that function was in fact to overwrite the target property and may not be the best idea for the intention here.
For those who are interested; I took the time to convert TopLayer's full solution to Ikarus code. Its a bit ugly, partially missing sanity checks and I don't really know what all the steps are for, but it does seem to work.