Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(dotnet test:*)"
]
},
"enableAllProjectMcpServers": false
}
4 changes: 4 additions & 0 deletions .github/workflows/nuget_push.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: NugetPush
on:
workflow_dispatch: {}
pull_request:
types: [closed]
branches:
- main

jobs:
push:
Expand Down
71 changes: 71 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

CommandForgeGenerator is a C# Source Generator that converts YAML-based command definitions (commands.yaml) from CommandForgeEditor into strongly-typed C# code. It generates type-safe command classes from game scripting definitions.

## Build and Development Commands

### Building the Project
```bash
# Build entire solution
dotnet build

# Build specific project
dotnet build CommandForgeGenerator/CommandForgeGenerator.csproj

# Build in Release mode
dotnet build -c Release

# Create NuGet package
dotnet pack CommandForgeGenerator/CommandForgeGenerator.csproj -c Release
```

### Running Tests
```bash
# Run all tests
dotnet test

# Run tests with detailed output
dotnet test --logger "console;verbosity=detailed"
```

## Architecture Overview

### Source Generator Pipeline
1. **Entry Point**: `CommandForgeGeneratorSourceGenerator` (CommandForgeGenerator.cs:9) - Implements `IIncrementalGenerator`
2. **YAML Processing**: `CommandSemanticsLoader` (Semantic/CommandSemanticsLoader.cs) - Parses commands.yaml files
3. **Code Generation**: `CodeGenerator` (CodeGenerate/CodeGenerator.cs) - Generates C# classes from parsed semantics

### Key Components

**YAML to Semantics Flow**:
- YAML files are converted to JSON using embedded YamlDotNet
- JSON is parsed using custom parser (`Json/JsonParser.cs`)
- Semantics are extracted into `CommandsSemantics` data structures

**Generated Code Structure**:
- `ICommandForgeCommand.g.cs` - Base interface for all commands
- `CommandId.g.cs` - Enum for command identifiers
- `[CommandName].g.cs` - Individual command classes with typed properties
- `CommandForgeLoader.g.cs` - Loader that deserializes JSON into command objects

**Property Type Mapping**:
- `string`, `int`, `float`, `bool` - Basic types
- `enum` → `string`
- `command` → `CommandId`
- `vector2/3/4` → `UnityEngine.Vector2/3/4`
- `vector2/3int` → `UnityEngine.Vector2/3Int`

### Important Implementation Details

1. **Conditional Compilation**: All generated code is wrapped in `#if ENABLE_COMMAND_FORGE_GENERATOR`
2. **Newtonsoft.Json Dependency**: Generated code requires Newtonsoft.Json for deserialization
3. **Unity Integration**: Vector types use global::UnityEngine namespace
4. **Error Handling**: Exceptions during generation create an Error.g.cs file with diagnostics

## Testing Approach

Tests use xUnit and Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit for verifying generated code. Test projects reference the generator as an Analyzer.
39 changes: 39 additions & 0 deletions CommandForgeGenerator.Tests/GenerateSampleTextCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#if ENABLE_COMMAND_FORGE_GENERATOR
namespace CommandForgeGenerator.Command
{
public partial class TextCommand : ICommandForgeCommand
{
public const string Type = "text";
public readonly CommandId CommandId;

public readonly string CharacterId;
public readonly bool IsOverrideCharacterName;
public readonly string? OverrideCharacterName;
public readonly string Body;


public static TextCommand Create(int commandId, global::Newtonsoft.Json.Linq.JToken json)
{

var CharacterId = (string)json["characterId"];
var IsOverrideCharacterName = (bool)json["isOverrideCharacterName"];
var OverrideCharacterName = json["overrideCharacterName"] == null ? null : (string)json["overrideCharacterName"];
var Body = (string)json["body"];


return new TextCommand(commandId, CharacterId, IsOverrideCharacterName, OverrideCharacterName, Body);
}

public TextCommand(int commandId, string CharacterId, bool IsOverrideCharacterName, string? OverrideCharacterName, string Body)
{
CommandId = (CommandId)commandId;

this.CharacterId = CharacterId;
this.IsOverrideCharacterName = IsOverrideCharacterName;
this.OverrideCharacterName = OverrideCharacterName;
this.Body = Body;

}
}
}
#endif
6 changes: 6 additions & 0 deletions CommandForgeGenerator.Tests/GenerateSampleUtl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CommandForgeGenerator.Command;

// 他の生成結果サンプルコマンドをコンパイルエラーにしないためのコード

public interface ICommandForgeCommand { }
public enum CommandId{}
10 changes: 10 additions & 0 deletions CommandForgeGenerator.Tests/GenerateTestCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.IO;

namespace CommandForgeGenerator.Tests;

