PubSubLib.Mirror.Generator 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package PubSubLib.Mirror.Generator --version 2.0.0
                    
NuGet\Install-Package PubSubLib.Mirror.Generator -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PubSubLib.Mirror.Generator" Version="2.0.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PubSubLib.Mirror.Generator" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="PubSubLib.Mirror.Generator">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PubSubLib.Mirror.Generator --version 2.0.0
                    
#r "nuget: PubSubLib.Mirror.Generator, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PubSubLib.Mirror.Generator@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PubSubLib.Mirror.Generator&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=PubSubLib.Mirror.Generator&version=2.0.0
                    
Install as a Cake Tool

Mirror Generator — PubSubLib.Mirror.Generator

Roslyn Incremental Source Generator tự động sinh code mirror proto từ class C# sang protobuf và ngược lại. Dùng cho cả Server (sở hữu dữ liệu, auto-commit) và Client (nhận mirror, read-only).

Mục lục


Cài đặt

<PackageReference Include="PubSubLib.Mirror.Generator" Version="2.0.0" />
<PackageReference Include="PubSubLib.Contracts" Version="2.0.0" />

Generator hoạt động ở compile-time — không cần runtime dependency ngoài PubSubLib.Contracts (chứa attribute definitions). Target netstandard2.0, tương thích Unity IL2CPP.


Tổng quan

Bạn viết                              Generator sinh ra
─────────                             ──────────────────
[partial class]                       [partial class bổ sung]
  + [MirrorProto(typeof(Msg))]   →    Implement IPlayerData + IPlayerDataInternal
                                       Field → Property mapping
                                       Dirty tracking
                                       Auto-commit qua MirrorProtoBus
                                       DoneInit() — kích hoạt OnCreate + OnClientConnect

  + [MirrorProtoClient(typeof(Msg))] → Implement IPlayerMirrorClient
                                        Field → Read-only property
                                        SyncFromProto()
                                        ApplyUpdate(byte[], string)

Generator quét các property của protobuf message type, bỏ qua PlayerId, IsOnLine, DataName và enum oneof case, rồi sinh code tương ứng.


Custom Attributes

[MirrorProto]

Dùng cho Server — class sở hữu dữ liệu, có quyền ghi và auto-commit.

namespace PubSubLib.Mirror;

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class MirrorProtoAttribute : Attribute
{
    public Type ProtoType { get; }      // Bắt buộc: Type của protobuf message (IMessage)
    public string? DataName { get; set; } // Tùy chọn: override DataName (mặc định = protoType.Name)
}

Ràng buộc:

  • Chỉ gắn lên class (không struct, không enum)
  • Class phải khai báo partial
  • ProtoType phải là concrete protobuf message (kế thừa IMessage)

Code sinh ra:

  • Implement IPlayerData + IPlayerDataInternal
  • Property get/set cho scalar, MirrorRepeatedList<T> cho repeated
  • Commit(string) — serialize thay đổi, gửi qua NATS
  • DoneInit() — public method, kiểm tra IsInitDone, fire IOnCreate.OnCreate(), nếu online thì fire IOnClientConnect.OnClientConnect()
  • OnChange / OnMessage / SendMessage callbacks
  • PlayerId, IsOnLine, DataName tự động

[MirrorProtoClient]

Dùng cho Client — nhận mirror từ server, read-only.

namespace PubSubLib.Mirror;

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class MirrorProtoClientAttribute : Attribute
{
    public Type ProtoType { get; }  // Bắt buộc: cùng protobuf type như server dùng
}

Ràng buộc:

  • Chỉ gắn lên class (không struct, không enum)
  • Class phải khai báo partial
  • ProtoType phải khớp với server

Code sinh ra:

  • Implement IPlayerMirrorClient
  • Property get-only cho mọi field
  • List<T> backing cho repeated, IReadOnlyList<T> expose
  • ApplyUpdate(byte[], string) — parse proto, sync dữ liệu
  • SyncFromProto() — ánh xạ proto → C# fields
  • OnCommit(string) — partial method (bạn có thể override)

Kiểu dữ liệu hỗ trợ

1. Scalar (đơn giản)

