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
93 changes: 93 additions & 0 deletions HeavenClient/Character/Family.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "Family.h"

#include <algorithm>
#include <unordered_map>
#include <utility>

Expand Down Expand Up @@ -57,6 +58,98 @@ namespace ms
return node->level > nodes[node->senior].level;
}

namespace
{
// StringPool 0x146d-0x1470 and the fee, 0x146c.
constexpr const char* JUNIOR_BOTH = "%d Reputation Points for yourself and %d Reputation Points for your Senior will be deducted\\nand all lower Family members will be affected.\\nDo you really want to separate from the selected character?";
constexpr const char* JUNIOR_ONE = "%d Reputation Points will be deducted from you, \\nand all lower Family members will be affected.\\nDo you really want to separate from the selected character?";
constexpr const char* SENIOR_BOTH = "%d Reputation Points for the Senior and %d Reputation Points for the 2nd Senior will be deducted, and all lower Family members will be affected.\\nDo you really want to separate from the selected character?";
constexpr const char* SENIOR_ONE = "%d Reputations for the Senior will be deducted, and\\nall lower Family members will be affected. Do you really want to separate from the selected character?";
constexpr const char* FEE = "\\n\\nFee:%d Mesos";

std::string format(std::string text, std::initializer_list<int32_t> values)
{
for (int32_t value : values)
{
size_t at = text.find("%d");

if (at != std::string::npos)
text.replace(at, 2, std::to_string(value));
}

return text;
}
}

std::string FamilyParting::text() const
{
std::string result = second_rep > 0
? format(junior ? JUNIOR_BOTH : SENIOR_BOTH, { rep, second_rep })
: format(junior ? JUNIOR_ONE : SENIOR_ONE, { rep });

if (fee > 0)
result += format(FEE, { fee });

return result;
}

FamilyParting FamilyChart::parting(const FamilyNode& other, bool junior) const
{
const FamilyNode& owner = *at(OWNER);
const FamilyNode& senior = junior ? owner : other;
const FamilyNode& below = junior ? other : owner;

// The junior's level sets the Rep, the levels between the fee.
int32_t level = below.level;
int32_t rep = (level / 20 + 10) * level;
int32_t gap = std::max(0, senior.level - below.level);

FamilyParting result;
result.id = other.id;
result.name = other.name;
result.junior = junior;
result.rep = 2 * rep;
result.second_rep = senior.senior >= 0 ? rep : 0;
result.fee = (gap + 2500) * gap;

return result;
}

std::optional<FamilyParting> FamilyChart::parting_at(size_t slot) const
{
const FamilyNode* owner = at(OWNER);
const FamilyNode* other = at(slot);

if (!mine || !owner || !other)
return std::nullopt;

if (owner->senior >= 0 && nodes[owner->senior].id == other->id)
return parting(*other, false);

for (int32_t junior : owner->juniors)
if (nodes[junior].id == other->id)
return parting(*other, true);

return std::nullopt;
}

std::optional<FamilyParting> FamilyChart::parting_named(const std::string& name) const
{
const FamilyNode* owner = at(OWNER);

if (!mine || !owner)
return std::nullopt;

if (owner->senior >= 0 && nodes[owner->senior].name == name)
return parting(nodes[owner->senior], false);

for (int32_t junior : owner->juniors)
if (nodes[junior].name == name)
return parting(nodes[junior], true);

return std::nullopt;
}

void FamilyChart::arrange(const FamilyNode& self)
{
slots.fill(-1);
Expand Down
26 changes: 26 additions & 0 deletions HeavenClient/Character/Family.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <array>
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -62,6 +63,24 @@ namespace ms
std::vector<int32_t> juniors;
};

// Parting the owner of the player's own tree from their senior or one
// of their juniors (BtBye, report b048).
struct FamilyParting
{
int32_t id = 0;
std::string name;
// A junior (SEPARATE_FAMILY_BY_SENIOR) or the senior (BY_JUNIOR).
bool junior = false;
// The Rep the confirmation says each side loses, the second 0 when
// there is no one above, and the fee in mesos.
int32_t rep = 0;
int32_t second_rep = 0;
int32_t fee = 0;

// The confirmation (0x80b385, 0x80b5d2).
std::string text() const;
};

