TZ.Multiplicity
2.5.1
dotnet add package TZ.Multiplicity --version 2.5.1
NuGet\Install-Package TZ.Multiplicity -Version 2.5.1
<PackageReference Include="TZ.Multiplicity" Version="2.5.1" />
<PackageVersion Include="TZ.Multiplicity" Version="2.5.1" />
<PackageReference Include="TZ.Multiplicity" />
paket add TZ.Multiplicity --version 2.5.1
#r "nuget: TZ.Multiplicity, 2.5.1"
#:package TZ.Multiplicity@2.5.1
#addin nuget:?package=TZ.Multiplicity&version=2.5.1
#tool nuget:?package=TZ.Multiplicity&version=2.5.1
Multiplicity
Multiplicity.Packets - это низкоуровневая библиотека для чтения и формирования сетевых пакетов Terraria.
NuGet package identifier:
TZ.Multiplicity
При этом namespace и имя основной сборки остаются прежними:
Multiplicity.Packets
Она нужна в двух типовых сценариях:
- когда удобно работать с пакетами как с обычными C#-объектами;
- когда нужен максимально дешёвый разбор входящего буфера без лишних копий и без создания object-модели.
Библиотека написана на чистом C#, без внешних зависимостей.
Что умеет
- сериализация и десериализация пакетов Terraria;
- enum и typed-модели пакетов
1..163; - поддержка протокола
317-318; - zero-copy разбор через
ReadOnlySpan<byte>для server hot-path; - публичные имена
PacketTypes, packet classes и packet views выровнены подTerrariaApi.Server.PacketTypesиз TSAPI.
Ограничения
ServerInfo (162)иPlayerPlatformInfo (163)пока остаются raw-placeholder пакетами;- для этих двух пакетов нет подтверждённого wire-layout по актуальному исходнику/декомпилу Terraria.
Установка
NuGet-пакет:
Install-Package TZ.Multiplicity
Текущий target framework проекта:
net9.0
Два слоя API
В библиотеке есть два разных слоя, и у каждого своя задача.
1. Object model
Namespace:
using Multiplicity.Packets;
Этот слой удобен, когда нужно:
- десериализовать пакет в обычный класс;
- изменить поля;
- сериализовать пакет обратно;
- работать не в самом горячем участке серверного кода.
Главная точка входа:
TerrariaPacket.Deserialize(...)
Поддерживаемые варианты:
TerrariaPacket.Deserialize(BinaryReader br, byte id)TerrariaPacket.Deserialize(ReadOnlyMemory<byte> packetBuffer)TerrariaPacket.Deserialize(byte[] buffer, int offset, int length)TerrariaPacket.DeserializePayload(PacketTypes packetType, ReadOnlyMemory<byte> payloadBuffer)TerrariaPacket.DeserializePayload(PacketTypes packetType, byte[] buffer, int offset, int length)
2. Packet views / zero-copy layer
Namespace:
using Multiplicity.Packets.Views;
Этот слой нужен для server hot-path, когда важно:
- не копировать весь пакет;
- не создавать
TerrariaPacket-объект; - быстро читать только нужные поля.
Главные типы:
PacketViewParserPacketViewPacketReader- typed views вида
PlayerUpdateView,PlayerInfoView,ProjectileNewViewи т.д.
PacketView работает поверх исходного буфера.
Это значит, что ReadOnlySpan<byte> внутри views живёт столько же, сколько живёт исходный packet slice.
Для server-side NetGetData, где у тебя часто есть только payload без Terraria header, есть отдельный вход:
PacketViewParser.ParsePayload(PacketTypes packetType, ReadOnlySpan<byte> payloadBuffer)PacketViewParser.ParsePayload(PacketTypes packetType, byte[] buffer, int offset, int length)PacketViewParser.TryParsePayload(...)PacketViewParser.TryParsePayloadBounded(...)for upper-bound payload windows such asNetGetData
Important:
TryParsePayload(..., buffer, offset, length, ...)treatslengthas an exact payload length.TryParsePayloadBounded(...)treatsavailableLengthas an upper bound and allows trailing bytes after the parsed payload.
Покрытие typed views
Сейчас через Multiplicity.Packets.Views покрыты:
- базовые hot-path пакеты:
PlayerInfoPlayerSlotTileGetSectionPlayerUpdateProjectileNewNpcUpdateChatMessageTilePlaceObjectLoadNetModuleWorldItemSyncPlayerHurtV2PlayerDeathV2
- player state пакеты:
PlayerSpawnPlayerActivePlayerHpPlayerManaPlayerTeamTogglePvpZonesPlayerAnimationPlayerDamage
- modern typed packets:
EmojiTileEntityDisplayDollItemSyncRequestTileEntityInteractionTileEntityHatRackItemSyncSyncTilePickingSyncRevengeMarkerRemoveRevengeMarkerLandGolfBallInCupFishOutNPCTamperWithNPCPlayLegacySoundUpdatePlayerLuckFactorsDeadPlayerSyncCavernMonsterTypeRequestNPCBuffRemovalSetCountsAsHostForGameplaySetMiscEventValuesRequestLucyPopupSyncProjectileTrackersShimmerActionsSyncLoadoutSpectatePlayerSyncItemDespawnItemUseSoundNpcHurtByDebuffTELeashedEntityAnchorPlaceItemTeamChangeFromUIExtraSpawnSectionLoadedRequestSectionSyncItemPosition
- generic view-patterns:
StringPacketViewUtf8StringPacketViewEmptyPayloadPacketViewTileEntityItemSyncPacketView
Пример 1. Разобрать пакет как объект
Когда нужна обычная object-модель:
using Multiplicity.Packets;
byte[] receiveBuffer = GetPacketBytes();
TerrariaPacket packet = TerrariaPacket.Deserialize(receiveBuffer, 0, receiveBuffer.Length);
switch (packet)
{
case PlayerInfo playerInfo:
Console.WriteLine($"Player #{playerInfo.PlayerId}: {playerInfo.Name}");
break;
case PlayerUpdate updatePlayer:
Console.WriteLine($"Player #{updatePlayer.PlayerId}: X={updatePlayer.PositionX}, Y={updatePlayer.PositionY}");
break;
}
Этот вариант проще для логики, где пакет потом нужно хранить, менять и сериализовать назад.
Пример 1.1. Разобрать только payload, если тип пакета уже известен
Это полезно для TSAPI/TShock NetGetData, где хук уже дал MsgID, Index и Length, а в буфере лежит только payload-срез:
using Multiplicity.Packets;
PacketTypes packetType = PacketTypes.ProjectileNew;
byte[] readBuffer = GetReceiveBuffer();
int offset = payloadOffset;
int length = payloadLength;
TerrariaPacket packet = TerrariaPacket.DeserializePayload(packetType, readBuffer, offset, length);
if (packet is ProjectileNew projectile)
{
Console.WriteLine($"Projectile #{projectile.Identity}: type={projectile.Type}, owner={projectile.Owner}");
}
Пример 2. Zero-copy разбор входящего буфера
Когда пакет приходит в server receive buffer и важна производительность:
using Multiplicity.Packets;
using Multiplicity.Packets.Views;
ReadOnlySpan<byte> buffer = receiveBuffer.AsSpan(offset, availableBytes);
if (PacketViewParser.TryParse(buffer, out PacketView packetView, out int consumed))
{
switch (packetView.PacketType)
{
case PacketTypes.PlayerUpdate:
{
PlayerUpdateView view = packetView.AsPlayerUpdateView();
Console.WriteLine($"Player #{view.PlayerId}: X={view.PositionX}, Y={view.PositionY}");
break;
}
case PacketTypes.PlayerInfo:
{
PlayerInfoView view = packetView.AsPlayerInfoView();
Console.WriteLine($"Player name: {view.GetName()}");
break;
}
}
// consumed = длина первого пакета в буфере
}
Здесь:
- весь пакет не копируется;
TerrariaPacket-объект не создаётся;- строки можно не материализовывать до последнего момента.
Пример 2.1. Zero-copy разбор только payload без Terraria header
using Multiplicity.Packets;
using Multiplicity.Packets.Views;
PacketTypes packetType = PacketTypes.PlayerUpdate;
ReadOnlySpan<byte> payload = receiveBuffer.AsSpan(payloadOffset, payloadLength);
PacketView packetView = PacketViewParser.ParsePayload(packetType, payload);
PlayerUpdateView view = packetView.AsPlayerUpdateView();
Console.WriteLine($"Player #{view.PlayerId}: X={view.PositionX}, Y={view.PositionY}");
Важно:
- у payload-only
PacketViewнет полного Terraria header; packetView.HasPacketSpanв таком случае будетfalse;packetView.GetPacketSpan()бросит исключение;- если нужен исходный slice без различия packet/payload, используй
packetView.SourceSpanилиview.SourceSpan.
Это основной сценарий для серверных inbound-хуков, где тип пакета уже известен, а лишний MemoryStream/BinaryReader не нужен.
Пример 3. Сформировать пакет и отправить
using Multiplicity.Packets;
var packet = new LoadNetModule
{
LoadedModule = new NetTextModule
{
PayloadKind = NetTextModulePayloadKind.ServerChatMessage,
AuthorId = 255,
ServerText = new NetworkText
{
Text = "Hello from Multiplicity",
TextMode = NetworkText.Mode.Literal
},
MessageColor = new ColorStruct
{
R = 255,
G = 240,
B = 120
}
}
};
byte[] bytes = packet.ToArray(includeHeader: true);
Send(bytes);
Если не нужен промежуточный массив, можно писать сразу в поток:
using Multiplicity.Packets;
using Stream stream = GetNetworkStream();
var packet = new Ping();
packet.ToStream(stream, includeHeader: true);
Если нужно сформировать только payload без Terraria header:
using Multiplicity.Packets;
var packet = new ProjectileNew
{
Identity = 42,
Type = 12,
Owner = 1
};
byte[] payload = packet.ToPayloadArray();
SendPayloadOnly(payload);
Пример 4. Работа со строковыми packet views без лишней аллокации
using Multiplicity.Packets;
using Multiplicity.Packets.Views;
if (PacketViewParser.TryParse(buffer, out PacketView packetView, out _)
&& packetView.PacketType == PacketTypes.HostToken)
{
Utf8StringPacketView view = new Utf8StringPacketView(packetView, PacketTypes.HostToken);
// Если нужна строка:
string token = view.GetString();
// Если нужна только проверка/сравнение, можно работать с Utf8Bytes
ReadOnlySpan<byte> utf8 = view.Utf8Bytes;
}
Когда какой слой использовать
Используй TerrariaPacket, если:
- важна простота;
- пакет нужно мутировать;
- пакет нужно сериализовать обратно;
- горячий путь не критичен.
Используй PacketView, если:
- это
NetGetData/ receive hot-path; - нужно быстро читать 1-2 поля и принимать решение;
- хочется минимум аллокаций;
- не нужен полноценный объект пакета.
Полезные файлы в проекте
Итог
Multiplicity.Packets сейчас даёт:
- object-model для удобной логики и сериализации;
- zero-copy views для производительного server-side разбора;
- typed packet coverage для протокола Terraria
317-318.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.1 | 149 | 3/10/2026 |