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
4 changes: 4 additions & 0 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_test(NAME equip_cash COMMAND equip_cash_tests)
add_executable(item_guard_tests tests/item_guard.cpp)
add_test(NAME item_guard COMMAND item_guard_tests)
add_executable(remote_gachapon_tests tests/remote_gachapon.cpp)
add_test(NAME remote_gachapon COMMAND remote_gachapon_tests)
add_executable(incubator_tests tests/incubator.cpp)
add_test(NAME incubator COMMAND incubator_tests)
add_executable(karma_scissors_tests tests/karma_scissors.cpp)
Expand Down Expand Up @@ -1033,6 +1035,8 @@ add_executable(HeavenClient
IO/UITypes/UIIncubator.h
IO/UITypes/UIKarmaScissors.cpp
IO/UITypes/UIKarmaScissors.h
IO/UITypes/UIRemoteGachapon.cpp
IO/UITypes/UIRemoteGachapon.h
IO/UITypes/UIVegaSpell.cpp
IO/UITypes/UIVegaSpell.h
IO/UITypes/UIEnchantSkill.cpp
Expand Down
38 changes: 38 additions & 0 deletions HeavenClient/Character/Inventory/RemoteGachapon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include <array>
#include <cstdint>

namespace ms
{
// The Remote Gachapon Ticket (5451xxx, v83 cash type 0x3b, report b196). Double-clicked in the Cash tab it opens
// a window of ten towns; a town's button sends the ticket with the button's index and closes the window, and the
// server answers as that town's machine.
namespace remote_gachapon
{
constexpr int32_t TICKET = 5451000;

// 0x48645b: class 545 is type 0x3b only for 5451xxx; Miu Miu (5450000) is type 0x25.
constexpr bool is_ticket(int32_t item_id)
{
return item_id / 1000 == 5451;
}

struct Place
{
int16_t x;
int16_t y;
};

// UIWindow.img/RemoteGachapon/Bt0..Bt9 from the window's top left (0x8111ac), in the order of the index
// they send (0x811a64): Henesys, Ellinia, Perion, Kerning City, Sleepywood, Mushroom Shrine, the Showa Spa's
// men's and women's, New Leaf City and Nautilus Harbor. The screen's order differs.
constexpr std::array<Place, 10> TOWNS = {{
{ 15, 113 }, { 128, 113 }, { 15, 134 }, { 128, 134 }, { 128, 155 },
{ 15, 176 }, { 15, 197 }, { 128, 197 }, { 128, 176 }, { 15, 155 }
}};

// Basic.img/BtClose.
constexpr Place CLOSE = { 227, 8 };
}
}
2 changes: 2 additions & 0 deletions HeavenClient/IO/UI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ namespace ms
types.emplace_back(UIElement::Type::VEGASPELL);
types.emplace_back(UIElement::Type::ITEMPROTECTOR);
types.emplace_back(UIElement::Type::KARMASCISSORS);
types.emplace_back(UIElement::Type::REMOTEGACHAPON);
types.emplace_back(UIElement::Type::INCUBATOR);
types.emplace_back(UIElement::Type::INVITATION);
types.emplace_back(UIElement::Type::VEGARESULT);
Expand Down Expand Up @@ -532,6 +533,7 @@ namespace ms
types.emplace_back(UIElement::Type::MAPLETV);
types.emplace_back(UIElement::Type::ITEMPROTECTOR);
types.emplace_back(UIElement::Type::KARMASCISSORS);
types.emplace_back(UIElement::Type::REMOTEGACHAPON);
types.emplace_back(UIElement::Type::INCUBATOR);
types.emplace_back(UIElement::Type::INVITATION);
types.emplace_back(UIElement::Type::MERCHANTVISITORS);
Expand Down
1 change: 1 addition & 0 deletions HeavenClient/IO/UIElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ namespace ms
VEGASPELL,
ITEMPROTECTOR,
KARMASCISSORS,
REMOTEGACHAPON,
VEGARESULT,
PETCHOOSER,
APRESET,
Expand Down
10 changes: 10 additions & 0 deletions HeavenClient/IO/UITypes/UIItemInventory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "UIItemProtector.h"
#include "UIIncubator.h"
#include "UIKarmaScissors.h"
#include "UIRemoteGachapon.h"
#include "UIMemoSend.h"
#include "UINameChange.h"
#include "UIKite.h"
Expand Down Expand Up @@ -59,6 +60,7 @@
#include "../../Character/Inventory/CashUpgrades.h"
#include "../../Character/Inventory/EquipCash.h"
#include "../../Character/Inventory/Incubator.h"
#include "../../Character/Inventory/RemoteGachapon.h"
#include "../../Character/Inventory/ItemDrop.h"
#include "../../Character/Inventory/ScrollRules.h"
#include "../../Data/CurseData.h"
Expand Down Expand Up @@ -1521,6 +1523,14 @@ namespace ms
if (may_use_cash_item() && !in_dialog())
UI::get().emplace<UIIncubator>(slot, item_id, InventoryType::Id::NONE, 0);
}
// A Remote Gachapon Ticket (type 0x3b) opens its
// window once the last request is answered
// (0x4efd25's silent wait, 0x485bf7).
else if (remote_gachapon::is_ticket(item_id))
{
if (Stage::get().get_player().can_request(ExclRequest::WAIT))
UI::get().emplace<UIRemoteGachapon>(item_id);
}
// A Water of Life (518xxxx) asks the server, which
// offers to revive a doll.
else if (item_id / 10000 == 518)
Expand Down
101 changes: 101 additions & 0 deletions HeavenClient/IO/UITypes/UIRemoteGachapon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include "UIRemoteGachapon.h"

