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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e4959a26874fd1a4081de17f7a1ada62
m_Address: ScriptableDatabase
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: be8c7bfaed8f7eb428b95bd26a52c525, type: 2}
m_SchemaSet:
Expand Down
8 changes: 8 additions & 0 deletions src/Game.Client/Assets/Programs/Editor/Database.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Game.Shared.Scriptable.Database;
using UnityEditor;
using UnityEngine;

namespace Game.Shared.Scriptable.Database.EditorTools
{
/// <summary>
/// [ScriptableTable] 型を集約するコンテナ ScriptableDatabase を扱うエディタコマンド。
/// テーブルクラス生成(ScriptableTableGenerator)とは責務が別のため独立コマンドにしている。
/// SO は「型」と「実体(.asset)」の2側面を持つため、両者を個別に実行できる2コマンドに分ける:
/// - Build : コンテナ「クラス」ScriptableDatabase.g.cs を生成(テーブル型の増減時・再コンパイル発生)。
/// - Register : ScriptableDatabase.asset を作成/更新し各 {Type}Table 資産を自動結線(資産の増減時・コード変更なし)。
/// Register が Build 済みの型を CreateInstance するため同一実行にはできず、分離が必然。
/// 本ビルダー自身が ScriptableDatabase を未生成段階でも壊れないよう、Register はコンテナ型を
/// リフレクションで参照する(コンパイル時依存を持たない)。
/// </summary>
public static class ScriptableDatabaseBuilder
{
private const string OutDir = "Assets/Programs/Runtime/Shared/Scriptable/Database/Generated";
private const string DatabaseAssetPath = "Assets/ProjectAssets/Database/ScriptableDatabase.asset";
private const string DatabaseClassName = "ScriptableDatabase";
private const string DatabaseNamespace = "Game.Shared.Scriptable.Database";

// ---- コマンド①: コンテナクラス生成 ----

[MenuItem("Tools/Scriptable Database/Build")]
public static void Build()
{
Directory.CreateDirectory(OutDir);
var recordTypes = TableTypes().OrderBy(t => t.Name).ToList();
File.WriteAllText($"{OutDir}/{DatabaseClassName}.g.cs", Emit(recordTypes));
AssetDatabase.Refresh();
Debug.Log($"[ScriptableDatabaseBuilder] {DatabaseClassName}.g.cs generated with {recordTypes.Count} table(s).");
}

private static string Emit(IReadOnlyList<Type> recordTypes)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using UnityEngine;");
sb.AppendLine();
sb.AppendLine($"namespace {DatabaseNamespace}");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>全 ScriptableTable を集約するコンテナ(Build コマンドで生成)。</summary>");
sb.AppendLine(" [CreateAssetMenu(menuName = \"Scriptable Database/Database\")]");
sb.AppendLine($" public sealed partial class {DatabaseClassName} : ScriptableObject");
sb.AppendLine(" {");
for (int i = 0; i < recordTypes.Count; i++)
{
var rec = recordTypes[i];
string tableType = TableTypeName(rec); // global::{ns}.{Name}Table
string prop = rec.Name + "Table"; // WeaponLevelMasterTable
string field = char.ToLowerInvariant(prop[0]) + prop.Substring(1); // weaponLevelMasterTable

if (i > 0) sb.AppendLine();
sb.AppendLine($" [SerializeField] private {tableType} {field};");
sb.AppendLine($" public {tableType} {prop} => {field};");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}

private static string TableTypeName(Type recordType)
{
string name = recordType.Name + "Table";
return recordType.Namespace != null ? $"global::{recordType.Namespace}.{name}" : $"global::{name}";
}

// ---- コマンド②: テーブル資産の自動登録 ----

[MenuItem("Tools/Scriptable Database/Register")]
public static void Register()
{
var dbType = FindDatabaseType();
if (dbType == null)
{
Debug.LogError("[ScriptableDatabaseBuilder] ScriptableDatabase 型が見つかりません。先に 'Tools/Scriptable Database/Build' を実行してください。");
return;
}

var database = AssetDatabase.LoadAssetAtPath(DatabaseAssetPath, dbType) as ScriptableObject;
if (database == null)
{
database = ScriptableObject.CreateInstance(dbType);
AssetDatabase.CreateAsset(database, DatabaseAssetPath);
Debug.Log($"[ScriptableDatabaseBuilder] Created {DatabaseAssetPath}.");
}

var so = new SerializedObject(database);
int wired = 0, missing = 0;
foreach (var field in TableFields(dbType))
{
var prop = so.FindProperty(field.Name);
if (prop == null) continue;

var guids = AssetDatabase.FindAssets($"t:{field.FieldType.Name}");
if (guids.Length == 0)
{
missing++;
Debug.LogWarning($"[ScriptableDatabaseBuilder] {field.FieldType.Name} の資産が見つかりません(フィールド '{field.Name}' は未結線)。");
continue;
}
if (guids.Length > 1)
{
Debug.LogWarning($"[ScriptableDatabaseBuilder] {field.FieldType.Name} の資産が複数あります。先頭を採用します。");
}

var path = AssetDatabase.GUIDToAssetPath(guids[0]);
prop.objectReferenceValue = AssetDatabase.LoadAssetAtPath(path, field.FieldType);
wired++;
}

so.ApplyModifiedProperties();
EditorUtility.SetDirty(database);
AssetDatabase.SaveAssets();
Debug.Log($"[ScriptableDatabaseBuilder] Register 完了: 結線 {wired} 件 / 欠落 {missing} 件。");
}

private static Type FindDatabaseType() =>
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(SafeTypes)
.FirstOrDefault(t => t.FullName == $"{DatabaseNamespace}.{DatabaseClassName}");

private static IEnumerable<FieldInfo> TableFields(Type dbType) =>
dbType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
.Where(f => IsScriptableTable(f.FieldType));

private static bool IsScriptableTable(Type t)
{
for (var b = t; b != null; b = b.BaseType)
{
if (b.IsGenericType && b.GetGenericTypeDefinition() == typeof(ScriptableTable<>))
return true;
}
return false;
}

// ---- 走査ヘルパ(ScriptableTableGenerator と同等) ----

private static IEnumerable<Type> TableTypes() =>
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(SafeTypes)
.Where(t => t.IsClass && t.GetCustomAttribute<ScriptableTableAttribute>() != null);

private static IEnumerable<Type> SafeTypes(Assembly a)
{
try { return a.GetTypes(); }
catch (ReflectionTypeLoadException e) { return e.Types.Where(x => x != null); }
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Game.Shared.Scriptable.Database;
using UnityEditor;
using UnityEngine;

namespace Game.Shared.Scriptable.Database.EditorTools
{
/// <summary>
/// ScriptableTable 派生アセット共通の Inspector。
/// 既定の描画に加え、未整列時の警告と「Sort & Validate」ボタンを表示する
/// (整列は OnValidate 自動実行をやめ、ここから手動実行する)。
/// </summary>
[CustomEditor(typeof(ScriptableTableBase), editorForChildClasses: true)]
public class ScriptableTableEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();

var table = (ScriptableTableBase)target;
if (!table.EditorIsSorted())
{
EditorGUILayout.HelpBox(
"records が主キー昇順に整列されていません(空要素を含む場合あり)。実行時の検索が誤動作します。『Sort & Validate』を実行してください。",
MessageType.Warning);
}

EditorGUILayout.Space();
if (GUILayout.Button("Sort & Validate"))
{
foreach (var o in targets)
{
var tb = (ScriptableTableBase)o;
Undo.RecordObject(tb, "Sort & Validate");
tb.EditorSortAndValidate();
EditorUtility.SetDirty(tb);
}
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading