Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
# Visual Studio cache directory
.vs/

# JetBrains IDE directory
.idea/

# Gradle cache directory
.gradle/

Expand Down
28 changes: 28 additions & 0 deletions Assets/Scenes/Main.unity
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ GameObject:
- component: {fileID: 9355361}
- component: {fileID: 9355362}
- component: {fileID: 9355363}
- component: {fileID: 9355364}
m_Layer: 0
m_Name: AvatarRoot
m_TagString: Untagged
Expand Down Expand Up @@ -233,6 +234,7 @@ MonoBehaviour:
- {fileID: 979165189}
- {fileID: 979165180}
- {fileID: 979165181}
springBonesDriver: {fileID: 9355364}
setsHighlight: 0
highlightCenter: {x: 0, y: 0.18, z: 0}
highlightSize: {x: 0.57, y: 2.3}
Expand All @@ -254,6 +256,18 @@ MonoBehaviour:
autoRotateSpeed: 10
autoRotateDelay: 1
returnSpeed: 2
--- !u!114 &9355364
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9355360}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95ea887eb11d7f94eaef9ace0bb62be9, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SpringBones.SpringBonesDriver
--- !u!1001 &64509934
PrefabInstance:
m_ObjectHideFlags: 0
Expand Down Expand Up @@ -2747,6 +2761,7 @@ GameObject:
m_Component:
- component: {fileID: 8085770638703914882}
- component: {fileID: 8085770638703914883}
- component: {fileID: 8085770638703914884}
m_Layer: 0
m_Name: AvatarRoot
m_TagString: Untagged
Expand Down Expand Up @@ -4566,9 +4581,22 @@ MonoBehaviour:
- {fileID: 64509949}
- {fileID: 64509940}
- {fileID: 64509941}
springBonesDriver: {fileID: 8085770638703914884}
setsHighlight: 1
highlightCenter: {x: 0, y: 0.18, z: 0}
highlightSize: {x: 0.57, y: 2.3}
--- !u!114 &8085770638703914884
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2042814726327143098}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95ea887eb11d7f94eaef9ace0bb62be9, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SpringBones.SpringBonesDriver
--- !u!114 &8186094461447319126
MonoBehaviour:
m_ObjectHideFlags: 0
Expand Down
6 changes: 3 additions & 3 deletions Assets/Scripts/AangConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ public static void RecreateFrom(string url)
foreach (var parameter in split)
{
var keyValueSplit = parameter.Split('=');
var key = HttpUtility.UrlDecode(keyValueSplit[0]);
var value = keyValueSplit.Length > 1 ? HttpUtility.UrlDecode(keyValueSplit[1]) : string.Empty;
var key = HttpUtility.UrlDecode(keyValueSplit[0]).Trim();
var value = (keyValueSplit.Length > 1 ? HttpUtility.UrlDecode(keyValueSplit[1]) : string.Empty).Trim();

switch (key)
{
Expand Down Expand Up @@ -298,7 +298,7 @@ public static void RecreateFrom(string url)
break;
case "env":
APIService.Environment = value == "dev" ? "zone" : "org";
Debug.Log($"Using environment {APIService.Environment}");
Debug.Log($"Using environment {APIService.Environment} (env value=[{value}])");
break;
case "disableLoader":
Instance.DisableLoader = bool.Parse(value);
Expand Down
1 change: 1 addition & 0 deletions Assets/Scripts/Data/ActiveEntityResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class Data
public string[] replaces = Array.Empty<string>();
public string[] removesDefaultHiding = Array.Empty<string>();
public bool loop; // For emotes only
public SpringBonesDto springBones;
}

[Serializable]
Expand Down
56 changes: 50 additions & 6 deletions Assets/Scripts/Data/EntityDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,25 @@ public class EntityDefinition
public readonly EntityType Type;
public readonly EntityFlags Flags;

[CanBeNull] private readonly SpringBonesDto _springBones;

[ItemCanBeNull] private readonly Dictionary<BodyShape, Representation> _representations;

/// <summary>
/// Returns the spring-bone params (boneName → params) declared in the wearable
/// metadata for the given body shape's mainFile, or null if none.
/// </summary>
[CanBeNull]
public IReadOnlyDictionary<string, SpringBoneParamsDto> GetSpringBoneParams(BodyShape shape)
{
if (_springBones?.models == null) return null;
if (!HasRepresentation(shape)) return null;

var mainFileHash = _representations[shape].MainFileHash;
if (string.IsNullOrEmpty(mainFileHash)) return null;
return _springBones.models.TryGetValue(mainFileHash, out var map) ? map : null;
}

public Representation[] GetAllRepresentations()
{
// TODO: Cleanup
Expand Down Expand Up @@ -48,28 +65,32 @@ public bool HasRepresentation(BodyShape bodyShape)
public Representation this[BodyShape shape] => _representations[shape] ?? throw new InvalidOperationException(
$"Missing {shape} representation for {URN}");

private EntityDefinition(string urn, string category, string thumbnail, EntityType type, EntityFlags flags, Dictionary<BodyShape, Representation> representations)
private EntityDefinition(string urn, string category, string thumbnail, EntityType type, EntityFlags flags,
Dictionary<BodyShape, Representation> representations, [CanBeNull] SpringBonesDto springBones)
{
URN = urn;
Category = category;
Thumbnail = thumbnail;
Type = type;
_representations = representations;
Flags = flags;
_springBones = springBones;
}

public class Representation
{
public readonly Dictionary<string, string> Files;
public readonly string MainFile;
public readonly string MainFileHash;
public readonly string[] Hides;
public readonly string[] RemovesDefaultHiding;

private Representation(Dictionary<string, string> files, string mainFile, string[] hides,
string[] removesDefaultHiding)
private Representation(Dictionary<string, string> files, string mainFile, string mainFileHash,
string[] hides, string[] removesDefaultHiding)
{
Files = files;
MainFile = mainFile;
MainFileHash = mainFileHash;
Hides = hides;
RemovesDefaultHiding = removesDefaultHiding;
}
Expand Down Expand Up @@ -112,9 +133,13 @@ public static Representation ForBodyShape(string bodyShape, ActiveEntity entity)
StringComparer.OrdinalIgnoreCase
);

var mainFileHash = entity.content
.FirstOrDefault(c => string.Equals(c.file, main, StringComparison.OrdinalIgnoreCase))?.hash;

var representation = new Representation(
filesDict,
entityRepresentation.mainFile,
mainFileHash,
entityRepresentation.overrideHides is { Length: > 0 }
? entityRepresentation.overrideHides
: data.hides.Union(entityRepresentation.overrideReplaces is { Length: > 0 }
Expand All @@ -135,7 +160,7 @@ public static Representation ForEmbeddedEmote(string emote)
{
["main"] = Path.Combine(Application.streamingAssetsPath, $"{emote}.glb")
},
"main", Array.Empty<string>(),
"main", null, Array.Empty<string>(),
Array.Empty<string>()
);
}
Expand All @@ -161,7 +186,8 @@ public static EntityDefinition FromActiveEntity(ActiveEntity entity)
[BodyShape.Female] = Representation.ForBodyShape(WearablesConstants.BODY_SHAPE_FEMALE, entity)
};

return new EntityDefinition(urn, category, thumbnail, type, flags, representations);
return new EntityDefinition(urn, category, thumbnail, type, flags, representations,
entity.IsEmote ? null : entity.metadata?.data?.springBones);
}

