40-codegen-protocol
Status: ACTIVE
AppliesTo: v10
SSOT: skills/devian-core/03-ssot/SKILL.md
Purpose
PROTOCOL(DomainType=PROTOCOL) 입력으로부터 C#/TS 프로토콜 코드를 생성하는 전체 흐름을 정의한다.
이 문서는 입력 포맷 / 레지스트리(결정성) / 경로 규약만 규정한다. 생성 코드의 구체적 API/산출물은 런타임/제너레이터 코드를 정답으로 본다.
Inputs
입력은 {buildInputJson}의 protocols 섹션(배열)이 정본이다.
{buildInputJson}위치는 유동적이다. 현재 프로젝트에서는input/input_common.json에 위치한다.
"protocols": [
{
"group": "Game",
"protocolDir": "./Protocols/Game",
"protocolFiles": ["C2Game.json", "Game2C.json"]
}
]
- •
group: ProtocolGroup 이름 (C# 프로젝트명, TS 폴더명에 사용) - •
protocolDir: Protocol JSON 및 Registry 파일이 위치한 디렉토리 - •
protocolFiles: 처리할 Protocol JSON 파일 목록 - •파일명 base가 ProtocolName이 된다. (예:
C2Game.json→C2Game)
금지 필드 (존재 시 빌드 실패):
- •
csTargetDir—csConfig.generateDir사용 - •
tsTargetDir—tsConfig.generateDir사용 - •
upmName— 자동 계산 (com.devian.protocol.{normalize(group)})
normalize 규칙 (요약): trim → 공백을
_로 치환 → 허용 문자 외 제거(영문/숫자/_/-만 남김) → 소문자화. 정확한 규칙은 빌더의normalizeUpmSuffixFromGroup()참조.
Protocol Spec JSON (필수 필드)
최소 구조:
{
"direction": "client_to_server | server_to_client | bidirectional",
"messages": [
{
"name": "MessageName",
"opcode": 100, // optional
"fields": [
{ "name": "field", "type": "int32", "tag": 1, "optional": true }
]
}
]
}
추가 키가 존재할 수 있다. "지원 여부/정확한 스키마"는 코드를 정답으로 본다.
Determinism Gate (opcode / tag)
Protocol 호환성을 위해 Registry 파일을 사용한다.
- •
{ProtocolName}.opcodes.json - •
{ProtocolName}.tags.json
Registry 파일은 protocolDir/Generated/에 위치하며, 빌드 시 갱신된다.
Registry는 "생성된 입력" 파일로, 기계가 생성하지만 입력 폴더에 보존된다.
정책:
- •명시 값 우선
- •레지스트리 값은 호환성 보존을 위해 유지
- •미지정 값은 결정적 규칙으로 자동 할당
- •Tag의 reserved range(19000..19999) 금지
자동 할당의 상세 규칙(최소값/정렬/증가)은 코드를 정답으로 본다.
Outputs & Paths
경로 규약은 SSOT를 따른다.
C# (ProtocolGroup = {ProtocolGroup}):
- •staging:
{tempDir}/Devian.Protocol.{ProtocolGroup}/cs/Generated/{ProtocolName}.g.cs - •final:
{csConfig.generateDir}/Devian.Protocol.{ProtocolGroup}/Generated/{ProtocolName}.g.cs - •프로젝트 파일:
{csConfig.generateDir}/Devian.Protocol.{ProtocolGroup}/Devian.Protocol.{ProtocolGroup}.csproj(수기/고정, 빌더가 생성/수정 금지) - •namespace:
Devian.Protocol.{ProtocolGroup}(변경 금지)
TypeScript:
- •staging:
{tempDir}/{ProtocolGroup}/ts/Generated/{ProtocolName}.g.ts - •final:
{tsConfig.generateDir}/devian-protocol-{protocolgroup}/Generated/{ProtocolName}.g.ts - •
index.ts는 모듈 루트에 존재하되 수기/고정, 빌더가 생성/수정 금지 - •패키지명:
@devian/protocol-{protocolgroup}
생성물 namespace 고정 (Hard Rule): C# 생성물 namespace는
Devian.Protocol.{ProtocolGroup}으로 고정이며, 런타임 모듈 단일화와 무관하게 변경하지 않는다.
C# Handlers 생성 (Hard Rule)
Stub abstract 메서드를 전부 구현해야 하는 부담을 제거하기 위해, Handlers 클래스를 자동 생성한다.
생성 파일:
- •staging:
{tempDir}/Devian.Protocol.{ProtocolGroup}/cs/Generated/{ProtocolName}_Handlers.g.cs - •final:
{csConfig.generateDir}/Devian.Protocol.{ProtocolGroup}/Generated/{ProtocolName}_Handlers.g.cs
namespace 고정:
- •
Devian.Protocol.{ProtocolGroup}(변경 금지)
생성 형태:
// {ProtocolName}_Handlers.g.cs
namespace Devian.Protocol.{ProtocolGroup}
{
public partial class {ProtocolName}_Handlers : {ProtocolName}.Stub
{
protected override void OnFoo({ProtocolName}.EnvelopeMeta meta, {ProtocolName}.Foo message)
{
OnFooImpl(meta, message);
}
partial void OnFooImpl({ProtocolName}.EnvelopeMeta meta, {ProtocolName}.Foo message);
// ... 모든 메시지에 대해 동일 패턴
}
}
핵심 규칙:
- •
*2C_Handlers : *2C.Stub형태로 상속 - •override는 생성 코드가 수행
- •실제 사용자 구현은
partial void On{Message}Impl(...)로 위임 - •partial 미구현 시 no-op (호출 제거)로 간주
빌더 touch 범위 정책 유지:
- •Protocol UPM에서 빌더가 손대는 건
Runtime/Generated/**뿐 - •
_Handlers.g.cs는Generated/**안에 생성되므로 정책 위반 아님
UPM 산출물 정책 (Hard Rule)
Protocol UPM(com.devian.protocol.*)은 Runtime-only이며, 빌더가 touch 가능한 범위는 Generated/ 뿐이다.**
| 대상 | 빌더 동작 |
|---|---|
Runtime/Devian.Protocol.{Group}.asmdef | 수기 파일 (빌더 수정 금지) |
Runtime/Generated/{ProtocolName}.g.cs | ✅ 생성/갱신 |
package.json | 수기 파일 (빌더 수정 금지) |
Editor/ 폴더 | ❌ 생성 금지, 존재 시 레거시 청소로 삭제 |
Runtime asmdef references 정책:
- •
Devian.Core - •
Devian.Domain.Common
SSOT:
skills/devian-protocol/03-ssot/SKILL.md— Protocol UPM 산출물 정책
Unity Compatibility (Hard Rule)
Unity 환경에서의 호환성을 위해 다음 규칙을 강제한다.
C# Protocol 코드 생성 시:
- •
System.Text.Json 사용 금지
- •Unity는
System.Text.Json을 기본 제공하지 않음 - •
using System.Text.Json;생성 금지 - •
JsonSerializer,JsonSerializerOptions등 사용 금지
- •Unity는
- •
CodecJson 생성 금지
- •JSON 코덱은 생성하지 않음
- •
CodecProtobuf만 생성 (기본 코덱) - •Stub 생성자 기본값도
CodecProtobuf사용
- •
ICodec 인터페이스
- •인터페이스는 유지 (확장성)
- •기본 구현체는
CodecProtobuf만 제공
Protocol 모듈 의존성 (Hard Rule)
C# PROTOCOL 모듈 의존성:
- •
Devian.Protocol.{ProtocolGroup}.csproj는 다음을 ProjectReference 한다:- •
..\Devian\Devian.csproj - •
..\Devian.Domain.Common\Devian.Domain.Common.csproj
- •
- •각 생성물(
{ProtocolName}.g.cs)은using Devian;을 포함해야 한다. (namespace는 Devian 단일)
TS PROTOCOL 패키지 의존성:
- •
@devian/protocol-{protocolgroup}는@devian/core+@devian/module-common을 의존한다.
Unity UPM:
- •Protocol용
.asmdef파일의references에Devian.Domain.Common포함 필수 - •예:
Devian.Protocol.Game.asmdef" → "references": [..., "Devian.Domain.Common"]
TypeScript Namespace 규칙
TS 생성물은 ProtocolName 단위로 네임스페이스가 생성된다.
생성 형태:
// {ProtocolName}.g.ts
export namespace {ProtocolName} {
export interface MessageName { ... }
export const Opcodes = { ... } as const;
}
핵심 규칙:
- •
.g.ts파일은export namespace {ProtocolName}만 생성 - •
index.ts에서 Direct export 제공 - •소비자 코드는 Direct import를 사용
생성 예시 (index.ts):
import * as C2GameMod from './C2Game.g';
import * as Game2CMod from './Game2C.g';
export const C2Game = C2GameMod.C2Game;
export const Game2C = Game2CMod.Game2C;
export { createServerRuntime } from './Generated/ServerRuntime.g';
export { createClientRuntime } from './Generated/ClientRuntime.g';
사용법 (권장):
import { C2Game, Game2C, createClientRuntime } from '@devian/protocol-game';
// 타입 사용
const req: C2Game.LoginRequest = { ... };
const ack: Game2C.LoginAck = { ... };
// Opcode 사용
const opcode = C2Game.Opcodes.LoginRequest;
ServerRuntime / ClientRuntime 생성 (TypeScript)
Protocol 그룹에 inbound와 outbound가 정확히 1개씩 존재하면 Runtime을 자동 생성한다.
ServerRuntime (서버 관점):
- •inbound: client_to_server (예: C2Game)
- •outbound: server_to_client (예: Game2C)
ClientRuntime (클라이언트 관점):
- •inbound: server_to_client (예: Game2C)
- •outbound: client_to_server (예: C2Game)
생성 조건:
- •inbound 1개 + outbound 1개 → 생성
- •bidirectional만 존재 → 생성 안함 (정상)
- •그 외 (0개, 2개 이상, 한쪽만 존재) → 빌드 에러
생성 파일:
- •
{tsConfig.generateDir}/devian-protocol-{group}/Generated/ServerRuntime.g.ts - •
{tsConfig.generateDir}/devian-protocol-{group}/Generated/ClientRuntime.g.ts
TypeScript package.json (생성 산출물)
devian-protocol-* 패키지의 package.json은 빌드 시스템이 생성하는 산출물이다.
수정 금지 정책:
- •수동 편집 금지
- •빌드 시 덮어쓰기됨
생성 내용:
- •
name:@devian/protocol-{group} - •
exports:.+ Runtime 존재 시./server-runtime,./client-runtime - •
dependencies:@devian/core
위 dependencies 목록에는 항상
@devian/module-common이 포함되어야 한다. (참조 판정 없음)
Implementation Reference (정본 위치)
구현 정본 파일:
| 파일 | 함수 | 역할 |
|---|---|---|
framework-ts/tools/builder/build.js | generateCsproj(...) | C# csproj 생성/보정 (ProtocolGroup 포함) |
framework-ts/tools/builder/build.js | ensureProtocolPackageJson(...) | TS package.json 생성/보정 |
framework-ts/tools/builder/generators/protocol-cs.js | generateCSharpProtocol(...) | C# {ProtocolName}.g.cs 생성 |
의존성 Hard Rule이 실제로 강제되는 지점:
- •C#:
- •csproj:
generateCsproj(...)가Devian.csproj+Devian.Domain.Common.csprojProjectReference 포함 - •g.cs:
generateCSharpProtocol(...)가using Devian;포함
- •csproj:
- •TypeScript:
- •package.json:
ensureProtocolPackageJson(...)가 dependencies에@devian/core+@devian/module-common포함
- •package.json:
Verification Checklist (Hard)
빌드 후 반드시 확인해야 하는 사항:
C#:
- •
생성된
framework-cs/module/Devian.Protocol.{ProtocolGroup}/Devian.Protocol.{ProtocolGroup}.csproj에..\Devian\Devian.csproj+..\Devian.Domain.Common\Devian.Domain.Common.csprojProjectReference 존재 - •
생성된
framework-cs/module/Devian.Protocol.{ProtocolGroup}/{ProtocolName}.g.cs상단에using Devian;존재 - •
생성된
{ProtocolName}.g.cs에System.Text.Json관련 코드 없음 - •
생성된
{ProtocolName}.g.cs에CodecJson클래스 없음
TypeScript:
- •생성된
framework-ts/module/devian-protocol-{group}/package.jsondependencies에
@devian/module-common존재
Unity UPM:
- •Protocol용
.asmdef파일의references에Devian.Domain.Common존재
Protocol Message Pooling
생성된 프로토콜 코드는 메시지 객체 풀링을 지원한다. 풀링은 새 API로 제공되며, 기존 Decode<T>()/Decode(opcode)는 호환성을 위해 그대로 유지된다.
생성 API (필수)
각 {ProtocolName}.g.cs의 public static partial class {ProtocolName} 내부에 아래 API가 생성되어야 한다:
// 풀 필드 (메시지 타입별, max=256) private static readonly PacketPool<Foo> _pool_Foo = new PacketPool<Foo>(256); // opcode 기반 풀링 디코드 public static object? RentDecodePooled(int opcode, ReadOnlySpan<byte> data); // 제네릭 풀링 디코드 public static T RentDecodePooled<T>(ReadOnlySpan<byte> data) where T : class, new(); // 반환 (타입 패턴 매칭) public static void ReturnPooled(object message); // 제네릭 반환 public static void ReturnPooled<T>(T message) where T : class;
Reset 규약 (필수)
풀에서 꺼낸 메시지는 디코드 전에 반드시 _Reset() 호출해야 한다.
List<T>? 필드의 Reset 처리:
- •
null로 버리지 말고Clear()호출 - •생성 형태:
if (Field != null) Field.Clear();
기존 API 호환성
- •
CodecProtobuf.Decode<T>(),Decode(opcode)는 그대로 유지 - •풀링은 새 API(
RentDecodePooled/ReturnPooled)로만 제공 - •기존 사용자 코드 파괴 금지
DoD (완료 정의)
- • 생성된
.g.cs에서PacketPool<T>가 "정의만" 되지 않고,RentDecodePooled/ReturnPooled에서 실제로 사용됨 - •
_Reset()메서드에서List<T>필드는?.Clear()형태로 리셋됨 - • 기존
Decode<T>()/Decode(opcode)는 그대로 유지됨 (호환성) - •
ReturnPooled(object)는 패턴 매칭으로 타입 분기하며,default는 조용히 무시
Reference
- •Policy SSOT:
skills/devian-core/03-ssot/SKILL.md - •동작 정본: 런타임/제너레이터 코드