#include "../Components/MapleButton.h"

#include "../../Character/Inventory/RemoteGachapon.h"
#include "../../Constants.h"
#include "../../Net/Packets/GameplayPackets.h"
#include "../../Util/NxFiles.h"

#include <nlnx/nx.hpp>

namespace ms
{
namespace
{
constexpr int16_t DRAG_HEIGHT = 25;

Point<int16_t> point(remote_gachapon::Place place)
{
return Point<int16_t>(place.x, place.y);
}
}

UIRemoteGachapon::UIRemoteGachapon(int32_t id)
: UIElement(Point<int16_t>(), Point<int16_t>()), item_id(id), dragged(false)
{
nl::node src = NxFiles::ui_classic["UIWindow.img"]["RemoteGachapon"];

Texture background = src["backgrnd"];
sprites.emplace_back(src["backgrnd"]);

for (uint16_t town = 0; town < remote_gachapon::TOWNS.size(); town++)
buttons[town] = std::make_unique<MapleButton>(src["Bt" + std::to_string(town)],
point(remote_gachapon::TOWNS[town]));

buttons[CLOSE] = std::make_unique<MapleButton>(NxFiles::ui_classic["Basic.img"]["BtClose"],
point(remote_gachapon::CLOSE));

// Centred, as v83 centres it in 800x600 (0x4edaef).
dimension = background.get_dimensions();
position = Point<int16_t>((Constants::Constants::get().get_viewwidth() - dimension.x()) / 2,
(Constants::Constants::get().get_viewheight() - dimension.y()) / 2);
}

void UIRemoteGachapon::remove_cursor()
{
UIElement::remove_cursor();

dragged = false;
}

Cursor::State UIRemoteGachapon::send_cursor(bool clicked, Point<int16_t> cursorpos)
{
if (dragged)
{
if (clicked)
{
position = cursorpos - drag_from;

return Cursor::State::CLICKING;
}

dragged = false;
}

Point<int16_t> cursor = cursorpos - position;
Cursor::State ret = UIElement::send_cursor(clicked, cursorpos);

// The top is the caption.
if (clicked && cursor.y() >= 0 && cursor.y() < DRAG_HEIGHT && !is_holding_button())
{
drag_from = cursor;
dragged = true;
}

return ret;
}

void UIRemoteGachapon::send_key(int32_t keycode, bool pressed, bool escape)
{
// Escape and Enter both close it without sending (0x4edd21).
if (pressed && (escape || keycode == KeyAction::Id::RETURN))
deactivate();
}

UIElement::Type UIRemoteGachapon::get_type() const
{
return TYPE;
}

Button::State UIRemoteGachapon::button_pressed(uint16_t buttonid)
{
// 0x811a64: a town sends the ticket and its index, then the window closes either way.
if (buttonid < remote_gachapon::TOWNS.size())
UseRemoteGachaponPacket(item_id, buttonid).dispatch();

deactivate();

return Button::State::NORMAL;
}
}
38 changes: 38 additions & 0 deletions HeavenClient/IO/UITypes/UIRemoteGachapon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include "../UIElement.h"

