From 250f3c55118530f9f4ab16b98b57fa0b30696906 Mon Sep 17 00:00:00 2001 From: robert-d-schultz Date: Wed, 13 May 2026 20:56:40 -0400 Subject: [PATCH] Updated Bmd handling with Warhammer 2 BMD file format insights --- .../ViewModels/BmdEditorViewModel.cs | 14 +- Editors/Reports/Bmd/BmdReportGenerator.cs | 19 +++ Shared/GameFiles/Bmd/BmdFile.cs | 85 ++++++---- Shared/GameFiles/Bmd/BmdParser.cs | 145 ++++++++++-------- 4 files changed, 162 insertions(+), 101 deletions(-) diff --git a/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs b/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs index 64cf26151..c9560ff53 100644 --- a/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs +++ b/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs @@ -358,11 +358,11 @@ private string GenerateComponentDetails(BmdElementViewModel component) details.AppendLine("VFX Details:"); details.AppendLine($" Version: {vfx.Vfx.VfxInfoVersion}"); details.AppendLine($" VFX String: {vfx.Vfx.VfxString}"); - details.AppendLine($" Flag Version: {vfx.Vfx.FlagVersion}"); - details.AppendLine($" Allow In Outfield: {vfx.Vfx.AllowInOutfield}"); - details.AppendLine($" Clamp To Water: {vfx.Vfx.ClampToWaterSurface}"); - details.AppendLine($" Visible In Tactical: {vfx.Vfx.VisibleInTactical}"); - details.AppendLine($" Only Visible In Tactical: {vfx.Vfx.OnlyVisibleInTactical}"); + details.AppendLine($" Flag Version: {vfx.Vfx.Flags.FlagVersion}"); + details.AppendLine($" Allow In Outfield: {vfx.Vfx.Flags.AllowInOutfield}"); + details.AppendLine($" Clamp To Water: {vfx.Vfx.Flags.ClampToWaterSurface}"); + details.AppendLine($" Visible In Tactical: {vfx.Vfx.Flags.VisibleInTactical}"); + details.AppendLine($" Only Visible In Tactical: {vfx.Vfx.Flags.OnlyVisibleInTactical}"); details.AppendLine($" Autoplay: {vfx.Vfx.Autoplay}"); details.AppendLine($" Visible In Shroud: {vfx.Vfx.VisibleInShroud}"); details.AppendLine($" Parent ID: {vfx.Vfx.ParentId}"); @@ -380,9 +380,9 @@ private string GenerateComponentDetails(BmdElementViewModel component) details.AppendLine($" Animation Speed 2: {light.Light.AnimationSpeedScale2:F2}"); details.AppendLine($" Color Min: {light.Light.ColorMin:F2}"); details.AppendLine($" Random Offset: {light.Light.RandomOffset:F2}"); - details.AppendLine($" WPLFT Type: {light.Light.WPLFTType}"); + details.AppendLine($" Falloff Type: {light.Light.FalloffType}"); details.AppendLine($" Height Mode: {light.Light.HeightMode}"); - details.AppendLine($" For Light Probe Only: {light.Light.ForLightProbeOnly}"); + details.AppendLine($" Light Probe Only: {light.Light.LightProbeOnly}"); break; case SpotLightInfoViewModel spotLight: diff --git a/Editors/Reports/Bmd/BmdReportGenerator.cs b/Editors/Reports/Bmd/BmdReportGenerator.cs index 0144c42ad..c433d466e 100644 --- a/Editors/Reports/Bmd/BmdReportGenerator.cs +++ b/Editors/Reports/Bmd/BmdReportGenerator.cs @@ -280,6 +280,13 @@ public void Create(string outputDir = null) vfxRecord.PositionY = vfx.Transform.M42; vfxRecord.PositionZ = vfx.Transform.M43; vfxRecord.VfxString = vfx.VfxString; + vfxRecord.EmissionRate = vfx.EmissionRate; + vfxRecord.InstanceName = vfx.InstanceName; + vfxRecord.ClampToSurface = vfx.Flags.ClampToSurface; + vfxRecord.SeasonSpring = vfx.Flags.SeasonSpring; + vfxRecord.SeasonSummer = vfx.Flags.SeasonSummer; + vfxRecord.SeasonAutumn = vfx.Flags.SeasonAutumn; + vfxRecord.SeasonWinter = vfx.Flags.SeasonWinter; vfxRecords.Add(vfxRecord); } @@ -326,6 +333,10 @@ public void Create(string outputDir = null) pointLightRecord.PositionY = pointLight.Position.Y; pointLightRecord.PositionZ = pointLight.Position.Z; pointLightRecord.Color = $"{pointLight.Red},{pointLight.Green},{pointLight.Blue}"; + pointLightRecord.FalloffType = pointLight.FalloffType; + pointLightRecord.LFRelative = pointLight.LFRelative; + pointLightRecord.LightProbeOnly = pointLight.LightProbeOnly; + pointLightRecord.PdlcMask = pointLight.PdlcMask; pointLightRecords.Add(pointLightRecord); } @@ -346,9 +357,17 @@ public void Create(string outputDir = null) { dynamic playableAreaRecord = new ExpandoObject(); playableAreaRecord.Path = path; + playableAreaRecord.PlayableAreaVersion = parsedFile.PlayableArea.PlayableAreaVersion; playableAreaRecord.BoundingBoxMinX = parsedFile.PlayableArea.BoundingBox.Length > 0 ? parsedFile.PlayableArea.BoundingBox[0] : 0; playableAreaRecord.BoundingBoxMinY = parsedFile.PlayableArea.BoundingBox.Length > 1 ? parsedFile.PlayableArea.BoundingBox[1] : 0; + playableAreaRecord.BoundingBoxMaxX = parsedFile.PlayableArea.BoundingBox.Length > 2 ? parsedFile.PlayableArea.BoundingBox[2] : 0; + playableAreaRecord.BoundingBoxMaxY = parsedFile.PlayableArea.BoundingBox.Length > 3 ? parsedFile.PlayableArea.BoundingBox[3] : 0; playableAreaRecord.HasBeenSet = parsedFile.PlayableArea.HasBeenSet; + playableAreaRecord.FlagVersion = parsedFile.PlayableArea.FlagVersion; + playableAreaRecord.Flag1 = parsedFile.PlayableArea.Flag1; + playableAreaRecord.Flag2 = parsedFile.PlayableArea.Flag2; + playableAreaRecord.Flag3 = parsedFile.PlayableArea.Flag3; + playableAreaRecord.Flag4 = parsedFile.PlayableArea.Flag4; playableAreaRecords.Add(playableAreaRecord); } diff --git a/Shared/GameFiles/Bmd/BmdFile.cs b/Shared/GameFiles/Bmd/BmdFile.cs index 7ff44ab2f..2f7ebc457 100644 --- a/Shared/GameFiles/Bmd/BmdFile.cs +++ b/Shared/GameFiles/Bmd/BmdFile.cs @@ -43,6 +43,10 @@ public class BmdFile public List TreeListReferences { get; set; } = new(); public List GrassListReferences { get; set; } = new(); public List WaterOutlines { get; set; } = new(); + + //Pharaoh Exclusive classes (for Pharaoh's version of the version 25 BMD format) + public CameraZoneNew CameraZoneNew { get; set; } = new(); + public MiscParams MiscParams { get; set; } = new(); } public class FastBinHeader @@ -91,6 +95,20 @@ public struct CultureMask public bool CultMaskChd { get; set; } } + public class BmdComponentFlags + { + public ushort FlagVersion { get; set; } + public bool AllowInOutfield { get; set; } + public bool ClampToSurface { get; set; } //Flag version 2 only + public bool ClampToWaterSurface { get; set; } + public bool SeasonSpring { get; set; } + public bool SeasonSummer { get; set; } + public bool SeasonAutumn { get; set; } + public bool SeasonWinter { get; set; } + public bool VisibleInTactical { get; set; } + public bool OnlyVisibleInTactical { get; set; } + } + public class BattlefieldBuilding { public ushort Version { get; set; } @@ -186,8 +204,8 @@ public class BmdInfo public ushort Version { get; set; } public string BmdString { get; set; } = string.Empty; public Matrix Transform { get; set; } = Matrix.Identity; - public byte[] SeasonsMaybe { get; set; } = new byte[4]; //is this ? - public CultureMask CultureMask { get; set; } + public byte[] SeasonsMaybe { get; set; } = new byte[4]; //this has to correspond to + public CultureMask CultureMask { get; set; } //"campaign_type_mask"? public string RegionString { get; set; } = string.Empty; public string HeightMode { get; set; } = string.Empty; public byte[] Uid { get; set; } = new byte[8]; @@ -229,7 +247,6 @@ public class CivilianShelter public ushort Version { get; set; } } - public class PropInfo { public ushort PropInfoVersion { get; set; } @@ -247,16 +264,7 @@ public class PropInfo public float DecalTiling { get; set; } public bool DecalOverrideGbufferNormal { get; set; } - public ushort FlagsVersion { get; set; } - public bool AllowInOutfield { get; set; } - public bool ClampToWaterSurface { get; set; } - public bool FlagBool3 { get; set; } //Flag version 3/4 or something - public bool SeasonSpring { get; set; } - public bool SeasonSummer { get; set; } - public bool SeasonAutumn { get; set; } - public bool SeasonWinter { get; set; } - public bool VisibleInTactical { get; set; } - public bool OnlyVisibleInTactical { get; set; } + public BmdComponentFlags Flags { get; set; } = new(); public bool VisibleInShroud { get; set; } public bool ApplyToTerrain { get; set; } @@ -285,14 +293,11 @@ public class VfxInfo public ushort VfxInfoVersion { get; set; } public string VfxString { get; set; } = string.Empty; public Matrix Transform { get; set; } = Matrix.Identity; - public byte[] Booleans { get; set; } = new byte[6]; - public ushort FlagVersion { get; set; } - public bool AllowInOutfield { get; set; } - public bool ClampToWaterSurface { get; set; } - public bool Version2ExtraBool { get; set; } - public byte[] Seasons { get; set; } = new byte[4]; - public bool VisibleInTactical { get; set; } - public bool OnlyVisibleInTactical { get; set; } + public float EmissionRate { get; set; } + public string InstanceName { get; set; } = string.Empty; + + public BmdComponentFlags Flags { get; set; } = new(); + public string HeightMode { get; set; } = string.Empty; public byte[] CultureMask { get; set; } = new byte[8]; public bool Autoplay { get; set; } @@ -380,12 +385,11 @@ public class PointLightInfo public float AnimationSpeedScale2 { get; set; } public float ColorMin { get; set; } public float RandomOffset { get; set; } - public string WPLFTType { get; set; } = string.Empty; - public byte SomeZero { get; set; } + public string FalloffType { get; set; } = string.Empty; + public byte LFRelative { get; set; } public string HeightMode { get; set; } = string.Empty; - public bool ForLightProbeOnly { get; set; } - public byte[] MoreData { get; set; } = new byte[4]; - public byte[] EvenMoreData { get; set; } = new byte[4]; + public bool LightProbeOnly { get; set; } + public ulong PdlcMask { get; set; } public byte[] MoreData2 { get; set; } = new byte[10]; } @@ -557,4 +561,33 @@ public class WaterOutline //TODO: Not properly implemented public ushort Version { get; set; } } + + + + //Pharaoh Exclusive classes (for Pharaoh's version of the version 25 BMD format) + //This is version hell! They didn't keep the versions consistent! + public class CameraZoneNew + { + //TODO: Not properly implemented + public ushort Version { get; set; } + } + public class MiscParams + { + public ushort Version { get; set; } + public string WaterPlaneMaterial { get; set; } = string.Empty; + public float NormalTiling { get; set; } + public float NormalStrengthScale { get; set; } + public float NormalTimeScale { get; set; } + public float DepthDistortionCoef { get; set; } + public float CausticsScale { get; set; } + public float CausticsMinDepthCoef { get; set; } + public float CausticsMaxDepthCoef { get; set; } + public float WaterOpacity { get; set; } + public float WaterSpeed { get; set; } + public float WaterDirection { get; set; } + public bool ShoreWaves { get; set; } + public RmvVector2 CausticsUvScale { get; set; } + public RmvVector3 WaterColor { get; set; } + public RmvVector3 WaterSpecular { get; set; } + } } diff --git a/Shared/GameFiles/Bmd/BmdParser.cs b/Shared/GameFiles/Bmd/BmdParser.cs index ced68a108..7b0eb7b91 100644 --- a/Shared/GameFiles/Bmd/BmdParser.cs +++ b/Shared/GameFiles/Bmd/BmdParser.cs @@ -86,10 +86,10 @@ public BmdFile Parse() earlyProp.EarlyVersionUnknownBool = _reader.ReadByte() != 0; if (earlyProp.PropInfoVersion > 2) //this being the seasons is a guess { - earlyProp.SeasonSpring = _reader.ReadByte() != 0; - earlyProp.SeasonSummer = _reader.ReadByte() != 0; - earlyProp.SeasonAutumn = _reader.ReadByte() != 0; - earlyProp.SeasonWinter = _reader.ReadByte() != 0; + earlyProp.Flags.SeasonSpring = _reader.ReadByte() != 0; + earlyProp.Flags.SeasonSummer = _reader.ReadByte() != 0; + earlyProp.Flags.SeasonAutumn = _reader.ReadByte() != 0; + earlyProp.Flags.SeasonWinter = _reader.ReadByte() != 0; } bmdFile.PropInfos.Add(earlyProp); @@ -302,16 +302,18 @@ private BattlefieldBuilding ReadBattlefieldBuilding() var building = new BattlefieldBuilding(); building.Version = _reader.ReadUInt16(); - // Read building attributes building.BuildingId = ReadString(); - building.ParentId = _reader.ReadInt32(); + if (building.Version > 8) + building.ParentId = _reader.ReadInt32(); + else + building.ParentId = _reader.ReadInt16(); building.BuildingKey = ReadString(); building.PositionType = ReadString(); - // Read transform matrix (3x4 matrix stored in row-major order) + // Transform matrix (3x4 matrix stored in row-major order) building.Transform = ReadRowMajorMatrix(false); - // Read properties inline + // Properties building.PropertiesVersion = _reader.ReadUInt16(); building.PropertiesBuildingId = ReadString(); building.StartingDamageUnary = _reader.ReadSingle(); @@ -325,14 +327,19 @@ private BattlefieldBuilding ReadBattlefieldBuilding() building.Lite = _reader.ReadByte() != 0; building.CastShadows = _reader.ReadByte() != 0; building.KeyBuilding = _reader.ReadByte() != 0; - building.KeyBuildingUseFort = _reader.ReadByte() != 0; - building.IsPropInOutfield = _reader.ReadByte() != 0; - building.SettlementLevelConfigurable = _reader.ReadByte() != 0; - building.HideTooltip = _reader.ReadByte() != 0; - building.IncludeInFog = _reader.ReadByte() != 0; + if (building.Version > 8) + { + building.KeyBuildingUseFort = _reader.ReadByte() != 0; + building.IsPropInOutfield = _reader.ReadByte() != 0; + building.SettlementLevelConfigurable = _reader.ReadByte() != 0; + building.HideTooltip = _reader.ReadByte() != 0; + building.IncludeInFog = _reader.ReadByte() != 0; + } building.HeightMode = ReadString(); - building.Uid = _reader.ReadInt64(); + + if (building.Version > 8) + building.Uid = _reader.ReadInt64(); return building; } @@ -530,30 +537,18 @@ private PropInfo ReadPropInfo(List propsList) prop.DecalParallaxScale = _reader.ReadSingle(); prop.DecalTiling = _reader.ReadSingle(); prop.DecalOverrideGbufferNormal = _reader.ReadByte() != 0; - - prop.FlagsVersion = _reader.ReadUInt16(); - prop.AllowInOutfield = _reader.ReadByte() != 0; - prop.ClampToWaterSurface = _reader.ReadByte() != 0; - if (prop.FlagsVersion == 2) //unknown extra bool that's only here for this version? - prop.FlagBool3 = _reader.ReadByte() != 0; - prop.SeasonSpring = _reader.ReadByte() != 0; - prop.SeasonSummer = _reader.ReadByte() != 0; - prop.SeasonAutumn = _reader.ReadByte() != 0; - prop.SeasonWinter = _reader.ReadByte() != 0; - if (prop.FlagsVersion > 3) - { - prop.VisibleInTactical = _reader.ReadByte() != 0; - prop.OnlyVisibleInTactical = _reader.ReadByte() != 0; - } + + prop.Flags = ReadBmdComponentFlags(); prop.VisibleInShroud = _reader.ReadByte() != 0; prop.ApplyToTerrain = _reader.ReadByte() != 0; prop.ApplyToPropsOrReceiveDecal = _reader.ReadByte() != 0; prop.RenderAboveSnow = _reader.ReadByte() != 0; - prop.HeightMode = ReadString(); + if (prop.PropInfoVersion > 7) + prop.HeightMode = ReadString(); - if (prop.PropInfoVersion > 15) + if (prop.PropInfoVersion > 14) prop.CultureMask = _reader.ReadBytes(8); else if (prop.PropInfoVersion > 10) { @@ -567,11 +562,11 @@ private PropInfo ReadPropInfo(List propsList) //there's an extra byte in here just for version 9, then it's gone in version 10 // (representing it with "CastsShadow") - if (prop.PropInfoVersion > 10 || prop.PropInfoVersion == 9) + if (prop.PropInfoVersion > 11 || prop.PropInfoVersion == 9) prop.CastsShadow = _reader.ReadByte() != 0; - if (prop.PropInfoVersion > 12) + if (prop.PropInfoVersion > 13) prop.NoCulling = _reader.ReadByte() != 0; - if (prop.PropInfoVersion > 14) + if (prop.PropInfoVersion > 15) prop.HasHeightPatch = _reader.ReadByte() != 0; if (prop.PropInfoVersion > 16) prop.ApplyHeightPatch = _reader.ReadByte() != 0; @@ -605,23 +600,15 @@ private VfxInfo ReadVfxInfo() // Read transform matrix (3x4 matrix stored in row-major order) vfx.Transform = ReadRowMajorMatrix(false); - vfx.Booleans = _reader.ReadBytes(6); - vfx.FlagVersion = _reader.ReadUInt16(); - vfx.AllowInOutfield = _reader.ReadByte() != 0; - vfx.ClampToWaterSurface = _reader.ReadByte() != 0; - if (vfx.FlagVersion == 2) //unknown extra bool that's only here for this version? - vfx.Version2ExtraBool = _reader.ReadByte() != 0; - vfx.Seasons = _reader.ReadBytes(4); - if (vfx.FlagVersion > 3) - { - vfx.VisibleInTactical = _reader.ReadByte() != 0; - vfx.OnlyVisibleInTactical = _reader.ReadByte() != 0; - } + + vfx.EmissionRate = _reader.ReadSingle(); + vfx.InstanceName = ReadString(); + vfx.Flags = ReadBmdComponentFlags(); if (vfx.VfxInfoVersion > 2) - { vfx.HeightMode = ReadString(); - + if (vfx.VfxInfoVersion > 3) + { if (vfx.VfxInfoVersion > 5) vfx.CultureMask = _reader.ReadBytes(8); else @@ -630,7 +617,9 @@ private VfxInfo ReadVfxInfo() var oldMask = _reader.ReadBytes(4); Array.Copy(oldMask, vfx.CultureMask, 4); } - + } + if (vfx.VfxInfoVersion > 4) + { vfx.Autoplay = _reader.ReadByte() != 0; vfx.VisibleInShroud = _reader.ReadByte() != 0; } @@ -784,14 +773,18 @@ private PointLightInfo ReadPointLightInfo() light.AnimationSpeedScale2 = _reader.ReadSingle(); light.ColorMin = _reader.ReadSingle(); light.RandomOffset = _reader.ReadSingle(); - light.WPLFTType = ReadString(); - light.SomeZero = _reader.ReadByte(); - light.HeightMode = ReadString(); - light.ForLightProbeOnly = _reader.ReadByte() != 0; - light.MoreData = _reader.ReadBytes(4); - if (light.PointLightInfoVersion > 5) + light.FalloffType = ReadString(); + light.LFRelative = _reader.ReadByte(); + if (light.PointLightInfoVersion > 1) + light.HeightMode = ReadString(); + if (light.PointLightInfoVersion > 2) { - light.EvenMoreData = _reader.ReadBytes(4); + light.LightProbeOnly = _reader.ReadByte() != 0; + + if (light.PointLightInfoVersion > 5) + light.PdlcMask = _reader.ReadUInt64(); + else + light.PdlcMask = _reader.ReadUInt32(); } if (light.PointLightInfoVersion > 6) { @@ -806,22 +799,16 @@ private BuildingProjectileEmitter ReadBuildingProjectileEmitter() emitter.BuildingProjectileEmitterVersion = _reader.ReadUInt16(); - // Read location (COORD - using RmvVector3 for 3D coordinates) emitter.Location = ReadRmvVector3(); - - // Read rotation[3] (3 floats) emitter.Rotation[0] = _reader.ReadSingle(); emitter.Rotation[1] = _reader.ReadSingle(); emitter.Rotation[2] = _reader.ReadSingle(); - // Read building_index (uint32) emitter.BuildingIndex = _reader.ReadUInt32(); - - // Read height_mode (length prefixed string) emitter.HeightMode = ReadString(); - // Read specialized_building_projectile_emitter_key (length prefixed string) - emitter.SpecializedBuildingProjectileEmitterKey = ReadString(); + if (emitter.BuildingProjectileEmitterVersion > 2) + emitter.SpecializedBuildingProjectileEmitterKey = ReadString(); return emitter; } @@ -839,13 +826,12 @@ private PlayableArea ReadPlayableArea() if (area.PlayableAreaVersion > 1) { - area.FlagVersion = _reader.ReadUInt16(); + if (area.PlayableAreaVersion > 2) + area.FlagVersion = _reader.ReadUInt16(); area.Flag1 = _reader.ReadByte() != 0; area.Flag2 = _reader.ReadByte() != 0; area.Flag3 = _reader.ReadByte() != 0; - - if (area.FlagVersion > 0) - area.Flag4 = _reader.ReadByte() != 0; + area.Flag4 = _reader.ReadByte() != 0; } return area; @@ -1114,6 +1100,29 @@ private WaterOutline ReadWaterOutline() throw new NotImplementedException("WaterOutline parsing not implemented yet"); } + + + //Helper Functions + private BmdComponentFlags ReadBmdComponentFlags() + { + var flags = new BmdComponentFlags(); + flags.FlagVersion = _reader.ReadUInt16(); + flags.AllowInOutfield = _reader.ReadByte() != 0; + if (flags.FlagVersion == 2) + flags.ClampToSurface = _reader.ReadByte() != 0; + flags.ClampToWaterSurface = _reader.ReadByte() != 0; + flags.SeasonSpring = _reader.ReadByte() != 0; + flags.SeasonSummer = _reader.ReadByte() != 0; + flags.SeasonAutumn = _reader.ReadByte() != 0; + flags.SeasonWinter = _reader.ReadByte() != 0; + if (flags.FlagVersion > 3) + { + flags.VisibleInTactical = _reader.ReadByte() != 0; + flags.OnlyVisibleInTactical = _reader.ReadByte() != 0; + } + return flags; + } + private string ReadString() { var length = _reader.ReadUInt16();