Skip to content
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This repository is a Rust workspace for a MapleStory server plus a nested C++ cl
- `agent_docs/combat.md`: mob spawning and control, melee attacks, EXP and level-ups, spending AP and SP, drops and pickup, touch damage and death
- `agent_docs/parties.md`: party state and flows, EXP sharing, party chat, and the client's party window and HP displays
- `agent_docs/buddies.md`: buddy list storage, requests, channel visibility, buddy chat, and the client's Friend tab
- `agent_docs/guilds.md`: guild storage, founding at Heracle, guild chat, name tags, and the client's Guild tab
- `agent_docs/guilds.md`: guild storage, founding at Heracle, invitations, leaving and expelling, guild chat, name tags, and the client's Guild tab
- `agent_docs/inventory.md`: inventory operations, item data, item moves, NPC shops, storage, and their packets
- `agent_docs/server-content.md`: the ignored content pack (script text, drops), placeholders, and the importer
- `agent_docs/client-testing.md`: fast client logic tests and private-display GUI verification
Expand Down
11 changes: 11 additions & 0 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_executable(mini_room_tests tests/mini_room.cpp Net/InPacket.cpp)
target_include_directories(mini_room_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME mini_room COMMAND mini_room_tests)
add_executable(personal_shop_tests tests/personal_shop.cpp Net/InPacket.cpp)
target_include_directories(personal_shop_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME personal_shop COMMAND personal_shop_tests)
add_executable(movement_path_tests tests/movement_path.cpp Net/InPacket.cpp Net/Handlers/Helpers/MovementParser.cpp)
target_include_directories(movement_path_tests PRIVATE ${PROJECT_SOURCE_DIR}/libs/NoLifeNx)
add_test(NAME movement_path COMMAND movement_path_tests)
Expand Down Expand Up @@ -309,6 +312,9 @@ add_executable(HeavenClient
Character/MonsterBook.h
Character/Party.cpp
Character/Party.h
Character/MiniRoomBalloon.cpp
Character/MiniRoomBalloon.h
Character/MiniRoomBox.h
Character/OtherChar.cpp
Character/OtherChar.h
Character/PassiveBuffs.cpp
Expand Down Expand Up @@ -586,6 +592,8 @@ add_executable(HeavenClient
IO/UITypes/UIInviteCard.h
IO/UITypes/UITradingRoom.cpp
IO/UITypes/UITradingRoom.h
IO/UITypes/UIPersonalShop.cpp
IO/UITypes/UIPersonalShop.h
IO/UITypes/UIMonsterBook.cpp
IO/UITypes/UIMonsterBook.h
IO/UITypes/UISkillBook.cpp
Expand Down Expand Up @@ -621,6 +629,9 @@ add_executable(HeavenClient
IO/BuddyCommands.h
IO/PartyCommands.cpp
IO/PartyCommands.h
IO/GuildCommands.cpp
IO/GuildCommands.h
IO/ChatCommand.h
IO/UI.cpp
IO/UI.h
IO/UIDragElement.h
Expand Down
17 changes: 17 additions & 0 deletions HeavenClient/Character/Char.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ namespace ms
// (FUN_005f0334). Not yet checked against period footage.
constexpr int16_t GUILD_TAG_GAP = 1;
constexpr Point<int16_t> EMBLEM_OFFSET = Point<int16_t>(-GuildMark::SIZE - 3, 6);

// v83 hangs a room's bubble over the avatar's drawn height, hat and
// all (FUN_0048e85a); this client doesn't measure it, so it takes a
// bare head's.
constexpr int16_t AVATAR_HEIGHT = 70;
}

Char::Char(int32_t o, const CharLook &lk, const std::string &name) : MapObject(o), look(lk), look_preview(lk), namelabel(
Expand Down Expand Up @@ -114,6 +119,8 @@ namespace ms

chatballoon.draw(absp - Point<int16_t>(0, 85));
hintballoon.draw(absp - Point<int16_t>(0, 85));
// v83 puts a room's balloon in front of the chat balloon.
miniroom.draw(absp, AVATAR_HEIGHT);

effects.drawabove(absp, alpha);
held_effects.drawabove(absp, alpha);
Expand Down Expand Up @@ -648,6 +655,16 @@ namespace ms
guildmark = GuildMark(emblem);
}

void Char::set_mini_room(const std::optional<MiniRoomBox> &box)
{
miniroom.change(box);
}

int32_t Char::mini_room_at(Point<int16_t> feet, Point<int16_t> cursor) const
{
return miniroom.serial_at(feet, AVATAR_HEIGHT, cursor);
}

CharLook &Char::get_look()
{
return look;
Expand Down
10 changes: 10 additions & 0 deletions HeavenClient/Character/Char.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

#include "CharEffect.h"
#include "GuildMark.h"
#include "MiniRoomBalloon.h"

#include "Inventory/Weapon.h"
#include "Look/Afterimage.h"
Expand Down Expand Up @@ -217,6 +218,14 @@ namespace ms
void set_guild_name(const std::string &name);
void set_guild_emblem(const GuildEmblem &emblem);

// Shows the balloon of the room the character keeps open, or takes
// it down.
void set_mini_room(const std::optional<MiniRoomBox> &box);

// The room whose balloon is at `cursor`, or 0, with the feet at `feet`
// on the screen.
int32_t mini_room_at(Point<int16_t> feet, Point<int16_t> cursor) const;

// Return if the char is in the Char::State::SIT state
bool is_sitting() const;

Expand Down Expand Up @@ -287,6 +296,7 @@ namespace ms
GuildMark guildmark;
ChatBalloon chatballoon;
ChatBalloon hintballoon;
MiniRoomBalloon miniroom;
EffectLayer effects;
EffectLayer held_effects;
Afterimage afterimage;
Expand Down
42 changes: 42 additions & 0 deletions HeavenClient/Character/Guild.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,33 @@ namespace ms
return exists() && !data.members.empty() && data.members.front().id == cid;
}

int32_t Guild::rank(int32_t cid) const
{
const GuildMember* member = find(cid);

return member ? member->rank : 0;
}

bool Guild::full() const
{
return data.members.size() >= static_cast<size_t>(data.capacity);
}

bool Guild::may_kick(int32_t self, int32_t picked)
{
switch (picked)
{
case 2:
return self == 1;
case 3:
case 4:
case 5:
return self == 1 || self == 2;
default:
return false;
}
}

const std::string& Guild::title(int32_t rank) const
{
static const std::string none;
Expand Down Expand Up @@ -134,4 +161,19 @@ namespace ms
{
data = GuildData();
}

void Guild::add(GuildMember member)
{
remove(member.id);
data.members.push_back(std::move(member));
}

void Guild::remove(int32_t cid)
{
auto& members = data.members;
members.erase(std::remove_if(members.begin(), members.end(), [cid](const GuildMember& member)
{
return member.id == cid;
}), members.end());
}
}
11 changes: 11 additions & 0 deletions HeavenClient/Character/Guild.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,25 @@ namespace ms
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 member's rank, or 0 for someone outside the guild.
int32_t rank(int32_t cid) const;
bool full() const;
// A rank's title, or "" for none.
const std::string& title(int32_t rank) const;
size_t online_count() const;
// Members who would hear guild chat: those online but the player.
std::vector<int32_t> online_ids(int32_t self) const;

// Whether a member of rank `self` may expel one of rank `picked`,
// 0 for none, by the guild window's rule (FUN_0090f17c): the master
// expels anyone else, a Jr. Master only those ranked below.
static bool may_kick(int32_t self, int32_t picked);

void update(GuildData data);
void clear();
// Someone joined, or left or was expelled.
void add(GuildMember member);
void remove(int32_t cid);

private:
GuildData data;
Expand Down
104 changes: 104 additions & 0 deletions HeavenClient/Character/MiniRoomBalloon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include "MiniRoomBalloon.h"

#include "../Util/Misc.h"
#include "../Util/NxFiles.h"

#include <nlnx/nx.hpp>

namespace ms
{
namespace
{
constexpr int8_t PERSONAL_SHOP = 4;

// The title wraps once at 100 and the rest is cut short
// (FUN_0048dcbd); the seat sign sits in the white box.
constexpr uint16_t TITLE_WIDTH = 100;
constexpr Point<int16_t> FIRST_LINE = Point<int16_t>(8, 8);
constexpr Point<int16_t> SECOND_LINE = Point<int16_t>(8, 22);
constexpr Point<int16_t> SEATS = Point<int16_t>(60, 39);
// v83 draws a line's cap top about 3 below where it is drawn, we 9.
constexpr Point<int16_t> TEXT_Y = Point<int16_t>(0, -6);

// A bubble floats this far over the head; a signboard stands 2 below
// the feet (FUN_0048e85a).
constexpr int16_t BUBBLE_GAP = 5;
constexpr int16_t SIGN_DROP = 2;
}

MiniRoomBalloon::MiniRoomBalloon() : shown(false), serial(0), signboard(false)
{}

void MiniRoomBalloon::change(const std::optional<MiniRoomBox> &box)
{
// Only shops have balloons here; games have none yet.
shown = box && box->type == PERSONAL_SHOP;

if (!shown)
return;

nl::node miniroom = NxFiles::ui_classic["ChatBalloon.img"]["miniroom"];

serial = box->serial;
signboard = box->skin != 0;
skin = miniroom["PSSkin"][std::to_string(box->skin)];

if (!skin.is_valid())
{
signboard = false;
skin = miniroom["PSSkin"]["0"];
}

seats = miniroom[box->users < box->max_users ? "Able" : "Disable"];

// v83 breaks the title where the first line runs out, not at a word.
Text measure = Text(Text::Font::A12M, Text::Alignment::LEFT, Color::Name::BLACK);
size_t fits = 0;

while (fits < box->title.size())
{
measure.change_text(box->title.substr(0, fits + 1));

if (measure.width() > TITLE_WIDTH)
break;

fits++;
}

first = Text(Text::Font::A12M, Text::Alignment::LEFT, Color::Name::BLACK, box->title.substr(0, fits), 0, false);
second = Text(Text::Font::A12M, Text::Alignment::LEFT, Color::Name::BLACK, box->title.substr(fits), 0, false);
string_format::format_with_ellipsis(second, TITLE_WIDTH);
}

void MiniRoomBalloon::draw(Point<int16_t> feet, int16_t avatar_height) const
{
if (!shown)
return;

Point<int16_t> top_left = bounds(feet, avatar_height).get_left_top();
// Text and the seat sign are placed from the skin's origin.
Point<int16_t> origin = top_left + skin.get_origin();

skin.draw(origin);
seats.draw(origin + SEATS);
first.draw(origin + FIRST_LINE + TEXT_Y);
second.draw(origin + SECOND_LINE + TEXT_Y);
}

int32_t MiniRoomBalloon::serial_at(Point<int16_t> feet, int16_t avatar_height, Point<int16_t> cursor) const
{
if (shown && bounds(feet, avatar_height).contains(cursor))
return serial;

return 0;
}

Rectangle<int16_t> MiniRoomBalloon::bounds(Point<int16_t> feet, int16_t avatar_height) const
{
Point<int16_t> size = skin.get_dimensions();
int16_t top = signboard ? SIGN_DROP - size.y() : -size.y() - avatar_height - BUBBLE_GAP;
Point<int16_t> top_left = feet + Point<int16_t>(-size.x() / 2, top);

return Rectangle<int16_t>(top_left, top_left + size);
}
}
42 changes: 42 additions & 0 deletions HeavenClient/Character/MiniRoomBalloon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#pragma once

#include "MiniRoomBox.h"

#include "../Graphics/Text.h"
#include "../Graphics/Texture.h"
#include "../Template/Rectangle.h"

#include <optional>

namespace ms
{
// The board over someone keeping a personal shop open (v83
// FUN_0048bbca): the permit's skin with the store's title and whether a
// seat is free. Double-clicking it visits the shop.
class MiniRoomBalloon
{
public:
MiniRoomBalloon();

// Shows `box`, or nothing.
void change(const std::optional<MiniRoomBox> &box);

void draw(Point<int16_t> feet, int16_t avatar_height) const;

// The room to visit when the balloon is clicked at `cursor`, or 0.
int32_t serial_at(Point<int16_t> feet, int16_t avatar_height, Point<int16_t> cursor) const;

private:
Rectangle<int16_t> bounds(Point<int16_t> feet, int16_t avatar_height) const;

bool shown;
int32_t serial;
// Skin 0 is a speech bubble over the head; the others are signboards
// standing on the ground.
bool signboard;
Texture skin;
Texture seats;
Text first;
Text second;
};
}
19 changes: 19 additions & 0 deletions HeavenClient/Character/MiniRoomBox.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <cstdint>
#include <string>

namespace ms
{
// The room a character keeps open, as the balloon over them shows it
// (UPDATE_CHAR_BOX and the player spawn, v83 FUN_0048bbca).
struct MiniRoomBox
{
int8_t type = 0;
int32_t serial = 0;
std::string title;
uint8_t skin = 0;
uint8_t users = 0;
uint8_t max_users = 0;
};
}
1 change: 1 addition & 0 deletions HeavenClient/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ namespace ms
settings.emplace<PosSHOP>();
settings.emplace<PosSTORAGE>();
settings.emplace<PosTRADE>();
settings.emplace<PosPERSONALSHOP>();
settings.emplace<PosNOTICE>();
settings.emplace<PosMESSENGER>();
settings.emplace<PosCHANNEL>();
Expand Down
7 changes: 7 additions & 0 deletions HeavenClient/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,13 @@ namespace ms
{}
};

// The default position of UIPersonalShop: centred, like the v83 dialog
struct PosPERSONALSHOP : public Configuration::PointEntry
{
PosPERSONALSHOP() : PointEntry("PosPERSONALSHOP", "(137, 23)")
{}
};

// The default position of UINotice
struct PosNOTICE : public Configuration::PointEntry
{
Expand Down
Loading
Loading