diff --git a/DCGEngine.sln b/DCGEngine.sln index e44f59e..06ec634 100644 --- a/DCGEngine.sln +++ b/DCGEngine.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleNode", "SVSim.B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine", "SVSim.BattleEngine\SVSim.BattleEngine.csproj", "{CCE23D9D-6A66-456B-9812-F09B1FDA3C81}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.BattleEngine.Tests", "SVSim.BattleEngine.Tests\SVSim.BattleEngine.Tests.csproj", "{68F3F596-CAD5-4326-8779-AD8C7BD20CDA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,5 +45,9 @@ Global {CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCE23D9D-6A66-456B-9812-F09B1FDA3C81}.Release|Any CPU.Build.0 = Release|Any CPU + {68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68F3F596-CAD5-4326-8779-AD8C7BD20CDA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SVSim.BattleEngine.Tests/ConstructionProbeTests.cs b/SVSim.BattleEngine.Tests/ConstructionProbeTests.cs new file mode 100644 index 0000000..2ca54b3 --- /dev/null +++ b/SVSim.BattleEngine.Tests/ConstructionProbeTests.cs @@ -0,0 +1,42 @@ +using System; +using NUnit.Framework; + +namespace SVSim.BattleEngine.Tests +{ + // M2 probe (go/no-go step 1): can BattleManagerBase / the two-player pair be constructed + // HEADLESS at all? This drives the real practice init path + // (`new SingleBattleMgr(StandardBattleMgrContentsCreator)`), which internally builds the + // BattlePlayer + BattleEnemy pair, against the M1 shim — with NO Unity runtime. + // + // The point of this test is diagnostic: if construction throws, the stack trace tells us the + // first shim gap on the *resolution* path (vs the compile path M1 already proved). We assert + // success, but a failure here is the informative outcome we want surfaced. + [TestFixture] + public class ConstructionProbeTests + { + [Test] + public void SingleBattleMgr_constructs_headless() + { + // Mirror the forecast flags the design pins (DP4 / §3): suppress VFX registration and + // collapse wait delays. Set before construction so any ctor-time VFX path no-ops. + HeadlessEngineEnv.EnsureInitialized(); + BattleManagerBase.IsForecast = true; + + SingleBattleMgr mgr = null; + try + { + mgr = new SingleBattleMgr(new HeadlessContentsCreator()); + } + catch (Exception ex) + { + Assert.Fail( + "Headless construction of SingleBattleMgr threw — first shim gap on the " + + "resolution path:\n" + ex); + } + + Assert.That(mgr, Is.Not.Null); + Assert.That(mgr.BattlePlayer, Is.Not.Null, "BattlePlayer (self) not created"); + Assert.That(mgr.BattleEnemy, Is.Not.Null, "BattleEnemy (opponent) not created"); + } + } +} diff --git a/SVSim.BattleEngine.Tests/HeadlessCardMaster.cs b/SVSim.BattleEngine.Tests/HeadlessCardMaster.cs new file mode 100644 index 0000000..63a6aa8 --- /dev/null +++ b/SVSim.BattleEngine.Tests/HeadlessCardMaster.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json; +using Wizard; + +namespace SVSim.BattleEngine.Tests +{ + // Populates the engine's static CardMaster headless, from the loader's cards.json dump + // (serialized CardCSVData objects). We bypass the network/Resources init path + // (CardMaster.InitializeCardMaster) and the private ctor/field via reflection — CardMaster + // exposes no public injection seam. Class cards (id < 100) resolve via the ctor's + // _classCardParam, so an empty load still satisfies construction; pass real ids for the oracle. + public static class HeadlessCardMaster + { + private static readonly string CardsJsonPath = + Path.Combine(AppContext.BaseDirectory, "Data", "cards.json"); + + // Load the given card ids (empty = none) into a fresh CardMaster registered as Default. + public static void Load(params int[] cardIds) + { + var want = new HashSet(cardIds); + var rows = new List(); + if (want.Count > 0) + { + using var doc = JsonDocument.Parse(File.ReadAllText(CardsJsonPath)); + int sort = 0; + foreach (var el in doc.RootElement.EnumerateArray()) + { + if (!el.TryGetProperty("card_id", out var idEl)) continue; + if (!int.TryParse(idEl.GetString(), out var id) || !want.Contains(id)) continue; + rows.Add(BuildCardCsvData(el, sort++)); + } + var missing = want.Except(rows.Select(r => int.Parse(r.card_id))).ToArray(); + if (missing.Length > 0) + throw new InvalidOperationException( + "cards.json missing requested ids: " + string.Join(",", missing)); + } + + var cm = NewCardMaster(rows); + InjectAsDefault(cm); + } + + // Construct a CardCSVData without running its CSV ctor; set each member from the JSON object + // by exact name match (cards.json keys == CardCSVData member names). + private static CardCSVData BuildCardCsvData(JsonElement el, int sortIndex) + { + var c = (CardCSVData)FormatterServices.GetUninitializedObject(typeof(CardCSVData)); + const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + foreach (var prop in el.EnumerateObject()) + { + string val = prop.Value.ValueKind == JsonValueKind.Null ? null : prop.Value.ToString(); + var f = typeof(CardCSVData).GetField(prop.Name, bf); + if (f != null) { SetMember(f.FieldType, val, v => f.SetValue(c, v)); continue; } + var p = typeof(CardCSVData).GetProperty(prop.Name, bf); + if (p != null && p.CanWrite) SetMember(p.PropertyType, val, v => p.SetValue(c, v)); + } + // SortIndex is normally set by the ctor; mirror it. + var si = typeof(CardCSVData).GetProperty("SortIndex", bf); + if (si != null && si.CanWrite) si.SetValue(c, sortIndex); + return c; + } + + private static void SetMember(Type t, string val, Action set) + { + if (t == typeof(string)) set(val); + else if (t == typeof(int)) set(int.TryParse(val, out var i) ? i : 0); + else if (t == typeof(bool)) set(val == "1" || string.Equals(val, "true", StringComparison.OrdinalIgnoreCase)); + // other types left at default + } + + private static CardMaster NewCardMaster(List rows) + { + var ctor = typeof(CardMaster).GetConstructor( + BindingFlags.Instance | BindingFlags.NonPublic, null, + new[] { typeof(List) }, null); + if (ctor == null) throw new InvalidOperationException("CardMaster(List) ctor not found"); + return (CardMaster)ctor.Invoke(new object[] { rows }); + } + + private static void InjectAsDefault(CardMaster cm) + { + var idType = typeof(CardMaster).GetNestedType("CardMasterId"); + var defaultId = Enum.Parse(idType, "Default"); + var dictType = typeof(Dictionary<,>).MakeGenericType(idType, typeof(CardMaster)); + var dict = (System.Collections.IDictionary)Activator.CreateInstance(dictType); + dict[defaultId] = cm; + var fld = typeof(CardMaster).GetField("_dictCardMaster", + BindingFlags.Static | BindingFlags.NonPublic); + fld.SetValue(null, dict); + } + } +} diff --git a/SVSim.BattleEngine.Tests/HeadlessFixture.cs b/SVSim.BattleEngine.Tests/HeadlessFixture.cs new file mode 100644 index 0000000..bb74487 --- /dev/null +++ b/SVSim.BattleEngine.Tests/HeadlessFixture.cs @@ -0,0 +1,77 @@ +using Wizard.Battle.Phase; +using Wizard.Battle.Recovery; +using Wizard.Battle.Replay; +using Wizard.Battle.Resource; +using Wizard.Battle.View.Vfx; +using Wizard.BattleMgr; + +namespace SVSim.BattleEngine.Tests +{ + // Initializes the global engine state a headless battle assumes exists. In the real client this + // is populated from /load/index at login; here we author the minimum the resolution path reads. + public static class HeadlessEngineEnv + { + private static bool _done; + + public static void EnsureInitialized() + { + if (_done) return; + // Wizard.Data.Load: static /load/index snapshot. The ctor's CreateBackgroundId reads + // Data.Load.data._userTutorial (LoadDetail self-inits _userTutorial). Suppress VFX too. + Wizard.Data.Load = new Load { data = new LoadDetail() }; + BattleManagerBase.IsForecast = true; + // CardMaster must be non-null before construction (the leader/class card looks up id 0). + // Empty load suffices for construction; the oracle reloads with the follower's real id. + HeadlessCardMaster.Load(); + // Master reference data (class-character list) for leader/class card resolution. + HeadlessMasterData.Install(); + // Player/enemy leaders (chara ids must map to a ClassCharacterMasterData in Master). + // Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/ + // AvatarBattle info (more null statics) which the resolution path doesn't need (the + // TryGet* accessors are null-tolerant). + var dm = GameMgr.GetIns().GetDataMgr(); + SetField(dm, "_playerCharaId", HeadlessMasterData.PlayerCharaId); + SetField(dm, "_enemyCharaId", HeadlessMasterData.EnemyCharaId); + _done = true; + } + + private static void SetField(object obj, string name, object value) + { + var f = obj.GetType().GetField(name, + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Public); + if (f == null) throw new System.InvalidOperationException( + $"{obj.GetType().Name} has no field '{name}'"); + f.SetValue(obj, value); + } + } + + // Test-side replica of the engine's own StandardBattleMgrContentsCreator (the practice/solo + // init path: GameMgr.cs:244 `new SingleBattleMgr(new StandardBattleMgrContentsCreator(null, null))`). + // Authored here (not copied) so we control the seed deterministically; uses the real engine + // managers verbatim. The real StandardBattleMgrContentsCreator + SingleBattlePhaseCreator were + // cut from the M1 copy set (entry-point constructors), so we reproduce them minimally. + public sealed class HeadlessContentsCreator : IBattleMgrContentsCreator + { + public int RandomSeed => 12345; // fixed; vanilla follower has no RNG so value is irrelevant + + // No-op managers (vs the practice path's file-backed SingleBattleRecoveryRecordManager): + // the ctor's FirstRecoverySetting/FirstReplaySetting dereference these, and recovery/replay + // recording is irrelevant to the M2 oracle, so use the engine's own null implementations. + public IRecoveryManager RecoveryManager { get; } = new NullRecoveryManager(); + public IRecoveryRecordManager RecoveryRecordManager { get; } = new NullRecoveryRecordManager(); + public IReplayRecordManager ReplayRecordManager { get; } = new NullReplayRecordManager(); + + public IBattleResourceMgr CreateResourceMgr() => new BattleResourceMgr(); + public VfxMgr CreateVfxMgr() => new VfxMgr(); + public IPhaseCreator CreatePhaseCreator(BattleManagerBase battleMgr) => + new HeadlessPhaseCreator(battleMgr); + } + + // Equivalent of the engine's SingleBattlePhaseCreator: inherits PhaseCreatorBase wholesale. + public sealed class HeadlessPhaseCreator : PhaseCreatorBase + { + public HeadlessPhaseCreator(BattleManagerBase battleMgr) : base(battleMgr) { } + } +} diff --git a/SVSim.BattleEngine.Tests/HeadlessMasterData.cs b/SVSim.BattleEngine.Tests/HeadlessMasterData.cs new file mode 100644 index 0000000..9fff543 --- /dev/null +++ b/SVSim.BattleEngine.Tests/HeadlessMasterData.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using Wizard; + +namespace SVSim.BattleEngine.Tests +{ + // Builds the minimal Data.Master reference context a headless battle reads. In the client this + // comes from the /load/index master section; here we author just enough for the resolution path + // (currently: ClassCharacterList, so the leader/class card can resolve player/enemy class_id). + // Entries are constructed without their CSV ctor (private setters set via reflection). + public static class HeadlessMasterData + { + public const int PlayerCharaId = 1; + public const int EnemyCharaId = 2; + public const int PlayerClassId = 1; // ClanType -> class card clan + public const int EnemyClassId = 2; + + public static void Install() + { + var master = (Master)FormatterServices.GetUninitializedObject(typeof(Master)); + // The resolution path reads many Master.* collections (e.g. WhenPlayEffectKeywordMaster) + // and calls LINQ on them unguarded. Default every collection member to an empty instance + // so those touches no-op instead of NRE; then override the ones we need with content. + EnsureEmptyCollections(master); + var list = new List + { + NewChara(PlayerCharaId, PlayerClassId), + NewChara(EnemyCharaId, EnemyClassId), + }; + SetMember(master, "ClassCharacterList", list); + Data.Master = master; + } + + // Initialize every List<>/array/Dictionary<> field/auto-property on the object to an empty + // non-null instance (only if currently null). + private static void EnsureEmptyCollections(object obj) + { + const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + foreach (var f in obj.GetType().GetFields(bf)) + { + if (f.GetValue(obj) != null) continue; + var empty = EmptyOf(f.FieldType); + if (empty != null) f.SetValue(obj, empty); + } + } + + private static object EmptyOf(Type t) + { + if (t.IsArray) return Array.CreateInstance(t.GetElementType(), 0); + if (t.IsGenericType) + { + var def = t.GetGenericTypeDefinition(); + if (def == typeof(List<>) || def == typeof(Dictionary<,>) || + def == typeof(HashSet<>) || def == typeof(IList<>) || + def == typeof(IDictionary<,>) || def == typeof(ICollection<>) || + def == typeof(IEnumerable<>)) + { + var concrete = def == typeof(List<>) || def == typeof(IList<>) || + def == typeof(ICollection<>) || def == typeof(IEnumerable<>) + ? typeof(List<>).MakeGenericType(t.GetGenericArguments()) + : def == typeof(HashSet<>) + ? typeof(HashSet<>).MakeGenericType(t.GetGenericArguments()) + : typeof(Dictionary<,>).MakeGenericType(t.GetGenericArguments()); + return Activator.CreateInstance(concrete); + } + } + return null; + } + + private static ClassCharacterMasterData NewChara(int charaId, int classId) + { + var c = (ClassCharacterMasterData)FormatterServices.GetUninitializedObject(typeof(ClassCharacterMasterData)); + SetMember(c, "chara_id", charaId); + SetMember(c, "class_id", classId); + SetMember(c, "skin_id", charaId); + SetMember(c, "is_usable", true); + return c; + } + + // Set a member (auto-property backing field or field) by name, tolerating private setters. + private static void SetMember(object obj, string name, object value) + { + var t = obj.GetType(); + const BindingFlags bf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + var p = t.GetProperty(name, bf); + if (p != null && p.SetMethod != null) { p.SetValue(obj, value); return; } + var f = t.GetField(name, bf) + ?? t.GetField($"<{name}>k__BackingField", bf); + if (f != null) { f.SetValue(obj, value); return; } + throw new InvalidOperationException($"{t.Name} has no settable member '{name}'"); + } + } +} diff --git a/SVSim.BattleEngine.Tests/SVSim.BattleEngine.Tests.csproj b/SVSim.BattleEngine.Tests/SVSim.BattleEngine.Tests.csproj new file mode 100644 index 0000000..83a312d --- /dev/null +++ b/SVSim.BattleEngine.Tests/SVSim.BattleEngine.Tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + + disable + disable + latest + false + true + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/SVSim.BattleEngine/COPIED.manifest.tsv b/SVSim.BattleEngine/COPIED.manifest.tsv index 89bf159..2d7982e 100644 --- a/SVSim.BattleEngine/COPIED.manifest.tsv +++ b/SVSim.BattleEngine/COPIED.manifest.tsv @@ -3308,3 +3308,4 @@ WrapVariableContentsScrollBarSize.cs WrapVariableContentsScrollBarSize.cs 05c94e YuwanField.cs YuwanField.cs 1368d0b2755edbae36ce4dcabd69d8d2f6ce854765ea46621199ef20d407d13a 0 iTween.cs iTween.cs 8da77cd885d8fb1e8727e91681ab5ac00a889d0fcc9b973a4162f15a0b642a54 0 llField.cs llField.cs a0e0eaed3f22a8c4ce47f82fa80346e3b99e3ac0a6765e1ad4ade3a87c1b0189 0 +Wizard.Battle.View/CardIconControl.cs Wizard.Battle.View/CardIconControl.cs affd5a289a04bc9f446f3e892403dd9cd560ee557de1e5cd743dcb031dab280c 0 diff --git a/SVSim.BattleEngine/Engine/Wizard.Battle.View/CardIconControl.cs b/SVSim.BattleEngine/Engine/Wizard.Battle.View/CardIconControl.cs new file mode 100644 index 0000000..a158d2f --- /dev/null +++ b/SVSim.BattleEngine/Engine/Wizard.Battle.View/CardIconControl.cs @@ -0,0 +1,54 @@ +using System; + +namespace Wizard.Battle.View; + +public class CardIconControl +{ + public static string[] SplitAndCompleteIconStr(string iconStr, string[] skillTypeStr) + { + int skillCount = skillTypeStr[0].Split(',').Length; + int num = 0; + if (skillTypeStr.Length > 1) + { + num = skillTypeStr[1].Split(',').Length; + } + if (iconStr == null) + { + string[] array = new string[2] + { + CompleteIconDefaultParam(skillCount), + null + }; + if (num > 0) + { + array[1] = CompleteIconDefaultParam(num); + } + return array; + } + iconStr = iconStr.Replace(" ", ""); + if (string.IsNullOrEmpty(iconStr)) + { + string[] array2 = new string[2] + { + CompleteIconDefaultParam(skillCount), + null + }; + if (num > 0) + { + array2[1] = CompleteIconDefaultParam(num); + } + return array2; + } + return iconStr.Split(new string[1] { "//" }, StringSplitOptions.RemoveEmptyEntries); + } + + public static string CompleteIconDefaultParam(int skillCount) + { + string text = string.Empty; + for (int i = 0; i < skillCount; i++) + { + text = ((!string.IsNullOrEmpty(text)) ? (text + ",none") : "none"); + } + return text; + } +} diff --git a/SVSim.BattleEngine/Shim/Generated/CardIconControl.g.cs b/SVSim.BattleEngine/Shim/Generated/CardIconControl.g.cs deleted file mode 100644 index cc7c153..0000000 --- a/SVSim.BattleEngine/Shim/Generated/CardIconControl.g.cs +++ /dev/null @@ -1,10 +0,0 @@ -// AUTO-GENERATED no-op stubs (m1_stub_gen) from Shadowverse_Code_2026-05-23\Wizard.Battle.View\CardIconControl.cs -using System; -namespace Wizard.Battle.View -{ -public partial class CardIconControl -{ - public static string[] SplitAndCompleteIconStr(string iconStr, string[] skillTypeStr) => default!; - public static string CompleteIconDefaultParam(int skillCount) => default!; -} -} diff --git a/SVSim.BattleEngine/Shim/GodObjects/GodObjects.cs b/SVSim.BattleEngine/Shim/GodObjects/GodObjects.cs index 1aea242..dd19d72 100644 --- a/SVSim.BattleEngine/Shim/GodObjects/GodObjects.cs +++ b/SVSim.BattleEngine/Shim/GodObjects/GodObjects.cs @@ -110,12 +110,15 @@ public class GameMgr private NetworkUserInfoData _netUser; public EffectMgr GetEffectMgr() => _effect ??= new EffectMgr(); - public SoundMgr GetSoundMgr() => _sound; - public DataMgr GetDataMgr() => _data; + public SoundMgr GetSoundMgr() => _sound ??= new SoundMgr(); + // Headless: hand back non-null no-op instances. The copied manager types are pure + // data/dictionary/no-op holders (no Unity in their ctors); the resolution-path ctor + // dereferences these immediately (CreateBackgroundId / CreateManager / UnityEventAgent wiring). + public DataMgr GetDataMgr() => _data ??= new DataMgr(); public GameObjMgr GetGameObjMgr() => _gameObj; - public PrefabMgr GetPrefabMgr() => _prefab; - public InputMgr GetInputMgr() => _input; - public BattleControl GetBattleCtrl() => _battleCtrl; + public PrefabMgr GetPrefabMgr() => _prefab ??= new PrefabMgr(); + public InputMgr GetInputMgr() => _input ??= new InputMgr(); + public BattleControl GetBattleCtrl() => _battleCtrl ??= new BattleControl(); public NetworkUserInfoData GetNetworkUserInfoData() => _netUser; public void SetNetworkUserInfoData(NetworkUserInfoData infoData) => _netUser = infoData; public Wizard.MailTopTask GetMailTopTask() => null; diff --git a/SVSim.BattleEngine/Shim/UnityEngine/UnityRuntime.cs b/SVSim.BattleEngine/Shim/UnityEngine/UnityRuntime.cs index ad2e621..417bf97 100644 --- a/SVSim.BattleEngine/Shim/UnityEngine/UnityRuntime.cs +++ b/SVSim.BattleEngine/Shim/UnityEngine/UnityRuntime.cs @@ -50,8 +50,19 @@ namespace UnityEngine { public static T Load(string path) where T : Object => null; public static T Load(string path, Type t) where T : Object => null; - public static Object Load(string path) => null; - public static Object Load(string path, Type t) => null; + // Headless: the non-generic Load is used by the copied PrefabMgr to back a prefab + // dictionary that the resolution-path ctor then Instantiate()s + GetComponent()s + // (e.g. Prefab/Game/UnityEventAgent). Return a cached no-op GameObject per path so that + // chain yields a non-null object. Typed asset loads go through the generic Load (null). + private static readonly System.Collections.Generic.Dictionary _loaded + = new System.Collections.Generic.Dictionary(); + public static Object Load(string path) + { + if (string.IsNullOrEmpty(path)) return null; + if (!_loaded.TryGetValue(path, out var go)) { go = new GameObject(path); _loaded[path] = go; } + return go; + } + public static Object Load(string path, Type t) => Load(path); public static T[] LoadAll(string path) where T : Object => new T[0]; public static Object[] LoadAll(string path) => new Object[0]; public static ResourceRequest LoadAsync(string path) where T : Object => null; diff --git a/SVSim.BattleEngine/Shim/UnityEngine/UnityShim.cs b/SVSim.BattleEngine/Shim/UnityEngine/UnityShim.cs index adc5f25..56f34e2 100644 --- a/SVSim.BattleEngine/Shim/UnityEngine/UnityShim.cs +++ b/SVSim.BattleEngine/Shim/UnityEngine/UnityShim.cs @@ -185,8 +185,25 @@ namespace UnityEngine public int layer { get; set; } public string tag { get; set; } public void SetActive(bool value) { } - public T GetComponent() => default; - public Component GetComponent(Type t) => null; + + // Headless component model: the resolution-path ctor (and copied views) acquire components + // off prefab GameObjects and use them unguarded (F1). Lazily create + cache a no-op instance + // per concrete Component-derived type so those touches resolve harmlessly instead of NRE. + // Non-Component T or abstract/uninstantiable T still returns default (null). + private System.Collections.Generic.Dictionary _components; + private object GetOrAddComponent(Type t) + { + if (t == null || t.IsAbstract || !typeof(Component).IsAssignableFrom(t)) return null; + _components ??= new System.Collections.Generic.Dictionary(); + if (_components.TryGetValue(t, out var c)) return c; + object inst; + try { inst = Activator.CreateInstance(t); } + catch { return null; } + _components[t] = inst; + return inst; + } + public T GetComponent() => (T)(GetOrAddComponent(typeof(T)) ?? default(T)); + public Component GetComponent(Type t) => (Component)GetOrAddComponent(t); public Component GetComponent(string t) => null; public T GetComponentInChildren() => default; public T GetComponentInChildren(bool includeInactive) => default; @@ -196,8 +213,8 @@ namespace UnityEngine public T GetComponentInParent(bool includeInactive) => default; public T[] GetComponents() => new T[0]; public Component[] GetComponents(Type t) => new Component[0]; - public T AddComponent() where T : Component => default; - public Component AddComponent(Type t) => null; + public T AddComponent() where T : Component => (T)(GetOrAddComponent(typeof(T)) ?? default(T)); + public Component AddComponent(Type t) => (Component)GetOrAddComponent(t); public void SendMessage(string method) { } public void SendMessage(string method, object value) { } public void SendMessage(string method, object value, SendMessageOptions options) { } diff --git a/SVSim.BattleEngine/Shim/View/SettingsUiStubs.cs b/SVSim.BattleEngine/Shim/View/SettingsUiStubs.cs index d048b48..6164073 100644 --- a/SVSim.BattleEngine/Shim/View/SettingsUiStubs.cs +++ b/SVSim.BattleEngine/Shim/View/SettingsUiStubs.cs @@ -48,7 +48,18 @@ namespace Wizard.Battle.View { // Decomp bases (dropped by the hand stub): both derive from BattleCardView, which // carries the IBattleCardView impl — so they convert to IBattleCardView via it. - public abstract class ClassBattleCardViewBase : BattleCardView { } + // The decomp ClassBattleCardViewBase also implements IClassBattleCardView; the resolution + // path casts the created view to it (ClassBattleCardBase.Setup), so re-attach the dropped + // interface here with no-op members (the leaf PlayerClassBattleCardView inherits them). + public abstract class ClassBattleCardViewBase : BattleCardView, IClassBattleCardView + { + public virtual Wizard.Battle.Player.ClassCharacter.IClassCharacter ClassCharacter => null; + public virtual void StartOutFrame() { } + public virtual void StartIntoFrame() { } + public virtual float GetCurrentClipTime() => 0f; + public virtual bool GetCurrentClipIsName(global::ClassCharaPrm.MotionType motionType) => false; + public virtual void ClearSpineObject() { } + } public class NullBattleCardView : BattleCardView { public NullBattleCardView() { } public NullBattleCardView(BuildInfo buildInfo) { } public static void ReleaseSharedDummy() { } } }