public class GenerateTestCode
{
// プロジェクトファイルに存在する GenerateSampleTextCommand.cs を取得する
public static string TextCommandStr => File.ReadAllText("../../../GenerateSampleTextCommand.cs");
public static string YamlFileStr => File.ReadAllText("../../../sampleCommands.yaml");
}
180 changes: 7 additions & 173 deletions CommandForgeGenerator.Tests/Test.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Linq;
using CommandForgeGenerator.Generator.CodeGenerate;
using CommandForgeGenerator.Generator.Json;
using CommandForgeGenerator.Generator.Semantic;
Expand Down Expand Up @@ -59,184 +61,16 @@ public void JsonParserTest()
[Fact]
public void GenerateTest()
{
var yaml = GetSampleYaml();
var yaml = GenerateTestCode.YamlFileStr;
var commandsSchema = CommandSemanticsLoader.GetCommandSemantics(yaml);
var codeFiles = CodeGenerator.Generate(commandsSchema);

Assert.Equal(16, codeFiles.Count);
var file = codeFiles.FirstOrDefault(c => c.FileName == "TextCommand.g.cs").Code;

#region Internal
//File.WriteAllText("/Users/katsumi.sato/Desktop/a/TextCommand.g.cs", file);

string GetSampleYaml()
{
return """
version: 1
commands:
- id: text
label: テキスト
description: 台詞を表示
commandListLabelFormat: "{character}「{body}」"
properties:
character:
type: enum
options: ["キャラA", "キャラB", "キャラC", "キャラD", "先生", "店員"]
required: true
body:
type: string
multiline: true
required: true

- id: emote
label: エモート
description: 立ち絵・表情切替
commandListLabelFormat: "EMOTE: {character}, {emotion}"
properties:
character:
type: enum
options: ["キャラA", "キャラB", "キャラC", "キャラD", "先生", "店員"]
required: true
emotion:
type: enum
options: ["通常", "笑顔", "驚き", "怒り", "悲しみ", "困惑", "照れ", "恐怖", "喜び", "真剣"]
required: true

- id: wait
label: 待機
description: 指定秒数だけウェイト
commandListLabelFormat: "WAIT: {seconds}"
defaultBackgroundColor: '#57e317'
properties:
seconds:
type: number
default: 0.5
constraints:
min: 0

- id: bgm
label: BGM
description: 背景音楽を変更
commandListLabelFormat: "BGM: {track}, volume={volume}"
properties:
track:
type: enum
options: ["なし", "日常", "緊張", "悲しい", "楽しい", "神秘的", "アクション", "ロマンティック", "エンディング"]
required: true
volume:
type: number
default: 1.0
constraints:
min: 0
max: 1.0

- id: sound
label: 効果音
description: 効果音を再生
commandListLabelFormat: "SOUND: {effect}, volume={volume}"
properties:
effect:
type: enum
options: ["ドア", "足音", "衝撃", "爆発", "鐘", "拍手", "警報", "雨", "雷", "風"]
required: true
volume:
type: number
default: 1.0
constraints:
min: 0
max: 1.0

- id: background
label: 背景
description: 背景画像を変更
commandListLabelFormat: "BG: {scene}, effect={transition}"
properties:
scene:
type: enum
options: ["教室", "廊下", "体育館", "屋上", "公園", "駅", "カフェ", "自宅", "図書館", "商店街"]
required: true
transition:
type: enum
options: ["なし", "フェード", "ワイプ", "クロスフェード", "フラッシュ"]
default: "なし"

- id: camera
label: カメラ
description: カメラワークを指定
commandListLabelFormat: "CAMERA: {action}, target={target}"
properties:
action:
type: enum
options: ["ズームイン", "ズームアウト", "パン左", "パン右", "シェイク", "フォーカス", "リセット"]
required: true
target:
type: enum
options: ["全体", "キャラA", "キャラB", "キャラC", "キャラD", "先生", "店員", "背景"]
default: "全体"

- id: choice
label: 選択肢
description: 選択肢を表示
commandListLabelFormat: "CHOICE: {options}"
properties:
options:
type: string
multiline: true
description: "選択肢を1行に1つずつ記述"
required: true
timeout:
type: number
default: 0
description: "自動選択までの秒数(0で無制限)"

- id: action
label: アクション
description: キャラクターのアクションを実行
commandListLabelFormat: "ACTION: {character}, {action}"
properties:
character:
type: enum
options: ["キャラA", "キャラB", "キャラC", "キャラD", "先生", "店員"]
required: true
action:
type: enum
options: ["歩く", "走る", "座る", "立つ", "ジャンプ", "踊る", "倒れる", "手を振る", "指さす", "抱きしめる"]
required: true
direction:
type: enum
options: ["左", "右", "上", "下", "中央"]
default: "中央"

- id: narration
label: ナレーション
description: ナレーションテキストを表示
commandListLabelFormat: "NARRATION: {text}"
properties:
text:
type: string
multiline: true
required: true
style:
type: enum
options: ["通常", "強調", "小さく", "斜体", "点滅"]
default: "通常"

- id: branch
label: 分岐
description: 他のコマンドを参照する分岐
commandListLabelFormat: "BRANCH: Target {targetCommand}"
defaultBackgroundColor: "#f9f0ff"
properties:
targetCommand:
type: command
required: true
commandTypes: ["text", "narration"] # Only allow text and narration commands
condition:
type: string
required: true
multiline: true
""";
}

#endregion

Assert.Equal(17, codeFiles.Count);
Assert.Equal(GenerateTestCode.TextCommandStr, codeFiles.FirstOrDefault(c => c.FileName == "TextCommand.g.cs").Code);
}
}
Loading
Loading