public static EntityDefinition FromBase64(byte[] b64)
Expand All @@ -173,6 +199,23 @@ public static EntityDefinition FromBase64(byte[] b64)
var raw = JsonUtility.FromJson<RawActiveEntity>(base64String);
var entity = raw.ToActiveEntity();

// JsonUtility can't deserialize the nested Dictionary in springBones.models,
// so re-parse just that field with Newtonsoft and inject it into the entity.
if (!entity.IsEmote && entity.metadata?.data != null)
{
try
{
var jo = Newtonsoft.Json.Linq.JObject.Parse(base64String);
var springBonesToken = jo["data"]?["springBones"];
if (springBonesToken != null)
entity.metadata.data.springBones = springBonesToken.ToObject<SpringBonesDto>();
}
catch (Exception e)
{
Debug.LogError($"[Base64] failed to parse springBones: {e.Message}");
}
}

return FromActiveEntity(entity);
}

Expand All @@ -188,7 +231,8 @@ public static EntityDefinition FromEmbeddedEmote(string emote, bool loop)
{
[BodyShape.Male] = Representation.ForEmbeddedEmote(emote),
[BodyShape.Female] = Representation.ForEmbeddedEmote(emote)
}
},
null
);
}
}
Expand Down
17 changes: 16 additions & 1 deletion Assets/Scripts/Data/RawActiveEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public class Translation
public string text;
}

