Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[melody] Internal logic improvements / fixes #85

Merged
merged 3 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions Zeal/EqStructures.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
#define USERCOLOR_ECHO_CHAT_9 0xFF + 68 // 68 - chat 9 echo
#define USERCOLOR_ECHO_CHAT_10 0xFF + 69 // 69 - chat 10 echo

constexpr WORD kInvalidSpellId = 0xffff; // spell_id used when not casting or empty spell gem

namespace Zeal
{
Expand Down Expand Up @@ -166,6 +167,53 @@ namespace Zeal
Stand = 10,
Taunt = 11
};
enum SpellTargetType
{
/* 01 */ TargetOptional = 0x01,
/* 02 */ AEClientV1 = 0x02,
/* 03 */ GroupV1 = 0x03,
/* 04 */ PBAE = 0x04,
/* 05 */ Target = 0x05,
/* 06 */ Self = 0x06,
/* 07 */ // NOT USED
/* 08 */ TargetedAE = 0x08,
/* 09 */ Animal = 0x09,
/* 10 */ Undead = 0x0a,
/* 11 */ Summoned = 0x0b,
/* 12 */ // NOT USED
/* 13 */ Tap = 0x0d,
/* 14 */ Pet = 0x0e,
/* 15 */ Corpse = 0x0f,
/* 16 */ Plant = 0x10,
/* 17 */ UberGiant = 0x11, //special giant
/* 18 */ UberDragon = 0x12, //special dragon
/* 19 */ // NOT USED
/* 20 */ TargetedAETap = 0x14,
/* 21 */ // NOT USED
/* 22 */ // NOT USED
/* 23 */ // NOT USED
/* 24 */ UndeadAE = 0x18,
/* 25 */ SummonedAE = 0x19,
/* 26 */ // NOT USED
/* 27 */ // NOT USED
/* 28 */ // NOT USED
/* 29 */ // NOT USED
/* 30 */ // NOT USED
/* 31 */ // NOT USED
/* 32 */ // NOT USED
/* 33 */ // NOT USED
/* 34 */ // NOT USED
/* 35 */ // NOT USED
/* 36 */ // NOT USED
/* 37 */ // NOT USED
/* 38 */ // NOT USED
/* 39 */ // NOT USED
/* 40 */ AEBard = 0x28,
/* 41 */ GroupV2 = 0x29,
/* 42 */ // NOT USED
/* 43 */ ProjectIllusion = 0x2b, // Not found in spell data, used internally.

};


}
Expand Down Expand Up @@ -453,13 +501,13 @@ namespace Zeal
{
return reinterpret_cast<short(__thiscall*)(EQCHARINFO*)>(0x4b9450)(this);
}
int cast(UINT gem, short spell_id, int* item, short item_slot)
int cast(UINT gem, WORD spell_id, int* item, short item_slot)
{
return reinterpret_cast<int(__thiscall*)(EQCHARINFO*, UINT, short, int*, short)>(0x4c483b)(this, gem, spell_id, item, item_slot);
return reinterpret_cast<int(__thiscall*)(EQCHARINFO*, UINT, WORD, int*, short)>(0x4c483b)(this, gem, spell_id, item, item_slot);
}
void stop_cast(UINT reason, short spell_id)
void stop_cast(UINT reason, WORD spell_id)
{
return reinterpret_cast<void(__thiscall*)(EQCHARINFO*, UINT, short)>(0x4cb510)(this, reason, spell_id);
return reinterpret_cast<void(__thiscall*)(EQCHARINFO*, UINT, WORD)>(0x4cb510)(this, reason, spell_id);
}
/* 0x0000 */ BYTE Unknown0000[2];
/* 0x0002 */ CHAR Name[64]; // [0x40]
Expand Down
2 changes: 1 addition & 1 deletion Zeal/commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ ChatCommands::ChatCommands(ZealService* zeal)
Zeal::EqGame::print_chat("item %s does not have a spell attached to it.", item->Name);
return true;
}
if (!self->ActorInfo || self->ActorInfo->CastingSpellId != 0xffff)
if (!self->ActorInfo || self->ActorInfo->CastingSpellId != kInvalidSpellId)
{
Zeal::EqGame::print_chat(USERCOLOR_SPELLS, "You must stop casting to cast this spell!");
return true;
Expand Down
2 changes: 1 addition & 1 deletion Zeal/labels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ bool GetLabelFromEq(int EqType, Zeal::EqUI::CXSTR* str, bool* override_color, UL
{
if (Zeal::EqGame::get_controlled()->ActorInfo->CastingSpellId) {
int spell_id = Zeal::EqGame::get_controlled()->ActorInfo->CastingSpellId;
if (spell_id == 65535) spell_id = 0; // avoid crash while player is not casting a spell
if (spell_id == kInvalidSpellId) spell_id = 0; // avoid crash while player is not casting a spell
Zeal::EqStructures::SPELL* casting_spell = Zeal::EqGame::get_spell_mgr()->Spells[spell_id];
Zeal::EqGame::CXStr_PrintString(str, "%s", casting_spell->Name);
*override_color = false;
Expand Down
43 changes: 28 additions & 15 deletions Zeal/melody.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
// failing (like Selo's indoors), the vulnerable timing window is dominant, making it
// hard to click off the melody with the UI. The new retry_count logic mitigates this.


constexpr int RETRY_COUNT_REWIND_LIMIT = 8; // Will rewind up to 8 times.
constexpr int RETRY_COUNT_END_LIMIT = 15; // Will terminate if 15 retries w/out a 'success'.
constexpr ULONGLONG MELODY_SONG_INTERVAL = 150; // Interval between songs. If too low, the song may not fire.

bool Melody::start(const std::vector<int>& new_songs)
{
Expand Down Expand Up @@ -79,6 +79,7 @@ bool Melody::start(const std::vector<int>& new_songs)
songs = new_songs;
current_index = -1;
retry_count = 0;
casting_melody_spell_id = kInvalidSpellId;
if (songs.size())
Zeal::EqGame::print_chat(USERCOLOR_SPELLS, "You begin playing a melody.");
return true;
Expand All @@ -91,11 +92,12 @@ void Melody::end()
current_index = -1;
songs.clear();
retry_count = 0;
casting_melody_spell_id = kInvalidSpellId;
Zeal::EqGame::print_chat(USERCOLOR_SPELL_FAILURE, "Your melody has ended.");
}
}

void Melody::handle_stop_cast_callback(BYTE reason)
void Melody::handle_stop_cast_callback(BYTE reason, WORD spell_id)
{
// Terminate melody on stop except for missed note (part of reason == 3) rewind attempts.
if (reason != 3 || !songs.size())
Expand All @@ -109,26 +111,29 @@ void Melody::handle_stop_cast_callback(BYTE reason)
// is not allowed in the zone), so we use a retry_count to limit the spammy loop that is
// difficult to click off with UI spell gems (/stopsong, /melody still work fine). The modulo
// check skips the rewind so it advances to the next song but then allows that song to retry.
if ((current_index >= 0) && (++retry_count % RETRY_COUNT_REWIND_LIMIT)) {
if (casting_melody_spell_id == spell_id && (++retry_count % RETRY_COUNT_REWIND_LIMIT)) {
current_index--;
if (current_index < 0) { // Handle wraparound.
current_index = songs.size() - 1;
}
}
casting_melody_spell_id = kInvalidSpellId;
}

void __fastcall StopCast(int t, int u, BYTE reason, short spell_id)
void __fastcall StopCast(int t, int u, BYTE reason, WORD spell_id)
{
ZealService::get_instance()->melody->handle_stop_cast_callback(reason);
ZealService::get_instance()->melody->handle_stop_cast_callback(reason, spell_id);
ZealService::get_instance()->hooks->hook_map["StopCast"]->original(StopCast)(t, u, reason, spell_id);
}

void Melody::stop_current_cast()
{
// Note: This code assumes the current_index is valid to look up the spell_id for the StopCast call.
Zeal::EqStructures::EQCHARINFO* char_info = Zeal::EqGame::get_char_info();
if (char_info && current_index>=0 && current_index < songs.size())
ZealService::get_instance()->hooks->hook_map["StopCast"]->original(StopCast)((int)char_info, 0, 0, char_info->MemorizedSpell[songs[current_index]]);
Zeal::EqStructures::Entity* self = Zeal::EqGame::get_self();
if (char_info && self && self->ActorInfo && self->ActorInfo->CastingSpellId != kInvalidSpellId) {
ZealService::get_instance()->hooks->hook_map["StopCast"]->original(StopCast)((int)char_info, 0, 0, self->ActorInfo->CastingSpellId);
}
casting_melody_spell_id = kInvalidSpellId;
}

void Melody::tick()
Expand Down Expand Up @@ -162,36 +167,44 @@ void Melody::tick()
return;
}

if ((current_timestamp - casting_visible_timestamp) < 150)
// Successfully finished casting current song/spell
// Reseting this field prevents the song from repeating after this point
casting_melody_spell_id = kInvalidSpellId;

// Wait for MELODY_SONG_INTERVAL ms between casting the next song
if ((current_timestamp - casting_visible_timestamp) < MELODY_SONG_INTERVAL)
return;

// Handles situations like trade windows, looting (Stance::Bind), and ducking.
if (!Zeal::EqGame::get_eq() || !Zeal::EqGame::get_eq()->IsOkToTransact() ||
self->StandingState != Stance::Stand)
return;

if (self->ActorInfo && self->ActorInfo->CastingSpellGemNumber == 255) //255 = Bard Singing
stop_current_cast(); //abort bard song if active.
stop_current_cast(); //abort bard song if active.

// Cast the next song in the melody
current_index++;
if (current_index >= songs.size() || current_index < 0)
current_index = 0;

int current_gem = songs[current_index]; // songs is 'guaranteed' to have a valid gem index from start().
if (char_info->MemorizedSpell[current_gem] == -1)
WORD current_gem_spell_id = char_info->MemorizedSpell[current_gem];
if (current_gem_spell_id == kInvalidSpellId)
return; //simply skip empty gem slots (unexpected to occur)

//handle a common issue of no target gracefully (notify once and skip to next song w/out retry failures).
if (Zeal::EqGame::get_spell_mgr() &&
Zeal::EqGame::get_spell_mgr()->Spells[char_info->MemorizedSpell[current_gem]]->TargetType == 5 &&
(Zeal::EqGame::get_spell_mgr()->Spells[current_gem_spell_id]->TargetType == Zeal::EqEnums::SpellTargetType::Target ||
Zeal::EqGame::get_spell_mgr()->Spells[current_gem_spell_id]->TargetType == Zeal::EqEnums::SpellTargetType::TargetedAE) &&
!Zeal::EqGame::get_target())
{
Zeal::EqGame::print_chat(USERCOLOR_SPELL_FAILURE, "You must first select a target for spell %i", current_gem + 1);
retry_count++; // Re-use the retry logic to limit runaway spam if entire song list is target-based.
return;
}

char_info->cast(current_gem, char_info->MemorizedSpell[current_gem], 0, 0);
casting_melody_spell_id = current_gem_spell_id;
char_info->cast(current_gem, current_gem_spell_id, 0, 0);
start_of_cast_timestamp = current_timestamp;
}

Expand Down
4 changes: 3 additions & 1 deletion Zeal/melody.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
#include "hook_wrapper.h"
#include "memory.h"
#include "EqUI.h"

class Melody
{
public:
bool start(const std::vector<int>& new_songs); //returns true if no errors
void end();
void handle_stop_cast_callback(BYTE reason);
void handle_stop_cast_callback(BYTE reason, WORD spell_id);
Melody(class ZealService* pHookWrapper, class IO_ini* ini);
~Melody();
private:
Expand All @@ -16,5 +17,6 @@ class Melody
int current_index = 0; // Active song index. -1 if not started yet.
std::vector<int> songs; // Gem indices (base 0) for melody.
int retry_count = 0; // Tracks unsuccessful song casts.
WORD casting_melody_spell_id = kInvalidSpellId; // Current melody song being cast. Is only a valid id while cast window is visible (actively casting).
};