// A Family Tree (FAMILY_CHART_RESULT), laid out as the client does
// (0x80a5f7): eleven boxes, four generations around the one viewed.
struct FamilyChart
Expand Down Expand Up @@ -91,12 +110,19 @@ namespace ms
// Whether a member's name shows red: a junior of the player's tree
// who passed their senior's level.
bool passed_senior(size_t slot) const;
// Parting from the member in a box, or the one named (senior
// first), when they are the owner's senior or junior.
std::optional<FamilyParting> parting_at(size_t slot) const;
std::optional<FamilyParting> parting_named(const std::string& name) const;

// Links the members as the client does: the first is the leader at
// the top and nobody's senior, the one viewed is the owner, and the
// others hang from their seniors. Without the owner in the packet,
// the player's own tree gets one from `self`.
void arrange(const FamilyNode& self);

private:
FamilyParting parting(const FamilyNode& other, bool junior) const;
};

// The player's family, as the server last described it.
Expand Down
43 changes: 43 additions & 0 deletions HeavenClient/IO/FamilyActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,19 @@ namespace ms
// StringPool 0x11e9 and 0x11f5.
constexpr const char* ADD_PROMPT = "Enter the name of the character you wish to add as Junior. You must be in the same channel as well as map to add.";
constexpr const char* CANNOT_ADD = "You cannot add this character as a Junior.";
// 0x11ea.
constexpr const char* PART_PROMPT = "Enter the name of the character you wish to delete.";
// A character's name.
constexpr size_t NAME_LIMIT = 12;

// v83 keeps it on yes and never clears it (ctx+0x3858).
std::string parted;

void deselect_tree()
{
if (auto tree = UI::get().get_element<UIFamilyTree>())
tree->deselect();
}
}

void toggle_window()
Expand Down Expand Up @@ -63,6 +74,38 @@ namespace ms
AddFamilyPacket(name).dispatch();
}

void prompt_parting(const FamilyChart& chart)
{
UI::get().emplace<UIEnterText>(PART_PROMPT, [chart](const std::string& name)
{
if (auto parting = chart.parting_named(name))
confirm_parting(*parting);
}, NAME_LIMIT);
}

void confirm_parting(const FamilyParting& parting)
{
UI::get().emplace<UIYesNo>(parting.text(), [parting](bool yes)
{
deselect_tree();

if (!yes || !Stage::get().get_player().get_family().loaded())
return;

if (parting.junior)
SeparateFamilyBySeniorPacket(parting.id).dispatch();
else
SeparateFamilyByJuniorPacket().dispatch();

parted = parting.name;
});
}

const std::string& parted_from()
{
return parted;
}

bool can_add_junior()
{
const Family& family = Stage::get().get_player().get_family();
Expand Down
10 changes: 10 additions & 0 deletions HeavenClient/IO/FamilyActions.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "../Character/Family.h"

#include <string>

namespace ms
Expand All @@ -22,6 +24,14 @@ namespace ms
// Whether the avatar menu has its Family row (0x9504ae): the
// family is known and has room below the player.
bool can_add_junior();
// BtBye without a box selected (0x80b7ee): a name, matched to the
// owner's senior and then juniors; nothing when none is.
void prompt_parting(const FamilyChart& chart);
// The one Yes/No, and on yes the send (0x80b385, 0x80b5d2). The tree
// is deselected either way.
void confirm_parting(const FamilyParting& parting);
// Whom the player last parted from, for FAMILY_RESULT 1.
const std::string& parted_from();
// A family text in the chat log, as v83's type 0xc.
void chat_line(const std::string& text);
}
Expand Down
39 changes: 36 additions & 3 deletions HeavenClient/IO/UITypes/UIFamilyTree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,23 @@ namespace ms
return chart.mine;
}

void UIFamilyTree::deselect()
{
selected = -1;
}

void UIFamilyTree::bye()
{
if (!chart.mine)
return;

if (selected < 0)
return FamilyActions::prompt_parting(chart);

if (auto parting = chart.parting_at(static_cast<size_t>(selected)))
FamilyActions::confirm_parting(*parting);
}

void UIFamilyTree::lay_out()
{
const FamilyNode* leader = chart.at(FamilyChart::LEADER);
Expand Down Expand Up @@ -187,9 +204,8 @@ namespace ms
}

buttons[JUNIOR]->set_state(chart.can_add_junior() ? Button::State::NORMAL : Button::State::DISABLED);
// Severing ties comes with the separation packets; until then the
// button stays off.
buttons[BYE]->set_state(Button::State::DISABLED);
// On whatever is selected, in the player's own tree only.
buttons[BYE]->set_state(chart.mine ? Button::State::NORMAL : Button::State::DISABLED);
}

