diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index d21ea8da92..448090585d 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -27,6 +27,8 @@ add_files(
string_builder.hpp
string_consumer.cpp
string_consumer.hpp
+ string_inplace.cpp
+ string_inplace.hpp
strong_typedef_type.hpp
utf8.cpp
utf8.hpp
diff --git a/src/core/string_inplace.cpp b/src/core/string_inplace.cpp
new file mode 100644
index 0000000000..248fe8d130
--- /dev/null
+++ b/src/core/string_inplace.cpp
@@ -0,0 +1,63 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/**
+ * @file string_inplace.cpp Inplace-replacement of textual and binary data.
+ */
+
+#include "../stdafx.h"
+#include "string_inplace.hpp"
+#include "../safeguards.h"
+
+/**
+ * Check whether any unused bytes are left between the Builder and Consumer position.
+ */
+[[nodiscard]] bool InPlaceBuilder::AnyBytesUnused() const noexcept
+{
+ return this->consumer.GetBytesRead() > this->position;
+}
+
+/**
+ * Get number of unused bytes left between the Builder and Consumer position.
+ */
+[[nodiscard]] InPlaceBuilder::size_type InPlaceBuilder::GetBytesUnused() const noexcept
+{
+ return this->consumer.GetBytesRead() - this->position;
+}
+
+/**
+ * Append buffer.
+ */
+void InPlaceBuilder::PutBuffer(const char *str, size_type len)
+{
+ auto unused = this->GetBytesUnused();
+ if (len > unused) NOT_REACHED();
+ std::copy(str, str + len, this->dest.data() + this->position);
+ this->position += len;
+}
+
+/**
+ * Create coupled Consumer+Builder pair.
+ * @param buffer Data to consume and replace.
+ * @note The lifetime of the buffer must exceed the lifetime of both the Consumer and the Builder.
+ */
+InPlaceReplacement::InPlaceReplacement(std::span buffer)
+ : consumer(buffer), builder(buffer, consumer)
+{
+}
+
+InPlaceReplacement::InPlaceReplacement(const InPlaceReplacement &src)
+ : consumer(src.consumer), builder(src.builder, consumer)
+{
+}
+
+InPlaceReplacement& InPlaceReplacement::operator=(const InPlaceReplacement &src)
+{
+ this->consumer = src.consumer;
+ this->builder.AssignBuffer(src.builder);
+ return *this;
+}
diff --git a/src/core/string_inplace.hpp b/src/core/string_inplace.hpp
new file mode 100644
index 0000000000..855ae55896
--- /dev/null
+++ b/src/core/string_inplace.hpp
@@ -0,0 +1,105 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/**
+ * @file string_inplace.hpp Inplace-replacement of textual and binary data.
+ */
+
+#ifndef STRING_INPLACE_HPP
+#define STRING_INPLACE_HPP
+
+#include "string_builder.hpp"
+#include "string_consumer.hpp"
+
+/**
+ * Builder implementation for InPlaceReplacement.
+ */
+class InPlaceBuilder final : public BaseStringBuilder
+{
+ std::span dest;
+ size_type position = 0;
+ const StringConsumer &consumer;
+
+ friend class InPlaceReplacement;
+ explicit InPlaceBuilder(std::span dest, const StringConsumer &consumer) : dest(dest), consumer(consumer) {}
+ InPlaceBuilder(const InPlaceBuilder &src, const StringConsumer &consumer) : dest(src.dest), position(src.position), consumer(consumer) {}
+ void AssignBuffer(const InPlaceBuilder &src) { this->dest = src.dest; this->position = src.position; }
+public:
+ InPlaceBuilder(const InPlaceBuilder &) = delete;
+ InPlaceBuilder& operator=(const InPlaceBuilder &) = delete;
+
+ /**
+ * Check whether any bytes have been written.
+ */
+ [[nodiscard]] bool AnyBytesWritten() const noexcept { return this->position != 0; }
+ /**
+ * Get number of already written bytes.
+ */
+ [[nodiscard]] size_type GetBytesWritten() const noexcept { return this->position; }
+ /**
+ * Get already written data.
+ */
+ [[nodiscard]] std::string_view GetWrittenData() const noexcept { return {this->dest.data(), this->position}; }
+
+ [[nodiscard]] bool AnyBytesUnused() const noexcept;
+ [[nodiscard]] size_type GetBytesUnused() const noexcept;
+
+ using BaseStringBuilder::PutBuffer;
+ void PutBuffer(const char *str, size_type len) override;
+
+ /**
+ * Implementation of std::back_insert_iterator for non-growing destination buffer.
+ */
+ class back_insert_iterator {
+ InPlaceBuilder *parent = nullptr;
+ public:
+ using value_type = void;
+ using difference_type = void;
+ using iterator_category = std::output_iterator_tag;
+ using pointer = void;
+ using reference = void;
+
+ back_insert_iterator(InPlaceBuilder &parent) : parent(&parent) {}
+
+ back_insert_iterator &operator++() { return *this; }
+ back_insert_iterator operator++(int) { return *this; }
+ back_insert_iterator &operator*() { return *this; }
+
+ back_insert_iterator &operator=(char value)
+ {
+ this->parent->PutChar(value);
+ return *this;
+ }
+ };
+ /**
+ * Create a back-insert-iterator.
+ */
+ back_insert_iterator back_inserter()
+ {
+ return back_insert_iterator(*this);
+ }
+};
+
+/**
+ * Compose data into a fixed size buffer, which is consumed at the same time.
+ * - The Consumer reads data from a buffer.
+ * - The Builder writes data to the buffer, replacing already consumed data.
+ * - The Builder asserts, if it overtakes the consumer.
+ */
+class InPlaceReplacement
+{
+public:
+ StringConsumer consumer; ///< Consumer from shared buffer
+ InPlaceBuilder builder; ///< Builder into shared buffer
+
+public:
+ InPlaceReplacement(std::span buffer);
+ InPlaceReplacement(const InPlaceReplacement &src);
+ InPlaceReplacement& operator=(const InPlaceReplacement &src);
+};
+
+#endif /* STRING_INPLACE_HPP */
diff --git a/src/settingsgen/CMakeLists.txt b/src/settingsgen/CMakeLists.txt
index 220ffb972b..7f7c1854b1 100644
--- a/src/settingsgen/CMakeLists.txt
+++ b/src/settingsgen/CMakeLists.txt
@@ -12,6 +12,7 @@ if (NOT HOST_BINARY_DIR)
../string.cpp
../core/string_builder.cpp
../core/string_consumer.cpp
+ ../core/string_inplace.cpp
../core/utf8.cpp
)
add_definitions(-DSETTINGSGEN)
diff --git a/src/strgen/CMakeLists.txt b/src/strgen/CMakeLists.txt
index 40eb11448f..64d1905089 100644
--- a/src/strgen/CMakeLists.txt
+++ b/src/strgen/CMakeLists.txt
@@ -14,6 +14,7 @@ if (NOT HOST_BINARY_DIR)
../string.cpp
../core/string_builder.cpp
../core/string_consumer.cpp
+ ../core/string_inplace.cpp
../core/utf8.cpp
)
add_definitions(-DSTRGEN)
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 29f97ad205..94579147fa 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -9,6 +9,7 @@ add_test_files(
mock_spritecache.h
string_builder.cpp
string_consumer.cpp
+ string_inplace.cpp
string_func.cpp
test_main.cpp
test_network_crypto.cpp
diff --git a/src/tests/string_inplace.cpp b/src/tests/string_inplace.cpp
new file mode 100644
index 0000000000..275c2c1fd3
--- /dev/null
+++ b/src/tests/string_inplace.cpp
@@ -0,0 +1,57 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/** @file string_inplace.cpp Test functionality from core/string_inplace. */
+
+#include "../stdafx.h"
+#include "../3rdparty/catch2/catch.hpp"
+#include "../core/string_inplace.hpp"
+#include "../safeguards.h"
+
+using namespace std::literals;
+
+TEST_CASE("InPlaceReplacement")
+{
+ std::array buffer{1, 2, 3, 4};
+ InPlaceReplacement inplace(buffer);
+
+ CHECK(!inplace.builder.AnyBytesWritten());
+ CHECK(inplace.builder.GetBytesWritten() == 0);
+ CHECK(inplace.builder.GetWrittenData() == ""sv);
+ CHECK(!inplace.builder.AnyBytesUnused());
+ CHECK(inplace.builder.GetBytesUnused() == 0);
+ CHECK(!inplace.consumer.AnyBytesRead());
+ CHECK(inplace.consumer.GetBytesRead() == 0);
+ CHECK(inplace.consumer.AnyBytesLeft());
+ CHECK(inplace.consumer.GetBytesLeft() == 4);
+
+ CHECK(inplace.consumer.ReadUint16LE() == 0x201);
+
+ CHECK(inplace.builder.GetBytesWritten() == 0);
+ CHECK(inplace.builder.GetBytesUnused() == 2);
+ CHECK(inplace.consumer.GetBytesRead() == 2);
+ CHECK(inplace.consumer.GetBytesLeft() == 2);
+
+ inplace.builder.PutUint8(11);
+
+ CHECK(inplace.builder.GetBytesWritten() == 1);
+ CHECK(inplace.builder.GetBytesUnused() == 1);
+ CHECK(inplace.consumer.GetBytesRead() == 2);
+ CHECK(inplace.consumer.GetBytesLeft() == 2);
+
+ inplace.builder.PutUint8(12);
+
+ CHECK(inplace.builder.GetBytesWritten() == 2);
+ CHECK(inplace.builder.GetBytesUnused() == 0);
+ CHECK(inplace.consumer.GetBytesRead() == 2);
+ CHECK(inplace.consumer.GetBytesLeft() == 2);
+
+ CHECK(buffer[0] == 11);
+ CHECK(buffer[1] == 12);
+ CHECK(buffer[2] == 3);
+ CHECK(buffer[3] == 4);
+}