## Summary Phase 3 of the anchored-time work: **every actively-written delta-time value in the engine now stores an anchored timestamp** — absolute on the wire, shifted forward by the downtime at load. Remaining time survives restarts (as delta did), and unlike delta, the bytes do not change on every save, so an idle world serializes identically save after save. The answer to "is it possible everywhere": **yes** — including the one case that looked impossible. ## The GenericPersistence problem, solved `GenericPersistence` bins (`Virtues.bin`, `StealableArtifacts.bin`, …) are raw payloads with no idx header, so they have no anchor of their own — anchored reads there would silently apply zero shift. But the anchor is a property of the **save**, not the file: every file in one save shares one `World.SaveStartTime`, and `Persistence.Load` reads **all** entity indexes (phase 1) before **any** persistence payload (phase 2). So the idx v5 header stamps a save-wide `World.LoadTimeShift`, and generic persistence readers inherit it. No file-format change, no per-bin header, old bins unaffected. ## Converted - **Item v10 → v11**: `LastMoved` — previously whole-minute delta, rewritten every save for every item, the single largest source of idle-save churn — and `DecayResetTime` (retiring the TODO from #2583). **Mobile v37 → v38**: the three stat-gain stamps. **BaseCreature v20 → v21**: `SummonEnd`. - **17 code-generated classes** (`[DeltaDateTime]` → `[AnchoredDateTime]`, version bump + `MigrateFrom` each): the five field spells, TransientItem, VirtueContext (×7 fields), PuzzleChestSolutionAndTime, BaseCamp, BaseBoat, RentedVendor, PlayerVendor, Ethics Player, Sheep, StarRoomGate, ChampionSpawn (×3), Corpse (`TimeOfDeath`, v19). The `MigrateFrom` bodies were generated from each class's current migration schema and are compiler-verified; VirtueContext's save-flagged nullables fall back to the same defaults the old deserialize left in place. Corpse's six migrations moved to a new `Corpse.Migrations.cs`. - **Hand-written sites**: StealableArtifacts (v2), VendorInventory (v1), ML quest objectives (persistence v3) — each gated on its own version. **Not converted, deliberately**: the ~25 read-only `ReadDeltaTime` sites in legacy version fallbacks and migration replays — they decode existing old bytes and must never change. `[DeltaDateTime]`/`WriteDeltaTime` remain available for them. ## Verification - Build 0 errors / 0 warnings; **837 + 708 tests green**. - Schema regeneration produced exactly the 17 expected new `vN.json` files (all `AnchoredTime` rule args), nothing else touched. - **New acceptance tests** pin the point of the whole effort: serializing the same item at two save times **5 hours apart produces byte-identical output**, and `LastMoved`/`DecayResetTime` round-trip **exactly** at sub-minute precision (the old minutes encoding destroyed both properties). ## Notes for review - `LastMoved` grows from a 1–3 byte encoded minutes value to 8-byte ticks per item — the price of byte-stability; it repays itself in incremental-save behavior since unchanged items now produce unchanged bytes. - BaseEscortable-style semantics are unchanged: anchored shift preserves *remaining* time exactly, the same contract delta provided, so no gameplay-visible behavior changes — deadlines simply stop being consumed by downtime that delta already protected against, now with stable bytes. ## Enforcement `WriteDeltaTime` is now `[Obsolete]` (interface + implementation). With the repo's warnings-as-errors, any new delta-time write — hand-written or emitted by a still-unconverted `[DeltaDateTime]` field — fails the build, with the migration instructions in the message. That the full solution still builds with **zero warnings** is itself the proof no active delta writer survived the conversion. `ReadDeltaTime` deliberately stays un-attributed: its remaining callers decode existing old bytes and are correct forever; its XML docs now state the legacy-decode-only contract.
593 lines
18 KiB
C#
593 lines
18 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: BufferWriter.cs *
|
|
* *
|
|
* This program 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, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using System.Collections;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using Server.Text;
|
|
|
|
namespace Server;
|
|
|
|
public class BufferWriter : IGenericWriter
|
|
{
|
|
private readonly Encoding _encoding;
|
|
private readonly bool _prefixStrings;
|
|
|
|
private long _bytesWritten;
|
|
private long _index;
|
|
|
|
protected long Index
|
|
{
|
|
get => _index;
|
|
set
|
|
{
|
|
if (value < 0 || value > _buffer.Length)
|
|
{
|
|
// If you are receiving this exception and your value is too large, you may need to use `Resize`
|
|
// If you are receiving this exception and your value is negative, you probably used Seek incorrectly.
|
|
throw new ArgumentOutOfRangeException(nameof(value));
|
|
}
|
|
|
|
_index = value;
|
|
|
|
if (value > _bytesWritten)
|
|
{
|
|
_bytesWritten = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
private byte[] _buffer;
|
|
|
|
public BufferWriter(byte[] buffer, bool prefixStr)
|
|
{
|
|
_prefixStrings = prefixStr;
|
|
_encoding = TextEncoding.UTF8;
|
|
_buffer = buffer;
|
|
}
|
|
|
|
public BufferWriter(bool prefixStr) : this(0, prefixStr)
|
|
{
|
|
}
|
|
|
|
public BufferWriter(int count, bool prefixStr)
|
|
{
|
|
_prefixStrings = prefixStr;
|
|
_encoding = TextEncoding.UTF8;
|
|
_buffer = GC.AllocateUninitializedArray<byte>(count < 1 ? BufferSize : count);
|
|
}
|
|
|
|
public virtual long Position => _index;
|
|
|
|
protected virtual int BufferSize => 256;
|
|
|
|
public byte[] Buffer => _buffer;
|
|
|
|
public virtual void Close()
|
|
{
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Resize(int size)
|
|
{
|
|
_bytesWritten = Math.Max(_bytesWritten, _index);
|
|
|
|
// We shouldn't ever resize to a 0 length buffer. That is dangerous
|
|
if (size <= 0)
|
|
{
|
|
size = BufferSize;
|
|
}
|
|
|
|
if (size < _buffer.Length)
|
|
{
|
|
_bytesWritten = size;
|
|
}
|
|
|
|
var newBuffer = GC.AllocateUninitializedArray<byte>(size);
|
|
_buffer.AsSpan(0, Math.Min(size, _buffer.Length)).CopyTo(newBuffer);
|
|
_buffer = newBuffer;
|
|
}
|
|
|
|
public virtual void Flush() => Resize(Math.Clamp(_buffer.Length * 2, BufferSize, _buffer.Length + 1024 * 1024 * 64));
|
|
|
|
/// <summary>
|
|
/// Ensures capacity, returns a ref at the current position, and advances the index.
|
|
/// The capacity check proves the caller's unaligned store is in-bounds, and the index
|
|
/// only moves forward between Seek calls, so no per-write validation is needed. Growth
|
|
/// (Flush -> Resize) always adds at least BufferSize, covering any primitive width.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private ref byte Reserve(int bytes)
|
|
{
|
|
if ((uint)(_index + bytes) > (uint)_buffer.Length)
|
|
{
|
|
Flush();
|
|
}
|
|
|
|
ref var result = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), (nint)_index);
|
|
_index += bytes;
|
|
return ref result;
|
|
}
|
|
|
|
public virtual void Write(byte[] bytes) => Write(bytes.AsSpan());
|
|
|
|
public virtual void Write(byte[] bytes, int offset, int count) => Write(bytes.AsSpan(offset, count));
|
|
|
|
public virtual void Write(ReadOnlySpan<byte> bytes)
|
|
{
|
|
var length = bytes.Length;
|
|
|
|
while (_buffer.Length - _index < length)
|
|
{
|
|
Flush();
|
|
}
|
|
|
|
bytes.CopyTo(_buffer.AsSpan((int)_index));
|
|
_index += length;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public virtual long Seek(long offset, SeekOrigin origin)
|
|
{
|
|
Debug.Assert(
|
|
origin != SeekOrigin.End || offset <= 0 && offset > -_buffer.Length,
|
|
"Attempting to seek to an invalid position using SeekOrigin.End"
|
|
);
|
|
Debug.Assert(
|
|
origin != SeekOrigin.Begin || offset >= 0 && offset < _buffer.Length,
|
|
"Attempting to seek to an invalid position using SeekOrigin.Begin"
|
|
);
|
|
Debug.Assert(
|
|
origin != SeekOrigin.Current || _index + offset >= 0 && _index + offset < _buffer.Length,
|
|
"Attempting to seek to an invalid position using SeekOrigin.Current"
|
|
);
|
|
|
|
_bytesWritten = Math.Max(_bytesWritten, _index);
|
|
|
|
return Index = Math.Max(0, origin switch
|
|
{
|
|
SeekOrigin.Current => _index + offset,
|
|
SeekOrigin.End => _bytesWritten + offset,
|
|
_ => offset // Begin
|
|
});
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(string value)
|
|
{
|
|
if (_prefixStrings)
|
|
{
|
|
if (value == null)
|
|
{
|
|
Write(false);
|
|
}
|
|
else
|
|
{
|
|
Write(true);
|
|
WriteRaw(value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WriteRaw(value);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(long value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BinaryPrimitives.ReverseEndianness(value);
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(8), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(ulong value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BinaryPrimitives.ReverseEndianness(value);
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(8), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(int value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BinaryPrimitives.ReverseEndianness(value);
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(4), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(uint value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BinaryPrimitives.ReverseEndianness(value);
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(4), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(short value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BinaryPrimitives.ReverseEndianness(value);
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(2), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(ushort value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BinaryPrimitives.ReverseEndianness(value);
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(2), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(double value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value)));
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(8), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(float value)
|
|
{
|
|
if (!BitConverter.IsLittleEndian)
|
|
{
|
|
value = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value)));
|
|
}
|
|
|
|
Unsafe.WriteUnaligned(ref Reserve(4), value);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(byte value) => Reserve(1) = value;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(sbyte value) => Reserve(1) = (byte)value;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(bool value) => Reserve(1) = Unsafe.As<bool, byte>(ref value);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Serial serial) => Write(serial.Value);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Type type)
|
|
{
|
|
if (type == null)
|
|
{
|
|
Write((byte)0);
|
|
}
|
|
else
|
|
{
|
|
Write((byte)0x2); // xxHash3 64bit
|
|
Write(AssemblyHandler.GetTypeHash(type));
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(decimal value)
|
|
{
|
|
Span<int> buffer = stackalloc int[sizeof(decimal) / 4];
|
|
decimal.GetBits(value, buffer);
|
|
|
|
Write(MemoryMarshal.Cast<int, byte>(buffer));
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void WriteEncodedInt(int value)
|
|
{
|
|
var v = (uint)value;
|
|
|
|
// FAST PATH: 1 byte (0 to 127).
|
|
// This keeps the inlined code incredibly tiny at the call site.
|
|
if (v < 0x80)
|
|
{
|
|
Reserve(1) = (byte)v;
|
|
}
|
|
else
|
|
{
|
|
// SLOW PATH: Push to a non-inlined method to prevent code bloat.
|
|
WriteEncodedIntMultiByte(v);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void WriteEncodedIntMultiByte(uint v)
|
|
{
|
|
// We already know v >= 0x80. Unroll the loop entirely based on magnitude.
|
|
// This allows us to call Reserve() exactly ONE time.
|
|
|
|
if (v < 0x4000) // 2 bytes
|
|
{
|
|
ref byte ptr = ref Reserve(2);
|
|
ptr = (byte)(v | 0x80);
|
|
Unsafe.Add(ref ptr, 1) = (byte)(v >> 7);
|
|
}
|
|
else if (v < 0x200000) // 3 bytes
|
|
{
|
|
ref byte ptr = ref Reserve(3);
|
|
ptr = (byte)(v | 0x80);
|
|
Unsafe.Add(ref ptr, 1) = (byte)((v >> 7) | 0x80);
|
|
Unsafe.Add(ref ptr, 2) = (byte)(v >> 14);
|
|
}
|
|
else if (v < 0x10000000) // 4 bytes
|
|
{
|
|
ref byte ptr = ref Reserve(4);
|
|
ptr = (byte)(v | 0x80);
|
|
Unsafe.Add(ref ptr, 1) = (byte)((v >> 7) | 0x80);
|
|
Unsafe.Add(ref ptr, 2) = (byte)((v >> 14) | 0x80);
|
|
Unsafe.Add(ref ptr, 3) = (byte)(v >> 21);
|
|
}
|
|
else // 5 bytes (including all negative numbers due to logical shift)
|
|
{
|
|
ref byte ptr = ref Reserve(5);
|
|
ptr = (byte)(v | 0x80);
|
|
Unsafe.Add(ref ptr, 1) = (byte)((v >> 7) | 0x80);
|
|
Unsafe.Add(ref ptr, 2) = (byte)((v >> 14) | 0x80);
|
|
Unsafe.Add(ref ptr, 3) = (byte)((v >> 21) | 0x80);
|
|
Unsafe.Add(ref ptr, 4) = (byte)(v >> 28);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(DateTime value)
|
|
{
|
|
// If DateTimeKind is Unspecified, we can't assume it needs to be converted.
|
|
if (value.Kind == DateTimeKind.Local)
|
|
{
|
|
value = value.ToUniversalTime();
|
|
}
|
|
|
|
Write(value.Ticks);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
[Obsolete("Delta time rewrites its bytes on every save. Write anchored time instead (WriteAnchoredTime, or [AnchoredDateTime] on generated fields); bump the containing type's version, as the wire format changes. Existing delta payloads remain readable through ReadDeltaTime in old-version fallbacks.")]
|
|
public void WriteDeltaTime(DateTime value)
|
|
{
|
|
if (value == DateTime.MinValue)
|
|
{
|
|
Write(long.MinValue);
|
|
return;
|
|
}
|
|
|
|
if (value == DateTime.MaxValue)
|
|
{
|
|
Write(long.MaxValue);
|
|
return;
|
|
}
|
|
|
|
if (value.Kind == DateTimeKind.Local)
|
|
{
|
|
value = value.ToUniversalTime();
|
|
}
|
|
|
|
// Technically supports negative deltas for times in the past
|
|
Write(value.Ticks - DateTime.UtcNow.Ticks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the absolute value; <see cref="IGenericReader.ReadAnchoredTime" /> re-bases it
|
|
/// by the elapsed time since the save started, so downtime does not age it and an
|
|
/// unchanged value serializes to identical bytes.
|
|
/// </summary>
|
|
public void WriteAnchoredTime(DateTime value)
|
|
{
|
|
if (value.Kind == DateTimeKind.Local)
|
|
{
|
|
value = value.ToUniversalTime();
|
|
}
|
|
|
|
Write(value.Ticks);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(IPAddress value)
|
|
{
|
|
Span<byte> stack = stackalloc byte[16];
|
|
value.TryWriteBytes(stack, out var bytesWritten);
|
|
Write((byte)bytesWritten);
|
|
Write(stack[..bytesWritten]);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(TimeSpan value) => Write(value.Ticks);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Point3D value)
|
|
{
|
|
Write(value.m_X);
|
|
Write(value.m_Y);
|
|
Write(value.m_Z);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Point2D value)
|
|
{
|
|
Write(value.m_X);
|
|
Write(value.m_Y);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Rectangle2D value)
|
|
{
|
|
Write(value.Start);
|
|
Write(value.End);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Rectangle3D value)
|
|
{
|
|
Write(value.Start);
|
|
Write(value.End);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Map value) => Write((byte)(value?.MapIndex ?? 0xFF));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Race value) => Write((byte)(value?.RaceIndex ?? 0xFF));
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public unsafe void WriteEnum<T>(T value) where T : unmanaged, Enum
|
|
{
|
|
switch (sizeof(T))
|
|
{
|
|
default:
|
|
{
|
|
throw new ArgumentException($"Argument of type {typeof(T)} is not a normal enum");
|
|
}
|
|
case 1:
|
|
{
|
|
Write(*(byte*)&value);
|
|
break;
|
|
}
|
|
case 2:
|
|
{
|
|
Write(*(ushort*)&value);
|
|
break;
|
|
}
|
|
case 4:
|
|
{
|
|
WriteEncodedInt(*(int*)&value);
|
|
break;
|
|
}
|
|
case 8:
|
|
{
|
|
Write(*(ulong*)&value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(Guid guid)
|
|
{
|
|
Span<byte> stack = stackalloc byte[16];
|
|
guid.TryWriteBytes(stack);
|
|
Write(stack);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(BitArray bitArray)
|
|
{
|
|
var bitLength = bitArray.Length;
|
|
var byteLength = (bitLength + 7) / 8;
|
|
|
|
WriteEncodedInt(bitLength);
|
|
|
|
var arrayBuffer = ArrayPool<byte>.Shared.Rent(byteLength);
|
|
try
|
|
{
|
|
bitArray.CopyTo(arrayBuffer, 0);
|
|
Write(arrayBuffer.AsSpan(0, byteLength));
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(arrayBuffer);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Write(TextDefinition def)
|
|
{
|
|
if (def == null)
|
|
{
|
|
WriteEncodedInt(3);
|
|
}
|
|
else if (def.Number > 0)
|
|
{
|
|
WriteEncodedInt(1);
|
|
WriteEncodedInt(def.Number);
|
|
}
|
|
else if (def.String != null)
|
|
{
|
|
WriteEncodedInt(2);
|
|
Write(def.String);
|
|
}
|
|
else
|
|
{
|
|
WriteEncodedInt(0); // Empty
|
|
}
|
|
}
|
|
|
|
public void WriteRaw(string value)
|
|
{
|
|
// Single pass, in place: reserve the UTF-8 worst case (3 bytes per char) plus a
|
|
// length prefix sized for that worst case, encode directly into the buffer, then
|
|
// write the actual byte count into the reserved prefix zero-padded to the same
|
|
// width. Readers accumulate 7-bit groups, so non-minimal prefixes decode
|
|
// identically — no second pass over the string, no scratch copy, no pooling.
|
|
var maxLength = value.Length * 3;
|
|
var prefixWidth = EncodedIntWidth(maxLength);
|
|
|
|
while (_buffer.Length - _index < prefixWidth + maxLength)
|
|
{
|
|
Flush();
|
|
}
|
|
|
|
var written = _encoding.GetBytes(value, _buffer.AsSpan((int)(_index + prefixWidth)));
|
|
|
|
WriteEncodedIntPadded(written, prefixWidth);
|
|
_index += written;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int EncodedIntWidth(int value) =>
|
|
value < 0x80 ? 1 : value < 0x4000 ? 2 : value < 0x20_0000 ? 3 : value < 0x1000_0000 ? 4 : 5;
|
|
|
|
private void WriteEncodedIntPadded(int value, int width)
|
|
{
|
|
var v = (uint)value;
|
|
|
|
for (var i = 1; i < width; i++)
|
|
{
|
|
_buffer[_index++] = (byte)(v | 0x80);
|
|
v >>= 7;
|
|
}
|
|
|
|
_buffer[_index++] = (byte)v; // fits in 7 bits because width >= EncodedIntWidth(value)
|
|
}
|
|
}
|