namespace ms
{
// The Remote Gachapon Ticket's window (v83 0x81104e on UIWindow.img/RemoteGachapon, report b196): a button for
// each town's machine, which sends the ticket with the town and closes the window. Nothing else is asked.
class UIRemoteGachapon : public UIElement
{
public:
static constexpr Type TYPE = UIElement::Type::REMOTEGACHAPON;
static constexpr bool FOCUSED = false;
static constexpr bool TOGGLED = false;

// For the ticket `item_id`.
UIRemoteGachapon(int32_t item_id);

void remove_cursor() override;

Cursor::State send_cursor(bool clicked, Point<int16_t> cursorpos) override;

void send_key(int32_t keycode, bool pressed, bool escape) override;

UIElement::Type get_type() const override;

protected:
Button::State button_pressed(uint16_t buttonid) override;

private:
// After the towns' buttons, 0 to 9.
static constexpr uint16_t CLOSE = 10;

int32_t item_id;
bool dragged;
Point<int16_t> drag_from;
};
}
1 change: 1 addition & 0 deletions HeavenClient/Net/OutPacket.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ namespace ms
SKILL_MACRO = 110,
USE_ITEM_REWARD = 112,
ITEM_MAKE = 113,
REMOTE_GACHAPON = 116,
WATER_OF_LIFE = 117,
ADMIN_BROADCAST = 118,
PARTY_OPERATION = 124,
Expand Down
13 changes: 13 additions & 0 deletions HeavenClient/Net/Packets/GameplayPackets.h
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,19 @@ namespace ms
}
};

// A Remote Gachapon Ticket's window picked town `index` (0xa14074): the
// ticket's item id, then the index.
// Opcode: REMOTE_GACHAPON(116)
class UseRemoteGachaponPacket : public OutPacket
{
public:
UseRemoteGachaponPacket(int32_t item_id, int32_t index) : OutPacket(OutPacket::Opcode::REMOTE_GACHAPON)
{
write_int(item_id);
write_int(index);
}
};

// Asks the server to revive a doll with the Water of Life just used
// (FUN_00a1dce6); it carries nothing.
// Opcode: WATER_OF_LIFE(117)
Expand Down
42 changes: 42 additions & 0 deletions HeavenClient/tests/remote_gachapon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include "../Character/Inventory/RemoteGachapon.h"
#include <iostream>
#include <set>
#include <stdexcept>
#include <utility>

void require(bool condition, const char* message)
{
if (!condition) throw std::runtime_error(message);
}

