Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# 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
This commit is contained in:
@@ -1,47 +1,268 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Ashwild.Network;
|
||||
|
||||
namespace Ashwild.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor tool that assigns a unique, stable id (>= 0) to every scene WorldObject in the open
|
||||
/// scene — Pickables and Harvestables alike, in one shared sequence. The ids are baked into the
|
||||
/// scene asset, so all clients load the exact same mapping (the scene is shared). Run after
|
||||
/// scattering or whenever scene objects are added/removed.
|
||||
/// 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>
|
||||
/// Finds all WorldObjects in the open scene and assigns them sequential unique ids.
|
||||
/// 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/Assign World Object IDs")]
|
||||
public static void Assign()
|
||||
[MenuItem("Tools/Ashwild/Setup World Object IDs")]
|
||||
public static void Setup()
|
||||
{
|
||||
WorldObject[] objects = Object.FindObjectsByType<WorldObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
if (objects.Length == 0)
|
||||
if (PrefabStageUtility.GetCurrentPrefabStage() != null)
|
||||
{
|
||||
Debug.Log("[WorldObjectIdAssigner] No WorldObject found in the open scene.");
|
||||
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;
|
||||
}
|
||||
|
||||
// Deterministic order so re-runs are stable within an editor session.
|
||||
System.Array.Sort(objects, (a, b) => a.GetInstanceID().CompareTo(b.GetInstanceID()));
|
||||
int clearedPrefabs = ClearPrefabIds();
|
||||
|
||||
int next = 0;
|
||||
foreach (WorldObject obj in objects)
|
||||
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)
|
||||
{
|
||||
SerializedObject so = new SerializedObject(obj);
|
||||
so.FindProperty("id").intValue = next++;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(obj);
|
||||
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++;
|
||||
}
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(objects[0].gameObject.scene);
|
||||
Debug.Log($"[WorldObjectIdAssigner] Assigned ids to {objects.Length} WorldObject(s) (pickables + harvestables). Save the scene to bake them.");
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user