public partial class StoryMgr : SingletonBase<StoryMgr>, I_StoryMgr
{

    private Dictionary<int, StoryController> _storyDic;


    private HashSet<StoryController> _storyList;


    private Dictionary<int, Dictionary<int, Action>> _nodeEntryCallback;


    private Dictionary<int, Dictionary<int, Action>> _nodeExitCallback;


    public List<int> ReadyMoveNextList;



    I_TableMgr _tableMgr => TableMgr.Instance;
    I_MonoMgr _monoMgr => MonoMgr.Instance;





    protected override void Init()
    {
        _storyDic = new();
        _storyList = new();
        _nodeEntryCallback = new();
        _nodeExitCallback = new();
        LoadStoryCatalog<StoryCatalogTable>();


    }



    void LoadStoryCatalog<T>() where T : class, I_Table
    {
        StoryCatalogTable allStory = _tableMgr.GetTable<T>() as StoryCatalogTable;
#if UNITY_EDITOR
        if (allStory == null)
        {
            Debug.LogError("故事總目錄加載失敗");
            return;
        }
#endif
        var nodesLogicDic = LoadAllNodeLogic();
        foreach (var story in allStory)
        {
            if (_storyDic.ContainsKey(story.ID) == true)
            {
                continue;
            }
            I_Table nodeTable = _tableMgr.GetTable(story.NodeTableName);
            I_Table lineTable = _tableMgr.GetTable(story.LineTableName);
            if (nodeTable == null || lineTable == null)
            {
                Logger.LogError($"故事 {story.ID} 加載失敗");
                return;
            }
            if (nodesLogicDic.TryGetValue(story.ID, out var smallNodeDic) == false)
            {
                throw new Exception($"沒有找到故事 {story.ID} 的節點邏輯");
            }
            var controller = new StoryController(story.ID, story.StartNodeID, nodeTable, lineTable, smallNodeDic);
            _storyDic[story.ID] = controller;
            _storyList.Add(controller);
        }
    }


    private Dictionary<int, Dictionary<int, Type>> LoadAllNodeLogic()
    {


        Assembly assembly = typeof(StoryMgr).Assembly;
        List<Type> nodeLogicList;
        try
        {
            nodeLogicList = assembly.GetTypes()
                      .Where(t => t.IsDefined(typeof(StoryNodeAttribute), false))
                      .Where(t => typeof(I_StoryLogic).IsAssignableFrom(t) && t.IsClass == true) 
                      .ToList();
        }
        catch (System.Exception ex)
        {
            throw new ApplicationException("故事邏輯類型無法載入（可檢查依賴缺失）", ex);
        }

        ushort? storyID;
        ushort? nodeID;
        Dictionary<int, Dictionary<int, Type>> bigNodeDic = new();
        Dictionary<int, Type> smallNodeDic;

        foreach (var nodeType in nodeLogicList)
        {
            storyID = nodeType.GetCustomAttribute<StoryNodeAttribute>().StoryID;
            nodeID = nodeType.GetCustomAttribute<StoryNodeAttribute>().NodeID;
            if (storyID.HasValue == false || nodeID.HasValue == false)
            {
                continue;
            }
            if (bigNodeDic.TryGetValue((int)storyID, out smallNodeDic) == false)
            {
                smallNodeDic = new();
                bigNodeDic[(int)storyID] = smallNodeDic;
            }
            if (smallNodeDic.ContainsKey((int)nodeID) == true)
            {
                throw new ApplicationException("故事節點邏輯有重覆");
            }
            else
            {
                smallNodeDic[(int)nodeID] = nodeType;
            }

        }
        return bigNodeDic;
    }



    public void LogicPass(int storyID, int passID)
    {
        if (_storyDic.TryGetValue(storyID, out var story) == false)
        {
            throw new Exception($"找不到故事 {storyID}");
        }
        story.LogicPass(passID);
    }



    void Update()
    {
        foreach (var story in _storyList)
        {
            story.NodeUpdate();
        }
    }





    public void Add_NodeEntry_Listener(int storyID, int nodeID, Action callback)
    {
        if (_nodeEntryCallback.TryGetValue(storyID, out var smallDic) == false || smallDic == null)
        {
            smallDic = new();
            _nodeEntryCallback[storyID] = smallDic;
        }
        smallDic[nodeID] += callback;
    }


