Merge pull request #10396 from german77/amiibo_write

input_common: Implement amiibo writing
This commit is contained in:
bunnei 2023-05-25 14:07:16 -07:00 committed by GitHub
commit ffa1fba7d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 387 additions and 68 deletions

View file

@ -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() {

View file

@ -426,11 +426,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() {
@ -449,11 +449,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) {

View file

@ -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,

View file

@ -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();

View file

@ -49,6 +49,7 @@ public:
DriverResult SetIrMode();
DriverResult SetNfcMode();
DriverResult SetRingConMode();
DriverResult WriteNfcData(std::span<const u8> data);
void SetCallbacks(const JoyconCallbacks& callbacks);

View file

@ -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;

View file

@ -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;
}

View file

@ -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{};
};