753 lines
28 KiB
C#
753 lines
28 KiB
C#
using System;
|
|
using System.IO;
|
|
using Unity.Netcode;
|
|
using Unity.Netcode.Components;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
public enum ItemCreationDefinitionKind
|
|
{
|
|
Generic,
|
|
Material,
|
|
Part,
|
|
Rocket,
|
|
Usable
|
|
}
|
|
|
|
public enum ItemCreationMovementKind
|
|
{
|
|
Carry,
|
|
Drag,
|
|
Hand,
|
|
Static
|
|
}
|
|
|
|
public sealed class ItemCreationWindow : EditorWindow
|
|
{
|
|
private const string DefaultDefinitionFolder = "Assets/CONTENT/Game/Data/Items";
|
|
private const string DefaultPrefabFolder = "Assets/CONTENT/Game/Prefabs/Items";
|
|
private const string DefaultCatalogPath =
|
|
"Assets/CONTENT/Game/Data/Items/ItemCatalog.asset";
|
|
private const string DefaultNetworkPrefabsPath = "Assets/DefaultNetworkPrefabs.asset";
|
|
|
|
[SerializeField] private string assetName = "NewItem";
|
|
[SerializeField] private string itemId = "new-item";
|
|
[SerializeField] private string displayName = "New Item";
|
|
[SerializeField] private ItemDefinition existingDefinition;
|
|
[SerializeField] private ItemCreationDefinitionKind definitionKind;
|
|
[SerializeField] private ItemCreationMovementKind movementKind =
|
|
ItemCreationMovementKind.Carry;
|
|
|
|
[SerializeField] private GameObject existingWorldPrefab;
|
|
[SerializeField] private GameObject visualPrefab;
|
|
[SerializeField] private string definitionFolder = DefaultDefinitionFolder;
|
|
[SerializeField] private string prefabFolder = DefaultPrefabFolder;
|
|
|
|
[SerializeField] private string partTypeId = "part";
|
|
[SerializeField] private PartProcessingState partProcessingState;
|
|
|
|
[SerializeField] private string rocketTypeId = "rocket";
|
|
[SerializeField] private float criticalImpactSpeed = 7f;
|
|
|
|
[SerializeField] private float dragWeight = 35f;
|
|
[SerializeField] private MonoScript handItemBehaviour;
|
|
|
|
[SerializeField] private bool addFurnaceMetalSource;
|
|
[SerializeField] private float furnaceMetalAmount = 25f;
|
|
|
|
[SerializeField] private ItemCatalog catalog;
|
|
[SerializeField] private NetworkPrefabsList networkPrefabsList;
|
|
|
|
[MenuItem("FORT-BO/Items/Item Creator")]
|
|
private static void Open()
|
|
{
|
|
ItemCreationWindow window = GetWindow<ItemCreationWindow>("Item Creator");
|
|
window.minSize = new Vector2(470f, 650f);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (catalog == null)
|
|
catalog = AssetDatabase.LoadAssetAtPath<ItemCatalog>(DefaultCatalogPath);
|
|
|
|
if (networkPrefabsList == null)
|
|
{
|
|
networkPrefabsList =
|
|
AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(DefaultNetworkPrefabsPath);
|
|
}
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
EditorGUILayout.LabelField("Identity", EditorStyles.boldLabel);
|
|
existingDefinition = (ItemDefinition)EditorGUILayout.ObjectField(
|
|
"Existing Definition",
|
|
existingDefinition,
|
|
typeof(ItemDefinition),
|
|
false);
|
|
using (new EditorGUI.DisabledScope(existingDefinition != null))
|
|
{
|
|
assetName = EditorGUILayout.TextField("Asset Name", assetName);
|
|
itemId = EditorGUILayout.TextField("Item Id", itemId);
|
|
displayName = EditorGUILayout.TextField("Display Name", displayName);
|
|
definitionKind = (ItemCreationDefinitionKind)EditorGUILayout.EnumPopup(
|
|
"Definition Kind",
|
|
definitionKind);
|
|
}
|
|
|
|
if (existingDefinition != null)
|
|
{
|
|
EditorGUILayout.LabelField(
|
|
"Definition Kind",
|
|
GetDefinitionKind(existingDefinition).ToString());
|
|
}
|
|
|
|
movementKind = (ItemCreationMovementKind)EditorGUILayout.EnumPopup(
|
|
"Movement Kind",
|
|
movementKind);
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Prefab", EditorStyles.boldLabel);
|
|
existingWorldPrefab = (GameObject)EditorGUILayout.ObjectField(
|
|
"Existing World Prefab",
|
|
existingWorldPrefab,
|
|
typeof(GameObject),
|
|
false);
|
|
|
|
bool hasWorldPrefab = ResolveExistingWorldPrefab() != null;
|
|
using (new EditorGUI.DisabledScope(hasWorldPrefab))
|
|
{
|
|
visualPrefab = (GameObject)EditorGUILayout.ObjectField(
|
|
"Visual Prefab",
|
|
visualPrefab,
|
|
typeof(GameObject),
|
|
false);
|
|
prefabFolder = EditorGUILayout.TextField("Prefab Folder", prefabFolder);
|
|
}
|
|
|
|
EditorGUILayout.HelpBox(
|
|
hasWorldPrefab
|
|
? "The existing prefab is configured in place. Existing components are preserved."
|
|
: "A new network prefab is created. Visual Prefab is added as a child and may be empty.",
|
|
MessageType.Info);
|
|
|
|
if (existingDefinition == null)
|
|
DrawDefinitionSpecificFields();
|
|
DrawMovementSpecificFields();
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Optional capabilities", EditorStyles.boldLabel);
|
|
addFurnaceMetalSource = EditorGUILayout.Toggle(
|
|
"Furnace Metal Source",
|
|
addFurnaceMetalSource);
|
|
if (addFurnaceMetalSource)
|
|
{
|
|
furnaceMetalAmount = EditorGUILayout.FloatField(
|
|
"Metal Amount",
|
|
furnaceMetalAmount);
|
|
}
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Registration", EditorStyles.boldLabel);
|
|
using (new EditorGUI.DisabledScope(existingDefinition != null))
|
|
{
|
|
definitionFolder = EditorGUILayout.TextField(
|
|
"Definition Folder",
|
|
definitionFolder);
|
|
}
|
|
catalog = (ItemCatalog)EditorGUILayout.ObjectField(
|
|
"Item Catalog",
|
|
catalog,
|
|
typeof(ItemCatalog),
|
|
false);
|
|
networkPrefabsList = (NetworkPrefabsList)EditorGUILayout.ObjectField(
|
|
"Network Prefabs List",
|
|
networkPrefabsList,
|
|
typeof(NetworkPrefabsList),
|
|
false);
|
|
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.HelpBox(
|
|
"Definition Kind describes what the item is. Movement Kind describes how its prefab " +
|
|
"is handled. They are intentionally independent.",
|
|
MessageType.None);
|
|
|
|
using (new EditorGUI.DisabledScope(EditorApplication.isPlayingOrWillChangePlaymode))
|
|
{
|
|
string buttonLabel = existingDefinition == null
|
|
? "Create and Register Item"
|
|
: "Configure and Register Item";
|
|
if (GUILayout.Button(buttonLabel, GUILayout.Height(34f)))
|
|
CreateAndRegisterItem();
|
|
}
|
|
}
|
|
|
|
private void DrawDefinitionSpecificFields()
|
|
{
|
|
switch (definitionKind)
|
|
{
|
|
case ItemCreationDefinitionKind.Part:
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Part", EditorStyles.boldLabel);
|
|
partTypeId = EditorGUILayout.TextField("Part Type Id", partTypeId);
|
|
partProcessingState = (PartProcessingState)EditorGUILayout.EnumPopup(
|
|
"Processing State",
|
|
partProcessingState);
|
|
break;
|
|
|
|
case ItemCreationDefinitionKind.Rocket:
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Rocket", EditorStyles.boldLabel);
|
|
rocketTypeId = EditorGUILayout.TextField("Rocket Type Id", rocketTypeId);
|
|
criticalImpactSpeed = EditorGUILayout.FloatField(
|
|
"Critical Impact Speed",
|
|
criticalImpactSpeed);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void DrawMovementSpecificFields()
|
|
{
|
|
switch (movementKind)
|
|
{
|
|
case ItemCreationMovementKind.Drag:
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Drag", EditorStyles.boldLabel);
|
|
dragWeight = EditorGUILayout.FloatField("Weight", dragWeight);
|
|
break;
|
|
|
|
case ItemCreationMovementKind.Hand:
|
|
EditorGUILayout.Space();
|
|
EditorGUILayout.LabelField("Hand", EditorStyles.boldLabel);
|
|
handItemBehaviour = (MonoScript)EditorGUILayout.ObjectField(
|
|
"Custom Hand Interactable",
|
|
handItemBehaviour,
|
|
typeof(MonoScript),
|
|
false);
|
|
EditorGUILayout.HelpBox(
|
|
"Leave empty to use NetworkUsableHandItemInteractable. Add a separate " +
|
|
"IHandItemUseAction component for the actual effect.",
|
|
MessageType.Info);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void CreateAndRegisterItem()
|
|
{
|
|
try
|
|
{
|
|
ValidateInput();
|
|
ItemDefinition definition;
|
|
string definitionPath;
|
|
string safeAssetName;
|
|
if (existingDefinition != null)
|
|
{
|
|
definition = existingDefinition;
|
|
definitionPath = AssetDatabase.GetAssetPath(definition);
|
|
safeAssetName = SanitizeFileName(definition.name);
|
|
}
|
|
else
|
|
{
|
|
EnsureFolder(definitionFolder);
|
|
safeAssetName = SanitizeFileName(assetName.Trim());
|
|
definitionPath = $"{definitionFolder}/{safeAssetName}.asset";
|
|
if (AssetDatabase.LoadMainAssetAtPath(definitionPath) != null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Asset already exists: {definitionPath}");
|
|
}
|
|
|
|
EnsureUniqueItemId(itemId);
|
|
definition = CreateDefinition(definitionPath);
|
|
}
|
|
|
|
GameObject worldPrefab = ConfigureOrCreateWorldPrefab(safeAssetName, definition);
|
|
|
|
SetObjectReference(
|
|
definition,
|
|
"worldPrefab",
|
|
worldPrefab.GetComponent<NetworkObject>());
|
|
RegisterInCatalog(definition);
|
|
RegisterNetworkPrefab(worldPrefab);
|
|
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Selection.activeObject = definition;
|
|
EditorGUIUtility.PingObject(worldPrefab);
|
|
Debug.Log(
|
|
$"Created '{definition.DisplayName}': {definitionPath} + " +
|
|
$"{AssetDatabase.GetAssetPath(worldPrefab)}.",
|
|
definition);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogException(exception);
|
|
EditorUtility.DisplayDialog("Item creation failed", exception.Message, "OK");
|
|
}
|
|
}
|
|
|
|
private void ValidateInput()
|
|
{
|
|
if (existingDefinition == null && string.IsNullOrWhiteSpace(assetName))
|
|
throw new InvalidOperationException("Asset Name is required.");
|
|
|
|
if (existingDefinition == null && string.IsNullOrWhiteSpace(itemId))
|
|
throw new InvalidOperationException("Item Id is required.");
|
|
|
|
if (existingDefinition == null &&
|
|
(string.IsNullOrWhiteSpace(definitionFolder) ||
|
|
!definitionFolder.StartsWith("Assets", StringComparison.Ordinal)))
|
|
{
|
|
throw new InvalidOperationException("Definition Folder must be inside Assets.");
|
|
}
|
|
|
|
GameObject resolvedWorldPrefab = ResolveExistingWorldPrefab();
|
|
if (resolvedWorldPrefab != null &&
|
|
!PrefabUtility.IsPartOfPrefabAsset(resolvedWorldPrefab))
|
|
{
|
|
throw new InvalidOperationException("Existing World Prefab must be a prefab asset.");
|
|
}
|
|
|
|
if (resolvedWorldPrefab != null)
|
|
ValidateExistingPrefabMovement(resolvedWorldPrefab);
|
|
|
|
if (ResolveExistingWorldPrefab() == null &&
|
|
(string.IsNullOrWhiteSpace(prefabFolder) ||
|
|
!prefabFolder.StartsWith("Assets", StringComparison.Ordinal)))
|
|
{
|
|
throw new InvalidOperationException("Prefab Folder must be inside Assets.");
|
|
}
|
|
|
|
if (movementKind == ItemCreationMovementKind.Hand)
|
|
ResolveHandItemBehaviourType();
|
|
|
|
if (EffectiveDefinitionKind == ItemCreationDefinitionKind.Rocket &&
|
|
movementKind is not ItemCreationMovementKind.Carry and
|
|
not ItemCreationMovementKind.Drag)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A rocket assembled by the workstation must use Carry or Drag movement.");
|
|
}
|
|
|
|
if (ResolveExistingWorldPrefab() == null && visualPrefab != null &&
|
|
(visualPrefab.GetComponentInChildren<NetworkObject>(true) != null ||
|
|
visualPrefab.GetComponentInChildren<Rigidbody>(true) != null))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Visual Prefab must contain visuals/colliders only, without NetworkObject or Rigidbody.");
|
|
}
|
|
|
|
if (existingDefinition == null && definitionKind == ItemCreationDefinitionKind.Part &&
|
|
string.IsNullOrWhiteSpace(partTypeId))
|
|
{
|
|
throw new InvalidOperationException("Part Type Id is required for a part.");
|
|
}
|
|
|
|
if (existingDefinition == null && definitionKind == ItemCreationDefinitionKind.Rocket &&
|
|
string.IsNullOrWhiteSpace(rocketTypeId))
|
|
{
|
|
throw new InvalidOperationException("Rocket Type Id is required for a rocket.");
|
|
}
|
|
}
|
|
|
|
private void ValidateExistingPrefabMovement(GameObject prefabAsset)
|
|
{
|
|
string prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
|
|
GameObject root = PrefabUtility.LoadPrefabContents(prefabPath);
|
|
try
|
|
{
|
|
EnsureSingleMovementBehaviour(root);
|
|
}
|
|
finally
|
|
{
|
|
PrefabUtility.UnloadPrefabContents(root);
|
|
}
|
|
}
|
|
|
|
private GameObject ResolveExistingWorldPrefab()
|
|
{
|
|
if (existingWorldPrefab != null)
|
|
return existingWorldPrefab;
|
|
|
|
return existingDefinition != null && existingDefinition.WorldPrefab != null
|
|
? existingDefinition.WorldPrefab.gameObject
|
|
: null;
|
|
}
|
|
|
|
private ItemCreationDefinitionKind EffectiveDefinitionKind =>
|
|
existingDefinition != null ? GetDefinitionKind(existingDefinition) : definitionKind;
|
|
|
|
private static ItemCreationDefinitionKind GetDefinitionKind(ItemDefinition definition) =>
|
|
definition switch
|
|
{
|
|
MaterialItemDefinition => ItemCreationDefinitionKind.Material,
|
|
PartItemDefinition => ItemCreationDefinitionKind.Part,
|
|
RocketItemDefinition => ItemCreationDefinitionKind.Rocket,
|
|
UsableItemDefinition => ItemCreationDefinitionKind.Usable,
|
|
_ => ItemCreationDefinitionKind.Generic
|
|
};
|
|
|
|
private ItemDefinition CreateDefinition(string path)
|
|
{
|
|
ItemDefinition definition = definitionKind switch
|
|
{
|
|
ItemCreationDefinitionKind.Material =>
|
|
CreateInstance<MaterialItemDefinition>(),
|
|
ItemCreationDefinitionKind.Part =>
|
|
CreateInstance<PartItemDefinition>(),
|
|
ItemCreationDefinitionKind.Rocket =>
|
|
CreateInstance<RocketItemDefinition>(),
|
|
ItemCreationDefinitionKind.Usable =>
|
|
CreateInstance<UsableItemDefinition>(),
|
|
_ => CreateInstance<ItemDefinition>()
|
|
};
|
|
|
|
definition.name = assetName.Trim();
|
|
AssetDatabase.CreateAsset(definition, path);
|
|
SetString(definition, "itemId", NormalizeId(itemId));
|
|
SetString(definition, "displayName", displayName?.Trim());
|
|
|
|
if (definition is PartItemDefinition)
|
|
{
|
|
SetString(definition, "partTypeId", NormalizeId(partTypeId));
|
|
SetInt(definition, "processingState", (int)partProcessingState);
|
|
}
|
|
else if (definition is RocketItemDefinition)
|
|
{
|
|
SetString(definition, "rocketTypeId", NormalizeId(rocketTypeId));
|
|
SetFloat(definition, "criticalImpactSpeed", Mathf.Max(0.1f, criticalImpactSpeed));
|
|
}
|
|
|
|
return definition;
|
|
}
|
|
|
|
private GameObject ConfigureOrCreateWorldPrefab(
|
|
string safeAssetName,
|
|
ItemDefinition definition)
|
|
{
|
|
string prefabPath;
|
|
GameObject prefabAsset = ResolveExistingWorldPrefab();
|
|
if (prefabAsset != null)
|
|
{
|
|
prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
|
|
}
|
|
else
|
|
{
|
|
EnsureFolder(prefabFolder);
|
|
prefabPath = $"{prefabFolder}/{safeAssetName}.prefab";
|
|
if (AssetDatabase.LoadMainAssetAtPath(prefabPath) != null)
|
|
throw new InvalidOperationException($"Prefab already exists: {prefabPath}");
|
|
|
|
CreateInitialPrefab(prefabPath, safeAssetName);
|
|
}
|
|
|
|
GameObject root = PrefabUtility.LoadPrefabContents(prefabPath);
|
|
try
|
|
{
|
|
ConfigurePrefabRoot(root, definition);
|
|
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
|
}
|
|
finally
|
|
{
|
|
PrefabUtility.UnloadPrefabContents(root);
|
|
}
|
|
|
|
return AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
|
}
|
|
|
|
private void CreateInitialPrefab(string prefabPath, string safeAssetName)
|
|
{
|
|
GameObject root = new(safeAssetName);
|
|
try
|
|
{
|
|
if (visualPrefab != null)
|
|
{
|
|
GameObject visual = PrefabUtility.InstantiatePrefab(visualPrefab) as GameObject;
|
|
if (visual == null)
|
|
visual = Instantiate(visualPrefab);
|
|
|
|
visual.name = "Visual";
|
|
visual.transform.SetParent(root.transform, false);
|
|
}
|
|
|
|
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
|
}
|
|
finally
|
|
{
|
|
DestroyImmediate(root);
|
|
}
|
|
}
|
|
|
|
private void ConfigurePrefabRoot(GameObject root, ItemDefinition definition)
|
|
{
|
|
EnsureSingleMovementBehaviour(root);
|
|
|
|
NetworkObject networkObject = GetOrAdd<NetworkObject>(root);
|
|
NetworkTransform networkTransform = GetOrAdd<NetworkTransform>(root);
|
|
networkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server;
|
|
networkTransform.UseUnreliableDeltas = true;
|
|
|
|
NetworkItem networkItem = GetOrAdd<NetworkItem>(root);
|
|
SetObjectReference(networkItem, "definition", definition);
|
|
|
|
bool needsPhysics = movementKind != ItemCreationMovementKind.Static ||
|
|
EffectiveDefinitionKind == ItemCreationDefinitionKind.Rocket;
|
|
Rigidbody body = needsPhysics ? GetOrAdd<Rigidbody>(root) : root.GetComponent<Rigidbody>();
|
|
if (body != null)
|
|
{
|
|
body.interpolation = RigidbodyInterpolation.Interpolate;
|
|
body.collisionDetectionMode = CollisionDetectionMode.Continuous;
|
|
}
|
|
|
|
if (needsPhysics)
|
|
{
|
|
NetworkRigidbody networkRigidbody = GetOrAdd<NetworkRigidbody>(root);
|
|
networkRigidbody.UseRigidBodyForMotion = true;
|
|
EnsureCollider(root);
|
|
}
|
|
|
|
switch (movementKind)
|
|
{
|
|
case ItemCreationMovementKind.Carry:
|
|
GetOrAdd<NetworkCarryableInteractable>(root);
|
|
if (body != null)
|
|
body.mass = 1f;
|
|
break;
|
|
|
|
case ItemCreationMovementKind.Drag:
|
|
NetworkDraggableInteractable draggable =
|
|
GetOrAdd<NetworkDraggableInteractable>(root);
|
|
float safeWeight = Mathf.Max(0.1f, dragWeight);
|
|
SetFloat(draggable, "weight", safeWeight);
|
|
if (body != null)
|
|
body.mass = safeWeight;
|
|
break;
|
|
|
|
case ItemCreationMovementKind.Hand:
|
|
Type handBehaviourType = ResolveHandItemBehaviourType();
|
|
if (root.GetComponent(handBehaviourType) == null)
|
|
root.AddComponent(handBehaviourType);
|
|
if (body != null)
|
|
body.mass = 0.5f;
|
|
break;
|
|
}
|
|
|
|
if (EffectiveDefinitionKind == ItemCreationDefinitionKind.Rocket)
|
|
{
|
|
GetOrAdd<NetworkRocketItem>(root);
|
|
GetOrAdd<RocketItemView>(root);
|
|
}
|
|
|
|
if (addFurnaceMetalSource)
|
|
{
|
|
FurnaceMetalSource source = GetOrAdd<FurnaceMetalSource>(root);
|
|
SetFloat(source, "metalAmount", Mathf.Max(0.01f, furnaceMetalAmount));
|
|
}
|
|
|
|
EditorUtility.SetDirty(networkObject);
|
|
}
|
|
|
|
private void EnsureSingleMovementBehaviour(GameObject root)
|
|
{
|
|
bool hasCarry = root.GetComponent<NetworkCarryableInteractable>() != null;
|
|
bool hasDrag = root.GetComponent<NetworkDraggableInteractable>() != null;
|
|
bool hasHand = root.GetComponent<NetworkHandItemInteractable>() != null;
|
|
int existingCount = (hasCarry ? 1 : 0) + (hasDrag ? 1 : 0) + (hasHand ? 1 : 0);
|
|
|
|
if (existingCount > 1)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The prefab already contains more than one movement behaviour.");
|
|
}
|
|
|
|
bool compatible = existingCount == 0 ||
|
|
(movementKind == ItemCreationMovementKind.Carry && hasCarry) ||
|
|
(movementKind == ItemCreationMovementKind.Drag && hasDrag) ||
|
|
(movementKind == ItemCreationMovementKind.Hand && hasHand);
|
|
if (!compatible)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The prefab already has a different movement behaviour. Remove or change it manually first.");
|
|
}
|
|
|
|
if (movementKind == ItemCreationMovementKind.Hand && hasHand)
|
|
{
|
|
NetworkHandItemInteractable existingHand =
|
|
root.GetComponent<NetworkHandItemInteractable>();
|
|
if (existingHand.GetType() != ResolveHandItemBehaviourType())
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"The prefab already uses {existingHand.GetType().Name}, but another hand " +
|
|
"behaviour was selected.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private Type ResolveHandItemBehaviourType()
|
|
{
|
|
if (handItemBehaviour == null)
|
|
return typeof(NetworkUsableHandItemInteractable);
|
|
|
|
Type type = handItemBehaviour.GetClass();
|
|
if (type == null || type.IsAbstract ||
|
|
!typeof(NetworkHandItemInteractable).IsAssignableFrom(type))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Use Behaviour must be a non-abstract NetworkHandItemInteractable script.");
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
private void EnsureUniqueItemId(string candidateId)
|
|
{
|
|
string normalizedId = NormalizeId(candidateId);
|
|
foreach (string guid in AssetDatabase.FindAssets("t:ItemDefinition"))
|
|
{
|
|
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|
ItemDefinition definition = AssetDatabase.LoadAssetAtPath<ItemDefinition>(path);
|
|
if (definition != null && definition.ItemId == normalizedId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Item Id '{normalizedId}' is already used by {path}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RegisterInCatalog(ItemDefinition definition)
|
|
{
|
|
if (catalog == null)
|
|
return;
|
|
|
|
SerializedObject serializedCatalog = new(catalog);
|
|
SerializedProperty items = serializedCatalog.FindProperty("items");
|
|
for (int index = 0; index < items.arraySize; index++)
|
|
{
|
|
if (items.GetArrayElementAtIndex(index).objectReferenceValue == definition)
|
|
return;
|
|
}
|
|
|
|
int newIndex = items.arraySize;
|
|
items.InsertArrayElementAtIndex(newIndex);
|
|
items.GetArrayElementAtIndex(newIndex).objectReferenceValue = definition;
|
|
serializedCatalog.ApplyModifiedPropertiesWithoutUndo();
|
|
EditorUtility.SetDirty(catalog);
|
|
}
|
|
|
|
private void RegisterNetworkPrefab(GameObject prefab)
|
|
{
|
|
if (networkPrefabsList == null || networkPrefabsList.Contains(prefab))
|
|
return;
|
|
|
|
networkPrefabsList.Add(new NetworkPrefab { Prefab = prefab });
|
|
EditorUtility.SetDirty(networkPrefabsList);
|
|
}
|
|
|
|
private static T GetOrAdd<T>(GameObject gameObject) where T : Component =>
|
|
gameObject.TryGetComponent(out T component) ? component : gameObject.AddComponent<T>();
|
|
|
|
private static void EnsureCollider(GameObject root)
|
|
{
|
|
if (root.GetComponentInChildren<Collider>(true) != null)
|
|
return;
|
|
|
|
BoxCollider collider = root.AddComponent<BoxCollider>();
|
|
if (!TryCalculateLocalRendererBounds(root, out Bounds bounds))
|
|
return;
|
|
|
|
collider.center = bounds.center;
|
|
collider.size = bounds.size;
|
|
}
|
|
|
|
private static bool TryCalculateLocalRendererBounds(GameObject root, out Bounds bounds)
|
|
{
|
|
Renderer[] renderers = root.GetComponentsInChildren<Renderer>(true);
|
|
if (renderers.Length == 0)
|
|
{
|
|
bounds = new Bounds(Vector3.zero, Vector3.one);
|
|
return false;
|
|
}
|
|
|
|
bool initialized = false;
|
|
bounds = default;
|
|
foreach (Renderer renderer in renderers)
|
|
{
|
|
Bounds worldBounds = renderer.bounds;
|
|
Vector3 center = root.transform.InverseTransformPoint(worldBounds.center);
|
|
Vector3 extents = worldBounds.extents;
|
|
for (int x = -1; x <= 1; x += 2)
|
|
for (int y = -1; y <= 1; y += 2)
|
|
for (int z = -1; z <= 1; z += 2)
|
|
{
|
|
Vector3 worldCorner = worldBounds.center +
|
|
Vector3.Scale(extents, new Vector3(x, y, z));
|
|
Vector3 localCorner = root.transform.InverseTransformPoint(worldCorner);
|
|
if (!initialized)
|
|
{
|
|
bounds = new Bounds(center, Vector3.zero);
|
|
initialized = true;
|
|
}
|
|
|
|
bounds.Encapsulate(localCorner);
|
|
}
|
|
}
|
|
|
|
return initialized;
|
|
}
|
|
|
|
private static void EnsureFolder(string folderPath)
|
|
{
|
|
string normalized = folderPath.Replace('\\', '/').TrimEnd('/');
|
|
if (AssetDatabase.IsValidFolder(normalized))
|
|
return;
|
|
|
|
string parent = Path.GetDirectoryName(normalized)?.Replace('\\', '/');
|
|
string folderName = Path.GetFileName(normalized);
|
|
if (string.IsNullOrEmpty(parent) || string.IsNullOrEmpty(folderName))
|
|
throw new InvalidOperationException($"Invalid folder path: {folderPath}");
|
|
|
|
EnsureFolder(parent);
|
|
AssetDatabase.CreateFolder(parent, folderName);
|
|
}
|
|
|
|
private static string NormalizeId(string value) =>
|
|
value.Trim().ToLowerInvariant().Replace(' ', '-');
|
|
|
|
private static string SanitizeFileName(string value)
|
|
{
|
|
foreach (char invalidCharacter in Path.GetInvalidFileNameChars())
|
|
value = value.Replace(invalidCharacter, '-');
|
|
return value;
|
|
}
|
|
|
|
private static void SetString(UnityEngine.Object target, string propertyName, string value) =>
|
|
Edit(target, propertyName, property => property.stringValue = value);
|
|
|
|
private static void SetFloat(UnityEngine.Object target, string propertyName, float value) =>
|
|
Edit(target, propertyName, property => property.floatValue = value);
|
|
|
|
private static void SetInt(UnityEngine.Object target, string propertyName, int value) =>
|
|
Edit(target, propertyName, property => property.intValue = value);
|
|
|
|
private static void SetObjectReference(
|
|
UnityEngine.Object target,
|
|
string propertyName,
|
|
UnityEngine.Object value) =>
|
|
Edit(target, propertyName, property => property.objectReferenceValue = value);
|
|
|
|
private static void Edit(
|
|
UnityEngine.Object target,
|
|
string propertyName,
|
|
Action<SerializedProperty> edit)
|
|
{
|
|
SerializedObject serializedObject = new(target);
|
|
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
|
if (property == null)
|
|
throw new MissingFieldException(target.GetType().Name, propertyName);
|
|
|
|
edit(property);
|
|
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
EditorUtility.SetDirty(target);
|
|
}
|
|
}
|