修改提交
This commit is contained in:
9
.com-unity-codely.json
Normal file
9
.com-unity-codely.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"unity_port": 25916,
|
||||
"created_date": "2026-02-10T01:48:10.3375388Z",
|
||||
"project_path": "D:/myself/Fishing2/Assets",
|
||||
"reloading": false,
|
||||
"reason": "ready",
|
||||
"seq": 1,
|
||||
"last_heartbeat": "2026-03-09T09:50:14.4988583Z"
|
||||
}
|
||||
@@ -18658,6 +18658,21 @@ MonoBehaviour:
|
||||
- {fileID: 102900000, guid: aa3f5467c0c153642ac320466aee0ec1, type: 3}
|
||||
FilterEnum: 0
|
||||
Filter: '*'
|
||||
- Path: Assets/ResRaw/Prefabs/Line/LineHand1.prefab
|
||||
Address: Plyaer/LineHand1
|
||||
Type: GameObject
|
||||
Bundle: main/plyaer.bundle
|
||||
Tags:
|
||||
Group:
|
||||
Name: Plyaer
|
||||
Enable: 1
|
||||
BundleMode: 0
|
||||
AddressMode: 2
|
||||
Tags:
|
||||
Collectors:
|
||||
- {fileID: 102900000, guid: aa3f5467c0c153642ac320466aee0ec1, type: 3}
|
||||
FilterEnum: 0
|
||||
Filter: '*'
|
||||
- Path: Assets/ResRaw/Prefabs/Line/LineSolver.prefab
|
||||
Address: Plyaer/LineSolver
|
||||
Type: GameObject
|
||||
|
||||
@@ -171,7 +171,7 @@ GameObject:
|
||||
- component: {fileID: 4477616030203838514}
|
||||
- component: {fileID: 8101446342893690422}
|
||||
- component: {fileID: 2923025939212586282}
|
||||
- component: {fileID: 9164732011369635724}
|
||||
- component: {fileID: 2431960384537678220}
|
||||
m_Layer: 14
|
||||
m_Name: Player
|
||||
m_TagString: Untagged
|
||||
@@ -342,7 +342,7 @@ MonoBehaviour:
|
||||
_standingDownwardForceScale: 1
|
||||
_camera: {fileID: 0}
|
||||
cameraParent: {fileID: 6835675132305341997}
|
||||
--- !u!114 &9164732011369635724
|
||||
--- !u!114 &2431960384537678220
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -351,16 +351,16 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 8172838236951268422}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 625346c970c542bab9f3a36f78720a77, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: a25908f34e4e4464a922b88337a5b733, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FPlayer
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.PlayerUnityComponent
|
||||
Root: {fileID: 8695154010886802211}
|
||||
Eye: {fileID: 5745877877928638952}
|
||||
FppLook: {fileID: 2969532427624891124}
|
||||
IK: {fileID: 1593568502960682634}
|
||||
ModelAsset: {fileID: 0}
|
||||
Character: {fileID: 0}
|
||||
FirstPerson: {fileID: 0}
|
||||
Character: {fileID: 8101446342893690422}
|
||||
FirstPerson: {fileID: 2923025939212586282}
|
||||
MouseSensitivity: 0.1
|
||||
invertLook: 1
|
||||
minPitch: -75
|
||||
|
||||
@@ -388,7 +388,7 @@ Transform:
|
||||
m_GameObject: {fileID: 174907465}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 8, y: -5, z: 0}
|
||||
m_LocalPosition: {x: 8.888889, y: -5, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
@@ -1629,8 +1629,8 @@ Camera:
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
near clip plane: 0.01
|
||||
far clip plane: 5000
|
||||
near clip plane: 0.1
|
||||
far clip plane: 3000
|
||||
field of view: 60.000004
|
||||
orthographic: 0
|
||||
orthographic size: 5
|
||||
@@ -1784,7 +1784,6 @@ GameObject:
|
||||
- component: {fileID: 1341717235351337375}
|
||||
- component: {fileID: 7388915548948935574}
|
||||
- component: {fileID: 7388915548948935578}
|
||||
- component: {fileID: 7388915548948935575}
|
||||
- component: {fileID: 7388915548948935576}
|
||||
- component: {fileID: 7388915548948935577}
|
||||
m_Layer: 0
|
||||
@@ -1806,29 +1805,6 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 2101c084ab66498bb634f122db410852, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.InputManager
|
||||
--- !u!114 &7388915548948935575
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7388915548948935573}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 53629a9cec2b4caf9a739c21f7abdf3c, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::NBF.FPlayerData
|
||||
ChangeItem: 0
|
||||
Run: 0
|
||||
IsGrounded: 0
|
||||
Speed: 0
|
||||
RotationSpeed: 0
|
||||
ReelSpeed: 0
|
||||
LineTension: 0
|
||||
IsLureRod: 0
|
||||
MoveInput: {x: 0, y: 0}
|
||||
EyeAngle: 0
|
||||
NextState: 0
|
||||
--- !u!114 &7388915548948935576
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace NBF
|
||||
PlayerAnimator = GetComponent<PlayerAnimator>();
|
||||
}
|
||||
|
||||
public void SetPlayer(FPlayer player)
|
||||
public void SetPlayer(Transform FppLook)
|
||||
{
|
||||
LookIk.solver.target = player.FppLook;
|
||||
LookIk.solver.target = FppLook;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ namespace NBF
|
||||
[SerializeField] private CameraAsset _cameraAsset;
|
||||
private CameraShowMode _lastMode = CameraShowMode.None;
|
||||
|
||||
private PlayerUnityComponent FollowPlayer;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_lastMode == Mode) return;
|
||||
@@ -43,27 +45,26 @@ namespace NBF
|
||||
|
||||
private void SetFPPCam()
|
||||
{
|
||||
var player = FPlayer.Instance;
|
||||
if (player != null)
|
||||
if (FollowPlayer != null)
|
||||
{
|
||||
_cameraAsset.fppVCam.LookAt = player.FppLook;
|
||||
_cameraAsset.fppVCam.Follow = player.ModelAsset.NeckTransform;
|
||||
_cameraAsset.fppVCam.LookAt = FollowPlayer.FppLook;
|
||||
_cameraAsset.fppVCam.Follow = FollowPlayer.ModelAsset.NeckTransform;
|
||||
}
|
||||
|
||||
_cameraAsset.fppVCam.Priority = 10;
|
||||
_cameraAsset.tppVCam.Priority = 0;
|
||||
// StartCoroutine(SnapToTarget());
|
||||
}
|
||||
|
||||
public void SetFppLook(Transform fppCamLook)
|
||||
public void SetFppLook(PlayerUnityComponent playerUnityComponent)
|
||||
{
|
||||
_cameraAsset.fppVCam.LookAt = fppCamLook;
|
||||
FollowPlayer = playerUnityComponent;
|
||||
_cameraAsset.fppVCam.LookAt = FollowPlayer.FppLook;
|
||||
Mode = CameraShowMode.FPP;
|
||||
}
|
||||
|
||||
public void SetFppFollow(Transform fppCamFollow)
|
||||
public void SetFppFollow(PlayerUnityComponent playerUnityComponent)
|
||||
{
|
||||
_cameraAsset.fppVCam.Follow = fppCamFollow;
|
||||
_cameraAsset.fppVCam.Follow = FollowPlayer.ModelAsset.NeckTransform;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace NBF
|
||||
{
|
||||
public enum SelectorRodSetting
|
||||
{
|
||||
Speed = 0,
|
||||
Drag = 1,
|
||||
Leeder = 2
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca512e729d2e4ef290f30f075bcdd698
|
||||
timeCreated: 1744039906
|
||||
File diff suppressed because it is too large
Load Diff
94
Assets/Scripts/Fishing/Data/LocalDataManager.cs
Normal file
94
Assets/Scripts/Fishing/Data/LocalDataManager.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
// // 新文件:D:\myself\Fishing2\Assets\Scripts\Fishing\Data\LocalDataManager.cs
|
||||
//
|
||||
// using System.Collections.Generic;
|
||||
// using UnityEngine;
|
||||
//
|
||||
// namespace NBF
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// 本地单机模式的数据管理器(模拟服务器转发)
|
||||
// /// </summary>
|
||||
// public class LocalDataManager : PlayerDataManager
|
||||
// {
|
||||
// public override bool IsLocalMode => true;
|
||||
//
|
||||
// private Dictionary<int, FPlayerData> _localPlayers = new();
|
||||
// private uint _sequenceCounter;
|
||||
//
|
||||
// protected void Awake()
|
||||
// {
|
||||
// Instance = this;
|
||||
// }
|
||||
//
|
||||
// public void RegisterPlayer(FPlayerData player)
|
||||
// {
|
||||
// if (!_localPlayers.ContainsKey(player.PlayerID))
|
||||
// {
|
||||
// _localPlayers.Add(player.PlayerID, player);
|
||||
// player.IsLocalPlayer = true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override void OnPlayerStateChanged(FPlayerData player, PlayerState newState)
|
||||
// {
|
||||
// // 本地模式下,广播给其他本地玩家(分屏)
|
||||
// foreach (var kvp in _localPlayers)
|
||||
// {
|
||||
// if (kvp.Value != player)
|
||||
// {
|
||||
// // 直接应用状态(或者加入简单的延迟模拟)
|
||||
// kvp.Value.State = newState;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override void OnHeldItemChanged(FPlayerData player, HeldItemInfo newItem)
|
||||
// {
|
||||
// foreach (var kvp in _localPlayers)
|
||||
// {
|
||||
// if (kvp.Value != player)
|
||||
// {
|
||||
// kvp.Value.CurrentHeldItem = newItem;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override void SendStateSnapshot(FPlayerData player)
|
||||
// {
|
||||
// _sequenceCounter++;
|
||||
// var snapshot = player.ToNetworkSnapshot(_sequenceCounter);
|
||||
//
|
||||
// // 本地广播
|
||||
// foreach (var kvp in _localPlayers)
|
||||
// {
|
||||
// if (kvp.Value != player)
|
||||
// {
|
||||
// ReceiveStateSnapshot(kvp.Key, snapshot);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override void ReceiveStateSnapshot(int playerID, PlayerStateSnapshot snapshot)
|
||||
// {
|
||||
// if (_localPlayers.TryGetValue(playerID, out var player))
|
||||
// {
|
||||
// player.ApplyFromNetworkSnapshot(snapshot);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 定时同步(例如每秒 10 次)
|
||||
// private float _syncTimer;
|
||||
// private void Update()
|
||||
// {
|
||||
// _syncTimer += Time.deltaTime;
|
||||
// if (_syncTimer >= 0.1f) // 10Hz
|
||||
// {
|
||||
// _syncTimer = 0;
|
||||
// foreach (var player in _localPlayers.Values)
|
||||
// {
|
||||
// SendStateSnapshot(player);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
3
Assets/Scripts/Fishing/Data/LocalDataManager.cs.meta
Normal file
3
Assets/Scripts/Fishing/Data/LocalDataManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec2bd63eb6c143fdb528da693a8c6969
|
||||
timeCreated: 1773028161
|
||||
60
Assets/Scripts/Fishing/Data/NetworkDataManager.cs
Normal file
60
Assets/Scripts/Fishing/Data/NetworkDataManager.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
// using UnityEngine;
|
||||
//
|
||||
// namespace NBF
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// 网络模式的数据管理器
|
||||
// /// </summary>
|
||||
// public class NetworkDataManager : PlayerDataManager
|
||||
// {
|
||||
// public override bool IsLocalMode => false;
|
||||
//
|
||||
// // TODO: 这里集成你的网络库(Steamworks、Photon、Mirror 等)
|
||||
// // public SteamNetworkClient NetworkClient;
|
||||
//
|
||||
// protected void Awake()
|
||||
// {
|
||||
// Instance = this;
|
||||
// }
|
||||
//
|
||||
// public override void OnPlayerStateChanged(FPlayerData player, PlayerState newState)
|
||||
// {
|
||||
// // 如果是本地玩家,发送到服务器
|
||||
// if (player.IsLocalPlayer)
|
||||
// {
|
||||
// SendStateSnapshot(player);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override void OnHeldItemChanged(FPlayerData player, HeldItemInfo newItem)
|
||||
// {
|
||||
// if (player.IsLocalPlayer)
|
||||
// {
|
||||
// // TODO: 发送物品切换消息到服务器
|
||||
// Debug.Log($"发送物品切换:{newItem.ItemType}, ConfigID={newItem.ConfigID}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override void SendStateSnapshot(FPlayerData player)
|
||||
// {
|
||||
// if (!player.IsLocalPlayer) return;
|
||||
//
|
||||
// // TODO: 通过 Steam 或其他网络库发送
|
||||
// // NetworkClient.SendStateSnapshot(player.ToNetworkSnapshot());
|
||||
// }
|
||||
//
|
||||
// public override void ReceiveStateSnapshot(int playerID, PlayerStateSnapshot snapshot)
|
||||
// {
|
||||
// // TODO: 从网络接收其他玩家的状态
|
||||
// // 找到或创建对应的玩家对象
|
||||
// var player = FindOrCreatePlayer(playerID);
|
||||
// player.ApplyFromNetworkSnapshot(snapshot);
|
||||
// }
|
||||
//
|
||||
// private FPlayerData FindOrCreatePlayer(int playerID)
|
||||
// {
|
||||
// // TODO: 实现玩家对象池或动态生成
|
||||
// return FindObjectOfType<FPlayerData>();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
3
Assets/Scripts/Fishing/Data/NetworkDataManager.cs.meta
Normal file
3
Assets/Scripts/Fishing/Data/NetworkDataManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1a06ba516ea46f0920729b2af6484c1
|
||||
timeCreated: 1773028213
|
||||
56
Assets/Scripts/Fishing/Data/PlayerDataManager.cs
Normal file
56
Assets/Scripts/Fishing/Data/PlayerDataManager.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// using System;
|
||||
// using System.Collections.Generic;
|
||||
// using UnityEngine;
|
||||
//
|
||||
// namespace NBF
|
||||
// {
|
||||
// public interface IDataSource
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 数据管理器基类(本地和网络共享接口)
|
||||
// /// </summary>
|
||||
// public class PlayerDataManager : MonoBehaviour
|
||||
// {
|
||||
// public static PlayerDataManager Instance { get; private set; }
|
||||
//
|
||||
// public FPlayerData Self { get; set; }
|
||||
//
|
||||
// private Dictionary<int, FPlayerData> _players = new Dictionary<int, FPlayerData>();
|
||||
//
|
||||
// protected void Awake()
|
||||
// {
|
||||
// Instance = this;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 玩家状态变更时调用
|
||||
// /// </summary>
|
||||
// public void OnPlayerStateChanged(FPlayerData player, PlayerState newState)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 手持物品变更时调用
|
||||
// /// </summary>
|
||||
// public void OnHeldItemChanged(FPlayerData player, HeldItemInfo newItem)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 发送玩家状态快照
|
||||
// /// </summary>
|
||||
// public void SendStateSnapshot(FPlayerData player)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 接收并应用网络快照
|
||||
// /// </summary>
|
||||
// public void ReceiveStateSnapshot(int playerID, PlayerStateSnapshot snapshot)
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
3
Assets/Scripts/Fishing/Data/PlayerDataManager.cs.meta
Normal file
3
Assets/Scripts/Fishing/Data/PlayerDataManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0478bf9a256a46beb65fd0307c7b5b9b
|
||||
timeCreated: 1773028149
|
||||
234
Assets/Scripts/Fishing/Data/StateEnterParams.cs
Normal file
234
Assets/Scripts/Fishing/Data/StateEnterParams.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态进入参数(用于网络同步和动画/表现播放)
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StateEnterParams
|
||||
{
|
||||
// 序列化友好的数据存储
|
||||
[SerializeField] private List<string> _keys = new();
|
||||
[SerializeField] private List<int> _intValues = new();
|
||||
[SerializeField] private List<float> _floatValues = new();
|
||||
[SerializeField] private List<Vector3> _vector3Values = new();
|
||||
[SerializeField] private List<Quaternion> _quaternionValues = new();
|
||||
|
||||
// 快速访问缓存
|
||||
private Dictionary<string, int> _intCache;
|
||||
private Dictionary<string, int> _floatCache;
|
||||
private Dictionary<string, int> _vector3Cache;
|
||||
private Dictionary<string, int> _quaternionCache;
|
||||
|
||||
public StateEnterParams()
|
||||
{
|
||||
InitializeCaches();
|
||||
}
|
||||
|
||||
private void InitializeCaches()
|
||||
{
|
||||
_intCache = new Dictionary<string, int>();
|
||||
_floatCache = new Dictionary<string, int>();
|
||||
_vector3Cache = new Dictionary<string, int>();
|
||||
_quaternionCache = new Dictionary<string, int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有参数
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_keys.Clear();
|
||||
_intValues.Clear();
|
||||
_floatValues.Clear();
|
||||
_vector3Values.Clear();
|
||||
_quaternionValues.Clear();
|
||||
|
||||
_intCache.Clear();
|
||||
_floatCache.Clear();
|
||||
_vector3Cache.Clear();
|
||||
_quaternionCache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 int 参数
|
||||
/// </summary>
|
||||
public void SetInt(string key, int value)
|
||||
{
|
||||
if (_intCache.TryGetValue(key, out int index))
|
||||
{
|
||||
_intValues[index] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keys.Add(key);
|
||||
_intValues.Add(value);
|
||||
_intCache[key] = _intValues.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 float 参数
|
||||
/// </summary>
|
||||
public void SetFloat(string key, float value)
|
||||
{
|
||||
if (_floatCache.TryGetValue(key, out int index))
|
||||
{
|
||||
_floatValues[index] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keys.Add(key);
|
||||
_floatValues.Add(value);
|
||||
_floatCache[key] = _floatValues.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Vector3 参数
|
||||
/// </summary>
|
||||
public void SetVector3(string key, Vector3 value)
|
||||
{
|
||||
if (_vector3Cache.TryGetValue(key, out int index))
|
||||
{
|
||||
_vector3Values[index] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keys.Add(key);
|
||||
_vector3Values.Add(value);
|
||||
_vector3Cache[key] = _vector3Values.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Quaternion 参数
|
||||
/// </summary>
|
||||
public void SetQuaternion(string key, Quaternion value)
|
||||
{
|
||||
if (_quaternionCache.TryGetValue(key, out int index))
|
||||
{
|
||||
_quaternionValues[index] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keys.Add(key);
|
||||
_quaternionValues.Add(value);
|
||||
_quaternionCache[key] = _quaternionValues.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 bool 参数
|
||||
/// </summary>
|
||||
public void SetBool(string key, bool value)
|
||||
{
|
||||
if (_intCache.TryGetValue(key, out int index))
|
||||
{
|
||||
_intValues[index] = value ? 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keys.Add(key);
|
||||
_intValues.Add(value ? 1 : 0);
|
||||
_intCache[key] = _intValues.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 int 参数
|
||||
/// </summary>
|
||||
public int GetInt(string key, int defaultValue = 0)
|
||||
{
|
||||
if (_intCache.TryGetValue(key, out int index) && index < _intValues.Count)
|
||||
{
|
||||
return _intValues[index];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 float 参数
|
||||
/// </summary>
|
||||
public float GetFloat(string key, float defaultValue = 0f)
|
||||
{
|
||||
if (_floatCache.TryGetValue(key, out int index) && index < _floatValues.Count)
|
||||
{
|
||||
return _floatValues[index];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Vector3 参数
|
||||
/// </summary>
|
||||
public Vector3 GetVector3(string key, Vector3 defaultValue = default)
|
||||
{
|
||||
if (_vector3Cache.TryGetValue(key, out int index) && index < _vector3Values.Count)
|
||||
{
|
||||
return _vector3Values[index];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Quaternion 参数
|
||||
/// </summary>
|
||||
public Quaternion GetQuaternion(string key, Quaternion defaultValue = default)
|
||||
{
|
||||
if (_quaternionCache.TryGetValue(key, out int index) && index < _quaternionValues.Count)
|
||||
{
|
||||
return _quaternionValues[index];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 bool 参数
|
||||
/// </summary>
|
||||
public bool GetBool(string key, bool defaultValue = false)
|
||||
{
|
||||
if (_intCache.TryGetValue(key, out int index) && index < _intValues.Count)
|
||||
{
|
||||
return _intValues[index] == 1;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含某个参数
|
||||
/// </summary>
|
||||
public bool HasKey(string key)
|
||||
{
|
||||
return _intCache.ContainsKey(key) ||
|
||||
_floatCache.ContainsKey(key) ||
|
||||
_vector3Cache.ContainsKey(key) ||
|
||||
_quaternionCache.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复制当前参数
|
||||
/// </summary>
|
||||
public StateEnterParams Clone()
|
||||
{
|
||||
var copy = new StateEnterParams
|
||||
{
|
||||
_keys = new List<string>(_keys),
|
||||
_intValues = new List<int>(_intValues),
|
||||
_floatValues = new List<float>(_floatValues),
|
||||
_vector3Values = new List<Vector3>(_vector3Values),
|
||||
_quaternionValues = new List<Quaternion>(_quaternionValues)
|
||||
};
|
||||
copy.InitializeCaches();
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Fishing/Data/StateEnterParams.cs.meta
Normal file
3
Assets/Scripts/Fishing/Data/StateEnterParams.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0ff43bbc7dd46b387e62f574ceb2523
|
||||
timeCreated: 1773029210
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Fantasy;
|
||||
using Fantasy.Async;
|
||||
using Fantasy.Entitas;
|
||||
using NBF.Fishing2;
|
||||
using RootMotion.FinalIK;
|
||||
using Log = NBC.Log;
|
||||
@@ -9,7 +10,6 @@ namespace NBF
|
||||
{
|
||||
public class Fishing
|
||||
{
|
||||
public FPlayer Player { get; private set; }
|
||||
private static Fishing _instance;
|
||||
|
||||
public static Fishing Instance
|
||||
@@ -22,6 +22,10 @@ namespace NBF
|
||||
}
|
||||
}
|
||||
|
||||
public MapRoom OldMap { get; private set; }
|
||||
|
||||
public MapRoom Map { get; private set; }
|
||||
|
||||
|
||||
public async FTask<bool> Go(int mapId, string roomCode = "")
|
||||
{
|
||||
@@ -37,6 +41,11 @@ namespace NBF
|
||||
RoomCode = roomCode
|
||||
});
|
||||
Log.Info($"进入地图请求返回={response.ErrorCode}");
|
||||
if (response.ErrorCode != 0)
|
||||
{
|
||||
Notices.Error("enter room error");
|
||||
return false;
|
||||
}
|
||||
LoadingPanel.Show();
|
||||
await ChangeMap(response.MapId, response.RoomCode, response.Units);
|
||||
LoadingPanel.Hide();
|
||||
@@ -46,18 +55,17 @@ namespace NBF
|
||||
|
||||
public async FTask ChangeMap(int mapId, string roomCode, List<MapUnitInfo> units)
|
||||
{
|
||||
OldMap = Map;
|
||||
Map = Entity.Create<MapRoom>(Game.Main,true, true);
|
||||
Map.Code = roomCode;
|
||||
Map.Map = mapId;
|
||||
var sceneName = "Map1";
|
||||
//加载场景==
|
||||
await SceneHelper.LoadScene(sceneName);
|
||||
CreateUnit();
|
||||
}
|
||||
|
||||
|
||||
private void CreateUnit()
|
||||
{
|
||||
var gameObject = PrefabsHelper.CreatePlayer(SceneSettings.Instance.Node);
|
||||
Player = gameObject.GetComponent<FPlayer>();
|
||||
CameraManager.Instance.Mode = CameraShowMode.FPP;
|
||||
foreach (var mapUnitInfo in units)
|
||||
{
|
||||
Map.AddUnit(mapUnitInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace NBF
|
||||
{
|
||||
public class FishingMap
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c361da1bcda647eb96fa27af894d2a7a
|
||||
timeCreated: 1766417823
|
||||
3
Assets/Scripts/Fishing/New.meta
Normal file
3
Assets/Scripts/Fishing/New.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b727e6041c1e459aabd0d5c41752dd8e
|
||||
timeCreated: 1773036926
|
||||
3
Assets/Scripts/Fishing/New/Data.meta
Normal file
3
Assets/Scripts/Fishing/New/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b61ff9bc02946a287bd0cca1aa2b6cb
|
||||
timeCreated: 1773037834
|
||||
63
Assets/Scripts/Fishing/New/Data/MapRoom.cs
Normal file
63
Assets/Scripts/Fishing/New/Data/MapRoom.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using Fantasy;
|
||||
using Fantasy.Entitas;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
/// <summary>
|
||||
/// 地图房间
|
||||
/// </summary>
|
||||
public class MapRoom : Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否本地房间
|
||||
/// </summary>
|
||||
public bool IsLocalRoom;
|
||||
|
||||
/// <summary>
|
||||
/// 房间序号id
|
||||
/// </summary>
|
||||
public int RoomId;
|
||||
|
||||
/// <summary>
|
||||
/// 房间代码
|
||||
/// </summary>
|
||||
public string Code = string.Empty;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 房间玩家
|
||||
/// </summary>
|
||||
public Dictionary<long, Player> Units = new Dictionary<long, Player>();
|
||||
|
||||
/// <summary>
|
||||
/// 房主
|
||||
/// </summary>
|
||||
public long Owner;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public long CreateTime;
|
||||
|
||||
/// <summary>
|
||||
/// 房间地图
|
||||
/// </summary>
|
||||
public int Map;
|
||||
|
||||
public void AddUnit(MapUnitInfo unit)
|
||||
{
|
||||
var player = Create<Player>(Game.Main, unit.Id, true, true);
|
||||
Units[unit.Id] = player;
|
||||
player.InitPlayer(unit);
|
||||
}
|
||||
|
||||
public void RemoveUnit(long id)
|
||||
{
|
||||
if (Units.Remove(id, out var player))
|
||||
{
|
||||
player.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Fishing/New/Data/MapRoom.cs.meta
Normal file
3
Assets/Scripts/Fishing/New/Data/MapRoom.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41009853ac444d87809e98fae2d1c597
|
||||
timeCreated: 1773036879
|
||||
97
Assets/Scripts/Fishing/New/Data/Player.cs
Normal file
97
Assets/Scripts/Fishing/New/Data/Player.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Collections.Generic;
|
||||
using Fantasy;
|
||||
using Fantasy.Entitas;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class Player : Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否本地玩家
|
||||
/// </summary>
|
||||
public bool IsLocalPlayer;
|
||||
|
||||
public bool IsLureRod => false;
|
||||
|
||||
public bool IsSelf => RoleModel.Instance.Id == Id;
|
||||
|
||||
// ========== 物理状态(高频同步) ==========
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector2 MoveInput;
|
||||
public float Speed;
|
||||
public float RotationSpeed;
|
||||
public bool IsGrounded;
|
||||
public bool Run;
|
||||
|
||||
// ========== 钓鱼相关状态(中频同步) ==========
|
||||
public float currentReelingSpeed;
|
||||
public float lineLength;
|
||||
public float reelSpeed;
|
||||
public float EyeAngle;
|
||||
|
||||
/// <summary>
|
||||
/// 标志量
|
||||
/// </summary>
|
||||
public long TagValue;
|
||||
|
||||
|
||||
// ========== 状态机 ==========
|
||||
private PlayerState _previousPlayerState = PlayerState.Idle;
|
||||
private PlayerState _playerState;
|
||||
public PlayerState PreviousState => _previousPlayerState;
|
||||
|
||||
/// <summary>
|
||||
/// 当前状态的进入参数(本地和远程都适用)
|
||||
/// </summary>
|
||||
public StateEnterParams CurrentStateParams { get; private set; } = new StateEnterParams();
|
||||
|
||||
public PlayerState State
|
||||
{
|
||||
get => _playerState;
|
||||
set
|
||||
{
|
||||
if (_playerState != value)
|
||||
{
|
||||
_previousPlayerState = _playerState;
|
||||
_playerState = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家的物品
|
||||
/// </summary>
|
||||
public Dictionary<long, PlayerItem> Items = new Dictionary<long, PlayerItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 当前手持物品id
|
||||
/// </summary>
|
||||
public long HandItemId;
|
||||
|
||||
/// <summary>
|
||||
/// 当前手持物品
|
||||
/// </summary>
|
||||
public PlayerItem HandItem => Items[HandItemId];
|
||||
|
||||
public void InitPlayer(MapUnitInfo unitInfo)
|
||||
{
|
||||
AddComponent<PlayerViewComponent>();
|
||||
if (unitInfo.Id == RoleModel.Instance.Id)
|
||||
{
|
||||
//自己
|
||||
AddComponent<PlayerInputComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void UnUseItem()
|
||||
{
|
||||
}
|
||||
|
||||
public void UseItem(ItemInfo item)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Fishing/New/Data/Player.cs.meta
Normal file
3
Assets/Scripts/Fishing/New/Data/Player.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f7d8cb1b2cd4e25913e17e2b54e7ad9
|
||||
timeCreated: 1773036936
|
||||
12
Assets/Scripts/Fishing/New/Data/PlayerItem.cs
Normal file
12
Assets/Scripts/Fishing/New/Data/PlayerItem.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Fantasy.Entitas;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerItem : Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置id
|
||||
/// </summary>
|
||||
public int ConfigID;
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Fishing/New/Data/PlayerItem.cs.meta
Normal file
3
Assets/Scripts/Fishing/New/Data/PlayerItem.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6649b122db2f46aea8147228c674a38c
|
||||
timeCreated: 1773037313
|
||||
3
Assets/Scripts/Fishing/New/View.meta
Normal file
3
Assets/Scripts/Fishing/New/View.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6965e70ae1742089580926e5b1adabf
|
||||
timeCreated: 1773037850
|
||||
3
Assets/Scripts/Fishing/New/View/Mono.meta
Normal file
3
Assets/Scripts/Fishing/New/View/Mono.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c38d17daa1164359ad9f9466be692b7c
|
||||
timeCreated: 1773038185
|
||||
185
Assets/Scripts/Fishing/New/View/Mono/PlayerAnimator.cs
Normal file
185
Assets/Scripts/Fishing/New/View/Mono/PlayerAnimator.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using KINEMATION.MagicBlend.Runtime;
|
||||
using NBC;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerAnimator : PlayerMonoBehaviour
|
||||
{
|
||||
public Animator _Animator;
|
||||
|
||||
private bool _isRodLayerEnabled;
|
||||
private bool _isInit;
|
||||
private PlayerIK _IK;
|
||||
private MagicBlending _magicBlending;
|
||||
private bool _IsInVehicle;
|
||||
|
||||
#region 参数定义
|
||||
|
||||
// public static readonly int IsSwiming = Animator.StringToHash("Swim");
|
||||
//
|
||||
// public static readonly int ThrowFar = Animator.StringToHash("ThrowFar");
|
||||
//
|
||||
// public static readonly int BoatDriving = Animator.StringToHash("BoatDriving");
|
||||
//
|
||||
// public static readonly int BaitInWater = Animator.StringToHash("BaitInWater");
|
||||
//
|
||||
// public static readonly int HeldRod = Animator.StringToHash("HeldRod");
|
||||
//
|
||||
// public static readonly int RodArming = Animator.StringToHash("RodArming");
|
||||
|
||||
public static readonly int Forward = Animator.StringToHash("Forward");
|
||||
|
||||
public static readonly int Turn = Animator.StringToHash("Turn");
|
||||
|
||||
public static readonly int OnGroundHash = Animator.StringToHash("OnGround");
|
||||
public static readonly int PrepareThrowHash = Animator.StringToHash("PrepareThrow");
|
||||
public static readonly int StartThrowHash = Animator.StringToHash("StartThrow");
|
||||
public static readonly int BaitThrownHash = Animator.StringToHash("BaitThrown");
|
||||
private static readonly int FishingUpHash = Animator.StringToHash("FishingUp");
|
||||
|
||||
public static readonly string LureRodLayer = "LureRod";
|
||||
public static readonly string HandRodLayer = "HandRod";
|
||||
|
||||
|
||||
public float FishingUp
|
||||
{
|
||||
get => _Animator.GetFloat(FishingUpHash);
|
||||
set => _Animator.SetFloat(FishingUpHash, value);
|
||||
}
|
||||
|
||||
public bool OnGround
|
||||
{
|
||||
get => _Animator.GetBool(OnGroundHash);
|
||||
set => _Animator.SetBool(OnGroundHash, value);
|
||||
}
|
||||
|
||||
public bool StartThrow
|
||||
{
|
||||
get => _Animator.GetBool(StartThrowHash);
|
||||
set => _Animator.SetBool(StartThrowHash, value);
|
||||
}
|
||||
|
||||
public bool BaitThrown
|
||||
{
|
||||
get => _Animator.GetBool(BaitThrownHash);
|
||||
set => _Animator.SetBool(BaitThrownHash, value);
|
||||
}
|
||||
|
||||
public bool PrepareThrow
|
||||
{
|
||||
get => _Animator.GetBool(PrepareThrowHash);
|
||||
set => _Animator.SetBool(PrepareThrowHash, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
_magicBlending = GetComponent<MagicBlending>();
|
||||
_Animator = GetComponent<Animator>();
|
||||
_Animator.keepAnimatorStateOnDisable = true;
|
||||
_IK = GetComponent<PlayerIK>();
|
||||
_isInit = true;
|
||||
// Player.OnFishingSetEquiped += OnFishingSetEquiped_OnRaised;
|
||||
// Player.OnFishingSetUnequip += OnFishingSetUnequip;
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Player.OnFishingSetEquiped -= OnFishingSetEquiped_OnRaised;
|
||||
// Player.OnFishingSetUnequip -= OnFishingSetUnequip;
|
||||
}
|
||||
|
||||
|
||||
private void OnFishingSetUnequip()
|
||||
{
|
||||
_isRodLayerEnabled = false;
|
||||
}
|
||||
|
||||
|
||||
private void OnFishingSetEquiped_OnRaised(FHandItem item)
|
||||
{
|
||||
if (item is FRod rod)
|
||||
{
|
||||
_isRodLayerEnabled = true;
|
||||
// var reel = Player.Rod.Reel;
|
||||
// _IK.SetBipedLeftHandIK(enabled: false, reel.FingersIKAnchor);
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SetLayerWeight(string layer, float weight)
|
||||
{
|
||||
_Animator.SetLayerWeight(_Animator.GetLayerIndex(layer), weight);
|
||||
}
|
||||
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
{
|
||||
float value3 = Mathf.Lerp(_Animator.GetFloat(Forward), Player.Speed / 5f,
|
||||
Time.deltaTime * 20f);
|
||||
float value4 = Mathf.Lerp(_Animator.GetFloat(Turn), Player.RotationSpeed,
|
||||
Time.deltaTime * 15f);
|
||||
_Animator.SetFloat(Forward, Mathf.Clamp01(value3));
|
||||
_Animator.SetFloat(Turn, Mathf.Clamp(value4, -1f, 1f));
|
||||
}
|
||||
|
||||
|
||||
_Animator.SetBool(OnGroundHash, _IsInVehicle || Player.IsGrounded);
|
||||
|
||||
|
||||
var isHandRodLayerEnabled = _isRodLayerEnabled && !Player.IsLureRod ? 1 : 0;
|
||||
|
||||
float handRodLayerWeight = _Animator.GetLayerWeight(_Animator.GetLayerIndex(HandRodLayer));
|
||||
SetLayerWeight(HandRodLayer,
|
||||
Mathf.MoveTowards(handRodLayerWeight, isHandRodLayerEnabled, Time.deltaTime * 3f));
|
||||
|
||||
|
||||
var isLureRodLayerEnabled = _isRodLayerEnabled && Player.IsLureRod ? 1 : 0;
|
||||
float lureRodLayerWeight = _Animator.GetLayerWeight(_Animator.GetLayerIndex(LureRodLayer));
|
||||
SetLayerWeight(LureRodLayer,
|
||||
Mathf.MoveTowards(lureRodLayerWeight, isLureRodLayerEnabled, Time.deltaTime * 3f));
|
||||
}
|
||||
|
||||
#region 动画事件
|
||||
|
||||
/// <summary>
|
||||
/// 抬杆到底动画事件
|
||||
/// </summary>
|
||||
public void OnRodPowerUp()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始抛出动画事件
|
||||
/// </summary>
|
||||
public void OnRodThrowStart()
|
||||
{
|
||||
// if (Player.State is PlayerStateThrow playerStateThrow)
|
||||
// {
|
||||
// playerStateThrow.OnRodThrowStart();
|
||||
// }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抛竿结束动画事件
|
||||
/// </summary>
|
||||
public void OnRodThrownEnd()
|
||||
{
|
||||
// if (Player.Fsm.CurrentState is PlayerStateThrow playerStateThrow)
|
||||
// {
|
||||
// playerStateThrow.OnRodThrownEnd();
|
||||
// }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerArm : MonoBehaviour
|
||||
public class PlayerArm : PlayerMonoBehaviour
|
||||
{
|
||||
public bool FixLowerArm;
|
||||
public bool IsLeft;
|
||||
@@ -18,7 +18,7 @@ namespace NBF
|
||||
|
||||
private const int MaxFixEyeAngle = 15;
|
||||
|
||||
public void Awake()
|
||||
protected override void OnAwake()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerChest : MonoBehaviour
|
||||
public class PlayerChest : PlayerMonoBehaviour
|
||||
{
|
||||
private const int MaxFixEyeAngle = 15;
|
||||
private const int MinFixEyeAngle = -10;
|
||||
@@ -14,8 +14,7 @@ namespace NBF
|
||||
|
||||
private void FixArmAngle()
|
||||
{
|
||||
var angle = FPlayerData.Instance.EyeAngle;
|
||||
|
||||
var angle = Player.EyeAngle;
|
||||
if (angle > MaxFixEyeAngle) angle = MaxFixEyeAngle;
|
||||
else if (angle < MinFixEyeAngle) angle = MinFixEyeAngle;
|
||||
var val = transform.localEulerAngles;
|
||||
58
Assets/Scripts/Fishing/New/View/Mono/PlayerIK.cs
Normal file
58
Assets/Scripts/Fishing/New/View/Mono/PlayerIK.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using RootMotion.FinalIK;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerIK : PlayerMonoBehaviour
|
||||
{
|
||||
public enum UpdateType
|
||||
{
|
||||
Update = 0,
|
||||
FixedUpdate = 1,
|
||||
LateUpdate = 2,
|
||||
Default = 3
|
||||
}
|
||||
|
||||
public UpdateType UpdateSelected;
|
||||
|
||||
private LookAtIK _LookAtIK;
|
||||
|
||||
[SerializeField] private float transitionWeightTimeScale = 1f;
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
_LookAtIK = GetComponent<LookAtIK>();
|
||||
}
|
||||
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (UpdateSelected == UpdateType.Update)
|
||||
{
|
||||
IKUpdateHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (UpdateSelected == UpdateType.FixedUpdate)
|
||||
{
|
||||
IKUpdateHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (UpdateSelected == UpdateType.LateUpdate)
|
||||
{
|
||||
IKUpdateHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void IKUpdateHandler()
|
||||
{
|
||||
_LookAtIK.UpdateSolverExternal();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Assets/Scripts/Fishing/New/View/Mono/PlayerMonoBehaviour.cs
Normal file
22
Assets/Scripts/Fishing/New/View/Mono/PlayerMonoBehaviour.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public abstract class PlayerMonoBehaviour : MonoBehaviour
|
||||
{
|
||||
public Player Player { get; private set; }
|
||||
|
||||
public PlayerUnityComponent UnityComponent { get; private set; }
|
||||
|
||||
protected void Awake()
|
||||
{
|
||||
UnityComponent = GetComponentInParent<PlayerUnityComponent>();
|
||||
Player = UnityComponent.Player;
|
||||
OnAwake();
|
||||
}
|
||||
|
||||
protected virtual void OnAwake()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fbde40efe5345cd8c05ea6c1f1914cf
|
||||
timeCreated: 1773040970
|
||||
25
Assets/Scripts/Fishing/New/View/Mono/PlayerUnityComponent.cs
Normal file
25
Assets/Scripts/Fishing/New/View/Mono/PlayerUnityComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using ECM2;
|
||||
using ECM2.Examples.FirstPerson;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerUnityComponent : MonoBehaviour
|
||||
{
|
||||
public Player Player { get; set; }
|
||||
public Transform Root;
|
||||
public Transform Eye;
|
||||
public Transform FppLook;
|
||||
public Transform IK;
|
||||
public PlayerModelAsset ModelAsset;
|
||||
public CharacterMovement Character;
|
||||
public FirstPersonCharacter FirstPerson;
|
||||
|
||||
[Header("视角相关")] public float MouseSensitivity = 0.1f;
|
||||
[Space(15f)] public bool invertLook = true;
|
||||
|
||||
public float minPitch = -60f;
|
||||
|
||||
public float maxPitch = 60f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a25908f34e4e4464a922b88337a5b733
|
||||
timeCreated: 1773038189
|
||||
200
Assets/Scripts/Fishing/New/View/PlayerInputComponent.cs
Normal file
200
Assets/Scripts/Fishing/New/View/PlayerInputComponent.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using Fantasy.Entitas;
|
||||
using Fantasy.Entitas.Interface;
|
||||
using NBC;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerInputComponent : Entity
|
||||
{
|
||||
public Player Player { get; private set; }
|
||||
public PlayerViewComponent View { get; private set; }
|
||||
|
||||
#region 生命周期
|
||||
|
||||
public class PlayerViewAwakeSystem : AwakeSystem<PlayerInputComponent>
|
||||
{
|
||||
protected override void Awake(PlayerInputComponent self)
|
||||
{
|
||||
self.Player = self.GetParent<Player>();
|
||||
self.View = self.Player.GetComponent<PlayerViewComponent>();
|
||||
self.AddInputEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerViewUpdateSystem : UpdateSystem<PlayerInputComponent>
|
||||
{
|
||||
protected override void Update(PlayerInputComponent self)
|
||||
{
|
||||
self.UpdateMove();
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerViewDestroySystem : DestroySystem<PlayerInputComponent>
|
||||
{
|
||||
protected override void Destroy(PlayerInputComponent self)
|
||||
{
|
||||
self.RemoveInputEvent();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input
|
||||
|
||||
private void AddInputEvent()
|
||||
{
|
||||
InputManager.OnPlayerPerformed += OnPlayerCanceled;
|
||||
InputManager.OnPlayerPerformed += OnPlayerPerformed;
|
||||
|
||||
InputManager.OnPlayerValueCanceled += OnPlayerValueCanceled;
|
||||
InputManager.OnPlayerValuePerformed += OnPlayerValuePerformed;
|
||||
}
|
||||
|
||||
private void RemoveInputEvent()
|
||||
{
|
||||
InputManager.OnPlayerPerformed += OnPlayerCanceled;
|
||||
InputManager.OnPlayerPerformed += OnPlayerPerformed;
|
||||
|
||||
InputManager.OnPlayerValueCanceled += OnPlayerValueCanceled;
|
||||
InputManager.OnPlayerValuePerformed += OnPlayerValuePerformed;
|
||||
}
|
||||
|
||||
private void OnPlayerPerformed(string action)
|
||||
{
|
||||
if (action == InputDef.Player.Run)
|
||||
{
|
||||
Player.Run = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerCanceled(string action)
|
||||
{
|
||||
if (action == InputDef.Player.Run)
|
||||
{
|
||||
Player.Run = false;
|
||||
}
|
||||
else if (action == InputDef.Player.ToBag)
|
||||
{
|
||||
//取消手持物品
|
||||
Log.Info($"取消手持物品");
|
||||
Player.UnUseItem();
|
||||
// Game.Instance.StartCoroutine(UnUseItem());
|
||||
}
|
||||
else if (action.StartsWith(InputDef.Player.QuickStarts))
|
||||
{
|
||||
var index = int.Parse(action.Replace(InputDef.Player.QuickStarts, string.Empty));
|
||||
Log.Info($"快速使用===={index}");
|
||||
var item = RoleModel.Instance.GetSlotItem(index - 1);
|
||||
if (item != null)
|
||||
{
|
||||
Player.UseItem(item);
|
||||
// Game.Instance.StartCoroutine(UseItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerValueCanceled(InputAction.CallbackContext context)
|
||||
{
|
||||
var actionName = context.action.name;
|
||||
if (actionName == InputDef.Player.Move)
|
||||
{
|
||||
Player.MoveInput = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerValuePerformed(InputAction.CallbackContext context)
|
||||
{
|
||||
var actionName = context.action.name;
|
||||
if (actionName == InputDef.Player.Move)
|
||||
{
|
||||
var v2 = context.ReadValue<Vector2>();
|
||||
Player.MoveInput = v2;
|
||||
}
|
||||
else if (actionName == InputDef.Player.Look)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Move
|
||||
|
||||
private Quaternion lastRotation;
|
||||
|
||||
private void UpdateMove()
|
||||
{
|
||||
UpdateGrounded();
|
||||
ProcessMoveStates();
|
||||
UpdateLookInput();
|
||||
}
|
||||
|
||||
private void ProcessMoveStates()
|
||||
{
|
||||
{
|
||||
var num2 = Player.Run ? 7 : 5;
|
||||
Vector3 vector2 = View.Unity.FirstPerson.GetRightVector() * Player.MoveInput.x * num2;
|
||||
vector2 += View.Unity.FirstPerson.GetForwardVector() * Player.MoveInput.y * num2;
|
||||
// if (checkWaterBound)
|
||||
// {
|
||||
// SetMovementDirectionWithRaycastCheck(vector2);
|
||||
// }
|
||||
// else
|
||||
{
|
||||
View.Unity.FirstPerson.SetMovementDirection(vector2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGrounded()
|
||||
{
|
||||
Player.IsGrounded = View.Unity.FirstPerson.IsGrounded();
|
||||
Player.Speed = View.Unity.FirstPerson.velocity.magnitude;
|
||||
|
||||
Quaternion rotation = View.Unity.FirstPerson.transform.rotation;
|
||||
|
||||
// 计算当前帧与上一帧的旋转差异
|
||||
Quaternion rotationDelta = rotation * Quaternion.Inverse(lastRotation);
|
||||
|
||||
// 将四元数转换为角度轴表示
|
||||
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
|
||||
|
||||
// 确保角度在0-360范围内
|
||||
if (angle > 180f) angle -= 360f;
|
||||
|
||||
// 获取Y轴旋转分量(归一化处理)
|
||||
float yRotation = 0f;
|
||||
if (Mathf.Abs(angle) > 0.001f && Mathf.Abs(axis.y) > 0.1f)
|
||||
{
|
||||
// 计算Y轴方向的旋转角度(考虑旋转轴方向)
|
||||
yRotation = angle * Mathf.Sign(axis.y);
|
||||
}
|
||||
|
||||
float maxTurnSpeed = 180f; // 度/秒
|
||||
// 转换为角速度并归一化到[-1, 1]
|
||||
float angularSpeed = yRotation / Time.deltaTime;
|
||||
float turnValue = Mathf.Clamp(angularSpeed / maxTurnSpeed, -1f, 1f);
|
||||
|
||||
|
||||
Player.RotationSpeed = turnValue;
|
||||
|
||||
lastRotation = rotation;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Look
|
||||
|
||||
private void UpdateLookInput()
|
||||
{
|
||||
Vector2 value = InputManager.GetLookInput();
|
||||
var u3d = View.Unity;
|
||||
u3d.FirstPerson.AddControlYawInput(value.x * u3d.MouseSensitivity);
|
||||
u3d.FirstPerson.AddControlPitchInput((u3d.invertLook ? 0f - value.y : value.y) * u3d.MouseSensitivity,
|
||||
u3d.minPitch, u3d.maxPitch);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d74cb6e741243478aeb0a3053211fcd
|
||||
timeCreated: 1773039193
|
||||
89
Assets/Scripts/Fishing/New/View/PlayerViewComponent.cs
Normal file
89
Assets/Scripts/Fishing/New/View/PlayerViewComponent.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Fantasy.Entitas;
|
||||
using Fantasy.Entitas.Interface;
|
||||
using NBF.Fishing2;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerViewComponent : Entity
|
||||
{
|
||||
public Player Player { get; private set; }
|
||||
|
||||
public PlayerUnityComponent Unity { get; private set; }
|
||||
|
||||
#region 生命周期
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
Player = GetParent<Player>();
|
||||
var gameObject = PrefabsHelper.CreatePlayer(SceneSettings.Instance.Node);
|
||||
Unity = gameObject.GetComponent<PlayerUnityComponent>();
|
||||
Unity.Player = Player;
|
||||
CreatePlayerModel();
|
||||
if (Player.IsSelf)
|
||||
{
|
||||
CameraManager.Instance.SetFppLook(Unity);
|
||||
}
|
||||
Unity.transform.localPosition = new Vector3(484, 1, 422);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
}
|
||||
|
||||
public void LateUpdate()
|
||||
{
|
||||
Player.EyeAngle = GameUtils.GetVerticalAngle(Unity.transform, Unity.FppLook);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 模型创建
|
||||
|
||||
private void CreatePlayerModel()
|
||||
{
|
||||
var modelObject = PrefabsHelper.CreatePlayer(Unity.Root, "Human_Male");
|
||||
modelObject.transform.localPosition = Vector3.zero;
|
||||
Unity.ModelAsset = modelObject.GetComponent<PlayerModelAsset>();
|
||||
Unity.ModelAsset.SetPlayer(Unity.FppLook);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class PlayerViewAwakeSystem : AwakeSystem<PlayerViewComponent>
|
||||
{
|
||||
protected override void Awake(PlayerViewComponent self)
|
||||
{
|
||||
self.Awake();
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerViewDestroySystem : DestroySystem<PlayerViewComponent>
|
||||
{
|
||||
protected override void Destroy(PlayerViewComponent self)
|
||||
{
|
||||
self.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerViewUpdateSystem : UpdateSystem<PlayerViewComponent>
|
||||
{
|
||||
protected override void Update(PlayerViewComponent self)
|
||||
{
|
||||
self.Update();
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerViewLateUpdateSystem : LateUpdateSystem<PlayerViewComponent>
|
||||
{
|
||||
protected override void LateUpdate(PlayerViewComponent self)
|
||||
{
|
||||
self.LateUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 418da378516646cdb672fa05c2066432
|
||||
timeCreated: 1773037811
|
||||
@@ -1,93 +0,0 @@
|
||||
using NBC;
|
||||
using NBF.Utils;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public partial class FPlayer
|
||||
{
|
||||
#region Input
|
||||
|
||||
private void AddInputEvent()
|
||||
{
|
||||
InputManager.OnPlayerPerformed += OnPlayerCanceled;
|
||||
InputManager.OnPlayerPerformed += OnPlayerPerformed;
|
||||
|
||||
InputManager.OnPlayerValueCanceled += OnPlayerValueCanceled;
|
||||
InputManager.OnPlayerValuePerformed += OnPlayerValuePerformed;
|
||||
}
|
||||
|
||||
private void RemoveInputEvent()
|
||||
{
|
||||
InputManager.OnPlayerPerformed += OnPlayerCanceled;
|
||||
InputManager.OnPlayerPerformed += OnPlayerPerformed;
|
||||
|
||||
InputManager.OnPlayerValueCanceled += OnPlayerValueCanceled;
|
||||
InputManager.OnPlayerValuePerformed += OnPlayerValuePerformed;
|
||||
}
|
||||
|
||||
private void OnPlayerPerformed(string action)
|
||||
{
|
||||
if (action == InputDef.Player.Run)
|
||||
{
|
||||
Data.Run = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerCanceled(string action)
|
||||
{
|
||||
if (action == InputDef.Player.Run)
|
||||
{
|
||||
Data.Run = false;
|
||||
}
|
||||
else if (action == InputDef.Player.ToBag)
|
||||
{
|
||||
//取消手持物品
|
||||
Log.Info($"取消手持物品");
|
||||
Game.Instance.StartCoroutine(UnUseItem());
|
||||
}
|
||||
else if (action.StartsWith(InputDef.Player.QuickStarts))
|
||||
{
|
||||
var index = int.Parse(action.Replace(InputDef.Player.QuickStarts, string.Empty));
|
||||
Log.Info($"快速使用===={index}");
|
||||
var item = RoleModel.Instance.GetSlotItem(index - 1);
|
||||
if (item != null)
|
||||
{
|
||||
Game.Instance.StartCoroutine(UseItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerValueCanceled(InputAction.CallbackContext context)
|
||||
{
|
||||
var actionName = context.action.name;
|
||||
if (actionName == InputDef.Player.Move)
|
||||
{
|
||||
// var v2 = context.ReadValue<Vector2>();
|
||||
Data.MoveInput = Vector2.zero;
|
||||
// SendMoveMessage(v2, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerValuePerformed(InputAction.CallbackContext context)
|
||||
{
|
||||
// var mapUnit = Parent as MapUnit;
|
||||
// Log.Info($"OnPlayerValuePerformed IsSelf={mapUnit.IsSelf()} id={mapUnit.Id}");
|
||||
var actionName = context.action.name;
|
||||
if (actionName == InputDef.Player.Move)
|
||||
{
|
||||
var v2 = context.ReadValue<Vector2>();
|
||||
Data.MoveInput = v2;
|
||||
// SendMoveMessage(v2, false);
|
||||
}
|
||||
else if (actionName == InputDef.Player.Look)
|
||||
{
|
||||
var v2 = context.ReadValue<Vector2>();
|
||||
// UpdatePlayerRotation(v2);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea2c2e2d4b4344f0bc9d58ba0aeec6fd
|
||||
timeCreated: 1766505279
|
||||
@@ -1,146 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public partial class FPlayer
|
||||
{
|
||||
#region Move
|
||||
|
||||
private Quaternion lastRotation;
|
||||
|
||||
private void UpdateMove()
|
||||
{
|
||||
UpdateGrounded();
|
||||
UpdateWater();
|
||||
ProcessMoveStates();
|
||||
UpdateLookInput();
|
||||
}
|
||||
|
||||
private void UpdateGrounded()
|
||||
{
|
||||
Data.IsGrounded = FirstPerson.IsGrounded();
|
||||
Data.Speed = FirstPerson.velocity.magnitude;
|
||||
|
||||
Quaternion rotation = FirstPerson.transform.rotation;
|
||||
|
||||
// 计算当前帧与上一帧的旋转差异
|
||||
Quaternion rotationDelta = rotation * Quaternion.Inverse(lastRotation);
|
||||
|
||||
// 将四元数转换为角度轴表示
|
||||
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
|
||||
|
||||
// 确保角度在0-360范围内
|
||||
if (angle > 180f) angle -= 360f;
|
||||
|
||||
// 获取Y轴旋转分量(归一化处理)
|
||||
float yRotation = 0f;
|
||||
if (Mathf.Abs(angle) > 0.001f && Mathf.Abs(axis.y) > 0.1f)
|
||||
{
|
||||
// 计算Y轴方向的旋转角度(考虑旋转轴方向)
|
||||
yRotation = angle * Mathf.Sign(axis.y);
|
||||
}
|
||||
|
||||
float maxTurnSpeed = 180f; // 度/秒
|
||||
// 转换为角速度并归一化到[-1, 1]
|
||||
float angularSpeed = yRotation / Time.deltaTime;
|
||||
float turnValue = Mathf.Clamp(angularSpeed / maxTurnSpeed, -1f, 1f);
|
||||
|
||||
|
||||
Data.RotationSpeed = turnValue;
|
||||
|
||||
lastRotation = rotation;
|
||||
}
|
||||
|
||||
private void UpdateWater()
|
||||
{
|
||||
// SceneSettings.Instance.Water.w
|
||||
}
|
||||
|
||||
private void ProcessMoveStates()
|
||||
{
|
||||
// if (CameraView.Value == CameraViewType.TPP)
|
||||
// {
|
||||
// float num = (IsRunPressed.Value ? MovementSpeed.Value : (MovementSpeed.Value * 0.5f));
|
||||
// num = (IsFlyModeEnabled ? (num * (float)FlySpeed) : num);
|
||||
// Vector3 zero = Vector3.zero;
|
||||
// zero += Vector3.right * MovementDirection.Value.x;
|
||||
// zero += Vector3.forward * MovementDirection.Value.y;
|
||||
// zero = zero.relativeTo(_CameraTPPTarget, _Character.GetUpVector());
|
||||
// _Character.RotateTowards(zero, Time.deltaTime * _RotateTPPSpeed);
|
||||
// float value = Vector3.Dot(_Character.GetForwardVector(), zero);
|
||||
// Vector3 vector = _Character.GetForwardVector() * Mathf.Clamp01(value) * num;
|
||||
// if (checkWaterBound)
|
||||
// {
|
||||
// SetMovementDirectionWithRaycastCheck(vector);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// _Character.SetMovementDirection(vector);
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
{
|
||||
var num2 = Data.Run ? 7 : 5;
|
||||
//(IsRunPressed.Value ? MovementSpeed.Value : (MovementSpeed.Value * 0.5f));
|
||||
// num2 = (IsFlyModeEnabled ? (num2 * (float)FlySpeed) : num2);
|
||||
Vector3 vector2 = FirstPerson.GetRightVector() * Data.MoveInput.x * num2;
|
||||
vector2 += FirstPerson.GetForwardVector() * Data.MoveInput.y * num2;
|
||||
// if (checkWaterBound)
|
||||
// {
|
||||
// SetMovementDirectionWithRaycastCheck(vector2);
|
||||
// }
|
||||
// else
|
||||
{
|
||||
FirstPerson.SetMovementDirection(vector2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Look
|
||||
|
||||
public float MouseSensitivity = 0.1f;
|
||||
[Space(15f)] public bool invertLook = true;
|
||||
|
||||
public float minPitch = -60f;
|
||||
|
||||
public float maxPitch = 60f;
|
||||
|
||||
private void UpdateLookInput()
|
||||
{
|
||||
// TPPLookTarget.position = base.transform.position;
|
||||
// if (CameraView.Value == CameraViewType.TPP)
|
||||
// {
|
||||
// lookXRot -= MouseInput.Value.y;
|
||||
// lookXRot = Mathf.Clamp(lookXRot, -25f, 55f);
|
||||
// lookYRot += MouseInput.Value.x;
|
||||
// lookYRot = Mathf.Repeat(lookYRot, 360f);
|
||||
// TPPLookTarget.localEulerAngles = new Vector3(lookXRot, lookYRot, 0f);
|
||||
// }
|
||||
// else if (CameraView.Value == CameraViewType.FPP)
|
||||
{
|
||||
// if (_IsInVehicle && PlayerState.Value == State.vehicle)
|
||||
// {
|
||||
// lookXRot -= MouseInput.Value.y;
|
||||
// lookXRot = Mathf.Clamp(lookXRot, VehicleLookXMinMax.x, VehicleLookXMinMax.y);
|
||||
// lookYRot += MouseInput.Value.x;
|
||||
// lookYRot = Mathf.Clamp(lookYRot, VehicleLookYMinMax.x, VehicleLookYMinMax.y);
|
||||
// VehicleLookTargetParent.localEulerAngles = new Vector3(lookXRot, lookYRot, 0f);
|
||||
// _character.CameraPitch = 0f;
|
||||
// }
|
||||
// else
|
||||
{
|
||||
Vector2 value = InputManager.GetLookInput();
|
||||
FirstPerson.AddControlYawInput(value.x * (float)MouseSensitivity);
|
||||
FirstPerson.AddControlPitchInput((invertLook ? (0f - value.y) : value.y) * (float)MouseSensitivity,
|
||||
minPitch, maxPitch);
|
||||
// lookXRot = base.transform.eulerAngles.x;
|
||||
// lookYRot = base.transform.eulerAngles.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae72860c045147af8022a3cbb30481ab
|
||||
timeCreated: 1766471468
|
||||
@@ -1,165 +1,142 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using ECM2;
|
||||
using ECM2.Examples.FirstPerson;
|
||||
using Fantasy;
|
||||
using NBC;
|
||||
using NBF.Fishing2;
|
||||
using NBF.Utils;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public partial class FPlayer : MonoService<FPlayer>
|
||||
{
|
||||
public Transform Root;
|
||||
public Transform Eye;
|
||||
public Transform FppLook;
|
||||
public Transform IK;
|
||||
public PlayerModelAsset ModelAsset;
|
||||
public CharacterMovement Character;
|
||||
public FirstPersonCharacter FirstPerson;
|
||||
public GameObject ModelGameObject { get; set; }
|
||||
|
||||
|
||||
public FPlayerData Data { get; private set; }
|
||||
|
||||
public readonly List<FRod> Tackles = new List<FRod>();
|
||||
public FRod Rod { get; private set; }
|
||||
public Fsm<FPlayer> Fsm { get; private set; }
|
||||
|
||||
public event Action<FHandItem> OnFishingSetEquiped;
|
||||
public event Action OnFishingSetUnequip;
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
Character = gameObject.GetComponent<CharacterMovement>();
|
||||
FirstPerson = gameObject.GetComponent<FirstPersonCharacter>();
|
||||
Data = FPlayerData.Instance;
|
||||
transform.localPosition = new Vector3(484, 1, 422);
|
||||
// Data.NeedChangeRightArmAngle = true;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitFsm();
|
||||
AddInputEvent();
|
||||
CreatePlayerModel();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
UpdateMove();
|
||||
Fsm?.Update();
|
||||
|
||||
Data.EyeAngle = GameUtils.GetVerticalAngle(transform, FppLook);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RemoveInputEvent();
|
||||
}
|
||||
|
||||
#region 状态机
|
||||
|
||||
private void InitFsm()
|
||||
{
|
||||
Fsm = new Fsm<FPlayer>("Player", this, true);
|
||||
Fsm.RegisterState<PlayerStateIdle>();
|
||||
Fsm.RegisterState<PlayerStateThrow>();
|
||||
Fsm.RegisterState<PlayerStateFishing>();
|
||||
Fsm.RegisterState<PlayerStateFight>();
|
||||
Fsm.RegisterState<PlayerStatePrepare>();
|
||||
Fsm.Start<PlayerStateIdle>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 角色模型
|
||||
|
||||
private void CreatePlayerModel()
|
||||
{
|
||||
var modelObject = PrefabsHelper.CreatePlayer(Root, "Human_Male");
|
||||
modelObject.transform.localPosition = Vector3.zero;
|
||||
ModelGameObject = modelObject;
|
||||
ModelAsset = modelObject.GetComponent<PlayerModelAsset>();
|
||||
ModelAsset.SetPlayer(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 使用物品
|
||||
|
||||
public IEnumerator UseItem(ItemInfo item)
|
||||
{
|
||||
if (Data.ChangeItem) yield break;
|
||||
Data.ChangeItem = true;
|
||||
var itemType = item?.ConfigId.GetItemType();
|
||||
if (itemType == ItemType.Rod)
|
||||
{
|
||||
//判断旧的是否要收回
|
||||
yield return UnUseItemConfirm();
|
||||
|
||||
Data.IsLureRod = true;
|
||||
var rodType = (ItemSubType)item.Config.Type;
|
||||
if (rodType == ItemSubType.RodTele)
|
||||
{
|
||||
Data.IsLureRod = false;
|
||||
}
|
||||
|
||||
Rod =
|
||||
item.Config.InstantiateAndComponent<FRod>(SceneSettings.Instance.GearNode, Vector3.zero,
|
||||
Quaternion.identity);
|
||||
yield return Rod.InitRod(this, item);
|
||||
Tackles.Add(Rod);
|
||||
OnFishingSetEquiped?.Invoke(Rod);
|
||||
}
|
||||
|
||||
Data.ChangeItem = false;
|
||||
}
|
||||
|
||||
public IEnumerator UnUseItem()
|
||||
{
|
||||
if (Data.ChangeItem) yield break;
|
||||
Data.ChangeItem = true;
|
||||
yield return UnUseItemConfirm();
|
||||
Data.ChangeItem = false;
|
||||
}
|
||||
|
||||
private IEnumerator UnUseItemConfirm()
|
||||
{
|
||||
if (Rod != null)
|
||||
{
|
||||
OnFishingSetUnequip?.Invoke();
|
||||
yield return Rod.Destroy();
|
||||
yield return new WaitForSeconds(0.35f);
|
||||
Destroy(Rod.gameObject);
|
||||
Tackles.Remove(Rod);
|
||||
Rod = null;
|
||||
yield return new WaitForSeconds(0.15f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 线断了
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <param name="loseBaitChance"></param>
|
||||
public void LineBreak(string msg, float loseBaitChance)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
// using System;
|
||||
// using System.Collections;
|
||||
// using System.Collections.Generic;
|
||||
// using ECM2;
|
||||
// using ECM2.Examples.FirstPerson;
|
||||
// using Fantasy;
|
||||
// using NBC;
|
||||
// using NBF.Fishing2;
|
||||
// using NBF.Utils;
|
||||
// using UnityEngine;
|
||||
// using UnityEngine.InputSystem;
|
||||
// using Object = UnityEngine.Object;
|
||||
//
|
||||
// namespace NBF
|
||||
// {
|
||||
// public partial class FPlayer : MonoService<FPlayer>
|
||||
// {
|
||||
// public Transform Root;
|
||||
// public Transform Eye;
|
||||
// public Transform FppLook;
|
||||
// public Transform IK;
|
||||
// public PlayerModelAsset ModelAsset;
|
||||
// public CharacterMovement Character;
|
||||
// public FirstPersonCharacter FirstPerson;
|
||||
// public GameObject ModelGameObject { get; set; }
|
||||
//
|
||||
//
|
||||
// // public FPlayerData Data { get; private set; }
|
||||
//
|
||||
// public readonly List<FRod> Tackles = new List<FRod>();
|
||||
// public FRod Rod { get; private set; }
|
||||
// public Fsm<FPlayer> Fsm { get; private set; }
|
||||
//
|
||||
// public event Action<FHandItem> OnFishingSetEquiped;
|
||||
// public event Action OnFishingSetUnequip;
|
||||
//
|
||||
// protected override void OnAwake()
|
||||
// {
|
||||
// Character = gameObject.GetComponent<CharacterMovement>();
|
||||
// FirstPerson = gameObject.GetComponent<FirstPersonCharacter>();
|
||||
// // Data = PlayerDataManager.Instance.Self;
|
||||
// transform.localPosition = new Vector3(484, 1, 422);
|
||||
// // Data.NeedChangeRightArmAngle = true;
|
||||
// }
|
||||
//
|
||||
// private void Start()
|
||||
// {
|
||||
// InitFsm();
|
||||
// CreatePlayerModel();
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private void LateUpdate()
|
||||
// {
|
||||
// Fsm?.Update();
|
||||
// }
|
||||
//
|
||||
// #region 状态机
|
||||
//
|
||||
// private void InitFsm()
|
||||
// {
|
||||
// Fsm = new Fsm<FPlayer>("Player", this, true);
|
||||
// Fsm.RegisterState<PlayerStateIdle>();
|
||||
// Fsm.RegisterState<PlayerStateThrow>();
|
||||
// Fsm.RegisterState<PlayerStateFishing>();
|
||||
// Fsm.RegisterState<PlayerStateFight>();
|
||||
// Fsm.RegisterState<PlayerStatePrepare>();
|
||||
// Fsm.Start<PlayerStateIdle>();
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region 角色模型
|
||||
//
|
||||
// private void CreatePlayerModel()
|
||||
// {
|
||||
// var modelObject = PrefabsHelper.CreatePlayer(Root, "Human_Male");
|
||||
// modelObject.transform.localPosition = Vector3.zero;
|
||||
// ModelGameObject = modelObject;
|
||||
// ModelAsset = modelObject.GetComponent<PlayerModelAsset>();
|
||||
// ModelAsset.SetPlayer(this);
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region 使用物品
|
||||
//
|
||||
// public IEnumerator UseItem(ItemInfo item)
|
||||
// {
|
||||
// // if (Data.ChangeItem) yield break;
|
||||
// // Data.ChangeItem = true;
|
||||
// // var itemType = item?.ConfigId.GetItemType();
|
||||
// // if (itemType == ItemType.Rod)
|
||||
// // {
|
||||
// // //判断旧的是否要收回
|
||||
// // yield return UnUseItemConfirm();
|
||||
// //
|
||||
// // Data.IsLureRod = true;
|
||||
// // var rodType = (ItemSubType)item.Config.Type;
|
||||
// // if (rodType == ItemSubType.RodTele)
|
||||
// // {
|
||||
// // Data.IsLureRod = false;
|
||||
// // }
|
||||
// //
|
||||
// // Rod =
|
||||
// // item.Config.InstantiateAndComponent<FRod>(SceneSettings.Instance.GearNode, Vector3.zero,
|
||||
// // Quaternion.identity);
|
||||
// // yield return Rod.InitRod(this, item);
|
||||
// // Tackles.Add(Rod);
|
||||
// // OnFishingSetEquiped?.Invoke(Rod);
|
||||
// // }
|
||||
// //
|
||||
// // Data.ChangeItem = false;
|
||||
// yield return null;
|
||||
// }
|
||||
//
|
||||
// public IEnumerator UnUseItem()
|
||||
// {
|
||||
// // if (Data.ChangeItem) yield break;
|
||||
// // Data.ChangeItem = true;
|
||||
// // yield return UnUseItemConfirm();
|
||||
// // Data.ChangeItem = false;
|
||||
// yield return null;
|
||||
// }
|
||||
//
|
||||
// private IEnumerator UnUseItemConfirm()
|
||||
// {
|
||||
// if (Rod != null)
|
||||
// {
|
||||
// OnFishingSetUnequip?.Invoke();
|
||||
// yield return Rod.Destroy();
|
||||
// yield return new WaitForSeconds(0.35f);
|
||||
// Destroy(Rod.gameObject);
|
||||
// Tackles.Remove(Rod);
|
||||
// Rod = null;
|
||||
// yield return new WaitForSeconds(0.15f);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
// }
|
||||
// }
|
||||
@@ -1,87 +0,0 @@
|
||||
// using System;
|
||||
// using UnityEngine;
|
||||
//
|
||||
// namespace NBF
|
||||
// {
|
||||
// // [Serializable]
|
||||
// // public enum PlayerState
|
||||
// // {
|
||||
// // idle = 0,
|
||||
// // move = 1,
|
||||
// // prepare = 2,
|
||||
// // casting = 3,
|
||||
// // fishing = 4,
|
||||
// // baitFlies = 5,
|
||||
// // fight = 6,
|
||||
// // fishView = 7,
|
||||
// // collectFish = 8,
|
||||
// // throwFish = 9,
|
||||
// // vehicle = 10,
|
||||
// // swiming = 11,
|
||||
// // flyModeDebug = 12,
|
||||
// // vehicleFishing = 13,
|
||||
// // preciseCastIdle = 14,
|
||||
// // preciseCastThrow = 15
|
||||
// // }
|
||||
//
|
||||
|
||||
//
|
||||
// public class FPlayerData : MonoService<FPlayerData>
|
||||
// {
|
||||
// private PlayerState _previousPlayerState = PlayerState.Idle;
|
||||
// private PlayerState _playerState;
|
||||
//
|
||||
// public bool ChangeItem;
|
||||
// public bool Run;
|
||||
// public bool IsGrounded;
|
||||
// public float Speed;
|
||||
// public float RotationSpeed;
|
||||
// public float ReelSpeed;
|
||||
// public float LineTension;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 是否路亚竿
|
||||
// /// </summary>
|
||||
// public bool IsLureRod;
|
||||
//
|
||||
// public Vector2 MoveInput;
|
||||
//
|
||||
// /// <summary>
|
||||
// ///
|
||||
// /// </summary>
|
||||
// public float EyeAngle;
|
||||
//
|
||||
//
|
||||
// public PlayerState PreviousState => _previousPlayerState;
|
||||
//
|
||||
// public PlayerState State
|
||||
// {
|
||||
// get => _playerState;
|
||||
// set
|
||||
// {
|
||||
// _previousPlayerState = _playerState;
|
||||
// _playerState = value;
|
||||
// NextState = value;
|
||||
// OnStateChange?.Invoke(_playerState);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// [SerializeField] private PlayerState NextState;
|
||||
//
|
||||
// public event Action<PlayerState> OnStateChange;
|
||||
//
|
||||
//
|
||||
// private void Start()
|
||||
// {
|
||||
// NextState = State;
|
||||
// }
|
||||
//
|
||||
// private void Update()
|
||||
// {
|
||||
// if (NextState != State)
|
||||
// {
|
||||
// State = NextState;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53629a9cec2b4caf9a739c21f7abdf3c
|
||||
timeCreated: 1766471002
|
||||
@@ -1,306 +0,0 @@
|
||||
using System;
|
||||
using KINEMATION.MagicBlend.Runtime;
|
||||
using NBC;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerAnimator : MonoBehaviour
|
||||
{
|
||||
public Animator _Animator;
|
||||
public FPlayer Player { get; private set; }
|
||||
|
||||
private bool _isRodLayerEnabled;
|
||||
private bool _isInit;
|
||||
private PlayerIK _IK;
|
||||
private MagicBlending _magicBlending;
|
||||
private bool _IsInVehicle;
|
||||
|
||||
#region 参数定义
|
||||
|
||||
// public static readonly int IsSwiming = Animator.StringToHash("Swim");
|
||||
//
|
||||
// public static readonly int ThrowFar = Animator.StringToHash("ThrowFar");
|
||||
//
|
||||
// public static readonly int BoatDriving = Animator.StringToHash("BoatDriving");
|
||||
//
|
||||
// public static readonly int BaitInWater = Animator.StringToHash("BaitInWater");
|
||||
//
|
||||
// public static readonly int HeldRod = Animator.StringToHash("HeldRod");
|
||||
//
|
||||
// public static readonly int RodArming = Animator.StringToHash("RodArming");
|
||||
|
||||
public static readonly int Forward = Animator.StringToHash("Forward");
|
||||
|
||||
public static readonly int Turn = Animator.StringToHash("Turn");
|
||||
|
||||
public static readonly int OnGroundHash = Animator.StringToHash("OnGround");
|
||||
public static readonly int PrepareThrowHash = Animator.StringToHash("PrepareThrow");
|
||||
public static readonly int StartThrowHash = Animator.StringToHash("StartThrow");
|
||||
public static readonly int BaitThrownHash = Animator.StringToHash("BaitThrown");
|
||||
private static readonly int FishingUpHash = Animator.StringToHash("FishingUp");
|
||||
|
||||
public static readonly string LureRodLayer = "LureRod";
|
||||
public static readonly string HandRodLayer = "HandRod";
|
||||
|
||||
|
||||
public float FishingUp
|
||||
{
|
||||
get => _Animator.GetFloat(FishingUpHash);
|
||||
set => _Animator.SetFloat(FishingUpHash, value);
|
||||
}
|
||||
|
||||
public bool OnGround
|
||||
{
|
||||
get => _Animator.GetBool(OnGroundHash);
|
||||
set => _Animator.SetBool(OnGroundHash, value);
|
||||
}
|
||||
|
||||
public bool StartThrow
|
||||
{
|
||||
get => _Animator.GetBool(StartThrowHash);
|
||||
set => _Animator.SetBool(StartThrowHash, value);
|
||||
}
|
||||
|
||||
public bool BaitThrown
|
||||
{
|
||||
get => _Animator.GetBool(BaitThrownHash);
|
||||
set => _Animator.SetBool(BaitThrownHash, value);
|
||||
}
|
||||
|
||||
public bool PrepareThrow
|
||||
{
|
||||
get => _Animator.GetBool(PrepareThrowHash);
|
||||
set => _Animator.SetBool(PrepareThrowHash, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Player = GetComponentInParent<FPlayer>();
|
||||
_magicBlending = GetComponent<MagicBlending>();
|
||||
_Animator = GetComponent<Animator>();
|
||||
_Animator.keepAnimatorStateOnDisable = true;
|
||||
_IK = GetComponent<PlayerIK>();
|
||||
_isInit = true;
|
||||
Player.OnFishingSetEquiped += OnFishingSetEquiped_OnRaised;
|
||||
Player.OnFishingSetUnequip += OnFishingSetUnequip;
|
||||
Player.Data.OnStateChange += PlayerFSMState_OnValueChanged;
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Player.OnFishingSetEquiped -= OnFishingSetEquiped_OnRaised;
|
||||
Player.OnFishingSetUnequip -= OnFishingSetUnequip;
|
||||
Player.Data.OnStateChange += PlayerFSMState_OnValueChanged;
|
||||
}
|
||||
|
||||
|
||||
private void OnFishingSetUnequip()
|
||||
{
|
||||
_isRodLayerEnabled = false;
|
||||
// _IK.SetBipedLeftHandIK(enabled: false, null);
|
||||
}
|
||||
|
||||
|
||||
private void OnFishingSetEquiped_OnRaised(FHandItem item)
|
||||
{
|
||||
if (item is FRod rod)
|
||||
{
|
||||
_isRodLayerEnabled = true;
|
||||
// var reel = Player.Rod.Reel;
|
||||
// _IK.SetBipedLeftHandIK(enabled: false, reel.FingersIKAnchor);
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SetLayerWeight(string layer, float weight)
|
||||
{
|
||||
_Animator.SetLayerWeight(_Animator.GetLayerIndex(layer), weight);
|
||||
}
|
||||
|
||||
private void PlayerFSMState_OnValueChanged(PlayerState state)
|
||||
{
|
||||
// switch (Player.Data.PreviousState)
|
||||
// {
|
||||
// case PlayerState.vehicle:
|
||||
// _IsInVehicle = false;
|
||||
// _Animator.SetBool(BoatDriving, value: false);
|
||||
// break;
|
||||
// case PlayerState.swiming:
|
||||
// _Animator.SetBool(IsSwiming, value: false);
|
||||
// break;
|
||||
// case PlayerState.preciseCastIdle:
|
||||
// _Animator.SetBool(PreciseIdle, value: false);
|
||||
// break;
|
||||
// case PlayerState.prepare:
|
||||
// _Animator.SetBool(RodArming, value: false);
|
||||
// break;
|
||||
// case PlayerState.casting:
|
||||
// _Animator.SetBool(ThrowFar, value: false);
|
||||
// break;
|
||||
// case PlayerState.collectFish:
|
||||
// _magicBlending.BlendAsset.globalWeight = 0f;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// switch (state)
|
||||
// {
|
||||
// switch (Player.Data.PreviousState)
|
||||
// {
|
||||
// case PlayerState.vehicle:
|
||||
// _IsInVehicle = false;
|
||||
// _Animator.SetBool(BoatDriving, value: false);
|
||||
// break;
|
||||
// case PlayerState.swiming:
|
||||
// _Animator.SetBool(IsSwiming, value: false);
|
||||
// break;
|
||||
// case PlayerState.preciseCastIdle:
|
||||
// _Animator.SetBool(PreciseIdle, value: false);
|
||||
// break;
|
||||
// case PlayerState.prepare:
|
||||
// _Animator.SetBool(RodArming, value: false);
|
||||
// break;
|
||||
// case PlayerState.casting:
|
||||
// _Animator.SetBool(ThrowFar, value: false);
|
||||
// break;
|
||||
// case PlayerState.collectFish:
|
||||
// _magicBlending.BlendAsset.globalWeight = 0f;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// switch (state)
|
||||
// {
|
||||
// case PlayerState.idle:
|
||||
// case PlayerState.move:
|
||||
// _Animator.SetBool(BaitInWater, value: false);
|
||||
// _Animator.SetBool(HeldRod, value: false);
|
||||
// _Animator.SetBool(ThrowFar, value: false);
|
||||
// _Animator.SetBool(RodArming, value: false);
|
||||
// break;
|
||||
// case PlayerState.prepare:
|
||||
// _Animator.SetBool(RodArming, value: true);
|
||||
// _Animator.SetBool(HeldRod, value: true);
|
||||
// break;
|
||||
// case PlayerState.fishing:
|
||||
// _Animator.SetBool(HeldRod, value: true);
|
||||
// _Animator.SetBool(BaitInWater, value: true);
|
||||
// break;
|
||||
// case PlayerState.vehicle:
|
||||
// _Animator.SetBool(BaitInWater, value: false);
|
||||
// _Animator.SetBool(HeldRod, value: false);
|
||||
// _Animator.SetBool(ThrowFar, value: false);
|
||||
// _Animator.SetBool(RodArming, value: false);
|
||||
// _Animator.SetBool(BoatDriving, value: true);
|
||||
// _IK.SetBipedLeftHandIK(enabled: true);
|
||||
// _IsInVehicle = true;
|
||||
// break;
|
||||
// case PlayerState.vehicleFishing:
|
||||
// _Animator.SetBool(BaitInWater, value: false);
|
||||
// _Animator.SetBool(HeldRod, value: false);
|
||||
// _Animator.SetBool(ThrowFar, value: false);
|
||||
// _Animator.SetBool(RodArming, value: false);
|
||||
// _IsInVehicle = true;
|
||||
// break;
|
||||
// case PlayerState.swiming:
|
||||
// _Animator.SetBool(IsSwiming, value: true);
|
||||
// break;
|
||||
// case PlayerState.collectFish:
|
||||
// _Animator.SetBool(BaitInWater, value: false);
|
||||
// _IK.SetAimIK(enabled: false);
|
||||
// _magicBlending.BlendAsset.globalWeight = 1f;
|
||||
// break;
|
||||
// case PlayerState.preciseCastIdle:
|
||||
// _Animator.SetBool(PreciseIdle, value: true);
|
||||
// break;
|
||||
// case PlayerState.casting:
|
||||
// case PlayerState.baitFlies:
|
||||
// case PlayerState.fight:
|
||||
// case PlayerState.fishView:
|
||||
// case PlayerState.throwFish:
|
||||
// case PlayerState.flyModeDebug:
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// if (Player.Data.State == PlayerState.swiming)
|
||||
// {
|
||||
// float value = Mathf.Lerp(_Animator.GetFloat(Forward), Player.Data.Speed / 2.5f,
|
||||
// Time.deltaTime * 5f);
|
||||
// float value2 = Mathf.Lerp(_Animator.GetFloat(Turn), Player.Data.RotationSpeed, Time.deltaTime * 5f);
|
||||
// _Animator.SetFloat(Forward, Mathf.Clamp01(value));
|
||||
// _Animator.SetFloat(Turn, Mathf.Clamp(value2, -1f, 1f));
|
||||
// }
|
||||
// else
|
||||
{
|
||||
float value3 = Mathf.Lerp(_Animator.GetFloat(Forward), Player.Data.Speed / 5f,
|
||||
Time.deltaTime * 20f);
|
||||
float value4 = Mathf.Lerp(_Animator.GetFloat(Turn), Player.Data.RotationSpeed, Time.deltaTime * 15f);
|
||||
_Animator.SetFloat(Forward, Mathf.Clamp01(value3));
|
||||
_Animator.SetFloat(Turn, Mathf.Clamp(value4, -1f, 1f));
|
||||
}
|
||||
|
||||
// var rod = Vector3.zero;
|
||||
// if (Player.Rod)
|
||||
// {
|
||||
// rod = Player.Rod.transform.position;
|
||||
// }
|
||||
|
||||
_Animator.SetBool(OnGroundHash, _IsInVehicle || Player.Data.IsGrounded);
|
||||
|
||||
|
||||
var isHandRodLayerEnabled = _isRodLayerEnabled && !Player.Data.IsLureRod ? 1 : 0;
|
||||
|
||||
float handRodLayerWeight = _Animator.GetLayerWeight(_Animator.GetLayerIndex(HandRodLayer));
|
||||
SetLayerWeight(HandRodLayer,
|
||||
Mathf.MoveTowards(handRodLayerWeight, isHandRodLayerEnabled, Time.deltaTime * 3f));
|
||||
|
||||
|
||||
var isLureRodLayerEnabled = _isRodLayerEnabled && Player.Data.IsLureRod ? 1 : 0;
|
||||
float lureRodLayerWeight = _Animator.GetLayerWeight(_Animator.GetLayerIndex(LureRodLayer));
|
||||
SetLayerWeight(LureRodLayer,
|
||||
Mathf.MoveTowards(lureRodLayerWeight, isLureRodLayerEnabled, Time.deltaTime * 3f));
|
||||
}
|
||||
|
||||
#region 动画事件
|
||||
|
||||
/// <summary>
|
||||
/// 抬杆到底动画事件
|
||||
/// </summary>
|
||||
public void OnRodPowerUp()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始抛出动画事件
|
||||
/// </summary>
|
||||
public void OnRodThrowStart()
|
||||
{
|
||||
if (Player.Fsm.CurrentState is PlayerStateThrow playerStateThrow)
|
||||
{
|
||||
playerStateThrow.OnRodThrowStart();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抛竿结束动画事件
|
||||
/// </summary>
|
||||
public void OnRodThrownEnd()
|
||||
{
|
||||
if (Player.Fsm.CurrentState is PlayerStateThrow playerStateThrow)
|
||||
{
|
||||
playerStateThrow.OnRodThrownEnd();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
using System;
|
||||
using RootMotion.FinalIK;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class PlayerIK : MonoBehaviour
|
||||
{
|
||||
public enum UpdateType
|
||||
{
|
||||
Update = 0,
|
||||
FixedUpdate = 1,
|
||||
LateUpdate = 2,
|
||||
Default = 3
|
||||
}
|
||||
|
||||
public UpdateType UpdateSelected;
|
||||
|
||||
// [SerializeField] private Transform _LeftHandTransform;
|
||||
|
||||
private LookAtIK _LookAtIK;
|
||||
|
||||
// private AimIK _AimIK;
|
||||
|
||||
// private FullBodyBipedIK _FullBodyIK;
|
||||
|
||||
// private ArmIK _ArmIK;
|
||||
|
||||
// private bool _isLeftHandEnabled;
|
||||
|
||||
// private bool _isRightHandEnabled;
|
||||
|
||||
// public bool isAimEnabled;
|
||||
|
||||
private bool _isFishingLeftArmEnabled;
|
||||
|
||||
[SerializeField] private float transitionWeightTimeScale = 1f;
|
||||
|
||||
// public Transform CurrentTarget => _FullBodyIK.solver.leftHandEffector.target;
|
||||
|
||||
// public Transform LeftHandTransform => _LeftHandTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_LookAtIK = GetComponent<LookAtIK>();
|
||||
// _AimIK = GetComponent<AimIK>();
|
||||
// _FullBodyIK = GetComponent<FullBodyBipedIK>();
|
||||
// _ArmIK = GetComponent<ArmIK>();
|
||||
// SetAimIK(enabled: false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void SetBipedIK(bool enabled)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetFishingLeftArm(bool enabled)
|
||||
{
|
||||
_isFishingLeftArmEnabled = enabled;
|
||||
}
|
||||
|
||||
// public void SetFishingLeftArm(bool enabled, Transform target)
|
||||
// {
|
||||
// _isFishingLeftArmEnabled = enabled;
|
||||
// _ArmIK.solver.arm.target = target;
|
||||
// }
|
||||
|
||||
// public void SetBipedLeftHandIK(bool enabled, bool instant = false)
|
||||
// {
|
||||
// _isLeftHandEnabled = enabled;
|
||||
// if (instant)
|
||||
// {
|
||||
// _FullBodyIK.solver.leftArmMapping.weight = (enabled ? 1f : 0f);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public void SetBipedRightHandIK(bool enabled, bool instant = false)
|
||||
// {
|
||||
// _isRightHandEnabled = enabled;
|
||||
// if (instant)
|
||||
// {
|
||||
// _FullBodyIK.solver.rightArmMapping.weight = (enabled ? 1f : 0f);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public void SetBipedLeftHandIK(bool enabled, Transform target, bool instant = false)
|
||||
// {
|
||||
// _isLeftHandEnabled = enabled;
|
||||
// _FullBodyIK.solver.leftHandEffector.target = target;
|
||||
// if (instant)
|
||||
// {
|
||||
// _FullBodyIK.solver.leftArmMapping.weight = (enabled ? 1f : 0f);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public void SetBipedRightHandIK(bool enabled, Transform target, bool instant = false)
|
||||
// {
|
||||
// _isRightHandEnabled = enabled;
|
||||
// _FullBodyIK.solver.rightHandEffector.target = target;
|
||||
// if (instant)
|
||||
// {
|
||||
// _FullBodyIK.solver.rightArmMapping.weight = (enabled ? 1f : 0f);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public void SetAimIK(bool enabled)
|
||||
// {
|
||||
// isAimEnabled = enabled;
|
||||
// }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (UpdateSelected == UpdateType.Update)
|
||||
{
|
||||
IKUpdateHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (UpdateSelected == UpdateType.FixedUpdate)
|
||||
{
|
||||
IKUpdateHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (UpdateSelected == UpdateType.LateUpdate)
|
||||
{
|
||||
IKUpdateHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private void IKUpdateHandler()
|
||||
{
|
||||
// _AimIK.UpdateSolverExternal();
|
||||
_LookAtIK.UpdateSolverExternal();
|
||||
// _FullBodyIK.UpdateSolverExternal();
|
||||
// _FullBodyIK.solver.Update();
|
||||
// _AimIK.solver.IKPositionWeight = Mathf.MoveTowards(_AimIK.solver.IKPositionWeight, isAimEnabled ? 1f : 0f,
|
||||
// Time.deltaTime * transitionWeightTimeScale);
|
||||
// _FullBodyIK.solver.leftArmMapping.weight = Mathf.MoveTowards(_FullBodyIK.solver.leftArmMapping.weight,
|
||||
// _isLeftHandEnabled ? 1f : 0f, Time.deltaTime * transitionWeightTimeScale);
|
||||
// _FullBodyIK.solver.rightArmMapping.weight = Mathf.MoveTowards(_FullBodyIK.solver.rightArmMapping.weight,
|
||||
// _isRightHandEnabled ? 1f : 0f, Time.deltaTime * transitionWeightTimeScale);
|
||||
// _FullBodyIK.solver.IKPositionWeight = Mathf.MoveTowards(_FullBodyIK.solver.IKPositionWeight,
|
||||
// _isLeftHandEnabled ? 1f : 0f, Time.deltaTime * transitionWeightTimeScale);
|
||||
// _ArmIK.solver.IKPositionWeight = Mathf.MoveTowards(_ArmIK.solver.IKPositionWeight,
|
||||
// _isFishingLeftArmEnabled ? 1f : 0f, Time.deltaTime * transitionWeightTimeScale);
|
||||
// _ArmIK.solver.IKRotationWeight = Mathf.MoveTowards(_ArmIK.solver.IKRotationWeight,
|
||||
// _isFishingLeftArmEnabled ? 1f : 0f, Time.deltaTime * transitionWeightTimeScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3b57223c7f94237869524280afe1672
|
||||
timeCreated: 1768138483
|
||||
@@ -3,9 +3,9 @@ using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public abstract class PlayerStateBase : FsmBaseState<FPlayer>
|
||||
public abstract class PlayerStateBase : FsmBaseState<Player>
|
||||
{
|
||||
protected FPlayer Player => _owner;
|
||||
protected Player Player => _owner;
|
||||
|
||||
/// <summary>
|
||||
/// 检查状态超时
|
||||
@@ -29,25 +29,25 @@ namespace NBF
|
||||
|
||||
if (InputManager.IsOp1)
|
||||
{
|
||||
if (!Player.Data.IsLureRod)
|
||||
{
|
||||
//抬杆
|
||||
isUpRod = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//收线
|
||||
isSubLine = true;
|
||||
}
|
||||
// if (!Player.Data.IsLureRod)
|
||||
// {
|
||||
// //抬杆
|
||||
// isUpRod = true;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// //收线
|
||||
// isSubLine = true;
|
||||
// }
|
||||
}
|
||||
|
||||
if (InputManager.IsOp2)
|
||||
{
|
||||
if (Player.Data.IsLureRod)
|
||||
{
|
||||
//抬杆
|
||||
isUpRod = true;
|
||||
}
|
||||
// if (Player.Data.IsLureRod)
|
||||
// {
|
||||
// //抬杆
|
||||
// isUpRod = true;
|
||||
// }
|
||||
}
|
||||
|
||||
//Player.ModelAsset.PlayerAnimator.FishingUp = 0;
|
||||
@@ -3,16 +3,14 @@ using UnityEngine;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public abstract class FGearBase : MonoBehaviour
|
||||
public abstract class FGearBase : PlayerMonoBehaviour
|
||||
{
|
||||
public FPlayer Player { get; protected set; }
|
||||
public FRod Rod { get; protected set; }
|
||||
public ItemInfo ItemInfo;
|
||||
|
||||
|
||||
public virtual void Init(FPlayer player, FRod rod)
|
||||
public virtual void Init(FRod rod)
|
||||
{
|
||||
Player = player;
|
||||
Rod = rod;
|
||||
OnInit();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class FHandItem : MonoBehaviour
|
||||
public class FHandItem : PlayerMonoBehaviour
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace NBF
|
||||
public class FRod : FHandItem
|
||||
{
|
||||
private float _tension;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 可用的
|
||||
/// </summary>
|
||||
@@ -20,7 +20,6 @@ namespace NBF
|
||||
|
||||
public RodAsset Asset;
|
||||
|
||||
public FPlayer Player { get; protected set; }
|
||||
public ItemInfo ItemInfo;
|
||||
|
||||
public FReel Reel;
|
||||
@@ -61,7 +60,7 @@ namespace NBF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -125,22 +124,26 @@ namespace NBF
|
||||
yield return 1;
|
||||
}
|
||||
|
||||
public IEnumerator InitRod(FPlayer player, ItemInfo itemInfo)
|
||||
public IEnumerator InitRod(ItemInfo itemInfo)
|
||||
{
|
||||
ItemInfo = itemInfo;
|
||||
Player = player;
|
||||
// Player = player;
|
||||
|
||||
var playerView = Player.GetComponent<PlayerViewComponent>();
|
||||
|
||||
var playerViewUnity = playerView.Unity;
|
||||
|
||||
transform.localPosition = Vector3.zero;
|
||||
transform.localRotation = Quaternion.identity;
|
||||
transform.localScale = Vector3.one;
|
||||
SceneSettings.Instance.GearNode.position = Player.transform.position;
|
||||
SceneSettings.Instance.GearNode.position = playerViewUnity.transform.position;
|
||||
yield return 1;
|
||||
var obj = new GameObject($"rod_{itemInfo.Id}_{itemInfo.ConfigId}");
|
||||
obj.transform.SetParent(SceneSettings.Instance.GearNode);
|
||||
// obj.transform.SetParent(player.transform);
|
||||
// obj.transform.localPosition = Vector3.zero;
|
||||
obj.transform.position = player.transform.position;
|
||||
obj.transform.rotation = player.transform.rotation;
|
||||
obj.transform.position = playerViewUnity.transform.position;
|
||||
obj.transform.rotation = playerViewUnity.transform.rotation;
|
||||
obj.transform.localScale = Vector3.one;
|
||||
GearRoot = obj.transform;
|
||||
|
||||
@@ -205,39 +208,39 @@ namespace NBF
|
||||
Reel.transform.SetParent(Asset.ReelConnector);
|
||||
Reel.transform.localPosition = Vector3.zero;
|
||||
Reel.transform.localEulerAngles = Vector3.zero;
|
||||
Reel.Init(player, this);
|
||||
Reel.Init(this);
|
||||
}
|
||||
|
||||
if (Bobber)
|
||||
{
|
||||
Bobber.Init(Player, this);
|
||||
Bobber.Init(this);
|
||||
}
|
||||
|
||||
if (Hook)
|
||||
{
|
||||
Hook.Init(Player, this);
|
||||
Hook.Init(this);
|
||||
}
|
||||
|
||||
if (Bait)
|
||||
{
|
||||
Bait.Init(Player, this);
|
||||
Bait.Init(this);
|
||||
}
|
||||
|
||||
if (Lure)
|
||||
{
|
||||
Lure.Init(Player, this);
|
||||
Lure.Init(this);
|
||||
}
|
||||
|
||||
if (Weight)
|
||||
{
|
||||
Weight.Init(Player, this);
|
||||
Weight.Init(this);
|
||||
}
|
||||
|
||||
yield return 1; //等待1帧
|
||||
|
||||
transform.SetParent(Player.ModelAsset.RodRoot);
|
||||
transform.SetParent(playerViewUnity.ModelAsset.RodRoot);
|
||||
transform.localPosition = Vector3.zero;
|
||||
transform.rotation = Player.ModelAsset.RodRoot.rotation;
|
||||
transform.rotation = playerViewUnity.ModelAsset.RodRoot.rotation;
|
||||
|
||||
Usable = true;
|
||||
}
|
||||
@@ -281,7 +284,7 @@ namespace NBF
|
||||
|
||||
Line = obj.GetComponent<FLine>();
|
||||
Line.transform.position = Asset.lineConnector.position;
|
||||
Line.Init(this.Player, this);
|
||||
Line.Init(this);
|
||||
|
||||
// var obiSolver = solver.GetComponent<ObiSolver>();
|
||||
// obiSolver.parameters.ambientWind = Vector3.zero;
|
||||
|
||||
@@ -32,9 +32,9 @@ namespace NBF
|
||||
{
|
||||
await LoginHelper.Login(InputAccount.text);
|
||||
|
||||
// await Fishing.Instance.Go(RoleModel.Instance.Info.MapId);
|
||||
await Fishing.Instance.Go(RoleModel.Instance.Info.MapId);
|
||||
|
||||
ChatTestPanel.Show();
|
||||
// ChatTestPanel.Show();
|
||||
|
||||
// FishingShopPanel.Show();
|
||||
|
||||
|
||||
512
Packages/cn.tuanjie.codely.bridge/CHANGELOG.md
Normal file
512
Packages/cn.tuanjie.codely.bridge/CHANGELOG.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Codely Bridge will be documented in this file.
|
||||
|
||||
## [1.0.23] - 2026-02-25
|
||||
|
||||
### Changed
|
||||
|
||||
- **UI**
|
||||
- Codely bridge 页面改版
|
||||
|
||||
## [1.0.22] - 2026-02-11
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ExecuteCSharpScript**
|
||||
- Added Unity.InputSystem assembly support for C# script execution
|
||||
- Scripts can now access Input System types and APIs when the package is installed
|
||||
|
||||
## [1.0.21] - 2026-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- **Batch Operations Refactoring**
|
||||
- Split generic `batch` action into two distinct operations: `create_batch` for write-only deterministic sequences and `edit_batch` for search-then-write edits
|
||||
- Added `HandleCreateBatch` method for write-only deterministic batch operations
|
||||
- Added `HandleEditBatch` method for search-then-write batch operations with captureAs support
|
||||
- Improved code clarity and prevented mixed read/write batch states
|
||||
- Maintained parity with TypeScript client schema
|
||||
- Added backward compatibility aliases for snake_case to camelCase parameters
|
||||
- Updated ValidActions list to include new batch operation types
|
||||
- Updated writeActions array to include new batch operations for state validation
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ManageAsset**
|
||||
- Enhanced batch operation handling with clearer separation of concerns
|
||||
- Improved parameter naming consistency with backward compatibility support
|
||||
|
||||
- **ManageGameObject**
|
||||
- Enhanced batch operation handling with clearer separation of concerns
|
||||
- Improved parameter naming consistency with backward compatibility support
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Updated `unity_asset_full_coverage.md` to reflect new batch operations
|
||||
- Updated `unity_gameobject_full_coverage.md` to reflect new batch operations
|
||||
- Updated `unity_workflow_full_coverage.md` with refined batch operation workflows
|
||||
- Updated `Tests/Coverage/README.md` documentation
|
||||
|
||||
|
||||
## [1.0.20] - 2026-02-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TCP Port Management on macOS**
|
||||
- Disabled ReuseAddress socket option on macOS in PortManager.cs
|
||||
|
||||
|
||||
## [1.0.19] - 2026-02-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TCP Port Management on macOS**
|
||||
- Disabled ReuseAddress socket option on macOS to prevent multiple Unity instances from listening on the same port
|
||||
- Ensures proper port exclusivity across Unity Editor instances
|
||||
|
||||
|
||||
## [1.0.18] - 2026-02-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TCP Connection Reliability**
|
||||
- Add LingerState to test listener to send RST on close (same as actual listener)
|
||||
- Increase immediate retry attempts from 3 to 5
|
||||
- Increase retry sleep time from 75ms to 150ms
|
||||
- Extend wait time on Windows from 100ms to 500ms to allow TCP port full release
|
||||
|
||||
|
||||
## [1.0.17] - 2026-02-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Revert IPV6 Loopback Support**
|
||||
|
||||
|
||||
## [1.0.16] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- **C# Script Execution**
|
||||
- New `ExecuteCSharpScript` tool for executing arbitrary C# code at runtime using Microsoft.CodeAnalysis.CSharp.Scripting (Roslyn compiler services)
|
||||
- Captures and returns Unity console logs during script execution
|
||||
- Supports custom using directives and assembly references
|
||||
- Enables dynamic C# code execution without requiring editor restart or recompilation
|
||||
- Added bundled Roslyn assemblies: `Codely.Microsoft.CodeAnalysis.dll`, `Codely.Microsoft.CodeAnalysis.CSharp.dll`, `Codely.Microsoft.CodeAnalysis.Scripting.dll`, `Codely.Microsoft.CodeAnalysis.CSharp.Scripting.dll`
|
||||
- Added supporting assemblies: `Codely.System.Collections.Immutable.dll`, `Codely.System.Reflection.Metadata.dll`, `Codely.System.Runtime.CompilerServices.Unsafe.dll`
|
||||
|
||||
### Fixed
|
||||
|
||||
- **UnityTcpBridge**
|
||||
- Reverted accidental handshake string change that broke package functionality (changed from incorrect 'WELCOME UNITY-TCP 1 FRAMING=1' back to correct 'WELCOME Codely-Bridge 1 FRAMING=1')
|
||||
- **Add IPV6 Loopback Support**
|
||||
|
||||
### Changed
|
||||
|
||||
- **Branding & Menu Structure**
|
||||
- Renamed "Unity TCP" to "Codely Bridge" across logging and port management
|
||||
- Simplified menu structure: removed redundant menu items and kept only "Window/Codely Bridge/Control Window"
|
||||
- Updated menu organization for better user experience
|
||||
|
||||
- **CI/CD**
|
||||
- Updated register_version job rules in CI pipeline
|
||||
|
||||
## [1.0.15] - 2026-01-29
|
||||
|
||||
### Changed
|
||||
|
||||
- **ManageScreenshot**
|
||||
- Simplified screenshot API: removed redundant `capture_game_view` action; its behavior is now covered by the unified `capture` action
|
||||
- `capture` action now consistently uses GameView reflection to capture what the user sees in both edit and play modes
|
||||
- Removed redundant `FlipTextureVertically` call and related comments for clearer, more maintainable code
|
||||
|
||||
- **Package Publishing**
|
||||
- Updated `.npmignore` to include `Tests/` directory in the published npm package so consumers can run and extend the test suite
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Screenshot Capture**
|
||||
- Fixed vertical flip of textures captured from RenderTexture so screenshots match on-screen orientation
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Updated `unity_screenshot_full_coverage.md` and `Tests/Coverage/README.md` to reflect the simplified screenshot API and current test structure
|
||||
|
||||
## [1.0.14] - 2026-01-22
|
||||
|
||||
### Changed
|
||||
|
||||
- **Dependency Management**
|
||||
- Swapped to bundled `Codely.Newtonsoft.Json.dll` in `Plugins/` directory instead of external `Newtonsoft.Json` package
|
||||
- Removed external `Newtonsoft.Json` dependency from `package.json`
|
||||
- Updated all code references to use bundled Newtonsoft.Json assembly
|
||||
|
||||
- **Architecture**
|
||||
- Moved runtime implementation code from `Runtime/` to `Editor/` scope to align with Unity usage patterns
|
||||
|
||||
### Added
|
||||
|
||||
- **Package Publishing**
|
||||
- Added `.npmignore` file to exclude unwanted files from npm publication
|
||||
- Excludes CI/CD configuration, build artifacts, Git metadata, IDE files, and test directories
|
||||
- Ensures only essential code and documentation are published to npm registry
|
||||
|
||||
- **CI/CD Pipeline**
|
||||
- Added automated npm pack and TOS (Tencent Object Storage) upload steps to deployment pipelines
|
||||
- Added backup version (1.0.0) upload for both staging and production environments
|
||||
- Improved deployment reliability with fallback version availability
|
||||
|
||||
## [1.0.13] - 2026-01-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Test Assembly Dependencies**
|
||||
- Fixed missing `Newtonsoft.Json.dll` reference in `UnityTcp.Editor.Tests.asmdef`
|
||||
- Added `com.unity.ext.nunit` package dependency to `package.json` for proper NUnit framework support
|
||||
- Ensures test assembly can properly reference required dependencies for compilation
|
||||
|
||||
## [1.0.12] - 2026-01-19
|
||||
|
||||
### Added
|
||||
|
||||
- **ManageGameObject**
|
||||
- Added `list_children` action for listing GameObject children with configurable depth
|
||||
- Support for three result modes: `auto` (default), `inline`, and `file`
|
||||
- Automatic fallback to file output when hierarchy exceeds `maxInlineItems` threshold (default: 200)
|
||||
- Depth-limited traversal with `depth` parameter (default: 1 for direct children)
|
||||
- `includeInactive` parameter to control whether inactive GameObjects are included
|
||||
- Iterative tree building to avoid stack overflow on deep hierarchies
|
||||
- JSON streaming to file for large results to prevent memory issues
|
||||
- New helper methods: `CountDescendantsUpToDepth`, `BuildChildrenTreeIterative`, `WriteChildrenTreeIterative`
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ManageScene**
|
||||
- Improved large scene hierarchy handling (>500 GameObjects)
|
||||
- Returns shallow root-only tree with hints instead of error when scene is too large
|
||||
- Changed `CountGameObjectsRecursive` to use iterative traversal (stack-based) to avoid stack overflow on deep hierarchies
|
||||
- Better user guidance for drilling down into large hierarchies incrementally
|
||||
|
||||
- **Coverage Tools**
|
||||
- Added `CodelyUnityCoverageTools` class for E2E test utilities
|
||||
- New `codely.generate_large_hierarchy` custom tool for quickly generating test hierarchies
|
||||
- Configurable generation parameters: root name, child/grandchild prefixes, and counts
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Added `unity_large_hierarchy_e2e_coverage.md` with 152 lines of E2E test scenarios for large hierarchy handling
|
||||
- Updated `unity_gameobject_full_coverage.md` with `list_children` action coverage
|
||||
- Updated `unity_scene_full_coverage.md` with improved large scene handling coverage
|
||||
|
||||
## [1.0.11] - 2026-01-13
|
||||
|
||||
### Added
|
||||
|
||||
- **Tuanjie Editor Scene File Support**
|
||||
- Added support for `.scene` file extension used by Tuanjie Editor
|
||||
- Implemented extension-aware scene path handling in `ManageScene`
|
||||
- Support both `.unity` (Unity Editor) and `.scene` (Tuanjie Editor) extensions based on editor type
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **UnityStateDirtyHook**
|
||||
- Added `.scene` extension detection for scene file change tracking
|
||||
- Ensures proper state tracking for both Unity and Tuanjie editor scene files
|
||||
|
||||
- **Documentation**
|
||||
- Improved documentation for scene file extension handling
|
||||
|
||||
## [1.0.10] - 2026-01-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- **ManageGameObject**
|
||||
- Updated default behavior for `searchInactive` parameter from `false` to `true`
|
||||
- Ensures inactive GameObjects are included in search results unless explicitly specified otherwise
|
||||
|
||||
- **UnityEngineObjectConverter**
|
||||
- Added support for `{"find":"...", "method":"..."}` reference format in object deserialization
|
||||
- Fixed deserialization errors when encountering find instruction format used by MCP tools for dynamic GameObject lookups
|
||||
- Implemented delegate pattern to call `ManageGameObject.FindObjectByInstruction` from Runtime assembly without direct Editor assembly reference
|
||||
|
||||
## [1.0.9] - 2026-01-05
|
||||
|
||||
### Changed
|
||||
|
||||
- **Unity Project Metadata**
|
||||
- Updated Unity project metadata GUIDs across all .meta files
|
||||
- Refreshed GUIDs in Editor, Runtime, and Tests directories
|
||||
|
||||
## [1.0.8] - 2025-12-16
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **Compilation Tracking**
|
||||
- Improved compilation error/warning count tracking with nullable integers
|
||||
- Changed `CompilationHelper.GetCompilationErrors()` and `GetCompilationWarnings()` to return `int?` instead of `int`
|
||||
- Updated `GetCompilationSummary()` to only include known values in result (removes misleading default 0 values)
|
||||
- Enhanced compilation result handling in `ManageEditor` to properly process nullable values
|
||||
- Improved type handling for compilation result payloads (supports both dictionary and anonymous object formats)
|
||||
- Added clarifying comments explaining why returning 0 for unknown counts is problematic
|
||||
- Better distinction between "0 errors/warnings" (validated) vs "unknown count" (not yet validated)
|
||||
|
||||
## [1.0.7] - 2025-12-15
|
||||
|
||||
### Changed
|
||||
|
||||
- **Package Renaming**
|
||||
- Renamed package from `com.unity.codely` to `cn.tuanjie.codely.bridge`
|
||||
- Updated all internal references and documentation to reflect new package name
|
||||
|
||||
- **Branding Update**
|
||||
- Renamed all menu item paths and labels in the Unity Editor
|
||||
|
||||
## [1.0.6] - 2025-12-10
|
||||
|
||||
### Added
|
||||
|
||||
- **Compilation Pipeline**
|
||||
- New `pipeline_kind: "compile"` field for structured hints to downstream tools
|
||||
- `requires_console_validation: true` flag to guide compilation validation
|
||||
- Comprehensive integration test documentation for compilation pipeline policy
|
||||
- Enhanced play mode state synchronization with `playMode` field in responses
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **StateComposer**
|
||||
- Simplified state reporting to focus on essential information (compiling vs idle)
|
||||
- Minimized state complexity with clear documentation directing users to specific diagnostic tools
|
||||
|
||||
- **ManageAsset**
|
||||
- Improved `AssetExists` method with ghost asset detection
|
||||
- New `BuildAssetNotFoundResponse` for better error messaging about desync issues
|
||||
- Enhanced asset validation and error handling
|
||||
|
||||
- **Test Coverage**
|
||||
- Added `unity_compile_pipeline_integration.md` with 152 lines of comprehensive test scenarios
|
||||
- Updated `unity_editor_full_coverage.md` with compilation pipeline requirements
|
||||
|
||||
## [1.0.5] - 2025-12-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Test Coverage**
|
||||
- Added comprehensive test coverage for `ExecuteCustomTool` functionality
|
||||
- UI Toolkit tools test coverage documentation and validation
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ManageUIToolkit**
|
||||
- Enhanced `link_uss_to_uxml` action with GUID support
|
||||
- Added `ResolveAssetPath` helper method for flexible path/GUID resolution
|
||||
- Improved parameter validation for mixed path/GUID usage
|
||||
|
||||
- **ManageShader**
|
||||
- Enhanced `ensure_material_shader_for_srp` action with `material_guid` parameter support
|
||||
- Improved parameter handling for material identification via path or GUID
|
||||
- Better error messages for missing material parameters
|
||||
|
||||
- **CodelyUnityValidationTools**
|
||||
- Added nested field path support in `validate_response`
|
||||
- Support for dot-notation field paths (e.g., "state.project.srp", "project.srp")
|
||||
- Automatic recursive field search when direct path fails
|
||||
- Enhanced field validation with path tracking and debugging
|
||||
|
||||
- **ManageBake**
|
||||
- Refactored NavMesh operations to use runtime reflection
|
||||
- Improved AI Navigation package detection and type resolution
|
||||
- Better compatibility with optional AI Navigation package installation
|
||||
- Enhanced error handling for missing package scenarios
|
||||
|
||||
## [1.0.4] - 2025-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- **Validation Tools Framework**
|
||||
- New `CodelyUnityValidationTools`: 15+ validation helpers for automated testing
|
||||
- `codely.validate_play_mode`: Validate current editor PlayMode state
|
||||
- `codely.validate_active_tool`: Validate current active editor tool
|
||||
- `codely.validate_not_compiling`: Ensure editor is not compiling
|
||||
- `codely.validate_tag_and_layer_exist`: Verify Tag/Layer existence
|
||||
- `codely.validate_window_open`: Check editor window state
|
||||
- `codely.validate_console_contains`: Validate console messages with filter
|
||||
- `codely.validate_console_count`: Verify console message counts
|
||||
- `codely.validate_active_scene`: Validate active scene properties
|
||||
- `codely.validate_scene_dirty`: Check scene dirty state
|
||||
- `codely.validate_hierarchy_root_count`: Verify hierarchy root object count
|
||||
- `codely.validate_gameobject_exists`: Check GameObject existence
|
||||
- `codely.validate_response`: Generic response validation
|
||||
|
||||
- **Compilation Pipeline**
|
||||
- `CompilationHelper`: New helper class for compilation status checking and error tracking
|
||||
- `start_compilation_pipeline` action in ManageEditor for standardized compile workflow
|
||||
- Block compilation during play mode to prevent editor errors
|
||||
|
||||
- **Test Coverage Documentation**
|
||||
- Complete test coverage specs for all Unity tools
|
||||
- `unity_editor_full_coverage.md`: 24 actions coverage
|
||||
- `unity_console_full_coverage.md`: Console operations coverage
|
||||
- `unity_scene_full_coverage.md`: Scene management coverage
|
||||
- `unity_gameobject_full_coverage.md`: GameObject operations coverage
|
||||
- `unity_asset_full_coverage.md`: Asset management coverage
|
||||
- `unity_script_full_coverage.md`: Script management coverage
|
||||
- `unity_shader_full_coverage.md`: Shader operations coverage
|
||||
- `unity_package_full_coverage.md`: Package manager coverage
|
||||
- `unity_menu_full_coverage.md`: Menu execution coverage
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **State Management**
|
||||
- State delta tracking added to async operation responses
|
||||
- Client state revision validation for all write operations
|
||||
- Enhanced console state tracking with `since_token` filtering
|
||||
|
||||
- **ManageEditor**
|
||||
- Idempotent `ensure_tag` and `ensure_layer` operations
|
||||
- Extended with compilation pipeline integration
|
||||
|
||||
- **ManageAsset**
|
||||
- Enhanced robustness with better error handling
|
||||
|
||||
- **ManageGameObject**
|
||||
- Improved serialization with `GameObjectSerializer` enhancements
|
||||
|
||||
- **ReadConsole**
|
||||
- Enhanced filtering with `since_token` support for incremental reads
|
||||
|
||||
- **ExecuteCustomTool**
|
||||
- Improved tool registry with better parameter validation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved Unity version compatibility across various tools
|
||||
|
||||
## [1.0.3] - 2025-12-01
|
||||
|
||||
### Added
|
||||
|
||||
- **State Management System**
|
||||
- `AsyncOperationTracker`: Comprehensive async operation management with progress tracking and cancellation support
|
||||
- `StateComposer`: Full Unity state composition including scene, project, packages, and shaders
|
||||
- `UnityStateDirtyHook`: Automatic tracking of Unity Editor state changes (hierarchy, project, selection, console)
|
||||
- `WriteGuard`: Thread-safe write operation protection with main thread enforcement
|
||||
- New `get_current_state` endpoint for retrieving complete Unity state snapshots
|
||||
|
||||
- **New Unity Tools**
|
||||
- `ManageBake`: Light baking controls (start, cancel, clear, status queries)
|
||||
- `ManagePackage`: Package manager operations with version pinning support (package@version syntax)
|
||||
- `ManageUIToolkit`: UI Toolkit template instantiation with automatic USS/C# generation
|
||||
|
||||
- **Custom Tool Execution Framework**
|
||||
- `ExecuteCustomTool`: Reflection-based tool discovery and execution via `[CustomTool]` attribute
|
||||
- Automatic tool registry with parameter validation and error handling
|
||||
- Support for custom tools without modifying CommandRegistry
|
||||
|
||||
- **Enhanced Existing Tools**
|
||||
- `ManageEditor`: Extended with state-aware operations and full state retrieval
|
||||
- `ManageGameObject`: Added find, query, parent/child operations, and component management
|
||||
- `ManageAsset`: New asset import, export, and metadata operations
|
||||
- `ManageScene`: Enhanced with scene creation and multi-scene management
|
||||
- `ManageShader`: Expanded with shader compilation, variant queries, and global property management
|
||||
- `ReadConsole`: Added scope-based console clearing and entry filtering
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **Response Helpers**: New state-aware methods (`SuccessWithDelta`, `SuccessWithState`, `Conflict`) for better change tracking
|
||||
- **CompilationHelper**: Improved compilation workflow handling with better async integration
|
||||
- **Test Coverage**: Added unit tests for `AsyncOperationTracker`, `StateComposer`, and `WriteGuard`
|
||||
|
||||
## [1.0.2] - 2025-11-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Unity Version Compatibility**: Added conditional compilation in `ManageGameObject.cs`
|
||||
- Uses `FindObjectsByType` with `FindObjectsInactive` enum for Unity 2022.2+
|
||||
- Falls back to `FindObjectsOfType` for Unity 2021.3 and earlier
|
||||
- Resolves CS0246 error: `FindObjectsInactive` type not found on Unity 2021
|
||||
- Maintains backward compatibility across Unity versions
|
||||
|
||||
## [1.0.1] - 2025-11-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛 **Fixed build compilation error**
|
||||
- Corrected assembly definition configuration for `UnityTcp.Editor.asmdef`
|
||||
- Changed from platform exclusion list to explicit Editor platform inclusion
|
||||
- Ensures Editor assembly only compiles in Unity Editor, not in game builds
|
||||
- Resolves compilation errors during game packaging for all platforms
|
||||
|
||||
## [1.0.0] - 2024-12-19
|
||||
|
||||
### Major Refactoring
|
||||
|
||||
- 🔄 **Complete removal of MCP (Model Context Protocol) logic**
|
||||
- Removed all MCP-specific components, tools, and protocol handling
|
||||
- Eliminated MCP server integration and HTTP server components
|
||||
- Removed MCP client models, configuration systems, and UI windows
|
||||
|
||||
### New TCP-Focused Architecture
|
||||
|
||||
- 🚀 **Pure TCP Socket Implementation**
|
||||
- New `UnityTcpBridge` class for TCP server management
|
||||
- Basic echo server implementation as starting point
|
||||
- Async/await patterns for non-blocking operations
|
||||
- Multi-client connection support with proper resource management
|
||||
|
||||
### Core TCP Features
|
||||
|
||||
- **Port Management**
|
||||
- Automatic port discovery and allocation
|
||||
- Project-specific port persistence
|
||||
- Smart port conflict resolution
|
||||
- Cross-platform compatibility
|
||||
|
||||
- **Connection Handling**
|
||||
- TCP listener with automatic client acceptance
|
||||
- Configurable socket options (keep-alive, timeouts)
|
||||
- Graceful connection cleanup on shutdown
|
||||
- Unity lifecycle integration (assembly reload, editor quit)
|
||||
|
||||
### Updated Components
|
||||
|
||||
- **Renamed Assemblies**: `UnityTcp.*` → `UnityTcp.*`
|
||||
- **Updated Namespaces**: All classes moved to `UnityTcp.Editor.*` namespace
|
||||
- **Simplified Helpers**: Kept only TCP-relevant utilities (PortManager, TcpLog)
|
||||
- **Package Rebranding**: Updated from "Unity MCP" to "Unity TCP Bridge"
|
||||
|
||||
### Removed Components
|
||||
|
||||
- All MCP protocol handling and message processing
|
||||
- MCP tool implementations (ManageScript, ManageAsset, etc.)
|
||||
- MCP UI windows and editor integrations
|
||||
- HTTP server and MCP server management
|
||||
- Telemetry and MCP-specific logging
|
||||
- Configuration builders and MCP client models
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Architecture**: Direct TCP socket server with customizable protocol handling
|
||||
- **Performance**: Lightweight implementation focused on TCP networking
|
||||
- **Compatibility**: Unity 2021.3+ with Newtonsoft.Json dependency
|
||||
- **Protocol**: Basic TCP with welcome handshake (easily customizable)
|
||||
|
||||
### Migration Guide
|
||||
|
||||
This is a breaking change that removes all MCP functionality:
|
||||
|
||||
1. **Previous MCP Users**: This package no longer provides MCP integration
|
||||
2. **TCP Socket Users**: Replace any `UnityTcpBridge` references with `UnityTcpBridge`
|
||||
3. **Custom Protocols**: Implement your protocol logic in `HandleClientAsync` method
|
||||
4. **Port Management**: Use `PortManager` for dynamic port allocation needs
|
||||
|
||||
### Development Notes
|
||||
|
||||
- Codebase reduced by ~80% by removing MCP complexity
|
||||
- Focus shifted to providing a clean TCP socket foundation
|
||||
- Easy to extend for custom networking protocols
|
||||
- Maintains Unity Editor integration for automatic lifecycle management
|
||||
|
||||
## Previous Versions
|
||||
|
||||
Previous versions (1.x.x) included MCP (Model Context Protocol) integration which has been completely removed in this version.
|
||||
7
Packages/cn.tuanjie.codely.bridge/CHANGELOG.md.meta
Normal file
7
Packages/cn.tuanjie.codely.bridge/CHANGELOG.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cb996b11fa557143a68a6fdb4f0cb76
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/cn.tuanjie.codely.bridge/Editor.meta
Normal file
8
Packages/cn.tuanjie.codely.bridge/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff37f8f7a26ddcf4a8ec576dd133285c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Packages/cn.tuanjie.codely.bridge/Editor/AssemblyInfo.cs
Normal file
3
Packages/cn.tuanjie.codely.bridge/Editor/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("UnityTcpTests.EditMode")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7083f648bddd50d41afc42cc3deb2577
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/cn.tuanjie.codely.bridge/Editor/Helpers.meta
Normal file
8
Packages/cn.tuanjie.codely.bridge/Editor/Helpers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 626ae8824a7df62489152ef051a0e2a9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages long-running asynchronous operation states (compilation, UPM, baking, etc.)
|
||||
/// Provides operation ID generation, status tracking, and timeout management.
|
||||
/// </summary>
|
||||
public static class AsyncOperationTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Job status enum matching MCP protocol.
|
||||
/// </summary>
|
||||
public enum JobStatus
|
||||
{
|
||||
Pending,
|
||||
Complete,
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Job type enum for categorizing operations.
|
||||
/// </summary>
|
||||
public enum JobType
|
||||
{
|
||||
Compilation,
|
||||
UpmPackage,
|
||||
NavMeshBake,
|
||||
LightingBake,
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tracked job/operation.
|
||||
/// </summary>
|
||||
public class Job
|
||||
{
|
||||
public string OpId { get; set; }
|
||||
public JobType Type { get; set; }
|
||||
public JobStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
public string Message { get; set; }
|
||||
public object Data { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public float Progress { get; set; } // 0.0 to 1.0
|
||||
}
|
||||
|
||||
// Job storage
|
||||
private static readonly Dictionary<string, Job> _jobs = new Dictionary<string, Job>();
|
||||
private static readonly object _jobsLock = new object();
|
||||
|
||||
// Default timeout in seconds
|
||||
private const int DefaultTimeoutSeconds = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new job with a unique op_id and registers it.
|
||||
/// </summary>
|
||||
public static Job CreateJob(JobType type, string message = null)
|
||||
{
|
||||
var job = new Job
|
||||
{
|
||||
OpId = GenerateOpId(),
|
||||
Type = type,
|
||||
Status = JobStatus.Pending,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Message = message ?? $"{type} operation started",
|
||||
Progress = 0.0f
|
||||
};
|
||||
|
||||
lock (_jobsLock)
|
||||
{
|
||||
_jobs[job.OpId] = job;
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a job by op_id.
|
||||
/// </summary>
|
||||
public static Job GetJob(string opId)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
return _jobs.TryGetValue(opId, out var job) ? job : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates job status to Complete.
|
||||
/// </summary>
|
||||
public static void CompleteJob(string opId, string message = null, object data = null)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
job.Status = JobStatus.Complete;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
job.Progress = 1.0f;
|
||||
if (message != null) job.Message = message;
|
||||
if (data != null) job.Data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates job status to Error.
|
||||
/// </summary>
|
||||
public static void FailJob(string opId, string errorMessage)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
job.Status = JobStatus.Error;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
job.ErrorMessage = errorMessage;
|
||||
job.Message = $"Operation failed: {errorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates job progress (0.0 to 1.0).
|
||||
/// </summary>
|
||||
public static void UpdateProgress(string opId, float progress, string message = null)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
job.Progress = Mathf.Clamp01(progress);
|
||||
if (message != null) job.Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a job from tracking.
|
||||
/// </summary>
|
||||
public static void RemoveJob(string opId)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
_jobs.Remove(opId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all pending jobs of a specific type.
|
||||
/// </summary>
|
||||
public static List<Job> GetPendingJobs(JobType? type = null)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
return _jobs.Values
|
||||
.Where(j => j.Status == JobStatus.Pending && (!type.HasValue || j.Type == type.Value))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up old jobs that have been completed or timed out.
|
||||
/// Should be called periodically.
|
||||
/// </summary>
|
||||
public static void CleanupOldJobs(int maxAgeSeconds = 3600)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddSeconds(-maxAgeSeconds);
|
||||
|
||||
lock (_jobsLock)
|
||||
{
|
||||
var toRemove = _jobs
|
||||
.Where(kv => kv.Value.CompletedAt.HasValue && kv.Value.CompletedAt.Value < cutoff)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var opId in toRemove)
|
||||
{
|
||||
_jobs.Remove(opId);
|
||||
}
|
||||
|
||||
if (toRemove.Count > 0)
|
||||
{
|
||||
Debug.Log($"[AsyncOperationTracker] Cleaned up {toRemove.Count} old jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a job has timed out.
|
||||
/// </summary>
|
||||
public static bool IsJobTimedOut(string opId, int timeoutSeconds = DefaultTimeoutSeconds)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
if (job.Status == JobStatus.Pending)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - job.CreatedAt).TotalSeconds;
|
||||
return elapsed > timeoutSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Pending async operation response for a job.
|
||||
/// Protocol: status/poll_interval/op_id
|
||||
/// </summary>
|
||||
public static object CreatePendingResponse(Job job, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
["status"] = "pending",
|
||||
["poll_interval"] = 1.0, // Poll every 1 second
|
||||
["op_id"] = job.OpId,
|
||||
["success"] = true,
|
||||
["message"] = job.Message,
|
||||
["data"] = new
|
||||
{
|
||||
type = job.Type.ToString(),
|
||||
progress = job.Progress,
|
||||
createdAt = job.CreatedAt.ToString("o")
|
||||
}
|
||||
};
|
||||
|
||||
// Add operations state delta showing the new pending operation
|
||||
var opDelta = StateComposer.CreateOperationsDelta(new[] {
|
||||
new { id = job.OpId, type = job.Type.ToString(), progress = job.Progress, message = job.Message }
|
||||
});
|
||||
response["state_delta"] = stateDelta != null
|
||||
? StateComposer.MergeStateDeltas(opDelta, stateDelta)
|
||||
: opDelta;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Complete async operation response for a job.
|
||||
/// Protocol: status/op_id
|
||||
/// </summary>
|
||||
public static object CreateCompleteResponse(Job job, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
["status"] = "complete",
|
||||
["op_id"] = job.OpId,
|
||||
["success"] = true,
|
||||
["message"] = job.Message,
|
||||
["data"] = job.Data
|
||||
};
|
||||
|
||||
// Include state_delta if provided
|
||||
if (stateDelta != null)
|
||||
{
|
||||
response["state_delta"] = stateDelta;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Error async operation response for a job.
|
||||
/// Protocol: status/op_id
|
||||
/// </summary>
|
||||
public static object CreateErrorResponse(Job job, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
["status"] = "error",
|
||||
["op_id"] = job.OpId,
|
||||
["success"] = false,
|
||||
["message"] = job.Message,
|
||||
["error"] = job.ErrorMessage
|
||||
};
|
||||
|
||||
// Include state_delta if provided
|
||||
if (stateDelta != null)
|
||||
{
|
||||
response["state_delta"] = stateDelta;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique operation ID.
|
||||
/// </summary>
|
||||
private static string GenerateOpId()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets count of all jobs by status.
|
||||
/// </summary>
|
||||
public static Dictionary<JobStatus, int> GetJobCounts()
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
return _jobs.Values
|
||||
.GroupBy(j => j.Status)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccdab4cb337ac174395ef110bfa8f3b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for handling binary framed TCP communication
|
||||
/// </summary>
|
||||
public static class BinaryFrameHelper
|
||||
{
|
||||
private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads
|
||||
private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients
|
||||
|
||||
/// <summary>
|
||||
/// Timeout-aware exact read helper with cancellation; avoids indefinite stalls and background task leaks
|
||||
/// </summary>
|
||||
public static async Task<byte[]> ReadExactAsync(NetworkStream stream, int count, int timeoutMs, CancellationToken cancel = default)
|
||||
{
|
||||
byte[] buffer = new byte[count];
|
||||
int offset = 0;
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
while (offset < count)
|
||||
{
|
||||
int remaining = count - offset;
|
||||
int remainingTimeout = timeoutMs <= 0
|
||||
? Timeout.Infinite
|
||||
: timeoutMs - (int)stopwatch.ElapsedMilliseconds;
|
||||
|
||||
// If a finite timeout is configured and already elapsed, fail immediately
|
||||
if (remainingTimeout != Timeout.Infinite && remainingTimeout <= 0)
|
||||
{
|
||||
throw new System.IO.IOException("Read timed out");
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
|
||||
if (remainingTimeout != Timeout.Infinite)
|
||||
{
|
||||
cts.CancelAfter(remainingTimeout);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if NETSTANDARD2_1 || NET6_0_OR_GREATER
|
||||
int read = await stream.ReadAsync(buffer.AsMemory(offset, remaining), cts.Token).ConfigureAwait(false);
|
||||
#else
|
||||
int read = await stream.ReadAsync(buffer, offset, remaining, cts.Token).ConfigureAwait(false);
|
||||
#endif
|
||||
if (read == 0)
|
||||
{
|
||||
throw new System.IO.IOException("Connection closed before reading expected bytes");
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancel.IsCancellationRequested)
|
||||
{
|
||||
throw new System.IO.IOException("Read timed out");
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a framed payload to the stream with default timeout
|
||||
/// </summary>
|
||||
public static async Task WriteFrameAsync(NetworkStream stream, byte[] payload)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(FrameIOTimeoutMs);
|
||||
await WriteFrameAsync(stream, payload, cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a framed payload to the stream with cancellation support
|
||||
/// </summary>
|
||||
public static async Task WriteFrameAsync(NetworkStream stream, byte[] payload, CancellationToken cancel)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(payload));
|
||||
}
|
||||
if ((ulong)payload.LongLength > MaxFrameBytes)
|
||||
{
|
||||
throw new System.IO.IOException($"Frame too large: {payload.LongLength}");
|
||||
}
|
||||
byte[] header = new byte[8];
|
||||
WriteUInt64BigEndian(header, (ulong)payload.LongLength);
|
||||
#if NETSTANDARD2_1 || NET6_0_OR_GREATER
|
||||
await stream.WriteAsync(header.AsMemory(0, header.Length), cancel).ConfigureAwait(false);
|
||||
await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancel).ConfigureAwait(false);
|
||||
#else
|
||||
await stream.WriteAsync(header, 0, header.Length, cancel).ConfigureAwait(false);
|
||||
await stream.WriteAsync(payload, 0, payload.Length, cancel).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a framed UTF-8 string from the stream
|
||||
/// </summary>
|
||||
public static async Task<string> ReadFrameAsUtf8Async(NetworkStream stream, int timeoutMs, CancellationToken cancel)
|
||||
{
|
||||
byte[] header = await ReadExactAsync(stream, 8, timeoutMs, cancel).ConfigureAwait(false);
|
||||
ulong payloadLen = ReadUInt64BigEndian(header);
|
||||
if (payloadLen > MaxFrameBytes)
|
||||
{
|
||||
throw new System.IO.IOException($"Invalid framed length: {payloadLen}");
|
||||
}
|
||||
if (payloadLen == 0UL)
|
||||
throw new System.IO.IOException("Zero-length frames are not allowed");
|
||||
if (payloadLen > int.MaxValue)
|
||||
{
|
||||
throw new System.IO.IOException("Frame too large for buffer");
|
||||
}
|
||||
int count = (int)payloadLen;
|
||||
byte[] payload = await ReadExactAsync(stream, count, timeoutMs, cancel).ConfigureAwait(false);
|
||||
return System.Text.Encoding.UTF8.GetString(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64 from a byte array in big-endian format
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64BigEndian(byte[] buffer)
|
||||
{
|
||||
if (buffer == null || buffer.Length < 8) return 0UL;
|
||||
return ((ulong)buffer[0] << 56)
|
||||
| ((ulong)buffer[1] << 48)
|
||||
| ((ulong)buffer[2] << 40)
|
||||
| ((ulong)buffer[3] << 32)
|
||||
| ((ulong)buffer[4] << 24)
|
||||
| ((ulong)buffer[5] << 16)
|
||||
| ((ulong)buffer[6] << 8)
|
||||
| buffer[7];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a UInt64 to a byte array in big-endian format
|
||||
/// </summary>
|
||||
public static void WriteUInt64BigEndian(byte[] dest, ulong value)
|
||||
{
|
||||
if (dest == null || dest.Length < 8)
|
||||
{
|
||||
throw new System.ArgumentException("Destination buffer too small for UInt64");
|
||||
}
|
||||
dest[0] = (byte)(value >> 56);
|
||||
dest[1] = (byte)(value >> 48);
|
||||
dest[2] = (byte)(value >> 40);
|
||||
dest[3] = (byte)(value >> 32);
|
||||
dest[4] = (byte)(value >> 24);
|
||||
dest[5] = (byte)(value >> 16);
|
||||
dest[6] = (byte)(value >> 8);
|
||||
dest[7] = (byte)(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9addeba67f678854eada157f672b975d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,232 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for Unity compilation status checking and error tracking
|
||||
/// </summary>
|
||||
public static class CompilationHelper
|
||||
{
|
||||
// Track last known compilation error/warning counts
|
||||
// IMPORTANT: Keep these nullable. Returning 0 when counts are unknown is misleading
|
||||
// (it can be interpreted as "validated: no errors/warnings").
|
||||
private static int? _lastErrorCount = null;
|
||||
private static int? _lastWarningCount = null;
|
||||
private static bool _trackingInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Helper to check compilation status across Unity versions
|
||||
/// </summary>
|
||||
public static bool IsCompiling()
|
||||
{
|
||||
if (EditorApplication.isCompiling)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
try
|
||||
{
|
||||
System.Type pipeline = System.Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
|
||||
var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (prop != null)
|
||||
{
|
||||
return (bool)prop.GetValue(null);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of compilation errors from the console.
|
||||
/// This is an approximation based on console log entries.
|
||||
/// </summary>
|
||||
public static int? GetCompilationErrors()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to get error count from LogEntries (internal API)
|
||||
var logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
|
||||
if (logEntriesType != null)
|
||||
{
|
||||
var getCountMethod = logEntriesType.GetMethod(
|
||||
"GetCount",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
||||
);
|
||||
|
||||
// Get count with error filter (mode = 1 for errors)
|
||||
var getCountByTypeMethod = logEntriesType.GetMethod(
|
||||
"GetCountsByType",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
||||
);
|
||||
|
||||
if (getCountByTypeMethod != null)
|
||||
{
|
||||
// GetCountsByType returns counts for errors, warnings, logs
|
||||
var counts = new int[3];
|
||||
getCountByTypeMethod.Invoke(null, new object[] { counts });
|
||||
_lastErrorCount = counts[0]; // Errors
|
||||
return _lastErrorCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[CompilationHelper] Failed to get error count: {e.Message}");
|
||||
}
|
||||
|
||||
return _lastErrorCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of compilation warnings from the console.
|
||||
/// This is an approximation based on console log entries.
|
||||
/// </summary>
|
||||
public static int? GetCompilationWarnings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to get warning count from LogEntries (internal API)
|
||||
var logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
|
||||
if (logEntriesType != null)
|
||||
{
|
||||
var getCountByTypeMethod = logEntriesType.GetMethod(
|
||||
"GetCountsByType",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
||||
);
|
||||
|
||||
if (getCountByTypeMethod != null)
|
||||
{
|
||||
// GetCountsByType returns counts for errors, warnings, logs
|
||||
var counts = new int[3];
|
||||
getCountByTypeMethod.Invoke(null, new object[] { counts });
|
||||
_lastWarningCount = counts[1]; // Warnings
|
||||
return _lastWarningCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[CompilationHelper] Failed to get warning count: {e.Message}");
|
||||
}
|
||||
|
||||
return _lastWarningCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets tracked error/warning counts.
|
||||
/// Should be called before starting a new compilation.
|
||||
/// </summary>
|
||||
public static void ResetCounts()
|
||||
{
|
||||
_lastErrorCount = null;
|
||||
_lastWarningCount = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a standard compilation pipeline:
|
||||
/// 1. Clears console and gets since_token
|
||||
/// 2. Requests compilation
|
||||
/// 3. Returns pending response with token for later log reading
|
||||
///
|
||||
/// This is the recommended pattern after any script modification.
|
||||
/// </summary>
|
||||
public static object StartCompilationPipeline()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Step 1: Clear console and get since_token
|
||||
var clearMethod = typeof(UnityTcp.Editor.Tools.ReadConsole).GetMethod(
|
||||
"HandleCommand",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public
|
||||
);
|
||||
|
||||
string sinceToken = null;
|
||||
if (clearMethod != null)
|
||||
{
|
||||
var clearParams = new Codely.Newtonsoft.Json.Linq.JObject
|
||||
{
|
||||
["action"] = "clear"
|
||||
};
|
||||
var clearResult = clearMethod.Invoke(null, new object[] { clearParams });
|
||||
|
||||
// Extract since_token from result
|
||||
if (clearResult != null)
|
||||
{
|
||||
var resultType = clearResult.GetType();
|
||||
var dataProp = resultType.GetProperty("data");
|
||||
if (dataProp != null)
|
||||
{
|
||||
var data = dataProp.GetValue(clearResult);
|
||||
if (data != null)
|
||||
{
|
||||
var tokenProp = data.GetType().GetProperty("sinceToken");
|
||||
sinceToken = tokenProp?.GetValue(data)?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: get token from StateComposer
|
||||
if (string.IsNullOrEmpty(sinceToken))
|
||||
{
|
||||
sinceToken = StateComposer.GetCurrentConsoleToken();
|
||||
}
|
||||
|
||||
// Step 2: Reset error counts
|
||||
ResetCounts();
|
||||
|
||||
// Step 3: Create compilation job
|
||||
var job = AsyncOperationTracker.CreateJob(
|
||||
AsyncOperationTracker.JobType.Compilation,
|
||||
"Script compilation pipeline started"
|
||||
);
|
||||
|
||||
// Step 4: Request compilation
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
|
||||
|
||||
// Step 5: Return pending response with token and structured pipeline hints
|
||||
var response = AsyncOperationTracker.CreatePendingResponse(job) as System.Collections.Generic.Dictionary<string, object>;
|
||||
if (response != null)
|
||||
{
|
||||
response["since_token"] = sinceToken;
|
||||
response["pipeline"] = new
|
||||
{
|
||||
step = "compiling",
|
||||
sinceToken = sinceToken
|
||||
};
|
||||
response["pipeline_kind"] = "compile";
|
||||
response["requires_console_validation"] = true;
|
||||
}
|
||||
|
||||
return response ?? AsyncOperationTracker.CreatePendingResponse(job);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"[CompilationHelper] StartCompilationPipeline failed: {e}");
|
||||
return Response.Error($"Failed to start compilation pipeline: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of the last compilation result.
|
||||
/// </summary>
|
||||
public static object GetCompilationSummary()
|
||||
{
|
||||
var errors = GetCompilationErrors();
|
||||
var warnings = GetCompilationWarnings();
|
||||
|
||||
// Only include fields that are actually known; returning 0 is misleading.
|
||||
var result = new System.Collections.Generic.Dictionary<string, object>
|
||||
{
|
||||
["isCompiling"] = IsCompiling()
|
||||
};
|
||||
|
||||
if (errors.HasValue) result["errors"] = errors.Value;
|
||||
if (warnings.HasValue) result["warnings"] = warnings.Value;
|
||||
if (errors.HasValue) result["success"] = errors.Value == 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96c791cc231905b4ca2231ba84bc1f4f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
274
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/ExecPath.cs
Normal file
274
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/ExecPath.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
internal static class ExecPath
|
||||
{
|
||||
private const string PrefClaude = "UnityTcp.ClaudeCliPath";
|
||||
|
||||
// Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
|
||||
internal static string ResolveClaude()
|
||||
{
|
||||
try
|
||||
{
|
||||
string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
|
||||
if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
|
||||
}
|
||||
catch { }
|
||||
|
||||
string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
|
||||
if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/opt/homebrew/bin/claude",
|
||||
"/usr/local/bin/claude",
|
||||
Path.Combine(home, ".local", "bin", "claude"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
|
||||
string nvmClaude = ResolveClaudeFromNvm(home);
|
||||
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
// Common npm global locations
|
||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
// Prefer .cmd (most reliable from non-interactive processes)
|
||||
Path.Combine(appData, "npm", "claude.cmd"),
|
||||
Path.Combine(localAppData, "npm", "claude.cmd"),
|
||||
// Fall back to PowerShell shim if only .ps1 is present
|
||||
Path.Combine(appData, "npm", "claude.ps1"),
|
||||
Path.Combine(localAppData, "npm", "claude.ps1"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
|
||||
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
// Linux
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/usr/local/bin/claude",
|
||||
"/usr/bin/claude",
|
||||
Path.Combine(home, ".local", "bin", "claude"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
|
||||
string nvmClaude = ResolveClaudeFromNvm(home);
|
||||
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which("claude", "/usr/local/bin:/usr/bin:/bin");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
|
||||
private static string ResolveClaudeFromNvm(string home)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(home)) return null;
|
||||
string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
|
||||
if (!Directory.Exists(nvmNodeDir)) return null;
|
||||
|
||||
string bestPath = null;
|
||||
Version bestVersion = null;
|
||||
foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
|
||||
{
|
||||
string name = Path.GetFileName(versionDir);
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
|
||||
string versionStr = name.Substring(1);
|
||||
int dashIndex = versionStr.IndexOf('-');
|
||||
if (dashIndex > 0)
|
||||
{
|
||||
versionStr = versionStr.Substring(0, dashIndex);
|
||||
}
|
||||
if (Version.TryParse(versionStr, out Version parsed))
|
||||
{
|
||||
string candidate = Path.Combine(versionDir, "bin", "claude");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
if (bestVersion == null || parsed > bestVersion)
|
||||
{
|
||||
bestVersion = parsed;
|
||||
bestPath = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestPath;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// Explicitly set the Claude CLI absolute path override in EditorPrefs
|
||||
internal static void SetClaudeCliPath(string absolutePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
|
||||
{
|
||||
EditorPrefs.SetString(PrefClaude, absolutePath);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Clear any previously set Claude CLI override path
|
||||
internal static void ClearClaudeCliPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (EditorPrefs.HasKey(PrefClaude))
|
||||
{
|
||||
EditorPrefs.DeleteKey(PrefClaude);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
internal static bool TryRun(
|
||||
string file,
|
||||
string args,
|
||||
string workingDir,
|
||||
out string stdout,
|
||||
out string stderr,
|
||||
int timeoutMs = 15000,
|
||||
string extraPathPrepend = null)
|
||||
{
|
||||
stdout = string.Empty;
|
||||
stderr = string.Empty;
|
||||
try
|
||||
{
|
||||
// Handle PowerShell scripts on Windows by invoking through powershell.exe
|
||||
bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = isPs1 ? "powershell.exe" : file,
|
||||
Arguments = isPs1
|
||||
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
|
||||
: args,
|
||||
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
if (!string.IsNullOrEmpty(extraPathPrepend))
|
||||
{
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
|
||||
? extraPathPrepend
|
||||
: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
|
||||
|
||||
var so = new StringBuilder();
|
||||
var se = new StringBuilder();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
|
||||
|
||||
if (!process.Start()) return false;
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
if (!process.WaitForExit(timeoutMs))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure async buffers are flushed
|
||||
process.WaitForExit();
|
||||
|
||||
stdout = so.ToString();
|
||||
stderr = se.ToString();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
private static string Which(string exe, string prependPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("/usr/bin/which", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
|
||||
using var p = Process.Start(psi);
|
||||
string output = p?.StandardOutput.ReadToEnd().Trim();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
private static string Where(string exe)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("where", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
using var p = Process.Start(psi);
|
||||
string first = p?.StandardOutput.ReadToEnd()
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4a955ce7b85e184597177720c6d46b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,536 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Serialization; // For Converters
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles serialization of GameObjects and Components for MCP responses.
|
||||
/// Includes reflection helpers and caching for performance.
|
||||
/// </summary>
|
||||
public static class GameObjectSerializer
|
||||
{
|
||||
// --- Data Serialization ---
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serializable representation of a GameObject.
|
||||
/// </summary>
|
||||
public static object GetGameObjectData(GameObject go)
|
||||
{
|
||||
if (go == null)
|
||||
return null;
|
||||
return new
|
||||
{
|
||||
name = go.name,
|
||||
instanceID = go.GetInstanceID(),
|
||||
tag = go.tag,
|
||||
layer = go.layer,
|
||||
activeSelf = go.activeSelf,
|
||||
activeInHierarchy = go.activeInHierarchy,
|
||||
isStatic = go.isStatic,
|
||||
scenePath = go.scene.path, // Identify which scene it belongs to
|
||||
transform = new // Serialize transform components carefully to avoid JSON issues
|
||||
{
|
||||
// Serialize Vector3 components individually to prevent self-referencing loops.
|
||||
// The default serializer can struggle with properties like Vector3.normalized.
|
||||
position = new
|
||||
{
|
||||
x = go.transform.position.x,
|
||||
y = go.transform.position.y,
|
||||
z = go.transform.position.z,
|
||||
},
|
||||
localPosition = new
|
||||
{
|
||||
x = go.transform.localPosition.x,
|
||||
y = go.transform.localPosition.y,
|
||||
z = go.transform.localPosition.z,
|
||||
},
|
||||
rotation = new
|
||||
{
|
||||
x = go.transform.rotation.eulerAngles.x,
|
||||
y = go.transform.rotation.eulerAngles.y,
|
||||
z = go.transform.rotation.eulerAngles.z,
|
||||
},
|
||||
localRotation = new
|
||||
{
|
||||
x = go.transform.localRotation.eulerAngles.x,
|
||||
y = go.transform.localRotation.eulerAngles.y,
|
||||
z = go.transform.localRotation.eulerAngles.z,
|
||||
},
|
||||
scale = new
|
||||
{
|
||||
x = go.transform.localScale.x,
|
||||
y = go.transform.localScale.y,
|
||||
z = go.transform.localScale.z,
|
||||
},
|
||||
forward = new
|
||||
{
|
||||
x = go.transform.forward.x,
|
||||
y = go.transform.forward.y,
|
||||
z = go.transform.forward.z,
|
||||
},
|
||||
up = new
|
||||
{
|
||||
x = go.transform.up.x,
|
||||
y = go.transform.up.y,
|
||||
z = go.transform.up.z,
|
||||
},
|
||||
right = new
|
||||
{
|
||||
x = go.transform.right.x,
|
||||
y = go.transform.right.y,
|
||||
z = go.transform.right.z,
|
||||
},
|
||||
},
|
||||
parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
|
||||
// Optionally include components, but can be large
|
||||
// components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
|
||||
// Or just component names:
|
||||
componentNames = go.GetComponents<Component>()
|
||||
.Select(c => c.GetType().FullName)
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Metadata Caching for Reflection ---
|
||||
private class CachedMetadata
|
||||
{
|
||||
public readonly List<PropertyInfo> SerializableProperties;
|
||||
public readonly List<FieldInfo> SerializableFields;
|
||||
|
||||
public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
|
||||
{
|
||||
SerializableProperties = properties;
|
||||
SerializableFields = fields;
|
||||
}
|
||||
}
|
||||
// Key becomes Tuple<Type, bool>
|
||||
private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
|
||||
// --- End Metadata Caching ---
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serializable representation of a Component, attempting to serialize
|
||||
/// public properties and fields using reflection, with caching and control over non-public fields.
|
||||
/// </summary>
|
||||
// Add the flag parameter here
|
||||
public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
|
||||
{
|
||||
// --- Add Early Logging ---
|
||||
// Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})");
|
||||
// --- End Early Logging ---
|
||||
|
||||
if (c == null) return null;
|
||||
Type componentType = c.GetType();
|
||||
|
||||
// --- Special handling for Transform to avoid reflection crashes and problematic properties ---
|
||||
if (componentType == typeof(Transform))
|
||||
{
|
||||
Transform tr = c as Transform;
|
||||
// Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})");
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", tr.GetInstanceID() },
|
||||
// Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'.
|
||||
{ "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles
|
||||
{ "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 },
|
||||
{ "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 },
|
||||
{ "childCount", tr.childCount },
|
||||
// Include standard Object/Component properties
|
||||
{ "name", tr.name },
|
||||
{ "tag", tr.tag },
|
||||
{ "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 }
|
||||
};
|
||||
}
|
||||
// --- End Special handling for Transform ---
|
||||
|
||||
// --- Special handling for Camera to avoid matrix-related crashes ---
|
||||
if (componentType == typeof(Camera))
|
||||
{
|
||||
Camera cam = c as Camera;
|
||||
var cameraProperties = new Dictionary<string, object>();
|
||||
|
||||
// List of safe properties to serialize
|
||||
var safeProperties = new Dictionary<string, Func<object>>
|
||||
{
|
||||
{ "nearClipPlane", () => cam.nearClipPlane },
|
||||
{ "farClipPlane", () => cam.farClipPlane },
|
||||
{ "fieldOfView", () => cam.fieldOfView },
|
||||
{ "renderingPath", () => (int)cam.renderingPath },
|
||||
{ "actualRenderingPath", () => (int)cam.actualRenderingPath },
|
||||
{ "allowHDR", () => cam.allowHDR },
|
||||
{ "allowMSAA", () => cam.allowMSAA },
|
||||
{ "allowDynamicResolution", () => cam.allowDynamicResolution },
|
||||
{ "forceIntoRenderTexture", () => cam.forceIntoRenderTexture },
|
||||
{ "orthographicSize", () => cam.orthographicSize },
|
||||
{ "orthographic", () => cam.orthographic },
|
||||
{ "opaqueSortMode", () => (int)cam.opaqueSortMode },
|
||||
{ "transparencySortMode", () => (int)cam.transparencySortMode },
|
||||
{ "depth", () => cam.depth },
|
||||
{ "aspect", () => cam.aspect },
|
||||
{ "cullingMask", () => cam.cullingMask },
|
||||
{ "eventMask", () => cam.eventMask },
|
||||
{ "backgroundColor", () => cam.backgroundColor },
|
||||
{ "clearFlags", () => (int)cam.clearFlags },
|
||||
{ "stereoEnabled", () => cam.stereoEnabled },
|
||||
{ "stereoSeparation", () => cam.stereoSeparation },
|
||||
{ "stereoConvergence", () => cam.stereoConvergence },
|
||||
{ "enabled", () => cam.enabled },
|
||||
{ "name", () => cam.name },
|
||||
{ "tag", () => cam.tag },
|
||||
{ "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } }
|
||||
};
|
||||
|
||||
foreach (var prop in safeProperties)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = prop.Value();
|
||||
if (value != null)
|
||||
{
|
||||
AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silently skip any property that fails
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", cam.GetInstanceID() },
|
||||
{ "properties", cameraProperties }
|
||||
};
|
||||
}
|
||||
// --- End Special handling for Camera ---
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", c.GetInstanceID() }
|
||||
};
|
||||
|
||||
// --- Get Cached or Generate Metadata (using new cache key) ---
|
||||
Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
|
||||
if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
|
||||
{
|
||||
var propertiesToCache = new List<PropertyInfo>();
|
||||
var fieldsToCache = new List<FieldInfo>();
|
||||
|
||||
// Traverse the hierarchy from the component type up to MonoBehaviour
|
||||
Type currentType = componentType;
|
||||
while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
|
||||
{
|
||||
// Get properties declared only at the current type level
|
||||
BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
foreach (var propInfo in currentType.GetProperties(propFlags))
|
||||
{
|
||||
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
|
||||
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
|
||||
// Add if not already added (handles overrides - keep the most derived version)
|
||||
if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) {
|
||||
propertiesToCache.Add(propInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get fields declared only at the current type level (both public and non-public)
|
||||
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
var declaredFields = currentType.GetFields(fieldFlags);
|
||||
|
||||
// Process the declared Fields for caching
|
||||
foreach (var fieldInfo in declaredFields)
|
||||
{
|
||||
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
|
||||
|
||||
// Add if not already added (handles hiding - keep the most derived version)
|
||||
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
|
||||
|
||||
bool shouldInclude = false;
|
||||
if (includeNonPublicSerializedFields)
|
||||
{
|
||||
// If TRUE, include Public OR NonPublic with [SerializeField]
|
||||
shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
|
||||
}
|
||||
else // includeNonPublicSerializedFields is FALSE
|
||||
{
|
||||
// If FALSE, include ONLY if it is explicitly Public.
|
||||
shouldInclude = fieldInfo.IsPublic;
|
||||
}
|
||||
|
||||
if (shouldInclude)
|
||||
{
|
||||
fieldsToCache.Add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the base type
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
// --- End Hierarchy Traversal ---
|
||||
|
||||
cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
|
||||
_metadataCache[cacheKey] = cachedData; // Add to cache with combined key
|
||||
}
|
||||
// --- End Get Cached or Generate Metadata ---
|
||||
|
||||
// --- Use cached metadata ---
|
||||
var serializablePropertiesOutput = new Dictionary<string, object>();
|
||||
|
||||
// --- Add Logging Before Property Loop ---
|
||||
// Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}...");
|
||||
// --- End Logging Before Property Loop ---
|
||||
|
||||
// Use cached properties
|
||||
foreach (var propInfo in cachedData.SerializableProperties)
|
||||
{
|
||||
string propName = propInfo.Name;
|
||||
|
||||
// --- Skip known obsolete/problematic Component shortcut properties ---
|
||||
bool skipProperty = false;
|
||||
if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
|
||||
propName == "light" || propName == "animation" || propName == "constantForce" ||
|
||||
propName == "renderer" || propName == "audio" || propName == "networkView" ||
|
||||
propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
|
||||
propName == "particleSystem" ||
|
||||
// Also skip potentially problematic Matrix properties prone to cycles/errors
|
||||
propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
|
||||
{
|
||||
// Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End Skip Generic Properties ---
|
||||
|
||||
// --- Skip Renderer.material / materials to avoid instantiating materials in edit mode ---
|
||||
if (!skipProperty &&
|
||||
(typeof(Renderer).IsAssignableFrom(componentType)) &&
|
||||
(propName == "material" || propName == "materials"))
|
||||
{
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End skip Renderer material properties ---
|
||||
|
||||
// --- Skip specific potentially problematic Camera properties ---
|
||||
if (componentType == typeof(Camera) &&
|
||||
(propName == "pixelRect" ||
|
||||
propName == "rect" ||
|
||||
propName == "cullingMatrix" ||
|
||||
propName == "useOcclusionCulling" ||
|
||||
propName == "worldToCameraMatrix" ||
|
||||
propName == "projectionMatrix" ||
|
||||
propName == "nonJitteredProjectionMatrix" ||
|
||||
propName == "previousViewProjectionMatrix" ||
|
||||
propName == "cameraToWorldMatrix"))
|
||||
{
|
||||
// Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}");
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End Skip Camera Properties ---
|
||||
|
||||
// --- Skip specific potentially problematic Transform properties ---
|
||||
if (componentType == typeof(Transform) &&
|
||||
(propName == "lossyScale" ||
|
||||
propName == "rotation" ||
|
||||
propName == "worldToLocalMatrix" ||
|
||||
propName == "localToWorldMatrix"))
|
||||
{
|
||||
// Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}");
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End Skip Transform Properties ---
|
||||
|
||||
// Skip if flagged
|
||||
if (skipProperty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// --- Add detailed logging ---
|
||||
// Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}");
|
||||
// --- End detailed logging ---
|
||||
object value = propInfo.GetValue(c);
|
||||
Type propType = propInfo.PropertyType;
|
||||
AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Debug.LogWarning($"Could not read property {propName} on {componentType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Add Logging Before Field Loop ---
|
||||
// Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}...");
|
||||
// --- End Logging Before Field Loop ---
|
||||
|
||||
// Use cached fields
|
||||
foreach (var fieldInfo in cachedData.SerializableFields)
|
||||
{
|
||||
try
|
||||
{
|
||||
// --- Add detailed logging for fields ---
|
||||
// Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}");
|
||||
// --- End detailed logging for fields ---
|
||||
object value = fieldInfo.GetValue(c);
|
||||
string fieldName = fieldInfo.Name;
|
||||
Type fieldType = fieldInfo.FieldType;
|
||||
AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}");
|
||||
}
|
||||
}
|
||||
// --- End Use cached metadata ---
|
||||
|
||||
if (serializablePropertiesOutput.Count > 0)
|
||||
{
|
||||
data["properties"] = serializablePropertiesOutput;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Helper function to decide how to serialize different types
|
||||
private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
|
||||
{
|
||||
// Simplified: Directly use CreateTokenFromValue which uses the serializer
|
||||
if (value == null)
|
||||
{
|
||||
dict[name] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use the helper that employs our custom serializer settings
|
||||
JToken token = CreateTokenFromValue(value, type);
|
||||
if (token != null) // Check if serialization succeeded in the helper
|
||||
{
|
||||
// Convert JToken back to a basic object structure for the dictionary
|
||||
dict[name] = ConvertJTokenToPlainObject(token);
|
||||
}
|
||||
// If token is null, it means serialization failed and a warning was logged.
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catch potential errors during JToken conversion or addition to dictionary
|
||||
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert JToken back to basic object structure
|
||||
private static object ConvertJTokenToPlainObject(JToken token)
|
||||
{
|
||||
if (token == null) return null;
|
||||
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Object:
|
||||
var objDict = new Dictionary<string, object>();
|
||||
foreach (var prop in ((JObject)token).Properties())
|
||||
{
|
||||
objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
|
||||
}
|
||||
return objDict;
|
||||
|
||||
case JTokenType.Array:
|
||||
var list = new List<object>();
|
||||
foreach (var item in (JArray)token)
|
||||
{
|
||||
list.Add(ConvertJTokenToPlainObject(item));
|
||||
}
|
||||
return list;
|
||||
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<long>(); // Use long for safety
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<double>(); // Use double for safety
|
||||
case JTokenType.String:
|
||||
return token.ToObject<string>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
case JTokenType.Date:
|
||||
return token.ToObject<DateTime>();
|
||||
case JTokenType.Guid:
|
||||
return token.ToObject<Guid>();
|
||||
case JTokenType.Uri:
|
||||
return token.ToObject<Uri>();
|
||||
case JTokenType.TimeSpan:
|
||||
return token.ToObject<TimeSpan>();
|
||||
case JTokenType.Bytes:
|
||||
return token.ToObject<byte[]>();
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
case JTokenType.Undefined:
|
||||
return null; // Treat undefined as null
|
||||
|
||||
default:
|
||||
// Fallback for simple value types not explicitly listed
|
||||
if (token is JValue jValue && jValue.Value != null)
|
||||
{
|
||||
return jValue.Value;
|
||||
}
|
||||
// Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Define custom JsonSerializerSettings for OUTPUT ---
|
||||
private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
new Vector3Converter(),
|
||||
new Vector2Converter(),
|
||||
new QuaternionConverter(),
|
||||
new ColorConverter(),
|
||||
new RectConverter(),
|
||||
new BoundsConverter(),
|
||||
new UnityEngineObjectConverter() // Handles serialization of references
|
||||
},
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
// ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
|
||||
};
|
||||
private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
|
||||
// --- End Define custom JsonSerializerSettings ---
|
||||
|
||||
// Helper to create JToken using the output serializer
|
||||
private static JToken CreateTokenFromValue(object value, Type type)
|
||||
{
|
||||
if (value == null) return JValue.CreateNull();
|
||||
|
||||
try
|
||||
{
|
||||
// Use the pre-configured OUTPUT serializer instance
|
||||
return JToken.FromObject(value, _outputSerializer);
|
||||
}
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
Debug.LogWarning($"[GameObjectSerializer] Codely.Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
|
||||
return null; // Indicate serialization failure
|
||||
}
|
||||
catch (Exception e) // Catch other unexpected errors
|
||||
{
|
||||
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
|
||||
return null; // Indicate serialization failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29be5222623c0324ca01f6b7ffaaa602
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for JSON command processing utilities
|
||||
/// </summary>
|
||||
public static class JsonCommandHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper method to check if a string is valid JSON
|
||||
/// </summary>
|
||||
public static bool IsValidJson(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
text = text.Trim();
|
||||
if (
|
||||
(text.StartsWith("{") && text.EndsWith("}"))
|
||||
|| // Object
|
||||
(text.StartsWith("[") && text.EndsWith("]"))
|
||||
) // Array
|
||||
{
|
||||
try
|
||||
{
|
||||
JToken.Parse(text);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get a summary of parameters for error reporting
|
||||
/// </summary>
|
||||
public static string GetParamsSummary(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
return @params == null || !@params.HasValues
|
||||
? "No parameters"
|
||||
: string.Join(
|
||||
", ",
|
||||
@params
|
||||
.Properties()
|
||||
.Select(static p =>
|
||||
$"{p.Name}: {p.Value?.ToString()?[..Math.Min(20, p.Value?.ToString()?.Length ?? 0)]}"
|
||||
)
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Could not summarize parameters";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a17149005eb51642ab32aa65be61cc7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for main thread operations
|
||||
/// </summary>
|
||||
public static class MainThreadHelper
|
||||
{
|
||||
private static int mainThreadId;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the main thread ID for safe thread checks
|
||||
/// Call this from the main thread during static constructor
|
||||
/// </summary>
|
||||
public static void InitializeMainThreadId()
|
||||
{
|
||||
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the given function on the Unity main thread and wait up to timeoutMs for the result.
|
||||
/// Returns null on timeout or error; caller should provide a fallback error response.
|
||||
/// </summary>
|
||||
public static object InvokeOnMainThreadWithTimeout(Func<object> func, int timeoutMs)
|
||||
{
|
||||
if (func == null) return null;
|
||||
try
|
||||
{
|
||||
// If mainThreadId is unknown, assume we're on main thread to avoid blocking the editor.
|
||||
if (mainThreadId == 0)
|
||||
{
|
||||
try { return func(); }
|
||||
catch (Exception ex) { throw new InvalidOperationException($"Main thread handler error: {ex.Message}", ex); }
|
||||
}
|
||||
// If we are already on the main thread, execute directly to avoid deadlocks
|
||||
try
|
||||
{
|
||||
if (Thread.CurrentThread.ManagedThreadId == mainThreadId)
|
||||
{
|
||||
return func();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
object result = null;
|
||||
Exception captured = null;
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
result = func();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
captured = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { tcs.TrySetResult(true); } catch { }
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for completion with timeout (Editor thread will pump delayCall)
|
||||
bool completed = tcs.Task.Wait(timeoutMs);
|
||||
if (!completed)
|
||||
{
|
||||
return null; // timeout
|
||||
}
|
||||
if (captured != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Main thread handler error: {captured.Message}", captured);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to invoke on main thread: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f24f49a4ec33ffe448b5c69d381dfa9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
479
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/PortManager.cs
Normal file
479
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/PortManager.cs
Normal file
@@ -0,0 +1,479 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages dynamic port allocation and persistent storage for Codely Bridge connections
|
||||
/// </summary>
|
||||
public static class PortManager
|
||||
{
|
||||
private static bool IsDebugEnabled()
|
||||
{
|
||||
try { return EditorPrefs.GetBool("UnityTcp.DebugLogs", false); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private const int DefaultPort = 25916;
|
||||
private const int MaxPortAttempts = 100;
|
||||
private const string RegistryFileName = ".com-unity-codely.json";
|
||||
|
||||
[Serializable]
|
||||
public class PortConfig
|
||||
{
|
||||
public int unity_port;
|
||||
public string created_date;
|
||||
public string project_path;
|
||||
|
||||
// Status/heartbeat fields
|
||||
public bool reloading;
|
||||
public string reason;
|
||||
public int seq;
|
||||
public string last_heartbeat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the port to use - either from storage or discover a new one
|
||||
/// Will try stored port first, then fallback to discovering new port
|
||||
/// </summary>
|
||||
/// <returns>Port number to use</returns>
|
||||
public static int GetPortWithFallback()
|
||||
{
|
||||
// Try to load stored port first, but only if it's from the current project
|
||||
var storedConfig = GetStoredPortConfig();
|
||||
if (storedConfig != null &&
|
||||
storedConfig.unity_port > 0 &&
|
||||
string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
|
||||
IsPortAvailable(storedConfig.unity_port))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Using stored port {storedConfig.unity_port} for current project");
|
||||
return storedConfig.unity_port;
|
||||
}
|
||||
|
||||
// If stored port exists but is currently busy, wait briefly for release
|
||||
if (storedConfig != null && storedConfig.unity_port > 0)
|
||||
{
|
||||
if (WaitForPortRelease(storedConfig.unity_port, 1500))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
|
||||
return storedConfig.unity_port;
|
||||
}
|
||||
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
|
||||
return storedConfig.unity_port;
|
||||
}
|
||||
|
||||
// If no valid stored port, find a new one and save it
|
||||
int newPort = FindAvailablePort();
|
||||
SavePort(newPort);
|
||||
return newPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover and save a new available port (used by Auto-Connect button)
|
||||
/// </summary>
|
||||
/// <returns>New available port</returns>
|
||||
public static int DiscoverNewPort()
|
||||
{
|
||||
int newPort = FindAvailablePort();
|
||||
SavePort(newPort);
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Discovered and saved new port: {newPort}");
|
||||
return newPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find an available port starting from the default port
|
||||
/// </summary>
|
||||
/// <returns>Available port number</returns>
|
||||
private static int FindAvailablePort()
|
||||
{
|
||||
// Always try default port first
|
||||
if (IsPortAvailable(DefaultPort))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Using default port {DefaultPort}");
|
||||
return DefaultPort;
|
||||
}
|
||||
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Default port {DefaultPort} is in use, searching for alternative...");
|
||||
|
||||
// Search for alternatives
|
||||
for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
|
||||
{
|
||||
if (IsPortAvailable(port))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Found available port {port}");
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific port is available for binding
|
||||
/// Uses same socket options as the actual TCP listener to ensure consistent behavior
|
||||
/// </summary>
|
||||
/// <param name="port">Port to check</param>
|
||||
/// <returns>True if port is available</returns>
|
||||
public static bool IsPortAvailable(int port)
|
||||
{
|
||||
TcpListener testListener = null;
|
||||
try
|
||||
{
|
||||
testListener = new TcpListener(IPAddress.Loopback, port);
|
||||
|
||||
// Use same socket options as the actual listener for consistent checking
|
||||
#if UNITY_EDITOR_WIN
|
||||
// On Windows: no reuse + exclusive access for strict isolation
|
||||
testListener.Server.SetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress,
|
||||
false
|
||||
);
|
||||
try
|
||||
{
|
||||
testListener.ExclusiveAddressUse = true; // Require exclusive access for availability check
|
||||
}
|
||||
catch { }
|
||||
#else
|
||||
// On macOS/Linux: Disable port reuse
|
||||
try
|
||||
{
|
||||
testListener.Server.SetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress,
|
||||
false
|
||||
);
|
||||
}
|
||||
catch { }
|
||||
#endif
|
||||
|
||||
// Minimize TIME_WAIT by sending RST on close (same as actual listener)
|
||||
try
|
||||
{
|
||||
testListener.Server.LingerState = new LingerOption(true, 0);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore if not supported on platform
|
||||
}
|
||||
|
||||
testListener.Start();
|
||||
testListener.Stop();
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { testListener?.Stop(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a port is currently being used by Codely Bridge server
|
||||
/// This helps avoid unnecessary port changes when Unity itself is using the port
|
||||
/// </summary>
|
||||
/// <param name="port">Port to check</param>
|
||||
/// <returns>True if port appears to be used by Codely Bridge server</returns>
|
||||
public static bool IsPortUsedByUnityTcp(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to make a quick connection to see if it's a Codely Bridge server
|
||||
using var client = new TcpClient();
|
||||
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
|
||||
if (connectTask.Wait(100)) // 100ms timeout
|
||||
{
|
||||
// If connection succeeded, it's likely the Codely Bridge server
|
||||
return client.Connected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect if another Codely Bridge instance is already using this port
|
||||
/// Provides better error reporting for port conflicts
|
||||
/// </summary>
|
||||
/// <param name="port">Port to check</param>
|
||||
/// <returns>Detailed information about port usage</returns>
|
||||
public static (bool inUse, string description) CheckPortConflict(int port)
|
||||
{
|
||||
// First check basic availability with exclusive access
|
||||
if (IsPortAvailable(port))
|
||||
{
|
||||
return (false, "Port is available");
|
||||
}
|
||||
|
||||
// Port is in use, try to determine what's using it
|
||||
if (IsPortUsedByUnityTcp(port))
|
||||
{
|
||||
return (true, "Port is in use by another Codely Bridge Bridge instance");
|
||||
}
|
||||
|
||||
// Port is in use by something else
|
||||
return (true, "Port is in use by another process");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for a port to become available for a limited amount of time.
|
||||
/// Used to bridge the gap during domain reload when the old listener
|
||||
/// hasn't released the socket yet.
|
||||
/// </summary>
|
||||
private static bool WaitForPortRelease(int port, int timeoutMs)
|
||||
{
|
||||
int waited = 0;
|
||||
const int step = 100;
|
||||
while (waited < timeoutMs)
|
||||
{
|
||||
if (IsPortAvailable(port))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the port is in use by a Codely Bridge instance, continue waiting briefly
|
||||
if (!IsPortUsedByUnityTcp(port))
|
||||
{
|
||||
// In use by something else; don't keep waiting
|
||||
return false;
|
||||
}
|
||||
|
||||
Thread.Sleep(step);
|
||||
waited += step;
|
||||
}
|
||||
return IsPortAvailable(port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save port to persistent storage, preserving existing status information
|
||||
/// </summary>
|
||||
/// <param name="port">Port to save</param>
|
||||
private static void SavePort(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load existing config to preserve status information
|
||||
var existingConfig = GetStoredPortConfig();
|
||||
|
||||
var portConfig = new PortConfig
|
||||
{
|
||||
unity_port = port,
|
||||
created_date = existingConfig?.created_date ?? DateTime.UtcNow.ToString("O"),
|
||||
project_path = Application.dataPath,
|
||||
|
||||
// Preserve existing status fields
|
||||
reloading = existingConfig?.reloading ?? false,
|
||||
reason = existingConfig?.reason ?? "ready",
|
||||
seq = existingConfig?.seq ?? 0,
|
||||
last_heartbeat = existingConfig?.last_heartbeat
|
||||
};
|
||||
|
||||
SavePortConfig(portConfig);
|
||||
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Saved port {port} to storage");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not save port to storage: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save port configuration to persistent storage
|
||||
/// </summary>
|
||||
/// <param name="portConfig">Port configuration to save</param>
|
||||
public static void SavePortConfig(PortConfig portConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
string registryDir = GetRegistryDirectory();
|
||||
Directory.CreateDirectory(registryDir);
|
||||
|
||||
string registryFile = GetRegistryFilePath();
|
||||
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
|
||||
// Write to project root config file
|
||||
File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
|
||||
|
||||
// Also maintain backwards compatibility by writing to legacy location
|
||||
try
|
||||
{
|
||||
string legacyDir = GetLegacyRegistryDirectory();
|
||||
Directory.CreateDirectory(legacyDir);
|
||||
string legacyFile = Path.Combine(legacyDir, "unity-tcp-port.json");
|
||||
File.WriteAllText(legacyFile, json, new System.Text.UTF8Encoding(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore legacy write failures
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not save port config to storage: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load port from persistent storage
|
||||
/// </summary>
|
||||
/// <returns>Stored port number, or 0 if not found</returns>
|
||||
private static int LoadStoredPort()
|
||||
{
|
||||
try
|
||||
{
|
||||
string registryFile = GetRegistryFilePath();
|
||||
|
||||
if (!File.Exists(registryFile))
|
||||
{
|
||||
// Backwards compatibility: try the legacy locations
|
||||
// First try the new legacy location in project root
|
||||
string projectLegacy = Path.Combine(GetRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(projectLegacy))
|
||||
{
|
||||
registryFile = projectLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Then try the old user home location
|
||||
string userHomeLegacy = Path.Combine(GetLegacyRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(userHomeLegacy))
|
||||
{
|
||||
registryFile = userHomeLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also check hash-based files in user home
|
||||
string hashBased = Path.Combine(GetLegacyRegistryDirectory(), $"unity-tcp-port-{ComputeProjectHash(Application.dataPath)}.json");
|
||||
if (File.Exists(hashBased))
|
||||
{
|
||||
registryFile = hashBased;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(registryFile);
|
||||
var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);
|
||||
|
||||
return portConfig?.unity_port ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not load port from storage: {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current stored port configuration
|
||||
/// </summary>
|
||||
/// <returns>Port configuration if exists, null otherwise</returns>
|
||||
public static PortConfig GetStoredPortConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
string registryFile = GetRegistryFilePath();
|
||||
|
||||
if (!File.Exists(registryFile))
|
||||
{
|
||||
// Backwards compatibility: try the legacy locations
|
||||
// First try the new legacy location in project root
|
||||
string projectLegacy = Path.Combine(GetRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(projectLegacy))
|
||||
{
|
||||
registryFile = projectLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Then try the old user home location
|
||||
string userHomeLegacy = Path.Combine(GetLegacyRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(userHomeLegacy))
|
||||
{
|
||||
registryFile = userHomeLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also check hash-based files in user home
|
||||
string hashBased = Path.Combine(GetLegacyRegistryDirectory(), $"unity-tcp-port-{ComputeProjectHash(Application.dataPath)}.json");
|
||||
if (File.Exists(hashBased))
|
||||
{
|
||||
registryFile = hashBased;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(registryFile);
|
||||
return JsonConvert.DeserializeObject<PortConfig>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not load port config: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRegistryDirectory()
|
||||
{
|
||||
// Use project root directory (parent of Assets folder)
|
||||
string assetsPath = Application.dataPath;
|
||||
string projectRoot = Directory.GetParent(assetsPath)?.FullName ?? assetsPath;
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
private static string GetLegacyRegistryDirectory()
|
||||
{
|
||||
// Legacy location in user home directory
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-tcp");
|
||||
}
|
||||
|
||||
private static string GetRegistryFilePath()
|
||||
{
|
||||
string dir = GetRegistryDirectory();
|
||||
return Path.Combine(dir, RegistryFileName);
|
||||
}
|
||||
|
||||
private static string ComputeProjectHash(string input)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SHA1 sha1 = SHA1.Create();
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
|
||||
byte[] hashBytes = sha1.ComputeHash(bytes);
|
||||
var sb = new StringBuilder();
|
||||
foreach (byte b in hashBytes)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
return sb.ToString()[..8]; // short, sufficient for filenames
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db27ee87f1c170b47928576e31cc2c9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
144
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/Response.cs
Normal file
144
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/Response.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides static methods for creating standardized success and error response objects.
|
||||
/// Ensures consistent JSON structure for communication back to the Codely client.
|
||||
///
|
||||
/// Response format aligns with the OpenAPI spec:
|
||||
/// - ImmediateResponse: { success, message, data?, state?, state_delta? }
|
||||
/// - PendingResponse: { _mcp_status, op_id, poll_interval, message, state?, state_delta? }
|
||||
/// </summary>
|
||||
public static class Response
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a standardized success response object with optional state.
|
||||
/// </summary>
|
||||
/// <param name="message">A message describing the successful operation.</param>
|
||||
/// <param name="data">Optional additional data to include in the response.</param>
|
||||
/// <param name="includeState">Whether to include full state snapshot (default: false).</param>
|
||||
/// <param name="stateDelta">Optional state delta for incremental updates.</param>
|
||||
/// <returns>An object representing the success response.</returns>
|
||||
public static object Success(string message, object data = null, bool includeState = false, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
{ "success", true },
|
||||
{ "message", message },
|
||||
// Always include current state revision so clients can keep client_state_rev in sync
|
||||
{ "rev", StateComposer.GetCurrentRevision() }
|
||||
};
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
response["data"] = data;
|
||||
}
|
||||
|
||||
// Include state if explicitly requested
|
||||
if (includeState)
|
||||
{
|
||||
response["state"] = StateComposer.BuildFullState();
|
||||
}
|
||||
|
||||
// Include state_delta if provided
|
||||
if (stateDelta != null)
|
||||
{
|
||||
response["state_delta"] = stateDelta;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standardized success response with automatic state delta.
|
||||
/// Use this for write operations that modify Unity state.
|
||||
/// </summary>
|
||||
public static object SuccessWithDelta(string message, object data = null, object stateDelta = null)
|
||||
{
|
||||
StateComposer.IncrementRevision();
|
||||
return Success(message, data, includeState: false, stateDelta: stateDelta);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standardized success response with full state snapshot.
|
||||
/// Use this for operations that require the client to have the latest state.
|
||||
/// </summary>
|
||||
public static object SuccessWithState(string message, object data = null)
|
||||
{
|
||||
StateComposer.IncrementRevision();
|
||||
return Success(message, data, includeState: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standardized error response object.
|
||||
/// </summary>
|
||||
/// <param name="errorCodeOrMessage">A message describing the error.</param>
|
||||
/// <param name="data">Optional additional data (e.g., error details) to include.</param>
|
||||
/// <param name="includeState">Whether to include full state snapshot for recovery (default: false).</param>
|
||||
/// <returns>An object representing the error response.</returns>
|
||||
public static object Error(string errorCodeOrMessage, object data = null, bool includeState = false)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
{ "success", false },
|
||||
{ "code", errorCodeOrMessage },
|
||||
{ "error", errorCodeOrMessage }
|
||||
};
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
response["data"] = data;
|
||||
}
|
||||
|
||||
// Include state on error for recovery scenarios
|
||||
if (includeState)
|
||||
{
|
||||
response["state"] = StateComposer.BuildFullState();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a conflict response for state revision mismatches.
|
||||
/// This is returned when client_state_rev doesn't match server's revision.
|
||||
/// </summary>
|
||||
/// <param name="clientRev">The client's provided revision.</param>
|
||||
/// <param name="serverRev">The server's current revision.</param>
|
||||
/// <returns>A conflict response with full state for synchronization.</returns>
|
||||
public static object Conflict(int clientRev, int serverRev)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "success", false },
|
||||
{ "code", "state_revision_conflict" },
|
||||
{ "error", $"State revision mismatch. Client: {clientRev}, Server: {serverRev}. Please refresh state." },
|
||||
{ "state", StateComposer.BuildFullState() }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy overload for backward compatibility.
|
||||
/// </summary>
|
||||
[Obsolete("Use Success(message, data, includeState, stateDelta) instead.")]
|
||||
public static object SuccessLegacy(string message, object data = null)
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = message,
|
||||
data = data,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new { success = true, message = message };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 635f93405037a114993e3a9a44c54745
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
public static class ServerPathResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to locate the package root directory for cn.tuanjie.codely.bridge.
|
||||
/// Returns true if found and sets packagePath to the package root folder.
|
||||
/// </summary>
|
||||
public static bool TryFindPackageRoot(out string packagePath, bool warnOnLegacyPackageId = true)
|
||||
{
|
||||
// Resolve via local package info (no network). Fall back to Client.List on older editors.
|
||||
try
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
// Primary: the package that owns this assembly
|
||||
var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
|
||||
if (owner != null)
|
||||
{
|
||||
if (TryResolvePackage(owner, out packagePath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary: scan all registered packages locally
|
||||
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
|
||||
{
|
||||
if (TryResolvePackage(p, out packagePath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Older Unity versions: use Package Manager Client.List as a fallback
|
||||
var list = UnityEditor.PackageManager.Client.List();
|
||||
while (!list.IsCompleted) { }
|
||||
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
|
||||
{
|
||||
foreach (var pkg in list.Result)
|
||||
{
|
||||
if (TryResolvePackage(pkg, out packagePath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
packagePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolvePackage(UnityEditor.PackageManager.PackageInfo p, out string packagePath, bool warnOnLegacyPackageId)
|
||||
{
|
||||
const string CurrentId = "cn.tuanjie.codely.bridge";
|
||||
|
||||
packagePath = null;
|
||||
if (p == null || p.name != CurrentId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
packagePath = p.resolvedPath;
|
||||
return !string.IsNullOrEmpty(packagePath) && Directory.Exists(packagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,783 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized state composition and revision tracking for Unity Editor state.
|
||||
/// Provides consistent state snapshots and incremental state_delta generation.
|
||||
/// </summary>
|
||||
public static class StateComposer
|
||||
{
|
||||
// Global state revision counter (incremented on every state change)
|
||||
private static int _globalRevision = 0;
|
||||
private static readonly object _revisionLock = new object();
|
||||
|
||||
// Console state tracking (shared with ReadConsole)
|
||||
private static string _currentConsoleToken = null;
|
||||
private static int _consoleUnreadCount = 0;
|
||||
private static readonly List<object> _lastConsoleErrors = new List<object>();
|
||||
private static readonly object _consoleLock = new object();
|
||||
|
||||
// Touched assets tracking
|
||||
private static readonly List<object> _touchedAssets = new List<object>();
|
||||
private static readonly object _assetsLock = new object();
|
||||
|
||||
// Pending operations tracking
|
||||
private static readonly List<object> _pendingOperations = new List<object>();
|
||||
private static readonly object _operationsLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Increment and return the next global revision number.
|
||||
/// Thread-safe.
|
||||
/// </summary>
|
||||
public static int IncrementRevision()
|
||||
{
|
||||
lock (_revisionLock)
|
||||
{
|
||||
return ++_globalRevision;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current global revision without incrementing.
|
||||
/// </summary>
|
||||
public static int GetCurrentRevision()
|
||||
{
|
||||
lock (_revisionLock)
|
||||
{
|
||||
return _globalRevision;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a complete Unity state snapshot with current revision.
|
||||
/// Note: Does NOT auto-increment revision - caller should decide when to increment.
|
||||
/// </summary>
|
||||
public static object BuildFullState()
|
||||
{
|
||||
int currentRev;
|
||||
lock (_revisionLock)
|
||||
{
|
||||
currentRev = _globalRevision;
|
||||
}
|
||||
|
||||
var state = new
|
||||
{
|
||||
editor = BuildEditorState(),
|
||||
project = BuildProjectState(),
|
||||
scene = BuildSceneState(),
|
||||
selection = BuildSelectionState(),
|
||||
console = BuildConsoleState(),
|
||||
assets = BuildAssetsState(),
|
||||
operations = BuildOperationsState(),
|
||||
policy = BuildPolicyState(),
|
||||
rev = currentRev
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a complete Unity state snapshot and increments revision.
|
||||
/// Use this for read operations that need to return fresh state.
|
||||
/// </summary>
|
||||
public static object BuildFullStateAndIncrement()
|
||||
{
|
||||
int newRev = IncrementRevision();
|
||||
|
||||
var state = new
|
||||
{
|
||||
editor = BuildEditorState(),
|
||||
project = BuildProjectState(),
|
||||
scene = BuildSceneState(),
|
||||
selection = BuildSelectionState(),
|
||||
console = BuildConsoleState(),
|
||||
assets = BuildAssetsState(),
|
||||
operations = BuildOperationsState(),
|
||||
policy = BuildPolicyState(),
|
||||
rev = newRev
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds editor-specific state.
|
||||
/// </summary>
|
||||
public static object BuildEditorState()
|
||||
{
|
||||
var playMode = EditorApplication.isPlaying ? "playing" :
|
||||
(EditorApplication.isPaused ? "paused" : "stopped");
|
||||
|
||||
// Get focused window
|
||||
string focusedWindow = null;
|
||||
if (EditorWindow.focusedWindow != null)
|
||||
{
|
||||
focusedWindow = EditorWindow.focusedWindow.GetType().Name;
|
||||
}
|
||||
|
||||
// Determine if operations require focus
|
||||
// This is a heuristic - some operations need the editor to be focused
|
||||
bool requiresFocusForOperations = DetermineIfFocusRequired();
|
||||
|
||||
return new
|
||||
{
|
||||
playMode = playMode,
|
||||
focusedWindow = focusedWindow,
|
||||
requiresFocusForOperations = requiresFocusForOperations,
|
||||
isCompiling = EditorApplication.isCompiling,
|
||||
isUpdating = EditorApplication.isUpdating,
|
||||
lastCompilation = BuildLastCompilationState(),
|
||||
timeSinceStartup = (float)EditorApplication.timeSinceStartup
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds last compilation state.
|
||||
///
|
||||
/// NOTE:
|
||||
/// - This is intentionally minimal and only reports whether Unity is
|
||||
/// currently compiling ("started" vs "idle").
|
||||
/// - It is NOT a per-compilation snapshot and does NOT expose error/
|
||||
/// warning counts for any specific pipeline.
|
||||
/// - For accurate diagnostics (including error/warning counts), callers
|
||||
/// must use:
|
||||
/// * Compilation deltas from StateComposer.CreateCompilationDelta
|
||||
/// (returned by wait_for_compile), and
|
||||
/// * The Unity console (read_console / unity_console) with sinceToken.
|
||||
/// </summary>
|
||||
private static object BuildLastCompilationState()
|
||||
{
|
||||
var status = EditorApplication.isCompiling ? "started" : "idle";
|
||||
|
||||
return new
|
||||
{
|
||||
status = status
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if current operations require focus.
|
||||
/// </summary>
|
||||
private static bool DetermineIfFocusRequired()
|
||||
{
|
||||
// Heuristic: Some operations need focus, especially during Play mode
|
||||
// or when performing visual operations like scene manipulation
|
||||
if (EditorApplication.isPlaying || EditorApplication.isPaused)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if SceneView needs focus for certain operations
|
||||
var sceneView = EditorWindow.focusedWindow as SceneView;
|
||||
if (sceneView != null)
|
||||
{
|
||||
return false; // Already focused
|
||||
}
|
||||
|
||||
return false; // Default: focus not strictly required
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds project-specific state.
|
||||
/// </summary>
|
||||
public static object BuildProjectState()
|
||||
{
|
||||
// Detect Render Pipeline
|
||||
string srp = "builtin";
|
||||
var currentRP = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
|
||||
if (currentRP != null)
|
||||
{
|
||||
string rpName = currentRP.GetType().Name.ToLowerInvariant();
|
||||
if (rpName.Contains("urp") || rpName.Contains("universal"))
|
||||
{
|
||||
srp = "urp";
|
||||
}
|
||||
else if (rpName.Contains("hdrp") || rpName.Contains("highdefinition"))
|
||||
{
|
||||
srp = "hdrp";
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
srp = srp,
|
||||
defineSymbols = GetScriptingDefineSymbols(),
|
||||
packages = GetInstalledPackages(),
|
||||
dirty = false // Would track if project settings are modified
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] GetScriptingDefineSymbols()
|
||||
{
|
||||
// Get scripting define symbols for current build target
|
||||
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
|
||||
var symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
|
||||
return string.IsNullOrEmpty(symbols) ?
|
||||
new string[0] :
|
||||
symbols.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
private static string[] GetInstalledPackages()
|
||||
{
|
||||
// Simplified - in production would use PackageManager API
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds scene-specific state.
|
||||
/// </summary>
|
||||
public static object BuildSceneState()
|
||||
{
|
||||
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
|
||||
|
||||
return new
|
||||
{
|
||||
activeScenePath = activeScene.path,
|
||||
dirty = activeScene.isDirty,
|
||||
hasNavMeshData = HasNavMeshData(),
|
||||
hasLightingData = HasLightingData()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasNavMeshData()
|
||||
{
|
||||
// Check if current scene has NavMesh data using runtime reflection
|
||||
try
|
||||
{
|
||||
// First, try to check NavMeshSurface components (com.unity.ai.navigation package)
|
||||
Type navMeshSurfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation");
|
||||
if (navMeshSurfaceType == null)
|
||||
{
|
||||
// Fallback: search in loaded assemblies
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
navMeshSurfaceType = assembly.GetType("Unity.AI.Navigation.NavMeshSurface");
|
||||
if (navMeshSurfaceType != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (navMeshSurfaceType != null)
|
||||
{
|
||||
// Check NavMeshSurface components for navMeshData
|
||||
var activeSurfacesProperty = navMeshSurfaceType.GetProperty("activeSurfaces", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (activeSurfacesProperty != null)
|
||||
{
|
||||
var activeSurfaces = activeSurfacesProperty.GetValue(null);
|
||||
if (activeSurfaces is System.Collections.IList surfaceList && surfaceList.Count > 0)
|
||||
{
|
||||
var navMeshDataProperty = navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null)
|
||||
{
|
||||
foreach (var surface in surfaceList)
|
||||
{
|
||||
if (surface != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check all NavMeshSurface components in the scene (including inactive)
|
||||
var allSurfaces = Resources.FindObjectsOfTypeAll(navMeshSurfaceType);
|
||||
if (allSurfaces != null && allSurfaces.Length > 0)
|
||||
{
|
||||
var navMeshDataProperty = navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null)
|
||||
{
|
||||
foreach (var surface in allSurfaces)
|
||||
{
|
||||
if (surface != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try to find NavMesh type using reflection (for built-in NavMesh)
|
||||
Type navMeshType = Type.GetType("UnityEngine.AI.NavMesh, UnityEngine.AIModule");
|
||||
if (navMeshType == null)
|
||||
{
|
||||
// Fallback: search in loaded assemblies
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
navMeshType = assembly.GetType("UnityEngine.AI.NavMesh");
|
||||
if (navMeshType != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (navMeshType == null)
|
||||
return false;
|
||||
|
||||
// Get CalculateTriangulation method
|
||||
MethodInfo calculateTriangulationMethod = navMeshType.GetMethod("CalculateTriangulation", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (calculateTriangulationMethod == null)
|
||||
return false;
|
||||
|
||||
// Call CalculateTriangulation using reflection
|
||||
var triangulation = calculateTriangulationMethod.Invoke(null, null);
|
||||
if (triangulation == null)
|
||||
return false;
|
||||
|
||||
// Get vertices property
|
||||
var verticesProperty = triangulation.GetType().GetProperty("vertices");
|
||||
if (verticesProperty == null)
|
||||
return false;
|
||||
|
||||
var vertices = verticesProperty.GetValue(triangulation) as Array;
|
||||
return vertices != null && vertices.Length > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any error occurs, assume no NavMesh data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasLightingData()
|
||||
{
|
||||
// Check if current scene has baked lighting
|
||||
return Lightmapping.giWorkflowMode == Lightmapping.GIWorkflowMode.OnDemand ||
|
||||
Lightmapping.lightingDataAsset != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds selection state.
|
||||
/// </summary>
|
||||
public static object BuildSelectionState()
|
||||
{
|
||||
var activeObject = Selection.activeGameObject;
|
||||
object activeObjectInfo = null;
|
||||
|
||||
if (activeObject != null)
|
||||
{
|
||||
activeObjectInfo = new
|
||||
{
|
||||
id = activeObject.GetInstanceID(),
|
||||
name = activeObject.name,
|
||||
hierarchy_path = GetHierarchyPath(activeObject)
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
activeObject = activeObjectInfo
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(GameObject go)
|
||||
{
|
||||
if (go == null) return "";
|
||||
|
||||
var path = go.name;
|
||||
var parent = go.transform.parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
path = parent.name + "/" + path;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds console state with real tracking data.
|
||||
/// </summary>
|
||||
public static object BuildConsoleState()
|
||||
{
|
||||
lock (_consoleLock)
|
||||
{
|
||||
return new
|
||||
{
|
||||
sinceToken = _currentConsoleToken,
|
||||
unreadCount = _consoleUnreadCount,
|
||||
lastErrors = _lastConsoleErrors.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates console state tracking. Called by ReadConsole.
|
||||
/// </summary>
|
||||
public static void UpdateConsoleState(string sinceToken, int unreadCount = 0, object[] lastErrors = null)
|
||||
{
|
||||
lock (_consoleLock)
|
||||
{
|
||||
_currentConsoleToken = sinceToken;
|
||||
_consoleUnreadCount = unreadCount;
|
||||
_lastConsoleErrors.Clear();
|
||||
if (lastErrors != null)
|
||||
{
|
||||
_lastConsoleErrors.AddRange(lastErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current console token.
|
||||
/// </summary>
|
||||
public static string GetCurrentConsoleToken()
|
||||
{
|
||||
lock (_consoleLock)
|
||||
{
|
||||
return _currentConsoleToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds assets state with tracked touched assets.
|
||||
/// </summary>
|
||||
public static object BuildAssetsState()
|
||||
{
|
||||
lock (_assetsLock)
|
||||
{
|
||||
return new
|
||||
{
|
||||
touched = _touchedAssets.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a touched asset to tracking. Called by asset operations.
|
||||
/// </summary>
|
||||
public static void AddTouchedAsset(string path, bool imported = false, bool hasMeta = true)
|
||||
{
|
||||
lock (_assetsLock)
|
||||
{
|
||||
_touchedAssets.Add(new { path, imported, hasMeta });
|
||||
// Keep only last 100 entries
|
||||
while (_touchedAssets.Count > 100)
|
||||
{
|
||||
_touchedAssets.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears touched assets list.
|
||||
/// </summary>
|
||||
public static void ClearTouchedAssets()
|
||||
{
|
||||
lock (_assetsLock)
|
||||
{
|
||||
_touchedAssets.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds pending operations state from AsyncOperationTracker.
|
||||
/// </summary>
|
||||
public static object BuildOperationsState()
|
||||
{
|
||||
// Get pending operations from AsyncOperationTracker
|
||||
var pendingJobs = AsyncOperationTracker.GetPendingJobs();
|
||||
var pending = pendingJobs.Select(job => new
|
||||
{
|
||||
id = job.OpId,
|
||||
type = job.Type.ToString(),
|
||||
progress = job.Progress,
|
||||
message = job.Message
|
||||
}).ToArray();
|
||||
|
||||
return new
|
||||
{
|
||||
pending = pending
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds policy state.
|
||||
/// </summary>
|
||||
public static object BuildPolicyState()
|
||||
{
|
||||
return new
|
||||
{
|
||||
writeGuardInPlayMode = "deny", // Default: deny writes in Play mode
|
||||
refreshMode = "debounced",
|
||||
consoleReadPolicy = "must_clear_before_read"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Console state delta.
|
||||
/// </summary>
|
||||
public static object CreateConsoleDelta(string sinceToken = null, int? unreadCount = null, object[] lastErrors = null)
|
||||
{
|
||||
var consoleDelta = new Dictionary<string, object>();
|
||||
|
||||
if (sinceToken != null) consoleDelta["sinceToken"] = sinceToken;
|
||||
if (unreadCount.HasValue) consoleDelta["unreadCount"] = unreadCount.Value;
|
||||
if (lastErrors != null) consoleDelta["lastErrors"] = lastErrors;
|
||||
|
||||
return new { console = consoleDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Compilation state delta.
|
||||
/// </summary>
|
||||
public static object CreateCompilationDelta(bool? isCompiling = null, string status = null, int? errors = null, int? warnings = null)
|
||||
{
|
||||
var editorDelta = new Dictionary<string, object>();
|
||||
var compilationDelta = new Dictionary<string, object>();
|
||||
|
||||
if (isCompiling.HasValue) editorDelta["isCompiling"] = isCompiling.Value;
|
||||
|
||||
if (status != null) compilationDelta["status"] = status;
|
||||
if (errors.HasValue) compilationDelta["errors"] = errors.Value;
|
||||
if (warnings.HasValue) compilationDelta["warnings"] = warnings.Value;
|
||||
|
||||
if (compilationDelta.Count > 0)
|
||||
{
|
||||
editorDelta["lastCompilation"] = compilationDelta;
|
||||
}
|
||||
|
||||
return new { editor = editorDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Scene state delta.
|
||||
/// </summary>
|
||||
public static object CreateSceneDelta(string activeScenePath = null, bool? dirty = null)
|
||||
{
|
||||
var sceneDelta = new Dictionary<string, object>();
|
||||
|
||||
if (activeScenePath != null) sceneDelta["activeScenePath"] = activeScenePath;
|
||||
if (dirty.HasValue) sceneDelta["dirty"] = dirty.Value;
|
||||
|
||||
return new { scene = sceneDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Asset state delta.
|
||||
/// </summary>
|
||||
public static object CreateAssetDelta(object[] touchedAssets)
|
||||
{
|
||||
return new
|
||||
{
|
||||
assets = new
|
||||
{
|
||||
touched = touchedAssets
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Editor state delta.
|
||||
/// </summary>
|
||||
public static object CreateEditorDelta(string focusedWindow = null, bool? isUpdating = null)
|
||||
{
|
||||
var editorDelta = new Dictionary<string, object>();
|
||||
|
||||
if (focusedWindow != null) editorDelta["focusedWindow"] = focusedWindow;
|
||||
if (isUpdating.HasValue) editorDelta["isUpdating"] = isUpdating.Value;
|
||||
|
||||
return new { editor = editorDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Operations state delta.
|
||||
/// </summary>
|
||||
public static object CreateOperationsDelta(object[] pendingOperations)
|
||||
{
|
||||
return new
|
||||
{
|
||||
operations = new
|
||||
{
|
||||
pending = pendingOperations
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates client state revision and returns conflict response if mismatched.
|
||||
/// Returns null if validation passes.
|
||||
/// </summary>
|
||||
public static object ValidateClientRevision(int? clientRev)
|
||||
{
|
||||
if (!clientRev.HasValue)
|
||||
{
|
||||
// No client revision provided - accept but don't enforce
|
||||
return null;
|
||||
}
|
||||
|
||||
int currentRev = GetCurrentRevision();
|
||||
if (clientRev.Value != currentRev)
|
||||
{
|
||||
// State mismatch - return 409-like conflict response with fresh state
|
||||
return new
|
||||
{
|
||||
success = false,
|
||||
message = $"State revision mismatch. Client: {clientRev.Value}, Server: {currentRev}. Please refresh state.",
|
||||
code = "state_revision_conflict",
|
||||
state = BuildFullStateAndIncrement()
|
||||
};
|
||||
}
|
||||
|
||||
return null; // Validation passed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates client state revision from JObject params.
|
||||
/// Returns null if validation passes, error response if conflict.
|
||||
/// </summary>
|
||||
public static object ValidateClientRevisionFromParams(Codely.Newtonsoft.Json.Linq.JObject @params)
|
||||
{
|
||||
int? clientRev = @params?["client_state_rev"]?.ToObject<int?>();
|
||||
return ValidateClientRevision(clientRev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple state deltas into one combined delta.
|
||||
/// </summary>
|
||||
public static object MergeStateDeltas(params object[] deltas)
|
||||
{
|
||||
if (deltas == null || deltas.Length == 0) return null;
|
||||
if (deltas.Length == 1) return deltas[0];
|
||||
|
||||
// Preserve legacy behavior: if only one non-null delta is provided, return it as-is.
|
||||
int nonNullCount = 0;
|
||||
object single = null;
|
||||
foreach (var d in deltas)
|
||||
{
|
||||
if (d == null) continue;
|
||||
nonNullCount++;
|
||||
single = d;
|
||||
if (nonNullCount > 1) break;
|
||||
}
|
||||
if (nonNullCount == 0) return null;
|
||||
if (nonNullCount == 1) return single;
|
||||
|
||||
var merged = new Dictionary<string, object>();
|
||||
|
||||
foreach (var delta in deltas)
|
||||
{
|
||||
if (delta == null) continue;
|
||||
|
||||
// Prefer a JSON/dictionary representation to avoid reflection issues
|
||||
// (e.g., when a state_delta is already a JObject/JToken).
|
||||
Dictionary<string, object> deltaDict = null;
|
||||
try
|
||||
{
|
||||
// Codely.Newtonsoft.Json.Linq types (JObject / JToken)
|
||||
if (delta is Codely.Newtonsoft.Json.Linq.JObject jObj)
|
||||
{
|
||||
deltaDict = jObj.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
else if (delta is Codely.Newtonsoft.Json.Linq.JToken jTok &&
|
||||
jTok.Type == Codely.Newtonsoft.Json.Linq.JTokenType.Object)
|
||||
{
|
||||
var asObj = jTok as Codely.Newtonsoft.Json.Linq.JObject;
|
||||
deltaDict = (asObj ?? Codely.Newtonsoft.Json.Linq.JObject.FromObject(jTok))
|
||||
.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
else if (delta is IDictionary<string, object> iDict)
|
||||
{
|
||||
deltaDict = new Dictionary<string, object>(iDict);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last resort: serialize arbitrary objects into a JObject then into a dictionary.
|
||||
var obj = Codely.Newtonsoft.Json.Linq.JObject.FromObject(delta);
|
||||
deltaDict = obj.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
deltaDict = null;
|
||||
}
|
||||
|
||||
if (deltaDict != null)
|
||||
{
|
||||
foreach (var kv in deltaDict)
|
||||
{
|
||||
if (kv.Value == null) continue;
|
||||
|
||||
if (merged.ContainsKey(kv.Key))
|
||||
{
|
||||
// Merge nested dictionaries (one level deep, consistent with legacy behavior)
|
||||
var existingDict = merged[kv.Key] as Dictionary<string, object>;
|
||||
var newDict = kv.Value as Dictionary<string, object>;
|
||||
if (existingDict != null && newDict != null)
|
||||
{
|
||||
foreach (var nk in newDict)
|
||||
{
|
||||
existingDict[nk.Key] = nk.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback: reflection-based merge (skip indexer properties to avoid invocation errors)
|
||||
try
|
||||
{
|
||||
var props = delta.GetType().GetProperties();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
if (prop.GetIndexParameters().Length > 0) continue;
|
||||
|
||||
object value = null;
|
||||
try { value = prop.GetValue(delta); } catch { continue; }
|
||||
if (value == null) continue;
|
||||
|
||||
if (merged.ContainsKey(prop.Name))
|
||||
{
|
||||
// Merge nested dictionaries
|
||||
if (merged[prop.Name] is Dictionary<string, object> existingDict &&
|
||||
value is Dictionary<string, object> newDict)
|
||||
{
|
||||
foreach (var kv in newDict)
|
||||
{
|
||||
existingDict[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[prop.Name] = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[prop.Name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore merge errors from unexpected delta shapes.
|
||||
}
|
||||
}
|
||||
|
||||
return merged.Count > 0 ? merged : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e6177725a55072419d7584603153d01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user