Second commit
This commit is contained in:
commit
36840f3ad4
999 files changed
+61364
No files matched your search
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class DirectoryUtil
|
||||
{
|
||||
private static readonly char[] s_PathSeparators = { '\\', '/' };
|
||||
|
||||
public static void EmptyAndCreateDirectory(string folder)
|
||||
{
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
Directory.Delete(folder, true);
|
||||
}
|
||||
|
||||
CreateAllDirectories(folder);
|
||||
}
|
||||
|
||||
public static int CreateAllDirectories(string folders)
|
||||
{
|
||||
int numCreated = 0;
|
||||
|
||||
string[] pathFolders = folders.Split(s_PathSeparators);
|
||||
if (pathFolders.Length <= 1)
|
||||
return numCreated;
|
||||
|
||||
string path = pathFolders[0];
|
||||
|
||||
for (int i = 1; i < pathFolders.Length; i++)
|
||||
{
|
||||
// Path.Combine breaks when it's absolute (because /Users becomes Users)
|
||||
path += Path.DirectorySeparatorChar + pathFolders[i];
|
||||
|
||||
if (i == pathFolders.Length - 1 && pathFolders[i].Contains('.'))
|
||||
{
|
||||
// Last component and it has a dot - it's a file!
|
||||
break;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
numCreated++;
|
||||
}
|
||||
}
|
||||
|
||||
return numCreated;
|
||||
}
|
||||
|
||||
public static async Task<bool> CopyDirectory(string source, string destination)
|
||||
{
|
||||
DirectoryInfo dir = new(source);
|
||||
|
||||
// Check if the source directory exists
|
||||
if (!dir.Exists)
|
||||
return false;
|
||||
|
||||
EmptyAndCreateDirectory(destination);
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
foreach (FileInfo file in dir.GetFiles())
|
||||
{
|
||||
string targetFilePath = Path.Combine(destination, file.Name);
|
||||
file.CopyTo(targetFilePath);
|
||||
}
|
||||
|
||||
// Recurse!
|
||||
foreach (DirectoryInfo subDir in dir.GetDirectories())
|
||||
{
|
||||
string newDestinationDir = Path.Combine(destination, subDir.Name);
|
||||
await CopyDirectory(subDir.FullName, newDestinationDir);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9657858345914445b2cabe032ca32b33
|
||||
timeCreated: 1707729759
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/DirectoryUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,327 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using Type = System.Type;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class FieldUtil
|
||||
{
|
||||
private static readonly string[] KNOWN_PREFIXES = { "m_", "_" };
|
||||
|
||||
private static readonly Dictionary<Type, object[]> s_CachedTypeAttributes = new();
|
||||
private static readonly Dictionary<MemberInfo, object[]> s_CachedFieldAttributes = new();
|
||||
private static readonly Dictionary<Type, FieldInfo[]> s_AllInstanceFieldsIncludingPrivateAndBaseClasses = new();
|
||||
|
||||
public static bool TryGetCustomAttribute<TAttribute>(this Type type, out TAttribute att)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
att = type.GetCachedAttribute<TAttribute>();
|
||||
return att != null;
|
||||
}
|
||||
|
||||
public static bool HasCustomAttribute<TAttribute>(this FieldInfo field)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
return field.GetCachedAttribute<TAttribute>() != null;
|
||||
}
|
||||
|
||||
public static bool HasCustomAttribute<TAttribute>(this Type type)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
return type.GetCachedAttribute<TAttribute>() != null;
|
||||
}
|
||||
|
||||
public static void GetCustomAttributes<TAttribute1>(this MemberInfo member, out TAttribute1 attr1)
|
||||
where TAttribute1 : Attribute
|
||||
{
|
||||
attr1 = default;
|
||||
|
||||
foreach (Attribute attribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (attribute is TAttribute1 tAttribute1)
|
||||
attr1 = tAttribute1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetCustomAttributes<TAttribute1, TAttribute2>(this MemberInfo member, out TAttribute1 attr1, out TAttribute2 attr2)
|
||||
where TAttribute1 : Attribute
|
||||
where TAttribute2 : Attribute
|
||||
{
|
||||
attr1 = default;
|
||||
attr2 = default;
|
||||
|
||||
foreach (Attribute attribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (attribute is TAttribute1 tAttribute1)
|
||||
attr1 = tAttribute1;
|
||||
if (attribute is TAttribute2 tAttribute2)
|
||||
attr2 = tAttribute2;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetCustomAttributes<TAttribute1, TAttribute2, TAttribute3>(this MemberInfo member, out TAttribute1 attr1, out TAttribute2 attr2, out TAttribute3 attr3)
|
||||
where TAttribute1 : Attribute
|
||||
where TAttribute2 : Attribute
|
||||
where TAttribute3 : Attribute
|
||||
{
|
||||
attr1 = default;
|
||||
attr2 = default;
|
||||
attr3 = default;
|
||||
|
||||
foreach (Attribute attribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (attribute is TAttribute1 tAttribute1)
|
||||
attr1 = tAttribute1;
|
||||
if (attribute is TAttribute2 tAttribute2)
|
||||
attr2 = tAttribute2;
|
||||
if (attribute is TAttribute3 tAttribute3)
|
||||
attr3 = tAttribute3;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetCustomAttributes<TAttribute1, TAttribute2, TAttribute3, TAttribute4>(this MemberInfo member, out TAttribute1 attr1, out TAttribute2 attr2, out TAttribute3 attr3, out TAttribute4 attr4)
|
||||
where TAttribute1 : Attribute
|
||||
where TAttribute2 : Attribute
|
||||
where TAttribute3 : Attribute
|
||||
where TAttribute4 : Attribute
|
||||
{
|
||||
attr1 = default;
|
||||
attr2 = default;
|
||||
attr3 = default;
|
||||
attr4 = default;
|
||||
|
||||
foreach (Attribute attribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (attribute is TAttribute1 tAttribute1)
|
||||
attr1 = tAttribute1;
|
||||
if (attribute is TAttribute2 tAttribute2)
|
||||
attr2 = tAttribute2;
|
||||
if (attribute is TAttribute3 tAttribute3)
|
||||
attr3 = tAttribute3;
|
||||
if (attribute is TAttribute4 tAttribute4)
|
||||
attr4 = tAttribute4;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetCustomAttributes<TAttribute1, TAttribute2, TAttribute3, TAttribute4, TAttribute5>(this MemberInfo member, out TAttribute1 attr1, out TAttribute2 attr2, out TAttribute3 attr3, out TAttribute4 attr4, out TAttribute5 attr5)
|
||||
where TAttribute1 : Attribute
|
||||
where TAttribute2 : Attribute
|
||||
where TAttribute3 : Attribute
|
||||
where TAttribute4 : Attribute
|
||||
where TAttribute5 : Attribute
|
||||
{
|
||||
attr1 = default;
|
||||
attr2 = default;
|
||||
attr3 = default;
|
||||
attr4 = default;
|
||||
attr5 = default;
|
||||
|
||||
foreach (Attribute attribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (attribute is TAttribute1 tAttribute1)
|
||||
attr1 = tAttribute1;
|
||||
if (attribute is TAttribute2 tAttribute2)
|
||||
attr2 = tAttribute2;
|
||||
if (attribute is TAttribute3 tAttribute3)
|
||||
attr3 = tAttribute3;
|
||||
if (attribute is TAttribute4 tAttribute4)
|
||||
attr4 = tAttribute4;
|
||||
if (attribute is TAttribute5 tAttribute5)
|
||||
attr5 = tAttribute5;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetCustomAttributes<TAttribute1, TAttribute2, TAttribute3, TAttribute4, TAttribute5, TAttribute6>(this MemberInfo member, out TAttribute1 attr1, out TAttribute2 attr2, out TAttribute3 attr3, out TAttribute4 attr4, out TAttribute5 attr5, out TAttribute6 attr6)
|
||||
where TAttribute1 : Attribute
|
||||
where TAttribute2 : Attribute
|
||||
where TAttribute3 : Attribute
|
||||
where TAttribute4 : Attribute
|
||||
where TAttribute5 : Attribute
|
||||
where TAttribute6 : Attribute
|
||||
{
|
||||
attr1 = default;
|
||||
attr2 = default;
|
||||
attr3 = default;
|
||||
attr4 = default;
|
||||
attr5 = default;
|
||||
attr6 = default;
|
||||
|
||||
foreach (Attribute attribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (attribute is TAttribute1 tAttribute1)
|
||||
attr1 = tAttribute1;
|
||||
if (attribute is TAttribute2 tAttribute2)
|
||||
attr2 = tAttribute2;
|
||||
if (attribute is TAttribute3 tAttribute3)
|
||||
attr3 = tAttribute3;
|
||||
if (attribute is TAttribute4 tAttribute4)
|
||||
attr4 = tAttribute4;
|
||||
if (attribute is TAttribute5 tAttribute5)
|
||||
attr5 = tAttribute5;
|
||||
if (attribute is TAttribute6 tAttribute6)
|
||||
attr6 = tAttribute6;
|
||||
}
|
||||
}
|
||||
|
||||
private static object[] GetCachedAttributes(this MemberInfo member)
|
||||
{
|
||||
if (s_CachedFieldAttributes.TryGetValue(member, out object[] attributes))
|
||||
return attributes;
|
||||
|
||||
attributes = member.GetCustomAttributes(true);
|
||||
s_CachedFieldAttributes[member] = attributes;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static object[] GetCachedAttributes(this Type type)
|
||||
{
|
||||
if (s_CachedTypeAttributes.TryGetValue(type, out object[] attributes))
|
||||
return attributes;
|
||||
|
||||
attributes = type.GetCustomAttributes(true);
|
||||
s_CachedTypeAttributes[type] = attributes;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static TAttribute GetCachedAttribute<TAttribute>(this MemberInfo member)
|
||||
{
|
||||
foreach (object availableAttribute in member.GetCachedAttributes())
|
||||
{
|
||||
if (availableAttribute is TAttribute castedAttribute)
|
||||
return castedAttribute;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static TAttribute GetCachedAttribute<TAttribute>(this Type type)
|
||||
{
|
||||
foreach (object availableAttribute in type.GetCachedAttributes())
|
||||
{
|
||||
if (availableAttribute is TAttribute castedAttribute)
|
||||
return castedAttribute;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static string GetFieldNameInMap(this string fieldName, NamingConvention fnc)
|
||||
{
|
||||
// Remove prefixes
|
||||
if (fnc != NamingConvention.PreserveExact)
|
||||
{
|
||||
foreach (string prefix in KNOWN_PREFIXES)
|
||||
{
|
||||
if (!fieldName.StartsWithInvariant(prefix, caseSensitive: true))
|
||||
continue;
|
||||
|
||||
fieldName = fieldName.Substring(prefix.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return fieldName.ToNamingConvention(fnc);
|
||||
}
|
||||
|
||||
public static int GetBitForSpawnFlagsField(this SpawnFlagsAttribute spawnFlags, TrembleSyncSettings syncSettings, MemberInfo mi)
|
||||
{
|
||||
if (spawnFlags is { Bit: >= 0 })
|
||||
return spawnFlags.Bit;
|
||||
|
||||
int bit = -1;
|
||||
foreach (FieldInfo availableField in mi.DeclaringType.GetAllInstanceFieldsIncludingPrivateAndBaseClasses())
|
||||
{
|
||||
// This isn't a boolean!
|
||||
if (availableField.FieldType != typeof(Boolean))
|
||||
continue;
|
||||
|
||||
availableField.GetCustomAttributes(
|
||||
out SpawnFlagsAttribute sfa,
|
||||
out NoSpawnFlagsAttribute noSfa,
|
||||
out TrembleAttribute ta,
|
||||
out NoTrembleAttribute nta,
|
||||
out SerializeField sf
|
||||
);
|
||||
|
||||
// This is categorically NOT a spawnflags var
|
||||
if (noSfa != null)
|
||||
continue;
|
||||
|
||||
// Not for Tremble
|
||||
if (nta != null)
|
||||
continue;
|
||||
|
||||
// This field doesn't have spawn flags - doesn't count!
|
||||
if (sfa == null && !syncSettings.AlwaysPackBoolsIntoSpawnFlags)
|
||||
continue;
|
||||
|
||||
// No tremble attribute, or no SerializeField/we're not considering SFs
|
||||
if (ta == null && (sf == null || !syncSettings.SyncSerializedFields))
|
||||
continue;
|
||||
|
||||
// It's a spawnflags field, increment
|
||||
bit++;
|
||||
|
||||
// It's the one we're looking for - stop!
|
||||
if (availableField == mi)
|
||||
break;
|
||||
}
|
||||
|
||||
return bit;
|
||||
}
|
||||
|
||||
public static Type GetFieldOrPropertyType(this MemberInfo mi)
|
||||
{
|
||||
switch (mi)
|
||||
{
|
||||
case PropertyInfo pi:
|
||||
return pi.PropertyType;
|
||||
|
||||
case FieldInfo fi:
|
||||
return fi.FieldType;
|
||||
}
|
||||
|
||||
Debug.LogError($"Unsupported field type {mi.GetType()}!");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Type GetFieldOrPropertyTypeOrElementType(this MemberInfo mi)
|
||||
{
|
||||
Type type = mi.GetFieldOrPropertyType();
|
||||
if (type is { IsArray: true })
|
||||
{
|
||||
type = type.GetElementType();
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
public static FieldInfo[] GetAllInstanceFieldsIncludingPrivateAndBaseClasses(this Type t)
|
||||
{
|
||||
Type originalType = t;
|
||||
|
||||
if (!s_AllInstanceFieldsIncludingPrivateAndBaseClasses.TryGetValue(originalType, out FieldInfo[] fields))
|
||||
{
|
||||
List<FieldInfo> fieldList = new(64);
|
||||
while (t != null && (t.Namespace == null || !t.Namespace.ContainsInvariant("UnityEngine", caseSensitive: true)))
|
||||
{
|
||||
fieldList.InsertRange(0, t.GetFields(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Public));
|
||||
t = t.BaseType;
|
||||
}
|
||||
|
||||
fields = fieldList.ToArray();
|
||||
s_AllInstanceFieldsIncludingPrivateAndBaseClasses[originalType] = fields;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52da94c615dc49adbb44d9880ad075c6
|
||||
timeCreated: 1705686190
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/FieldUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class FloatFormatUtil
|
||||
{
|
||||
private static readonly char[] s_FormatArr = { 'F', '0' };
|
||||
|
||||
public static string ToStringInvariant(this float f, int decimals = 2)
|
||||
{
|
||||
if (decimals > 9)
|
||||
throw new ArgumentException("ToStringInvariant only supports up to 9 decimal places :(");
|
||||
|
||||
s_FormatArr[1] = (char)('0' + decimals);
|
||||
return f.ToString(new(s_FormatArr), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this Vector2 v, int decimals = 2)
|
||||
=> $"{v.x.ToStringInvariant(decimals)} {v.y.ToStringInvariant(decimals)}";
|
||||
|
||||
public static string ToStringInvariant(this Vector3 v, int decimals = 2)
|
||||
=> $"{v.x.ToStringInvariant(decimals)} {v.y.ToStringInvariant(decimals)} {v.z.ToStringInvariant(decimals)}";
|
||||
|
||||
public static string ToStringInvariant(this Color c, int decimals = 2)
|
||||
=> $"{c.r.ToStringInvariant(decimals)} {c.g.ToStringInvariant(decimals)} {c.b.ToStringInvariant(decimals)}";
|
||||
|
||||
public static bool TryParseFloat(this string s, out float f)
|
||||
=> float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out f);
|
||||
public static bool TryParseFloat(this ReadOnlySpan<char> s, out float f)
|
||||
=> float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42bb3c4e19fb4804bfaa5dd98d17cee0
|
||||
timeCreated: 1722209446
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/FloatFormatUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class GameObjectUtil
|
||||
{
|
||||
public static GameObject Find_Recursive(this GameObject go, string findName)
|
||||
{
|
||||
if (go.name.EqualsInvariant(findName, caseSensitive: true))
|
||||
return go;
|
||||
|
||||
for (int i = 0; i < go.transform.childCount; i++)
|
||||
{
|
||||
GameObject inner = Find_Recursive(go.transform.GetChild(i).gameObject, findName);
|
||||
if (inner)
|
||||
return inner;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a7a9d2123c941af9e5751aad84bf341
|
||||
timeCreated: 1724939527
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/GameObjectUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
// This allows you to use properties with init-only setters,
|
||||
// e.g. public int MyProperty { get; init; }
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
public struct IsExternalInit {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9c41efc8d38494ebd53a358fbff6c96
|
||||
timeCreated: 1707729573
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/InitHack.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,361 @@
|
||||
//
|
||||
// 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 System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class MD3Util
|
||||
{
|
||||
private const int MAX_SURF_INDICES = 12000;
|
||||
|
||||
private const string IDP3_SIGNATURE = "IDP3";
|
||||
private const int IDP3_VERSION = 15;
|
||||
|
||||
public static bool HasModel(GameObject prefab, out Bounds bounds)
|
||||
{
|
||||
bool result = false;
|
||||
bounds = new();
|
||||
|
||||
foreach (MeshFilter mf in prefab.GetComponentsInChildren<MeshFilter>())
|
||||
{
|
||||
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
|
||||
if (!mr)
|
||||
continue;
|
||||
|
||||
if (!mf.sharedMesh)
|
||||
continue;
|
||||
|
||||
result = true;
|
||||
GetTransformedMeshVertices(mr.transform, isSkinnedMesh: false, mf.sharedMesh, ref bounds);
|
||||
}
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in prefab.GetComponentsInChildren<SkinnedMeshRenderer>())
|
||||
{
|
||||
if (!smr.sharedMesh)
|
||||
continue;
|
||||
|
||||
result = true;
|
||||
GetTransformedMeshVertices(smr.transform, isSkinnedMesh: true, smr.sharedMesh, ref bounds);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Bounds GetPrefabBounds(GameObject prefab)
|
||||
{
|
||||
Bounds boundingBox = new();
|
||||
|
||||
foreach (MeshFilter mf in prefab.GetComponentsInChildren<MeshFilter>())
|
||||
{
|
||||
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
|
||||
if (!mr)
|
||||
continue;
|
||||
|
||||
if (!mf.sharedMesh)
|
||||
continue;
|
||||
|
||||
GetTransformedMeshVertices(mr.transform, isSkinnedMesh: false, mf.sharedMesh, ref boundingBox);
|
||||
}
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in prefab.GetComponentsInChildren<SkinnedMeshRenderer>())
|
||||
{
|
||||
if (!smr.sharedMesh)
|
||||
continue;
|
||||
GetTransformedMeshVertices(smr.transform, isSkinnedMesh: true, smr.sharedMesh, ref boundingBox);
|
||||
}
|
||||
|
||||
return boundingBox;
|
||||
}
|
||||
|
||||
public static Vector3 GetPrefabUnitySpawnOffset(GameObject prefab, float importScale)
|
||||
{
|
||||
Bounds boundingBox = GetPrefabBounds(prefab);
|
||||
|
||||
Vector3 boundsMin = boundingBox.min.UnityToQ3Vector(scale: importScale);
|
||||
Vector3 boundsMax = boundingBox.max.UnityToQ3Vector(scale: importScale);
|
||||
Vector3 boundsCentre = (boundsMin + boundsMax) / 2f;
|
||||
|
||||
return -boundsCentre.Q3ToUnityVector(importScale);
|
||||
}
|
||||
|
||||
public static bool SaveMD3(GameObject prefab, string md3Path, MaterialNameLookup materialNameLookup, float importScale, out Bounds boundingBox)
|
||||
{
|
||||
// Gather MD3 surfaces to write from renderers
|
||||
List<MD3Surface> surfaces = GatherMD3SurfacesFromRenderers(prefab, materialNameLookup, out boundingBox, out int allSurfacesSize);
|
||||
if (surfaces.Count == 0)
|
||||
return false;
|
||||
|
||||
// Now write the MD3 file
|
||||
int fileSize = allSurfacesSize + 164;
|
||||
string modelName = GetPathInFolder(md3Path, "baseq3");
|
||||
|
||||
using FileStream stream = new(md3Path, FileMode.Create, FileAccess.Write, FileShare.None, 32768);
|
||||
using StreamWriteIO writer = new(stream);
|
||||
|
||||
// IDP3 signature & version
|
||||
writer.WriteString(IDP3_SIGNATURE);
|
||||
writer.WriteInt32(IDP3_VERSION);
|
||||
|
||||
// Model name (64 chars)
|
||||
writer.WriteString(modelName, 64);
|
||||
|
||||
// Flags, frames, tags, surfaces, skins
|
||||
writer.WriteInt32(0); // flags 0
|
||||
writer.WriteInt32(1); // frames
|
||||
writer.WriteInt32(0); // tags 0
|
||||
writer.WriteInt32(surfaces.Count); // surfaces
|
||||
writer.WriteInt32(0); // skins 0
|
||||
|
||||
// Write metrics
|
||||
const int FRAME_OFFSET = 108;
|
||||
writer.WriteInt32(FRAME_OFFSET);
|
||||
const int TAGS_OFFSET = 164;
|
||||
writer.WriteInt32(TAGS_OFFSET);
|
||||
const int SURFACE_OFFSET = 164;
|
||||
writer.WriteInt32(SURFACE_OFFSET);
|
||||
|
||||
writer.WriteInt32(fileSize); // file size bytes
|
||||
|
||||
// Write first frame
|
||||
Vector3 boundsMin = boundingBox.min.UnityToQ3Vector(scale: importScale);
|
||||
Vector3 boundsMax = boundingBox.max.UnityToQ3Vector(scale: importScale);
|
||||
Vector3 boundsCentre = (boundsMin + boundsMax) / 2f;
|
||||
FixBounds(ref boundsMin, ref boundsMax);
|
||||
|
||||
writer.WriteFloat(boundsMin.x); // bounds (min)
|
||||
writer.WriteFloat(boundsMin.y);
|
||||
writer.WriteFloat(boundsMin.z);
|
||||
|
||||
writer.WriteFloat(boundsMax.x); // bounds (max)
|
||||
writer.WriteFloat(boundsMax.y);
|
||||
writer.WriteFloat(boundsMax.z);
|
||||
|
||||
writer.WriteFloat(0f); // origin
|
||||
writer.WriteFloat(0f);
|
||||
writer.WriteFloat(0f);
|
||||
|
||||
writer.WriteFloat((boundsMax - boundsMin).magnitude); // radius
|
||||
|
||||
writer.WriteString("frame1", 16); // frame name
|
||||
|
||||
// Write surfaces
|
||||
for (int surfIdx = 0; surfIdx < surfaces.Count; surfIdx++)
|
||||
{
|
||||
MD3Surface surface = surfaces[surfIdx];
|
||||
int surfVertCount = surface.Vertices.Length;
|
||||
|
||||
int triangleCount = surfVertCount / 3;
|
||||
int triangleOffset = 176;
|
||||
int shaderOffset = 108;
|
||||
int uvsOffset = triangleOffset + triangleCount * 12;
|
||||
int vtxOffset = uvsOffset + surfVertCount * 8;
|
||||
|
||||
writer.WriteString(IDP3_SIGNATURE); // IDP3
|
||||
|
||||
// Surface
|
||||
writer.WriteString(surface.Name, 64); // surf name
|
||||
writer.WriteInt32(0); // flags 0
|
||||
writer.WriteInt32(1); // frames
|
||||
writer.WriteInt32(1); // shaders
|
||||
|
||||
writer.WriteInt32(surfVertCount);
|
||||
writer.WriteInt32(triangleCount);
|
||||
writer.WriteInt32(triangleOffset);
|
||||
writer.WriteInt32(shaderOffset);
|
||||
writer.WriteInt32(uvsOffset);
|
||||
writer.WriteInt32(vtxOffset);
|
||||
writer.WriteInt32(surface.DataSize);
|
||||
|
||||
// Shader
|
||||
writer.WriteString($"models/{surface.Texture}.png", 64); // shader name
|
||||
writer.WriteInt32(surfIdx); // Shader index
|
||||
|
||||
// Tris
|
||||
for (int triIdx = 0; triIdx < surfVertCount; triIdx++)
|
||||
{
|
||||
writer.WriteInt32(triIdx);
|
||||
}
|
||||
|
||||
// Surfs
|
||||
for (int uvIdx = 0; uvIdx < surface.Uvs.Length; uvIdx++)
|
||||
{
|
||||
writer.WriteFloat(surface.Uvs[uvIdx].x);
|
||||
writer.WriteFloat(1f - surface.Uvs[uvIdx].y);
|
||||
}
|
||||
|
||||
// Verts
|
||||
for (int vertIdx = 0; vertIdx < surfVertCount; vertIdx++)
|
||||
{
|
||||
Vector3 md3Vector = surface.Vertices[vertIdx].UnityToQ3Vector(scale: importScale);
|
||||
md3Vector -= boundsCentre;
|
||||
|
||||
writer.WriteInt16((short)(md3Vector.x * 64f));
|
||||
writer.WriteInt16((short)(md3Vector.y * 64f));
|
||||
writer.WriteInt16((short)(md3Vector.z * 64f));
|
||||
writer.WriteNormalAsLatLong(surface.Normals[vertIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<MD3Surface> GatherMD3SurfacesFromRenderers(GameObject prefab, MaterialNameLookup materialNameLookup, out Bounds boundingBox, out int allSurfacesSize)
|
||||
{
|
||||
List<MD3Surface> surfaces = new();
|
||||
allSurfacesSize = 0;
|
||||
|
||||
boundingBox = new();
|
||||
|
||||
foreach (MeshFilter mf in prefab.GetComponentsInChildren<MeshFilter>())
|
||||
{
|
||||
MeshRenderer mr = mf.GetComponent<MeshRenderer>();
|
||||
if (!mr)
|
||||
continue;
|
||||
|
||||
AddMeshToMD3(mr.transform, isSkinnedMesh: false, mf.sharedMesh, mr.sharedMaterials, surfaces, materialNameLookup, ref allSurfacesSize, ref boundingBox);
|
||||
}
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in prefab.GetComponentsInChildren<SkinnedMeshRenderer>())
|
||||
{
|
||||
AddMeshToMD3(smr.transform, isSkinnedMesh: true, smr.sharedMesh, smr.sharedMaterials, surfaces, materialNameLookup, ref allSurfacesSize, ref boundingBox);
|
||||
}
|
||||
|
||||
return surfaces;
|
||||
}
|
||||
|
||||
private static void FixBounds(ref Vector3 min, ref Vector3 max)
|
||||
{
|
||||
Vector3 newMin = new(Mathf.Min(min.x, max.x), Mathf.Min(min.y, max.y), Mathf.Min(min.z, max.z));
|
||||
Vector3 newMax = new(Mathf.Max(min.x, max.x), Mathf.Max(min.y, max.y), Mathf.Max(min.z, max.z));
|
||||
|
||||
min = newMin;
|
||||
max = newMax;
|
||||
}
|
||||
|
||||
private static Vector3[] GetTransformedMeshVertices(Transform meshTransform, bool isSkinnedMesh, Mesh unityMesh, ref Bounds allBounds)
|
||||
{
|
||||
// Gather mesh data
|
||||
Vector3[] meshVertices = unityMesh.vertices;
|
||||
|
||||
Vector3 min = allBounds.min;
|
||||
Vector3 max = allBounds.max;
|
||||
|
||||
Matrix4x4 ltwMatrix = meshTransform.localToWorldMatrix;
|
||||
for (int i = 0; i < meshVertices.Length; i++)
|
||||
{
|
||||
// Transform vertex according to the prefab root it's from
|
||||
if (isSkinnedMesh)
|
||||
{
|
||||
meshVertices[i] = meshTransform.TransformVector(meshVertices[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
meshVertices[i] = ltwMatrix.MultiplyPoint(meshVertices[i]);
|
||||
}
|
||||
|
||||
// Clamp vertices, MD3 uses Int16 vectors for vertex positions, so each vertex can be in range from -12.75 to 12.75
|
||||
float meshVertX = meshVertices[i].x; // = Mathf.Clamp(meshVertices[i].x, -12.75f, +12.75f);
|
||||
float meshVertY = meshVertices[i].y; // = Mathf.Clamp(meshVertices[i].y, -12.75f, +12.75f);
|
||||
float meshVertZ = meshVertices[i].z; // = Mathf.Clamp(meshVertices[i].z, -12.75f, +12.75f);
|
||||
|
||||
// Inlined Mathf.Min and Mathf.Max
|
||||
min.x = min.x < meshVertX ? min.x : meshVertX;
|
||||
min.y = min.y < meshVertY ? min.y : meshVertY;
|
||||
min.z = min.z < meshVertZ ? min.z : meshVertZ;
|
||||
|
||||
max.x = max.x > meshVertX ? max.x : meshVertX;
|
||||
max.y = max.y > meshVertY ? max.y : meshVertY;
|
||||
max.z = max.z > meshVertZ ? max.z : meshVertZ;
|
||||
}
|
||||
allBounds.SetMinMax(min, max);
|
||||
|
||||
return meshVertices;
|
||||
}
|
||||
|
||||
private static void AddMeshToMD3(Transform meshTransform, bool isSkinnedMesh, Mesh unityMesh, Material[] materials, List<MD3Surface> surfaces, MaterialNameLookup materialNameLookup, ref int allSurfacesSize, ref Bounds allBounds)
|
||||
{
|
||||
// Gather triangles per submesh
|
||||
List<int[]> submeshTriIndices = new(unityMesh.subMeshCount);
|
||||
for (int i = 0; i < unityMesh.subMeshCount; i++)
|
||||
{
|
||||
submeshTriIndices.Add(unityMesh.GetTriangles(i));
|
||||
}
|
||||
|
||||
// Gather mesh data
|
||||
Vector3[] meshVertices = GetTransformedMeshVertices(meshTransform, isSkinnedMesh, unityMesh, ref allBounds);
|
||||
Vector3[] meshNormals = unityMesh.normals;
|
||||
Vector2[] meshUVs = unityMesh.uv;
|
||||
|
||||
// Write one surface per Unity submesh
|
||||
for (int submeshIdx = 0; submeshIdx < unityMesh.subMeshCount; submeshIdx++)
|
||||
{
|
||||
if (submeshTriIndices[submeshIdx].Length >= MAX_SURF_INDICES)
|
||||
{
|
||||
Debug.LogError("Mesh has too many surfaces - skipping!");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add verts and triangles
|
||||
int vertIndexCount = submeshTriIndices[submeshIdx].Length;
|
||||
|
||||
Vector3[] surfaceVertices = new Vector3[vertIndexCount];
|
||||
Vector3[] surfaceNormals = new Vector3[vertIndexCount];
|
||||
Vector2[] surfaceUVs = new Vector2[vertIndexCount];
|
||||
|
||||
int[] thisSubmeshTriIndices = submeshTriIndices[submeshIdx];
|
||||
for (int vIdx = 0; vIdx < vertIndexCount; vIdx++)
|
||||
{
|
||||
int index = thisSubmeshTriIndices[vIdx];
|
||||
|
||||
surfaceVertices[vIdx] = meshVertices[index];
|
||||
surfaceNormals[vIdx] = meshNormals[index];
|
||||
|
||||
// Some meshes have no UVs (uh-oh...)
|
||||
if (meshUVs.Length > 0)
|
||||
{
|
||||
surfaceUVs[vIdx] = meshUVs[index];
|
||||
}
|
||||
}
|
||||
|
||||
// Write material names
|
||||
string textureName;
|
||||
if (materials[submeshIdx] && materialNameLookup != null)
|
||||
{
|
||||
string path = TrembleAssetLoader.GetPath(materials[submeshIdx]);
|
||||
textureName = materialNameLookup.GetPrefabNameFromMaterialPath(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
textureName = "missing";
|
||||
}
|
||||
|
||||
// Create surface
|
||||
MD3Surface currentSurface = new()
|
||||
{
|
||||
Name = $"surface{surfaces.Count}",
|
||||
Texture = textureName,
|
||||
|
||||
Vertices = surfaceVertices,
|
||||
Normals = surfaceNormals,
|
||||
Uvs = surfaceUVs,
|
||||
};
|
||||
|
||||
surfaces.Add(currentSurface);
|
||||
allSurfacesSize += currentSurface.DataSize;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPathInFolder(string path, string folder)
|
||||
{
|
||||
int i = path.IndexOfInvariant(folder + "/");
|
||||
return path.Substring(i + folder.Length + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6df817cee22fba4bb61fab7a3e48367
|
||||
timeCreated: 1628505675
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/MD3Util.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,468 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Experimental.Rendering;
|
||||
using UnityEngine.Rendering;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
#if HDRP_INSTALLED
|
||||
using File = System.IO.File;
|
||||
#endif
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
/// <summary>
|
||||
/// Camera/lighting setup for taking material screenshots. Do not use directly.
|
||||
/// </summary>
|
||||
public class MapTextureSnapshotter
|
||||
{
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
// State
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
private GameObject m_SceneRoot;
|
||||
private Camera m_Camera;
|
||||
private MeshRenderer m_Renderer;
|
||||
|
||||
private readonly Dictionary<Vector2Int, TexturePair> m_RenderTextures = new();
|
||||
private Texture2D m_CaptureTexture;
|
||||
private Light m_StudioLight;
|
||||
|
||||
private AmbientMode m_OriginalAmbientMode;
|
||||
private Color m_OriginalAmbientColour;
|
||||
private float m_OriginalAmbientIntensity;
|
||||
|
||||
private readonly List<GameObject> m_Lights = new();
|
||||
|
||||
private TrembleSyncSettings m_Settings;
|
||||
|
||||
private struct TexturePair
|
||||
{
|
||||
public Texture2D Texture;
|
||||
public RenderTexture RenderTexture;
|
||||
}
|
||||
|
||||
public void Init(TrembleSyncSettings settings)
|
||||
{
|
||||
m_Settings = settings;
|
||||
|
||||
SetupGameObjects(m_Settings.MaterialCaptureLightIntensity, m_Settings.MaterialCaptureLightAngle);
|
||||
|
||||
List<Vector2Int> resolutions = new()
|
||||
{
|
||||
m_Settings.MaterialExportSize.ToVector2IntSize()
|
||||
};
|
||||
|
||||
foreach (Material material in m_Settings.GetMaterialGroupsOrDefault().SelectMany(mg => mg.Materials))
|
||||
{
|
||||
MaterialRenderData renderData = m_Settings.GetMaterialRenderData(material);
|
||||
Vector2Int dataRes = new(renderData.ResolutionX.ToIntSize(), renderData.ResolutionY.ToIntSize());
|
||||
|
||||
if (!resolutions.Contains(dataRes))
|
||||
resolutions.Add(dataRes);
|
||||
|
||||
if (!material.TryGetMainTex(out Texture2D tex))
|
||||
continue;
|
||||
|
||||
Vector2Int res = new(tex.width, tex.height);
|
||||
if (!resolutions.Contains(res))
|
||||
resolutions.Add(res);
|
||||
}
|
||||
|
||||
foreach (Vector2Int res in resolutions)
|
||||
{
|
||||
GetOrCreateTexturePair(res);
|
||||
}
|
||||
|
||||
// Move far away!
|
||||
m_SceneRoot.transform.position = Vector3.up * 10000f;
|
||||
|
||||
// Set up lighting
|
||||
m_OriginalAmbientMode = RenderSettings.ambientMode;
|
||||
m_OriginalAmbientColour = RenderSettings.ambientLight;
|
||||
m_OriginalAmbientIntensity = RenderSettings.ambientIntensity;
|
||||
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
foreach (Light foundLight in GameObject.FindObjectsByType<Light>(FindObjectsInactive.Exclude))
|
||||
#elif UNITY_2022_1_OR_NEWER
|
||||
foreach (Light foundLight in GameObject.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
|
||||
#else
|
||||
foreach (Light foundLight in GameObject.FindObjectsOfType<Light>(false))
|
||||
#endif
|
||||
{
|
||||
if (!foundLight.gameObject.activeSelf || foundLight == m_StudioLight)
|
||||
continue;
|
||||
|
||||
m_Lights.Add(foundLight.gameObject);
|
||||
foundLight.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
RenderSettings.ambientMode = AmbientMode.Flat;
|
||||
RenderSettings.ambientLight = Color.white;
|
||||
}
|
||||
|
||||
public void DeInit()
|
||||
{
|
||||
GameObject.DestroyImmediate(m_SceneRoot);
|
||||
|
||||
if (m_RenderTextures.Values.Any(x => RenderTexture.active == x.RenderTexture))
|
||||
{
|
||||
RenderTexture.active = null;
|
||||
}
|
||||
|
||||
// Release RT next frame - it seems if it's active above, it's not
|
||||
// cleared/unset until next frame causing an error (:
|
||||
foreach (TexturePair texPair in m_RenderTextures.Values)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (texPair.RenderTexture)
|
||||
{
|
||||
#endif
|
||||
texPair.RenderTexture.Release();
|
||||
#if UNITY_EDITOR
|
||||
}
|
||||
};
|
||||
#endif
|
||||
}
|
||||
m_RenderTextures.Clear();
|
||||
|
||||
foreach (GameObject foundLight in m_Lights)
|
||||
{
|
||||
foundLight.SetActive(true);
|
||||
}
|
||||
m_Lights.Clear();
|
||||
|
||||
// Restore lighting
|
||||
RenderSettings.ambientMode = m_OriginalAmbientMode;
|
||||
RenderSettings.ambientLight = m_OriginalAmbientColour;
|
||||
RenderSettings.ambientIntensity = m_OriginalAmbientIntensity;
|
||||
}
|
||||
|
||||
private TexturePair GetOrCreateTexturePair(Vector2Int resolution)
|
||||
{
|
||||
if (!m_RenderTextures.TryGetValue(resolution, out TexturePair texturePair))
|
||||
{
|
||||
RenderTexture rt = new(resolution.x, resolution.y, 8, GraphicsFormat.R8G8B8A8_UNorm);
|
||||
rt.Create();
|
||||
|
||||
Texture2D output = new(resolution.x, resolution.y, rt.graphicsFormat, TextureCreationFlags.None);
|
||||
|
||||
m_RenderTextures[resolution] = texturePair = new()
|
||||
{
|
||||
Texture = output,
|
||||
RenderTexture = rt,
|
||||
};
|
||||
}
|
||||
|
||||
return texturePair;
|
||||
}
|
||||
|
||||
public void SnapshotMaterial(Material m, string outPath)
|
||||
{
|
||||
MaterialRenderData renderData = m_Settings.GetMaterialRenderData(m);
|
||||
|
||||
Vector2Int res = renderData.GetResolutionOrDefault();
|
||||
|
||||
Texture2D mainTex = null;
|
||||
if (renderData.ExportMode == MaterialImportMode.Dynamic)
|
||||
{
|
||||
if (!m.TryGetMainTex(out mainTex))
|
||||
{
|
||||
renderData.ExportMode = MaterialImportMode.FixedSize;
|
||||
Debug.LogWarning($"Couldn't find a Main Texture for material \"{m.name}\"," +
|
||||
"falling back to legacy render mode." +
|
||||
"Make sure the shader has a texture marked as [Main Texture] or" +
|
||||
"a texture named _MainTex, _BaseColor, or _Color.");
|
||||
}
|
||||
|
||||
if (mainTex && !renderData.IsResolutionOverridden)
|
||||
{
|
||||
res = new(mainTex.width, mainTex.height);
|
||||
}
|
||||
}
|
||||
|
||||
TexturePair tex = GetOrCreateTexturePair(res);
|
||||
|
||||
// Do not remove - these are used in HDRP
|
||||
bool createdTempMaterial = false;
|
||||
Material tempMaterial = null;
|
||||
|
||||
switch (renderData.ExportMode)
|
||||
{
|
||||
case MaterialImportMode.FixedSize:
|
||||
// Set up RT and material - we have to save it for HDRP
|
||||
createdTempMaterial = TryCreateUnlitCopy(m, out tempMaterial, renderData.ExportMode);
|
||||
|
||||
#if HDRP_INSTALLED && UNITY_EDITOR
|
||||
if (createdTempMaterial)
|
||||
{
|
||||
string tempName = Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(m)) + "_TrembleTemp.mat";
|
||||
string tempPath = Path.Combine("Assets", tempName);
|
||||
AssetDatabase.CreateAsset(tempMaterial, tempPath);
|
||||
}
|
||||
#endif
|
||||
|
||||
m_Renderer.material = tempMaterial ? tempMaterial : m;
|
||||
|
||||
// Render once
|
||||
m_Camera.targetTexture = tex.RenderTexture;
|
||||
m_Camera.Render();
|
||||
|
||||
if (RenderTexture.active != tex.RenderTexture)
|
||||
RenderTexture.active = tex.RenderTexture;
|
||||
|
||||
// Copy texture from RT
|
||||
tex.Texture.ReadPixels(new(0, 0, res.x, res.y), 0, 0);
|
||||
File.WriteAllBytes(outPath, tex.Texture.EncodeToPNG());
|
||||
|
||||
break;
|
||||
case MaterialImportMode.Dynamic:
|
||||
#if UNITY_EDITOR
|
||||
string path = AssetDatabase.GetAssetPath(mainTex);
|
||||
|
||||
//TODO(jwf): colourise by main colour?
|
||||
|
||||
// Is it a PNG, and we're not overriding resolution? Just copy it directly
|
||||
if (!renderData.IsResolutionOverridden && path.EndsWithInvariant(".png"))
|
||||
{
|
||||
File.Copy(Application.dataPath + path.Remove(0, 6), outPath, true);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Else we make a copy to resize
|
||||
m_Camera.targetTexture = null;
|
||||
GL.sRGBWrite = true;
|
||||
Graphics.Blit(mainTex, tex.RenderTexture);
|
||||
|
||||
RenderTexture.active = tex.RenderTexture;
|
||||
|
||||
// Copy texture from RT
|
||||
tex.Texture.ReadPixels(new(0, 0, res.x, res.y), 0, 0);
|
||||
File.WriteAllBytes(outPath, tex.Texture.EncodeToPNG());
|
||||
break;
|
||||
}
|
||||
|
||||
// Delete temp material, if we made one
|
||||
#if HDRP_INSTALLED && UNITY_EDITOR
|
||||
if (createdTempMaterial)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(tempMaterial));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> s_ShaderRemaps = new()
|
||||
{
|
||||
{ "Standard", "Legacy Shaders/Diffuse" }, // or Particles/Standard Unlit?
|
||||
{ "Universal Render Pipeline/Lit", "Universal Render Pipeline/Unlit" },
|
||||
{ "HDRP/Lit", "HDRP/Unlit" }
|
||||
};
|
||||
|
||||
private bool TryCreateUnlitCopy(Material material, out Material newMaterial, MaterialImportMode mode)
|
||||
{
|
||||
// No lights in the scene. Assume we don't need to tweak materials as they're using ambient colour
|
||||
if (mode is MaterialImportMode.Dynamic || (m_Lights.Count == 0 && mode is MaterialImportMode.FixedSize))
|
||||
{
|
||||
newMaterial = material;
|
||||
return false;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
foreach ((string originalShader, string remapShader) in s_ShaderRemaps)
|
||||
{
|
||||
if (!material.shader.name.Equals(originalShader))
|
||||
continue;
|
||||
|
||||
newMaterial = mode is MaterialImportMode.FixedSize ? new(Shader.Find(remapShader)) : new(Shader.Find(originalShader));
|
||||
CopyMaterialProperties_Recursive(material, newMaterial);
|
||||
|
||||
// Ensure colours/textures set properly
|
||||
newMaterial.color = material.color;
|
||||
newMaterial.mainTexture = material.mainTexture;
|
||||
|
||||
// Render after post-processing (mostly for HDRP)
|
||||
newMaterial.renderQueue = 2501;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
newMaterial = material;
|
||||
return false;
|
||||
}
|
||||
|
||||
[Conditional("UNITY_EDITOR")]
|
||||
private static void CopyMaterialProperties_Recursive(Material from, Material to)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
if (from.parent)
|
||||
{
|
||||
// If parent, copy that one first!
|
||||
CopyMaterialProperties_Recursive(from.parent, to);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Copy shader keywords
|
||||
to.shaderKeywords = from.shaderKeywords;
|
||||
|
||||
// Iterate over all properties of the material
|
||||
Shader shader = from.shader;
|
||||
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
int propertyCount = shader.GetPropertyCount();
|
||||
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
{
|
||||
// Get the property type and name
|
||||
ShaderPropertyType propertyType = shader.GetPropertyType(i);
|
||||
string propertyName = shader.GetPropertyName(i);
|
||||
|
||||
// Copy the property value based on its type
|
||||
switch (propertyType)
|
||||
{
|
||||
case ShaderPropertyType.Color:
|
||||
to.SetColor(propertyName, from.GetColor(propertyName));
|
||||
break;
|
||||
case ShaderPropertyType.Vector:
|
||||
to.SetVector(propertyName, from.GetVector(propertyName));
|
||||
break;
|
||||
case ShaderPropertyType.Float:
|
||||
case ShaderPropertyType.Range:
|
||||
to.SetFloat(propertyName, from.GetFloat(propertyName));
|
||||
break;
|
||||
case ShaderPropertyType.Texture:
|
||||
to.SetTexture(propertyName, from.GetTexture(propertyName));
|
||||
to.SetTextureOffset(propertyName, from.GetTextureOffset(propertyName));
|
||||
to.SetTextureScale(propertyName, from.GetTextureScale(propertyName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
#else
|
||||
int propertyCount = ShaderUtil.GetPropertyCount(shader);
|
||||
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
{
|
||||
// Get the property type and name
|
||||
ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType(shader, i);
|
||||
string propertyName = ShaderUtil.GetPropertyName(shader, i);
|
||||
|
||||
// Copy the property value based on its type
|
||||
switch (propertyType)
|
||||
{
|
||||
case ShaderUtil.ShaderPropertyType.Color:
|
||||
to.SetColor(propertyName, from.GetColor(propertyName));
|
||||
break;
|
||||
case ShaderUtil.ShaderPropertyType.Vector:
|
||||
to.SetVector(propertyName, from.GetVector(propertyName));
|
||||
break;
|
||||
case ShaderUtil.ShaderPropertyType.Float:
|
||||
case ShaderUtil.ShaderPropertyType.Range:
|
||||
to.SetFloat(propertyName, from.GetFloat(propertyName));
|
||||
break;
|
||||
case ShaderUtil.ShaderPropertyType.TexEnv:
|
||||
to.SetTexture(propertyName, from.GetTexture(propertyName));
|
||||
to.SetTextureOffset(propertyName, from.GetTextureOffset(propertyName));
|
||||
to.SetTextureScale(propertyName, from.GetTextureScale(propertyName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Copy render queue and other settings
|
||||
to.renderQueue = from.renderQueue;
|
||||
to.enableInstancing = from.enableInstancing;
|
||||
to.doubleSidedGI = from.doubleSidedGI;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
private void SetupGameObjects(float lightIntensity, float lightAngle)
|
||||
{
|
||||
m_SceneRoot = new("Scene Root");
|
||||
|
||||
// Set up gameobjects
|
||||
GameObject snapCamera = new("Camera");
|
||||
snapCamera.transform.SetParent(m_SceneRoot.transform, false);
|
||||
snapCamera.transform.localPosition = new(0f, 0f, -0.1f);
|
||||
|
||||
m_Camera = snapCamera.AddComponent<Camera>();
|
||||
m_Camera.orthographic = true;
|
||||
m_Camera.allowHDR = false;
|
||||
m_Camera.orthographicSize = 0.5f;
|
||||
m_Camera.nearClipPlane = 0.01f;
|
||||
m_Camera.farClipPlane = 0.5f;
|
||||
m_Camera.backgroundColor = Color.black;
|
||||
m_Camera.clearFlags = CameraClearFlags.Color;
|
||||
|
||||
Vector3 lightDirection = Vector3.Lerp(Vector3.right, Vector3.forward, lightAngle);
|
||||
|
||||
GameObject snapLight = new("Light");
|
||||
snapLight.transform.SetParent(m_SceneRoot.transform, false);
|
||||
snapLight.transform.localPosition = new(0f, 0f, -0.1f);
|
||||
snapLight.transform.rotation = Quaternion.LookRotation(lightDirection);
|
||||
m_StudioLight = snapLight.AddComponent<Light>();
|
||||
m_StudioLight.type = LightType.Directional;
|
||||
m_StudioLight.intensity = lightIntensity;
|
||||
m_StudioLight.shadows = LightShadows.None;
|
||||
m_StudioLight.color = Color.white;
|
||||
|
||||
GameObject snapQuad = new("Quad");
|
||||
snapQuad.transform.SetParent(m_SceneRoot.transform, false);
|
||||
snapQuad.transform.localPosition = Vector3.zero;
|
||||
|
||||
MeshFilter meshFilter = snapQuad.AddComponent<MeshFilter>();
|
||||
meshFilter.sharedMesh = CreateQuadMesh();
|
||||
|
||||
m_Renderer = snapQuad.AddComponent<MeshRenderer>();
|
||||
}
|
||||
|
||||
private Mesh CreateQuadMesh()
|
||||
{
|
||||
Mesh quadMesh = new()
|
||||
{
|
||||
name = "Quad",
|
||||
vertices = new Vector3[]
|
||||
{
|
||||
new(-0.5f, -0.5f, 0f),
|
||||
new(-0.5f, +0.5f, 0f),
|
||||
new(+0.5f, +0.5f, 0f),
|
||||
new(+0.5f, -0.5f, 0f),
|
||||
},
|
||||
normals = new Vector3[]
|
||||
{
|
||||
new(0f, 0f, -1f),
|
||||
new(0f, 0f, -1f),
|
||||
new(0f, 0f, -1f),
|
||||
new(0f, 0f, -1f),
|
||||
},
|
||||
uv = new Vector2[]
|
||||
{
|
||||
new(0f, 0f),
|
||||
new(0f, 1f),
|
||||
new(1f, 1f),
|
||||
new(1f, 0f),
|
||||
},
|
||||
};
|
||||
|
||||
List<int> indices = new() { 0, 1, 2, 0, 2, 3 };
|
||||
quadMesh.SetIndices(indices, MeshTopology.Triangles, 0);
|
||||
return quadMesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd6405c6b01049ba9a15b8f11f80f46d
|
||||
timeCreated: 1707139093
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/MapTextureSnapshotter.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,451 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public class MeshBuilder
|
||||
{
|
||||
public delegate Material FindMaterialFunc(string name);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
private readonly TrembleMapImportSettings m_ImportSettings;
|
||||
private readonly string m_WorldspawnName;
|
||||
private readonly float m_SmoothingAngle;
|
||||
private readonly float m_MaxMeshSurfaceArea;
|
||||
private readonly FindMaterialFunc m_MaterialFinder;
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
// State
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
private int m_MeshCounter;
|
||||
|
||||
private readonly List<int> m_FaceIndicesBuffer = new();
|
||||
private readonly List<BspMeshVertex> m_MeshVerticesBuffer = new(2048);
|
||||
private readonly List<BspSubMesh> m_MeshSubmeshesBuffer = new(16);
|
||||
private readonly Dictionary<int, int> m_BaseIndexToIndex = new(2048);
|
||||
|
||||
public MeshBuilder(TrembleMapImportSettings importSettings, FindMaterialFunc materialFinder, string worldspawnName, float smoothingAngle, float maxMeshSurfaceArea)
|
||||
{
|
||||
m_ImportSettings = importSettings;
|
||||
m_WorldspawnName = worldspawnName;
|
||||
m_SmoothingAngle = smoothingAngle;
|
||||
m_MaxMeshSurfaceArea = maxMeshSurfaceArea;
|
||||
m_MaterialFinder = materialFinder;
|
||||
}
|
||||
|
||||
private static readonly HashSet<int> s_InnerFaceList = new(2048);
|
||||
public void BuildMesh_Recursive(MapBsp mapBsp, Transform parent, int nodeID, HashSet<int> faceList, float importScale, int unityLayer, List<GameObject> intoListOrNull = null)
|
||||
{
|
||||
// Not a leaf - recurse unless smol
|
||||
if (nodeID >= 0)
|
||||
{
|
||||
bool subdivide = TrembleSyncSettings.Get().SubdivideAllLeafs;
|
||||
ref BspNode node = ref mapBsp.Nodes[nodeID];
|
||||
|
||||
if (!subdivide)
|
||||
{
|
||||
float sqrSurfArea = 0;
|
||||
BspParser.GetSurfaceAreaSqr(mapBsp, node, ref sqrSurfArea, faceList);
|
||||
|
||||
subdivide = (sqrSurfArea > m_MaxMeshSurfaceArea * m_MaxMeshSurfaceArea);
|
||||
}
|
||||
|
||||
if (subdivide)
|
||||
{
|
||||
s_InnerFaceList.Clear();
|
||||
BuildMesh_Recursive(mapBsp, parent, node.Child1, s_InnerFaceList, importScale, unityLayer, intoListOrNull);
|
||||
BuildMesh_Recursive(mapBsp, parent, node.Child2, s_InnerFaceList, importScale, unityLayer, intoListOrNull);
|
||||
|
||||
// Don't build this one
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a leaf, or as small as we want to go - create!
|
||||
Bounds bounds = new();
|
||||
if (nodeID < 0)
|
||||
{
|
||||
bounds.min = new Vector3(mapBsp.Leaves[-nodeID - 1].Min.x, mapBsp.Leaves[-nodeID - 1].Min.z, mapBsp.Leaves[-nodeID - 1].Min.y) * importScale;
|
||||
bounds.max = new Vector3(mapBsp.Leaves[-nodeID - 1].Max.x, mapBsp.Leaves[-nodeID - 1].Max.z, mapBsp.Leaves[-nodeID - 1].Max.y) * importScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds.min = new Vector3(mapBsp.Nodes[nodeID].Min.x, mapBsp.Nodes[nodeID].Min.z, mapBsp.Nodes[nodeID].Min.y) * importScale;
|
||||
bounds.max = new Vector3(mapBsp.Nodes[nodeID].Max.x, mapBsp.Nodes[nodeID].Max.z, mapBsp.Nodes[nodeID].Max.y) * importScale;
|
||||
}
|
||||
|
||||
GameObject meshGo = BuildMesh(mapBsp, parent, nodeID, true, m_WorldspawnName, true, bounds.center, true);
|
||||
if (meshGo)
|
||||
{
|
||||
meshGo.layer = unityLayer;
|
||||
intoListOrNull?.Add(meshGo);
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject BuildMesh(MapBsp mapBsp, Transform parent, int sourceID, bool isBspRoot, string modelName, bool isFromNode, Vector3 origin, bool isStatic, Quaternion? rotation = null, float smoothingAngleOverride = -1f)
|
||||
{
|
||||
// Thank you again John Evans. This method would have been awful to figure out by myself!
|
||||
|
||||
// Collect face indices
|
||||
m_FaceIndicesBuffer.Clear();
|
||||
if (isFromNode)
|
||||
{
|
||||
if (sourceID < 0)
|
||||
{
|
||||
BspParser.GetFaceList(mapBsp, mapBsp.Leaves[-sourceID - 1], m_FaceIndicesBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
BspParser.GetFaceList(mapBsp, mapBsp.Nodes[sourceID], m_FaceIndicesBuffer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < mapBsp.Models[sourceID].NumFaces; i++)
|
||||
{
|
||||
m_FaceIndicesBuffer.Add(mapBsp.Models[sourceID].StartFaceIdx + i);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_FaceIndicesBuffer.Count == 0)
|
||||
return null;
|
||||
|
||||
// Gather vertices and submeshes
|
||||
m_MeshVerticesBuffer.Clear();
|
||||
m_MeshSubmeshesBuffer.Clear();
|
||||
m_BaseIndexToIndex.Clear();
|
||||
|
||||
// Get faces for this mesh
|
||||
foreach (BspFace face in m_FaceIndicesBuffer.Select(faceIdx => mapBsp.Faces[faceIdx]))
|
||||
{
|
||||
// Get each vertex for this face
|
||||
for (int faceVertIdx = face.MeshVertexStartIdx; faceVertIdx < face.MeshVertexStartIdx + face.NumMeshVertices; faceVertIdx++)
|
||||
{
|
||||
int baseIdx = face.VertexStartIdx + mapBsp.MeshVertices[faceVertIdx];
|
||||
|
||||
// Find or add a vertex to this mesh
|
||||
int localVertIndex = FindOrAddVertexForBaseIndex(baseIdx, mapBsp, origin, rotation);
|
||||
AddVertexToSubmeshWithTexID(m_MeshSubmeshesBuffer, face.TexID, localVertIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Discover and apply materials
|
||||
List<Material> meshMaterials = new(m_MeshSubmeshesBuffer.Count);
|
||||
{
|
||||
for (int materialIdx = 0; materialIdx < m_MeshSubmeshesBuffer.Count; materialIdx++)
|
||||
{
|
||||
string matName = mapBsp.TexInfos[m_MeshSubmeshesBuffer[materialIdx].TexID].Name;
|
||||
|
||||
// Skip? Skip!
|
||||
if (matName.ContainsInvariant(TBConsts.SKIP_TEXTURE, caseSensitive: true))
|
||||
{
|
||||
m_MeshSubmeshesBuffer.RemoveAt(materialIdx);
|
||||
materialIdx--;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Material foundMaterial = m_MaterialFinder.Invoke(matName);
|
||||
meshMaterials.Add(foundMaterial);
|
||||
|
||||
if (foundMaterial)
|
||||
{
|
||||
m_ImportSettings.AddDependency(foundMaterial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create Unity mesh
|
||||
Mesh mesh = CreateUnityMesh(isBspRoot, ref modelName, m_MeshVerticesBuffer, m_MeshSubmeshesBuffer, smoothingAngleOverride);
|
||||
|
||||
bool isTinyMesh = mesh.triangles.Length == 0 || Mathf.Max(mesh.bounds.extents.x, mesh.bounds.extents.y, mesh.bounds.extents.z) < 0.1f;
|
||||
if (isTinyMesh)
|
||||
return null;
|
||||
|
||||
m_ImportSettings.SaveObjectInMap(mesh.name, mesh);
|
||||
|
||||
// Create model GameObject and attach mesh
|
||||
GameObject modelObject = new(modelName);
|
||||
modelObject.transform.parent = parent;
|
||||
modelObject.transform.position = origin;
|
||||
modelObject.AddComponent<MeshFilter>().mesh = mesh;
|
||||
modelObject.AddComponent<MeshRenderer>().sharedMaterials = meshMaterials.ToArray();
|
||||
|
||||
TrembleSyncSettings syncSettings = TrembleSyncSettings.Get();
|
||||
|
||||
// Generate mesh collider
|
||||
Mesh collisionMesh;
|
||||
if (syncSettings.PipelineSimplifyCollisionMeshes)
|
||||
{
|
||||
using TrembleTimerScope _ = new(TrembleTimer.Context.SimplifyCollisionMeshes);
|
||||
collisionMesh = GenerateSimplifiedUnityCollisionMesh(mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
collisionMesh = Mesh.Instantiate(mesh);
|
||||
}
|
||||
|
||||
collisionMesh.name = mesh.name.Replace("_mesh", "_collision");
|
||||
|
||||
m_ImportSettings.SaveObjectInMap(collisionMesh.name, collisionMesh);
|
||||
modelObject.AddComponent<MeshCollider>().sharedMesh = collisionMesh;
|
||||
|
||||
// Calculate lightmap UVs
|
||||
#if UNITY_EDITOR
|
||||
if (isStatic && syncSettings.PipelineUnwrapUV2)
|
||||
{
|
||||
using TrembleTimerScope _ = new(TrembleTimer.Context.GenerateUV2);
|
||||
Unwrapping.GenerateSecondaryUVSet(mesh, new()
|
||||
{
|
||||
hardAngle = 70.0f,
|
||||
angleError = 8.0f,
|
||||
areaError = 15.0f,
|
||||
packMargin = mapBsp.Options.UV2Padding,
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
modelObject.isStatic = isStatic;
|
||||
return modelObject;
|
||||
}
|
||||
|
||||
private Mesh CreateUnityMesh(bool isBspRoot, ref string modelName, List<BspMeshVertex> meshVertices, List<BspSubMesh> meshSubmeshes, float smoothingAngleOverride)
|
||||
{
|
||||
IndexFormat meshFormat = IndexFormat.UInt16;
|
||||
foreach (BspSubMesh submesh in meshSubmeshes)
|
||||
{
|
||||
if (submesh.Triangles.Count * 3 > 65535)
|
||||
{
|
||||
Debug.Log($"Note: Mesh '{modelName}' using 32-bit indices due to high index count. Render performance may be reduced!");
|
||||
meshFormat = IndexFormat.UInt32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Unity mesh
|
||||
Mesh mesh = new()
|
||||
{
|
||||
indexFormat = meshFormat,
|
||||
subMeshCount = meshSubmeshes.Count,
|
||||
vertices = meshVertices.Select(mv => mv.Position).ToArray(),
|
||||
normals = meshVertices.Select(mv => mv.Normal).ToArray(),
|
||||
uv = meshVertices.Select(mv => mv.UV).ToArray(),
|
||||
};
|
||||
|
||||
for (int i = 0; i < meshSubmeshes.Count; i++)
|
||||
{
|
||||
mesh.SetTriangles(meshSubmeshes[i].Triangles.ToArray(), i);
|
||||
}
|
||||
mesh.RecalculateBounds();
|
||||
mesh.RecalculateTangents();
|
||||
|
||||
// Name the mesh
|
||||
if (isBspRoot)
|
||||
{
|
||||
modelName += $"_{++m_MeshCounter}";
|
||||
mesh.name = $"{modelName}_mesh";
|
||||
}
|
||||
else
|
||||
{
|
||||
mesh.name = $"{modelName}_{++m_MeshCounter}_mesh";
|
||||
}
|
||||
|
||||
float smoothingAngle = m_SmoothingAngle;
|
||||
if (smoothingAngleOverride >= 0f)
|
||||
smoothingAngle = smoothingAngleOverride;
|
||||
|
||||
// Smooth out normals if required
|
||||
if (smoothingAngle > 0 && TrembleSyncSettings.Get().PipelineSmoothMeshNormals)
|
||||
{
|
||||
using TrembleTimerScope _ = new(TrembleTimer.Context.SmoothMeshNormals);
|
||||
SmoothMeshNormals(mesh, smoothingAngle);
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
private int FindOrAddVertexForBaseIndex(int baseIdx, MapBsp mapBsp, Vector3 origin, Quaternion? rotation)
|
||||
{
|
||||
// Find existing value
|
||||
if (m_BaseIndexToIndex.TryGetValue(baseIdx, out int currentMeshVertIdx))
|
||||
return currentMeshVertIdx;
|
||||
|
||||
Vector3 position = mapBsp.Vertices[baseIdx].Position - origin;
|
||||
Vector3 normal = mapBsp.Vertices[baseIdx].Normal;
|
||||
|
||||
if (rotation != null)
|
||||
{
|
||||
position = rotation.Value * position;
|
||||
normal = rotation.Value * normal;
|
||||
}
|
||||
|
||||
// Not found, add a new one
|
||||
BspMeshVertex currentMeshVertex = new()
|
||||
{
|
||||
Position = position,
|
||||
Normal = normal,
|
||||
UV = mapBsp.Vertices[baseIdx].TexCoord + new Vector2(0.5f, 0.5f),
|
||||
BaseIndex = baseIdx
|
||||
};
|
||||
m_MeshVerticesBuffer.Add(currentMeshVertex);
|
||||
m_BaseIndexToIndex.Add(baseIdx, m_MeshVerticesBuffer.Count - 1);
|
||||
return m_MeshVerticesBuffer.Count - 1;
|
||||
}
|
||||
|
||||
private static void AddVertexToSubmeshWithTexID(List<BspSubMesh> meshSubmeshes, int texID, int localVertexIdx)
|
||||
{
|
||||
// Find a submesh that uses this texture ID
|
||||
foreach (BspSubMesh subMesh in meshSubmeshes)
|
||||
{
|
||||
if (texID != subMesh.TexID)
|
||||
continue;
|
||||
|
||||
subMesh.Triangles.Add(localVertexIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Not found, make a new submesh for this face
|
||||
meshSubmeshes.Add(new()
|
||||
{
|
||||
TexID = texID,
|
||||
Triangles = new() { localVertexIdx },
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
// Utilities
|
||||
// -----------------------------------------------------------------------------------------------------------------------------
|
||||
private static void SmoothMeshNormals(Mesh mesh, float maxAngle = 45.0f)
|
||||
{
|
||||
Vector3[] newVertices = mesh.vertices;
|
||||
Vector3[] newNormals = mesh.normals;
|
||||
|
||||
Vector3[] originalNormals = new Vector3[newNormals.Length];
|
||||
Array.Copy(newNormals, originalNormals, newNormals.Length);
|
||||
|
||||
float cosThreshold = Mathf.Cos(Mathf.Deg2Rad * maxAngle);
|
||||
|
||||
for (int i = 0; i < newVertices.Length; i++)
|
||||
{
|
||||
int groupCount = 1;
|
||||
Vector3 currentNormal = originalNormals[i];
|
||||
|
||||
for (int j = 0; j < newVertices.Length; j++)
|
||||
{
|
||||
if (i == j)
|
||||
continue;
|
||||
|
||||
// Inlined SqrMagnitude to avoid Vector3 .ctor
|
||||
ref Vector3 from = ref newVertices[i];
|
||||
ref Vector3 to = ref newVertices[j];
|
||||
|
||||
float xDiff = from.x - to.x;
|
||||
float yDiff = from.y - to.y;
|
||||
float zDiff = from.z - to.z;
|
||||
float sqrDistance = xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
|
||||
|
||||
if (sqrDistance >= 0.01f * 0.01f)
|
||||
continue;
|
||||
|
||||
// Merge verts!
|
||||
to = from;
|
||||
|
||||
// Also inline AngleBetween and avoid Acos for speed up... these 3 changes reduce from 700ms to about 20ms ;)
|
||||
float cosBetween = Vector3.Dot(originalNormals[i], originalNormals[j]);
|
||||
|
||||
if (cosBetween > cosThreshold)
|
||||
{
|
||||
currentNormal += originalNormals[j];
|
||||
groupCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (groupCount >= 2)
|
||||
{
|
||||
newNormals[i] = currentNormal / groupCount;
|
||||
}
|
||||
}
|
||||
|
||||
mesh.vertices = newVertices;
|
||||
mesh.normals = newNormals;
|
||||
mesh.RecalculateBounds();
|
||||
mesh.RecalculateTangents();
|
||||
}
|
||||
|
||||
private static Mesh GenerateSimplifiedUnityCollisionMesh(Mesh inputMesh, float threshold = 0.01f)
|
||||
{
|
||||
Mesh outputMesh = new() { name = inputMesh.name};
|
||||
|
||||
Vector3[] inputVertices = inputMesh.vertices;
|
||||
int inputVertexCount = inputMesh.vertexCount;
|
||||
int[] inputTriangles = inputMesh.triangles;
|
||||
int[] vertexRemap = new int[inputVertexCount];
|
||||
|
||||
List<Vector3> newVertices = new() { inputVertices[0] };
|
||||
|
||||
float sqrThreshold = threshold * threshold;
|
||||
|
||||
for (int inputVertIdx = 0; inputVertIdx < inputVertexCount; inputVertIdx++)
|
||||
{
|
||||
bool wasRemapped = false;
|
||||
|
||||
float newVertCount = newVertices.Count;
|
||||
for (int newVertIdx = 0; newVertIdx < newVertCount; newVertIdx++)
|
||||
{
|
||||
Vector3 from = inputVertices[inputVertIdx];
|
||||
Vector3 to = newVertices[newVertIdx];
|
||||
|
||||
float xDiff = from.x - to.x;
|
||||
float yDiff = from.y - to.y;
|
||||
float zDiff = from.z - to.z;
|
||||
float sqrDistance = xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
|
||||
|
||||
if (sqrDistance > sqrThreshold)
|
||||
continue;
|
||||
|
||||
vertexRemap[inputVertIdx] = newVertIdx;
|
||||
wasRemapped = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!wasRemapped)
|
||||
{
|
||||
newVertices.Add(inputVertices[inputVertIdx]);
|
||||
vertexRemap[inputVertIdx] = newVertices.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create triangles array
|
||||
int[] newTriangles = new int[inputTriangles.Length];
|
||||
int triPtr = 0;
|
||||
|
||||
for (int i = 0; i < inputTriangles.Length; i += 3)
|
||||
{
|
||||
if (vertexRemap[inputTriangles[i]] == vertexRemap[inputTriangles[i + 1]] || vertexRemap[inputTriangles[i]] == vertexRemap[inputTriangles[i + 2]] || vertexRemap[inputTriangles[i + 1]] == vertexRemap[inputTriangles[i + 2]])
|
||||
continue;
|
||||
|
||||
newTriangles[triPtr++] = vertexRemap[inputTriangles[i + 0]];
|
||||
newTriangles[triPtr++] = vertexRemap[inputTriangles[i + 1]];
|
||||
newTriangles[triPtr++] = vertexRemap[inputTriangles[i + 2]];
|
||||
}
|
||||
|
||||
outputMesh.vertices = newVertices.ToArray();
|
||||
outputMesh.triangles = newTriangles;
|
||||
return outputMesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61078239862345b09915cb704095f22d
|
||||
timeCreated: 1709824324
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/MeshBuilder.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class NamingConventionUtil
|
||||
{
|
||||
public static string ToNamingConvention(this string input, NamingConvention convention)
|
||||
{
|
||||
switch (convention)
|
||||
{
|
||||
case NamingConvention.SnakeCase:
|
||||
return input.ToSnakeCase();
|
||||
case NamingConvention.UpperCamelCase:
|
||||
return CapitaliseFirstLetter(input, true);
|
||||
case NamingConvention.LowerCamelCase:
|
||||
return CapitaliseFirstLetter(input, false);
|
||||
case NamingConvention.HumanFriendly:
|
||||
return ToHuman(input);
|
||||
case NamingConvention.PreserveExact:
|
||||
return input;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private static readonly StringBuilder s_SnakeBuilder = new(128);
|
||||
private static string ToSnakeCase(this string inString)
|
||||
{
|
||||
s_SnakeBuilder.Clear();
|
||||
|
||||
for (int i = 0; i < inString.Length; i++)
|
||||
{
|
||||
if (inString[i] == ' ')
|
||||
{
|
||||
s_SnakeBuilder.Append('_');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Char.IsUpper(inString[i]) && i > 0)
|
||||
{
|
||||
// Not if we just output an underscore!
|
||||
if (s_SnakeBuilder[^1] != '_')
|
||||
{
|
||||
// If the previous character wasn't uppercase
|
||||
if (!Char.IsUpper(inString[i - 1]))
|
||||
{
|
||||
s_SnakeBuilder.Append('_');
|
||||
}
|
||||
// Or the next character isn't uppercase
|
||||
else if (i < inString.Length - 1 && Char.IsLower(inString[i + 1]))
|
||||
{
|
||||
s_SnakeBuilder.Append('_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s_SnakeBuilder.Append(Char.ToLowerInvariant(inString[i]));
|
||||
}
|
||||
return s_SnakeBuilder.ToString();
|
||||
}
|
||||
|
||||
private static string ToHuman(this string inString)
|
||||
{
|
||||
s_SnakeBuilder.Clear();
|
||||
|
||||
int firstIndex = inString.IndexOf('_') + 1;
|
||||
|
||||
for (int i = firstIndex; i < inString.Length; i++)
|
||||
{
|
||||
// Straight up convert underscores to spaces
|
||||
if (inString[i] == '_')
|
||||
{
|
||||
s_SnakeBuilder.Append(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Char.IsUpper(inString[i]) && i > firstIndex)
|
||||
{
|
||||
// Not if we just output a space!
|
||||
if (s_SnakeBuilder[^1] != ' ' && s_SnakeBuilder[^1] != '_')
|
||||
{
|
||||
// If the previous character wasn't uppercase
|
||||
if (!Char.IsUpper(inString[i - 1]))
|
||||
{
|
||||
s_SnakeBuilder.Append(' ');
|
||||
}
|
||||
// Or the next character isn't uppercase
|
||||
else if (i < inString.Length - 1 && Char.IsLower(inString[i + 1]))
|
||||
{
|
||||
s_SnakeBuilder.Append(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i == firstIndex || s_SnakeBuilder[^1] == ' ')
|
||||
{
|
||||
s_SnakeBuilder.Append(Char.ToUpperInvariant(inString[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
s_SnakeBuilder.Append(Char.ToLowerInvariant(inString[i]));
|
||||
}
|
||||
}
|
||||
return s_SnakeBuilder.ToString();
|
||||
}
|
||||
|
||||
public static string FromHuman(ReadOnlySpan<char> inString)
|
||||
{
|
||||
s_SnakeBuilder.Clear();
|
||||
|
||||
int inBrackets = 0;
|
||||
|
||||
for (int i = 0; i < inString.Length; i++)
|
||||
{
|
||||
if (inString[i] == '(') { inBrackets++; continue; }
|
||||
if (inString[i] == ')') { inBrackets--; continue; }
|
||||
|
||||
|
||||
if (!Char.IsLetter(inString[i]) && !Char.IsNumber(inString[i]) && inString[i] != '_')
|
||||
continue;
|
||||
|
||||
if (inBrackets > 0)
|
||||
continue;
|
||||
|
||||
if (i == 0 || inString[i - 1] == ' ')
|
||||
{
|
||||
s_SnakeBuilder.Append(Char.ToUpperInvariant(inString[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
s_SnakeBuilder.Append(inString[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return s_SnakeBuilder.ToString().Trim();
|
||||
}
|
||||
public static string FromHuman(string inString) => FromHuman(inString.AsSpan());
|
||||
|
||||
private static string CapitaliseFirstLetter(string input, bool capitalise)
|
||||
{
|
||||
if (input.IsNullOrEmpty())
|
||||
return "";
|
||||
|
||||
if (input.Length == 1)
|
||||
return capitalise ? input.ToUpperInvariant() : input.ToLowerInvariant();
|
||||
|
||||
return (capitalise ? Char.ToUpperInvariant(input[0]) : Char.ToLowerInvariant(input[0]))
|
||||
+ input.Substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 795d7b8deadc141e6a5a75234bb0b0b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/NamingConventionUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class PackedResourceUtil
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
public static async Task<string> PackFolderIntoResource(string folderPath, string resourceName)
|
||||
{
|
||||
if (!Directory.Exists(folderPath))
|
||||
return null;
|
||||
|
||||
string containingFolder = Path.Combine(Directory.GetCurrentDirectory(), "Assets", "StreamingAssets");
|
||||
string resourcePath = Path.Combine(containingFolder, resourceName + ".tpak");
|
||||
|
||||
DirectoryUtil.CreateAllDirectories(containingFolder);
|
||||
File.Delete(resourcePath);
|
||||
|
||||
TpakArchive tpak = new(resourcePath);
|
||||
tpak.AddDirectory(folderPath);
|
||||
await tpak.PackAsync();
|
||||
|
||||
return resourcePath;
|
||||
}
|
||||
#endif
|
||||
|
||||
public static async Task<bool> UnpackResourceIntoFolder(string resourceName, string folderPath)
|
||||
{
|
||||
// Empty directory
|
||||
DirectoryUtil.CreateAllDirectories(folderPath);
|
||||
|
||||
string resourcePath = Path.Combine(Application.streamingAssetsPath, resourceName + ".tpak");
|
||||
if (!File.Exists(resourcePath))
|
||||
return false;
|
||||
|
||||
TpakArchive tpak = new(resourcePath);
|
||||
await tpak.UnpackAsync(folderPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 309d4c10ad944b18a2e61762c8125acc
|
||||
timeCreated: 1715783554
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/PackedResourceUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class PrefabUtil
|
||||
{
|
||||
public static GameObject InstantiatePrefab(GameObject prefab, Transform parent = null)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
return Application.isPlaying
|
||||
? GameObject.Instantiate(prefab, parent)
|
||||
: (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
#else
|
||||
return GameObject.Instantiate(prefab, parent);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9d995248a5748abbedb56c4af36f30b
|
||||
timeCreated: 1715879939
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/PrefabUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
//#define USE_SYNC_PROGRESS
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class ProgressUtil
|
||||
{
|
||||
public static int Start(string taskName)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
#if USE_SYNC_PROGRESS
|
||||
EditorUtility.DisplayProgressBar("Trembling...", taskName, 0f);
|
||||
return -1;
|
||||
#else
|
||||
return Progress.Start(taskName, options: Progress.Options.Managed | Progress.Options.Sticky);
|
||||
#endif
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Succeed(int task)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
#if USE_SYNC_PROGRESS
|
||||
EditorUtility.DisplayProgressBar("Trembling...", "done", 1f);
|
||||
EditorUtility.ClearProgressBar();
|
||||
#else
|
||||
if (task == 0)
|
||||
return;
|
||||
Progress.Finish(task);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Fail(int task)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
#if USE_SYNC_PROGRESS
|
||||
EditorUtility.ClearProgressBar();
|
||||
#else
|
||||
if (task == 0)
|
||||
return;
|
||||
Progress.Finish(task, Progress.Status.Failed);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Report(int task, int step, int totalSteps, string description)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
#if USE_SYNC_PROGRESS
|
||||
float progress = (float)step / (float)totalSteps;
|
||||
EditorUtility.DisplayProgressBar("Tremble", $"{description} ({step + 1}/{totalSteps} - {progress*100:F0}%)", progress);
|
||||
#else
|
||||
if (task == 0)
|
||||
return;
|
||||
|
||||
float progress = (float)step / (float)totalSteps;
|
||||
Progress.Report(task, step, totalSteps, $"{description} ({step + 1}/{totalSteps} - {progress*100:F0}%)");
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bf65aab508f4d5a99f9f050ab34c9fd
|
||||
timeCreated: 1715667825
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/ProgressUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class StreamWriterUtil
|
||||
{
|
||||
public static void WriteIndent(this StreamWriter sw, int indent)
|
||||
{
|
||||
for (int i = 0; i < indent; i++)
|
||||
{
|
||||
sw.Write('\t');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fe95984a548d45aca3824cfa3d3d8e0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/StreamWriterUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class StringUtil
|
||||
{
|
||||
public static bool Split(this string input, char splitter, out string first, out string rest)
|
||||
=> Split(input.AsSpan(), splitter, out first, out rest);
|
||||
|
||||
public static bool Split(this ReadOnlySpan<char> input, char splitter, out string first, out string rest)
|
||||
{
|
||||
bool insideQuotes = false;
|
||||
int index = -1;
|
||||
for (int c = 0; c < input.Length; c++)
|
||||
{
|
||||
if (input[c] == '"')
|
||||
{
|
||||
insideQuotes = !insideQuotes;
|
||||
}
|
||||
|
||||
if (insideQuotes || input[c] != splitter)
|
||||
continue;
|
||||
|
||||
index = c;
|
||||
break;
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
first = null;
|
||||
rest = input.ToString();
|
||||
return false;
|
||||
}
|
||||
|
||||
first = input[..index].ToString().Trim();
|
||||
rest = input[(index + 1)..].ToString().Trim();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsNullOrEmpty(this string input) => string.IsNullOrEmpty(input);
|
||||
|
||||
public static bool EqualsInvariant(this string input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.Equals(other, compareType);
|
||||
}
|
||||
public static bool ContainsInvariant(this string input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.Contains(other, compareType);
|
||||
}
|
||||
|
||||
public static bool StartsWithInvariant(this string input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.StartsWith(other, compareType);
|
||||
}
|
||||
public static bool EndsWithInvariant(this string input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.EndsWith(other, compareType);
|
||||
}
|
||||
public static int IndexOfInvariant(this string input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.IndexOf(other, compareType);
|
||||
}
|
||||
public static int LastIndexOfInvariant(this string input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.LastIndexOf(other, compareType);
|
||||
}
|
||||
|
||||
public static bool EqualsInvariant(this ReadOnlySpan<char> input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.Equals( other, compareType);
|
||||
}
|
||||
public static bool ContainsInvariant(this ReadOnlySpan<char> input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.Contains( other, compareType);
|
||||
}
|
||||
|
||||
public static bool StartsWithInvariant(this ReadOnlySpan<char> input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.StartsWith( other, compareType);
|
||||
}
|
||||
public static bool EndsWithInvariant(this ReadOnlySpan<char> input, string other, bool caseSensitive = false)
|
||||
{
|
||||
StringComparison compareType = caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase;
|
||||
return input.EndsWith( other, compareType);
|
||||
}
|
||||
|
||||
private static readonly HashSet<char> DEFAULT_ILLEGAL_CHARS = new() { '{', '}', ' ', '[', ']', '"', '\'', '.', ',' };
|
||||
public static string Sanitise(this string input, HashSet<char> illegalCharacters = null, char? replacement = null)
|
||||
{
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
illegalCharacters ??= DEFAULT_ILLEGAL_CHARS;
|
||||
|
||||
StringBuilder newName = new(input.Length);
|
||||
foreach (char c in input)
|
||||
{
|
||||
if (illegalCharacters.Contains(c))
|
||||
{
|
||||
if (replacement.HasValue)
|
||||
{
|
||||
newName.Append(replacement.Value);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
newName.Append(c);
|
||||
}
|
||||
|
||||
return newName.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a1e2c6b1ca3410bae79a376273f1e2f
|
||||
timeCreated: 1712132630
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/StringUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class TextureUtil
|
||||
{
|
||||
public static Texture2D GenerateColourTexture(Color32 colour)
|
||||
{
|
||||
int size = TrembleSyncSettings.Get().MaterialExportSize.ToIntSize();
|
||||
int sizeSqr = size * size;
|
||||
|
||||
Texture2D texture = new(size, size);
|
||||
|
||||
if (s_ColourBuffer == null || s_ColourBuffer.Length != sizeSqr)
|
||||
{
|
||||
s_ColourBuffer = new Color32[sizeSqr];
|
||||
}
|
||||
|
||||
for (int i = 0; i < sizeSqr; i++)
|
||||
{
|
||||
s_ColourBuffer[i] = colour;
|
||||
s_ColourBuffer[i].a = 0xFF;
|
||||
}
|
||||
|
||||
texture.SetPixels32(s_ColourBuffer);
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static Texture2D GenerateCheckerTexture(int checkerSize = 64) => GenerateCheckerTexture(Color.black, Color.white, checkerSize);
|
||||
|
||||
public static Texture2D GenerateCheckerTexture(Color32 colour1, Color32 colour2, int checkerSize = 64)
|
||||
{
|
||||
int size = TrembleSyncSettings.Get().MaterialExportSize.ToIntSize();
|
||||
|
||||
Texture2D texture = new(size, size);
|
||||
texture.SetPixels32(GenerateCheckerColours(size, colour1, colour2, checkerSize));
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static Texture2D GenerateClipTexture(Color32 colour1, Color32 colour2, Color32 clipText, int checkerSize = 64)
|
||||
{
|
||||
int size = TrembleSyncSettings.Get().MaterialExportSize.ToIntSize();
|
||||
|
||||
Texture2D texture = new(size, size);
|
||||
Color32[] image = GenerateCheckerColours(size, colour1, colour2, checkerSize);
|
||||
ApplyClipTexture(image, size, clipText, checkerSize);
|
||||
texture.SetPixels32(image);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static Texture2D GenerateSkipTexture(Color32 colour1, Color32 colour2, Color32 clipText, int checkerSize = 64)
|
||||
{
|
||||
int size = TrembleSyncSettings.Get().MaterialExportSize.ToIntSize();
|
||||
|
||||
Texture2D texture = new(size, size);
|
||||
Color32[] image = GenerateCheckerColours(size, colour1, colour2, checkerSize);
|
||||
ApplySkipTexture(image, size, clipText, checkerSize);
|
||||
texture.SetPixels32(image);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static Color32[] s_ColourBuffer;
|
||||
|
||||
private static Color32[] GenerateCheckerColours(int size, Color32 colour1, Color32 colour2, int checkerSize = 64)
|
||||
{
|
||||
int sizeSqr = size * size;
|
||||
|
||||
if (s_ColourBuffer == null || s_ColourBuffer.Length != sizeSqr)
|
||||
{
|
||||
s_ColourBuffer = new Color32[sizeSqr];
|
||||
}
|
||||
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
int xCoord = x / checkerSize;
|
||||
int yCoord = y / checkerSize;
|
||||
|
||||
if ((xCoord + yCoord) % 2 == 0)
|
||||
{
|
||||
s_ColourBuffer[y * size + x] = colour1;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_ColourBuffer[y * size + x] = colour2;
|
||||
}
|
||||
|
||||
s_ColourBuffer[y * size + x].a = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
return s_ColourBuffer;
|
||||
}
|
||||
|
||||
// Cheeky clip texture
|
||||
private const char _ = ' ';
|
||||
|
||||
private static readonly string[] s_ClipTexture =
|
||||
{
|
||||
"", "", // 2-line space
|
||||
_ + "/--" + _ + "| " + _ + "|" + _ + "/-|" + _ + _ + _,
|
||||
_ + "| " + _ + "| " + _ + " " + _ + "| |" + _ + _ + _,
|
||||
_ + "| " + _ + "| " + _ + "|" + _ + "+_/" + _ + _ + _,
|
||||
_ + "| " + _ + "| " + _ + "|" + _ + "| " + _ + _ + _,
|
||||
_ + "|__" + _ + "|__" + _ + "|" + _ + "| " + _ + _ + _,
|
||||
"", // 1-line space
|
||||
};
|
||||
|
||||
private static readonly string[] s_SkipTexture =
|
||||
{
|
||||
"", "", // 2-line space
|
||||
_ + "/--" + _ + "| |" + _ + "|" + _ + "/-|" + _ + _ + _,
|
||||
_ + "| " + _ + "| /" + _ + " " + _ + "| |" + _ + _ + _,
|
||||
_ + "---" + _ + "|/ " + _ + "|" + _ + "+-/" + _ + _ + _,
|
||||
_ + " |" + _ + "|+ " + _ + "|" + _ + "| " + _ + _ + _,
|
||||
_ + "--+" + _ + "| +" + _ + "|" + _ + "| " + _ + _ + _,
|
||||
"", // 1-line space
|
||||
};
|
||||
|
||||
private static void ApplyClipTexture(Color32[] image, int size, Color32 textColour, int checkerSize = 64)
|
||||
=> ApplyWordTexture(image, s_ClipTexture, size, textColour, checkerSize);
|
||||
|
||||
private static void ApplySkipTexture(Color32[] image, int size, Color32 textColour, int checkerSize = 64)
|
||||
=> ApplyWordTexture(image, s_SkipTexture, size, textColour, checkerSize);
|
||||
|
||||
private static void ApplyWordTexture(Color32[] image, string[] wordTexture, int size, Color32 textColour, int checkerSize = 64)
|
||||
{
|
||||
int clipWidth = 16;
|
||||
float clipScale = (float)size / (float)checkerSize;
|
||||
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
// Offset every other row
|
||||
int offset = ((y / checkerSize) & 1) == 0 ? 0 : checkerSize;
|
||||
|
||||
// Scale the text
|
||||
int scaledX = (int)((x + offset) / clipScale);
|
||||
int scaledY = (int)(y / clipScale);
|
||||
|
||||
// Flip rows (textures, amirite?!)
|
||||
int row = size - scaledY - 1;
|
||||
|
||||
// Pull the pixel data from the array
|
||||
string wordRow = wordTexture[row % wordTexture.Length];
|
||||
char wordValue = wordRow.Length > 0 ? wordRow[scaledX % clipWidth] : _;
|
||||
|
||||
if (wordValue != _)
|
||||
{
|
||||
// Write pixel if it's on
|
||||
image[y * size + x] = textColour;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Account for weird custom choices for main texture names
|
||||
private static readonly int[] LIKELY_MAIN_TEXTURE_IDS =
|
||||
{
|
||||
Shader.PropertyToID("_BaseColor"),
|
||||
Shader.PropertyToID("_Color"),
|
||||
Shader.PropertyToID("_maintex"),
|
||||
Shader.PropertyToID("_Maintex"),
|
||||
};
|
||||
public static bool TryGetMainTex(this Material material, out Texture2D mainTexture)
|
||||
{
|
||||
if (!material)
|
||||
{
|
||||
mainTexture = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// First check for _MainTex or [MainTexture]
|
||||
mainTexture = material.mainTexture as Texture2D;
|
||||
|
||||
if (!mainTexture)
|
||||
{
|
||||
foreach (int name in LIKELY_MAIN_TEXTURE_IDS)
|
||||
{
|
||||
if (!material.HasTexture(name))
|
||||
continue;
|
||||
|
||||
mainTexture = material.GetTexture(name) as Texture2D;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return mainTexture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90d10613740347268599188e50f56c1f
|
||||
timeCreated: 1709808325
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/TextureUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,237 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class TrenchBroomUtil
|
||||
{
|
||||
|
||||
private static string TrenchBroomPathFromPrefs
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
get => EditorPrefs.GetString("Tremble_TBPath", null);
|
||||
set => EditorPrefs.SetString("Tremble_TBPath", value);
|
||||
#else
|
||||
get => PlayerPrefs.GetString("Tremble_TBPath", null);
|
||||
set => PlayerPrefs.SetString("Tremble_TBPath", value);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static string TrenchBroomPath
|
||||
{
|
||||
get
|
||||
{
|
||||
// First look for a running Trenchbroom
|
||||
Process[] procs = Process.GetProcessesByName("trenchbroom");
|
||||
if (procs.Length > 0 && procs[0].MainModule != null)
|
||||
{
|
||||
TrenchBroomPathFromPrefs = procs[0].MainModule.FileName;
|
||||
return procs[0].MainModule.FileName;
|
||||
}
|
||||
|
||||
// Not running - try a path we previously knew about
|
||||
string path = TrenchBroomPathFromPrefs;
|
||||
if (path != null && File.Exists(path))
|
||||
return path;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private set => TrenchBroomPathFromPrefs = value;
|
||||
}
|
||||
|
||||
public static bool IsTrenchBroomRunning
|
||||
{
|
||||
get
|
||||
{
|
||||
Process proc = Process.GetProcessesByName("trenchbroom").FirstOrDefault();
|
||||
if (proc == null)
|
||||
return false;
|
||||
|
||||
TrenchBroomPath = proc.MainModule.FileName;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DoesEnginePathExistInTrenchBroomPrefs()
|
||||
{
|
||||
string preferencesFile = Path.Combine(GetTrenchBroomConfigFolder(), "Preferences.json");
|
||||
string key = $"Games/{TrembleConsts.GAME_NAME}/Path";
|
||||
|
||||
if (!File.Exists(preferencesFile))
|
||||
return false;
|
||||
|
||||
// Read in current config
|
||||
foreach (string line in File.ReadLines(preferencesFile))
|
||||
{
|
||||
string trimmedLine = line.Trim(' ', ',');
|
||||
|
||||
// Ignore braces and empty lines
|
||||
if (trimmedLine.IsNullOrEmpty() || trimmedLine is "{" or "}")
|
||||
continue;
|
||||
|
||||
// Existing path? - patch it!
|
||||
if (trimmedLine.StartsWith($"\"{key}\""))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<bool> WriteEnginePathIntoTrenchBroomPrefs()
|
||||
{
|
||||
// I didn't want to import a JSON library so - this is it.
|
||||
// Read each line into an array (ignore the braces) and remove the commas.
|
||||
// Find if there's a current path - if so, patch it up.
|
||||
// if not, add a new one at the end.
|
||||
|
||||
string preferencesFile = Path.Combine(GetTrenchBroomConfigFolder(), "Preferences.json");
|
||||
string key = $"Games/{TrembleConsts.GAME_NAME}/Path";
|
||||
|
||||
string projectFolderUnix = Directory.GetCurrentDirectory().Replace('\\', '/');
|
||||
string entry = $"\"{key}\": \"{projectFolderUnix}\"";
|
||||
|
||||
bool wasPatched = false;
|
||||
List<string> configLines = new();
|
||||
|
||||
if (File.Exists(preferencesFile))
|
||||
{
|
||||
// Read in current config
|
||||
foreach (string line in File.ReadLines(preferencesFile))
|
||||
{
|
||||
string trimmedLine = line.Trim(' ', ',');
|
||||
|
||||
// Ignore braces and empty lines
|
||||
if (trimmedLine.IsNullOrEmpty() || trimmedLine is "{" or "}")
|
||||
continue;
|
||||
|
||||
// Existing path? - patch it!
|
||||
if (trimmedLine.StartsWith($"\"{key}\""))
|
||||
{
|
||||
configLines.Add(entry);
|
||||
wasPatched = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
configLines.Add(trimmedLine);
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasPatched)
|
||||
{
|
||||
// We didn't find the game - append to the end (before close brace)
|
||||
configLines.Add(entry);
|
||||
}
|
||||
|
||||
// Write out new config
|
||||
for (int lineIdx = 0; lineIdx < configLines.Count; lineIdx++)
|
||||
{
|
||||
// Indent
|
||||
configLines[lineIdx] = $" {configLines[lineIdx]}";
|
||||
|
||||
// Add commas (not on the last one!)
|
||||
if (lineIdx < configLines.Count - 1)
|
||||
{
|
||||
configLines[lineIdx] += ",";
|
||||
}
|
||||
}
|
||||
|
||||
configLines.Insert(0, "{");
|
||||
configLines.Add("}");
|
||||
|
||||
DirectoryUtil.CreateAllDirectories(preferencesFile);
|
||||
await File.WriteAllLinesAsync(preferencesFile, configLines);
|
||||
|
||||
return !wasPatched;
|
||||
}
|
||||
|
||||
private static string GetTrenchBroomConfigFolder() =>
|
||||
Application.platform switch
|
||||
{
|
||||
RuntimePlatform.OSXEditor or RuntimePlatform.OSXPlayer
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "TrenchBroom"),
|
||||
RuntimePlatform.WindowsEditor or RuntimePlatform.WindowsPlayer
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TrenchBroom"),
|
||||
RuntimePlatform.LinuxEditor or RuntimePlatform.LinuxPlayer
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".TrenchBroom"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(Application.platform), "Unknown platform!")
|
||||
};
|
||||
|
||||
public static string GetGameFolder() => Path.Combine(GetTrenchBroomConfigFolder(), "games", "Unity", Application.productName);
|
||||
|
||||
public static void OpenWithTrenchBroom(string mapFile)
|
||||
{
|
||||
switch (Application.platform)
|
||||
{
|
||||
case RuntimePlatform.OSXEditor:
|
||||
case RuntimePlatform.OSXPlayer:
|
||||
RunCommand($"open -a TrenchBroom '{mapFile}'");
|
||||
break;
|
||||
|
||||
case RuntimePlatform.WindowsEditor:
|
||||
case RuntimePlatform.WindowsPlayer:
|
||||
// Sad. On Windows, TB is mostly portable so we may have to just show the file in Explorer, lol
|
||||
if (TrenchBroomPath != null)
|
||||
{
|
||||
RunCommand($"\"{TrenchBroomPath}\" \"{mapFile}\"", useShell: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorUtility.DisplayDialog(
|
||||
title: "Where's TrenchBroom?",
|
||||
message: "Tremble could not find TrenchBroom. Ensure TrenchBroom is installed and " +
|
||||
"running, and Tremble will make a note of where it's installed for next time." +
|
||||
" For now, I'll show your map in Explorer.", "Okay");
|
||||
#endif
|
||||
RunCommand($"explorer /select, \"{mapFile}\"");
|
||||
}
|
||||
break;
|
||||
|
||||
case RuntimePlatform.LinuxEditor:
|
||||
case RuntimePlatform.LinuxPlayer:
|
||||
RunCommand($"/usr/bin/trenchbroom '{mapFile}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunCommand(string command, bool useShell = true)
|
||||
{
|
||||
command.Split(' ', out string exe, out string args);
|
||||
|
||||
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
|
||||
// Windows Command Prompt
|
||||
if (useShell)
|
||||
{
|
||||
args = $"/c \"{exe} {args}\"";
|
||||
exe = "cmd.exe";
|
||||
}
|
||||
#else
|
||||
// Under macOS and Linux we assume /bin/sh is a thing
|
||||
if (useShell)
|
||||
{
|
||||
args = $"-c \"{exe} {args}\"";
|
||||
exe = "/bin/sh";
|
||||
}
|
||||
#endif
|
||||
|
||||
Process.Start(new ProcessStartInfo(exe, args) { WorkingDirectory = Directory.GetCurrentDirectory() });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7bf4a6e8c1404a21adc02ab38521201
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/TrenchBroomUtil.cs
|
||||
uploadId: 939664
|
||||
@@ -0,0 +1,365 @@
|
||||
//
|
||||
// This file is part of the Tremble package by Tiny Goose.
|
||||
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
||||
//
|
||||
|
||||
//#define USE_TYPEDB_IN_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if !UNITY_EDITOR || USE_TYPEDB_IN_EDITOR
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
#endif
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
|
||||
namespace TinyGoose.Tremble
|
||||
{
|
||||
public static class TypeDatabase
|
||||
{
|
||||
private static Dictionary<string, Type> s_ClassLookup;
|
||||
private static Dictionary<Type, object[]> s_ClassAttributes;
|
||||
|
||||
public static Type GetType(string fullName)
|
||||
{
|
||||
if (fullName == null)
|
||||
return null;
|
||||
|
||||
#if UNITY_EDITOR && !USE_TYPEDB_IN_EDITOR
|
||||
// Unsupported - but very fast!
|
||||
return Unsupported.GetTypeFromFullName(fullName);
|
||||
#else
|
||||
if (s_ClassLookup == null)
|
||||
{
|
||||
GenerateClassLookup();
|
||||
}
|
||||
|
||||
return s_ClassLookup.GetValueOrDefault(fullName);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static IList<Type> GetTypesWithAttribute<TAttribute>()
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
#if UNITY_EDITOR && !USE_TYPEDB_IN_EDITOR
|
||||
return TypeCache.GetTypesWithAttribute<TAttribute>();
|
||||
#else
|
||||
if (s_ClassLookup == null)
|
||||
{
|
||||
GenerateClassLookup();
|
||||
}
|
||||
|
||||
List<Type> types = new(256);
|
||||
foreach (KeyValuePair<Type, object[]> kvp in s_ClassAttributes)
|
||||
{
|
||||
foreach (object attribute in kvp.Value)
|
||||
{
|
||||
Type attributeType = attribute.GetType();
|
||||
|
||||
if (attributeType == typeof(TAttribute) || attributeType.IsSubclassOf(typeof(TAttribute)))
|
||||
{
|
||||
types.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return types;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#if !UNITY_EDITOR || USE_TYPEDB_IN_EDITOR
|
||||
private static readonly HashSet<string> FORBIDDEN_ASSEMBLIES = new()
|
||||
{
|
||||
"mscorlib",
|
||||
"UnityEngine",
|
||||
"UnityEngine.AIModule",
|
||||
"UnityEngine.AMDModule",
|
||||
"UnityEngine.ARModule",
|
||||
"UnityEngine.AccessibilityModule",
|
||||
"UnityEngine.AndroidJNIModule",
|
||||
"UnityEngine.AnimationModule",
|
||||
"UnityEngine.AssetBundleModule",
|
||||
"UnityEngine.AudioModule",
|
||||
"UnityEngine.ClothModule",
|
||||
"UnityEngine.ClusterInputModule",
|
||||
"UnityEngine.ClusterRendererModule",
|
||||
"UnityEngine.ContentLoadModule",
|
||||
"UnityEngine.CoreModule",
|
||||
"UnityEngine.CrashReportingModule",
|
||||
"UnityEngine.DSPGraphModule",
|
||||
"UnityEngine.DirectorModule",
|
||||
"UnityEngine.GIModule",
|
||||
"UnityEngine.GameCenterModule",
|
||||
"UnityEngine.GraphicsStateCollectionSerializerModule",
|
||||
"UnityEngine.GridModule",
|
||||
"UnityEngine.HierarchyCoreModule",
|
||||
"UnityEngine.HotReloadModule",
|
||||
"UnityEngine.IMGUIModule",
|
||||
"UnityEngine.ImageConversionModule",
|
||||
"UnityEngine.InputModule",
|
||||
"UnityEngine.InputForUIModule",
|
||||
"UnityEngine.InputLegacyModule",
|
||||
"UnityEngine.JSONSerializeModule",
|
||||
"UnityEngine.LocalizationModule",
|
||||
"UnityEngine.MarshallingModule",
|
||||
"UnityEngine.MultiplayerModule",
|
||||
"UnityEngine.NVIDIAModule",
|
||||
"UnityEngine.ParticleSystemModule",
|
||||
"UnityEngine.PerformanceReportingModule",
|
||||
"UnityEngine.PhysicsModule",
|
||||
"UnityEngine.Physics2DModule",
|
||||
"UnityEngine.PropertiesModule",
|
||||
"UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule",
|
||||
"UnityEngine.ScreenCaptureModule",
|
||||
"UnityEngine.ShaderVariantAnalyticsModule",
|
||||
"UnityEngine.SharedInternalsModule",
|
||||
"UnityEngine.SpriteMaskModule",
|
||||
"UnityEngine.SpriteShapeModule",
|
||||
"UnityEngine.StreamingModule",
|
||||
"UnityEngine.SubstanceModule",
|
||||
"UnityEngine.SubsystemsModule",
|
||||
"UnityEngine.TLSModule",
|
||||
"UnityEngine.TerrainModule",
|
||||
"UnityEngine.TerrainPhysicsModule",
|
||||
"UnityEngine.TextCoreFontEngineModule",
|
||||
"UnityEngine.TextCoreTextEngineModule",
|
||||
"UnityEngine.TextRenderingModule",
|
||||
"UnityEngine.TilemapModule",
|
||||
"UnityEngine.UIModule",
|
||||
"UnityEngine.UIElementsModule",
|
||||
"UnityEngine.UmbraModule",
|
||||
"UnityEngine.UnityAnalyticsModule",
|
||||
"UnityEngine.UnityAnalyticsCommonModule",
|
||||
"UnityEngine.UnityConnectModule",
|
||||
"UnityEngine.UnityCurlModule",
|
||||
"UnityEngine.UnityTestProtocolModule",
|
||||
"UnityEngine.UnityWebRequestModule",
|
||||
"UnityEngine.UnityWebRequestAssetBundleModule",
|
||||
"UnityEngine.UnityWebRequestAudioModule",
|
||||
"UnityEngine.UnityWebRequestTextureModule",
|
||||
"UnityEngine.UnityWebRequestWWWModule",
|
||||
"UnityEngine.VFXModule",
|
||||
"UnityEngine.VRModule",
|
||||
"UnityEngine.VehiclesModule",
|
||||
"UnityEngine.VideoModule",
|
||||
"UnityEngine.VirtualTexturingModule",
|
||||
"UnityEngine.WindModule",
|
||||
"UnityEngine.XRModule",
|
||||
"UnityEditor",
|
||||
"UnityEditor.AccessibilityModule",
|
||||
"UnityEditor.AdaptivePerformanceModule",
|
||||
"UnityEditor.BuildProfileModule",
|
||||
"UnityEditor.CoreBusinessMetricsModule",
|
||||
"UnityEditor.CoreModule",
|
||||
"UnityEditor.DeviceSimulatorModule",
|
||||
"UnityEditor.DiagnosticsModule",
|
||||
"UnityEditor.EditorToolbarModule",
|
||||
"UnityEditor.GIModule",
|
||||
"UnityEditor.GraphViewModule",
|
||||
"UnityEditor.GraphicsStateCollectionSerializerModule",
|
||||
"UnityEditor.GridAndSnapModule",
|
||||
"UnityEditor.GridModule",
|
||||
"UnityEditor.MultiplayerModule",
|
||||
"UnityEditor.Physics2DModule",
|
||||
"UnityEditor.PhysicsModule",
|
||||
"UnityEditor.PresetsUIModule",
|
||||
"UnityEditor.PropertiesModule",
|
||||
"UnityEditor.QuickSearchModule",
|
||||
"UnityEditor.SafeModeModule",
|
||||
"UnityEditor.SceneTemplateModule",
|
||||
"UnityEditor.SceneViewModule",
|
||||
"UnityEditor.ShaderFoundryModule",
|
||||
"UnityEditor.SketchUpModule",
|
||||
"UnityEditor.SpriteMaskModule",
|
||||
"UnityEditor.SpriteShapeModule",
|
||||
"UnityEditor.SubstanceModule",
|
||||
"UnityEditor.TerrainModule",
|
||||
"UnityEditor.TextCoreFontEngineModule",
|
||||
"UnityEditor.TextCoreTextEngineModule",
|
||||
"UnityEditor.TextRenderingModule",
|
||||
"UnityEditor.TilemapModule",
|
||||
"UnityEditor.TreeModule",
|
||||
"UnityEditor.UIAutomationModule",
|
||||
"UnityEditor.UIBuilderModule",
|
||||
"UnityEditor.UIElementsModule",
|
||||
"UnityEditor.UIElementsSamplesModule",
|
||||
"UnityEditor.UmbraModule",
|
||||
"UnityEditor.UnityConnectModule",
|
||||
"UnityEditor.VFXModule",
|
||||
"UnityEditor.VideoModule",
|
||||
"UnityEditor.XRModule",
|
||||
"netstandard",
|
||||
"System.Core",
|
||||
"System",
|
||||
"Mono.Security",
|
||||
"System.Configuration",
|
||||
"System.Xml",
|
||||
"System.Numerics",
|
||||
"System.Data",
|
||||
"System.Transactions",
|
||||
"System.EnterpriseServices",
|
||||
"System.Runtime.Serialization",
|
||||
"System.ServiceModel.Internals",
|
||||
"System.Xml.Linq",
|
||||
"Unity.Cecil",
|
||||
"Bee.BeeDriver2",
|
||||
"Bee.BinLog",
|
||||
"UnityEditor.Graphs",
|
||||
"UnityEditor.OSXStandalone.Extensions",
|
||||
"UnityEditor.iOS.Extensions.Xcode",
|
||||
"Autodesk.Fbx.BuildTestAssets",
|
||||
"Autodesk.Fbx",
|
||||
"Autodesk.Fbx.Editor",
|
||||
"PPv2URPConverters",
|
||||
"Unity.Addressables",
|
||||
"Unity.Addressables.Editor",
|
||||
"Unity.AI.Navigation",
|
||||
"Unity.AI.Navigation.Editor.ConversionSystem",
|
||||
"Unity.AI.Navigation.Editor",
|
||||
"Unity.AI.Navigation.Updater",
|
||||
"Unity.Bindings.OpenImageIO.Editor",
|
||||
"Unity.Burst.CodeGen",
|
||||
"Unity.Burst",
|
||||
"Unity.Burst.Editor",
|
||||
"Unity.Cinemachine",
|
||||
"Unity.Cinemachine.Editor",
|
||||
"Unity.CollabProxy.Editor",
|
||||
"Unity.Collections.CodeGen",
|
||||
"Unity.Collections",
|
||||
"Unity.Collections.Editor",
|
||||
"Unity.EditorCoroutines.Editor",
|
||||
"Unity.Formats.Fbx.Editor",
|
||||
"Unity.Formats.Fbx.Runtime",
|
||||
"Unity.InputSystem",
|
||||
"Unity.InputSystem.ForUI",
|
||||
"Unity.InputSystem.TestFramework",
|
||||
"Unity.Mathematics",
|
||||
"Unity.Mathematics.Editor",
|
||||
"Unity.MemoryProfiler",
|
||||
"Unity.MemoryProfiler.Editor",
|
||||
"Unity.MemoryProfiler.Editor.MemoryProfilerModule",
|
||||
"Unity.PerformanceTesting",
|
||||
"Unity.PerformanceTesting.Editor",
|
||||
"Unity.PlasticSCM.Editor",
|
||||
"Unity.Profiling.Core",
|
||||
"Unity.Recorder.Base",
|
||||
"Unity.Recorder",
|
||||
"Unity.Recorder.Editor",
|
||||
"Unity.Rendering.LightTransport.Editor",
|
||||
"Unity.Rendering.LightTransport.Runtime",
|
||||
"Unity.RenderPipeline.Universal.ShaderLibrary",
|
||||
"Unity.RenderPipelines.Core.Editor",
|
||||
"Unity.RenderPipelines.Core.Editor.Shared",
|
||||
"Unity.RenderPipelines.Core.Runtime",
|
||||
"Unity.RenderPipelines.Core.Runtime.Shared",
|
||||
"Unity.RenderPipelines.Core.ShaderLibrary",
|
||||
"Unity.RenderPipelines.GPUDriven.Runtime",
|
||||
"Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary",
|
||||
"Unity.RenderPipelines.Universal.2D.Runtime",
|
||||
"Unity.RenderPipelines.Universal.Config.Runtime",
|
||||
"Unity.RenderPipelines.Universal.Editor",
|
||||
"Unity.RenderPipelines.Universal.Runtime",
|
||||
"Unity.RenderPipelines.Universal.Shaders",
|
||||
"Unity.ResourceManager",
|
||||
"Unity.Rider.Editor",
|
||||
"Unity.ScriptableBuildPipeline",
|
||||
"Unity.ScriptableBuildPipeline.Editor",
|
||||
"Unity.Searcher.Editor",
|
||||
"Unity.Serialization",
|
||||
"Unity.Serialization.Editor",
|
||||
"Unity.Services.Authentication",
|
||||
"Unity.Services.Authentication.Editor",
|
||||
"Unity.Services.Authentication.Editor.Shared",
|
||||
"Unity.Services.Authentication.PlayerAccounts",
|
||||
"Unity.Services.Authentication.PlayerAccounts.Editor",
|
||||
"Unity.Services.Core.Analytics",
|
||||
"Unity.Services.Core.Configuration",
|
||||
"Unity.Services.Core.Configuration.Editor",
|
||||
"Unity.Services.Core.Device",
|
||||
"Unity.Services.Core",
|
||||
"Unity.Services.Core.Editor",
|
||||
"Unity.Services.Core.Environments",
|
||||
"Unity.Services.Core.Environments.Editor",
|
||||
"Unity.Services.Core.Environments.Internal",
|
||||
"Unity.Services.Core.Internal",
|
||||
"Unity.Services.Core.Networking",
|
||||
"Unity.Services.Core.Registration",
|
||||
"Unity.Services.Core.Scheduler",
|
||||
"Unity.Services.Core.Telemetry",
|
||||
"Unity.Services.Core.Threading",
|
||||
"Unity.Services.Ugc",
|
||||
"Unity.Services.Ugc.DocCodeSamples",
|
||||
"Unity.Services.Ugc.Internal.Generated",
|
||||
"Unity.Settings.Editor",
|
||||
"Unity.ShaderGraph.Editor",
|
||||
"Unity.ShaderGraph.Utilities",
|
||||
"Unity.Splines",
|
||||
"Unity.Splines.Editor",
|
||||
"Unity.TextMeshPro",
|
||||
"Unity.TextMeshPro.Editor",
|
||||
"Unity.Timeline",
|
||||
"Unity.Timeline.Editor",
|
||||
"UnityEditor.TestRunner",
|
||||
"UnityEditor.UI",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEngine.UI",
|
||||
"Mono.Cecil.Pdb",
|
||||
"SingularityGroup.HotReload.RuntimeDependencies",
|
||||
"Unity.Burst.Cecil.Pdb",
|
||||
"Unity.Collections.LowLevel.ILSupport",
|
||||
"nunit.framework",
|
||||
"SingularityGroup.HotReload.EditorDependencies",
|
||||
"unityplastic",
|
||||
"Unity.Plastic.Antlr3.Runtime",
|
||||
"Unity.Burst.Cecil.Mdb",
|
||||
"Unity.Burst.Cecil",
|
||||
"SingularityGroup.HotReload.RuntimeDependencies2020",
|
||||
"DOTweenEditor",
|
||||
"Mono.Cecil.Mdb",
|
||||
"Unity.Plastic.Newtonsoft.Json",
|
||||
"Unity.Burst.Unsafe",
|
||||
"Mono.Cecil.Rocks",
|
||||
"Mono.Cecil",
|
||||
"Unity.Burst.Cecil.Rocks",
|
||||
"System.Net.Http",
|
||||
"Unity.CompilationPipeline.Common",
|
||||
"System.Drawing",
|
||||
"System.Runtime",
|
||||
"Mono.Posix",
|
||||
"System.Web",
|
||||
"Microsoft.GeneratedCode",
|
||||
"JetBrains.Rider.Unity.Editor.Plugin.Net46.Repacked",
|
||||
"ExCSS.Unity",
|
||||
"Microsoft.GeneratedCode",
|
||||
};
|
||||
|
||||
public static void GenerateClassLookup()
|
||||
{
|
||||
s_ClassLookup = new(1024);
|
||||
s_ClassAttributes = new(1024);
|
||||
|
||||
IEnumerable<Type> types = AppDomain.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.Where(ass => !FORBIDDEN_ASSEMBLIES.Contains(ass.GetName().Name))
|
||||
.SelectMany(ass => ass.GetTypes())
|
||||
.Where(t => t.FullName != null);
|
||||
|
||||
foreach (Type availableType in types)
|
||||
{
|
||||
s_ClassLookup[availableType.FullName] = availableType;
|
||||
|
||||
if (availableType.IsSubclassOf(typeof(MonoBehaviour)) || availableType.IsSubclassOf(typeof(TrembleFieldConverter)))
|
||||
{
|
||||
s_ClassAttributes.Add(availableType, availableType.GetCustomAttributes(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e121f8ff98d4f5f98898fd968b7d827
|
||||
timeCreated: 1715667413
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 277805
|
||||
packageName: Tremble - Map Importer
|
||||
packageVersion: 1.11.1
|
||||
assetPath: Assets/Tremble/Utils/TypeDatabase.cs
|
||||
uploadId: 939664
|
||||
Reference in new issue
Block a user