AgentSkillsCN

40 Codegen Protocol

40 Codegen 协议

SKILL.md

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에 위치한다.

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.jsonC2Game)

금지 필드 (존재 시 빌드 실패):

  • csTargetDircsConfig.generateDir 사용
  • tsTargetDirtsConfig.generateDir 사용
  • upmName — 자동 계산 (com.devian.protocol.{normalize(group)})

normalize 규칙 (요약): trim → 공백을 _로 치환 → 허용 문자 외 제거(영문/숫자/_/-만 남김) → 소문자화. 정확한 규칙은 빌더의 normalizeUpmSuffixFromGroup() 참조.

Protocol Spec JSON (필수 필드)

최소 구조:

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는 "생성된 입력" 파일로, 기계가 생성하지만 입력 폴더에 보존된다.

정책:

  1. 명시 값 우선
  2. 레지스트리 값은 호환성 보존을 위해 유지
  3. 미지정 값은 결정적 규칙으로 자동 할당
  4. 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} (변경 금지)

생성 형태:

csharp
// {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.csGenerated/** 안에 생성되므로 정책 위반 아님

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 코드 생성 시:

  1. System.Text.Json 사용 금지

    • Unity는 System.Text.Json을 기본 제공하지 않음
    • using System.Text.Json; 생성 금지
    • JsonSerializer, JsonSerializerOptions 등 사용 금지
  2. CodecJson 생성 금지

    • JSON 코덱은 생성하지 않음
    • CodecProtobuf만 생성 (기본 코덱)
    • Stub 생성자 기본값도 CodecProtobuf 사용
  3. 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 파일의 referencesDevian.Domain.Common 포함 필수
  • 예: Devian.Protocol.Game.asmdef" → "references": [..., "Devian.Domain.Common"]

TypeScript Namespace 규칙

TS 생성물은 ProtocolName 단위로 네임스페이스가 생성된다.

생성 형태:

typescript
// {ProtocolName}.g.ts
export namespace {ProtocolName} {
    export interface MessageName { ... }
    export const Opcodes = { ... } as const;
}

핵심 규칙:

  1. .g.ts 파일은 export namespace {ProtocolName}만 생성
  2. index.ts에서 Direct export 제공
  3. 소비자 코드는 Direct import를 사용

생성 예시 (index.ts):

typescript
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';

사용법 (권장):

typescript
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.jsgenerateCsproj(...)C# csproj 생성/보정 (ProtocolGroup 포함)
framework-ts/tools/builder/build.jsensureProtocolPackageJson(...)TS package.json 생성/보정
framework-ts/tools/builder/generators/protocol-cs.jsgenerateCSharpProtocol(...)C# {ProtocolName}.g.cs 생성

의존성 Hard Rule이 실제로 강제되는 지점:

  • C#:
    • csproj: generateCsproj(...)Devian.csproj + Devian.Domain.Common.csproj ProjectReference 포함
    • g.cs: generateCSharpProtocol(...)using Devian; 포함
  • TypeScript:
    • package.json: ensureProtocolPackageJson(...)가 dependencies에 @devian/core + @devian/module-common 포함

Verification Checklist (Hard)

빌드 후 반드시 확인해야 하는 사항:

C#:

  1. 생성된 framework-cs/module/Devian.Protocol.{ProtocolGroup}/Devian.Protocol.{ProtocolGroup}.csproj..\Devian\Devian.csproj + ..\Devian.Domain.Common\Devian.Domain.Common.csproj ProjectReference 존재

  2. 생성된 framework-cs/module/Devian.Protocol.{ProtocolGroup}/{ProtocolName}.g.cs 상단에 using Devian; 존재

  3. 생성된 {ProtocolName}.g.csSystem.Text.Json 관련 코드 없음

  4. 생성된 {ProtocolName}.g.csCodecJson 클래스 없음

TypeScript:

  1. 생성된 framework-ts/module/devian-protocol-{group}/package.json dependencies에
    @devian/module-common 존재

Unity UPM:

  1. Protocol용 .asmdef 파일의 referencesDevian.Domain.Common 존재

Protocol Message Pooling

생성된 프로토콜 코드는 메시지 객체 풀링을 지원한다. 풀링은 새 API로 제공되며, 기존 Decode<T>()/Decode(opcode)는 호환성을 위해 그대로 유지된다.

생성 API (필수)

{ProtocolName}.g.cspublic static partial class {ProtocolName} 내부에 아래 API가 생성되어야 한다:

csharp
// 풀 필드 (메시지 타입별, 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
  • 동작 정본: 런타임/제너레이터 코드