From fdb2002f77de6af19cc7f526b2e7540c329161c3 Mon Sep 17 00:00:00 2001
From: Narr the Reg <juangerman-13@hotmail.com>
Date: Wed, 17 May 2023 22:17:16 -0600
Subject: [PATCH] input_common: Implement amiibo writting

---
 src/core/hid/emulated_controller.cpp          |   9 +-
 src/core/hle/service/nfc/common/device.cpp    |   8 +-
 src/input_common/drivers/joycon.cpp           |   8 +-
 src/input_common/helpers/joycon_driver.cpp    |  20 ++
 src/input_common/helpers/joycon_driver.h      |   1 +
 .../helpers/joycon_protocol/joycon_types.h    |  50 ++-
 .../helpers/joycon_protocol/nfc.cpp           | 332 +++++++++++++++---
 .../helpers/joycon_protocol/nfc.h             |  27 +-
 8 files changed, 387 insertions(+), 68 deletions(-)

diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index 366880711b..bbfea71174 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -1283,9 +1283,14 @@ bool EmulatedController::HasNfc() const {
 }
 
 bool EmulatedController::WriteNfc(const std::vector<u8>& data) {
-    auto& nfc_output_device = output_devices[3];
+    auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
+    auto& nfc_virtual_output_device = output_devices[3];
 
-    return nfc_output_device->WriteNfcData(data) == Common::Input::NfcState::Success;
+    if (nfc_output_device->SupportsNfc() != Common::Input::NfcState::NotSupported) {
+        return nfc_output_device->WriteNfcData(data) == Common::Input::NfcState::Success;
+    }
+
+    return nfc_virtual_output_device->WriteNfcData(data) == Common::Input::NfcState::Success;
 }
 
 void EmulatedController::SetLedPattern() {
diff --git a/src/core/hle/service/nfc/common/device.cpp b/src/core/hle/service/nfc/common/device.cpp
index 322bde2ed3..8a7e9edacf 100644
--- a/src/core/hle/service/nfc/common/device.cpp
+++ b/src/core/hle/service/nfc/common/device.cpp
@@ -421,11 +421,11 @@ Result NfcDevice::Flush() {
 
     tag_data.write_counter++;
 
-    FlushWithBreak(NFP::BreakType::Normal);
+    const auto result = FlushWithBreak(NFP::BreakType::Normal);
 
     is_data_moddified = false;
 
-    return ResultSuccess;
+    return result;
 }
 
 Result NfcDevice::FlushDebug() {
@@ -444,11 +444,11 @@ Result NfcDevice::FlushDebug() {
 
     tag_data.write_counter++;
 
-    FlushWithBreak(NFP::BreakType::Normal);
+    const auto result = FlushWithBreak(NFP::BreakType::Normal);
 
     is_data_moddified = false;
 
-    return ResultSuccess;
+    return result;
 }
 
 Result NfcDevice::FlushWithBreak(NFP::BreakType break_type) {
diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp
index 653862a727..b2b5677c8c 100644
--- a/src/input_common/drivers/joycon.cpp
+++ b/src/input_common/drivers/joycon.cpp
@@ -291,9 +291,13 @@ Common::Input::NfcState Joycons::SupportsNfc(const PadIdentifier& identifier_) c
     return Common::Input::NfcState::Success;
 };
 
-Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier_,
+Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier,
                                               const std::vector<u8>& data) {
-    return Common::Input::NfcState::NotSupported;
+    auto handle = GetHandle(identifier);
+    if (handle->WriteNfcData(data) != Joycon::DriverResult::Success) {
+        return Common::Input::NfcState::WriteFailed;
+    }
+    return Common::Input::NfcState::Success;
 };
 
 Common::Input::DriverResult Joycons::SetPollingMode(const PadIdentifier& identifier,
diff --git a/src/input_common/helpers/joycon_driver.cpp b/src/input_common/helpers/joycon_driver.cpp
index 83429a3368..95106f16d8 100644
--- a/src/input_common/helpers/joycon_driver.cpp
+++ b/src/input_common/helpers/joycon_driver.cpp
@@ -492,6 +492,26 @@ DriverResult JoyconDriver::SetRingConMode() {
     return result;
 }
 
+DriverResult JoyconDriver::WriteNfcData(std::span<const u8> data) {
+    std::scoped_lock lock{mutex};
+    disable_input_thread = true;
+
+    if (!supported_features.nfc) {
+        return DriverResult::NotSupported;
+    }
+    if (!nfc_protocol->IsEnabled()) {
+        return DriverResult::Disabled;
+    }
+    if (!amiibo_detected) {
+        return DriverResult::ErrorWritingData;
+    }
+
+    const auto result = nfc_protocol->WriteAmiibo(data);
+
+    disable_input_thread = false;
+    return result;
+}
+
 bool JoyconDriver::IsConnected() const {
     std::scoped_lock lock{mutex};
     return is_connected.load();
diff --git a/src/input_common/helpers/joycon_driver.h b/src/input_common/helpers/joycon_driver.h
index 72a9e71dcf..e9b2fccbb8 100644
--- a/src/input_common/helpers/joycon_driver.h
+++ b/src/input_common/helpers/joycon_driver.h
@@ -49,6 +49,7 @@ public:
     DriverResult SetIrMode();
     DriverResult SetNfcMode();
     DriverResult SetRingConMode();
+    DriverResult WriteNfcData(std::span<const u8> data);
 
     void SetCallbacks(const JoyconCallbacks& callbacks);
 
diff --git a/src/input_common/helpers/joycon_protocol/joycon_types.h b/src/input_common/helpers/joycon_protocol/joycon_types.h
index 353dc744d5..5007b0e188 100644
--- a/src/input_common/helpers/joycon_protocol/joycon_types.h
+++ b/src/input_common/helpers/joycon_protocol/joycon_types.h
@@ -23,6 +23,7 @@ constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x
 
 using MacAddress = std::array<u8, 6>;
 using SerialNumber = std::array<u8, 15>;
+using TagUUID = std::array<u8, 7>;
 
 enum class ControllerType : u8 {
     None = 0x00,
@@ -276,12 +277,13 @@ enum class MCUPacketFlag : u8 {
     LastCommandPacket = 0x08,
 };
 
-enum class NFCReadCommand : u8 {
+enum class NFCCommand : u8 {
     CancelAll = 0x00,
     StartPolling = 0x01,
     StopPolling = 0x02,
     StartWaitingRecieve = 0x04,
-    Ntag = 0x06,
+    ReadNtag = 0x06,
+    WriteNtag = 0x08,
     Mifare = 0x0F,
 };
 
@@ -292,14 +294,19 @@ enum class NFCTagType : u8 {
 
 enum class NFCPages {
     Block0 = 0,
+    Block3 = 3,
     Block45 = 45,
     Block135 = 135,
     Block231 = 231,
 };
 
 enum class NFCStatus : u8 {
+    Ready = 0x00,
+    Polling = 0x01,
     LastPackage = 0x04,
+    WriteDone = 0x05,
     TagLost = 0x07,
+    WriteReady = 0x09,
 };
 
 enum class IrsMode : u8 {
@@ -559,13 +566,32 @@ static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an inv
 struct NFCReadCommandData {
     u8 unknown;
     u8 uuid_length;
-    u8 unknown_2;
-    std::array<u8, 6> uid;
+    TagUUID uid;
     NFCTagType tag_type;
     NFCReadBlockCommand read_block;
 };
 static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size");
 
+#pragma pack(push, 1)
+struct NFCWriteCommandData {
+    u8 unknown;
+    u8 uuid_length;
+    TagUUID uid;
+    NFCTagType tag_type;
+    u8 unknown2;
+    u8 unknown3;
+    u8 unknown4;
+    u8 unknown5;
+    u8 unknown6;
+    u8 unknown7;
+    u8 unknown8;
+    u8 magic;
+    u16_be write_count;
+    u8 amiibo_version;
+};
+static_assert(sizeof(NFCWriteCommandData) == 0x15, "NFCWriteCommandData is an invalid size");
+#pragma pack(pop)
+
 struct NFCPollingCommandData {
     u8 enable_mifare;
     u8 unknown_1;
@@ -576,8 +602,8 @@ struct NFCPollingCommandData {
 static_assert(sizeof(NFCPollingCommandData) == 0x05, "NFCPollingCommandData is an invalid size");
 
 struct NFCRequestState {
-    NFCReadCommand command_argument;
-    INSERT_PADDING_BYTES(0x1);
+    NFCCommand command_argument;
+    u8 block_id;
     u8 packet_id;
     MCUPacketFlag packet_flag;
     u8 data_length;
@@ -591,6 +617,18 @@ struct NFCRequestState {
 };
 static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size");
 
+struct NFCDataChunk {
+    u8 nfc_page;
+    u8 data_size;
+    std::array<u8, 0xFF> data;
+};
+
+struct NFCWritePackage {
+    NFCWriteCommandData command_data;
+    u8 number_of_chunks;
+    std::array<NFCDataChunk, 4> data_chunks;
+};
+
 struct IrsConfigure {
     MCUCommand command;
     MCUSubCommand sub_command;
diff --git a/src/input_common/helpers/joycon_protocol/nfc.cpp b/src/input_common/helpers/joycon_protocol/nfc.cpp
index 46c9e9489f..3b7a628e56 100644
--- a/src/input_common/helpers/joycon_protocol/nfc.cpp
+++ b/src/input_common/helpers/joycon_protocol/nfc.cpp
@@ -34,6 +34,12 @@ DriverResult NfcProtocol::EnableNfc() {
 
         result = ConfigureMCU(config);
     }
+    if (result == DriverResult::Success) {
+        result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC);
+    }
+    if (result == DriverResult::Success) {
+        result = WaitUntilNfcIs(NFCStatus::Ready);
+    }
 
     return result;
 }
@@ -56,27 +62,20 @@ DriverResult NfcProtocol::StartNFCPollingMode() {
     LOG_DEBUG(Input, "Start NFC pooling Mode");
     ScopedSetBlocking sb(this);
     DriverResult result{DriverResult::Success};
-    TagFoundData tag_data{};
 
-    if (result == DriverResult::Success) {
-        result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC);
-    }
-    if (result == DriverResult::Success) {
-        result = WaitUntilNfcIsReady();
-    }
     if (result == DriverResult::Success) {
         MCUCommandResponse output{};
         result = SendStopPollingRequest(output);
     }
     if (result == DriverResult::Success) {
-        result = WaitUntilNfcIsReady();
+        result = WaitUntilNfcIs(NFCStatus::Ready);
     }
     if (result == DriverResult::Success) {
         MCUCommandResponse output{};
         result = SendStartPollingRequest(output);
     }
     if (result == DriverResult::Success) {
-        result = WaitUntilNfcIsPolling();
+        result = WaitUntilNfcIs(NFCStatus::Polling);
     }
     if (result == DriverResult::Success) {
         is_enabled = true;
@@ -112,6 +111,49 @@ DriverResult NfcProtocol::ScanAmiibo(std::vector<u8>& data) {
     return result;
 }
 
+DriverResult NfcProtocol::WriteAmiibo(std::span<const u8> data) {
+    LOG_DEBUG(Input, "Write amiibo");
+    ScopedSetBlocking sb(this);
+    DriverResult result{DriverResult::Success};
+    TagUUID tag_uuid = GetTagUUID(data);
+    TagFoundData tag_data{};
+
+    if (result == DriverResult::Success) {
+        result = IsTagInRange(tag_data, 7);
+    }
+    if (result == DriverResult::Success) {
+        if (tag_data.uuid != tag_uuid) {
+            result = DriverResult::InvalidParameters;
+        }
+    }
+    if (result == DriverResult::Success) {
+        MCUCommandResponse output{};
+        result = SendStopPollingRequest(output);
+    }
+    if (result == DriverResult::Success) {
+        result = WaitUntilNfcIs(NFCStatus::Ready);
+    }
+    if (result == DriverResult::Success) {
+        MCUCommandResponse output{};
+        result = SendStartPollingRequest(output, true);
+    }
+    if (result == DriverResult::Success) {
+        result = WaitUntilNfcIs(NFCStatus::WriteReady);
+    }
+    if (result == DriverResult::Success) {
+        result = WriteAmiiboData(tag_uuid, data);
+    }
+    if (result == DriverResult::Success) {
+        result = WaitUntilNfcIs(NFCStatus::WriteDone);
+    }
+    if (result == DriverResult::Success) {
+        MCUCommandResponse output{};
+        result = SendStopPollingRequest(output);
+    }
+
+    return result;
+}
+
 bool NfcProtocol::HasAmiibo() {
     if (update_counter++ < AMIIBO_UPDATE_DELAY) {
         return true;
@@ -129,7 +171,7 @@ bool NfcProtocol::HasAmiibo() {
     return result == DriverResult::Success;
 }
 
-DriverResult NfcProtocol::WaitUntilNfcIsReady() {
+DriverResult NfcProtocol::WaitUntilNfcIs(NFCStatus status) {
     constexpr std::size_t timeout_limit = 10;
     MCUCommandResponse output{};
     std::size_t tries = 0;
@@ -145,28 +187,7 @@ DriverResult NfcProtocol::WaitUntilNfcIsReady() {
         }
     } while (output.mcu_report != MCUReport::NFCState ||
              (output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
-             output.mcu_data[5] != 0x31 || output.mcu_data[6] != 0x00);
-
-    return DriverResult::Success;
-}
-
-DriverResult NfcProtocol::WaitUntilNfcIsPolling() {
-    constexpr std::size_t timeout_limit = 10;
-    MCUCommandResponse output{};
-    std::size_t tries = 0;
-
-    do {
-        auto result = SendNextPackageRequest(output, {});
-
-        if (result != DriverResult::Success) {
-            return result;
-        }
-        if (tries++ > timeout_limit) {
-            return DriverResult::Timeout;
-        }
-    } while (output.mcu_report != MCUReport::NFCState ||
-             (output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
-             output.mcu_data[5] != 0x31 || output.mcu_data[6] != 0x01);
+             output.mcu_data[5] != 0x31 || output.mcu_data[6] != static_cast<u8>(status));
 
     return DriverResult::Success;
 }
@@ -188,7 +209,7 @@ DriverResult NfcProtocol::IsTagInRange(TagFoundData& data, std::size_t timeout_l
              (output.mcu_data[6] != 0x09 && output.mcu_data[6] != 0x04));
 
     data.type = output.mcu_data[12];
-    data.uuid.resize(output.mcu_data[14]);
+    data.uuid_size = std::min(output.mcu_data[14], static_cast<u8>(sizeof(TagUUID)));
     memcpy(data.uuid.data(), output.mcu_data.data() + 15, data.uuid.size());
 
     return DriverResult::Success;
@@ -245,17 +266,94 @@ DriverResult NfcProtocol::GetAmiiboData(std::vector<u8>& ntag_data) {
     return DriverResult::Timeout;
 }
 
-DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output) {
+DriverResult NfcProtocol::WriteAmiiboData(const TagUUID& tag_uuid, std::span<const u8> data) {
+    constexpr std::size_t timeout_limit = 60;
+    const auto nfc_data = MakeAmiiboWritePackage(tag_uuid, data);
+    const std::vector<u8> nfc_buffer_data = SerializeWritePackage(nfc_data);
+    std::span<const u8> buffer(nfc_buffer_data);
+    MCUCommandResponse output{};
+    u8 block_id = 1;
+    u8 package_index = 0;
+    std::size_t tries = 0;
+    std::size_t current_position = 0;
+
+    LOG_INFO(Input, "Writing amiibo data");
+
+    auto result = SendWriteAmiiboRequest(output, tag_uuid);
+
+    if (result != DriverResult::Success) {
+        return result;
+    }
+
+    // Read Tag data but ignore the actual sent data
+    while (tries++ < timeout_limit) {
+        result = SendNextPackageRequest(output, package_index);
+        const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+        if (result != DriverResult::Success) {
+            return result;
+        }
+
+        if ((output.mcu_report == MCUReport::NFCReadData ||
+             output.mcu_report == MCUReport::NFCState) &&
+            nfc_status == NFCStatus::TagLost) {
+            return DriverResult::ErrorReadingData;
+        }
+
+        if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
+            package_index++;
+            continue;
+        }
+
+        if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
+            LOG_INFO(Input, "Finished reading amiibo");
+            break;
+        }
+    }
+
+    // Send Data. Nfc buffer size is 31, Send the data in smaller packages
+    while (current_position < buffer.size() && tries++ < timeout_limit) {
+        const std::size_t next_position =
+            std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
+        const std::size_t block_size = next_position - current_position;
+        const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
+
+        SendWriteDataAmiiboRequest(output, block_id, is_last_packet,
+                                   buffer.subspan(current_position, block_size));
+
+        const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+        if ((output.mcu_report == MCUReport::NFCReadData ||
+             output.mcu_report == MCUReport::NFCState) &&
+            nfc_status == NFCStatus::TagLost) {
+            return DriverResult::ErrorReadingData;
+        }
+
+        // Increase position when data is confirmed by the joycon
+        if (output.mcu_report == MCUReport::NFCState &&
+            (output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
+            output.mcu_data[3] == block_id) {
+            block_id++;
+            current_position = next_position;
+        }
+    }
+
+    return result;
+}
+
+DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output,
+                                                  bool is_second_attempt) {
     NFCRequestState request{
-        .command_argument = NFCReadCommand::StartPolling,
-        .packet_id = 0x0,
+        .command_argument = NFCCommand::StartPolling,
+        .block_id = {},
+        .packet_id = {},
         .packet_flag = MCUPacketFlag::LastCommandPacket,
         .data_length = sizeof(NFCPollingCommandData),
         .nfc_polling =
             {
-                .enable_mifare = 0x01,
-                .unknown_1 = 0x00,
-                .unknown_2 = 0x00,
+                .enable_mifare = 0x00,
+                .unknown_1 = static_cast<u8>(is_second_attempt ? 0xe8 : 0x00),
+                .unknown_2 = static_cast<u8>(is_second_attempt ? 0x03 : 0x00),
                 .unknown_3 = 0x2c,
                 .unknown_4 = 0x01,
             },
@@ -271,10 +369,11 @@ DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output) {
 
 DriverResult NfcProtocol::SendStopPollingRequest(MCUCommandResponse& output) {
     NFCRequestState request{
-        .command_argument = NFCReadCommand::StopPolling,
-        .packet_id = 0x0,
+        .command_argument = NFCCommand::StopPolling,
+        .block_id = {},
+        .packet_id = {},
         .packet_flag = MCUPacketFlag::LastCommandPacket,
-        .data_length = 0,
+        .data_length = {},
         .raw_data = {},
         .crc = {},
     };
@@ -288,10 +387,11 @@ DriverResult NfcProtocol::SendStopPollingRequest(MCUCommandResponse& output) {
 
 DriverResult NfcProtocol::SendNextPackageRequest(MCUCommandResponse& output, u8 packet_id) {
     NFCRequestState request{
-        .command_argument = NFCReadCommand::StartWaitingRecieve,
+        .command_argument = NFCCommand::StartWaitingRecieve,
+        .block_id = {},
         .packet_id = packet_id,
         .packet_flag = MCUPacketFlag::LastCommandPacket,
-        .data_length = 0,
+        .data_length = {},
         .raw_data = {},
         .crc = {},
     };
@@ -305,17 +405,17 @@ DriverResult NfcProtocol::SendNextPackageRequest(MCUCommandResponse& output, u8
 
 DriverResult NfcProtocol::SendReadAmiiboRequest(MCUCommandResponse& output, NFCPages ntag_pages) {
     NFCRequestState request{
-        .command_argument = NFCReadCommand::Ntag,
-        .packet_id = 0x0,
+        .command_argument = NFCCommand::ReadNtag,
+        .block_id = {},
+        .packet_id = {},
         .packet_flag = MCUPacketFlag::LastCommandPacket,
         .data_length = sizeof(NFCReadCommandData),
         .nfc_read =
             {
                 .unknown = 0xd0,
-                .uuid_length = 0x07,
-                .unknown_2 = 0x00,
+                .uuid_length = sizeof(NFCReadCommandData::uid),
                 .uid = {},
-                .tag_type = NFCTagType::AllTags,
+                .tag_type = NFCTagType::Ntag215,
                 .read_block = GetReadBlockCommand(ntag_pages),
             },
         .crc = {},
@@ -328,12 +428,135 @@ DriverResult NfcProtocol::SendReadAmiiboRequest(MCUCommandResponse& output, NFCP
                        output);
 }
 
+DriverResult NfcProtocol::SendWriteAmiiboRequest(MCUCommandResponse& output,
+                                                 const TagUUID& tag_uuid) {
+    NFCRequestState request{
+        .command_argument = NFCCommand::ReadNtag,
+        .block_id = {},
+        .packet_id = {},
+        .packet_flag = MCUPacketFlag::LastCommandPacket,
+        .data_length = sizeof(NFCReadCommandData),
+        .nfc_read =
+            {
+                .unknown = 0xd0,
+                .uuid_length = sizeof(NFCReadCommandData::uid),
+                .uid = tag_uuid,
+                .tag_type = NFCTagType::Ntag215,
+                .read_block = GetReadBlockCommand(NFCPages::Block3),
+            },
+        .crc = {},
+    };
+
+    std::array<u8, sizeof(NFCRequestState)> request_data{};
+    memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+    request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+    return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+                       output);
+}
+
+DriverResult NfcProtocol::SendWriteDataAmiiboRequest(MCUCommandResponse& output, u8 block_id,
+                                                     bool is_last_packet,
+                                                     std::span<const u8> data) {
+    const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
+    NFCRequestState request{
+        .command_argument = NFCCommand::WriteNtag,
+        .block_id = block_id,
+        .packet_id = {},
+        .packet_flag =
+            is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
+        .data_length = static_cast<u8>(data_size),
+        .raw_data = {},
+        .crc = {},
+    };
+    memcpy(request.raw_data.data(), data.data(), data_size);
+
+    std::array<u8, sizeof(NFCRequestState)> request_data{};
+    memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+    request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+    return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+                       output);
+}
+
+std::vector<u8> NfcProtocol::SerializeWritePackage(const NFCWritePackage& package) const {
+    const std::size_t header_size =
+        sizeof(NFCWriteCommandData) + sizeof(NFCWritePackage::number_of_chunks);
+    std::vector<u8> serialized_data(header_size);
+    std::size_t start_index = 0;
+
+    memcpy(serialized_data.data(), &package, header_size);
+    start_index += header_size;
+
+    for (const auto& data_chunk : package.data_chunks) {
+        const std::size_t chunk_size =
+            sizeof(NFCDataChunk::nfc_page) + sizeof(NFCDataChunk::data_size) + data_chunk.data_size;
+
+        serialized_data.resize(start_index + chunk_size);
+        memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
+        start_index += chunk_size;
+    }
+
+    return serialized_data;
+}
+
+NFCWritePackage NfcProtocol::MakeAmiiboWritePackage(const TagUUID& tag_uuid,
+                                                    std::span<const u8> data) const {
+    return {
+        .command_data{
+            .unknown = 0xd0,
+            .uuid_length = sizeof(NFCReadCommandData::uid),
+            .uid = tag_uuid,
+            .tag_type = NFCTagType::Ntag215,
+            .unknown2 = 0x00,
+            .unknown3 = 0x01,
+            .unknown4 = 0x04,
+            .unknown5 = 0xff,
+            .unknown6 = 0xff,
+            .unknown7 = 0xff,
+            .unknown8 = 0xff,
+            .magic = data[16],
+            .write_count = static_cast<u16>((data[17] << 8) + data[18]),
+            .amiibo_version = data[19],
+        },
+        .number_of_chunks = 3,
+        .data_chunks =
+            {
+                MakeAmiiboChunk(0x05, 0x20, data),
+                MakeAmiiboChunk(0x20, 0xf0, data),
+                MakeAmiiboChunk(0x5c, 0x98, data),
+            },
+    };
+}
+
+NFCDataChunk NfcProtocol::MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const {
+    constexpr u8 PAGE_SIZE = 4;
+
+    if (static_cast<std::size_t>(page * PAGE_SIZE) + size >= data.size()) {
+        return {};
+    }
+
+    NFCDataChunk chunk{
+        .nfc_page = page,
+        .data_size = size,
+        .data = {},
+    };
+    std::memcpy(chunk.data.data(), data.data() + (page * PAGE_SIZE), size);
+    return chunk;
+}
+
 NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(NFCPages pages) const {
     switch (pages) {
     case NFCPages::Block0:
         return {
             .block_count = 1,
         };
+    case NFCPages::Block3:
+        return {
+            .block_count = 1,
+            .blocks =
+                {
+                    NFCReadBlock{0x03, 0x03},
+                },
+        };
     case NFCPages::Block45:
         return {
             .block_count = 1,
@@ -368,6 +591,17 @@ NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(NFCPages pages) const {
     };
 }
 
+TagUUID NfcProtocol::GetTagUUID(std::span<const u8> data) const {
+    if (data.size() < 10) {
+        return {};
+    }
+
+    // crc byte 3 is omitted in this operation
+    return {
+        data[0], data[1], data[2], data[4], data[5], data[6], data[7],
+    };
+}
+
 bool NfcProtocol::IsEnabled() const {
     return is_enabled;
 }
diff --git a/src/input_common/helpers/joycon_protocol/nfc.h b/src/input_common/helpers/joycon_protocol/nfc.h
index c9e9af03fa..eb58c427db 100644
--- a/src/input_common/helpers/joycon_protocol/nfc.h
+++ b/src/input_common/helpers/joycon_protocol/nfc.h
@@ -27,6 +27,8 @@ public:
 
     DriverResult ScanAmiibo(std::vector<u8>& data);
 
+    DriverResult WriteAmiibo(std::span<const u8> data);
+
     bool HasAmiibo();
 
     bool IsEnabled() const;
@@ -37,18 +39,20 @@ private:
 
     struct TagFoundData {
         u8 type;
-        std::vector<u8> uuid;
+        u8 uuid_size;
+        TagUUID uuid;
     };
 
-    DriverResult WaitUntilNfcIsReady();
-
-    DriverResult WaitUntilNfcIsPolling();
+    DriverResult WaitUntilNfcIs(NFCStatus status);
 
     DriverResult IsTagInRange(TagFoundData& data, std::size_t timeout_limit = 1);
 
     DriverResult GetAmiiboData(std::vector<u8>& data);
 
-    DriverResult SendStartPollingRequest(MCUCommandResponse& output);
+    DriverResult WriteAmiiboData(const TagUUID& tag_uuid, std::span<const u8> data);
+
+    DriverResult SendStartPollingRequest(MCUCommandResponse& output,
+                                         bool is_second_attempt = false);
 
     DriverResult SendStopPollingRequest(MCUCommandResponse& output);
 
@@ -56,8 +60,21 @@ private:
 
     DriverResult SendReadAmiiboRequest(MCUCommandResponse& output, NFCPages ntag_pages);
 
+    DriverResult SendWriteAmiiboRequest(MCUCommandResponse& output, const TagUUID& tag_uuid);
+
+    DriverResult SendWriteDataAmiiboRequest(MCUCommandResponse& output, u8 block_id,
+                                            bool is_last_packet, std::span<const u8> data);
+
+    std::vector<u8> SerializeWritePackage(const NFCWritePackage& package) const;
+
+    NFCWritePackage MakeAmiiboWritePackage(const TagUUID& tag_uuid, std::span<const u8> data) const;
+
+    NFCDataChunk MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const;
+
     NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const;
 
+    TagUUID GetTagUUID(std::span<const u8> data) const;
+
     bool is_enabled{};
     std::size_t update_counter{};
 };