Kursach_StealthAction/Assets/Tremble/TrembleMapImport.cs
2026-10-11 19:16:11 +03:00

1086 lines
34 KiB
C#

//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
// This class based on BSP Map Tools for Unity by John Evans (evans3d512@gmail.com)
//
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UnityEngine.Serialization;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace TinyGoose.Tremble
{
public static class TBConsts
{
public const string TB_PREFIX = "_tb_";
public const string ID = TB_PREFIX + "id";
public const string NAME = TB_PREFIX + "name";
public const string OMIT_FROM_EXPORT = TB_PREFIX + "layer_omit_from_export";
public const string GROUP = TB_PREFIX + "group";
public const string LAYER = TB_PREFIX + "layer";
public const string TYPE = TB_PREFIX + "type";
public const string TEXTURES = TB_PREFIX + "textures";
public const string EMPTY_TEXTURE = "__TB_empty";
public const string CLIP_TEXTURE = "clip";
public const string SKIP_TEXTURE = "skip";
public const string TRIGGER_TEXTURE = "trigger";
public const string SPECIAL_TEXTURE = "special";
}
public class TrembleMapImportSettings
{
// -----------------------------------------------------------------------------------------------------------------------------
// Statics
// -----------------------------------------------------------------------------------------------------------------------------
private static TrembleMapImportSettings s_CurrentImportSettings;
internal static void INTERNAL_SetCurrent(TrembleMapImportSettings settings) => s_CurrentImportSettings = settings;
public static TrembleMapImportSettings Current
{
get
{
if (s_CurrentImportSettings == null)
{
Debug.LogError("Cannot get TrembleMapImportSettings when no import is in progress!");
}
return s_CurrentImportSettings;
}
}
// -----------------------------------------------------------------------------------------------------------------------------
// State
// -----------------------------------------------------------------------------------------------------------------------------
private MapProcessorBase[] m_MapProcessors;
private readonly Dictionary<string, List<GameObject>> m_EntityIDToGameObjects = new();
private readonly Dictionary<int, GameObject> m_SerialNumberToGameObject = new();
public MapProcessorClass[] ProcessorClasses { private get; init; }
public bool SplitMesh { get; init; } = true;
public float MaxMeshSurfaceArea { get; init; } = 3000;
public float SmoothingAngle { get; init; } = 45;
public string ExtraCommandLineArgs { get; init; } = "";
public string AssetGUID { get; init; } = null;
public Action<string, Object> OnObjectAdded { private get; init; } = null;
public Action<Object> OnDependencyAdded { get; init; }
public Action<GameObject, Type> OnEntityAdded { get; init; } = null;
public Action<string[]> OnWadsFound { get; init; } = null;
public Action<string> OnWarning { private get; init; } = null;
public Action<string> OnError { private get; init; } = null;
public Action<Q3Map2Result, List<string>> OnQ3Map2Result { get; init; } = null;
// -----------------------------------------------------------------------------------------------------------------------------
// Getters
// -----------------------------------------------------------------------------------------------------------------------------
public static TrembleMapImportSettings Default => new();
public MapProcessorBase[] Processors
{
get
{
if (m_MapProcessors == null)
{
m_MapProcessors = ProcessorClasses == null
? Array.Empty<MapProcessorBase>()
: ProcessorClasses
.Where(pc => pc is { IsValid: true })
.Select(pc => pc.CreateInstance())
.ToArray();
}
return m_MapProcessors;
}
}
public Dictionary<string, List<GameObject>> AllEntityIDs => m_EntityIDToGameObjects;
// -----------------------------------------------------------------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------------------------------------------------------------
public void MapGameObjectToEntity(GameObject obj, BspEntity entity)
{
m_SerialNumberToGameObject[entity.SerialNumber] = obj;
if (entity.HasID())
{
if (m_EntityIDToGameObjects.TryGetValue(entity.GetID(), out List<GameObject> list))
{
list.Add(obj);
}
else
{
m_EntityIDToGameObjects.Add(entity.GetID(), new() { obj });
}
}
}
public bool TryGetGameObjectsForID(string id, out List<GameObject> objs)
{
if (id == null)
{
objs = null;
return false;
}
return m_EntityIDToGameObjects.TryGetValue(id, out objs);
}
public bool TryGetGameObjectForEntity(BspEntity entity, out GameObject obj) => m_SerialNumberToGameObject.TryGetValue(entity.SerialNumber, out obj);
public bool TryGetComponentForEntity<TComponent>(BspEntity entity, out TComponent component)
where TComponent : Component
{
if (!m_SerialNumberToGameObject.TryGetValue(entity.SerialNumber, out GameObject obj)
|| !obj.TryGetComponent(out component))
{
component = default;
return false;
}
return true;
}
public void Warn(string warning)
{
if (OnWarning != null)
OnWarning.Invoke(warning);
else
Debug.LogWarning(warning);
}
public void Error(string error)
{
if (OnError != null)
OnError.Invoke(error);
else
Debug.LogError(error);
}
public void AddDependency(Object obj)
{
if (!obj)
return;
OnDependencyAdded?.Invoke(obj);
}
public void SaveObjectInMap(string meshName, Object obj) => OnObjectAdded?.Invoke(meshName, obj);
}
public class TrembleMapImport
{
// -----------------------------------------------------------------------------------------------------------------------------
// State
// -----------------------------------------------------------------------------------------------------------------------------
private readonly TrembleMapImportSettings m_ImportSettings;
private TrembleSyncSettings m_SyncSettings;
private static readonly Dictionary<string, Material> s_MaterialCache = new();
private MeshBuilder m_MeshBuilder;
private int m_GroupCounter;
// Groups
private Transform[] m_GroupTransforms;
private MapTypeLookup m_MapTypeLookup;
private PrefabNameLookup m_PrefabNameLookup;
private MaterialNameLookup m_MaterialNameLookup;
private TrembleFieldConverterCollection m_FieldConverter;
public TrembleMapImport(TrembleMapImportSettings importSettings)
{
m_ImportSettings = importSettings;
}
private MapDocument CreateFailedMap(Q3Map2Result result, string reason)
{
GameObject nullObj = new($"Map: {result}");
MapDocument doc = nullObj.AddComponent<MapDocument>();
doc.INTERNAL_SetResult(result, reason);
return doc;
}
public async Task<MapDocument> ImportMapAndCreatePrefab(string mapFilePath, MapBsp existingMapBsp = null)
{
if (mapFilePath == null || !File.Exists(mapFilePath))
return CreateFailedMap(Q3Map2Result.Failed, $"Map file path '{mapFilePath}' was NULL or does not exist!");
TrembleMapImportSettings.INTERNAL_SetCurrent(m_ImportSettings);
m_SyncSettings = TrembleSyncSettings.Get();
int task = ProgressUtil.Start($"Import {Path.GetFileName(mapFilePath)}");
// -----------------------------------------------------------------------------------------------------------------------------
// Set up build folder, and discover groups
// -----------------------------------------------------------------------------------------------------------------------------
ProgressUtil.Report(task, 1, 2, "Setting up build");
// -----------------------------------------------------------------------------------------------------------------------------
// Create lookups
// -----------------------------------------------------------------------------------------------------------------------------
using (TrembleTimerScope _ = new(TrembleTimer.Context.CreateTypeLookups))
{
m_MapTypeLookup = new(m_SyncSettings);
m_PrefabNameLookup = new(m_SyncSettings);
m_MaterialNameLookup = new(m_SyncSettings);
m_FieldConverter = new();
}
MapBsp mapBsp = existingMapBsp;
if (mapBsp == null)
{
// -----------------------------------------------------------------------------------------------------------------------------
// Run Q3Map2
// -----------------------------------------------------------------------------------------------------------------------------
ProgressUtil.Report(task, 2, 2, "Running Q3Map2");
List<string> output = new(1024);
mapBsp = MapCompiler.BuildBsp(
m_SyncSettings,
mapFilePath,
$"{m_SyncSettings.ExtraCommandLineArgs} {m_ImportSettings.ExtraCommandLineArgs}",
out Q3Map2Result result,
onOutputMessage: m => output.Add(m));
m_ImportSettings.OnQ3Map2Result?.Invoke(result, output);
switch (result)
{
case Q3Map2Result.Failed:
ProgressUtil.Fail(task);
m_ImportSettings.Error("Map compiler did not run correctly or was not found! Try syncing and then reimporting your map.");
return CreateFailedMap(Q3Map2Result.Failed, "Map compiler did not run correctly!");
case Q3Map2Result.FailedWithMissingTextures:
m_ImportSettings.Error("Map compiler did not find textures correctly!");
#if UNITY_EDITOR
if (!Application.isBatchMode && !Application.isPlaying)
{
if (SceneView.lastActiveSceneView)
{
SceneView.lastActiveSceneView.ShowNotification(new("Tremble: Reimport this map to fix texture UVs!"));
}
}
#endif
break;
}
if (mapBsp == null)
{
m_ImportSettings.Error("Map compiler did not generate a BSP file.");
ProgressUtil.Fail(task);
return CreateFailedMap(Q3Map2Result.Failed, "Map compiler did not generate a BSP file.");
}
}
// Preload assets, if required
if (TrembleAssetLoader.SupportsPreloading)
{
List<string> pathsToPreload = new(128);
mapBsp.GatherResourceList(m_MaterialNameLookup, m_PrefabNameLookup, pathsToPreload);
await TrembleAssetLoader.PreloadAssetsByPaths(pathsToPreload);
}
// Check if the map has a Wad file set
BspEntity worldspawn = mapBsp.GetWorldspawnEntity();
if (worldspawn.TryGetString("wad", out string wadEntry) && !wadEntry.IsNullOrEmpty())
{
m_ImportSettings.OnWadsFound?.Invoke(wadEntry.Split(";"));
}
// Finally create objects
m_GroupTransforms = new Transform[mapBsp.Groups.Length];
MapDocument mapDocument = ParseBspIntoGameObject(mapBsp);
ProgressUtil.Succeed(task);
TrembleMapImportSettings.INTERNAL_SetCurrent(null);
return mapDocument;
}
private void ForeachProcessor(Action<MapProcessorBase> action)
{
using TrembleTimerScope __ = new(TrembleTimer.Context.RunMapProcessors);
foreach (MapProcessorBase p in m_ImportSettings.Processors)
{
using TrembleTimerScope _ = new(p.GetType());
action(p);
}
}
private MapDocument ParseBspIntoGameObject(MapBsp mapBsp)
{
using TrembleTimerScope __ = new(TrembleTimer.Context.CreateGameObjects);
// Thank you John Evans. This is quite close to the original UBSPMapTools code.
// Create processor
GameObject root = new(mapBsp.MapName, typeof(MapDocument));
Transform rootTransform = root.transform;
ForeachProcessor(processor => processor.RootTransform = rootTransform);
GameObject looseEntities = new("entities");
looseEntities.transform.SetParent(rootTransform);
ForeachProcessor(processor => processor.OnProcessingStarted(root, mapBsp));
// Build meshes from brushes
int unityLayer = LayerMask.NameToLayer(m_SyncSettings.WorldspawnLayer);
if (unityLayer < 0)
{
m_ImportSettings.Warn($"Could not found layer {m_SyncSettings.WorldspawnLayer} - using Default!");
unityLayer = 0;
}
m_MeshBuilder = new(m_ImportSettings, FindMaterial, m_SyncSettings.MainMeshName, m_ImportSettings.SmoothingAngle, m_ImportSettings.MaxMeshSurfaceArea);
GameObject worldspawnObject;
if (!m_SyncSettings.PipelineSplitMesh || !m_ImportSettings.SplitMesh)
{
string mainName = m_SyncSettings.MainMeshName;
worldspawnObject = m_MeshBuilder.BuildMesh(mapBsp, rootTransform, 0, true, mainName, false, Vector3.zero, true);
if (worldspawnObject)
{
worldspawnObject.layer = unityLayer;
}
else
{
m_ImportSettings.Error("Missing worldspawn brush?!");
}
}
else
{
using TrembleTimerScope _ = new(TrembleTimer.Context.SplitMesh);
worldspawnObject = new(m_SyncSettings.MainMeshName);
worldspawnObject.transform.SetParent(rootTransform);
worldspawnObject.layer = unityLayer;
HashSet<int> uniqueFaceList = new(2048);
m_MeshBuilder.BuildMesh_Recursive(mapBsp, worldspawnObject.transform, 0, uniqueFaceList, m_SyncSettings.ImportScale, unityLayer);
}
// Add the worldspawn script
Type worldspawnScriptType = m_SyncSettings.WorldspawnScript?.Class ?? typeof(Worldspawn);
Worldspawn worldspawnScript = (Worldspawn)worldspawnObject.AddComponent(worldspawnScriptType);
// Custom entities/prefabs/brushes/etc - create scripts and spawn
foreach (BspEntity entity in mapBsp.Entities)
{
// Work out layer/group
int groupIdx = 0;
if (!m_SyncSettings.DiscardMapGroups)
{
if (entity.TryGetInt(TBConsts.GROUP, out int group))
{
groupIdx = group;
}
if (entity.TryGetInt(TBConsts.LAYER, out int layer))
{
groupIdx = layer;
}
if (entity.TryGetInt(TBConsts.ID, out int groupOrLayer))
{
groupIdx = groupOrLayer;
}
}
// Process entity and store in group if needed
Transform parent = FindOrCreateGroup(mapBsp, groupIdx, rootTransform, looseEntities.transform);
if (!parent)
{
// Owning group/layer is not for export!
continue;
}
try
{
GameObject entityGameObject = CreateEntityGameObject(mapBsp, root, worldspawnObject, parent, entity);
if (!entityGameObject)
continue;
entityGameObject.transform.SetAsLastSibling();
// Store IDs
m_ImportSettings.MapGameObjectToEntity(entityGameObject, entity);
}
catch (Exception ex)
{
m_ImportSettings.Error($"Failed to create entity '{entity.GetID()}' - {ex.Message}");
Debug.LogException(ex);
}
}
// Now set script values on all entities
foreach (BspEntity entity in mapBsp.Entities)
{
if (!m_ImportSettings.TryGetGameObjectForEntity(entity, out GameObject instance))
continue;
SetFieldValuesOnAllScripts(instance, entity);
// Move to parent, if it has one
if (entity.TryGetString(FgdConsts.PROPERTY_PARENT, out string parentTarget) &&
TryFindParent(parentTarget, entity, out Transform newParent))
{
instance.transform.SetParent(newParent, true);
}
}
// Now fire OnImportFromMapEntity
foreach (BspEntity entity in mapBsp.Entities)
{
if (!m_ImportSettings.TryGetGameObjectForEntity(entity, out GameObject instance))
continue;
foreach (IOnImportFromMapEntity pifme in instance.GetComponents<IOnImportFromMapEntity>())
{
pifme.OnImportFromMapEntity(mapBsp, entity);
}
}
ForeachProcessor(processor => processor.OnProcessingCompleted(root, mapBsp));
// Move loose entities to end
looseEntities.transform.SetAsLastSibling();
// Write data into the Worldspawn
// (BSP name, original asset GUID, worldspawn root, and targetname lookup)
List<IDMapping> mappings = new(256);
foreach (KeyValuePair<string, List<GameObject>> kvp in m_ImportSettings.AllEntityIDs)
{
mappings.Add(IDMapping.Create(kvp.Key, kvp.Value.ToArray()));
}
MapDocument mapDocument = root.GetComponent<MapDocument>();
mapDocument.INTERNAL_SetData(
bspName: mapBsp.MapName,
originalAssetGuid: m_ImportSettings.AssetGUID ?? Guid.NewGuid().ToString(),
worldspawnObject: worldspawnScript.gameObject,
mapImportedTime: (ulong)DateTime.UtcNow.Ticks,
ids: mappings.ToArray());
return mapDocument;
}
private bool TryFindParent(string parentTarget, BspEntity entity, out Transform parent)
{
if (!m_ImportSettings.TryGetGameObjectsForID(parentTarget, out List<GameObject> targets) || targets.Count == 0)
{
m_ImportSettings.Warn($"Could not find parent '{parentTarget}' for {entity.GetClassname()} entity '{entity.GetID()}'");
parent = null;
return false;
}
parent = targets[0].transform;
return true;
}
private GameObject CreateEntityGameObject(MapBsp mapBsp, GameObject root, GameObject worldspawnObject, Transform parentTransform, BspEntity entity)
{
// Class name
string classname = entity.GetClassname();
if (classname.EqualsInvariant(FgdConsts.WORLDSPAWN, caseSensitive: true)) // "worldspawn" entities are just part of the world!
{
ForeachProcessor(processor => processor.ProcessWorldSpawnProperties(mapBsp, entity, root));
return worldspawnObject;
}
// It's a prefab?
if (m_PrefabNameLookup.IsValidAsset(classname) && m_PrefabNameLookup.TryGetPrefabPathFromMapName(classname, out string prefabPath))
{
GameObject prefab = TrembleAssetLoader.LoadPrefabByPath(prefabPath);
m_ImportSettings.AddDependency(prefab);
if (!prefab)
{
m_ImportSettings.Error($"Couldn't find prefab at path '{prefabPath}' for classname '{classname}'!");
return null;
}
bool wasActivePrefab = prefab.activeSelf;
prefab.SetActive(false);
GameObject instance = PrefabUtil.InstantiatePrefab(prefab, parentTransform);
if (wasActivePrefab)
{
prefab.SetActive(true);
}
SetObjectName(root, instance, entity);
Vector3 offset = Vector3.zero;
if (TrembleAssetLoader.TryGetAssetDataFromMapName(classname, out AssetMetadata metadata))
{
offset = metadata.SpawnOffset;
}
else
{
Debug.LogWarning($"Could not find spawn offset for prefab '{prefab.name}'!");
}
SetTransformValuesFromEntity(instance, entity, offset);
ForeachProcessor(processor => processor.ProcessPrefabEntity(mapBsp, entity, instance));
if (wasActivePrefab)
{
instance.SetActive(true);
}
return instance;
}
// It's a brush or point entity
if (m_MapTypeLookup.TryGetEntityTypeFromMapName(classname, out EntityType type))
{
if (type == EntityType.Brush) // brush entity
{
if (!entity.TryGetString("model", out string modelIdxString) || !modelIdxString.StartsWith("*"))
return null;
int modelIdx = int.Parse(modelIdxString.Substring(1));
string brushClass = entity.GetClassname();
m_MapTypeLookup.TryGetClassFromMapName(brushClass, out Type componentType);
BrushEntityAttribute bea = componentType.GetCustomAttribute<BrushEntityAttribute>();
Vector3 modelCentre = (mapBsp.Models[modelIdx].Min + mapBsp.Models[modelIdx].Max) * 0.5f;
// Rotate back properly
Quaternion? rotation = null;
Quaternion? inverseRotation = null;
if (entity.TryGetRotation(FgdConsts.PROPERTY_ANGLES, out Quaternion angles))
{
rotation = angles;
inverseRotation = Quaternion.Inverse(rotation.Value);
}
int unityLayer = 0;
if (!bea.LayerName.IsNullOrEmpty())
{
unityLayer = LayerMask.NameToLayer(bea.LayerName);
if (unityLayer < 0)
{
m_ImportSettings.Warn($"Could not found layer {bea.LayerName} - using Default!");
unityLayer = 0;
}
}
// Allow configuring a smoothing angle per brush entity.
float smoothingAngle = m_ImportSettings.SmoothingAngle;
if (TrembleSyncSettings.Get().PipelineSmoothMeshNormals)
{
// By default the smoothing pipeline acts as "_phong=1"
// Allow the official terminology to override the smoothing angle
smoothingAngle = entity.GetFloat("_phong_angle", smoothingAngle);
// We allow inverted behaviour where users can write "_phong=0" to disable smoothing
if (entity.GetInt("_phong", 1) == 0)
{
smoothingAngle = 0f;
}
}
// Implement a "_static=1" flag for detail brush entities that need UV2
bool isStatic = entity.GetBoolean("_static", false);
GameObject brush = m_MeshBuilder.BuildMesh(mapBsp, parentTransform, modelIdx, false, brushClass, false, modelCentre, isStatic, inverseRotation, smoothingAngle);
if (!brush)
{
Debug.LogError($"Discarded brush entity of class {brushClass} due to its model being degenerate!");
return null;
}
brush.transform.position = modelCentre;
if (rotation.HasValue)
{
brush.transform.rotation = rotation.Value;
}
SetObjectName(root, brush, entity);
// Find type
brush.AddComponent(componentType);
m_ImportSettings.OnEntityAdded?.Invoke(brush, componentType);
// Modify components based on type
if (bea.BrushType is BrushType.Trigger or BrushType.Liquid)
{
// Remove the mesh renderer/filter unless liquid
if (bea.BrushType != BrushType.Liquid)
{
GameObject.DestroyImmediate(brush.GetComponent<MeshRenderer>());
GameObject.DestroyImmediate(brush.GetComponent<MeshFilter>());
}
// Make it a trigger instead
if (brush.TryGetComponent(out MeshCollider meshCollider))
{
meshCollider.convex = true;
meshCollider.isTrigger = true;
}
}
if (bea.BrushType == BrushType.Invisible)
{
// Remove the mesh renderer/filter
GameObject.DestroyImmediate(brush.GetComponent<MeshRenderer>());
GameObject.DestroyImmediate(brush.GetComponent<MeshFilter>());
}
// Change layer if needed
if (!bea.LayerName.IsNullOrEmpty())
{
brush.layer = unityLayer;
}
ForeachProcessor(processor => processor.ProcessBrushEntity(mapBsp, entity, brush));
return brush;
}
else // point entity
{
string pointClass = entity.GetClassname();
if (pointClass.EqualsInvariant(TrembleConsts.MAPFIX_ENTITY_NAME, caseSensitive: true))
{
// This point entity is used to fix map import - ignore it!
return null;
}
GameObject pointEntity = new(pointClass);
pointEntity.transform.SetParent(parentTransform);
SetTransformValuesFromEntity(pointEntity, entity);
SetObjectName(root, pointEntity, entity);
// Find type
if (m_MapTypeLookup.TryGetClassFromMapName(pointClass, out Type componentType))
{
pointEntity.AddComponent(componentType);
m_ImportSettings.OnEntityAdded?.Invoke(pointEntity, componentType);
}
else
{
m_ImportSettings.Warn($"No type found for Point entity class: {pointClass} in map! Try Map Repair.");
}
ForeachProcessor(processor => processor.ProcessPointEntity(mapBsp, entity, pointEntity));
return pointEntity;
}
}
if (!classname.EqualsInvariant(TrembleConsts.MAPFIX_ENTITY_NAME, caseSensitive: true))
{
m_ImportSettings.Warn($"No type found for entity class: {classname} in map! Try Map Repair.");
}
return null;
}
// -----------------------------------------------------------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------------------------------------------------------
private Material FindMaterial(string mapName)
{
// Have we seen this material before?
if (s_MaterialCache.TryGetValue(mapName, out Material foundMaterial) && foundMaterial)
return foundMaterial;
// Special case: "__TB_empty" to null
if (mapName.EqualsInvariant(TBConsts.EMPTY_TEXTURE) ||
mapName.ContainsInvariant(TBConsts.SKIP_TEXTURE))
return null;
// Special case: map "clip" to "M_NullRender"
if (mapName.ContainsInvariant(TBConsts.CLIP_TEXTURE))
{
Material clipMaterial = TrembleAssetLoader.LoadClipMaterial();
s_MaterialCache[mapName] = clipMaterial;
return clipMaterial;
}
// Look it up
if (!m_MaterialNameLookup.TryGetMaterialPathFromMapName(mapName, out string materialPath))
{
#if !UNITY_EDITOR && !UNITY_SERVER
string texturePath = Path.Combine(TrembleConsts.BASEQ3_PATH, "textures", mapName + ".png");
if (File.Exists(texturePath))
{
// Try to create a runtime material for loose PNG
Material looseMaterial = CreateRuntimeMaterial(texturePath);
s_MaterialCache[mapName] = looseMaterial;
return looseMaterial;
}
#endif
if (!mapName.Contains(TBConsts.SPECIAL_TEXTURE))
{
m_ImportSettings.Warn($"Failed to find a Unity material for {mapName}!");
}
return null;
}
// Load it up
Material material = TrembleAssetLoader.LoadAssetByPath<Material>(materialPath);
if (!material)
{
m_ImportSettings.Warn($"Material at path '{materialPath}' could not be loaded!");
return null;
}
s_MaterialCache[mapName] = material;
return material;
}
#if UNITY_EDITOR
public static Material CreateEditorMaterial(string textureAssetPath, float smoothness = -1f, float metallic = -1)
{
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(textureAssetPath);
string textureName = Path.GetFileNameWithoutExtension(textureAssetPath);
return CreateMaterial(textureName, texture, smoothness, metallic);
}
#endif
public static Material CreateRuntimeMaterial(string pngPath, float smoothness = -1f, float metallic = -1)
{
// Load texture
byte[] textureData = File.ReadAllBytes(pngPath);
Texture2D texture = new(1, 1);
texture.LoadImage(textureData, markNonReadable: true);
string textureName = Path.GetFileNameWithoutExtension(pngPath);
return CreateMaterial(textureName, texture, smoothness, metallic);
}
public static Material CreateMaterial(string textureName, Texture2D texture, float smoothness = -1f, float metallic = -1)
{
if (smoothness == -1)
{
smoothness = GetParamValue(textureName, "s", 0.5f);
}
if (metallic == -1)
{
metallic = GetParamValue(textureName, "m", 0f);
}
// Create material
#if URP_INSTALLED
Material m = new(Shader.Find("Universal Render Pipeline/Lit"));
m.mainTexture = texture;
m.SetFloat("_Smoothness", smoothness);
m.SetFloat("_Metallic", metallic);
return m;
#elif HDRP_INSTALLED
Material m = new(Shader.Find("HDRP/Lit"));
m.SetTexture("_BaseColorMap", texture); // Use _BaseColorMap instead of _MainTex
m.SetFloat("_Smoothness", smoothness);
m.SetFloat("_Metallic", metallic);
return m;
#else
Material m = new(Shader.Find("Standard"));
m.mainTexture = texture;
m.SetFloat("_Glossiness", smoothness); // Use _Glossiness instead of _Smoothness
m.SetFloat("_Metallic", metallic);
return m;
#endif
}
private static float GetParamValue(string input, string param, float defaultValue)
{
string marker = $"_{param}";
// Find marker!
int index = input.IndexOfInvariant(marker);
if (index == -1)
return defaultValue;
index += marker.Length;
// Collect numbers into array
char[] buffer = new char[8];
int bufPtr = 0;
while (index < input.Length && bufPtr < buffer.Length && Char.IsNumber(input[index]))
{
buffer[bufPtr++] = input[index++];
}
// Parse to float
ReadOnlySpan<char> strBuffer = new(buffer);
if (!strBuffer.TryParseFloat(out float value))
return defaultValue;
return value / 100f;
}
private readonly StringBuilder m_NameBuilder = new(128);
private void SetObjectName(GameObject root, GameObject go, BspEntity entity)
{
m_NameBuilder.Clear();
string classname = entity.GetClassname();
m_NameBuilder.Append(classname);
m_NameBuilder.Append(' ');
bool appendSerialNumber = true;
string id = entity.GetID();
if (id != null)
{
m_NameBuilder.Append('\'');
m_NameBuilder.Append(id);
m_NameBuilder.Append('\'');
GameObject existing = root.Find_Recursive(m_NameBuilder.ToString());
if (existing)
{
m_NameBuilder.Append(' ');
}
else
{
appendSerialNumber = false;
}
}
if (appendSerialNumber)
{
// Append global entity counter, e.g. #12 if not named
m_NameBuilder.Append('#');
m_NameBuilder.Append(entity.SerialNumber);
}
go.name = m_NameBuilder.ToString();
}
private void SetTransformValuesFromEntity(GameObject instance, BspEntity entity) => SetTransformValuesFromEntity(instance, entity, Vector3.zero);
private void SetTransformValuesFromEntity(GameObject instance, BspEntity entity, Vector3 offset)
{
Transform instanceTransform = instance.transform;
// Set position
instanceTransform.position = entity.GetPosition();
// Set scale
if (entity.TryGetUntransformedVector(FgdConsts.PROPERTY_SCALE, out Vector3 scale))
{
instanceTransform.localScale = scale.Q3ToUnityVector();
}
// Set rotation
if (entity.TryGetRotation(FgdConsts.PROPERTY_ANGLES, out Quaternion angles))
{
instanceTransform.rotation = angles;
}
// Finally move by offset, based on scaling and rotation
Vector3 scaledOffset = offset;
scaledOffset.Scale(instanceTransform.localScale);
instanceTransform.position += instanceTransform.rotation * scaledOffset;
#if !UNITY_EDITOR
if (instance.TryGetComponent(out Rigidbody rb))
{
#if UNITY_2022_1_OR_NEWER
rb.Move(instanceTransform.position, instanceTransform.rotation);
#else
rb.position = instanceTransform.position;
rb.rotation = instanceTransform.rotation;
#endif
}
#endif
}
private void SetFieldValuesOnAllScripts(GameObject instance, BspEntity entity)
{
bool hasSetFieldValues = false; // Only set fields on first EntityAttributeBase found
foreach (MonoBehaviour mb in instance.GetComponents<MonoBehaviour>())
{
if (!mb)
{
Debug.LogWarning($"Prefab '{instance.name}' has a missing script!'");
continue;
}
Type scriptType = mb.GetType();
if (!hasSetFieldValues && scriptType.IsSubclassOf(typeof(Worldspawn)) || scriptType.HasCustomAttribute<EntityAttributeBase>())
{
foreach (FieldInfo fi in scriptType.GetAllInstanceFieldsIncludingPrivateAndBaseClasses())
{
SetFieldValue(entity, mb, fi);
}
hasSetFieldValues = true;
}
}
}
private void SetFieldValue(BspEntity entity, MonoBehaviour mb, FieldInfo target)
{
target.GetCustomAttributes(out TrembleAttribute ta, out NoTrembleAttribute nta, out SerializeField sfa, out FormerlySerializedAsAttribute fsa);
// Work out names for this field
List<string> knownFieldNames = new() { target.Name.GetFieldNameInMap(m_SyncSettings.FieldNamingConvention) };
if (ta?.OverrideName != null)
{
knownFieldNames.Insert(0, ta.OverrideName);
}
if (fsa != null)
{
knownFieldNames.Add(fsa.oldName.GetFieldNameInMap(m_SyncSettings.FieldNamingConvention));
}
if (ta == null && !(m_SyncSettings.SyncSerializedFields && sfa != null) && !target.IsPublic)
{
// If not public, and not [Tremble], skip!
return;
}
if (nta != null)
{
// Specifically NOT serialised in maps - skip!
return;
}
// Special case: Prefabs and Materials
bool isMaterialOrPrefabField = target.FieldType == typeof(Material) || target.FieldType == typeof(GameObject);
if (isMaterialOrPrefabField && ta == null && !m_SyncSettings.SyncMaterialsAndPrefabs)
{
// This is a Material or Prefab but we've turned off the setting
return;
}
// Work out which name to use
int matchingFieldIdx = 0;
for (int knownFieldIdx = 0; knownFieldIdx < knownFieldNames.Count; knownFieldIdx++)
{
string knownFieldName = knownFieldNames[knownFieldIdx];
if (!entity.HasKey(knownFieldName))
continue;
matchingFieldIdx = knownFieldIdx;
break;
}
// Try to convert value!
string fieldName = knownFieldNames[matchingFieldIdx];
TrembleFieldConverter converter = m_FieldConverter.GetConverterForType(target.FieldType);
if (converter != null)
{
if (target.FieldType.IsArray)
{
// Use a converter to convert the map value
if (!converter.TryGetValuesFromMap(entity, fieldName, mb.gameObject, target, out object[] values))
return;
Type targetElementType = target.FieldType.GetElementType()!;
Array filteredArray = Array.CreateInstance(targetElementType, values.Length);
for (int i = 0; i < values.Length; i++)
{
object valueAtIndex = values[i];
if (valueAtIndex == null)
continue;
if (targetElementType.IsAssignableFrom(valueAtIndex.GetType()))
{
filteredArray.SetValue(valueAtIndex, i);
}
else
{
Debug.LogWarning($"Element {i} of {mb.name}::{target.Name} is NOT compatible with an array of {targetElementType}s (it's a {valueAtIndex.GetType().Name}) and was skipped!");
}
}
target.SetValue(mb, filteredArray);
}
else
{
// Use a converter to convert the map value
if (!converter.TryGetValueFromMap(entity, fieldName, mb.gameObject, target, out object value))
return;
if (target.FieldType == typeof(Transform) && value is Component c)
{
value = c.transform;
}
target.SetValue(mb, value);
}
}
else if (ta != null)
{
// Nothing worked - log an import warning
m_ImportSettings.Warn($"Saw field '{target.Name}' of type {target.FieldType}, which is not yet supported. Try adding a custom {nameof(TrembleFieldConverter)} for it, if you like!");
}
}
private Transform FindOrCreateGroup(MapBsp mapBsp, int groupIdx, Transform rootTransform, Transform otherEntitiesTransform)
{
// Root group, or we have no matching group
if (groupIdx == 0 || m_GroupTransforms == null || groupIdx >= m_GroupTransforms.Length)
return otherEntitiesTransform;
// Existing - return it!
if (m_GroupTransforms[groupIdx])
return m_GroupTransforms[groupIdx];
if (mapBsp.Groups[groupIdx].IsNoExport)
return null;
// New group, create and return - but be careful not to name the same as others
string newName = $"{mapBsp.Groups[groupIdx].LabelName} #{++m_GroupCounter}";
GameObject group = new(newName);
group.transform.SetParent(rootTransform, false);
m_GroupTransforms[groupIdx] = group.transform;
return m_GroupTransforms[groupIdx];
}
}
}