int main()
{
try
{
using namespace ms::remote_gachapon;

require(is_ticket(TICKET) && is_ticket(5451999), "5451xxx are tickets");
require(!is_ticket(5450000) && !is_ticket(5220000), "Miu Miu and the Gachapon Ticket aren't");

// The background is 246x235 and a town's button 103x18 (UIClassic.nx).
std::set<std::pair<int16_t, int16_t>> places;
for (Place town : TOWNS)
{
require(town.x >= 0 && town.x + 103 <= 246 && town.y >= 0 && town.y + 18 <= 235, "a town fits");
places.insert({ town.x, town.y });
}
require(places.size() == TOWNS.size(), "no two towns overlap");
require(TOWNS[0].x == 15 && TOWNS[0].y == 113, "Henesys sends 0 from the top left");
require(TOWNS[4].x == 128 && TOWNS[4].y == 155, "Sleepywood sends 4 from the third row's right");
require(TOWNS[9].x == 15 && TOWNS[9].y == 155, "Nautilus Harbor sends 9 from the third row's left");
require(CLOSE.x + 12 <= 246, "the close button fits");
}
catch (const std::exception& e)
{
std::cerr << "FAILED: " << e.what() << std::endl;
return 1;
}

std::cout << "remote_gachapon tests passed" << std::endl;
return 0;
}
3 changes: 2 additions & 1 deletion agent_docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ In the shop, `CASHSHOP_OPERATION` (0xE5) buys a commodity and sets the wish
list (`net/src/packet/handle/world/cash_shop.rs`). A purchase is checked against
`Etc.nx` `Commodity.img` (`net::cash_shop`): the item must be on sale and allowed
for the character's gender. Cash items that don't work here yet count as sold
out, alone or in a package: Character Transfer, Maple Life and Remote Gachapon
out, alone or in a package: Character Transfer and Maple Life, and Remote
Gachapon's class but for Miu Miu and the Remote Gachapon Ticket
(`INERT_CLASSES`). `db::cash_shop::buy` then takes the price from the
chosen balance and adds the item to the Cash Inventory in one transaction. The
client gets the item (0x57) and its new balance, or a failed purchase (0x58,
Expand Down
19 changes: 17 additions & 2 deletions agent_docs/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -980,8 +980,23 @@ and the item as in an inventory. The v83 client shows it as a chat line,
on hover; a channel of -1 would name the Cash Shop Surprise. HeavenClient
reads it in `MessagingHandlers.cpp` (case 11), writes the line with
`Megaphones::gachapon` and shows it as `UIChatBar::GACHAPON`, without the
tooltip. The Remote Gachapon (5451000, USE_REMOTE_GACHAPON `0x74`) isn't
handled and isn't sold.
tooltip.

The Remote Gachapon Ticket (5451000, report b196) opens a window of ten town
buttons in the v83 client; a button sends USE_REMOTE (`0x74`, 0xa14074):
`i32` the ticket's item id, `i32` the button, 0 to 9, which are the machines
in `MACHINES`' order (`gachapon::remote_machine`). The client closes the
window and expects no answer of its own. `RemoteGachaponHandler` ignores
another item or button (Cosmic disconnects), and the machine's script
(`NpcScript::remote_gachapon`) answers as its NPC wherever the player is.
As Cosmic's `gachaponRemote` has it, there is no yes or no: the first page
names the prize as `Effect::Gachapon` takes the remote ticket, with the
same full-bag check and world notice as a machine. Without the ticket
nothing is said. HeavenClient opens the window (`UIRemoteGachapon`,
UIClassic's `UIWindow.img/RemoteGachapon`, the buttons where the v83 client
puts them, `remote_gachapon::TOWNS`) for any 5451xxx double-clicked in the
Cash tab once the last request is answered, and sends `UseRemoteGachaponPacket`;
Escape and Enter close it without sending. The Cash Shop sells the ticket.

### Item Maker

Expand Down
2 changes: 1 addition & 1 deletion agent_docs/npc-dialogue.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ a conversation already open.
| `hair_*`, `face_*`, `lens_*`, `skin_*` | Clicking one of 53 stylists: hair salons, plastic surgeons, lens shops and skin care (`net::stylists`) | A menu of the NPC's services, each for its coupon. A VIP coupon lets the player pick the look on a preview; a regular or experimental one rolls it after a yes. A cut keeps the hair's colour and a face its lens colour, while a dye or a lens changes only the colour; a style that doesn't come in the player's colour is offered in colour 0. A salon's hair membership card (5420000-06) makes its cuts free and is kept, and a one-time lens (5152100-07) pays for its own colour. Without the coupon the NPC says so and nothing changes. Claudia (9201039) is not done yet |
| `multipet_success` | Clicking the hidden spot (9102100) on the Pet-Walking Road (100000202) | While Bartos's test (4646) is started, yes gives the note (4031921) the test asks for, once; otherwise one page |
| `q4647e` | QUEST_ACTION 5 for quest 4647 (Bartos) | Two pages, then takes a Pet Snack (5460000), completes the quest and teaches Follow the Lead for the player's line (8, 10000018 or 20000024); without a snack, one page |
| Gachapon machines | Clicking a machine (9100100-9100107, 9100109, 9100117) | With a Gachapon Ticket (5220000), yes trades it for the prize rolled when the conversation opened, and the next page names it; without one, a menu explains the machine. See `agent_docs/inventory.md`, "Gachapon" |
| Gachapon machines | Clicking a machine (9100100-9100107, 9100109, 9100117) | With a Gachapon Ticket (5220000), yes trades it for the prize rolled when the conversation opened, and the next page names it; without one, a menu explains the machine. A Remote Gachapon Ticket (5451000) plays a machine from afar with no yes, and the machine names the prize at once. See `agent_docs/inventory.md`, "Gachapon" |

The five Explorer instructors (`INSTRUCTORS` in `npc_scripts.rs`) make a
beginner a Warrior, Magician, Bowman, Thief, or Pirate. The beginner needs
Expand Down
Loading
Loading