message PlayerProfileMsg {
    int64 player_id = 1;   // → long PlayerId (RESERVED - bị bỏ qua)
    int32 level = 2;       // → int Level
    float health = 3;      // → float Health
    bool is_vip = 4;       // → bool IsVip
    string name = 5;       // → string Name
    bytes data = 6;        // → byte[] Data
}
// Server
[MirrorProto(typeof(PlayerProfileMsg), DataName = "PlayerProfile")]
public partial class PlayerProfileData { }

// Client
[MirrorProtoClient(typeof(PlayerProfileMsg))]
public partial class PlayerProfileClient { }

Server: Property có get/set. Commit gửi toàn bộ scalar (không dirty tracking cho value type).

Client: Property get-only. ApplyUpdate() gán giá trị từ proto.

Proto type C# type
int64 long
int32 int
uint32 uint
float float
double double
bool bool
string string
bytes byte[]

2. Repeated (danh sách)

message InventoryMsg {
    repeated string items = 1;       // → MirrorRepeatedList<string> Items
    repeated int32 quantities = 2;   // → MirrorRepeatedList<int> Quantities
}
// Server: MirrorRepeatedList<T> — có IsDirty tracking
var data = manager.CreateData<InventoryData>(playerId);
data.Items.Add("Sword");    // IsDirty = true
data.Items.Add("Shield");
data.Commit("loot");         // serialize Items[], clear dirty

// Client: IReadOnlyList<T> — read-only
var client = playerSpeaks.GetData<InventoryClient>();
foreach (var item in client.Items) { ... }

Server:

  • Backing field là MirrorRepeatedList<T> với IsDirty flag
  • Khi commit: nếu IsDirty → serialize toàn bộ list, nếu không dirty → bỏ qua (tiết kiệm bandwidth)
  • ClearDirty() reset flag sau commit

Client:

  • Backing là List<T>, expose qua IReadOnlyList<T>
  • ApplyUpdate() clear list rồi AddRange từ proto

3. Vector3 (đơn)

message PositionMsg {
    repeated float position_vector3 = 1;  // → Vector3 Position
}

Quy ước: property name kết thúc bằng Vector3, type là repeated float — generator hiểu đây là 3 float (x, y, z).

// Server
data.Position = new Vector3 { x = 1, y = 2, z = 3 };
// Commit → serialize 3 float: [1, 2, 3]

// Client
Vector3 pos = client.Position;  // get-only

4. Vector3 (danh sách)

message PathMsg {
    repeated float waypoints_vector3_s = 1;  // → MirrorRepeatedList<Vector3> Waypoints
}

Quy ước: property name kết thúc bằng Vector3S, type là repeated float. Generator chia flat array thành từng bộ 3 float.

// Server
data.Waypoints.Add(new Vector3 { x = 0, y = 0, z = 0 });
data.Waypoints.Add(new Vector3 { x = 1, y = 2, z = 3 });
// Commit → serialize [0,0,0, 1,2,3] (6 float)

// Client
foreach (var wp in client.Waypoints) { ... }  // IReadOnlyList<Vector3>

5. Struct Group (Struct of Arrays)

Pattern SoA (Struct of Arrays) — tối ưu cho nhiều instance cùng loại. Trigger khi có ít nhất 2 repeated field có tên theo pattern PrefixXStructNameXMemberName.

message TeamMsg {
    repeated int64 struct_x_player_x_id = 1;      // → Players[].Id
    repeated string struct_x_player_x_name = 2;    // → Players[].Name
    repeated int32 struct_x_player_x_level = 3;    // → Players[].Level
}

Quy ước tách tên:

  • Tách PascalCase thành các từ
  • Struct → prefix (bị bỏ qua)
  • Player → tên struct (từ index 1 đến index n-1)
  • Id, Name, Level → tên member (từ cuối cùng)
  • Tối thiểu 2 field chung struct name để kích hoạt

Generator sinh ra:

// Nested struct (tự động)
public readonly struct Player
{
    public long Id { get; }
    public string Name { get; }
    public int Level { get; }
    // Constructor(long id, string name, int level)
}

// Property truy cập
public MirrorRepeatedList<Player> Players { get; }  // plural: StructName + "s"
// Server
data.Players.Add(new Player(1, "Alice", 10));
data.Players.Add(new Player(2, "Bob", 15));
data.Commit("team_update");

