1466 lines
58 KiB
C#
1466 lines
58 KiB
C#
using CriWareFormats;
|
|
using Gallop;
|
|
using Gallop.Live;
|
|
using Gallop.Live.Cutt;
|
|
using NAudio.Wave;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UmaMusumeAudio;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using Debug = UnityEngine.Debug;
|
|
using Random = UnityEngine.Random;
|
|
|
|
public class UmaViewerBuilder : MonoBehaviour
|
|
{
|
|
public static UmaViewerBuilder Instance;
|
|
static UmaViewerMain Main => UmaViewerMain.Instance;
|
|
static UmaViewerUI UI => UmaViewerUI.Instance;
|
|
static UISettingsModel ModelSettings => UmaViewerUI.Instance.ModelSettings;
|
|
|
|
public List<Shader> ShaderList = new List<Shader>();
|
|
public Material TransMaterialCharas;
|
|
public UmaContainerCharacter CurrentUMAContainer;
|
|
public UmaContainer CurrentOtherContainer;
|
|
|
|
public UmaHeadData CurrentHead;
|
|
|
|
public List<AudioSource> CurrentAudioSources = new List<AudioSource>();
|
|
public List<UmaLyricsData> CurrentLyrics = new List<UmaLyricsData>();
|
|
|
|
// Used for keeping track for exports
|
|
public List<UmaDatabaseEntry> CurrentLiveSoundAWB = new List<UmaDatabaseEntry>();
|
|
public int CurrentLiveSoundAWBIndex = -1;
|
|
|
|
public AnimatorOverrideController OverrideController;
|
|
public AnimatorOverrideController FaceOverrideController;
|
|
public AnimatorOverrideController CameraOverrideController;
|
|
public Animator AnimationCameraAnimator;
|
|
public Camera AnimationCamera;
|
|
|
|
public GameObject LiveControllerPrefab;
|
|
|
|
// ---- Normal (non-mob, non-mini) costume visibility options ----
|
|
public enum NormalCostumeHideMode
|
|
{
|
|
None = 0,
|
|
// Hides the entire body renderer hierarchy (clothes are usually baked into the body prefab).
|
|
HideAllBodyRenderers = 1,
|
|
// Tries to hide only "clothes" renderers by matching renderer/material names (adjust keywords after checking logs).
|
|
HideByNameOrMaterial = 2
|
|
}
|
|
|
|
[Header("Normal Costume Visibility")]
|
|
public NormalCostumeHideMode NormalHideMode = NormalCostumeHideMode.None;
|
|
|
|
[Tooltip("Prints body renderers/materials once after loading a normal model (useful to tune keywords).")]
|
|
public bool DumpNormalRenderersOnce = false;
|
|
|
|
// Keywords used by HideByNameOrMaterial (edit these based on Dump logs).
|
|
public string[] NormalClothKeywords = new[] { "cloth", "clothes", "dress", "wear", "costume", "skirt", "uniform", "outer" };
|
|
public string[] NormalBodyKeywords = new[] { "skin", "body", "bdy", "face", "head", "eye", "hair" };
|
|
|
|
private void Awake()
|
|
{
|
|
Instance = this;
|
|
}
|
|
|
|
public IEnumerator LoadUma(CharaEntry chara, string costumeId, bool mini, string haedCostumeId = "")
|
|
{
|
|
int id = chara.Id;
|
|
var umaContainer = new GameObject($"Chara_{id}_{costumeId}").AddComponent<UmaContainerCharacter>();
|
|
CurrentUMAContainer = umaContainer;
|
|
|
|
if (mini)
|
|
{
|
|
umaContainer.CharaData = UmaDatabaseController.ReadCharaData(chara);
|
|
LoadMiniUma(umaContainer, chara, costumeId);
|
|
}
|
|
else if (chara.IsMob)
|
|
{
|
|
umaContainer.CharaData = UmaDatabaseController.ReadCharaData(chara);
|
|
LoadMobUma(umaContainer, chara, costumeId, loadMotion: true);
|
|
}
|
|
else if (ModelSettings.IsHeadFix && CurrentHead != null && CurrentHead.chara.IsMob)
|
|
{
|
|
umaContainer.CharaData = UmaDatabaseController.ReadCharaData(CurrentHead.chara);
|
|
LoadMobUma(umaContainer, CurrentHead.chara, costumeId, chara.Id, true);
|
|
}
|
|
else
|
|
{
|
|
umaContainer.CharaData = UmaDatabaseController.ReadCharaData(chara);
|
|
LoadNormalUma(umaContainer, chara, costumeId, true, haedCostumeId);
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
public void LoadLiveUma(List<LiveCharacterLoadData> characters)
|
|
{
|
|
for (int i = 0; i < characters.Count; i++)
|
|
{
|
|
if (characters[i].CharaEntry.Name != "")
|
|
{
|
|
var umaContainer = new GameObject($"Chara_{characters[i].CharaEntry.Id}_{characters[i].CostumeId}").AddComponent<UmaContainerCharacter>();
|
|
umaContainer.IsLive = true;
|
|
umaContainer.CharaData = UmaDatabaseController.ReadCharaData(characters[i].CharaEntry);
|
|
var charObjs = Gallop.Live.Director.instance.charaObjs;
|
|
umaContainer.transform.parent = charObjs[i];
|
|
umaContainer.transform.localPosition = new Vector3();
|
|
|
|
if (characters[i].CharaEntry.IsMob)
|
|
{
|
|
LoadMobUma(umaContainer, characters[i].CharaEntry, characters[i].CostumeId);
|
|
}
|
|
else
|
|
{
|
|
LoadNormalUma(umaContainer, characters[i].CharaEntry, characters[i].CostumeId, false, characters[i].HeadCostumeId);
|
|
}
|
|
|
|
if (umaContainer.UmaAnimator != null)
|
|
{
|
|
umaContainer.UmaAnimator.enabled = false;
|
|
umaContainer.isAnimatorControl = false;
|
|
}
|
|
|
|
umaContainer.ConfigureLivePhysics();
|
|
Gallop.Live.LiveRuntimeDiagnostics.RecordInstantiation($"character_slot_{i}", umaContainer.gameObject);
|
|
|
|
Gallop.Live.Director.instance.CharaContainerScript.Add(umaContainer);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LoadNormalUma(UmaContainerCharacter umaContainer, CharaEntry chara, string costumeId, bool loadMotion = false, string haedCostumeId = "")
|
|
{
|
|
int id = chara.Id;
|
|
umaContainer.CharaEntry = chara;
|
|
DataRow charaData = umaContainer.CharaData;
|
|
bool genericCostume = umaContainer.IsGeneric = costumeId.Length >= 4;
|
|
string skin, height, socks, bust, sex, shape, costumeIdShort = "";
|
|
skin = charaData["skin"].ToString();
|
|
height = charaData["height"].ToString();
|
|
socks = charaData["socks"].ToString();
|
|
bust = charaData["bust"].ToString();
|
|
sex = charaData["sex"].ToString();
|
|
shape = charaData["shape"].ToString();
|
|
|
|
UmaDatabaseEntry asset = null;
|
|
|
|
umaContainer.VarCostumeIdLong = costumeId;
|
|
if (genericCostume)
|
|
{
|
|
costumeIdShort = costumeId.Remove(costumeId.LastIndexOf('_'));
|
|
umaContainer.VarCostumeIdShort = costumeIdShort;
|
|
umaContainer.VarBust = bust;
|
|
umaContainer.VarSkin = skin;
|
|
umaContainer.VarSocks = socks;
|
|
umaContainer.VarHeight = height;
|
|
|
|
// Pattern for generic body type is as follows:
|
|
//
|
|
// (costume id)_(body_type_sub)_(body_setting)_(height)_(shape)_(bust)
|
|
//
|
|
// body_type_sub is used for variants like the summer/winter uniform or the swimsuit/towel
|
|
// body_setting is used for subvariants of each variant like the big belly version of the uniform, and the genders for the tracksuits
|
|
//
|
|
// Some models will naturally be missing due to how this system is designed.
|
|
|
|
string body = UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/pfb_bdy{costumeId}_{height}_{shape}_{bust}";
|
|
UmaViewerMain.Instance.AbList.TryGetValue(body, out asset);
|
|
}
|
|
else UmaViewerMain.Instance.AbList.TryGetValue(UmaDatabaseController.BodyPath + $"bdy{id}_{costumeId}/pfb_bdy{id}_{costumeId}", out asset);
|
|
|
|
if (asset == null)
|
|
{
|
|
Debug.LogError("No body, can't load!");
|
|
UmaViewerUI.Instance?.ShowMessage("No body, can't load!", UIMessageType.Error);
|
|
return;
|
|
}
|
|
else if (genericCostume)
|
|
{
|
|
string texPattern1 = "", texPattern2 = "", texPattern3 = "", texPattern4 = "", texPattern5 = "";
|
|
switch (costumeId.Split('_')[0])
|
|
{
|
|
case "0001":
|
|
texPattern1 = $"tex_bdy{costumeIdShort}_00_{skin}_{bust}_0{socks}";
|
|
texPattern2 = $"tex_bdy{costumeIdShort}_00_0_{bust}";
|
|
texPattern3 = $"tex_bdy{costumeIdShort}_zekken";
|
|
texPattern4 = $"tex_bdy{costumeIdShort}_00_waku";
|
|
texPattern5 = $"tex_bdy{costumeIdShort}_num";
|
|
break;
|
|
case "0003":
|
|
texPattern1 = $"tex_bdy{costumeIdShort}_00_{skin}_{bust}";
|
|
texPattern2 = $"tex_bdy{costumeIdShort}_00_0_{bust}";
|
|
break;
|
|
case "0006": //last var is color?
|
|
texPattern1 = $"tex_bdy{costumeId}_{skin}_{bust}_0{0}";
|
|
texPattern2 = $"tex_bdy{costumeId}_0_{bust}_00_";
|
|
break;
|
|
default:
|
|
texPattern1 = $"tex_bdy{costumeId}_{skin}_{bust}";
|
|
texPattern2 = $"tex_bdy{costumeId}_0_{bust}";
|
|
break;
|
|
}
|
|
Debug.Log(texPattern1 + " " + texPattern2);
|
|
//Load Body Textures
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith(UmaDatabaseController.BodyPath)
|
|
&& (a.Name.Contains(texPattern1)
|
|
|| a.Name.Contains(texPattern2)
|
|
|| (string.IsNullOrEmpty(texPattern3) ? false : a.Name.Contains(texPattern3))
|
|
|| (string.IsNullOrEmpty(texPattern4) ? false : a.Name.Contains(texPattern4))
|
|
|| (string.IsNullOrEmpty(texPattern5) ? false : a.Name.Contains(texPattern5)))))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
//Load Body
|
|
umaContainer.LoadBody(asset);
|
|
|
|
//Load Physics
|
|
if (Main.AbList.TryGetValue(UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/clothes/pfb_bdy{costumeIdShort}_cloth00", out _))
|
|
{
|
|
var asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/clothes/pfb_bdy{costumeIdShort}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/clothes/pfb_bdy{costumeIdShort}_bust{bust}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
umaContainer.LoadBody(asset);
|
|
// Preserve master5's verified thigh/hip skirt collision fallback for winner outfits.
|
|
const string schoolUniformCollision = "3d/chara/body/bdy0002_00/clothes/pfb_bdy0002_00_cloth00";
|
|
if (Main.AbList.TryGetValue(schoolUniformCollision, out UmaDatabaseEntry schoolCollisionAsset))
|
|
umaContainer.LoadSchoolUniformSkirtCollision(schoolCollisionAsset);
|
|
|
|
//Load Physics
|
|
var asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{id}_{costumeId}/clothes/pfb_bdy{id}_{costumeId}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{id}_{costumeId}/clothes/pfb_bdy{id}_{costumeId}_bust_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
}
|
|
|
|
// Record Head Data
|
|
int head_id;
|
|
string head_costumeId;
|
|
int tailId = Convert.ToInt32(charaData["tail_model_id"]);
|
|
|
|
if (ModelSettings.IsHeadFix && CurrentHead != null)
|
|
{
|
|
head_id = CurrentHead.id;
|
|
head_costumeId = CurrentHead.costumeId;
|
|
tailId = CurrentHead.tailId;
|
|
}
|
|
else
|
|
{
|
|
head_id = id;
|
|
head_costumeId = string.IsNullOrEmpty(haedCostumeId) ? costumeId : haedCostumeId;
|
|
|
|
CurrentHead = new UmaHeadData
|
|
{
|
|
id = id,
|
|
costumeId = head_costumeId,
|
|
tailId = tailId,
|
|
chara = chara
|
|
};
|
|
}
|
|
|
|
string head = UmaDatabaseController.HeadPath + $"chr{head_id}_{head_costumeId}/pfb_chr{head_id}_{head_costumeId}";
|
|
asset = null;
|
|
Main.AbList.TryGetValue(head, out asset);
|
|
|
|
bool isDefaultHead = false;
|
|
//Some costumes don't have custom heads
|
|
if (head_costumeId != "00" && asset == null)
|
|
{
|
|
asset = Main.AbList[UmaDatabaseController.HeadPath + $"chr{head_id}_00/pfb_chr{head_id}_00"];
|
|
isDefaultHead = true;
|
|
}
|
|
|
|
if (asset != null)
|
|
{
|
|
//Load Hair Textures
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"{UmaDatabaseController.HeadPath}chr{head_id}_{head_costumeId}/textures")))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
|
|
//Load Head
|
|
umaContainer.LoadHead(asset);
|
|
|
|
//Load Physics
|
|
// Load head CySpring physics.
|
|
//角色实际加载了哪个头,就必须加载同一个头目录下的 cloth。
|
|
string actualHeadCostumeId = isDefaultHead ? "00" : head_costumeId;
|
|
|
|
string headClothPath =
|
|
UmaDatabaseController.HeadPath +
|
|
$"chr{head_id}_{actualHeadCostumeId}/clothes/" +
|
|
$"pfb_chr{head_id}_{actualHeadCostumeId}_cloth00";
|
|
|
|
if (Main.AbList.TryGetValue(headClothPath, out UmaDatabaseEntry headClothAsset))
|
|
{
|
|
Debug.Log($"[UmaViewerBuilder] Load head physics: {headClothPath}");
|
|
umaContainer.LoadPhysics(headClothAsset);
|
|
}
|
|
else
|
|
{
|
|
string message =
|
|
$"[UmaViewerBuilder] Head CySpring physics not found: {headClothPath}";
|
|
Debug.LogError(message);
|
|
// CySpring 缺失会继续加载角色,但必须通过用户界面的错误级别提示用户。
|
|
UmaViewerUI.Instance?.ShowMessage(
|
|
$"Head CySpring physics not found:\n{headClothPath}",
|
|
UIMessageType.Error
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//修改(载入专用尾巴相关)
|
|
if (tailId > 0)
|
|
{
|
|
string costumePrefixForTail = $"{id}_{costumeId}";
|
|
string exclusiveTailName = $"tail{costumePrefixForTail}";
|
|
string exclusiveTailPath = $"3d/chara/tail/{exclusiveTailName}/";
|
|
string exclusiveTailPfb = $"{exclusiveTailPath}pfb_{exclusiveTailName}";
|
|
|
|
if (Main.AbList.TryGetValue(exclusiveTailPfb, out asset))
|
|
{
|
|
umaContainer.LoadExclusiveTail(asset);
|
|
|
|
string clothPath = $"{exclusiveTailPath}clothes/pfb_{exclusiveTailName}_cloth00";
|
|
if (Main.AbList.TryGetValue(clothPath, out var clothAsset))
|
|
{
|
|
umaContainer.LoadPhysics(clothAsset);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string tailName = $"tail{tailId.ToString().PadLeft(4, '0')}_00";
|
|
string tailPath = $"3d/chara/tail/{tailName}/";
|
|
string tailPfb = $"{tailPath}pfb_{tailName}";
|
|
asset = null;
|
|
|
|
if (Main.AbList.TryGetValue(tailPfb, out asset) && asset != null)
|
|
{
|
|
foreach (var asset2 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"{tailPath}textures/tex_{tailName}_{head_id}") || a.Name.StartsWith($"{tailPath}textures/tex_{tailName}_0000")))
|
|
{
|
|
umaContainer.LoadTextures(asset2);
|
|
}
|
|
|
|
umaContainer.LoadTail(asset);
|
|
|
|
string clothPath = $"{tailPath}clothes/pfb_{tailName}_cloth00";
|
|
if (Main.AbList.TryGetValue(clothPath, out var clothAsset))
|
|
{
|
|
umaContainer.LoadPhysics(clothAsset);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("no tail");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
umaContainer.LoadPhysics();
|
|
umaContainer.SetDynamicBoneEnable(ModelSettings.DynamicBoneEnable);
|
|
umaContainer.LoadFaceMorph(id, costumeId);
|
|
umaContainer.TearControllers.ForEach(a => a.SetDir(a.CurrentDir));
|
|
umaContainer.HeadBone = (GameObject)umaContainer.Body.GetComponent<AssetHolder>()._assetTable["head"];
|
|
umaContainer.EyeHeight = umaContainer.Head.GetComponent<AssetHolder>()._assetTableValue["head_center_offset_y"];
|
|
umaContainer.MergeModel();
|
|
ApplyNormalCostumeVisibilityOptions(umaContainer);
|
|
umaContainer.SetHeight(-1);
|
|
umaContainer.Initialize(!ModelSettings.IsTPose);
|
|
|
|
umaContainer.Position = umaContainer.transform.Find("Position");
|
|
umaContainer.SetupBoneHandles();
|
|
|
|
if (!ModelSettings.IsTPose && loadMotion)
|
|
{
|
|
if (Main.AbList.TryGetValue($"3d/motion/event/body/chara/chr{id}_00/anm_eve_chr{id}_00_idle01_loop", out UmaDatabaseEntry entry))
|
|
{
|
|
umaContainer.LoadAnimation(entry);
|
|
}
|
|
}
|
|
|
|
|
|
//修改(载入通用服装ColorSet相关)
|
|
UI.ClearColorSetButtons();
|
|
UI.LoadColorSetButtons();
|
|
|
|
|
|
}
|
|
|
|
private void LoadMobUma(UmaContainerCharacter umaContainer, CharaEntry chara, string costumeId, int bodyid = -1, bool loadMotion = false)
|
|
{
|
|
int id = chara.Id;
|
|
umaContainer.CharaEntry = chara;
|
|
umaContainer.IsMob = chara.IsMob;
|
|
umaContainer.MobDressColor = UmaDatabaseController.ReadMobDressColor(umaContainer.CharaData["dress_color_id"].ToString());
|
|
umaContainer.MobHeadColor = UmaDatabaseController.ReadMobHairColor(umaContainer.CharaData["chara_hair_color"].ToString());
|
|
|
|
DataRow charaData = umaContainer.CharaData;
|
|
bool genericCostume = umaContainer.IsGeneric = costumeId.Length >= 4;
|
|
string skin, height, socks, bust, sex, shape, costumeIdShort = "";
|
|
string faceid, hairid, personality;
|
|
|
|
faceid = charaData["chara_face_model"].ToString();
|
|
hairid = charaData["chara_hair_model"].ToString();
|
|
personality = charaData["default_personality"].ToString();
|
|
skin = charaData["chara_skin_color"].ToString();
|
|
height = "1";
|
|
socks = charaData["socks"].ToString();
|
|
bust = charaData["chara_bust_size"].ToString();
|
|
sex = charaData["sex"].ToString();
|
|
shape = "0";
|
|
|
|
UmaDatabaseEntry asset = null;
|
|
if (genericCostume)
|
|
{
|
|
costumeIdShort = costumeId.Remove(costumeId.LastIndexOf('_'));
|
|
umaContainer.VarCostumeIdShort = costumeIdShort;
|
|
umaContainer.VarCostumeIdLong = costumeId;
|
|
umaContainer.VarBust = bust;
|
|
umaContainer.VarSkin = skin;
|
|
umaContainer.VarSocks = socks;
|
|
umaContainer.VarHeight = height;
|
|
|
|
// Pattern for generic body type is as follows:
|
|
//
|
|
// (costume id)_(body_type_sub)_(body_setting)_(height)_(shape)_(bust)
|
|
//
|
|
// body_type_sub is used for variants like the summer/winter uniform or the swimsuit/towel
|
|
// body_setting is used for subvariants of each variant like the big belly version of the uniform, and the genders for the tracksuits
|
|
//
|
|
// Some models will naturally be missing due to how this system is designed.
|
|
|
|
string body = "";
|
|
body = UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/pfb_bdy{costumeId}_{height}_{shape}_{bust}";
|
|
|
|
Debug.Log("Looking for " + body);
|
|
Main.AbList.TryGetValue(body, out asset);
|
|
}
|
|
else Main.AbList.TryGetValue($"{UmaDatabaseController.BodyPath}bdy{bodyid}_{costumeId}/pfb_bdy{bodyid}_{costumeId}", out asset);
|
|
|
|
if (asset == null)
|
|
{
|
|
Debug.LogError("No body, can't load!");
|
|
UmaViewerUI.Instance?.ShowMessage("No body, can't load!", UIMessageType.Error);
|
|
return;
|
|
}
|
|
else if (genericCostume)
|
|
{
|
|
string texPattern1 = "", texPattern2 = "", texPattern3 = "", texPattern4 = "", texPattern5 = "";
|
|
switch (costumeId.Split('_')[0])
|
|
{
|
|
case "0001":
|
|
texPattern1 = $"tex_bdy{costumeIdShort}_00_{skin}_{bust}_0{socks}";
|
|
texPattern2 = $"tex_bdy{costumeIdShort}_00_0_{bust}";
|
|
texPattern3 = $"tex_bdy{costumeIdShort}_zekken";
|
|
texPattern4 = $"tex_bdy{costumeIdShort}_00_waku";
|
|
texPattern5 = $"tex_bdy{costumeIdShort}_num";
|
|
break;
|
|
case "0003":
|
|
texPattern1 = $"tex_bdy{costumeIdShort}_00_{skin}_{bust}";
|
|
texPattern2 = $"tex_bdy{costumeIdShort}_00_0_{bust}";
|
|
break;
|
|
case "0006": //last var is color?
|
|
texPattern1 = $"tex_bdy{costumeId}_{skin}_{bust}_0{0}";
|
|
texPattern2 = $"tex_bdy{costumeId}_0_{bust}_00_";
|
|
break;
|
|
default:
|
|
texPattern1 = $"tex_bdy{costumeId}_{skin}_{bust}";
|
|
texPattern2 = $"tex_bdy{costumeId}_0_{bust}";
|
|
break;
|
|
}
|
|
Debug.Log(texPattern1 + " " + texPattern2);
|
|
//Load Body Textures
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith(UmaDatabaseController.BodyPath)
|
|
&& (a.Name.Contains(texPattern1)
|
|
|| a.Name.Contains(texPattern2)
|
|
|| (string.IsNullOrEmpty(texPattern3) ? false : a.Name.Contains(texPattern3))
|
|
|| (string.IsNullOrEmpty(texPattern4) ? false : a.Name.Contains(texPattern4))
|
|
|| (string.IsNullOrEmpty(texPattern5) ? false : a.Name.Contains(texPattern5)))))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
|
|
//Load Body
|
|
umaContainer.LoadBody(asset);
|
|
|
|
//Load Physics
|
|
if (Main.AbList.TryGetValue(UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/clothes/pfb_bdy{costumeIdShort}_cloth00", out _))
|
|
{
|
|
var asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/clothes/pfb_bdy{costumeIdShort}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{costumeIdShort}/clothes/pfb_bdy{costumeIdShort}_bust{bust}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
umaContainer.LoadBody(asset);
|
|
//Load Physics
|
|
var asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{bodyid}_{costumeId}/clothes/pfb_bdy{bodyid}_{costumeId}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
asset1 = Main.AbList[UmaDatabaseController.BodyPath + $"bdy{bodyid}_{costumeId}/clothes/pfb_bdy{bodyid}_{costumeId}_bust_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
}
|
|
|
|
// Record Head Data
|
|
int head_id = 1;
|
|
string head_costumeId = "00";
|
|
int tailId = 1;
|
|
|
|
CurrentHead = new UmaHeadData
|
|
{
|
|
id = id,
|
|
costumeId = costumeId,
|
|
tailId = tailId,
|
|
chara = chara
|
|
};
|
|
|
|
var head_s = head_id.ToString().PadLeft(4, '0');
|
|
string head = UmaDatabaseController.HeadPath + $"chr{head_s}_{head_costumeId}/pfb_chr{head_s}_{head_costumeId}_face{faceid.PadLeft(3, '0')}";
|
|
string hair = UmaDatabaseController.HeadPath + $"chr{head_s}_{head_costumeId}/pfb_chr{head_s}_{head_costumeId}_hair{hairid.PadLeft(3, '0')}";
|
|
Main.AbList.TryGetValue(head, out asset);
|
|
Main.AbList.TryGetValue(hair, out UmaDatabaseEntry hairasset);
|
|
bool isDefaultHead = true;
|
|
|
|
|
|
if (asset != null && hairasset != null)
|
|
{
|
|
//Load Face And Hair Textures
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"{UmaDatabaseController.HeadPath}chr{head_s}_{head_costumeId}/textures")))
|
|
{
|
|
if (asset1.Name.Contains($"tex_chr{head_s}_{head_costumeId}_face{faceid.PadLeft(3, '0')}_0")
|
|
|| asset1.Name.Contains($"tex_chr{head_s}_{head_costumeId}_face{faceid.PadLeft(3, '0')}_{skin}"))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
|
|
if (asset1.Name.Contains($"tex_chr{head_s}_{head_costumeId}_hair{hairid.PadLeft(3, '0')}"))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
}
|
|
|
|
//Load face
|
|
umaContainer.LoadHead(asset);
|
|
//Load hair
|
|
umaContainer.LoadHair(hairasset);
|
|
|
|
umaContainer.MergeHairModel();
|
|
|
|
//Load Physics
|
|
// Load mob hair CySpring physics.
|
|
string paddedHairId = hairid.PadLeft(3, '0');
|
|
|
|
string hairClothPath =
|
|
$"{UmaDatabaseController.HeadPath}" +
|
|
$"chr{head_s}_{head_costumeId}/clothes/" +
|
|
$"pfb_chr{head_s}_{head_costumeId}_hair{paddedHairId}_cloth00";
|
|
|
|
if (Main.AbList.TryGetValue(
|
|
hairClothPath,
|
|
out UmaDatabaseEntry hairClothAsset))
|
|
{
|
|
Debug.Log($"[UmaViewerBuilder] Load mob hair physics: {hairClothPath}");
|
|
umaContainer.LoadPhysics(hairClothAsset);
|
|
}
|
|
else
|
|
{
|
|
// 部分资源可能使用普通 cloth00,而不是 hairXXX_cloth00。
|
|
string fallbackHeadClothPath =
|
|
$"{UmaDatabaseController.HeadPath}" +
|
|
$"chr{head_s}_{head_costumeId}/clothes/" +
|
|
$"pfb_chr{head_s}_{head_costumeId}_cloth00";
|
|
|
|
if (Main.AbList.TryGetValue(
|
|
fallbackHeadClothPath,
|
|
out UmaDatabaseEntry fallbackHeadClothAsset))
|
|
{
|
|
// 缺失专用 CySpring 时先报错,仍使用普通头部物理维持可预览状态。
|
|
string message =
|
|
$"[UmaViewerBuilder] Mob hair CySpring is missing. " +
|
|
$"Fallback to head physics: {fallbackHeadClothPath}\n" +
|
|
$"missing={hairClothPath}";
|
|
Debug.LogError(message);
|
|
UmaViewerUI.Instance?.ShowMessage(
|
|
$"Mob hair CySpring physics not found. Using fallback physics.\n" +
|
|
$"missing={hairClothPath}\nfallback={fallbackHeadClothPath}",
|
|
UIMessageType.Error
|
|
);
|
|
|
|
umaContainer.LoadPhysics(fallbackHeadClothAsset);
|
|
}
|
|
else
|
|
{
|
|
string message =
|
|
"[UmaViewerBuilder] Mob head CySpring physics not found.\n" +
|
|
$"hair={hairClothPath}\n" +
|
|
$"fallback={fallbackHeadClothPath}";
|
|
Debug.LogError(message);
|
|
UmaViewerUI.Instance?.ShowMessage(
|
|
"Mob head CySpring physics not found.\n" +
|
|
$"hair={hairClothPath}\nfallback={fallbackHeadClothPath}",
|
|
UIMessageType.Error
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tailId > 0)
|
|
{
|
|
string tailName = $"tail{tailId.ToString().PadLeft(4, '0')}_00";
|
|
string tailPath = $"3d/chara/tail/{tailName}/";
|
|
string tailPfb = tailPath + $"pfb_{tailName}";
|
|
asset = UmaViewerMain.Instance.AbChara.FirstOrDefault(a => a.Name == tailPfb);
|
|
if (asset != null)
|
|
{
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"{tailPath}textures/tex_{tailName}_{head_s}") || a.Name.StartsWith($"{tailPath}textures/tex_{tailName}_0000")))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
umaContainer.LoadTail(asset);
|
|
|
|
|
|
//Load Physics
|
|
if (Main.AbList.TryGetValue($"{tailPath}clothes/pfb_{tailName}_cloth00", out UmaDatabaseEntry asset2))
|
|
{
|
|
umaContainer.LoadPhysics(asset2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("no tail");
|
|
}
|
|
}
|
|
|
|
umaContainer.LoadPhysics(); //Need to load physics before loading FacialMorph
|
|
umaContainer.SetDynamicBoneEnable(ModelSettings.DynamicBoneEnable);
|
|
|
|
//Load FacialMorph
|
|
umaContainer.LoadFaceMorph(id, costumeId);
|
|
|
|
umaContainer.TearControllers.ForEach(a => a.SetDir(a.CurrentDir));
|
|
umaContainer.HeadBone = (GameObject)umaContainer.Body.GetComponent<AssetHolder>()._assetTable["head"];
|
|
umaContainer.EyeHeight = umaContainer.Head.GetComponent<AssetHolder>()._assetTableValue["head_center_offset_y"];
|
|
umaContainer.MergeModel();
|
|
umaContainer.SetHeight(-1);
|
|
umaContainer.Initialize(!ModelSettings.IsTPose);
|
|
|
|
umaContainer.Position = umaContainer.transform.Find("Position");
|
|
umaContainer.SetupBoneHandles();
|
|
|
|
if (!ModelSettings.IsTPose && loadMotion)
|
|
{
|
|
if (Main.AbList.TryGetValue($"3d/motion/event/body/type00/anm_eve_type00_homestand{personality.PadLeft(2, '0')}_loop", out UmaDatabaseEntry entry))
|
|
{
|
|
umaContainer.LoadAnimation(entry);
|
|
}
|
|
}
|
|
|
|
|
|
//修改(载入通用服装ColorSet相关)
|
|
UI.ClearColorSetButtons();
|
|
|
|
|
|
}
|
|
|
|
private void LoadMiniUma(UmaContainerCharacter umaContainer, CharaEntry chara, string costumeId)
|
|
{
|
|
int id = chara.Id;
|
|
umaContainer.CharaEntry = chara;
|
|
DataRow charaData = umaContainer.CharaData;
|
|
umaContainer.IsMini = true;
|
|
bool isGeneric = umaContainer.IsGeneric = costumeId.Length >= 4;
|
|
int tailId = Convert.ToInt32(charaData["tail_model_id"]);
|
|
string skin = charaData["skin"].ToString(),
|
|
height = charaData["height"].ToString(),
|
|
socks = charaData["socks"].ToString(),
|
|
bust = charaData["bust"].ToString(),
|
|
sex = charaData["sex"].ToString(),
|
|
costumeIdShort = "";
|
|
bool customHead = true;
|
|
|
|
UmaDatabaseEntry asset = null;
|
|
if (isGeneric)
|
|
{
|
|
costumeIdShort = costumeId.Remove(costumeId.LastIndexOf('_'));
|
|
string body = $"3d/chara/mini/body/mbdy{costumeIdShort}/pfb_mbdy{costumeId}_0";
|
|
Main.AbList.TryGetValue(body,out asset);
|
|
}
|
|
else Main.AbList.TryGetValue($"3d/chara/mini/body/mbdy{id}_{costumeId}/pfb_mbdy{id}_{costumeId}",out asset);
|
|
if (asset == null)
|
|
{
|
|
Debug.LogError("No body, can't load!");
|
|
UmaViewerUI.Instance?.ShowMessage("No body, can't load!", UIMessageType.Error);
|
|
return;
|
|
}
|
|
else if (isGeneric)
|
|
{
|
|
string texPattern1 = "";
|
|
switch (costumeId.Split('_')[0])
|
|
{
|
|
case "0003":
|
|
texPattern1 = $"tex_mbdy{costumeIdShort}_00_{skin}_{0}";
|
|
break;
|
|
default:
|
|
texPattern1 = $"tex_mbdy{costumeId}_{skin}_{0}";
|
|
break;
|
|
}
|
|
//Load Body Textures
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith("3d/chara/mini/body/") && a.Name.Contains(texPattern1)))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
//Load Body
|
|
umaContainer.LoadBody(asset);
|
|
}
|
|
else
|
|
umaContainer.LoadBody(asset);
|
|
|
|
string hair = $"3d/chara/mini/head/mchr{id}_{costumeId}/pfb_mchr{id}_{costumeId}_hair";
|
|
Main.AbList.TryGetValue(hair, out asset);
|
|
if (costumeId != "00" && asset == null)
|
|
{
|
|
customHead = false;
|
|
Main.AbList.TryGetValue($"3d/chara/mini/head/mchr{id}_00/pfb_mchr{id}_00_hair", out asset);
|
|
}
|
|
|
|
if (asset != null)
|
|
{
|
|
//Load Hair Textures
|
|
if (customHead)
|
|
{
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"3d/chara/mini/head/mchr{id}_{costumeId}/textures")))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"3d/chara/mini/head/mchr{id}_00/textures")))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
}
|
|
|
|
//Load Hair (don't know why loadhead is used)
|
|
umaContainer.LoadHead(asset);
|
|
}
|
|
|
|
string head = $"3d/chara/mini/head/mchr0001_00/pfb_mchr0001_00_face0";
|
|
Main.AbList.TryGetValue(head,out asset);
|
|
if (asset != null)
|
|
{
|
|
//Load Head Textures
|
|
foreach (var asset1 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"3d/chara/mini/head/mchr0001_00/textures/tex_mchr0001_00_face0_{skin}")))
|
|
{
|
|
umaContainer.LoadTextures(asset1);
|
|
}
|
|
//Load Head
|
|
umaContainer.LoadHead(asset);
|
|
}
|
|
|
|
if (tailId > 0)
|
|
{
|
|
string tailName = $"mtail{tailId.ToString().PadLeft(4, '0')}_00";
|
|
string tailPath = $"3d/chara/mini/tail/{tailName}/";
|
|
string tailPfb = tailPath + $"pfb_{tailName}";
|
|
asset = null;
|
|
Main.AbList.TryGetValue(tailPfb, out asset);
|
|
if (asset != null)
|
|
{
|
|
foreach (var asset2 in UmaViewerMain.Instance.AbChara.Where(a => a.Name.StartsWith($"{tailPath}textures/tex_{tailName}_{id.ToString().PadLeft(4,'0')}")))
|
|
{
|
|
umaContainer.LoadTextures(asset2);
|
|
}
|
|
|
|
umaContainer.LoadTail(asset);
|
|
|
|
//Load Physics
|
|
var asset1 = Main.AbList[$"{tailPath}clothes/pfb_{tailName}_cloth00"];
|
|
umaContainer.LoadPhysics(asset1);
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("no tail");
|
|
}
|
|
}
|
|
|
|
umaContainer.MergeModel();
|
|
var matList = new List<MeshRenderer>(umaContainer.GetComponentsInChildren<MeshRenderer>());
|
|
var eyemat = matList.FirstOrDefault(a => a.gameObject.name.Equals("M_Eye"));
|
|
var mouthmat = matList.FirstOrDefault(a => a.gameObject.name.Equals("M_Mouth"));
|
|
var eyebrowLmat = matList.FirstOrDefault(a => a.gameObject.name.Equals("M_Mayu_L"));
|
|
var eyebrowRmat = matList.FirstOrDefault(a => a.gameObject.name.Equals("M_Mayu_R"));
|
|
UI.LoadFacialPanelsMini(eyemat.material, eyebrowLmat.material, eyebrowRmat.material, mouthmat.material);
|
|
|
|
umaContainer.Position = umaContainer.transform.Find("Position");
|
|
umaContainer.SetupBoneHandles();
|
|
|
|
if (Main.AbList.TryGetValue($"3d/motion/mini/event/body/chara/chr{id}_00/anm_min_eve_chr{id}_00_idle01_loop",out asset))
|
|
{
|
|
umaContainer.LoadAnimation(asset);
|
|
}
|
|
|
|
|
|
//修改(载入通用服装ColorSet相关)
|
|
UI.ClearColorSetButtons();
|
|
|
|
|
|
}
|
|
|
|
public void LoadProp(UmaDatabaseEntry entry)
|
|
{
|
|
UnloadProp();
|
|
UmaAssetManager.UnloadAllBundle();
|
|
|
|
var prop = new GameObject(Path.GetFileName(entry.Name)).AddComponent<UmaContainerProp>();
|
|
if (!prop.LoadProp(entry))
|
|
{
|
|
Destroy(prop.gameObject);
|
|
return;
|
|
}
|
|
|
|
CurrentOtherContainer = prop;
|
|
}
|
|
|
|
public void LoadLive(LiveEntry live, List<LiveCharacterSelect> characters)
|
|
{
|
|
if (!LiveAutoSelection.FillMissing(Main, live, characters, UI.CostumeIconDefault))
|
|
{
|
|
UI.ShowMessage("Cannot fill the live selection: no available character-owned winning costume matches an empty slot.", UIMessageType.Error);
|
|
return;
|
|
}
|
|
|
|
// 在旧 UI 场景卸载前复制成普通 C# 数据。后续异步加载不会丢角色选择。
|
|
List<LiveCharacterLoadData> liveCharacters =
|
|
LiveCharacterLoadData.CaptureAll(characters);
|
|
|
|
bool requireStage = UI.isRequireStage;
|
|
int liveMode = UI.LiveMode;
|
|
// Opt-in diagnostics begin before root registration, then follow the
|
|
// exact dependency-expanded request count shown by the progress UI.
|
|
LiveRuntimeDiagnostics.BeginPreload(live, liveCharacters, requireStage);
|
|
List<UmaDatabaseEntry> preloadEntries =
|
|
LiveResourceRegister.RegisterDownload(live, liveCharacters, requireStage);
|
|
|
|
UmaAssetManager.PreLoadAndRun(preloadEntries, delegate
|
|
{
|
|
LiveRuntimeDiagnostics.RecordPhase("preload_callback");
|
|
UmaSceneController.LoadScene(
|
|
"LiveScene",
|
|
delegate
|
|
{
|
|
GameObject mainLive = Instantiate(LiveControllerPrefab);
|
|
Director controller = mainLive.GetComponentInChildren<Director>();
|
|
controller.live = live;
|
|
controller.IsRecordVMD = UI.isRecordVMD;
|
|
controller.RequireStage = requireStage;
|
|
|
|
var binder = mainLive.GetComponent<CyalumeAutoBinder>() ??
|
|
mainLive.AddComponent<CyalumeAutoBinder>();
|
|
|
|
if (mainLive.GetComponent<Gallop.Live.StageBlinkLightDriver>() == null)
|
|
mainLive.AddComponent<Gallop.Live.StageBlinkLightDriver>();
|
|
|
|
if (mainLive.GetComponent<Gallop.Live.StageUVScrollLightDriver>() == null)
|
|
mainLive.AddComponent<Gallop.Live.StageUVScrollLightDriver>();
|
|
|
|
if (mainLive.GetComponent<Gallop.Live.MonitorUvMovieProvider>() == null)
|
|
mainLive.AddComponent<Gallop.Live.MonitorUvMovieProvider>();
|
|
|
|
if (mainLive.GetComponent<Gallop.Live.StageMonitorDriver>() == null)
|
|
mainLive.AddComponent<Gallop.Live.StageMonitorDriver>();
|
|
|
|
binder.musicId = live.MusicId;
|
|
|
|
var transferObjs = new List<GameObject>
|
|
{
|
|
mainLive,
|
|
GameObject.Find("ViewerMain"),
|
|
GameObject.Find("Directional Light"),
|
|
GameObject.Find("GlobalShaderController"),
|
|
GameObject.Find("AudioManager")
|
|
};
|
|
|
|
foreach (GameObject obj in transferObjs)
|
|
{
|
|
if (obj != null)
|
|
{
|
|
SceneManager.MoveGameObjectToScene(
|
|
obj,
|
|
SceneManager.GetSceneByName("LiveScene"));
|
|
}
|
|
}
|
|
|
|
controller.Initialize();
|
|
LiveRuntimeDiagnostics.RecordPhase("director_initialized", controller);
|
|
// Native full-mode callback (RVA 0x1ba8981..0x1ba8b76) cycles
|
|
// selected identities through the raw authored channel count.
|
|
// Simple mode uses only the UI's basic-motion selection capacity.
|
|
int motionChannelCount = controller._liveTimelineControl.data.worksheetList[0].charaMotSeqList.Count;
|
|
if (liveMode == 1 && liveCharacters.Count > 0 && motionChannelCount > liveCharacters.Count)
|
|
{
|
|
var channelCharacters = new List<LiveCharacterLoadData>(motionChannelCount);
|
|
for (int i = 0; i < motionChannelCount; i++)
|
|
channelCharacters.Add(liveCharacters[i % liveCharacters.Count]);
|
|
liveCharacters = channelCharacters;
|
|
}
|
|
|
|
|
|
|
|
LoadLiveUma(liveCharacters);
|
|
LiveRuntimeDiagnostics.RecordPhase("characters_loaded", controller);
|
|
|
|
var lyrics = LoadLiveLyrics(live.MusicId);
|
|
if (lyrics != null)
|
|
LiveViewerUI.Instance.CurrentLyrics = lyrics;
|
|
},
|
|
delegate
|
|
{
|
|
var director = Director.instance;
|
|
var liveAssetPaths = new HashSet<string>(StringComparer.Ordinal);
|
|
LivePropsController.CollectResourcePaths(
|
|
director._liveTimelineControl.data.propsSettings,
|
|
director.CharaContainerScript, liveAssetPaths);
|
|
LiveEffectController.CollectResourcePaths(
|
|
director._liveTimelineControl.GetWorkSheetBySheetIndex(LiveTimelineDefine.SheetIndex.MainLive),
|
|
TimelinePlayerMode.Default, variation => variation == 0, liveAssetPaths);
|
|
foreach (var path in LiveLightShaftsResources.GetTexturePaths(
|
|
director._liveTimelineControl.data.indirectLightShaftsSettings))
|
|
if (!string.IsNullOrEmpty(path)) liveAssetPaths.Add(path);
|
|
if (director.RequireStage)
|
|
{
|
|
liveAssetPaths.Add(FootLightRuntime.DefaultSpotlightPath);
|
|
liveAssetPaths.Add(FootLightRuntime.DefaultShadowPath);
|
|
}
|
|
if (director.RequireStage && director._liveTimelineControl.data.worksheetList.Any(
|
|
sheet => sheet?.spotlight3dList != null && sheet.spotlight3dList.Count != 0))
|
|
liveAssetPaths.Add(Director.Spotlight3dControllerPath);
|
|
foreach (var sheet in director._liveTimelineControl.data.worksheetList)
|
|
{
|
|
if (sheet?.lightProjectionList == null) continue;
|
|
foreach (var group in sheet.lightProjectionList)
|
|
{
|
|
if (group?.keys?.thisList == null) continue;
|
|
foreach (var key in group.keys.thisList)
|
|
{
|
|
if (key.TextureId >= 0 && !key.UseMonitorMovie())
|
|
liveAssetPaths.Add(group.ContentType == 1
|
|
? Director.GetMirrorBallProjectionTexturePath(key.TextureId)
|
|
: Director.GetLightProjectionTexturePath(key.TextureId));
|
|
if (key.AnimationParam != null && key.AnimationParam.TextureId >= 0)
|
|
liveAssetPaths.Add(Director.GetLightProjectionTexturePath(key.AnimationParam.TextureId));
|
|
}
|
|
}
|
|
}
|
|
var effectEntries = new List<UmaDatabaseEntry>();
|
|
foreach (var path in liveAssetPaths)
|
|
{
|
|
var entry = LiveEffectController.FindResourceEntry(path);
|
|
if (entry != null && !effectEntries.Contains(entry)) effectEntries.Add(entry);
|
|
}
|
|
UmaAssetManager.PreLoadAndRun(effectEntries, delegate
|
|
{
|
|
try
|
|
{
|
|
if (director == null) return;
|
|
director.InitializeUI();
|
|
director.InitializeTimeline(liveCharacters, liveMode);
|
|
director.InitializeMusic(live.MusicId, liveCharacters);
|
|
LiveRuntimeDiagnostics.RecordPhase("timeline_music_initialized", director);
|
|
director.Play();
|
|
}
|
|
finally
|
|
{
|
|
// Effect.Load holds its own recursive references; release this preload's roots.
|
|
foreach (var entry in effectEntries) UmaAssetManager.UnloadAssetBundle(entry, false);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
//Use decrypt function
|
|
public void LoadLiveSound(int songid, UmaDatabaseEntry SongAwb, bool needLyrics = true)
|
|
{
|
|
CurrentLiveSoundAWBIndex = -1; // mix awb together
|
|
ClearLiveSounds();
|
|
//load character voice
|
|
if (SongAwb != null)
|
|
{
|
|
PlaySound(SongAwb);
|
|
AddLiveSound(SongAwb);
|
|
}
|
|
|
|
//load BG
|
|
string nameVar = $"snd_bgm_live_{songid}_oke";
|
|
UmaDatabaseEntry BGawb = Main.AbSounds.FirstOrDefault(a => a.Name.Contains(nameVar) && a.Name.EndsWith("awb"));
|
|
if (BGawb != null)
|
|
{
|
|
var BGclip = LoadAudio(BGawb);
|
|
if (BGclip.Count > 0)
|
|
{
|
|
AddAudioSource(BGclip[0]);
|
|
AddLiveSound(BGawb);
|
|
}
|
|
}
|
|
|
|
if (needLyrics)
|
|
{
|
|
LoadLiveLyrics(songid);
|
|
}
|
|
}
|
|
|
|
public void loadLivePreviewSound(int songid)
|
|
{
|
|
string nameVar = $"snd_bgm_live_{songid}_preview_02";
|
|
UmaDatabaseEntry previewAwb = Main.AbSounds.FirstOrDefault(a => a.Name.Contains(nameVar) && a.Name.EndsWith("awb"));
|
|
if (previewAwb != null)
|
|
{
|
|
PlaySound(previewAwb, volume : 0.4f, loop : true);
|
|
}
|
|
}
|
|
|
|
public void PlaySound(UmaDatabaseEntry SongAwb, int subindex = -1, float volume = 1, bool loop = false)
|
|
{
|
|
CurrentLyrics.Clear();
|
|
if (CurrentAudioSources.Count > 0)
|
|
{
|
|
var tmp = CurrentAudioSources[0];
|
|
CurrentAudioSources.Clear();
|
|
Destroy(tmp.gameObject);
|
|
UI.AudioSettings.ResetPlayer();
|
|
}
|
|
if (subindex == -1)
|
|
{
|
|
foreach (AudioClip clip in LoadAudio(SongAwb))
|
|
{
|
|
AddAudioSource(clip, volume, loop);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddAudioSource(LoadAudio(SongAwb)[subindex], volume, loop);
|
|
}
|
|
|
|
}
|
|
|
|
public void SetLastAudio(UmaDatabaseEntry AudioAwb, int index)
|
|
{
|
|
ClearLiveSounds();
|
|
AddLiveSound(AudioAwb);
|
|
CurrentLiveSoundAWBIndex = index;
|
|
}
|
|
|
|
private void AddAudioSource(AudioClip clip, float volume = 1, bool loop = false)
|
|
{
|
|
AudioSource source;
|
|
if (CurrentAudioSources.Count > 0)
|
|
{
|
|
source = CurrentAudioSources[0].gameObject.AddComponent<AudioSource>();
|
|
}
|
|
else
|
|
{
|
|
source = new GameObject("SoundController").AddComponent<AudioSource>();
|
|
}
|
|
CurrentAudioSources.Add(source);
|
|
source.clip = clip;
|
|
source.volume = volume;
|
|
source.loop = loop;
|
|
source.Play();
|
|
}
|
|
|
|
|
|
|
|
public List<UmaWaveStream> LoadAudioStreams(UmaDatabaseEntry awb)
|
|
{
|
|
var streams = new List<UmaWaveStream>();
|
|
string awbPath = awb.FilePath;
|
|
if (!File.Exists(awbPath)) return streams;
|
|
|
|
FileStream awbFile = File.OpenRead(awbPath);
|
|
AwbReader awbReader = new AwbReader(awbFile);
|
|
|
|
foreach (Wave wave in awbReader.Waves)
|
|
{
|
|
var stream = new UmaWaveStream(awbReader, wave.WaveId);
|
|
streams.Add(stream);
|
|
}
|
|
|
|
|
|
return streams;
|
|
}
|
|
|
|
public static List<AudioClip> LoadAudio(UmaDatabaseEntry awb)
|
|
{
|
|
List<AudioClip> clips = new List<AudioClip>();
|
|
string awbPath = awb.FilePath;
|
|
if (!File.Exists(awbPath)) return clips;
|
|
|
|
FileStream awbFile = File.OpenRead(awbPath);
|
|
AwbReader awbReader = new AwbReader(awbFile);
|
|
|
|
foreach (Wave wave in awbReader.Waves)
|
|
{
|
|
var stream = new UmaWaveStream(awbReader, wave.WaveId);
|
|
var sampleProvider = stream.ToSampleProvider();
|
|
|
|
int channels = stream.WaveFormat.Channels;
|
|
int bytesPerSample = stream.WaveFormat.BitsPerSample / 8;
|
|
int sampleRate = stream.WaveFormat.SampleRate;
|
|
|
|
AudioClip clip = AudioClip.Create(
|
|
Path.GetFileNameWithoutExtension(awb.Name) + "_" + wave.WaveId.ToString(),
|
|
(int)(stream.Length / channels / bytesPerSample),
|
|
channels,
|
|
sampleRate,
|
|
true,
|
|
data => sampleProvider.Read(data, 0, data.Length),
|
|
position => stream.Position = position * channels * bytesPerSample);
|
|
|
|
clips.Add(clip);
|
|
}
|
|
|
|
return clips;
|
|
}
|
|
|
|
public List<UmaLyricsData> LoadLiveLyrics(int songid)
|
|
{
|
|
if (CurrentLyrics.Count > 0) CurrentLyrics.Clear();
|
|
|
|
string lyricsVar = $"live/musicscores/m{songid}/m{songid}_lyrics";
|
|
UmaDatabaseEntry lyricsAsset = Main.AbList[lyricsVar];
|
|
AssetBundle bundle = UmaAssetManager.LoadAssetBundle(lyricsAsset);
|
|
TextAsset asset = bundle.LoadAsset<TextAsset>(Path.GetFileNameWithoutExtension(lyricsVar));
|
|
string[] lines = asset.text.Split("\n"[0]);
|
|
|
|
for (int i = 1; i < lines.Length; i++)
|
|
{
|
|
string[] words = lines[i].Split(',');
|
|
if (words.Length > 0)
|
|
{
|
|
try
|
|
{
|
|
UmaLyricsData lyricsData = new UmaLyricsData()
|
|
{
|
|
time = float.Parse(words[0]) / 1000,
|
|
text = (words.Length > 1) ? words[1] : ""
|
|
};
|
|
CurrentLyrics.Add(lyricsData);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
return CurrentLyrics;
|
|
}
|
|
|
|
public void LoadAssetPath(string path, Transform SetParent)
|
|
{
|
|
var go = Instantiate(UmaViewerMain.Instance.AbList[path].Get<GameObject>(), SetParent);
|
|
Gallop.Live.LiveRuntimeDiagnostics.RecordInstantiation(path, go);
|
|
|
|
// 统计 missing script 数量并输出一次性汇总警告(避免刷屏)
|
|
int missingCount = Gallop.Live.StageController.CountMissingScripts(go);
|
|
if (missingCount > 0)
|
|
{
|
|
Debug.LogWarning($"[LoadAssetPath] '{path}' 实例化后有 {missingCount} 个 missing script 组件(来自原始游戏的不可用脚本)");
|
|
}
|
|
}
|
|
|
|
public void SetPreviewCamera(AnimationClip clip)
|
|
{
|
|
if (clip)
|
|
{
|
|
if (!AnimationCameraAnimator.runtimeAnimatorController)
|
|
{
|
|
AnimationCameraAnimator.runtimeAnimatorController = Instantiate(CameraOverrideController);
|
|
}
|
|
(AnimationCameraAnimator.runtimeAnimatorController as AnimatorOverrideController)["clip_1"] = clip;
|
|
AnimationCamera.enabled = true;
|
|
AnimationCameraAnimator.Play("motion_1", 0, 0);
|
|
CurrentUMAContainer?.SetHeight(0);
|
|
}
|
|
else
|
|
{
|
|
AnimationCamera.enabled = false;
|
|
CurrentUMAContainer?.SetHeight(-1);
|
|
}
|
|
}
|
|
|
|
public Sprite LoadCharaIcon(string id)
|
|
{
|
|
string value = $"chara/chr{id}/chr_icon_{id}";
|
|
if (UmaViewerMain.Instance.AbList.TryGetValue(value, out UmaDatabaseEntry entry))
|
|
{
|
|
AssetBundle assetBundle = UmaAssetManager.LoadAssetBundle(entry, true);
|
|
if (assetBundle.Contains($"chr_icon_{id}"))
|
|
{
|
|
Texture2D texture = (Texture2D)assetBundle.LoadAsset($"chr_icon_{id}");
|
|
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
|
|
UmaAssetManager.UnloadAssetBundle(entry, false);
|
|
return sprite;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public Sprite LoadMobCharaIcon(string id)
|
|
{
|
|
string value = $"mob/mob_chr_icon_{id}_000001_01";
|
|
if (UmaViewerMain.Instance.AbList.TryGetValue(value, out UmaDatabaseEntry entry))
|
|
{
|
|
string path = entry.FilePath;
|
|
AssetBundle assetBundle = UmaAssetManager.LoadAssetBundle(entry, true);
|
|
if (assetBundle.Contains($"mob_chr_icon_{id}_000001_01"))
|
|
{
|
|
Texture2D texture = (Texture2D)assetBundle.LoadAsset($"mob_chr_icon_{id}_000001_01");
|
|
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
|
|
UmaAssetManager.UnloadAssetBundle(entry, false);
|
|
return sprite;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public Sprite LoadSprite(UmaDatabaseEntry item)
|
|
{
|
|
AssetBundle assetBundle = UmaAssetManager.LoadAssetBundle(item, true);
|
|
Texture2D texture = (Texture2D)assetBundle.LoadAsset(assetBundle.GetAllAssetNames()[0]);
|
|
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
|
|
UmaAssetManager.UnloadAssetBundle(item, false);
|
|
return sprite;
|
|
}
|
|
|
|
public Sprite LoadLiveIcon(int musicid)
|
|
{
|
|
string value = $"live/jacket/jacket_icon_l_{musicid}";
|
|
|
|
if (UmaViewerMain.Instance.AbList.TryGetValue(value, out UmaDatabaseEntry entry))
|
|
{
|
|
AssetBundle assetBundle = UmaAssetManager.LoadAssetBundle(entry, true);
|
|
if (assetBundle.Contains($"jacket_icon_l_{musicid}"))
|
|
{
|
|
Texture2D texture = (Texture2D)assetBundle.LoadAsset($"jacket_icon_l_{musicid}");
|
|
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
|
|
UmaAssetManager.UnloadAssetBundle(entry, false);
|
|
return sprite;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void UnloadProp()
|
|
{
|
|
if (CurrentOtherContainer != null)
|
|
{
|
|
Destroy(CurrentOtherContainer.gameObject);
|
|
}
|
|
}
|
|
|
|
public void UnloadUma()
|
|
{
|
|
PoseManager.SetPoseModeStatic(false);
|
|
|
|
if (CurrentUMAContainer != null)
|
|
{
|
|
//It seems that OnDestroy will executed after new model loaded, which cause new FacialPanels empty...
|
|
UI.currentFaceDrivenKeyTarget = null;
|
|
UI.LoadEmotionPanels(null);
|
|
UI.LoadFacialPanels(null);
|
|
if (UI.ModelSettings.MaterialsList)
|
|
foreach (Transform t in UI.ModelSettings.MaterialsList.content)
|
|
{
|
|
Destroy(t.gameObject);
|
|
}
|
|
|
|
if (CurrentUMAContainer.transform.parent && CurrentUMAContainer.transform.parent.name.Contains("Root"))
|
|
{
|
|
Destroy(CurrentUMAContainer.transform.parent.gameObject);
|
|
}
|
|
else
|
|
{
|
|
Destroy(CurrentUMAContainer.gameObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearMorphs()
|
|
{
|
|
var umaContainer = CurrentUMAContainer;
|
|
if (umaContainer != null && umaContainer.FaceDrivenKeyTarget != null)
|
|
{
|
|
foreach (var container in UI.EmotionList.GetComponentsInChildren<UmaUIContainer>())
|
|
{
|
|
if (container.Slider != null)
|
|
container.Slider.value = 0;
|
|
}
|
|
foreach (var container in UI.FacialList.GetComponentsInChildren<UmaUIContainer>())
|
|
{
|
|
if (container.Slider != null)
|
|
container.Slider.SetValueWithoutNotify(0);
|
|
}
|
|
if (umaContainer.FaceDrivenKeyTarget)
|
|
{
|
|
umaContainer.FaceDrivenKeyTarget.ClearAllWeights();
|
|
umaContainer.FaceDrivenKeyTarget.ChangeMorph();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UnloadAllBundle() => UmaAssetManager.UnloadAllBundle(true);
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
if (AnimationCamera && AnimationCamera.enabled == true)
|
|
{
|
|
var fov = AnimationCamera.gameObject.transform.parent.transform.localScale.x;
|
|
AnimationCamera.fieldOfView = fov;
|
|
}
|
|
}
|
|
|
|
private void AddLiveSound(UmaDatabaseEntry entry)
|
|
{
|
|
UmaAssetManager.LoadAssetBundle(entry, false, false);
|
|
CurrentLiveSoundAWB.Add(entry);
|
|
}
|
|
|
|
private void ClearLiveSounds()
|
|
{
|
|
foreach (var liveSound in CurrentLiveSoundAWB)
|
|
{
|
|
UmaAssetManager.UnloadAssetBundle(liveSound, true);
|
|
}
|
|
CurrentLiveSoundAWB.Clear();
|
|
}
|
|
private bool _dumpedNormalRenderers = false;
|
|
|
|
private void ApplyNormalCostumeVisibilityOptions(UmaContainerCharacter uma)
|
|
{
|
|
if (uma == null) return;
|
|
|
|
if (DumpNormalRenderersOnce && !_dumpedNormalRenderers)
|
|
{
|
|
DumpBodyRenderersAndMaterials(uma);
|
|
_dumpedNormalRenderers = true;
|
|
}
|
|
|
|
switch (NormalHideMode)
|
|
{
|
|
case NormalCostumeHideMode.HideAllBodyRenderers:
|
|
HideAllBodyRenderers(uma);
|
|
break;
|
|
case NormalCostumeHideMode.HideByNameOrMaterial:
|
|
HideCostumeRenderersByNameOrMaterial(uma);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HideAllBodyRenderers(UmaContainerCharacter uma)
|
|
{
|
|
// Clothes are typically baked into the body prefab, so hiding Body renderers hides "costume" too.
|
|
var body = uma.Body;
|
|
if (!body) return;
|
|
|
|
foreach (var r in body.GetComponentsInChildren<Renderer>(true))
|
|
r.enabled = false;
|
|
}
|
|
|
|
private void HideCostumeRenderersByNameOrMaterial(UmaContainerCharacter uma)
|
|
{
|
|
// Conservative: hide only renderers that look like clothes, and avoid anything that looks like skin/body/face/hair.
|
|
// Tune NormalClothKeywords / NormalBodyKeywords after checking Dump logs.
|
|
var root = uma.gameObject;
|
|
if (!root) return;
|
|
|
|
foreach (var r in root.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
|
{
|
|
if (r == null) continue;
|
|
|
|
bool looksCloth = MatchesAny(r.name, NormalClothKeywords) || MaterialsMatchAny(r.sharedMaterials, NormalClothKeywords);
|
|
if (!looksCloth) continue;
|
|
|
|
bool looksBody = MatchesAny(r.name, NormalBodyKeywords) || MaterialsMatchAny(r.sharedMaterials, NormalBodyKeywords);
|
|
if (looksBody) continue;
|
|
|
|
r.enabled = false;
|
|
}
|
|
}
|
|
|
|
private void DumpBodyRenderersAndMaterials(UmaContainerCharacter uma)
|
|
{
|
|
if (uma == null) return;
|
|
var body = uma.Body;
|
|
if (!body)
|
|
{
|
|
Debug.LogWarning("[UmaViewerBuilder] DumpNormalRenderersOnce: Body is null.");
|
|
return;
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
var renderers = body.GetComponentsInChildren<Renderer>(true);
|
|
sb.AppendLine($"[UmaViewerBuilder] Body renderer dump: {renderers.Length} renderers under {body.name}");
|
|
|
|
foreach (var r in renderers)
|
|
{
|
|
sb.Append($"- {r.GetType().Name}: {r.name} mats=[");
|
|
var mats = r.sharedMaterials;
|
|
if (mats != null)
|
|
{
|
|
for (int i = 0; i < mats.Length; i++)
|
|
{
|
|
var m = mats[i];
|
|
sb.Append(m ? m.name : "null");
|
|
if (i < mats.Length - 1) sb.Append(", ");
|
|
}
|
|
}
|
|
sb.AppendLine("]");
|
|
}
|
|
|
|
Debug.Log(sb.ToString());
|
|
}
|
|
|
|
private static bool MatchesAny(string value, string[] keywords)
|
|
{
|
|
if (string.IsNullOrEmpty(value) || keywords == null) return false;
|
|
var v = value.ToLowerInvariant();
|
|
for (int i = 0; i < keywords.Length; i++)
|
|
{
|
|
var k = keywords[i];
|
|
if (string.IsNullOrEmpty(k)) continue;
|
|
if (v.Contains(k.ToLowerInvariant())) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool MaterialsMatchAny(Material[] materials, string[] keywords)
|
|
{
|
|
if (materials == null || keywords == null) return false;
|
|
for (int i = 0; i < materials.Length; i++)
|
|
{
|
|
var m = materials[i];
|
|
if (!m) continue;
|
|
if (MatchesAny(m.name, keywords)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|