Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
095bcb8
L182: the Cash Shop's result notices in v83's words
neeerp Oct 18, 2026
54669d5
L182: SHOW_STATUS_INFO modes 2 and 6 to 13 as v83 shows them
neeerp Oct 18, 2026
9fe7bb8
L182: confirm before passing the party leadership
neeerp Oct 18, 2026
84baf90
L182: answer a bare /w NAME as v83 answers /whisper NAME
neeerp Oct 18, 2026
ed8cedb
L182: answer refused quest steps the way v83 does
neeerp Oct 18, 2026
1d02138
L182: ask before spending AP on MaxHP or MaxMP
neeerp Oct 18, 2026
ea4b180
L182: open a world's channels only when the server answers
neeerp Oct 18, 2026
c12d897
L182: show v83's notices for shop and storage failures
neeerp Oct 18, 2026
9f17f51
L182: word prompts the way v83 does
neeerp Oct 18, 2026
bb318d4
L182: write Monster Book card lines the way v83 does
neeerp Oct 18, 2026
3f34f60
L182: refuse skills with v83's cost and weapon lines
neeerp Oct 18, 2026
3193cfe
L182: word messenger invite results as v83 does
neeerp Oct 18, 2026
9505e4a
L182: refuse item use and drops the map forbids
neeerp Oct 18, 2026
f72744e
L188: refuse item use and drops the map forbids on the server
neeerp Oct 18, 2026
08bd56a
L182: show invitations as v83's fading cards and answer busy party in…
neeerp Oct 18, 2026
c35fa48
L187: charge Shadow Meso's mesos and roll its damage as v83 does
neeerp Oct 18, 2026
d9261d6
L181: open v83's menu on another character
neeerp Oct 18, 2026
92a3731
L182: v83's buddy flags, arrival cards and request checks
neeerp Oct 18, 2026
7f0ba95
L182: v83 quest conversation sequencing
neeerp Oct 18, 2026
9ecf19b
L182: v83 NPC quest menu groups and lines
neeerp Oct 18, 2026
a954b3c
L182: talk to NPCs as v83 decides
neeerp Oct 18, 2026
e87a42f
L182: read the NPC dialog's speaker byte as v83 does
neeerp Oct 18, 2026
ff40365
L182: draw #e text in bold until #n
neeerp Oct 18, 2026
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
22 changes: 20 additions & 2 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_test(NAME chat_wrap COMMAND chat_wrap_tests)
add_executable(curse_words_tests tests/curse_words.cpp)
add_test(NAME curse_words COMMAND curse_words_tests)
add_executable(character_name_tests tests/character_name.cpp)
add_test(NAME character_name COMMAND character_name_tests)
add_executable(quest_result_tests tests/quest_result.cpp)
add_test(NAME quest_result COMMAND quest_result_tests)
add_executable(map_tooltip_tests tests/map_tooltip.cpp)
add_test(NAME map_tooltip COMMAND map_tooltip_tests)
add_executable(cash_catalog_tests tests/cash_catalog.cpp)
Expand Down Expand Up @@ -93,6 +97,8 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_test(NAME buff_row COMMAND buff_row_tests)
add_executable(item_drop_tests tests/item_drop.cpp)
add_test(NAME item_drop COMMAND item_drop_tests)
add_executable(field_limit_tests tests/field_limit.cpp)
add_test(NAME field_limit COMMAND field_limit_tests)
add_executable(scroll_rules_tests tests/scroll_rules.cpp)
add_test(NAME scroll_rules COMMAND scroll_rules_tests)
add_executable(pickup_tests tests/pickup.cpp)
Expand Down Expand Up @@ -191,6 +197,9 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_executable(char_click_tests tests/char_click.cpp)
target_include_directories(char_click_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME char_click COMMAND char_click_tests)
add_executable(context_menu_tests tests/context_menu.cpp)
target_include_directories(context_menu_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME context_menu COMMAND context_menu_tests)
add_executable(excl_request_tests tests/excl_request.cpp)
add_test(NAME excl_request COMMAND excl_request_tests)
add_executable(portal_touch_tests tests/portal_touch.cpp Gameplay/MapleMap/Portal.cpp)
Expand All @@ -208,6 +217,8 @@ if(HEAVENCLIENT_BUILD_TESTS)
SUPPORTED_QUESTS="${PROJECT_SOURCE_DIR}/../game-data/supported_quests.txt"
DEFAULT_QUEST_NX="${PROJECT_SOURCE_DIR}/../assets/game-data/Quest.nx")
add_test(NAME quest_catalog COMMAND quest_catalog_tests)
add_executable(skill_weapon_tests tests/skill_weapon.cpp)
add_test(NAME skill_weapon COMMAND skill_weapon_tests)
endif()