// Client
foreach (var p in client.Players)  // IReadOnlyList<Player>
{
    Console.WriteLine($"{p.Name} level {p.Level}");
}

Cách commit struct group:

  1. Clear tất cả proto repeated field (struct_x_player_x_id, struct_x_player_x_name, struct_x_player_x_level)
  2. Duyệt từng Player trong Players
  3. Add p.Idstruct_x_player_x_id
  4. Add p.Namestruct_x_player_x_name
  5. Add p.Levelstruct_x_player_x_level

6. Struct + Vector3 con

Vector3 có thể nằm trong struct group. 2 dạng:

a. Vector3 đơn trong struct
message UnitMsg {
    repeated int64 struct_x_unit_x_id = 1;
    repeated float struct_x_unit_x_position_vector3 = 2;  // → Unit.Position (Vector3)
}

position_vector3: tên member kết thúc bằng Vector3, type repeated float.

Struct sinh ra:

public readonly struct Unit
{
    public long Id { get; }
    public Vector3 Position { get; }
}

Commit: mỗi struct element → 3 float vào proto repeated field.

b. Vector3 array trong struct
message PathUnitMsg {
    repeated int64 struct_x_unit_x_id = 1;
    repeated float struct_x_unit_x_waypoints_vector3_s_value = 2;   // giá trị flat
    repeated int32 struct_x_unit_x_waypoints_vector3_s_count = 3;   // số lượng Vector3 mỗi element
}

Quy ước: Mỗi cặp {prefix}Vector3SValue + {prefix}Vector3SCount trong cùng struct group tạo ra list Vector3 trong struct.

  • *Vector3SValue (repeated float) — tất cả x,y,z của mọi element nối tiếp
  • *Vector3SCount (repeated int32) — số lượng Vector3 trong mỗi element

Struct sinh ra:

public readonly struct Unit
{
    public long Id { get; }
    public IReadOnlyList<Vector3> Waypoints { get; }  // List<Vector3> trên server
}

Commit:

  • Clear *Vector3SValue*Vector3SCount
  • Duyệt từng Unit: add waypoints.Count vào *Vector3SCount, add x,y,z từng waypoint vào *Vector3SValue

7. Primitive Array trong struct

Cho phép mỗi struct entry chứa một mảng kiểu nguyên thủy (int, long, float, double, string, bool).

message TroopData {
    repeated string struct_x_player_x_name = 1;
    repeated int64 struct_x_player_x_bean_array_value = 2;   // giá trị flat
    repeated int32 struct_x_player_x_bean_array_count = 3;   // số phần tử mỗi entry
}

Quy ước: Mỗi cặp {prefix}ArrayValue + {prefix}ArrayCount trong cùng struct group tạo ra mảng {prefix}[] trong struct.

  • *ArrayValue (repeated T) — tất cả giá trị của mọi element nối tiếp
  • *ArrayCount (repeated int32) — số lượng phần tử trong mỗi entry
  • T có thể là bất kỳ kiểu nguyên thủy: int, long, float, double, string, bool

Struct sinh ra:

public readonly struct Player
{
    public string Name { get; }
    public long[] Bean { get; }
}

Commit:

  • Clear *ArrayValue*ArrayCount
  • Duyệt từng Player: add bean.Length vào *ArrayCount, add từng giá trị vào *ArrayValue
  • Array null hoặc rỗng → count = 0, không add value nào

SyncFromProto (client):

  • Dùng *ArrayCount[i] để biết số phần tử của entry thứ i
  • Đọc *ArrayCount[i] phần tử từ *ArrayValue bằng offset tracker

Multiple primitive arrays trong cùng struct:

message MultiArrayMsg {
    repeated int64 struct_x_player_x_bean_array_value = 1;
    repeated int32 struct_x_player_x_bean_array_count = 2;
    repeated float struct_x_player_x_scores_array_value = 3;
    repeated int32 struct_x_player_x_scores_array_count = 4;
}

struct Player { long[] Bean; float[] Scores; }


Quy luật đặt tên proto

DataName

Attribute DataName mặc định Override
[MirrorProto] protoType.Name (vd: "RemoveWatcherCmd") Qua DataName = "..."
[MirrorProtoClient] protoType.Name (không override được)