void UIFamilyTree::draw(float inter) const
Expand Down Expand Up @@ -405,10 +421,27 @@ namespace ms
}
}

// Only a box that lights up, with someone in it, and nothing else.
void UIFamilyTree::doubleclick(Point<int16_t> cursorpos)
{
if (!chart.mine)
return;

int16_t slot = slot_at(cursorpos - position);

if (!hoverable(slot) || !chart.at(slot))
return;

selected = slot;
bye();
}

Button::State UIFamilyTree::button_pressed(uint16_t buttonid)
{
if (buttonid == JUNIOR)
FamilyActions::prompt_junior();
else if (buttonid == BYE)
bye();

return Button::State::NORMAL;
}
Expand Down
5 changes: 5 additions & 0 deletions HeavenClient/IO/UITypes/UIFamilyTree.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace ms
void draw(float inter) const override;

Cursor::State send_cursor(bool clicked, Point<int16_t> cursorpos) override;
void doubleclick(Point<int16_t> cursorpos) override;
void remove_cursor() override;
void send_key(int32_t keycode, bool pressed, bool escape) override;

Expand All @@ -37,6 +38,8 @@ namespace ms
// A relative logged in or out (FAMILY_NOTIFY_LOGIN_OR_LOGOUT).
void set_online(const std::string& name, bool online);
bool is_mine() const;
// The parting asked about is answered, either way.
void deselect();

protected:
Button::State button_pressed(uint16_t buttonid) override;
Expand All @@ -49,6 +52,8 @@ namespace ms
bool hoverable(int16_t slot) const;
std::string member_tooltip(size_t slot) const;
void clear_tooltip();
// BtBye (0x80b7cc): the box selected, or a name asked for.
void bye();

FamilyChart chart;

Expand Down
2 changes: 1 addition & 1 deletion HeavenClient/IO/UITypes/UIInviteCard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ namespace ms
{ "icon5", "Party invite", Point<int16_t>(26, 4) },
{ "icon5", "Guild invite", Point<int16_t>(26, 4), true },
{ "icon5", "Guild Alliance Invitation.", Point<int16_t>(26, 4), true, true },
// v83 writes it as the alliance's (FUN_00524298).
// "from '<name>'" first, as the guild's (0x5221fa, report b047).
{ "icon8", "It's a Family Invitation.", Point<int16_t>(26, 4), true }
};

Expand Down
17 changes: 15 additions & 2 deletions HeavenClient/Net/Handlers/FamilyHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ namespace ms
{
// FAMILY_RESULT's codes (0xa345fb, report b039 1.3) with their
// StringPool texts. 0x4b and 0x4c go to the chat log; the others
// are OK dialogs. Code 1, the end of a relationship the player
// severed, comes with separation.
// are OK dialogs. Code 1 (0x120a) names whom the player parted
// from, by the name kept when they said yes (report b048).
const char* result_text(int32_t code)
{
switch (code)
{
case 1: return "You have severed ties with %s.\\nYour family relationship has ended.";
case 0x40: return "You cannot add this character as a Junior.";
case 0x41: return "The name you entered is incorrect or he/she is currently not logged in.";
case 0x42: return "You belong to the same family.";
Expand All @@ -49,6 +50,16 @@ namespace ms
}
}

std::string with_name(std::string text, const std::string& name)
{
size_t at = text.find("%s");

if (at != std::string::npos)
text.replace(at, 2, name);

return text;
}

std::string with_number(std::string text, int32_t value)
{
size_t at = text.find("%d");
Expand Down Expand Up @@ -127,6 +138,8 @@ namespace ms

if (code == 0x4b || code == 0x4c)
FamilyActions::chat_line(text);
else if (code == 1)
ok(with_name(text, FamilyActions::parted_from()));
else
ok(with_number(text, arg));
}
Expand Down
2 changes: 2 additions & 0 deletions HeavenClient/Net/OutPacket.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ namespace ms
OPEN_FAMILY_PEDIGREE = 145,
OPEN_FAMILY = 146,
ADD_FAMILY = 147,
SEPARATE_FAMILY_BY_SENIOR = 148,
SEPARATE_FAMILY_BY_JUNIOR = 149,
ACCEPT_FAMILY = 150,
USE_FAMILY = 151,
CHANGE_FAMILY_MESSAGE = 152,
Expand Down
Loading
Loading