include_directories(.)
Expand Down Expand Up @@ -313,6 +324,7 @@ add_executable(HeavenClient
Character/QuestInfo.h
Character/QuestLog.cpp
Character/QuestLog.h
Character/QuestResult.h
Character/SkillBook.cpp
Character/SkillBook.h
Character/SkillId.h
Expand Down Expand Up @@ -468,6 +480,7 @@ add_executable(HeavenClient
IO/Components/SliderSteps.h
IO/Components/EquipTooltip.cpp
IO/Components/EquipTooltip.h
IO/Components/ContextMenuLayout.h
IO/Components/FadeNotice.h
IO/Components/Gauge.cpp
IO/Components/Gauge.h
Expand Down Expand Up @@ -523,6 +536,8 @@ add_executable(HeavenClient
IO/UITypes/UIGameOption.h
IO/UITypes/UICharInfo.cpp
IO/UITypes/UICharInfo.h
IO/UITypes/UIContextMenu.cpp
IO/UITypes/UIContextMenu.h
IO/UITypes/UICharCreation.cpp
IO/UITypes/UICharCreation.h
IO/UITypes/UICharSelect.cpp
Expand Down Expand Up @@ -567,8 +582,8 @@ add_executable(HeavenClient
IO/UITypes/UIShop.h
IO/UITypes/UIStorage.cpp
IO/UITypes/UIStorage.h
IO/UITypes/UITradeInvite.cpp
IO/UITypes/UITradeInvite.h
IO/UITypes/UIInviteCard.cpp
IO/UITypes/UIInviteCard.h
IO/UITypes/UITradingRoom.cpp
IO/UITypes/UITradingRoom.h
IO/UITypes/UIMonsterBook.cpp
Expand Down Expand Up @@ -602,6 +617,8 @@ add_executable(HeavenClient
IO/KeyType.h
IO/Messages.cpp
IO/Messages.h
IO/BuddyCommands.cpp
IO/BuddyCommands.h
IO/PartyCommands.cpp
IO/PartyCommands.h
IO/UI.cpp
Expand Down Expand Up @@ -710,6 +727,7 @@ add_executable(HeavenClient
Template/Singleton.h
Template/TimedQueue.h
Template/TypeMap.h
Util/CharacterName.h
Util/ChatWrap.h
Util/CurseWords.h
Util/HardwareInfo.h
Expand Down
93 changes: 86 additions & 7 deletions HeavenClient/Character/BuddyList.cpp
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
#include "BuddyList.h"

#include <algorithm>
#include <cctype>
#include <utility>

namespace ms
{
namespace
{
bool same_name(const std::string& a, const std::string& b)
{
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
[](unsigned char x, unsigned char y) { return std::tolower(x) == std::tolower(y); });
}

// The flags whose online state follows the channel.
bool by_channel(uint8_t flag)
{
return flag == 0 || flag == 3 || flag == 4;
}
}

bool Buddy::pending() const
{
return flag == 1;
}

bool Buddy::online() const
{
return channel >= 0;
return flag != 2 && !(by_channel(flag) && channel < 0);
}

bool Buddy::hears_chat() const
{
return id != 0 && by_channel(flag) && channel >= 0;
}

bool Buddy::operator==(const Buddy& other) const
{
return id == other.id && name == other.name && group == other.group && channel == other.channel;
return id == other.id && name == other.name && group == other.group && channel == other.channel
&& flag == other.flag && in_cash_shop == other.in_cash_shop;
}

const std::vector<Buddy>& BuddyList::get_buddies() const
Expand All @@ -28,9 +56,23 @@ namespace ms
return nullptr;
}

const Buddy* BuddyList::find_listed(const std::string& name) const
{
for (auto& buddy : buddies)
if (!buddy.pending() && same_name(buddy.name, name))
return &buddy;

return nullptr;
}

size_t BuddyList::listed() const
{
return std::count_if(buddies.begin(), buddies.end(), [](const Buddy& buddy) { return !buddy.pending(); });
}

bool BuddyList::is_full() const
{
return buddies.size() >= capacity;
return listed() >= capacity;
}

uint8_t BuddyList::get_capacity() const
Expand All @@ -43,7 +85,7 @@ namespace ms
std::vector<int32_t> result;

for (auto& buddy : buddies)
if (buddy.online())
if (buddy.hears_chat())
result.push_back(buddy.id);

return result;
Expand All @@ -54,11 +96,48 @@ namespace ms
buddies = std::move(new_buddies);
}

void BuddyList::set_channel(int32_t cid, int32_t channel)
void BuddyList::add(Buddy buddy)
{
buddies.push_back(std::move(buddy));
}

void BuddyList::replace(int32_t cid, Buddy buddy)
{
for (auto& old : buddies)
if (old.id == cid)
{
old = std::move(buddy);
return;
}
}

// Coming online counts first; otherwise only a move onto the player's
// channel is told.
BuddyList::Arrival BuddyList::set_channel(int32_t cid, int32_t channel, bool in_cash_shop, int32_t own_channel)
{
for (auto& buddy : buddies)
if (buddy.id == cid)
buddy.channel = channel;
{
if (buddy.id != cid)
continue;

int32_t old = buddy.channel;

if (old == channel && buddy.in_cash_shop == in_cash_shop)
return Arrival::NONE;

buddy.channel = channel;
buddy.in_cash_shop = in_cash_shop;

if (old < 0 && channel >= 0)
return Arrival::LOGGED_IN;

if (old != channel && channel == own_channel)
return Arrival::SAME_CHANNEL;

return Arrival::NONE;
}

return Arrival::NONE;
}

void BuddyList::set_capacity(uint8_t new_capacity)
Expand Down
27 changes: 25 additions & 2 deletions HeavenClient/Character/BuddyList.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ namespace ms
// Zero-based channel, or negative while offline or until they add
// the player back.
int32_t channel = -1;
// v83's GW_Friend flag: 1 while they ask to be added and the player
// hasn't answered, 2 for someone always shown offline.
uint8_t flag = 0;
bool in_cash_shop = false;

// A request, which v83 keeps in the list but never shows or counts.
bool pending() const;
// v83's online test (FUN_00908fc4): flags 0, 3 and 4 go by the
// channel, 2 is always offline, and the rest always online.
bool online() const;
// Whether buddy chat goes to them (FUN_00a3fb25).
bool hears_chat() const;
bool operator==(const Buddy& other) const;
};

Expand All @@ -25,17 +35,30 @@ namespace ms
public:
static constexpr uint8_t DEFAULT_CAPACITY = 20;

// Buddies in the server's order (by name).
// What a channel update tells the player (FUN_00a3f2e8, mode 0x14).
enum class Arrival { NONE, LOGGED_IN, SAME_CHANNEL };

// Buddies in the server's order (by name), requests included.
const std::vector<Buddy>& get_buddies() const;
const Buddy* find(int32_t cid) const;
// A buddy who isn't a request, by name regardless of case
// (FUN_00a3fa90).
const Buddy* find_listed(const std::string& name) const;
// How many buddies there are, not counting requests.
size_t listed() const;
bool is_full() const;
uint8_t get_capacity() const;

// Buddies who would hear buddy chat.
std::vector<int32_t> online_ids() const;

void update(std::vector<Buddy> buddies);
void set_channel(int32_t cid, int32_t channel);
// A request's entry, which v83 adds as it comes (FUN_00a40028).
void add(Buddy buddy);
// Overwrites the entry for cid, if there is one (FUN_00a400ad).
void replace(int32_t cid, Buddy buddy);
// own_channel: the player's, zero-based.
Arrival set_channel(int32_t cid, int32_t channel, bool in_cash_shop, int32_t own_channel);
void set_capacity(uint8_t capacity);

private:
Expand Down
12 changes: 12 additions & 0 deletions HeavenClient/Character/Guild.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ namespace ms
return nullptr;
}

const GuildMember* Guild::find_by_name(const std::string& name) const
{
if (!exists())
return nullptr;

for (auto& member : data.members)
if (member.name == name)
return &member;

return nullptr;
}

bool Guild::is_master(int32_t cid) const
{
return exists() && !data.members.empty() && data.members.front().id == cid;
Expand Down
2 changes: 2 additions & 0 deletions HeavenClient/Character/Guild.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ namespace ms
bool exists() const;
const GuildData& get_data() const;
const GuildMember* find(int32_t cid) const;
// A member by name, matching case (FUN_00a39282).
const GuildMember* find_by_name(const std::string& name) const;
// The client takes the first member for the master.
bool is_master(int32_t cid) const;
// A rank's title, or "" for none.
Expand Down
30 changes: 29 additions & 1 deletion HeavenClient/Character/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
#include "Player.h"

#include "PlayerStates.h"
#include "SkillId.h"

#include "../Audio/Audio.h"
#include "../Data/EquipData.h"
#include "../Data/SkillData.h"
#include "../Data/WeaponData.h"
#include "../Gameplay/Combat/MagicDamage.h"
#include "../Gameplay/Stage.h"
#include "../IO/Messages.h"
#include "../IO/UI.h"

#include "../IO/UITypes/UIRevive.h"
Expand Down Expand Up @@ -205,6 +207,8 @@ namespace ms
{
if (type == InventoryType::Id::USE && (itemid / 10000 == 228 || itemid / 10000 == 229))
request(SkillBookPacket(slot, itemid), ExclRequest::USE_WAIT);
else if (type == InventoryType::Id::USE && !Stage::get().get_mapinfo().can_use_item(itemid))
InChatMessage(Messages::ITEM_FIELD).drop();
else if (type == InventoryType::Id::USE && disease::can_use_item(get_diseases()))
request(UseItemPacket(slot, itemid), ExclRequest::USE_WAIT);
else if (type == InventoryType::Id::SETUP && itemid / 10000 == 301)
Expand Down Expand Up @@ -607,7 +611,31 @@ namespace ms
uint16_t mp = stats.get_stat(MapleStat::Id::MP);
uint16_t bullets = inventory.get_most_bullets();

return move.can_use(level, weapon, job, hp, mp, bullets);
SpecialMove::ForbidReason reason = move.can_use(level, weapon, job, hp, mp, bullets);

// v83 checks the skill's mesos and item right after its HP and MP,
// before the weapon (FUN_00764256).
if (!move.is_skill() || reason == SpecialMove::ForbidReason::FBR_OTHER ||
reason == SpecialMove::ForbidReason::FBR_HPCOST || reason == SpecialMove::ForbidReason::FBR_MPCOST)
return reason;

const SkillData::Stats &cost = SkillData::get(move.get_id()).get_stats(level);
int32_t mesos = cost.mesocost;

// Shadow Meso costs more while Shadow Partner is on.
if (move.get_id() == SkillId::SHADOW_MESO && has_buff(Buffstat::Id::SHADOWPARTNER))
{
int32_t partner = skillbook.get_level(SkillId::SHADOW_PARTNER);
mesos += SkillData::get(SkillId::SHADOW_PARTNER).get_stats(partner).y * mesos / 100;
}

if (mesos > 0 && inventory.get_meso() < mesos)
return SpecialMove::ForbidReason::FBR_MESOCOST;

if (cost.itemcost > 0 && cost.itemcount > 0 && inventory.get_total_item_count(cost.itemcost) < cost.itemcount)
return SpecialMove::ForbidReason::FBR_ITEMCOST;

return reason;
}

Attack Player::prepare_attack(int32_t skill, bool whack) const
Expand Down
Loading
Loading