From fdb2002f77de6af19cc7f526b2e7540c329161c3 Mon Sep 17 00:00:00 2001 From: Narr the Reg Date: Wed, 17 May 2023 22:17:16 -0600 Subject: input_common: Implement amiibo writting --- 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 +++- src/input_common/helpers/joycon_protocol/nfc.cpp | 332 ++++++++++++++++++--- src/input_common/helpers/joycon_protocol/nfc.h | 27 +- 6 files changed, 376 insertions(+), 62 deletions(-) (limited to 'src/input_common') diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp index 653862a72..b2b5677c8 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& 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 83429a336..95106f16d 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 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 72a9e71dc..e9b2fccbb 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 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 353dc744d..5007b0e18 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 DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x using MacAddress = std::array; using SerialNumber = std::array; +using TagUUID = std::array; 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 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 data; +}; + +struct NFCWritePackage { + NFCWriteCommandData command_data; + u8 number_of_chunks; + std::array 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 46c9e9489..3b7a628e5 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& data) { return result; } +DriverResult NfcProtocol::WriteAmiibo(std::span 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(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(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& ntag_data) { return DriverResult::Timeout; } -DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output) { +DriverResult NfcProtocol::WriteAmiiboData(const TagUUID& tag_uuid, std::span data) { + constexpr std::size_t timeout_limit = 60; + const auto nfc_data = MakeAmiiboWritePackage(tag_uuid, data); + const std::vector nfc_buffer_data = SerializeWritePackage(nfc_data); + std::span 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(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(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(is_second_attempt ? 0xe8 : 0x00), + .unknown_2 = static_cast(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 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 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(data_size), + .raw_data = {}, + .crc = {}, + }; + memcpy(request.raw_data.data(), data.data(), data_size); + + std::array 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 NfcProtocol::SerializeWritePackage(const NFCWritePackage& package) const { + const std::size_t header_size = + sizeof(NFCWriteCommandData) + sizeof(NFCWritePackage::number_of_chunks); + std::vector 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 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((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 data) const { + constexpr u8 PAGE_SIZE = 4; + + if (static_cast(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 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 c9e9af03f..eb58c427d 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& data); + DriverResult WriteAmiibo(std::span data); + bool HasAmiibo(); bool IsEnabled() const; @@ -37,18 +39,20 @@ private: struct TagFoundData { u8 type; - std::vector 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& data); - DriverResult SendStartPollingRequest(MCUCommandResponse& output); + DriverResult WriteAmiiboData(const TagUUID& tag_uuid, std::span 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 data); + + std::vector SerializeWritePackage(const NFCWritePackage& package) const; + + NFCWritePackage MakeAmiiboWritePackage(const TagUUID& tag_uuid, std::span data) const; + + NFCDataChunk MakeAmiiboChunk(u8 page, u8 size, std::span data) const; + NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const; + TagUUID GetTagUUID(std::span data) const; + bool is_enabled{}; std::size_t update_counter{}; }; -- cgit v1.2.3