    public void Remove_NodeEntry_Listener(int storyID, int nodeID, Action callback)
    {
        if (_nodeEntryCallback.TryGetValue(storyID, out var smallDic) == false || smallDic == null)
        {
            return;
        }
        if (smallDic.TryGetValue(nodeID, out var action) == false || action == null)
        {
            return;
        }
        action -= callback;
    }



    public void Trigger_NodeEntry(int storyID, int nodeID)
    {
        if (_nodeEntryCallback.TryGetValue(storyID, out var smallDic) == false || smallDic == null)
        {
            return;
        }
        if (smallDic.TryGetValue(nodeID, out var action) == false || action == null)
        {
            return;
        }
        action.Invoke();
    }
    #endregion



    public void Add_NodeExit_Listener(int storyID, int nodeID, Action callback)
    {
        if (_nodeExitCallback.TryGetValue(storyID, out var smallDic) == false || smallDic == null)
        {
            smallDic = new();
            _nodeExitCallback[storyID] = smallDic;
        }
        smallDic[nodeID] += callback;
    }



    public void Remove_NodeExit_Listener(int storyID, int nodeID, Action callback)
    {
        if (_nodeExitCallback.TryGetValue(storyID, out var smallDic) == false || smallDic == null)
        {
            return;
        }
        if (smallDic.TryGetValue(nodeID, out var action) == false || action == null)
        {
            return;
        }
        action -= callback;
    }

    public void Trigger_NodeExit(int storyID, int nodeID)
    {
        if (_nodeExitCallback.TryGetValue(storyID, out var smallDic) == false || smallDic == null)
        {
            return;
        }
        if (smallDic.TryGetValue(nodeID, out var action) == false || action == null)
        {
            return;
        }
        action.Invoke();
    }

}


public interface I_StoryMgr
{
    void Add_NodeEntry_Listener(int storyID, int nodeID, Action callback);
    void Add_NodeExit_Listener(int storyID, int nodeID, Action callback);
    void LogicPass(int storyID, int passID);

    void Trigger_NodeEntry(int storyID, int nodeID);
    void Trigger_NodeExit(int storyID, int nodeID);
    void Remove_NodeEntry_Listener(int storyID, int nodeID, Action callback);
    void Remove_NodeExit_Listener(int storyID, int nodeID, Action callback);
}

public interface I_StoryLogic
{

    public void EntryNode();


    public void FixedUpdateNode();


    public void UpdateNode();


    public void LaterUpdateNode();


    public void ExitNode();
}

public interface I_StoryLine : I_TableRow
{

    public int ID { get; set; }


    public int MyNodeID { get; set; }


    public int NextNodeID { get; set; }


    public bool IsPass { get; set; }
}

public interface I_StoryNode : I_TableRow
{

    public int ID { get; set; }


    public string Logic { get; set; }


    public bool IsHide { get; set; }


    public List<int> LineList { get; set; }
}

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Tunity.Table;
using UnityEngine;

public partial class StoryMgr : SingletonBase<StoryMgr>, I_StoryMgr
{

    public class StoryController
    {

        public int ID;



        public int CurrentStoryNodeID;


        public I_StoryLogic CurrentStoryNodeLogic;


        public Dictionary<ushort, I_StoryLogic> _storyLogicDic;




        List<int> _historyList;

        private I_Table _nodeTable;
        private I_Table _lineTable;

        private I_StoryMgr _storyMgr => StoryMgr.Instance;



        public StoryController(int storyID,
                               int startNodeID,
                               I_Table nodeTable,
                               I_Table lineTable,
                               Dictionary<int, Type> logicTypeDic)
        {
            this.ID = storyID;
            _nodeTable = nodeTable ?? throw new ArgumentNullException($"是空的");
            _lineTable = lineTable ?? throw new ArgumentNullException($"是空的");
            _storyLogicDic = new();
            _historyList = new();
            InitNodeLogic(logicTypeDic);
            CurrentStoryNodeID = startNodeID; 
            _historyList.Add(startNodeID); 

            if (_storyLogicDic.TryGetValue((ushort)CurrentStoryNodeID, out var i_StoryLogic) == false)
            {
                return;
            }
            CurrentStoryNodeLogic = i_StoryLogic;
        }