DataName dùng để:

  • Key phân biệt các loại dữ liệu trong IPlayerSpeaksManager
  • Route mirror message từ server → client đúng type
  • Phải giống nhau giữa server và client để mirror hoạt động

Tên property → tên field

Proto property C# backing field C# property
WatcherId _watcherId WatcherId
Position _position Position
IsOnline _isOnline IsOnline

Quy tắc: first char lowercase + prefix _.

Tên bị reserved (bỏ qua)

Generator luôn bỏ qua các proto property sau:

  • PlayerId — tự động quản lý bởi IPlayerData
  • IsOnLine — tự động quản lý bởi IPlayerSpeaksManager
  • DataName — tự động từ attribute/proto type name
  • Property kết thúc bằng "Case" có type là enum (protobuf oneof case)

Struct group naming

Proto:      Struct_X_Player_X_Id
            ─────── ────── ──
            prefix  struct  member
  • Prefix: Từ đầu tiên (thường là Struct), bị bỏ qua
  • Struct name: Các từ ở giữa (vd: Player, InventoryItem)
  • Member name: Từ cuối cùng (vd: Id, Name, Position)
  • Tách bằng chữ in hoa (PascalCase)

Vector3 naming

Pattern Ý nghĩa Ví dụ
{name}Vector3 Vector3 đơn PositionVector3Vector3 Position
{name}Vector3S List Vector3 WaypointsVector3SList<Vector3> Waypoints
{name}Vector3SValue Array Vector3 trong struct PathVector3SValue + PathVector3SCount
{name}Vector3SCount Đếm số Vector3/element trong struct Đi cùng *Vector3SValue

Primitive Array naming

Pattern Ý nghĩa Ví dụ
{name}ArrayValue Mảng nguyên thủy trong struct BeanArrayValuelong[] Bean
{name}ArrayCount Đếm số phần tử/element Đi cùng *ArrayValue, type repeated int32

Quy tắc chung

1. Class phải là partial

// ĐÚNG
[MirrorProto(typeof(FooMsg))]
public partial class FooData { }

// SAI — thiếu partial
[MirrorProto(typeof(FooMsg))]
public class FooData { }  // Generator không chạy

2. ProtoType phải là concrete protobuf message

// ĐÚNG — PlayerProfileMsg là class sinh từ .proto
[MirrorProto(typeof(PlayerProfileMsg))]

// SAI — IMessage là interface
[MirrorProto(typeof(IMessage))]

3. Struct group cần ít nhất 2 field

// SAI — chỉ 1 field, không đủ để tạo struct group
repeated int64 struct_x_foo_x_id = 1;

// ĐÚNG — 2+ field chung struct name
repeated int64 struct_x_foo_x_id = 1;
repeated string struct_x_foo_x_name = 2;

4. Dirty tracking (server only)

  • Scalar value types (int, float, bool, ...): không dirty tracking — luôn serialize khi commit
  • Repeated fields (MirrorRepeatedList<T>): có IsDirty — chỉ serialize khi có thay đổi
  • Vector3: property setter luôn set dirty cho repeated backing
  • Struct groups: backing là MirrorRepeatedList<T> — dirty khi thêm/xóa

5. Thread safety

  • MirrorProtoBus dùng Channel<Action> — tất cả serialize/commit chạy tuần tự
  • Commit() enqueue action, không block thread gọi
  • Callback OnChange chạy trên background thread

6. Message routing

  • Server Commit("init") → NATS PlayerSpeaks.Msg → Router → TCP → Client ApplyUpdate(data, "init")
  • Server SendMessage<T>(subject, data) → NATS → Router → TCP → Client DispatchMessage(subject, data)
  • Client SendMessage<T>(subject, data) → TCP → Router → NATS → Server OnMessage

So sánh Server vs Client

Khía cạnh Server ([MirrorProto]) Client ([MirrorProtoClient])
Interface IPlayerData + IPlayerDataInternal IPlayerMirrorClient
DataName Có thể override Luôn là protoType.Name
Scalar get/set get-only
Repeated MirrorRepeatedList<T> (có IsDirty) IReadOnlyList<T> (List<T> backing)
Vector3 get/set get-only
Struct group MirrorRepeatedList<Struct> IReadOnlyList<Struct>
Commit Commit(string) — serialize + gửi NATS Không có (chỉ nhận)
Sync Không cần (là source of truth) ApplyUpdate(byte[], string)
SendMessage SendMessage<T>(subject, data) SendMessage<T>(subject, data)
OnMessage OnMessage<T>(subject, callback) OnMessage<T>(subject, callback)

