namreeb / nampower

Dramatically increase cast efficiency on the 1.12.1 client!
Other
90 stars 18 forks source link

Macro approach less than ideal #2

Closed namreeb closed 6 years ago

namreeb commented 7 years ago

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:

  1. Allow spell cast to be sent even if one is currently underway
  2. Launch spell cast bar once spell is cast, rather than upon acknowledgement from the server

I've created this issue to give myself a convenient place to store research into achieving this goal.

namreeb commented 7 years ago

Chronological list of control flow divergence

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.

namreeb commented 7 years ago

Error Handling

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.

namreeb commented 7 years ago

Normal Spell Cast

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:

  1. Spell_C_CastSpell (0x6E4B60)
  2. Spell_C_TargetSpell (0x6E5250)
  3. Spell_C_HandleSpriteClick (0x6E5B40)
  4. 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.

namreeb commented 7 years ago

Possible Solution 1

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:

  1. Spell_C_CastSpell at 0x6E4FF5, which is just a check for non-zero cast time.
  2. CGUnit_C::PlayImpactKit at 0x60F374.
namreeb commented 7 years ago

Possible Solution 2

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.

tserafin commented 7 years ago

Hi namreeb,

I am quite interested in your progress on this ticket and am wondering how you are tracking with this?

Regards

namreeb commented 7 years ago

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.

namreeb commented 6 years ago

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.