        private void InitNodeLogic(Dictionary<int, Type> logicTypeDic)
        {

            I_StoryLogic logicInstance;
            foreach (var iRow in _nodeTable)
            {
                if (iRow is I_StoryNode node == false)
                {
                    throw new Exception($"故事 {ID} 的節點表格式錯誤");
                }
                if (node.IsHide == true)
                {
                    continue;
                }
                if (logicTypeDic.TryGetValue(node.ID, out var type) == false)
                {
                    throw new Exception($"故事 {ID} 的節點 {node.ID} 加載不完整");
                }
                logicInstance = (I_StoryLogic)Activator.CreateInstance(type);
                if (logicInstance == null)
                {
                    throw new Exception($"創建 {type.Name} 對象失敗");
                }
                _storyLogicDic[(ushort)node.ID] = logicInstance;

            }

        }




        public void NodeUpdate()
        {
            CurrentStoryNodeLogic?.UpdateNode();
        }




        public void CheckNowPass()
        {
            var nowNode = _nodeTable.GetRow<int, I_StoryNode>(CurrentStoryNodeID);
            if (nowNode == null)
            {
                throw new Exception($"加載故事 {ID} 節點 {CurrentStoryNodeID} 失敗");
            }
            var lineList = nowNode.LineList;
            foreach (var lineID in lineList)
            {
                var line = _lineTable.GetRow<int, I_StoryLine>(lineID);
                if (line == null)
                {
                    throw new Exception($"加載故事 {ID} 連接線 {lineID} 失敗");
                }
                if (line.IsPass == false)
                {
                    continue;
                }

                MoveNextNode(line.NextNodeID);
                return;
            }
        }



        public void LogicPass(int id)
        {
            var line = _lineTable.GetRow<int, I_StoryLine>(id);
            if (line == null)
            {
                throw new Exception($"加載故事 {ID} 連接線 {id} 失敗");
            }
            line.IsPass = true;
            if (line.MyNodeID != CurrentStoryNodeID)
            {
                return;
            }
            MoveNextNode(line.NextNodeID);

        }







        public void MoveNextNode(int nextNodeID)
        {

            CurrentStoryNodeLogic?.ExitNode();
            _storyMgr.Trigger_NodeExit(ID, CurrentStoryNodeID);
            CurrentStoryNodeID = nextNodeID;
            _historyList.Add(nextNodeID);
            if (_storyLogicDic.TryGetValue((ushort)nextNodeID, out var nextNodeLogic) == false || nextNodeLogic == null)
            {
                CurrentStoryNodeLogic = null;
                return;
            }
            CurrentStoryNodeLogic = nextNodeLogic;
            CurrentStoryNodeLogic?.EntryNode();
            _storyMgr.Trigger_NodeEntry(ID, nextNodeID);
            CheckNowPass();
            CurrentStoryNodeLogic?.UpdateNode();
        }
    }

}


[StoryNode(1, 1)]
public class Node0001001 : I_StoryLogic
{
    public Node0001001()
    {

    }

    public void EntryNode()
    {

    }

    public void ExitNode()
    {

    }

    public void FixedUpdateNode()
    {

    }

    public void LaterUpdateNode()
    {

    }

    public void UpdateNode()
    {

    }
}

[System.AttributeUsage(System.AttributeTargets.Class)]
public class StoryNodeAttribute : System.Attribute
{

    public ushort StoryID;


    public ushort NodeID;


    public StoryNodeAttribute(ushort StoryID, ushort NodeID)
    {
        this.StoryID = StoryID;
        this.NodeID = NodeID;
    }
}

這是一個基於節點的故事流程管理器，數據會從 TableMgr 加載而來，
我們關注的重點是這個管理器的使用流程，使用方式，方法，用途，請替我寫一份使用守則/手冊，
讓其他使用者看這個守則/手冊就知道它的詳情，請以.md 的格式去畫寫，用直白且簡單的句字即可