First green of the M2 go/no-go probe: `new SingleBattleMgr(StandardBattleMgr- ContentsCreator)` now builds the two-player pair fully headless against the shim, no Unity runtime. Verdict: headless construction is feasible; every blocker was a mechanical no-op shim fill or data seam, not a Unity/logic wall. Shim fills (authored): - GameMgr: lazy non-null DataMgr/PrefabMgr/InputMgr/SoundMgr/BattleControl. - GameObject: lazy cached component model so GetComponent<T>/AddComponent<T> return non-null no-op instances for Component-derived T (F1: unguarded view touches). - Resources.Load(string): cached non-null GameObject so the prefab->Instantiate-> GetComponent chain (UnityEventAgent) yields a real object. - ClassBattleCardViewBase: re-attach dropped IClassBattleCardView (no-op members); ClassBattleCardBase.Setup casts the created view to it. Engine copy (DP1/DP3 mis-cut fix): - CardIconControl.cs copied verbatim (manifested) + generated null-stub deleted. SplitAndCompleteIconStr is pure string logic on the resolution path that M1 had wrongly stubbed as "View" -> null deref in SkillCreator.CreateBuildInfo. Test harness (SVSim.BattleEngine.Tests, authored fixture): - HeadlessContentsCreator/HeadlessPhaseCreator: deterministic replica of the solo practice init (StandardBattleMgrContentsCreator + SingleBattlePhaseCreator) with no-op recovery/replay managers. - HeadlessCardMaster: reflects the loader cards.json dump into CardMaster. - HeadlessMasterData: minimal Data.Master (class-character list, empty collections) + Data.Load + player/enemy chara ids. - ConstructionProbeTests.SingleBattleMgr_constructs_headless — GREEN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
4.4 KiB
C#
96 lines
4.4 KiB
C#
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<ClassCharacterMasterData>
|
|
{
|
|
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}'");
|
|
}
|
|
}
|
|
}
|