All-in-One Solution for Indie Game Development · Empowering Indie Developers' Dreams
Documentation · Quick Start · QQ Group: 467608841 / 233840761
ProtoExport is a multi-language code generator for Proto protocol files. It exports five target languages — C#, TypeScript, C++, Lua, and Go — and offers three usage options: CLI, a cross-platform GUI, and Docker images.
- Multi-language export: C# (
.cs), TypeScript (.ts), C++ (.h), Lua (.lua), Go (.go) - Three usage options: CLI (
dotnet ProtoExport.dll), Avalonia GUI (ProtoExporterGUI), and Docker images (linux/amd64,linux/arm64) - Stable message IDs: a lock file persists the
(Module, message name) → SubIdmapping, so protocol numbers no longer drift when proto files are edited - Server-only protocols: proto files ending with
-s/_sare included only in server-side exports - Comment validation: four levels of comment completeness checks —
none/container/member/all - Persistent GUI configuration: saved per mode to
Setting.json, deep-merged with defaults for lossless upgrades
The ProtoExport project is configured to emit its build output to a fixed sibling path: ../Protobuf/Tools/ (relative to the repository root, i.e. <workspace>/Protobuf/Tools/). The path is anchored to the .csproj location via $(MSBuildThisFileDirectory), so it resolves identically on any machine that clones Tools and Protobuf as sibling directories.
This fixed location is consumed by the Proto2*Export.{sh,bat} scripts in the Protobuf repo (e.g. dotnet ./Tools/ProtoExport.dll ...), which expect the DLL at Protobuf/Tools/ProtoExport.dll.
# From the Tools/ProtoExport directory (or the repo root)
dotnet build ProtoExport/ProtoExport.csproj -c Release
# Output lands at: ../Protobuf/Tools/ProtoExport.dll (no TFM/RID subdirectory)Build properties that make the fixed path work (set in ProtoExport/ProtoExport.csproj):
OutputPath=$(MSBuildThisFileDirectory)../../Protobuf/Tools/— single output dir for Debug and Release.AppendTargetFrameworkToOutputPath=false— nonet10.0/subdirectory.AppendRuntimeIdentifierToOutputPath=false— nowin-x64/,linux-arm64/subdirectory.
OutputPathis used instead ofOutDirto avoid being overridden by the SDK's defaultoutput-pathstarget.
| Mode | Output Language | File Extension | Description |
|---|---|---|---|
csharp |
C# | .cs |
For Server, Unity, Godot, Stride, Flax, etc. |
typescript |
TypeScript | .ts |
For LayaAir, Cocos Creator, Phaser, etc. |
cpp |
C++ | .h |
For Unreal Engine, etc. |
lua |
Lua | .lua |
For Defold, Solar2D, Dora SSR, etc. |
go |
Go | .go |
For Go game servers, etc. |
Pre-built Docker images are available for linux/amd64 and linux/arm64.
Docker Hub
docker pull gameframex/gameframex-tools:latestGitHub Container Registry (GHCR)
docker pull ghcr.io/gameframex/gameframex.tools:latestUsage
docker run --rm \
-v /path/to/protos:/protos \
-v /path/to/output:/output \
gameframex/gameframex-tools:latest \
--mode csharp --isServer true --usingStatements "using System|using ProtoBuf|using System.Collections.Generic|using GameFrameX.NetWork.Abstractions|using GameFrameX.NetWork.Messages" --isGenerateDescription true --inputPath /protos --outputPath /output --namespaceName GameFrameX.Proto.ProtoGenerates C# code with [ProtoContract] / [ProtoMember] attributes. All behavior is controlled via CLI parameters — no hardcoded engine-specific logic.
Generates code with server-specific using statements, [Description] attributes, and includes server-only proto files.
Local:
dotnet ProtoExport.dll \
--mode csharp \
--isServer true \
--usingStatements "using System|using ProtoBuf|using System.Collections.Generic|using GameFrameX.NetWork.Abstractions|using GameFrameX.NetWork.Messages" \
--isGenerateDescription true \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../Server/GameFrameX.Proto/Proto \
--namespaceName GameFrameX.Proto.Proto \
--isGenerateErrorCode trueDocker:
docker run --rm \
-v ./Protobuf:/protos \
-v ./Server/GameFrameX.Proto/Proto:/output \
gameframex/gameframex-tools:latest \
--mode csharp --isServer true \
--usingStatements "using System|using ProtoBuf|using System.Collections.Generic|using GameFrameX.NetWork.Abstractions|using GameFrameX.NetWork.Messages" \
--isGenerateDescription true \
--inputPath /protos --outputPath /output --namespaceName GameFrameX.Proto.ProtoGenerates code with Unity-specific using statements. Server-only proto files are automatically skipped.
dotnet ProtoExport.dll \
--mode csharp \
--usingStatements "using System|using ProtoBuf|using System.Collections.Generic|using GameFrameX.Network.Runtime" \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../Unity/Assets/Hotfix/Proto \
--namespaceName Hotfix.Proto \
--isGenerateErrorCode trueSame as Unity but with Godot-specific namespace.
dotnet ProtoExport.dll \
--mode csharp \
--usingStatements "using System|using ProtoBuf|using System.Collections.Generic|using GameFrameX.Network.Runtime" \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../Godot/Proto \
--namespaceName Proto \
--isGenerateErrorCode trueGenerates .ts files with export namespace, export class, and export enum, plus an aggregated ProtoMessageRegister.ts file. Server-only proto files are automatically skipped.
dotnet ProtoExport.dll \
--mode typescript \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../Laya/src/gameframex/protobuf \
--isGenerateErrorCode truedotnet ProtoExport.dll \
--mode typescript \
--importPath "./lib/network/" \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../CocosCreator/assets/scripts/protobuf \
--isGenerateErrorCode trueDocker:
docker run --rm \
-v ./Protobuf:/protos \
-v ./Laya/src/gameframex/protobuf:/output \
gameframex/gameframex-tools:latest \
--mode typescript --inputPath /protos --outputPath /outputGenerates C++ header files with #pragma once, namespace, enum class, and class definitions. Classes with MessageObject base include MESSAGE_ID and Clear() method.
dotnet ProtoExport.dll \
--mode cpp \
--usingStatements "#include <cstdint>|#include <string>|#include <vector>|#include <unordered_map>" \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../Unreal/Source/Proto \
--namespaceName GameFrameX.ProtoGenerates .lua files with LuaDoc (EmmyLua) type annotations and module-based message definitions. Includes a ProtoMessageRegister.lua aggregate file.
dotnet ProtoExport.dll \
--mode lua \
--importPath "./network/" \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../Defold/scripts/protobufDocker:
docker run --rm \
-v ./Protobuf:/protos \
-v ./Defold/scripts/protobuf:/output \
gameframex/gameframex-tools:latest \
--mode lua --importPath "./network/" --inputPath /protos --outputPath /outputGenerates Go struct definitions with protobuf tags, enum type definitions, and a message_register.go aggregate file. Uses --namespaceName as the Go package name (last segment if dot-separated).
dotnet ProtoExport.dll \
--mode go \
--usingStatements "google.golang.org/protobuf/runtime/protoimpl" \
--inputPath ./../../../../../Protobuf \
--outputPath ./../../../../../GoServer/proto \
--namespaceName protoDocker:
docker run --rm \
-v ./Protobuf:/protos \
-v ./GoServer/proto:/output \
gameframex/gameframex-tools:latest \
--mode go --inputPath /protos --outputPath /output --namespaceName protoA cross-platform Avalonia GUI that wraps the ProtoExport CLI, so you can generate code without typing command-line arguments by hand.
- Switch between 7 export modes (Server / Unity / Godot / TypeScript / C++ / Lua / Go) from a dropdown
- Edit every CLI parameter visually (namespace, using/import, comment validation level, error codes, Description, server mode)
- Browse and pick paths with the folder picker (no more typing paths by hand)
- Switch the UI between Chinese and English on the fly
- Persistent configuration (saved per mode to
Setting.jsonin the program directory, deep-merged with defaults for lossless upgrades) - Real-time log output panel
dotnet publish ProtoExporterGUI/ProtoExporterGUI.csproj -c Release -r win-x64 --no-self-contained
# Other RIDs: osx-arm64 / osx-x64 / linux-x64 / linux-arm64The output is a single-file executable (runtime not included; the target machine needs the .NET 10 runtime preinstalled).
- Launch the app
- Select the export mode
- Fill in or browse for the input/output paths
- Click Export
- Check the log panel
The GUI is functionally equivalent to the CLI; for CI/automation scenarios, the CLI or Docker is still recommended.
Pre-built scripts are available in the Protobuf/ directory:
| Script | Description |
|---|---|
Proto2CsExport_Server.sh/.bat |
Export C# for Server |
Proto2CsExport_Client.sh/.bat |
Export C# for Unity Client |
Proto2TsExport.sh/.bat |
Export TypeScript |
Proto2CppExport.sh/.bat |
Export C++ |
Proto2LuaExport.sh/.bat |
Export Lua |
Proto2GoExport.sh/.bat |
Export Go |
This tool has specific requirements for .proto file formatting. Please follow the rules below to ensure correct code generation.
syntax = "proto3"; // Required: only proto3 is supported
package Basic;
option module = 10; // Module ID: file name prefix (_0010_Name.proto) or this declaration; must match when both present
// Request heartbeat
message ReqHeartBeat
{
int64 Timestamp = 1; // Timestamp
}- Request messages: Must start with
Req(e.g.,ReqLogin,ReqHeartBeat) - Response messages: Must start with
Resp(e.g.,RespLogin) - Notification messages: Must start with
Notify(e.g.,NotifyBagInfoChanged) - All message names, field names, enum names, and enum values must use UpperCamelCase
Module ID can be declared in two ways, with the file name prefix taking precedence:
- File name prefix:
_<moduleID>_<name>.proto(e.g.,_0010_Basic.proto→ module 10,_-0120_Inner_Social.proto→ module -120;-is also accepted as the separator, e.g.,_0010-Basic.proto) - option declaration:
option module = <id>;
Rules:
- When both are present they must match, otherwise the build fails
- A file name starting with
_+ digits but missing the second separator (e.g.,_0010Basic) is treated as a naming error and fails the build - When neither is present the build fails
| ID Range | Purpose |
|---|---|
0 ~ 32767 |
Client-Server communication |
-32768 ~ -1 |
Server-Server communication |
- Message field numbers must be less than 800 (values >= 800 are system-reserved and will cause parse errors)
ErrorCodeis a reserved field name in response messages — do not define it manually.Respmessages automatically generate anErrorCodefield
- No nested types: Nesting of
message,enum, or any custom type inside another message is not supported - No RPC definitions: RPC service definitions in proto files are not supported
- Only proto3:
syntax = "proto3";is required; proto2 is not supported
- Add a comment line above message and enum definitions:
// Request heartbeat
message ReqHeartBeat
{
int64 Timestamp = 1;
}- Add inline comments at the end of field lines:
// Player information
message PlayerInfo
{
int64 Id = 1; // Player ID
string Name = 2; // Player name
uint32 Level = 3; // Player level
int32 State = 4; // Player state
}The TestProtos/ directory contains example proto files covering all major patterns:
| File | Pattern | Module ID |
|---|---|---|
heartbeat.proto |
Basic Req/Resp | 1 (client-server) |
player.proto |
Req/Resp/Notify + enum + map | 2 (client-server) |
bag.proto |
enum + repeated + map + Notify | 3 (client-server) |
admin-s.proto |
Server-only proto (-s suffix) |
99 (client-server) |
server-internal-s.proto |
Server-server communication (negative module ID) | -1 (server-server) |
| Parameter | Required | Default | Description |
|---|---|---|---|
--mode |
Yes | - | Language mode: csharp, typescript, cpp, lua, go |
--inputPath |
Yes | - | Path to the .proto files directory |
--outputPath |
Yes | - | Output path for generated files |
--namespaceName |
No | "" |
Namespace for generated code (C# only, ignored by TypeScript) |
--isGenerateErrorCode |
No | true |
Whether to auto-generate ErrorCode field in response messages |
--requireComments |
No | none |
Comment validation level: none (no validation), container (message/enum must have comments), member (fields/enum members must have comments), all (both) |
| Parameter | Required | Default | Description |
|---|---|---|---|
--usingStatements |
No | "" |
Using statements separated by | (e.g., "using System|using ProtoBuf|using System.Collections.Generic") |
--isGenerateDescription |
No | false |
Whether to generate [System.ComponentModel.Description] attributes |
--isServer |
No | false |
Whether to include server-only proto files (files ending with -s or _s) |
Note: boolean options (
--isGenerateErrorCode,--isGenerateDescription,--isServer, etc.) are presence-based flags: passing the flag turns it on —--xxx falsecannot turn it off (thefalseliteral is ignored). The "Default" column is the value used when the option is omitted (e.g.--isGenerateErrorCodeis on even when omitted).
| Parameter | Required | Default | Description |
|---|---|---|---|
--importPath |
No | "../network/" |
Import path prefix for generated import statements |
--isGenerateDescription |
No | false |
Whether to generate JSDoc-style comments |
| Parameter | Required | Default | Description |
|---|---|---|---|
--isGenerateErrorCodeExcelFile |
No | true |
Whether to generate error code Excel file |
--errorCodeExcelFilePath |
No | "" |
Custom path for error code Excel file |
The legacy MessageIdHandler assigned SubIds by auto-incrementing in line order within each proto file (starting from 10 per module). This means any structural edit to a proto file shifts the entire module's SubIds:
| Operation | Consequence |
|---|---|
| Insert a message in the middle | All messages after it get SubId +1 |
| Reorder message definitions | The affected messages swap SubIds |
| Delete a message | All messages after it get SubId -1 |
SubId is the addressing key of the runtime network protocol (MessageID = (Module << 16) | SubId). Once it drifts, the registries generated for C# / C++ / Go / Lua / TypeScript become wholesale misaligned with old online clients: historical Opcodes carried by old packets get parsed as a different message. Line order is merely a textual property and should never participate in protocol semantics.
Once enabled, MessageIdCoordinator persists the (Module, message name) → SubId mapping to a JSON lock file (proto-message-ids.lock.json). Allocation decisions are based purely on names, never on line order:
| Scenario | Allocation Decision |
|---|---|
| Message name already in the lock | Reuse the historical SubId; never rewritten |
| Message name not in the lock | max(used IDs) + 1; gaps are never backfilled |
| Present in the lock but missing from the current protos (deleted/renamed) | Moved to the retired section, never recycled |
Key rules:
schemaVersion: the lock file carries a schema version; on an incompatible version the exporter fails outright instead of silently reshuffling- Each module (
option module = <id>) counts independently; ID spaces are not shared across modules - The valid SubId range is
1..65535(16 bits); the starting point within a module is10 - Serialization emits keys in dictionary order, so diffs contain only real changes and are easy to review in PRs
- Lock file writes are atomic (write to
.tmpfirst, then rename)
# 1. Generate the seed once: freeze the Opcodes "as of this moment" as the lock starting point
bash tools/migrate-message-id-lock.sh
# Equivalent CLI:
# dotnet ProtoExport.dll --inputPath <protos> --outputPath <out> \
# --mode csharp --messageIdLockPath ./proto-message-ids.lock.json --regenerate-lock
# 2. Review and commit the lock file to git
git add proto-message-ids.lock.json && git commit -m "chore(proto): freeze message id lock"
# 3. Every export from now on (local + CI + Docker) must pass the lock path
dotnet ProtoExport.dll ... --messageIdLockPath ./proto-message-ids.lock.jsonNote: Migration does not roll back historical drift. The misalignment caused by the old version's line-order auto-increment has already happened; the seed merely freezes the current state so that allocation stays stable from here on. If drift has already reached production, a separate protocol alignment effort is required.
Related CLI parameters:
| Parameter | Description |
|---|---|
--messageIdLockPath |
Path to the lock file; leave it empty to disable lock mode and fall back to the legacy line-order auto-increment behavior (backward compatible) |
--print-lock |
Read and print the lock file contents without triggering an export (for review) |
--regenerate-lock |
Serialize the currently parsed Opcodes into a lock seed (one-time migration use) |
When two people add messages to the same module concurrently, both compute the same max+1, so the lock file is bound to conflict on merge. Resolution:
- Take either side of the lock file (the two conflicting versions are equivalent in content, differing only in entry ordering)
- Re-run the exporter once (with
--messageIdLockPath) so the message merged later is automatically reassigned the next available ID - Commit the lock file again
This is a feature, not a defect: surfacing the conflict guarantees that "one SubId never has two owners" — silent auto-merge could instead let two messages share the same SubId and cause parsing misalignment at runtime. The same applies to the retired section: IDs of deleted/renamed messages stay occupied forever, and new messages can only take max+1.
When using Docker, paths are mapped as follows:
-v <host-path>:<container-path>mounts a host directory into the container--inputPathand--outputPathmust reference the container-side paths (e.g./protos,/output), not the host paths
# Example: host ./my-protos -> container /protos
docker run --rm \
-v $(pwd)/my-protos:/protos \
-v $(pwd)/my-output:/output \
gameframex/gameframex-tools:latest \
--mode csharp --isServer true \
--usingStatements "using System|using ProtoBuf|using System.Collections.Generic|using GameFrameX.NetWork.Abstractions|using GameFrameX.NetWork.Messages" \
--isGenerateDescription true \
--inputPath /protos --outputPath /output --namespaceName GameFrameX.Proto.Proto- .NET 10 SDK — build the project and run the CLI
- Avalonia — cross-platform UI framework for the GUI tool (ProtoExporterGUI)
- Docker (optional) — run the exporter as a container
See CHANGELOG.md.
See LICENSE.md for license information.