Skip to content

Commit

Permalink
Support multi-camera songs (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
hozuki committed Sep 23, 2020
1 parent cc0eb1f commit a4dc89b
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 190 deletions.
22 changes: 0 additions & 22 deletions src/MillionDance/Core/IO/ResourceLoader.LoadedDance.cs

This file was deleted.

170 changes: 120 additions & 50 deletions src/MillionDance/Core/IO/ResourceLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
using OpenMLTD.MillionDance.Entities.Mltd;

namespace OpenMLTD.MillionDance.Core.IO {
internal static partial class ResourceLoader {
internal static class ResourceLoader {

[NotNull]
public static TransformHierarchies LoadTransformHierarchies([NotNull] string filePath) {
Expand Down Expand Up @@ -143,58 +143,40 @@ public static AvatarWrapper LoadHeadAvatar([NotNull] string filePath) {
}

[NotNull]
public static AnimationSet<CharacterImasMotionAsset> LoadCamera([NotNull] string filePath) {
public static CameraAnimationSet<CharacterImasMotionAsset> LoadCamera([NotNull] string filePath, [CanBeNull] int? desiredCameraNumber) {
CharacterImasMotionAsset cam = null, apa = null, apg = null, bpg = null;

const string defCamEnds = "_cam.imo";
const string apaCamEnds = "_apa.imo";
const string apgCamEnds = "_apg.imo";
const string bpgCamEnds = "_bpg.imo";
const string apaPortraitCamEnds = "_tate_apa.imo";
const string apgPortraitCamEnds = "_tate_apg.imo";
const string bpgPortraitCamEnds = "_tate_bpg.imo"; // should not exist

var manager = new AssetsManager();
manager.LoadFiles(filePath);

var ser = new ScriptableObjectSerializer();

foreach (var assetFile in manager.assetsFileList) {
foreach (var obj in assetFile.Objects) {
if (obj.type != ClassIDType.MonoBehaviour) {
continue;
}

var behaviour = obj as MonoBehaviour;

if (behaviour == null) {
throw new ArgumentException("An object serialized as MonoBehaviour is actually not a MonoBehaviour.");
}

if (behaviour.m_Name.EndsWith(defCamEnds)) {
cam = ser.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(apaCamEnds) && !behaviour.m_Name.EndsWith(apaPortraitCamEnds)) {
apa = ser.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(apgCamEnds) && !behaviour.m_Name.EndsWith(apgPortraitCamEnds)) {
apg = ser.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(bpgCamEnds) && !behaviour.m_Name.EndsWith(bpgPortraitCamEnds)) {
bpg = ser.Deserialize<CharacterImasMotionAsset>(behaviour);
}

if (cam != null && apa != null && apg != null && bpg != null) {
int cameraNumber;

if (desiredCameraNumber != null) {
do {
// Some songs, e.g. Alliance Stardust (alstar), has more than one cameras.
// In these cases the user must select the appropriate camera number.
if (TryLoadSpecifiedCamera(manager, ser, desiredCameraNumber.Value, ref cam, ref apa, ref apg, ref bpg)) {
cameraNumber = desiredCameraNumber.Value;
break;
}
}

TryLoadFirstFoundCamera(manager, ser, ref cam, ref apa, ref apg, ref bpg);
cameraNumber = InvalidCameraNumber;
} while (false);
} else {
TryLoadFirstFoundCamera(manager, ser, ref cam, ref apa, ref apg, ref bpg);
cameraNumber = UnspecifiedCameraNumber;
}

return AnimationSet.Create(cam, apg, apa, bpg);
return AnimationSet.CreateCamera(cameraNumber, cam, apg, apa, bpg);
}

[NotNull]
public static LoadedDance LoadDance([NotNull] string filePath, int motionNumber, int formationNumber) {
public static DanceAnimationSet<IBodyAnimationSource> LoadDance([NotNull] string filePath, int motionNumber, int formationNumber) {
IBodyAnimationSource defaultSource = null, anotherSource = null, specialSource = null, gorgeousSource = null;
bool anyAnimationLoaded;
var suggestedPosition = InvalidDancePosition;
int suggestedPosition;

// About number of main dance animations and special/another appeal animations:
// Most songs have 1 dance animation (i.e. animation for 1 idol, multiplied by 3/4/5 etc.) and 1 appeal animation
Expand All @@ -206,8 +188,9 @@ public static LoadedDance LoadDance([NotNull] string filePath, int motionNumber,

{
// First try with legacy bundles
var loaded = LoadDanceLegacy(filePath, motionNumber, formationNumber, out suggestedPosition);
var loaded = LoadDanceLegacy(filePath, motionNumber, formationNumber);

suggestedPosition = loaded.SuggestedPosition;
anyAnimationLoaded = loaded.Default != null || loaded.Special != null || loaded.Another != null || loaded.Gorgeous != null;

if (loaded.Default != null) {
Expand All @@ -229,7 +212,9 @@ public static LoadedDance LoadDance([NotNull] string filePath, int motionNumber,

if (!anyAnimationLoaded) {
// If failed, try the new one (from ~mid 2018?)
var loaded = LoadDanceCompiled(filePath, motionNumber, formationNumber, out suggestedPosition);
var loaded = LoadDanceCompiled(filePath, motionNumber, formationNumber);

suggestedPosition = loaded.SuggestedPosition;

if (loaded.Default != null) {
defaultSource = new CompiledBodyAnimationSource(loaded.Default);
Expand All @@ -248,9 +233,9 @@ public static LoadedDance LoadDance([NotNull] string filePath, int motionNumber,
}
}

var animationSet = AnimationSet.Create(defaultSource, specialSource, anotherSource, gorgeousSource);
var animationSet = AnimationSet.CreateDance(suggestedPosition, defaultSource, specialSource, anotherSource, gorgeousSource);

return new LoadedDance(animationSet, suggestedPosition);
return animationSet;
}

public static (ScenarioObject, ScenarioObject, ScenarioObject) LoadScenario([NotNull] string filePath) {
Expand Down Expand Up @@ -295,7 +280,7 @@ public static (ScenarioObject, ScenarioObject, ScenarioObject) LoadScenario([Not
}

[NotNull]
private static AnimationSet<CharacterImasMotionAsset> LoadDanceLegacy([NotNull] string filePath, int motionNumber, int formationNumber, out int suggestedPosition) {
private static DanceAnimationSet<CharacterImasMotionAsset> LoadDanceLegacy([NotNull] string filePath, int motionNumber, int formationNumber) {
CharacterImasMotionAsset dan = null, apa = null, apg = null, bpg = null;

var danComp = $"{motionNumber:00}_dan.imo";
Expand Down Expand Up @@ -337,13 +322,13 @@ private static AnimationSet<CharacterImasMotionAsset> LoadDanceLegacy([NotNull]
}
}

suggestedPosition = GetSuggestedDancePosition(manager);
var suggestedPosition = GetSuggestedDancePosition(manager);

return AnimationSet.Create(dan, apg, apa, bpg);
return AnimationSet.CreateDance(suggestedPosition, dan, apg, apa, bpg);
}

[NotNull]
private static AnimationSet<AnimationClip> LoadDanceCompiled([NotNull] string filePath, int motionNumber, int formationNumber, out int suggestedPosition) {
private static DanceAnimationSet<AnimationClip> LoadDanceCompiled([NotNull] string filePath, int motionNumber, int formationNumber) {
AnimationClip dan = null, apa = null, apg = null, bpg = null;

var danComp = $"{motionNumber:00}_dan";
Expand Down Expand Up @@ -382,9 +367,90 @@ private static AnimationSet<AnimationClip> LoadDanceCompiled([NotNull] string fi
}
}

suggestedPosition = GetSuggestedDancePosition(manager);
var suggestedPosition = GetSuggestedDancePosition(manager);

return AnimationSet.CreateDance(suggestedPosition, dan, apg, apa, bpg);
}

private static bool TryLoadSpecifiedCamera([NotNull] AssetsManager manager, [NotNull] ScriptableObjectSerializer serializer, int cameraNumber, [CanBeNull] ref CharacterImasMotionAsset cam, [CanBeNull] ref CharacterImasMotionAsset apa, [CanBeNull] ref CharacterImasMotionAsset apg, [CanBeNull] ref CharacterImasMotionAsset bpg) {
var camNumStr = cameraNumber.ToString("00");
var defCamEnds = $"_{camNumStr}_cam.imo";
var apaCamEnds = $"_{camNumStr}_apa.imo";
var apgCamEnds = $"_{camNumStr}_apg.imo";
var bpgCamEnds = $"_{camNumStr}_bpg.imo";
// var apaPortraitCamEnds = $"_{camNumStr}_tate_apa.imo";
// var apgPortraitCamEnds = $"_{camNumStr}_tate_apg.imo";
// var bpgPortraitCamEnds = $"_{camNumStr}_tate_bpg.imo"; // should not exist

foreach (var assetFile in manager.assetsFileList) {
foreach (var obj in assetFile.Objects) {
if (obj.type != ClassIDType.MonoBehaviour) {
continue;
}

var behaviour = obj as MonoBehaviour;

if (behaviour == null) {
throw new ArgumentException("An object serialized as MonoBehaviour is actually not a MonoBehaviour.");
}

if (behaviour.m_Name.EndsWith(defCamEnds)) {
cam = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(apaCamEnds)) {
apa = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(apgCamEnds)) {
apg = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(bpgCamEnds)) {
bpg = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
}

if (cam != null && apa != null && apg != null && bpg != null) {
break;
}
}
}

return cam != null && apa != null && apg != null && bpg != null;
}

private static bool TryLoadFirstFoundCamera([NotNull] AssetsManager manager, [NotNull] ScriptableObjectSerializer serializer, [CanBeNull] ref CharacterImasMotionAsset cam, [CanBeNull] ref CharacterImasMotionAsset apa, [CanBeNull] ref CharacterImasMotionAsset apg, [CanBeNull] ref CharacterImasMotionAsset bpg) {
const string defCamEnds = "_cam.imo";
const string apaCamEnds = "_apa.imo";
const string apgCamEnds = "_apg.imo";
const string bpgCamEnds = "_bpg.imo";
const string apaPortraitCamEnds = "_tate_apa.imo";
const string apgPortraitCamEnds = "_tate_apg.imo";
const string bpgPortraitCamEnds = "_tate_bpg.imo"; // should not exist

foreach (var assetFile in manager.assetsFileList) {
foreach (var obj in assetFile.Objects) {
if (obj.type != ClassIDType.MonoBehaviour) {
continue;
}

var behaviour = obj as MonoBehaviour;

if (behaviour == null) {
throw new ArgumentException("An object serialized as MonoBehaviour is actually not a MonoBehaviour.");
}

if (behaviour.m_Name.EndsWith(defCamEnds)) {
cam = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(apaCamEnds) && !behaviour.m_Name.EndsWith(apaPortraitCamEnds)) {
apa = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(apgCamEnds) && !behaviour.m_Name.EndsWith(apgPortraitCamEnds)) {
apg = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
} else if (behaviour.m_Name.EndsWith(bpgCamEnds) && !behaviour.m_Name.EndsWith(bpgPortraitCamEnds)) {
bpg = serializer.Deserialize<CharacterImasMotionAsset>(behaviour);
}

if (cam != null && apa != null && apg != null && bpg != null) {
break;
}
}
}

return AnimationSet.Create(dan, apg, apa, bpg);
return cam != null && apa != null && apg != null && bpg != null;
}

public static (SwayController Body, SwayController Head) LoadSwayControllers([NotNull] string bodyFilePath, [NotNull] string headFilePath) {
Expand All @@ -405,6 +471,12 @@ public static (SwayController Body, SwayController Head) LoadSwayControllers([No
return (Body: body, Head: head);
}

internal const int InvalidDancePosition = -1;

internal const int UnspecifiedCameraNumber = 0;

internal const int InvalidCameraNumber = -1;

[CanBeNull]
private static SwayController LoadSwayController([NotNull] string filePath) {
SwayController result = null;
Expand Down Expand Up @@ -474,7 +546,5 @@ private static int GetSuggestedDancePosition([NotNull] AssetsManager manager) {
[NotNull]
private static readonly Regex DanceAssetVaguePattern = new Regex(@"(?<position>\d{2})_dan", RegexOptions.CultureInvariant);

private const int InvalidDancePosition = -1;

}
}
6 changes: 4 additions & 2 deletions src/MillionDance/Core/MltdAnimation.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
namespace OpenMLTD.MillionDance.Core {
internal static class MltdAnimation {

public const int MinMotion = 1;
public const int MinDance = 1;

public const int MaxMotion = 13;
public const int MaxDance = 13;

public const int MinFormation = 1;

public const int MaxFormation = 39;

public const int MinCamera = 1;

}
}
9 changes: 7 additions & 2 deletions src/MillionDance/Entities/Internal/AnimationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ namespace OpenMLTD.MillionDance.Entities.Internal {
internal static class AnimationSet {

[NotNull]
public static AnimationSet<T> Create<T>([CanBeNull] T @default, [CanBeNull] T special, [CanBeNull] T another, [CanBeNull] T gorgeous) {
return new AnimationSet<T>(@default, another, special, gorgeous);
public static CameraAnimationSet<T> CreateCamera<T>(int cameraNumber, [CanBeNull] T @default, [CanBeNull] T special, [CanBeNull] T another, [CanBeNull] T gorgeous) {
return new CameraAnimationSet<T>(cameraNumber, @default, another, special, gorgeous);
}

[NotNull]
public static DanceAnimationSet<T> CreateDance<T>(int suggestedPosition, [CanBeNull] T @default, [CanBeNull] T special, [CanBeNull] T another, [CanBeNull] T gorgeous) {
return new DanceAnimationSet<T>(suggestedPosition, @default, another, special, gorgeous);
}

}
Expand Down
4 changes: 2 additions & 2 deletions src/MillionDance/Entities/Internal/AnimationSet`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace OpenMLTD.MillionDance.Entities.Internal {
/// A set of animations
/// </summary>
/// <typeparam name="T">The type containing animation data</typeparam>
internal sealed class AnimationSet<T> {
internal class AnimationSet<T> {

public AnimationSet([CanBeNull] T @default, [CanBeNull] T another, [CanBeNull] T special, [CanBeNull] T gorgeous) {
private protected AnimationSet([CanBeNull] T @default, [CanBeNull] T another, [CanBeNull] T special, [CanBeNull] T gorgeous) {
Default = @default;
Special = special;
Another = another;
Expand Down
23 changes: 23 additions & 0 deletions src/MillionDance/Entities/Internal/CameraAnimationSet`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using JetBrains.Annotations;
using OpenMLTD.MillionDance.Core;
using OpenMLTD.MillionDance.Core.IO;

namespace OpenMLTD.MillionDance.Entities.Internal {
internal sealed class CameraAnimationSet<T> : AnimationSet<T> {

internal CameraAnimationSet(int cameraNumber, [CanBeNull] T @default, [CanBeNull] T another, [CanBeNull] T special, [CanBeNull] T gorgeous)
: base(@default, another, special, gorgeous) {
CameraNumber = cameraNumber;
}

/// <summary>
/// Specified camera number (starting from 1). Can be <see cref="ResourceLoader.UnspecifiedCameraNumber"/> if not specified, or
/// <see cref="ResourceLoader.InvalidCameraNumber"/> if specified camera is not found.
/// </summary>
/// <remarks>
/// It can be checked against <see cref="MltdAnimation.MinCamera"/>.
/// </remarks>
public int CameraNumber { get; }

}
}
22 changes: 22 additions & 0 deletions src/MillionDance/Entities/Internal/DanceAnimationSet`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using JetBrains.Annotations;
using OpenMLTD.MillionDance.Core;
using OpenMLTD.MillionDance.Core.IO;

namespace OpenMLTD.MillionDance.Entities.Internal {
internal sealed class DanceAnimationSet<T> : AnimationSet<T> {

internal DanceAnimationSet(int suggestedPosition, [CanBeNull] T @default, [CanBeNull] T another, [CanBeNull] T special, [CanBeNull] T gorgeous)
: base(@default, another, special, gorgeous) {
SuggestedPosition = suggestedPosition;
}

/// <summary>
/// Suggested dance position. Can be <see cref="ResourceLoader.InvalidDancePosition"/> if not set.
/// </summary>
/// <remarks>
/// It can be checked against <see cref="MltdAnimation.MinDance"/> and <see cref="MltdAnimation.MaxDance"/>.
/// </remarks>
public int SuggestedPosition { get; }

}
}
Loading

0 comments on commit a4dc89b

Please sign in to comment.