static string ExtractHashFromUrl(string url)
{
if (string.IsNullOrEmpty(url)) return null;
var trimmed = url.TrimEnd('/');
var slash = trimmed.LastIndexOf('/');
return slash >= 0 && slash + 1 < trimmed.Length ? trimmed[(slash + 1)..] : null;
}

public ActiveEntity ToActiveEntity()
{
var reps = IsEmote ? emoteDataADR74.representations : data.representations;
Expand All @@ -64,7 +72,14 @@ public ActiveEntity ToActiveEntity()
type = IsEmote ? "emote" : "wearable",
content = reps
.SelectMany(r => r.contents
.Select(c => new ActiveEntity.Content { file = c.key, url = c.url }))
.Select(c => new ActiveEntity.Content
{
file = c.key,
url = c.url,
// Spring-bone metadata keys models by content hash (CID). The CID is the
// last path segment of the builder-api URL — extract it so the lookup works.
hash = ExtractHashFromUrl(c.url),
}))
.GroupBy(c => c.file)
.Select(g => g.First())
.ToArray(),
Expand Down
62 changes: 62 additions & 0 deletions Assets/Scripts/Data/SpringBonesDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace Data
{
[Serializable]
public class SpringBonesDto
{
public int version;
public Dictionary<string, Dictionary<string, SpringBoneParamsDto>> models;
}

[Serializable]
public class SpringBoneParamsDto
{
public float stiffness;
public float drag;

[JsonConverter(typeof(Vector3JsonConverter))]
public Vector3 gravityDir;

public float gravityPower;
public float hitRadius;
public bool isRoot;
}

public class Vector3JsonConverter : JsonConverter<Vector3>
{
public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(value.x);
writer.WritePropertyName("y");
writer.WriteValue(value.y);
writer.WritePropertyName("z");
writer.WriteValue(value.z);
writer.WriteEndObject();
}

public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return existingValue;
var t = JToken.Load(reader);
if (t.Type == JTokenType.Array)
{
return new Vector3(
t.Count() > 0 ? t[0].Value<float>() : 0f,
t.Count() > 1 ? t[1].Value<float>() : 0f,
t.Count() > 2 ? t[2].Value<float>() : 0f);
}
return new Vector3(
t["x"]?.Value<float>() ?? 0f,
t["y"]?.Value<float>() ?? 0f,
t["z"]?.Value<float>() ?? 0f);
}
}
}
3 changes: 3 additions & 0 deletions Assets/Scripts/Data/SpringBonesDto.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Assets/Scripts/JSBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ public void SetUrns(string value) =>
[UsedImplicitly]
public void SetUsername(string value) => AangConfiguration.Instance.Username = value;

[UsedImplicitly]
public void SetSpringBonesParams(string value)
{
SpringBones.SpringBonesParamsPayload payload;
try { payload = SpringBones.SpringBonesParamsPayload.Parse(value); }
catch (Exception e)
{
Debug.LogError($"[SpringBones] failed to parse SetSpringBonesParams payload: {e.Message}");
return;
}
if (payload == null) return;
previewController.SetSpringBonesParams(payload);
}

[UsedImplicitly]
public void GetElementBounds(string elementName) => configuratorUIPresenter.GetElementBounds(elementName);

Expand Down
Loading
Loading