Closed namreeb closed 6 years ago
0x6E4D43: Check to prevent error spam when the player is casting the currently-casting spell repeatedly.
if ( spellId == s_modalSpellID )
{
SndInterfacePlayInterfaceSound("igPlayerInviteDecline");
return false;
}
Solution: Replace JNZ 0x6E4D62
with JMP 0x6E4D62
at 0x6E4D49
.
0x6E4DE3: If a modal spell (one which created a cast bar) is underway, report that another spell is in progress.
if ( Spell_C_IsModal() )
{
modalSpell = s_modalSpellID < 0 || s_modalSpellID > g_spellDB.m_maxID ? NULL : g_spellDB.m_recordsById[s_modalSpellID];
if ( !(modalSpell->Attributes & (SPELL_ATTR_ON_NEXT_SWING_2|SPELL_ATTR_ON_NEXT_SWING_1)) )
{
Spell_C_SpellFailed(v44->Id, SPELL_FAILED_SPELL_IN_PROGRESS, -1, -1);
return false;
}
}
Solution: If we end up being successful at changing when the cast bar appears, this probably wont be necessary to bypass. For now, it can be bypassed by changing JZ 0x6E4DE6
with JMP 0x6E4DE6
at 0x6E4D9E
.
Edit: Another benefit to not bypassing this check (if it's true that we don't need to) is that it will reduce spam to the server.
This post assumes the two above patches have been made.
When a spell is casting, and the same spell is cast again, two different errors are reported: spell not ready, and interrupted (and the original spell cast is interrupted). This interruption comes from CGUnit_C::CheckAndReportSpellInhibitFlags()
, cast by Spell_C_CastSpell()
at 0x6E4F3B
:
if ( !RangeCheckSelected(this, spell, guid, 1) || !CGUnit_C::CheckAndReportSpellInhibitFlags(this, spell, a2) )
{
return false;
}
The logic in CGUnit_C::CheckAndReportSpellInhibitFlags()
is as follows:
// is this spell on cooldown?
if ( Spell_C_GetSpellCooldown(spellRec->Id, 0, 0, 0, 0) )
{
repeatId = GetAutoRepeatSpellId();
if ( repeatId >= 0 && repeatId <= g_spellDB.m_maxID )
{
repeatSpell = g_spellDB.m_recordsById[repeatId];
if ( repeatSpell )
{
// should the current cast be cancelled?
if ( repeatSpell->AttributesEx3 & SPELL_ATTR_EX3_REQ_WAND )// <-- bad name
{
pkt.m_buffer = 0;
pkt.m_base = 0;
pkt.m_alloc = 0;
pkt.m_size = 0;
pkt.m_read = -1;
pkt.VMT = (CDataStoreVMT *)CDataStore::`vftable';
CDataStore::Put32(&pkt, 0x12F); // CMSG_CANCEL_CAST
repeatId = GetAutoRepeatSpellId();
CDataStore::Put32(&pkt, repeatId);
pkt.m_read = 0;
ClientServices_Send(&pkt);
Send_CMSG_CANCEL_AUTO_REPEAT_SPELL();
pkt.VMT = (CDataStoreVMT *)CDataStore::`vftable';
if ( pkt.m_alloc != -1 )
CDataStoreVMT_InternalDestroy(&pkt, &pkt.m_buffer, &pkt.m_base, &pkt.m_alloc);
}
}
}
// fail: not ready yet
Spell_C_SpellFailed(spellRec->Id, SPELL_FAILED_NOT_READY, -1, -1);
return false;
}
Note that the check for the auto repeating spell is not relevant to our task. I think there is no problem with CGUnit_C::CheckAndReportSpellInhibitFlags()
calling Spell_C_SpellFailed()
when the requested spell is on cooldown, but Spell_C_SpellFailed()
will call Spell_C_CancelSpell()
if the failed spell is the current modal.
Solution: Replace JNZ 0x6E2228
with JMP 0x6E2228
at 0x6E2207
.
At this point, there is no unexpected behavior, but no new behavior either. The next step is to find why CMSG_CAST_SPELL
is not sent. This should give some indication of what state needs to be updated when a spell cast is attempted, rather than when the server acknowledges it.
EDIT: There is a problem with allowing this message in that if CGUnit_C::CheckAndReportSpellInhibitFlags()
returns false
, the cast will not proceed. To bypass this check, replace JZ 0060962C
with JMP 0060962C
at 0x609570
.
This usually comes from a sprite click (specifically, the action bar). Consequently, the spell cast is triggered by Spell_C_HandleSpriteClick()
. The call stack on a normal call looks like this:
Spell_C_CastSpell (0x6E4B60)
Spell_C_TargetSpell (0x6E5250)
Spell_C_HandleSpriteClick (0x6E5B40)
SendCast (0x6E54F0)
Spell_C_CastSpell()
is always called, even if a spell cast is already underway. Spell_C_TargetSpell()
, however, is not. Therefore the control flow divergence is somewhere within Spell_C_CastSpell()
. A few reasons why are mentioned in the first comment, however these are all cases that should be okay to keep. What we really want to find is what state is cleared when a response is received from the server.
CMSG_CAST_SPELL
does not appear to be sent to the server until after SMSG_CAST_RESULT
is received. The handler for this function is at 0x6E7A70
.
Hook Spell_C_CastSpell
. If it returns true, a successful cast was sent. Begin a timer for the duration of the cast. Once the timer elapses, reset s_modalSpellID
. How do we find the cast time? Remember to account for haste buffs (i.e. Mind Quickening Gem) and push-back from damage. The cast bar accounts for these things, so surely they are calculated somewhere?
Spell push-back is controlled by the server (since it most roll for pushback resist mechanics). The client is informed of delays via SMSG_SPELL_DELAYED
. However this opcode is handled by the built-in lua interface code, and there is no convenient method of reading this. Therefore we probably will have to calculate the expected cast time, considering haste (de)buffs, and adjust based on intercepting SMSG_SPELL_DELAYED
. This does not handle the case of casting a spell before the removal of a haste buff has arrived at the client, however.
CGPlayer_C::GetSpellCastingTime()
is at 0x5EE150
, and is cast by:
Spell_C_CastSpell
at 0x6E4FF5
, which is just a check for non-zero cast time.CGUnit_C::PlayImpactKit
at 0x60F374
.Begin the cast bar when the cast is sent, rather than when it is acknowledged by the server. The cast bar is controlled by the interface files internal to the games MPQ files. Specifically: Interface\FrameXML\CastingBarFrame.lua
. The cast bar is first created by the SPELLCAST_START
event, which has a numeric id of 337
.
The SPELLCAST_START
event is triggered by the handler for SMSG_SPELL_START
at 0x6E7A53
:
FrameScript_SignalEvent(EVENT_SPELLCAST_START, "%s%d", spellName, msCastTime);
This is not a complete solution in and of itself. In very rare instances, we will have outdated information about our own spell haste. Our haste may be lower than we are aware, and in extremely rare conditions it may be higher than we are aware (although I think this is practically impossible except with extremely high latency, greater than the duration of the global cooldown). In the event that our haste is lower than we are aware, there shouldn't be any problem, as our cast will actually finish before the cast bar does. In the event that our haste is higher than we are aware, there will still be a slight unnecessary delay before the next cast may complete, but this will be left as a future enhancement to resolve (since in reality it will probably never happen).
To achieve this solution, the call to FrameScript_SignalEvent(337)
at 0x6E7A53
must be removed, and an equivalent call to FrameScript_SignalEvent(337)
must be added upon the successful completion of Spell_C_CastSpell
.
Hi namreeb,
I am quite interested in your progress on this ticket and am wondering how you are tracking with this?
Regards
Hello, @tserafin. Thank you for your interest. I have started writing code for this and testing it in-game, but it is not quite working yet. I don't have an ETA. In fact, I'm still not even certain that this is a viable solution. Initial results are encouraging, though.
I have just pushed code for version 2.0, which solves this issue. I do not plan to upload binaries for it until I can test it a bit more.
The approach of requiring the user to make a macro of
/script CastSpellAtTarget(id)
is less than ideal. It would be better if we could reprogram the client's own behavior. The two primary things to accomplish are:I've created this issue to give myself a convenient place to store research into achieving this goal.