Ví dụ hoàn chỉnh

Proto definition

// PubSubLib.Contracts / Messages / PlayerMessages.proto
syntax = "proto3";

message PlayerProfileMsg {
    int64 player_id = 1;                    // RESERVED - bị bỏ qua
    int32 level = 2;
    int32 gold = 3;
    string nickname = 4;
    repeated string achievements = 5;       // repeated field
    repeated float home_position_vector3 = 6; // Vector3 đơn

    // Struct group: InventoryItem
    repeated int32 struct_x_inventory_item_x_id = 10;
    repeated int32 struct_x_inventory_item_x_count = 11;

    // Struct group: Friend (có Vector3 đơn)
    repeated int64 struct_x_friend_x_player_id = 20;
    repeated string struct_x_friend_x_name = 21;
    repeated float struct_x_friend_x_position_vector3 = 22;
}

Server side

using PubSubLib;
using PubSubLib.Messages;
using PubSubLib.Mirror;

[MirrorProto(typeof(PlayerProfileMsg), DataName = "PlayerProfile")]
public partial class PlayerProfileData { }

// Sử dụng
var manager = IPlayerSpeaksManager.Create(config);
manager.OnDefault<PlayerProfileData>(async profile =>
{
    profile.Level = 1;
    profile.Gold = 100;
    profile.Nickname = "NewPlayer";
    profile.Achievements.Add("first_login");
    profile.HomePosition = new Vector3 { x = 0, y = 0, z = 0 };

    // Struct group
    profile.InventoryItems.Add(new InventoryItem(1001, 5));  // id=1001, count=5
    profile.Friends.Add(new Friend(42, "Alice", new Vector3 { x = 1, y = 2, z = 3 }));

    await Task.CompletedTask;
});

// Commit gửi dữ liệu đến client
var profile = manager.CreateData<PlayerProfileData>(playerId);
profile.Level = 10;
profile.Achievements.Add("level_10");
profile.Commit("level_up");  // serialize + NATS → Router → Client

Client side

using PubSubLib.Mirror;

[MirrorProtoClient(typeof(PlayerProfileMsg))]
public partial class PlayerProfileClient
{
    // Optional: override OnCommit để nhận thông báo khi có update
    partial void OnCommit(string commit)
    {
        Console.WriteLine($"[Client] Received commit: {commit}");
    }
}

// Sử dụng
var playerSpeaks = IPlayerSpeaksClientModule.Create().Get();
playerSpeaks.AddData<PlayerProfileClient>();

// Sau khi kết nối và nhận mirror:
var profile = playerSpeaks.GetData<PlayerProfileClient>();
if (profile != null)
{
    Console.WriteLine($"Level: {profile.Level}, Gold: {profile.Gold}");
    foreach (var item in profile.InventoryItems)
        Console.WriteLine($"  Item {item.Id} x{item.Count}");
    foreach (var friend in profile.Friends)
        Console.WriteLine($"  Friend: {friend.Name} at ({friend.Position.x}, {friend.Position.y})");
}

// Gửi message lên server
profile.SendMessage("buy_item", new BuyItemMsg { ItemId = 1001 }.ToByteArray());

// Nhận message từ server (đã được đăng ký tự động qua OnMessage)
There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has 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.0.1 40 7/16/2026
2.0.0 44 7/15/2026
1.20.5 91 7/6/2026
1.20.3 90 7/6/2026
1.20.1 92 7/6/2026
1.20.0 94 7/6/2026
1.19.9 95 7/6/2026
1.19.8 101 6/24/2026
1.19.7 105 6/24/2026
1.19.5 94 6/23/2026
1.19.4 100 6/23/2026
1.19.3 103 6/23/2026
1.19.2 97 6/23/2026
1.19.1 103 6/23/2026
1.13.0 106 6/22/2026
1.12.0 100 6/22/2026
1.11.0 99 6/21/2026
1.10.0 101 6/21/2026