78bfdf2828
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
271 lines
11 KiB
C#
271 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using Ashwild.Network;
|
|
|
|
namespace Ashwild.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// The one menu item that keeps the baked ids of scene WorldObjects — Pickables and Harvestables
|
|
/// alike — correct. The id is the object's identity across the network: every client loads the same
|
|
/// scene, so the same id must designate the same object on every machine.
|
|
///
|
|
/// It does the whole job in one click, in the order the three steps have to run:
|
|
/// strip the ids that ended up baked in prefab <i>assets</i>, give an id to every scene object that
|
|
/// lacks a valid one, then verify the result and report it. Splitting them into separate menu items
|
|
/// only creates ways to run them in the wrong order, or to forget one.
|
|
///
|
|
/// Assignment is <b>stable and additive</b>: ids already baked into the scene are kept, and only the
|
|
/// objects that need one (never assigned, or colliding with another object) are given the next free
|
|
/// number. An earlier version renumbered everything from zero on each run, ordered by
|
|
/// <c>InstanceID</c> — an order that is not stable between editor sessions — so a single new bush
|
|
/// reshuffled every id in the scene and produced an unreviewable diff.
|
|
///
|
|
/// Ids belong to a scene, never to a prefab asset: an id baked into a prefab is inherited by every
|
|
/// instance placed from it, so all of them collide on that one id from the moment they are created —
|
|
/// which reads, in game, as objects that play their interaction and give nothing.
|
|
/// </summary>
|
|
public static class WorldObjectIdAssigner
|
|
{
|
|
#region Constants
|
|
|
|
private const string LogPrefix = "[WorldObjectIds]";
|
|
|
|
/// <summary>
|
|
/// How many fixed objects are named in the summary before it collapses into a count — a freshly
|
|
/// scattered zone can need hundreds of ids, and an unreadable console helps nobody.
|
|
/// </summary>
|
|
private const int MaxNamesInSummary = 8;
|
|
|
|
#endregion
|
|
|
|
#region Menu
|
|
|
|
/// <summary>
|
|
/// Clears prefab-baked ids, assigns an id to every scene WorldObject that lacks a valid one, then
|
|
/// validates the outcome. Reports what changed and, if anything is still wrong, logs each culprit
|
|
/// with its object as context so clicking the message selects it in the hierarchy.
|
|
/// </summary>
|
|
[MenuItem("Tools/Ashwild/Setup World Object IDs")]
|
|
public static void Setup()
|
|
{
|
|
if (PrefabStageUtility.GetCurrentPrefabStage() != null)
|
|
{
|
|
Debug.LogError($"{LogPrefix} Ids are per scene, not per prefab. Close the prefab stage and run this " +
|
|
"from the scene that contains the objects.");
|
|
return;
|
|
}
|
|
|
|
int clearedPrefabs = ClearPrefabIds();
|
|
|
|
if (!TryCollect(out List<WorldObject> objects)) return;
|
|
|
|
List<WorldObject> assigned = AssignMissingIds(objects);
|
|
if (assigned.Count > 0) MarkScenesDirty(objects);
|
|
|
|
Report(objects, clearedPrefabs, assigned);
|
|
Validate(objects);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Steps
|
|
|
|
/// <summary>
|
|
/// Strips the id baked into WorldObject prefab <i>assets</i>, so newly placed instances start
|
|
/// unassigned instead of all inheriting the same id. Scene instances keep their own id, which is
|
|
/// stored as a prefab override and left untouched — but an instance that had no override now reads
|
|
/// as unassigned, which is exactly why the assignment step has to run after this one.
|
|
/// Returns how many prefabs were cleaned.
|
|
/// </summary>
|
|
private static int ClearPrefabIds()
|
|
{
|
|
string[] guids = AssetDatabase.FindAssets("t:Prefab");
|
|
int cleared = 0;
|
|
|
|
foreach (string guid in guids)
|
|
{
|
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(guid));
|
|
if (prefab == null) continue;
|
|
|
|
bool changed = false;
|
|
foreach (WorldObject obj in prefab.GetComponentsInChildren<WorldObject>(true))
|
|
{
|
|
if (obj.Id < 0) continue;
|
|
WriteId(obj, -1);
|
|
changed = true;
|
|
}
|
|
|
|
if (!changed) continue;
|
|
|
|
EditorUtility.SetDirty(prefab);
|
|
cleared++;
|
|
}
|
|
|
|
if (cleared > 0) AssetDatabase.SaveAssets();
|
|
return cleared;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gives the next free id to every object that has none or that duplicates an id already taken,
|
|
/// leaving valid ids untouched. Returns the objects that were changed.
|
|
/// </summary>
|
|
private static List<WorldObject> AssignMissingIds(List<WorldObject> objects)
|
|
{
|
|
HashSet<int> taken = new HashSet<int>();
|
|
List<WorldObject> assigned = new List<WorldObject>();
|
|
int next = 0;
|
|
|
|
foreach (WorldObject obj in objects)
|
|
{
|
|
if (obj.Id >= 0 && taken.Add(obj.Id)) continue;
|
|
|
|
while (!taken.Add(next)) next++;
|
|
WriteId(obj, next);
|
|
assigned.Add(obj);
|
|
}
|
|
|
|
return assigned;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Re-checks the ids after the fact and logs every remaining problem against its object. Nothing
|
|
/// should ever be reported here — it is the proof that the run actually worked, rather than a
|
|
/// summary claiming it did.
|
|
/// </summary>
|
|
private static void Validate(List<WorldObject> objects)
|
|
{
|
|
Dictionary<int, WorldObject> byId = new Dictionary<int, WorldObject>();
|
|
int problems = 0;
|
|
|
|
foreach (WorldObject obj in objects)
|
|
{
|
|
if (obj.Id < 0)
|
|
{
|
|
problems++;
|
|
Debug.LogError($"{LogPrefix} '{GetPath(obj)}' still has no id — it will not be interactable in game.", obj);
|
|
continue;
|
|
}
|
|
|
|
if (byId.TryGetValue(obj.Id, out WorldObject other))
|
|
{
|
|
problems++;
|
|
Debug.LogError($"{LogPrefix} '{GetPath(obj)}' still shares id {obj.Id} with '{GetPath(other)}' — " +
|
|
"using one silently disables the other.", obj);
|
|
continue;
|
|
}
|
|
|
|
byId.Add(obj.Id, obj);
|
|
}
|
|
|
|
if (problems > 0)
|
|
Debug.LogError($"{LogPrefix} {problems} id problem(s) survived the setup — this should not happen; " +
|
|
"check the errors above.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs what the run changed, in one line, and reminds to save when the scene was touched.
|
|
/// </summary>
|
|
private static void Report(List<WorldObject> objects, int clearedPrefabs, List<WorldObject> assigned)
|
|
{
|
|
string prefabs = clearedPrefabs > 0 ? $"Cleared the baked id of {clearedPrefabs} prefab(s). " : string.Empty;
|
|
|
|
if (assigned.Count == 0)
|
|
{
|
|
Debug.Log($"{LogPrefix} {prefabs}All {objects.Count} scene WorldObject(s) already have a unique id — nothing to assign.");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"{LogPrefix} {prefabs}Assigned an id to {assigned.Count} of {objects.Count} scene WorldObject(s): " +
|
|
$"{Describe(assigned)}. Save the scene to bake them.");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Helpers
|
|
|
|
/// <summary>
|
|
/// Gathers the WorldObjects of the open scenes, in a deterministic order (scene, then hierarchy
|
|
/// path) so two runs walk them the same way and hand out the same ids.
|
|
/// </summary>
|
|
private static bool TryCollect(out List<WorldObject> objects)
|
|
{
|
|
objects = null;
|
|
|
|
WorldObject[] found = Object.FindObjectsByType<WorldObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
if (found.Length == 0)
|
|
{
|
|
Debug.Log($"{LogPrefix} No WorldObject found in the open scene(s).");
|
|
return false;
|
|
}
|
|
|
|
objects = new List<WorldObject>(found);
|
|
objects.Sort((a, b) =>
|
|
{
|
|
int byScene = string.CompareOrdinal(a.gameObject.scene.path, b.gameObject.scene.path);
|
|
return byScene != 0 ? byScene : string.CompareOrdinal(GetPath(a), GetPath(b));
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the id through SerializedObject so the change is recorded as a prefab override on an
|
|
/// instance, rather than silently editing the shared prefab value.
|
|
/// </summary>
|
|
private static void WriteId(WorldObject obj, int id)
|
|
{
|
|
SerializedObject so = new SerializedObject(obj);
|
|
so.FindProperty("id").intValue = id;
|
|
so.ApplyModifiedPropertiesWithoutUndo();
|
|
EditorUtility.SetDirty(obj);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks every scene that owns one of these objects dirty, since the open scenes may be several
|
|
/// (additive loading) and only the touched ones need saving.
|
|
/// </summary>
|
|
private static void MarkScenesDirty(List<WorldObject> objects)
|
|
{
|
|
HashSet<Scene> scenes = new HashSet<Scene>();
|
|
foreach (WorldObject obj in objects)
|
|
scenes.Add(obj.gameObject.scene);
|
|
|
|
foreach (Scene scene in scenes)
|
|
if (scene.IsValid()) EditorSceneManager.MarkSceneDirty(scene);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A short, readable summary of the objects that were given an id.
|
|
/// </summary>
|
|
private static string Describe(List<WorldObject> objects)
|
|
{
|
|
List<string> names = new List<string>();
|
|
for (int i = 0; i < objects.Count && i < MaxNamesInSummary; i++)
|
|
names.Add($"{objects[i].name} (id {objects[i].Id})");
|
|
|
|
if (objects.Count > MaxNamesInSummary) names.Add($"… and {objects.Count - MaxNamesInSummary} more");
|
|
return string.Join(", ", names);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hierarchy path of an object, used both for the deterministic ordering and for error messages
|
|
/// that must point at one specific object among many identically named ones.
|
|
/// </summary>
|
|
private static string GetPath(WorldObject obj)
|
|
{
|
|
string path = obj.name;
|
|
Transform current = obj.transform.parent;
|
|
while (current != null)
|
|
{
|
|
path = $"{current.name}/{path}";
|
|
current = current.parent;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|