├── src ├── MphRead │ ├── MphRead.csproj │ ├── Entities │ │ ├── Enemies │ │ │ ├── 43_SlenchNest.cs │ │ │ ├── 51_CarnivorousPlant.cs │ │ │ ├── 42_SlenchShield.cs │ │ │ ├── 25_GoreaHead.cs │ │ │ ├── 21_CretaphidCrystal.cs │ │ │ ├── 27_GoreaLeg.cs │ │ │ ├── 50_HitZone.cs │ │ │ ├── 29_GoreaSealSphere1.cs │ │ │ ├── 47_GreaterIthrak.cs │ │ │ ├── 40_EnemySpawner.cs │ │ │ ├── 30_Trocra.cs │ │ │ ├── 32_GoreaSealSphere2.cs │ │ │ ├── 11_Shriekbat.cs │ │ │ ├── 16_Blastcap.cs │ │ │ ├── 04_Petrasyl2.cs │ │ │ ├── 01_Zoomer.cs │ │ │ ├── 49_ForceFieldLock.cs │ │ │ └── 03_Petrasyl1.cs │ │ ├── LightSourceEntity.cs │ │ ├── PlayerSpawnEntity.cs │ │ ├── Players │ │ │ ├── PlayerAi.cs │ │ │ └── DynamicLightEntity.cs │ │ ├── PointModuleEntity.cs │ │ ├── FlagBaseEntity.cs │ │ ├── MorphCameraEntity.cs │ │ ├── BeamEffectEntity.cs │ │ ├── ForceFieldEntity.cs │ │ └── JumpPadEntity.cs │ ├── Utility │ │ ├── Console.cs │ │ ├── Rng.cs │ │ ├── Output.cs │ │ ├── Parser.cs │ │ └── Archive.cs │ ├── Formats │ │ ├── Culling.cs │ │ ├── FhSound.cs │ │ ├── Frontend.cs │ │ └── NodeData.cs │ ├── Messaging.cs │ ├── Testing │ │ └── TestOverlay.cs │ └── Export │ │ └── Images.cs └── MphRead.sln ├── LICENSE ├── README.md └── .gitignore /src/MphRead/MphRead.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | true 7 | embedded 8 | enable 9 | True 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 NoneGiven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/43_SlenchNest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MphRead.Formats.Culling; 3 | using OpenTK.Mathematics; 4 | 5 | namespace MphRead.Entities.Enemies 6 | { 7 | public class Enemy43Entity : EnemyInstanceEntity 8 | { 9 | private readonly EnemySpawnEntity _spawner; 10 | 11 | public Enemy43Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 12 | : base(data, nodeRef, scene) 13 | { 14 | var spawner = data.Spawner as EnemySpawnEntity; 15 | Debug.Assert(spawner != null); 16 | _spawner = spawner; 17 | } 18 | 19 | protected override void EnemyInitialize() 20 | { 21 | Transform = _spawner.Transform; 22 | _health = _healthMax = 100; 23 | Flags |= EnemyFlags.Visible; 24 | Flags |= EnemyFlags.Invincible; 25 | Flags |= EnemyFlags.NoMaxDistance; 26 | HealthbarMessageId = 2; 27 | _boundingRadius = 0; 28 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, rad: 0); 29 | SetUpModel("BigEyeNest"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MphRead/Utility/Console.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace MphRead 7 | { 8 | internal static class ConsoleSetup 9 | { 10 | [DllImport("kernel32.dll")] 11 | private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); 12 | 13 | [DllImport("kernel32.dll")] 14 | private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); 15 | 16 | [DllImport("kernel32.dll", SetLastError = true)] 17 | private static extern IntPtr GetStdHandle(int nStdHandle); 18 | 19 | [DllImport("kernel32.dll")] 20 | public static extern uint GetLastError(); 21 | 22 | public static void Run() 23 | { 24 | CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; 25 | Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); 26 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 27 | { 28 | IntPtr iStdOut = GetStdHandle(-11); 29 | GetConsoleMode(iStdOut, out uint outConsoleMode); 30 | outConsoleMode |= 4; 31 | SetConsoleMode(iStdOut, outConsoleMode); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MphRead.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29319.158 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MphRead", "MphRead\MphRead.csproj", "{086E4265-E23A-4BC4-940D-D5866BD1A209}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DB47478D-FE94-4DE8-B1D2-3E08B6FFF7C8}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {086E4265-E23A-4BC4-940D-D5866BD1A209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {086E4265-E23A-4BC4-940D-D5866BD1A209}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {086E4265-E23A-4BC4-940D-D5866BD1A209}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {086E4265-E23A-4BC4-940D-D5866BD1A209}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {A235AFD3-7A10-49B2-9F0C-71C64BE69F95} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/51_CarnivorousPlant.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MphRead.Formats.Culling; 3 | using OpenTK.Mathematics; 4 | 5 | namespace MphRead.Entities.Enemies 6 | { 7 | public class Enemy51Entity : EnemyInstanceEntity 8 | { 9 | private readonly EnemySpawnEntity _spawner; 10 | 11 | public Enemy51Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 12 | : base(data, nodeRef, scene) 13 | { 14 | var spawner = data.Spawner as EnemySpawnEntity; 15 | Debug.Assert(spawner != null); 16 | _spawner = spawner; 17 | } 18 | 19 | protected override void EnemyInitialize() 20 | { 21 | Transform = _data.Spawner.Transform; 22 | _prevPos = Position; 23 | Flags |= EnemyFlags.Visible; 24 | Flags |= EnemyFlags.Static; 25 | Flags |= EnemyFlags.NoMaxDistance; // the game doesn't set this 26 | _health = _healthMax = _spawner.Data.Fields.S07.EnemyHealth; 27 | _boundingRadius = Fixed.ToFloat(1843); 28 | _hurtVolumeInit = new CollisionVolume(new Vector3(0, Fixed.ToFloat(409), 0), _boundingRadius); 29 | _hurtVolume = CollisionVolume.Transform(_hurtVolumeInit, Transform); 30 | ObjectMetadata meta = Metadata.GetObjectById(_spawner.Data.Fields.S07.EnemySubtype); 31 | SetUpModel(meta.Name); 32 | } 33 | 34 | protected override void EnemyProcess() 35 | { 36 | ContactDamagePlayer(_spawner.Data.Fields.S07.EnemyDamage, knockback: false); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/42_SlenchShield.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MphRead.Formats.Culling; 3 | using OpenTK.Mathematics; 4 | 5 | namespace MphRead.Entities.Enemies 6 | { 7 | public class Enemy42Entity : EnemyInstanceEntity 8 | { 9 | private readonly Enemy41Entity _slench; 10 | public Enemy41Entity Slench => _slench; 11 | 12 | public Enemy42Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 13 | : base(data, nodeRef, scene) 14 | { 15 | var spawner = data.Spawner as Enemy41Entity; 16 | Debug.Assert(spawner != null); 17 | _slench = spawner; 18 | } 19 | 20 | protected override void EnemyInitialize() 21 | { 22 | Transform = _slench.Transform; 23 | _health = _healthMax = 255; 24 | Flags |= EnemyFlags.NoMaxDistance; 25 | HealthbarMessageId = 2; 26 | _boundingRadius = 0; 27 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, rad: 1); 28 | } 29 | 30 | protected override void EnemyProcess() 31 | { 32 | Vector3 facing = _slench.FacingVector.Normalized(); 33 | Position = _slench.Position + facing * _slench.ShieldOffset; 34 | } 35 | 36 | protected override bool EnemyTakeDamage(EntityBase? source) 37 | { 38 | if (!_slench.SlenchFlags.TestFlag(SlenchFlags.EyeClosed) && _slench.SlenchFlags.TestFlag(SlenchFlags.Vulnerable)) 39 | { 40 | _slench.Flags &= ~EnemyFlags.Invincible; 41 | _slench.TakeDamage((uint)(_healthMax - _health), source); 42 | _slench.Flags |= EnemyFlags.Invincible; 43 | } 44 | else 45 | { 46 | _slench.ShieldTakeDamage(source); 47 | } 48 | _health = _healthMax; 49 | return false; 50 | } 51 | 52 | public void UpdateScanId(int scanId) 53 | { 54 | _scanId = scanId; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/MphRead/Entities/LightSourceEntity.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Mathematics; 2 | 3 | namespace MphRead.Entities 4 | { 5 | public class LightSourceEntity : EntityBase 6 | { 7 | private readonly LightSourceEntityData _data; 8 | protected override Vector4? OverrideColor { get; } = new ColorRgb(0xFF, 0xDE, 0xAD).AsVector4(); 9 | 10 | public CollisionVolume Volume { get; } 11 | public bool Light1Enabled { get; } 12 | public Vector3 Light1Vector { get; } 13 | public Vector3 Light1Color { get; } 14 | public bool Light2Enabled { get; } 15 | public Vector3 Light2Vector { get; } 16 | public Vector3 Light2Color { get; } 17 | 18 | public LightSourceEntity(LightSourceEntityData data, Scene scene) : base(EntityType.LightSource, scene) 19 | { 20 | _data = data; 21 | Id = data.Header.EntityId; 22 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 23 | Volume = CollisionVolume.Move(_data.Volume, Position); 24 | Light1Enabled = _data.Light1Enabled != 0; 25 | Light1Vector = _data.Light1Vector.ToFloatVector(); 26 | Light1Color = _data.Light1Color.AsVector3(); 27 | Light2Enabled = _data.Light2Enabled != 0; 28 | Light2Vector = _data.Light2Vector.ToFloatVector(); 29 | Light2Color = _data.Light2Color.AsVector3(); 30 | AddPlaceholderModel(); 31 | } 32 | 33 | public override void GetDisplayVolumes() 34 | { 35 | if (_scene.ShowVolumes == VolumeDisplay.LightColor1 || _scene.ShowVolumes == VolumeDisplay.LightColor2) 36 | { 37 | Vector3 color = Vector3.Zero; 38 | if (_scene.ShowVolumes == VolumeDisplay.LightColor1 && _data.Light1Enabled != 0) 39 | { 40 | color = Light1Color; 41 | } 42 | else if (_scene.ShowVolumes == VolumeDisplay.LightColor2 && _data.Light2Enabled != 0) 43 | { 44 | color = Light2Color; 45 | } 46 | AddVolumeItem(Volume, color); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/MphRead/Entities/PlayerSpawnEntity.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Mathematics; 2 | 3 | namespace MphRead.Entities 4 | { 5 | public class PlayerSpawnEntity : EntityBase 6 | { 7 | private readonly PlayerSpawnEntityData _data; 8 | public PlayerSpawnEntityData Data => _data; 9 | protected override Vector4? OverrideColor { get; } = new ColorRgb(0x7F, 0x00, 0x00).AsVector4(); 10 | private bool _active = false; 11 | 12 | public bool IsActive => _active; 13 | public bool Availability => _data.Availability != 0; 14 | public ushort Cooldown { get; set; } 15 | 16 | public PlayerSpawnEntity(PlayerSpawnEntityData data, string nodeName, Scene scene) 17 | : base(EntityType.PlayerSpawn, nodeName, scene) 18 | { 19 | _data = data; 20 | Id = data.Header.EntityId; 21 | AddPlaceholderModel(); 22 | } 23 | 24 | public override void Initialize() 25 | { 26 | base.Initialize(); 27 | SetTransform(_data.Header.FacingVector, _data.Header.UpVector, _data.Header.Position); 28 | if (_scene.GameMode == GameMode.SinglePlayer) 29 | { 30 | _active = GameState.StorySave.InitRoomState(_scene.RoomId, Id, active: _data.Active != 0) != 0; 31 | } 32 | else 33 | { 34 | _active = _data.Active != 0; 35 | } 36 | } 37 | 38 | public override bool Process() 39 | { 40 | if (Cooldown > 0) 41 | { 42 | Cooldown--; 43 | } 44 | return base.Process(); 45 | } 46 | 47 | public override void HandleMessage(MessageInfo info) 48 | { 49 | if (info.Message == Message.Activate || (info.Message == Message.SetActive && (int)info.Param1 != 0)) 50 | { 51 | _active = true; 52 | if (_scene.GameMode == GameMode.SinglePlayer) 53 | { 54 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 3); 55 | } 56 | } 57 | else if (info.Message == Message.SetActive && (int)info.Param1 == 0) 58 | { 59 | _active = false; 60 | if (_scene.GameMode == GameMode.SinglePlayer) 61 | { 62 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 1); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Players/PlayerAi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MphRead.Entities 4 | { 5 | public class AiData 6 | { 7 | // todo: all field names (and also implementations) 8 | public AiFlags1 Flags1 { get; set; } // todo?: not currently convinced these need to be bitfields 9 | public AiFlags2 Flags2 { get; set; } 10 | public ushort HealthThreshold { get; set; } 11 | public uint Field110 { get; set; } 12 | } 13 | 14 | public partial class PlayerEntity 15 | { 16 | public AiData AiData { get; init; } = new AiData(); 17 | } 18 | 19 | [Flags] 20 | public enum AiFlags1 : uint 21 | { 22 | None = 0, 23 | Bit0 = 1, 24 | Bit1 = 2, 25 | Bit2 = 4, 26 | Bit3 = 8, 27 | Bit4 = 0x10, 28 | Bit5 = 0x20, 29 | Bit6 = 0x40, 30 | Bit7 = 0x80, 31 | Bit8 = 0x100, 32 | Bit9 = 0x200, 33 | Bit10 = 0x400, 34 | Bit11 = 0x800, 35 | Bit12 = 0x1000, 36 | Bit13 = 0x2000, 37 | Bit14 = 0x4000, 38 | Bit15 = 0x8000, 39 | Bit16 = 0x10000, 40 | Bit17 = 0x20000, 41 | Bit18 = 0x40000, 42 | Bit19 = 0x80000, 43 | Bit20 = 0x100000, 44 | Bit21 = 0x200000, 45 | Bit22 = 0x400000, 46 | Bit23 = 0x800000, 47 | Bit24 = 0x1000000, 48 | Bit25 = 0x2000000, 49 | Bit26 = 0x4000000, 50 | Bit27 = 0x8000000, 51 | Bit28 = 0x10000000, 52 | Bit29 = 0x20000000, 53 | Bit30 = 0x40000000, 54 | Bit31 = 0x80000000 55 | } 56 | 57 | [Flags] 58 | public enum AiFlags2 : uint 59 | { 60 | None = 0, 61 | Bit0 = 1, 62 | Bit1 = 2, 63 | Bit2 = 4, 64 | Bit3 = 8, 65 | Bit4 = 0x10, 66 | Bit5 = 0x20, 67 | Bit6 = 0x40, 68 | Bit7 = 0x80, 69 | Bit8 = 0x100, 70 | Bit9 = 0x200, 71 | Bit10 = 0x400, 72 | Bit11 = 0x800, 73 | Bit12 = 0x1000, 74 | Bit13 = 0x2000, 75 | Bit14 = 0x4000, 76 | Bit15 = 0x8000, 77 | Bit16 = 0x10000, 78 | Bit17 = 0x20000, 79 | Bit18 = 0x40000, 80 | Bit19 = 0x80000, 81 | Bit20 = 0x100000, 82 | Bit21 = 0x200000, 83 | Bit22 = 0x400000, 84 | Bit23 = 0x800000, 85 | Bit24 = 0x1000000, 86 | Bit25 = 0x2000000, 87 | Bit26 = 0x4000000, 88 | Bit27 = 0x8000000, 89 | Bit28 = 0x10000000, 90 | Bit29 = 0x20000000, 91 | Bit30 = 0x40000000, 92 | Bit31 = 0x80000000 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/MphRead/Utility/Rng.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MphRead 4 | { 5 | public class Rng 6 | { 7 | public const uint Rng1StartValue = 0x3DE9179BU; 8 | public const uint Rng2StartValue = 0; 9 | 10 | public static uint Rng1 { get; private set; } = Rng1StartValue; 11 | public static uint Rng2 { get; private set; } = Rng2StartValue; 12 | 13 | public static uint CallRng(ref uint rng, uint value) 14 | { 15 | rng *= 0x7FF8A3ED; 16 | rng += 0x2AA01D31; 17 | return (uint)((rng >> 16) * (long)value / 0x10000L); 18 | } 19 | 20 | public static uint GetRandomInt1(int value) 21 | { 22 | return GetRandomInt1((uint)value); 23 | } 24 | 25 | public static uint GetRandomInt2(int value) 26 | { 27 | return GetRandomInt2((uint)value); 28 | } 29 | 30 | public static uint GetRandomInt1(uint value) 31 | { 32 | Rng1 *= 0x7FF8A3ED; 33 | Rng1 += 0x2AA01D31; 34 | return (uint)((Rng1 >> 16) * (long)value / 0x10000L); 35 | } 36 | 37 | public static uint GetRandomInt2(uint value) 38 | { 39 | Rng2 *= 0x7FF8A3ED; 40 | Rng2 += 0x2AA01D31; 41 | return (uint)((Rng2 >> 16) * (long)value / 0x10000L); 42 | } 43 | 44 | public static void SetRng1(uint value) 45 | { 46 | Rng1 = value; 47 | } 48 | 49 | public static void SetRng2(uint value) 50 | { 51 | Rng2 = value; 52 | } 53 | 54 | public static void DoDamageShake(int damage) 55 | { 56 | int shake = (int)(damage * 40.96f); 57 | if (shake < 204) 58 | { 59 | shake = 204; 60 | } 61 | DoCameraShake(shake); 62 | } 63 | 64 | public static void DoCameraShake(int shake) 65 | { 66 | Console.WriteLine($"shake {shake}"); 67 | uint rng = Rng2; 68 | int frames = 0; 69 | while (shake > 0) 70 | { 71 | frames++; 72 | GetRandomInt2(1); 73 | GetRandomInt2(1); 74 | GetRandomInt2(1); 75 | shake = (int)((3481L * shake + 2048) >> 12); 76 | if (shake < 41) 77 | { 78 | shake = 0; 79 | } 80 | } 81 | int calls = frames * 3; 82 | Console.WriteLine($"{frames} frame{(frames == 1 ? "" : "s")}, {calls} calls"); 83 | Console.WriteLine($"rng {rng:X8} --> {Rng2:X8}"); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/MphRead/Formats/Culling.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTK.Mathematics; 3 | 4 | namespace MphRead.Formats.Culling 5 | { 6 | public struct NodeRef 7 | { 8 | public int PartIndex; 9 | public int NodeIndex; 10 | public int ModelIndex; 11 | 12 | public static readonly NodeRef None = new NodeRef 13 | { 14 | PartIndex = -1, 15 | NodeIndex = -1, 16 | ModelIndex = -1 17 | }; 18 | 19 | public NodeRef(int partIndex, int nodeIndex, int modelIndex) 20 | { 21 | PartIndex = partIndex; 22 | NodeIndex = nodeIndex; 23 | ModelIndex = modelIndex; 24 | } 25 | 26 | public static bool operator ==(NodeRef lhs, NodeRef rhs) 27 | { 28 | return lhs.PartIndex == rhs.PartIndex && lhs.NodeIndex == rhs.NodeIndex 29 | && lhs.ModelIndex == rhs.ModelIndex; 30 | } 31 | 32 | public static bool operator !=(NodeRef lhs, NodeRef rhs) 33 | { 34 | return lhs.PartIndex != rhs.PartIndex || lhs.NodeIndex != rhs.NodeIndex 35 | || lhs.ModelIndex != rhs.ModelIndex; 36 | } 37 | 38 | public override bool Equals(object? obj) 39 | { 40 | return obj is NodeRef other && PartIndex == other.PartIndex 41 | && NodeIndex == other.NodeIndex && ModelIndex == other.ModelIndex; 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | return HashCode.Combine(PartIndex, NodeIndex, ModelIndex); 47 | } 48 | } 49 | 50 | public class RoomPartVisInfo 51 | { 52 | public NodeRef NodeRef; 53 | public float ViewMinX; 54 | public float ViewMaxX; 55 | public float ViewMinY; 56 | public float ViewMaxY; 57 | public RoomPartVisInfo? Next; 58 | } 59 | 60 | public class RoomFrustumItem 61 | { 62 | public NodeRef NodeRef; 63 | public readonly FrustumInfo Info; 64 | public RoomFrustumItem? Next; 65 | 66 | public RoomFrustumItem() 67 | { 68 | NodeRef = NodeRef.None; 69 | Info = new FrustumInfo(); 70 | } 71 | } 72 | 73 | public class FrustumInfo 74 | { 75 | public int Index; 76 | public int Count; 77 | public readonly FrustumPlane[] Planes = new FrustumPlane[10]; 78 | } 79 | 80 | public struct FrustumPlane 81 | { 82 | public int XIndex1; 83 | public int XIndex2; 84 | public int YIndex1; 85 | public int YIndex2; 86 | public int ZIndex1; 87 | public int ZIndex2; 88 | public Vector4 Plane; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/MphRead/Entities/PointModuleEntity.cs: -------------------------------------------------------------------------------- 1 | namespace MphRead.Entities 2 | { 3 | public class PointModuleEntity : EntityBase 4 | { 5 | private readonly PointModuleEntityData _data; 6 | public PointModuleEntity? Next { get; private set; } 7 | public PointModuleEntity? Prev { get; private set; } 8 | 9 | private static PointModuleEntity? _current; 10 | public static PointModuleEntity? Current => _current; 11 | 12 | public const int StartId = 50; 13 | 14 | public PointModuleEntity(PointModuleEntityData data, Scene scene) : base(EntityType.PointModule, scene) 15 | { 16 | _data = data; 17 | Id = data.Header.EntityId; 18 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 19 | ModelInstance inst = SetUpModel("pick_morphball", firstHunt: true); 20 | Active = false; 21 | inst.Active = false; 22 | } 23 | 24 | public override void Initialize() 25 | { 26 | base.Initialize(); 27 | if (_data.NextId != 0 && _scene.TryGetEntity(_data.NextId, out EntityBase? entity)) 28 | { 29 | Next = (PointModuleEntity)entity; 30 | } 31 | if (_data.PrevId != 0 && _scene.TryGetEntity(_data.PrevId, out entity)) 32 | { 33 | Prev = (PointModuleEntity)entity; 34 | } 35 | } 36 | 37 | public override bool Process() 38 | { 39 | if (_current == null && Id == StartId) 40 | { 41 | SetCurrent(); 42 | } 43 | return base.Process(); 44 | } 45 | 46 | public void SetCurrent() 47 | { 48 | if (_current != this) 49 | { 50 | UpdateChain(_current, false); 51 | _current = this; 52 | UpdateChain(_current, true); 53 | } 54 | } 55 | 56 | private void UpdateChain(PointModuleEntity? entity, bool state) 57 | { 58 | int i = 0; 59 | while (entity != null && i < 5) 60 | { 61 | entity.SetActive(state); 62 | entity = entity.Next; 63 | i++; 64 | } 65 | } 66 | 67 | public override void SetActive(bool active) 68 | { 69 | base.SetActive(active); 70 | _models[0].Active = Active; 71 | } 72 | 73 | public override EntityBase? GetParent() 74 | { 75 | return Prev; 76 | } 77 | 78 | public override EntityBase? GetChild() 79 | { 80 | return Next; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/25_GoreaHead.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Effects; 2 | using MphRead.Formats.Culling; 3 | using OpenTK.Mathematics; 4 | 5 | namespace MphRead.Entities.Enemies 6 | { 7 | public class Enemy25Entity : GoreaEnemyEntityBase 8 | { 9 | private Node _attachNode = null!; 10 | private Enemy24Entity _gorea1A = null!; 11 | public int Damage { get; set; } 12 | private EffectEntry? _flashEffect = null; 13 | 14 | public Enemy25Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 15 | : base(data, nodeRef, scene) 16 | { 17 | } 18 | 19 | protected override void EnemyInitialize() 20 | { 21 | if (_owner is Enemy24Entity owner) 22 | { 23 | _gorea1A = owner; 24 | InitializeCommon(owner.Spawner); 25 | Flags |= EnemyFlags.Invincible; 26 | Flags &= ~EnemyFlags.Visible; 27 | _state1 = _state2 = 255; 28 | ModelInstance ownerModel = _owner.GetModels()[0]; 29 | _attachNode = ownerModel.Model.GetNodeByName("Head")!; 30 | Position += _attachNode.Position; 31 | _prevPos = Position; 32 | SetTransform(owner.FacingVector, owner.UpVector, Position); 33 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, Fixed.ToFloat(1314)); 34 | } 35 | } 36 | 37 | protected override void EnemyProcess() 38 | { 39 | Matrix4 transform = GetNodeTransform(_gorea1A, _attachNode); 40 | Position = transform.Row3.Xyz; 41 | if (_flashEffect != null) 42 | { 43 | if (_flashEffect.IsFinished) 44 | { 45 | RemoveFlashEffect(); 46 | } 47 | else 48 | { 49 | Vector3 position = Position + _gorea1A.FacingVector * Fixed.ToFloat(2949); 50 | position += _gorea1A.UpVector * Fixed.ToFloat(-939); 51 | _flashEffect.Transform(_gorea1A.FacingVector, _gorea1A.UpVector, position); 52 | } 53 | } 54 | } 55 | 56 | private void RemoveFlashEffect() 57 | { 58 | if (_flashEffect != null) 59 | { 60 | _scene.DetachEffectEntry(_flashEffect, setExpired: false); 61 | _flashEffect = null; 62 | } 63 | } 64 | 65 | public void RespawnFlashEffect() 66 | { 67 | RemoveFlashEffect(); 68 | Vector3 spawnPos = Position + _gorea1A.FacingVector * Fixed.ToFloat(2949); 69 | spawnPos += _gorea1A.UpVector * Fixed.ToFloat(-939); 70 | _flashEffect = SpawnEffectGetEntry(104, spawnPos, extensionFlag: false); // goreaEyeFlash 71 | } 72 | 73 | protected override bool EnemyTakeDamage(EntityBase? source) 74 | { 75 | Damage = 65535 - _health; 76 | _health = 65535; 77 | return true; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/21_CretaphidCrystal.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MphRead.Formats.Culling; 3 | using OpenTK.Mathematics; 4 | 5 | namespace MphRead.Entities.Enemies 6 | { 7 | public class Enemy21Entity : EnemyInstanceEntity 8 | { 9 | private readonly Enemy19Entity _cretaphid; 10 | private Node _attachNode = null!; 11 | private EquipInfo _equipInfo = null!; 12 | private int _ammo = 1000; 13 | 14 | public Enemy21Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 15 | : base(data, nodeRef, scene) 16 | { 17 | var owner = data.Spawner as Enemy19Entity; 18 | Debug.Assert(owner != null); 19 | _cretaphid = owner; 20 | } 21 | 22 | public void SetUp(Node attachNode, int scanId, uint effectiveness, ushort health, Vector3 position) 23 | { 24 | HealthbarMessageId = 1; 25 | _attachNode = attachNode; 26 | _scanId = scanId; 27 | Metadata.LoadEffectiveness(effectiveness, BeamEffectiveness); 28 | _health = _healthMax = health; 29 | Flags |= EnemyFlags.Invincible; 30 | Flags |= EnemyFlags.NoMaxDistance; 31 | Matrix4 transform = GetTransformMatrix(attachNode.Transform.Row2.Xyz, attachNode.Transform.Row1.Xyz); 32 | transform.Row3.Xyz = attachNode.Transform.Row3.Xyz + position; 33 | Transform = transform; 34 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, 1); 35 | _boundingRadius = 1; 36 | _equipInfo = new EquipInfo(Weapons.BossWeapons[0], _beams); 37 | _equipInfo.GetAmmo = () => _ammo; 38 | _equipInfo.SetAmmo = (newAmmo) => _ammo = newAmmo; 39 | } 40 | 41 | protected override void EnemyProcess() 42 | { 43 | _cretaphid.UpdateTransforms(rootPosition: false); 44 | Position = _attachNode.Animation.Row3.Xyz + _cretaphid.Position; 45 | if (_health > 0 && !_cretaphid.SoundSource.CheckEnvironmentSfx(5)) // CYLINDER_BOSS_ATTACK 46 | { 47 | _cretaphid.SoundSource.PlayEnvironmentSfx(6); // CYLINDER_BOSS_SPIN 48 | } 49 | } 50 | 51 | public void SpawnBeam(ushort damage) 52 | { 53 | _equipInfo.Weapon.UnchargedDamage = damage; 54 | _equipInfo.Weapon.SplashDamage = damage; 55 | _equipInfo.Weapon.HeadshotDamage = damage; 56 | Vector3 spawnDir = (PlayerEntity.Main.Position.AddY(0.5f) - Position).Normalized(); 57 | BeamProjectileEntity.Spawn(this, _equipInfo, Position, spawnDir, BeamSpawnFlags.None, _cretaphid.NodeRef, _scene); 58 | } 59 | 60 | protected override bool EnemyTakeDamage(EntityBase? source) 61 | { 62 | if (_health == 0) 63 | { 64 | _health = 1; 65 | Flags |= EnemyFlags.Invincible; 66 | _scene.SendMessage(Message.SetActive, this, _cretaphid, param1: 0, param2: 0); 67 | } 68 | return false; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MphRead 2 | This project is a reverse engineering and game recreation effort comprising a model viewer, scene renderer, and general parser for file formats used in the Nintendo DS game Metroid Prime Hunters. The renderer is implemented using OpenGL via the [OpenTK](https://github.com/opentk/opentk) library with audio through [OpenAL Soft](https://github.com/kcat/openal-soft). Documentation of various game features can be found in the [wiki](https://github.com/NoneGiven/MphRead/wiki). 3 | 4 | ## Features 5 | - Recreates the gameplay of the original game 6 | - Stores save data to allow playing through the story mode 7 | - Renders individual models or complete game rooms with entities 8 | - Visualizes collision data for rooms and entities 9 | - Exports models to COLLADA, textures to PNG, and sound effects to WAV 10 | - Generates Python scripts to import model animations and more into Blender 11 | 12 | ## Planned 13 | - Music playback 14 | - Room editor and save editor 15 | - Render more things, implement more gameplay logic 16 | - And even more! 17 | 18 | ## Usage 19 | 20 | After setup, MphRead can be launched from the executable with no arguments, and menu prompts will appear to help you set up the scene. 21 | 22 | See the [full setup and export guide](https://github.com/NoneGiven/MphRead/wiki/Setup-&-Export-Guide) for details on setup and command line options. 23 | 24 | ## Building 25 | 26 | If you do not want to build from source, simply download and run the latest [release](https://github.com/NoneGiven/MphRead/releases). 27 | 28 | ### With Visual Studio 29 | 30 | With a recent version of [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) installed, you should be able to open the solution and build immediately. 31 | 32 | ### Without Visual Studio 33 | 34 | - Install the .NET SDK. The [latest stable version](https://dotnet.microsoft.com/en-us/download/dotnet/latest) is recommended, while the minimum required version is [.NET 7.0](https://dotnet.microsoft.com/en-us/download/dotnet/7.0). 35 | - Run `dotnet build` in the `src/MphRead` directory. 36 | 37 | ## Acknowledgements 38 | 39 | A significant portion of this project's code was based on the file format information or source code from several other projects. 40 | 41 | - **dsgraph** - The original MPH model viewer, on which all other projects are built. 42 | - **[Chemical's model format](https://gitlab.com/ch-mcl/metroid-prime-hunters-file-document/-/blob/master/Model/BinModel.md)** - Documentation of the model format. 43 | - **[McKay42's mph-model-viewer](https://github.com/McKay42/mph-model-viewer)** - COLLADA export method. 44 | - **[McKay42's mph-arc-extractor](https://github.com/McKay42/mph-arc-extractor)** - ARC file format information. 45 | - **[Barubary's dsdecmp](https://github.com/Barubary/dsdecmp)** - LZ10 compression routines. 46 | - **[loveemu's swav2wav](https://github.com/loveemu/loveemu-lab)** - SWAV conversion function. 47 | 48 | ## Special Thanks 49 | 50 | This project's reverse engineering effort was developed parallel to **[hackyourlife's mph-viewer](https://github.com/hackyourlife/mph-viewer)**, a model viewer implementation in C. Major features such as the transparency rendering implementation were derived from its source code. 51 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/27_GoreaLeg.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Formats.Culling; 2 | using OpenTK.Mathematics; 3 | 4 | namespace MphRead.Entities.Enemies 5 | { 6 | public class Enemy27Entity : GoreaEnemyEntityBase 7 | { 8 | private Enemy24Entity _gorea1A = null!; 9 | private Node _kneeNode = null!; 10 | public int Index { get; set; } 11 | 12 | public Enemy27Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 13 | : base(data, nodeRef, scene) 14 | { 15 | } 16 | 17 | protected override void EnemyInitialize() 18 | { 19 | if (_owner is Enemy24Entity owner) 20 | { 21 | _gorea1A = owner; 22 | InitializeCommon(owner.Spawner); 23 | Flags &= ~EnemyFlags.Visible; 24 | Flags |= EnemyFlags.Invincible; 25 | _state1 = _state2 = 255; 26 | _prevPos = Position; 27 | SetTransform(owner.FacingVector, owner.UpVector, Position); 28 | _hurtVolumeInit = new CollisionVolume(Vector3.UnitY, Vector3.Zero, Fixed.ToFloat(736), Fixed.ToFloat(9700)); 29 | _health = 65535; 30 | _healthMax = 120; 31 | SetKneeNode(owner); 32 | } 33 | } 34 | 35 | public void SetKneeNode(GoreaEnemyEntityBase parent) 36 | { 37 | string nodeName; 38 | if (Index == 2) 39 | { 40 | nodeName = "BK_Knee"; 41 | } 42 | else if (Index == 1) 43 | { 44 | nodeName = "R_Knee"; 45 | } 46 | else 47 | { 48 | nodeName = "L_Knee"; 49 | } 50 | ModelInstance ownerModel = parent.GetModels()[0]; 51 | _kneeNode = ownerModel.Model.GetNodeByName(nodeName)!; 52 | Matrix4 transform = GetNodeTransform(parent, _kneeNode); 53 | Position = transform.Row3.Xyz; 54 | } 55 | 56 | protected override void EnemyProcess() 57 | { 58 | Matrix4 transform = GetNodeTransform(_gorea1A, _kneeNode); 59 | Position = transform.Row3.Xyz; 60 | Vector3 cylinderVec = transform.Row0.Xyz.Normalized(); 61 | if (Index != 1) 62 | { 63 | cylinderVec *= -1; 64 | } 65 | Vector3 cylinderPos = cylinderVec * Fixed.ToFloat(-9700); 66 | _hurtVolumeInit = new CollisionVolume(cylinderVec, cylinderPos, _hurtVolumeInit.CylinderRadius, _hurtVolumeInit.CylinderDot); 67 | CheckPlayerCollision(factor: 0.25f, damage: 10); // 1024 68 | } 69 | 70 | private void CheckPlayerCollision(float factor, int damage) 71 | { 72 | if (!HitPlayers[PlayerEntity.Main.SlotIndex]) 73 | { 74 | return; 75 | } 76 | Vector3 between = (PlayerEntity.Main.Position - Position).WithY(0); 77 | between = between.LengthSquared > 1 / 128f 78 | ? between.Normalized() 79 | : FacingVector; 80 | PlayerEntity.Main.Speed += between * factor; 81 | PlayerEntity.Main.TakeDamage(damage, DamageFlags.None, null, this); 82 | } 83 | 84 | protected override bool EnemyTakeDamage(EntityBase? source) 85 | { 86 | _health = 65535; 87 | return false; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/MphRead/Entities/FlagBaseEntity.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Mathematics; 2 | 3 | namespace MphRead.Entities 4 | { 5 | public class FlagBaseEntity : EntityBase 6 | { 7 | private readonly FlagBaseEntityData _data; 8 | private readonly CollisionVolume _volume; 9 | private readonly bool _capture = false; 10 | 11 | // flag base has a model in Bounty, but is invisible in Capture 12 | protected override Vector4? OverrideColor { get; } = new ColorRgb(15, 207, 255).AsVector4(); 13 | 14 | public FlagBaseEntity(FlagBaseEntityData data, Scene scene) : base(EntityType.FlagBase, scene) 15 | { 16 | _data = data; 17 | Id = data.Header.EntityId; 18 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 19 | _volume = CollisionVolume.Move(_data.Volume, Position); 20 | // note: an explicit mode check is necessary because e.g. Sic Transit has OctolithFlags/FlagBases 21 | // enabled in Defender mode according to their layer masks, but they don't appear in-game 22 | GameMode mode = scene.GameMode; 23 | if (mode == GameMode.Capture) 24 | { 25 | AddPlaceholderModel(); 26 | } 27 | else if (mode == GameMode.Bounty || mode == GameMode.BountyTeams) 28 | { 29 | SetUpModel("flagbase_cap"); 30 | } 31 | _capture = mode == GameMode.Capture; 32 | } 33 | 34 | public override bool Process() 35 | { 36 | base.Process(); 37 | for (int i = 0; i < _scene.Entities.Count; i++) 38 | { 39 | EntityBase entity = _scene.Entities[i]; 40 | if (entity.Type != EntityType.Player) 41 | { 42 | continue; 43 | } 44 | var player = (PlayerEntity)entity; 45 | if (player.OctolithFlag == null || _capture && player.TeamIndex != _data.TeamId) 46 | { 47 | continue; 48 | } 49 | if (_volume.TestPoint(player.Position)) 50 | { 51 | if (_capture && !CheckOwnOctolith(player)) 52 | { 53 | if (player == PlayerEntity.Main) 54 | { 55 | PlayerEntity.Main.QueueHudMessage(128, 50, 1 / 1000f, 0, 232); // your octolith is missing! 56 | } 57 | continue; 58 | } 59 | player.OctolithFlag.OnCaptured(); 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | private bool CheckOwnOctolith(PlayerEntity player) 66 | { 67 | for (int i = 0; i < _scene.Entities.Count; i++) 68 | { 69 | EntityBase entity = _scene.Entities[i]; 70 | if (entity.Type != EntityType.OctolithFlag) 71 | { 72 | continue; 73 | } 74 | var octolith = (OctolithFlagEntity)entity; 75 | if (octolith.Data.TeamId == player.TeamIndex && !octolith.AtBase) 76 | { 77 | return false; 78 | } 79 | } 80 | return true; 81 | } 82 | 83 | // todo: is_visible 84 | public override void GetDisplayVolumes() 85 | { 86 | if (_scene.ShowVolumes == VolumeDisplay.FlagBase) 87 | { 88 | AddVolumeItem(_volume, Vector3.One); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/MphRead/Formats/FhSound.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace MphRead.Formats.Sound 7 | { 8 | public static partial class SoundRead 9 | { 10 | public static void ExportAllFh(bool adpcmRoundingError = false) 11 | { 12 | ExportFhBgm(adpcmRoundingError); 13 | ExportFhGlobalSfx(adpcmRoundingError); 14 | ExportFhMenuSfx(adpcmRoundingError); 15 | ExportFhSfx(adpcmRoundingError); 16 | } 17 | 18 | public static void ExportFhSfx(bool adpcmRoundingError = false) 19 | { 20 | ExportSamples(ReadFhSfx(), adpcmRoundingError, prefix: "fh_"); 21 | } 22 | 23 | public static void ExportFhBgm(bool adpcmRoundingError = false) 24 | { 25 | ExportSamples(ReadFhBgm(), adpcmRoundingError, prefix: "fh_bgm_"); 26 | } 27 | 28 | public static void ExportFhMenuSfx(bool adpcmRoundingError = false) 29 | { 30 | ExportSamples(ReadFhMenuSfx(), adpcmRoundingError, prefix: "fh_menu_"); 31 | } 32 | 33 | public static void ExportFhGlobalSfx(bool adpcmRoundingError = false) 34 | { 35 | ExportSamples(ReadFhGlobalSfx(), adpcmRoundingError, prefix: "fh_lid_"); 36 | } 37 | 38 | public static IReadOnlyList ReadFhSfx() 39 | { 40 | return ReadFhSoundFile("SFXDATA.BIN"); 41 | } 42 | 43 | public static IReadOnlyList ReadFhBgm() 44 | { 45 | return ReadFhSoundFile("BGMDATA.BIN"); 46 | } 47 | 48 | public static IReadOnlyList ReadFhMenuSfx() 49 | { 50 | return ReadFhSoundFile("MENUSFXDATA.BIN"); 51 | } 52 | 53 | public static IReadOnlyList ReadFhGlobalSfx() 54 | { 55 | return ReadFhSoundFile("GLOBALSFXDATA.BIN"); 56 | } 57 | 58 | public static IReadOnlyList ReadFhSoundFile(string filename) 59 | { 60 | string path = Paths.Combine(Paths.FhFileSystem, "sound", filename); 61 | var bytes = new ReadOnlySpan(File.ReadAllBytes(path)); 62 | uint count = Read.SpanReadUint(bytes, 0); 63 | var samples = new List(); 64 | uint id = 0; 65 | IReadOnlyList offsets = Read.DoOffsets(bytes, 4, count); 66 | foreach (uint offset in offsets) 67 | { 68 | FhSoundSampleHeader header = Read.DoOffset(bytes, offset); 69 | if (header.DataSize <= 4) 70 | { 71 | samples.Add(SoundSample.CreateNull(id)); 72 | } 73 | else 74 | { 75 | long start = offset + Marshal.SizeOf(); 76 | uint size = header.DataSize; 77 | samples.Add(new SoundSample(id, offset, header, bytes.Slice(start, size))); 78 | } 79 | id++; 80 | } 81 | return samples; 82 | } 83 | } 84 | 85 | // size: 24 86 | public readonly struct FhSoundSampleHeader 87 | { 88 | public readonly uint DataSize; 89 | public readonly uint DataPointer; // always zero in the file 90 | public readonly ushort SampleRate; 91 | public readonly ushort FieldA; // probably padding 92 | public readonly ushort Volume; 93 | public readonly byte FieldE; // padding padding 94 | public readonly byte Format; 95 | public readonly uint LoopStart; 96 | public readonly uint LoopEnd; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/MphRead/Entities/MorphCameraEntity.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Formats; 2 | using OpenTK.Mathematics; 3 | 4 | namespace MphRead.Entities 5 | { 6 | public class MorphCameraEntity : EntityBase 7 | { 8 | private readonly MorphCameraEntityData _data; 9 | protected override Vector4? OverrideColor { get; } = new ColorRgb(0x00, 0xFF, 0x00).AsVector4(); 10 | 11 | private readonly CollisionVolume _volume; 12 | private static readonly Vector3 _volumeColor = new Vector3(1, 1, 0); 13 | 14 | public MorphCameraEntity(MorphCameraEntityData data, string nodeName, Scene scene) 15 | : base(EntityType.MorphCamera, nodeName, scene) 16 | { 17 | _data = data; 18 | Id = data.Header.EntityId; 19 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 20 | _volume = CollisionVolume.Move(_data.Volume, Position); 21 | AddPlaceholderModel(); 22 | } 23 | 24 | public override bool Process() 25 | { 26 | CollisionResult discard = default; 27 | for (int i = 0; i < _scene.Entities.Count; i++) 28 | { 29 | EntityBase entity = _scene.Entities[i]; 30 | if (entity.Type != EntityType.Player) 31 | { 32 | continue; 33 | } 34 | var player = (PlayerEntity)entity; 35 | if (player.IsAltForm) 36 | { 37 | if (player.MorphCamera == null) 38 | { 39 | if (CollisionDetection.CheckVolumesOverlap(_volume, player.Volume, ref discard)) 40 | { 41 | player.MorphCamera = this; 42 | player.CameraInfo.NodeRef = NodeRef; 43 | player.RefreshExternalCamera(); 44 | } 45 | } 46 | else if (player.MorphCamera == this 47 | && !CollisionDetection.CheckVolumesOverlap(_volume, player.Volume, ref discard)) 48 | { 49 | player.MorphCamera = null; 50 | player.ResumeOwnCamera(); 51 | player.RefreshExternalCamera(); 52 | } 53 | } 54 | else if (player.MorphCamera != null) 55 | { 56 | player.MorphCamera = null; 57 | player.ResumeOwnCamera(); 58 | } 59 | } 60 | return base.Process(); 61 | } 62 | 63 | public override void GetDisplayVolumes() 64 | { 65 | if (_scene.ShowVolumes == VolumeDisplay.MorphCamera) 66 | { 67 | AddVolumeItem(_volume, _volumeColor); 68 | } 69 | } 70 | } 71 | 72 | public class FhMorphCameraEntity : EntityBase 73 | { 74 | private readonly FhMorphCameraEntityData _data; 75 | protected override Vector4? OverrideColor { get; } = new ColorRgb(0x00, 0xFF, 0x00).AsVector4(); 76 | 77 | private readonly CollisionVolume _volume; 78 | private static readonly Vector3 _volumeColor = new Vector3(1, 1, 0); 79 | 80 | public FhMorphCameraEntity(FhMorphCameraEntityData data, Scene scene) : base(EntityType.MorphCamera, scene) 81 | { 82 | _data = data; 83 | Id = data.Header.EntityId; 84 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 85 | _volume = CollisionVolume.Move(_data.Volume, Position); 86 | AddPlaceholderModel(); 87 | } 88 | 89 | public override void GetDisplayVolumes() 90 | { 91 | if (_scene.ShowVolumes == VolumeDisplay.MorphCamera) 92 | { 93 | AddVolumeItem(_volume, _volumeColor); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/MphRead/Messaging.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MphRead.Entities; 3 | 4 | namespace MphRead 5 | { 6 | public readonly struct MessageInfo 7 | { 8 | public readonly Message Message; 9 | public readonly EntityBase Sender; 10 | public readonly EntityBase? Target; 11 | public readonly object Param1; 12 | public readonly object Param2; 13 | public readonly ulong ExecuteFrame; 14 | public readonly ulong QueuedFrame; 15 | 16 | public MessageInfo(Message message, EntityBase sender, EntityBase? target, object param1, object param2, 17 | ulong executeFrame, ulong queuedFrame) 18 | { 19 | 20 | Message = message; 21 | Sender = sender; 22 | Target = target; 23 | Param1 = param1; 24 | Param2 = param2; 25 | ExecuteFrame = executeFrame; 26 | QueuedFrame = queuedFrame; 27 | } 28 | } 29 | 30 | public partial class Scene 31 | { 32 | private const int _queueSize = 40; 33 | private readonly List _queue = new List(_queueSize); 34 | public IReadOnlyList MessageQueue => _queue; 35 | 36 | public void SendMessage(Message message, EntityBase sender, EntityBase? target, object param1, object param2) 37 | { 38 | ulong frame = _frameCount; 39 | if (target == null) 40 | { 41 | frame++; 42 | } 43 | DispatchOrQueueMessage(message, sender, target, param1, param2, frame); 44 | } 45 | 46 | public void SendMessage(Message message, EntityBase sender, EntityBase? target, object param1, object param2, int delay) 47 | { 48 | if (delay < 0) 49 | { 50 | delay = 0; 51 | } 52 | DispatchOrQueueMessage(message, sender, target, param1, param2, _frameCount + (ulong)delay); 53 | } 54 | 55 | private void DispatchOrQueueMessage(Message message, EntityBase sender, EntityBase? target, object param1, object param2, ulong frame) 56 | { 57 | var info = new MessageInfo(message, sender, target, param1, param2, frame, _frameCount); 58 | if (frame <= _frameCount) 59 | { 60 | DispatchMessage(info); 61 | } 62 | else 63 | { 64 | QueueMessage(info); 65 | } 66 | } 67 | 68 | private void DispatchMessage(MessageInfo info) 69 | { 70 | if (info.Message == Message.SetTriggerState) 71 | { 72 | int index = (int)info.Param1; 73 | GameState.StorySave.TriggerState[index / 8] |= (byte)(1 << (index % 8)); 74 | } 75 | else if (info.Message == Message.ClearTriggerState) 76 | { 77 | int index = (int)info.Param1; 78 | GameState.StorySave.TriggerState[index / 8] &= (byte)~(1 << (index % 8)); 79 | } 80 | else if (info.Target != null) 81 | { 82 | info.Target.HandleMessage(info); 83 | } 84 | } 85 | 86 | private void QueueMessage(MessageInfo info) 87 | { 88 | if (_queue.Count < _queueSize) 89 | { 90 | _queue.Add(info); 91 | } 92 | } 93 | 94 | private void ProcessMessageQueue() 95 | { 96 | for (int i = 0; i < _queue.Count; i++) 97 | { 98 | MessageInfo info = _queue[i]; 99 | if (info.ExecuteFrame <= _frameCount) 100 | { 101 | DispatchMessage(info); 102 | _queue.RemoveAt(i); 103 | i--; 104 | } 105 | } 106 | } 107 | 108 | public void ClearMessageQueue() 109 | { 110 | _queue.Clear(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/MphRead/Entities/BeamEffectEntity.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Formats.Collision; 2 | using OpenTK.Mathematics; 3 | 4 | namespace MphRead.Entities 5 | { 6 | public readonly struct BeamEffectEntityData 7 | { 8 | // used as a type for icewave/sniperbeam/cylburn, or subtract 3 to get an effect ID to spawn 9 | public readonly int Type; 10 | // in game this is treated as an anim ID for icewave/sniperbeam/cylburn, but all of those only have one animation anyway 11 | public readonly bool NoSplat; 12 | public readonly Matrix4 Transform; 13 | public readonly EntityCollision? EntityCollision; 14 | 15 | public BeamEffectEntityData(int type, bool noSplat, Matrix4 transform, EntityCollision? entCol = null) 16 | { 17 | Type = type; 18 | NoSplat = noSplat; 19 | Transform = transform; 20 | EntityCollision = entCol; 21 | } 22 | } 23 | 24 | public class BeamEffectEntity : EntityBase 25 | { 26 | private int _lifespan = 0; 27 | 28 | public BeamEffectEntity(Scene scene) : base(EntityType.BeamEffect, scene) 29 | { 30 | } 31 | 32 | public void Spawn(BeamEffectEntityData data) 33 | { 34 | _models.Clear(); 35 | // already loaded by scene setup 36 | ModelInstance model; 37 | if (data.Type == 0) 38 | { 39 | model = SetUpModel("iceWave", 0, AnimFlags.NoLoop); 40 | } 41 | else if (data.Type == 1) 42 | { 43 | model = SetUpModel("sniperBeam", 0, AnimFlags.NoLoop); 44 | } 45 | else if (data.Type == 2) 46 | { 47 | model = SetUpModel("cylBossLaserBurn"); 48 | } 49 | else 50 | { 51 | throw new ProgramException("Invalid beam effect type."); 52 | } 53 | _lifespan = 0; 54 | // in-game all the group types are checked, but we're just checking what's actually used 55 | if (model.Model.AnimationGroups.Node.Count > 0 && data.Type != 2) 56 | { 57 | _lifespan = (model.Model.AnimationGroups.Node[0].FrameCount - 1) * 2; 58 | } 59 | else if (model.Model.AnimationGroups.Material.Count > 0) 60 | { 61 | _lifespan = (model.Model.AnimationGroups.Material[0].FrameCount - 1) * 2; 62 | } 63 | Transform = data.Transform; 64 | if (data.Type == 0) 65 | { 66 | _scene.SpawnEffect(78, data.Transform.ClearScale()); // iceWave 67 | } 68 | } 69 | 70 | public void Reposition(Vector3 offset) 71 | { 72 | // todo?: update more effect stuff? 73 | Position += offset; 74 | } 75 | 76 | public override bool Process() 77 | { 78 | if (_lifespan-- <= 0) 79 | { 80 | return false; 81 | } 82 | return base.Process(); 83 | } 84 | 85 | public override void Destroy() 86 | { 87 | _scene.UnlinkBeamEffect(this); 88 | base.Destroy(); 89 | } 90 | 91 | public static BeamEffectEntity? Create(BeamEffectEntityData data, Scene scene) 92 | { 93 | // ptodo: effect and type 0 both need to use mtxptr 94 | if (data.Type >= 3) 95 | { 96 | int effectId = data.Type - 3; 97 | if (data.NoSplat) 98 | { 99 | if (effectId == 1) 100 | { 101 | // powerBeam --> powerBeamNoSplat 102 | effectId = 2; 103 | } 104 | else if (effectId == 92) 105 | { 106 | // powerBeamCharge --> powerBeamChargeNoSplat 107 | effectId = 98; 108 | } 109 | } 110 | scene.SpawnEffect(effectId, data.Transform, entCol: data.EntityCollision); 111 | return null; 112 | } 113 | return scene.InitBeamEffect(data); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/50_HitZone.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MphRead.Formats.Culling; 3 | 4 | namespace MphRead.Entities.Enemies 5 | { 6 | public class Enemy50Entity : EnemyInstanceEntity 7 | { 8 | private readonly EnemyInstanceEntity _enemyOwner; 9 | 10 | public Enemy50Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 11 | : base(data, nodeRef, scene) 12 | { 13 | var owner = data.Spawner as EnemyInstanceEntity; 14 | Debug.Assert(owner != null); 15 | _enemyOwner = owner; 16 | } 17 | 18 | protected override void EnemyProcess() 19 | { 20 | Transform = _enemyOwner.Transform.ClearScale(); 21 | if (_enemyOwner.EnemyType == EnemyType.FireSpawn) 22 | { 23 | var fireSpawn = (Enemy39Entity)_enemyOwner; 24 | ContactDamagePlayer(fireSpawn.Values.ContactDamage, knockback: true); 25 | } 26 | } 27 | 28 | protected override bool EnemyTakeDamage(EntityBase? source) 29 | { 30 | if (_enemyOwner.EnemyType != EnemyType.FireSpawn) 31 | { 32 | Effectiveness eff0 = _enemyOwner.BeamEffectiveness[0]; 33 | Effectiveness eff1 = _enemyOwner.BeamEffectiveness[1]; 34 | Effectiveness eff2 = _enemyOwner.BeamEffectiveness[2]; 35 | Effectiveness eff3 = _enemyOwner.BeamEffectiveness[3]; 36 | Effectiveness eff4 = _enemyOwner.BeamEffectiveness[4]; 37 | Effectiveness eff5 = _enemyOwner.BeamEffectiveness[5]; 38 | Effectiveness eff6 = _enemyOwner.BeamEffectiveness[6]; 39 | Effectiveness eff7 = _enemyOwner.BeamEffectiveness[7]; 40 | Effectiveness eff8 = _enemyOwner.BeamEffectiveness[8]; 41 | _enemyOwner.BeamEffectiveness[0] = BeamEffectiveness[0]; 42 | _enemyOwner.BeamEffectiveness[1] = BeamEffectiveness[1]; 43 | _enemyOwner.BeamEffectiveness[2] = BeamEffectiveness[2]; 44 | _enemyOwner.BeamEffectiveness[3] = BeamEffectiveness[3]; 45 | _enemyOwner.BeamEffectiveness[4] = BeamEffectiveness[4]; 46 | _enemyOwner.BeamEffectiveness[5] = BeamEffectiveness[5]; 47 | _enemyOwner.BeamEffectiveness[6] = BeamEffectiveness[6]; 48 | _enemyOwner.BeamEffectiveness[7] = BeamEffectiveness[7]; 49 | _enemyOwner.BeamEffectiveness[8] = BeamEffectiveness[8]; 50 | _enemyOwner.TakeDamage((uint)(_healthMax - _health), source); 51 | _enemyOwner.BeamEffectiveness[0] = eff0; 52 | _enemyOwner.BeamEffectiveness[1] = eff1; 53 | _enemyOwner.BeamEffectiveness[2] = eff2; 54 | _enemyOwner.BeamEffectiveness[3] = eff3; 55 | _enemyOwner.BeamEffectiveness[4] = eff4; 56 | _enemyOwner.BeamEffectiveness[5] = eff5; 57 | _enemyOwner.BeamEffectiveness[6] = eff6; 58 | _enemyOwner.BeamEffectiveness[7] = eff7; 59 | _enemyOwner.BeamEffectiveness[8] = eff8; 60 | } 61 | return _health > 0; 62 | } 63 | 64 | public void SetUp(ushort health, CollisionVolume hurtVolume, float boundingRadius) 65 | { 66 | _health = _healthMax = health; 67 | _hurtVolumeInit = hurtVolume; 68 | _boundingRadius = boundingRadius; 69 | } 70 | 71 | public override void HandleMessage(MessageInfo info) 72 | { 73 | if (info.Message == Message.SetActive 74 | && info.Sender is EnemyInstanceEntity enemy && enemy.EnemyType == EnemyType.FireSpawn) 75 | { 76 | if (info.Param1 is int value && value == 1) 77 | { 78 | Flags |= EnemyFlags.CollidePlayer; 79 | Flags |= EnemyFlags.CollideBeam; 80 | // todo?: main player slot index for consistency? 81 | HitPlayers[0] = true; 82 | } 83 | else 84 | { 85 | Flags &= ~EnemyFlags.CollidePlayer; 86 | Flags &= ~EnemyFlags.CollideBeam; 87 | ClearHitPlayers(); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/29_GoreaSealSphere1.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Formats.Culling; 2 | using OpenTK.Mathematics; 3 | 4 | namespace MphRead.Entities.Enemies 5 | { 6 | public class Enemy29Entity : GoreaEnemyEntityBase 7 | { 8 | private Enemy28Entity _gorea1B = null!; 9 | private Node _attachNode = null!; 10 | 11 | private int _damage = 0; 12 | public int Damage => _damage; 13 | private int _damageTimer = 0; 14 | public int DamageTimer => _damageTimer; 15 | public ColorRgb Ambient { get; set; } 16 | public ColorRgb Diffuse { get; set; } 17 | 18 | public Enemy29Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 19 | : base(data, nodeRef, scene) 20 | { 21 | } 22 | 23 | protected override void EnemyInitialize() 24 | { 25 | if (_owner is Enemy28Entity owner) 26 | { 27 | _gorea1B = owner; 28 | Flags &= ~EnemyFlags.Visible; 29 | Flags |= EnemyFlags.NoHomingNc; 30 | Flags |= EnemyFlags.NoHomingCo; 31 | Flags |= EnemyFlags.Invincible; 32 | Flags &= ~EnemyFlags.CollidePlayer; 33 | Flags &= ~EnemyFlags.CollideBeam; 34 | Flags |= EnemyFlags.NoMaxDistance; 35 | _state1 = _state2 = 255; 36 | HealthbarMessageId = 7; 37 | _attachNode = owner.GetModels()[0].Model.GetNodeByName("ChestBall1")!; 38 | SetTransform(owner.FacingVector, owner.UpVector, owner.Position); 39 | Position += _attachNode.Position; 40 | _prevPos = Position; 41 | _boundingRadius = 1; 42 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, _owner.Scale.X); 43 | _health = 65535; 44 | _healthMax = 3000; 45 | } 46 | } 47 | 48 | public void Activate() 49 | { 50 | _scanId = Metadata.EnemyScanIds[(int)EnemyType]; 51 | Position = _gorea1B.Position; 52 | Flags &= ~EnemyFlags.Visible; 53 | Flags |= EnemyFlags.CollidePlayer; 54 | Flags |= EnemyFlags.CollideBeam; 55 | Flags |= EnemyFlags.NoHomingNc; 56 | Flags &= ~EnemyFlags.NoHomingCo; 57 | } 58 | 59 | public void Deactivate() 60 | { 61 | _scanId = 0; 62 | Flags &= ~EnemyFlags.Visible; 63 | Flags &= ~EnemyFlags.CollidePlayer; 64 | Flags &= ~EnemyFlags.CollideBeam; 65 | Flags |= EnemyFlags.Invincible; 66 | Flags |= EnemyFlags.NoHomingNc; 67 | Flags |= EnemyFlags.NoHomingCo; 68 | } 69 | 70 | protected override void EnemyProcess() 71 | { 72 | if (_gorea1B.Flags.TestFlag(EnemyFlags.Visible)) 73 | { 74 | Matrix4 transform = GetNodeTransform(_gorea1B, _attachNode); 75 | Position = transform.Row3.Xyz; 76 | } 77 | if (_damageTimer > 0) 78 | { 79 | _damageTimer--; 80 | } 81 | } 82 | 83 | protected override bool EnemyTakeDamage(EntityBase? source) 84 | { 85 | // note: the game does some conversion on Shock Coil damage but doesn't actually change the value 86 | int change = 65535 - _health; 87 | _damage += change; 88 | if (_damage > _healthMax) 89 | { 90 | _damage = _healthMax; 91 | } 92 | int prevDamage = _damage - change; 93 | _health = 65535; 94 | if (!Flags.TestFlag(EnemyFlags.Invincible)) 95 | { 96 | // do SFX and effect only if the damage total has reached a new multiple of 10, 97 | // excluding 1000, from a base of 0/1000/2000 depending on the phase 98 | int damage = _damage; 99 | if (damage / 1000 == prevDamage / 1000) 100 | { 101 | damage %= 1000; 102 | prevDamage %= 1000; 103 | if (damage / 10 > prevDamage / 10) 104 | { 105 | _soundSource.PlaySfx(SfxId.GOREA_1B_DAMAGE); 106 | SpawnEffect(44, Position); // goreaShoulderHits 107 | } 108 | } 109 | _damageTimer = 10 * 2; // todo: FPS stuff 110 | Material material = _gorea1B.GetModels()[0].Model.GetMaterialByName("ChestCore")!; 111 | material.Ambient = Ambient; 112 | material.Diffuse = new ColorRgb(31, 0, 0); 113 | } 114 | return false; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/47_GreaterIthrak.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Formats.Culling; 2 | 3 | namespace MphRead.Entities.Enemies 4 | { 5 | public class Enemy47Entity : Enemy46Entity 6 | { 7 | public Enemy47Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 8 | : base(data, nodeRef, scene) 9 | { 10 | } 11 | 12 | protected override void EnemyInitialize() 13 | { 14 | EnemySpawnEntityData data = _spawner.Data; 15 | Setup(data.Header.Position.ToFloatVector(), data.Header.FacingVector.ToFloatVector(), effectiveness: 0, 16 | data.Fields.S05.Volume0, data.Fields.S05.Volume1, data.Fields.S05.Volume2, data.Fields.S05.Volume3); 17 | } 18 | 19 | protected override void CallSubroutine() 20 | { 21 | CallSubroutine(Metadata.Enemy47Subroutines, this); 22 | } 23 | 24 | protected override void UpdateMouthMaterial() 25 | { 26 | _mouthMaterial.Diffuse = new ColorRgb(14, 14, 14); 27 | } 28 | 29 | #region Boilerplate 30 | 31 | public static bool Behavior00(Enemy47Entity enemy) 32 | { 33 | return enemy.Behavior00(); 34 | } 35 | 36 | public static bool Behavior01(Enemy47Entity enemy) 37 | { 38 | return enemy.Behavior01(); 39 | } 40 | 41 | public static bool Behavior02(Enemy47Entity enemy) 42 | { 43 | return enemy.Behavior02(); 44 | } 45 | 46 | public static bool Behavior03(Enemy47Entity enemy) 47 | { 48 | return enemy.Behavior03(); 49 | } 50 | 51 | public static bool Behavior04(Enemy47Entity enemy) 52 | { 53 | return enemy.Behavior04(); 54 | } 55 | 56 | public static bool Behavior05(Enemy47Entity enemy) 57 | { 58 | return enemy.Behavior05(); 59 | } 60 | 61 | public static bool Behavior06(Enemy47Entity enemy) 62 | { 63 | return enemy.Behavior06(); 64 | } 65 | 66 | public static bool Behavior07(Enemy47Entity enemy) 67 | { 68 | return enemy.Behavior07(); 69 | } 70 | 71 | public static bool Behavior08(Enemy47Entity enemy) 72 | { 73 | return enemy.Behavior08(); 74 | } 75 | 76 | public static bool Behavior09(Enemy47Entity enemy) 77 | { 78 | return enemy.Behavior09(); 79 | } 80 | 81 | public static bool Behavior10(Enemy47Entity enemy) 82 | { 83 | return enemy.Behavior10(); 84 | } 85 | 86 | public static bool Behavior11(Enemy47Entity enemy) 87 | { 88 | return enemy.Behavior11(); 89 | } 90 | 91 | public static bool Behavior12(Enemy47Entity enemy) 92 | { 93 | return enemy.Behavior12(); 94 | } 95 | 96 | public static bool Behavior13(Enemy47Entity enemy) 97 | { 98 | return enemy.Behavior13(); 99 | } 100 | 101 | public static bool Behavior14(Enemy47Entity enemy) 102 | { 103 | return enemy.Behavior14(); 104 | } 105 | 106 | public static bool Behavior15(Enemy47Entity enemy) 107 | { 108 | return enemy.Behavior15(); 109 | } 110 | 111 | public static bool Behavior16(Enemy47Entity enemy) 112 | { 113 | return enemy.Behavior16(); 114 | } 115 | 116 | public static bool Behavior17(Enemy47Entity enemy) 117 | { 118 | return enemy.Behavior17(); 119 | } 120 | 121 | public static bool Behavior18(Enemy47Entity enemy) 122 | { 123 | return enemy.Behavior18(); 124 | } 125 | 126 | public static bool Behavior19(Enemy47Entity enemy) 127 | { 128 | return enemy.Behavior19(); 129 | } 130 | 131 | public static bool Behavior20(Enemy47Entity enemy) 132 | { 133 | return enemy.Behavior20(); 134 | } 135 | 136 | public static bool Behavior21(Enemy47Entity enemy) 137 | { 138 | return enemy.Behavior21(); 139 | } 140 | 141 | public static bool Behavior22(Enemy47Entity enemy) 142 | { 143 | return enemy.Behavior22(); 144 | } 145 | 146 | public static bool Behavior23(Enemy47Entity enemy) 147 | { 148 | return enemy.Behavior23(); 149 | } 150 | 151 | public static bool Behavior24(Enemy47Entity enemy) 152 | { 153 | return enemy.Behavior24(); 154 | } 155 | 156 | public static bool Behavior25(Enemy47Entity enemy) 157 | { 158 | return enemy.Behavior25(); 159 | } 160 | 161 | #endregion 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/40_EnemySpawner.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MphRead.Formats.Collision; 3 | using MphRead.Formats.Culling; 4 | using OpenTK.Mathematics; 5 | 6 | namespace MphRead.Entities.Enemies 7 | { 8 | public class Enemy40Entity : EnemyInstanceEntity 9 | { 10 | public enum SpawnerModelType 11 | { 12 | Spawner, 13 | Nest 14 | } 15 | 16 | private readonly EnemySpawnEntity _spawner; 17 | private byte _animTimer = 0; 18 | private EntityCollision? _parentEntCol = null; 19 | private Matrix4 _invTransform = Matrix4.Identity; 20 | 21 | public SpawnerModelType ModelType { get; private set; } 22 | 23 | public Enemy40Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 24 | : base(data, nodeRef, scene) 25 | { 26 | var spawner = data.Spawner as EnemySpawnEntity; 27 | Debug.Assert(spawner != null); 28 | _spawner = spawner; 29 | } 30 | 31 | // this happens in the spawner's set_entity_refs in-game 32 | protected override void EnemyInitialize() 33 | { 34 | Transform = _data.Spawner.Transform; 35 | if (_data.Spawner is EnemySpawnEntity spawner && spawner.ParentEntCol != null) 36 | { 37 | _parentEntCol = spawner.ParentEntCol; 38 | _invTransform = _transform * spawner.ParentEntCol.Inverse2; 39 | } 40 | _boundingRadius = Fixed.ToFloat(3072); 41 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, _boundingRadius); 42 | _hurtVolume = CollisionVolume.Transform(_hurtVolumeInit, Transform); 43 | _healthMax = _health = _spawner.Data.SpawnerHealth; 44 | Flags |= EnemyFlags.Visible; 45 | if (_parentEntCol == null) 46 | { 47 | Flags |= EnemyFlags.Static; 48 | } 49 | string model = "EnemySpawner"; 50 | if (_spawner.Data.EnemyType == EnemyType.WarWasp || _spawner.Data.EnemyType == EnemyType.BarbedWarWasp) 51 | { 52 | model = "PlantCarnivarous_Pod"; 53 | ModelType = SpawnerModelType.Nest; 54 | } 55 | ModelInstance inst = SetUpModel(model); 56 | if (_spawner.Data.EnemyType != EnemyType.WarWasp && _spawner.Data.EnemyType != EnemyType.BarbedWarWasp) 57 | { 58 | if (_spawner.Flags.TestFlag(SpawnerFlags.Active)) 59 | { 60 | inst.SetAnimation(1); 61 | } 62 | else 63 | { 64 | inst.SetAnimation(2, AnimFlags.Paused | AnimFlags.Ended); 65 | } 66 | } 67 | else 68 | { 69 | inst.SetAnimation(0); 70 | } 71 | _spawner.Flags |= SpawnerFlags.HasModel; 72 | } 73 | 74 | protected override void EnemyProcess() 75 | { 76 | if (_parentEntCol != null) 77 | { 78 | Transform = _invTransform * _parentEntCol.Transform; 79 | } 80 | if (_spawner.Flags.TestFlag(SpawnerFlags.Active) && !_spawner.Flags.TestFlag(SpawnerFlags.Suspended)) 81 | { 82 | Flags &= ~EnemyFlags.Invincible; 83 | } 84 | else 85 | { 86 | Flags |= EnemyFlags.Invincible; 87 | } 88 | if (_spawner.Flags.TestFlag(SpawnerFlags.PlayAnimation)) 89 | { 90 | _spawner.Flags &= ~SpawnerFlags.PlayAnimation; 91 | if (_animTimer == 0) 92 | { 93 | _soundSource.PlaySfx(SfxId.ENEMY_SPAWNER_SPAWN); 94 | } 95 | _animTimer = 15 * 2; // todo: FPS stuff 96 | if (ModelType == SpawnerModelType.Spawner) 97 | { 98 | _models[0].SetAnimation(2, AnimFlags.NoLoop); 99 | } 100 | } 101 | else if (ModelType == SpawnerModelType.Spawner) 102 | { 103 | if (Flags.TestFlag(EnemyFlags.Invincible)) 104 | { 105 | _models[0].SetAnimation(2, AnimFlags.Ended | AnimFlags.Paused); 106 | } 107 | else if (_models[0].AnimInfo.Flags[0].TestFlag(AnimFlags.Ended) && _models[0].AnimInfo.Index[0] != 1) 108 | { 109 | if (_models[0].AnimInfo.Index[0] != 0) 110 | { 111 | _models[0].SetAnimation(1); 112 | } 113 | else 114 | { 115 | // set health to 0 when dying animation finishes 116 | _health = 0; 117 | } 118 | } 119 | } 120 | if (_animTimer > 0) 121 | { 122 | _animTimer--; 123 | } 124 | } 125 | 126 | protected override bool EnemyTakeDamage(EntityBase? source) 127 | { 128 | if (_health == 0 && ModelType == SpawnerModelType.Spawner) 129 | { 130 | // keep health above 0 to finish playing dying animation 131 | if (_models[0].AnimInfo.Index[0] != 0) 132 | { 133 | _models[0].SetAnimation(0, AnimFlags.NoLoop); 134 | } 135 | _health = 1; 136 | } 137 | return false; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/30_Trocra.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using MphRead.Formats; 5 | using MphRead.Formats.Culling; 6 | using OpenTK.Mathematics; 7 | 8 | namespace MphRead.Entities.Enemies 9 | { 10 | public class Enemy30Entity : GoreaEnemyEntityBase 11 | { 12 | private readonly EnemySpawnEntity _spawner = null!; 13 | public Enemy28Entity? Gorea1B { get; set; } 14 | public int Index { get; set; } 15 | public Vector3 Field174 { get; set; } 16 | public int Field184 { get; set; } 17 | public int State { get; set; } 18 | 19 | public Enemy30Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 20 | : base(data, nodeRef, scene) 21 | { 22 | var spawner = data.Spawner as EnemySpawnEntity; 23 | Debug.Assert(spawner != null); 24 | _spawner = spawner; 25 | } 26 | 27 | protected override void EnemyInitialize() 28 | { 29 | InitializeCommon(_spawner); 30 | Flags |= EnemyFlags.OnRadar; 31 | Flags &= ~EnemyFlags.NoHomingCo; 32 | _health = 15; 33 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, 1); 34 | SetUpModel("PowerBomb"); 35 | Gorea1B = null; 36 | } 37 | 38 | protected override void EnemyProcess() 39 | { 40 | if (Flags.TestFlag(EnemyFlags.Visible)) 41 | { 42 | if (_health > 0 && HitPlayers[PlayerEntity.Main.SlotIndex]) 43 | { 44 | DieAndSpawnEffect(164); // goreaCrystalHit 45 | } 46 | if (_health > 0) 47 | { 48 | CollisionResult discard = default; 49 | Vector3 travel = _prevPos - Position; 50 | if (travel.LengthSquared > 1 / 128f 51 | && CollisionDetection.CheckBetweenPoints(_prevPos, Position, TestFlags.Beams, _scene, ref discard)) 52 | { 53 | DieAndSpawnEffect(164); // goreaCrystalHit 54 | } 55 | } 56 | } 57 | } 58 | 59 | private void DieAndSpawnEffect(int effectId) 60 | { 61 | SpawnEffect(effectId, Position); 62 | Vector3 between = PlayerEntity.Main.Position - Position; 63 | float distance = between.Length; 64 | if (distance < 2) // 8192 65 | { 66 | CollisionResult discard = default; 67 | var limitMin = new Vector3(Position.X - 2, Position.Y - 2, Position.Z - 2); 68 | var limitMax = new Vector3(Position.X + 2, Position.Y + 2, Position.Z + 2); 69 | IReadOnlyList candidates 70 | = CollisionDetection.GetCandidatesForLimits(null, Vector3.Zero, 0, limitMin, limitMax, includeEntities: false, _scene); 71 | if (!CollisionDetection.CheckBetweenPoints(candidates, _prevPos, Position, TestFlags.Beams, _scene, ref discard)) 72 | { 73 | int damage = 15; 74 | float force = 1; 75 | if (!HitPlayers[PlayerEntity.Main.SlotIndex]) 76 | { 77 | float factor = Math.Clamp(distance / 2, 0, 1); 78 | damage -= (int)MathF.Round(damage - 15 * factor); 79 | force -= factor; 80 | } 81 | if (distance > 1 / 128f) 82 | { 83 | between = between.Normalized() * force; 84 | } 85 | else 86 | { 87 | between = Vector3.UnitY * force; 88 | } 89 | PlayerEntity.Main.TakeDamage(damage, DamageFlags.NoDmgInvuln, between, this); 90 | } 91 | } 92 | _soundSource.PlaySfx(SfxId.GOREA_ATTACK3B, sourceOnly: true); 93 | _health = 0; 94 | Flags &= ~EnemyFlags.Visible; 95 | Flags &= ~EnemyFlags.CollidePlayer; 96 | Flags &= ~EnemyFlags.CollideBeam; 97 | Flags &= ~EnemyFlags.OnRadar; 98 | Flags |= EnemyFlags.Invincible; 99 | Position = Position.WithY(524288); // 0x7FFFFFFF 100 | _speed = Vector3.Zero; 101 | } 102 | 103 | protected override bool EnemyTakeDamage(EntityBase? source) 104 | { 105 | if (_health == 0) 106 | { 107 | if (Gorea1B != null) 108 | { 109 | _scene.SendMessage(Message.Destroyed, this, Gorea1B, 0, 0); 110 | } 111 | bool spawn = false; 112 | ItemType itemType = ItemType.None; 113 | uint rand = Rng.GetRandomInt2(190); 114 | if (rand < 10) 115 | { 116 | spawn = true; 117 | itemType = ItemType.HealthSmall; 118 | } 119 | else if (rand < 70) 120 | { 121 | spawn = true; 122 | itemType = ItemType.UASmall; 123 | } 124 | if (spawn) 125 | { 126 | int despawnTime = 300 * 2; // todo: FPS stuff 127 | var item = new ItemInstanceEntity(new ItemInstanceEntityData(Position, itemType, despawnTime), NodeRef, _scene); 128 | _scene.AddEntity(item); 129 | } 130 | } 131 | return false; 132 | } 133 | 134 | public void Explode() 135 | { 136 | DieAndSpawnEffect(75); // goreaCrystalExplode 137 | } 138 | 139 | public void SetSpeed(Vector3 speed) 140 | { 141 | _speed = speed; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Players/DynamicLightEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTK.Mathematics; 3 | 4 | namespace MphRead.Entities 5 | { 6 | public class DynamicLightEntityBase : EntityBase 7 | { 8 | protected Vector3 _light1Vector; 9 | protected Vector3 _light1Color; 10 | protected Vector3 _light2Vector; 11 | protected Vector3 _light2Color; 12 | 13 | public Vector3 Light1Vector => _light1Vector; 14 | public Vector3 Light1Color => _light1Color; 15 | public Vector3 Light2Vector => _light2Vector; 16 | public Vector3 Light2Color => _light2Color; 17 | 18 | protected bool _useRoomLights = false; 19 | 20 | public DynamicLightEntityBase(EntityType type, Scene scene) : base(type, scene) 21 | { 22 | } 23 | 24 | private const float _colorStep = 8 / 255f; 25 | 26 | // todo?: FPS stuff 27 | protected void UpdateLightSources(Vector3 position) 28 | { 29 | static float UpdateChannel(float current, float source, float frames) 30 | { 31 | float diff = source - current; 32 | if (MathF.Abs(diff) < _colorStep) 33 | { 34 | return source; 35 | } 36 | int factor; 37 | if (current > source) 38 | { 39 | factor = (int)MathF.Truncate((diff + _colorStep) / (8 * _colorStep)); 40 | if (factor <= -1) 41 | { 42 | return current + (factor - 1) * _colorStep * frames; 43 | } 44 | return current - _colorStep * frames; 45 | } 46 | factor = (int)MathF.Truncate(diff / (8 * _colorStep)); 47 | if (factor >= 1) 48 | { 49 | return current + factor * _colorStep * frames; 50 | } 51 | return current + _colorStep * frames; 52 | } 53 | bool hasLight1 = false; 54 | bool hasLight2 = false; 55 | Vector3 light1Color = _light1Color; 56 | Vector3 light1Vector = _light1Vector; 57 | Vector3 light2Color = _light2Color; 58 | Vector3 light2Vector = _light2Vector; 59 | float frames = _scene.FrameTime * 30; 60 | for (int i = 0; i < _scene.Entities.Count; i++) 61 | { 62 | EntityBase entity = _scene.Entities[i]; 63 | if (entity.Type != EntityType.LightSource) 64 | { 65 | continue; 66 | } 67 | var lightSource = (LightSourceEntity)entity; 68 | if (lightSource.Volume.TestPoint(position)) 69 | { 70 | if (lightSource.Light1Enabled) 71 | { 72 | hasLight1 = true; 73 | light1Vector.X += (lightSource.Light1Vector.X - light1Vector.X) / 8f * frames; 74 | light1Vector.Y += (lightSource.Light1Vector.Y - light1Vector.Y) / 8f * frames; 75 | light1Vector.Z += (lightSource.Light1Vector.Z - light1Vector.Z) / 8f * frames; 76 | light1Color.X = UpdateChannel(light1Color.X, lightSource.Light1Color.X, frames); 77 | light1Color.Y = UpdateChannel(light1Color.Y, lightSource.Light1Color.Y, frames); 78 | light1Color.Z = UpdateChannel(light1Color.Z, lightSource.Light1Color.Z, frames); 79 | } 80 | if (lightSource.Light2Enabled) 81 | { 82 | hasLight2 = true; 83 | light2Vector.X += (lightSource.Light2Vector.X - light2Vector.X) / 8f * frames; 84 | light2Vector.Y += (lightSource.Light2Vector.Y - light2Vector.Y) / 8f * frames; 85 | light2Vector.Z += (lightSource.Light2Vector.Z - light2Vector.Z) / 8f * frames; 86 | light2Color.X = UpdateChannel(light2Color.X, lightSource.Light2Color.X, frames); 87 | light2Color.Y = UpdateChannel(light2Color.Y, lightSource.Light2Color.Y, frames); 88 | light2Color.Z = UpdateChannel(light2Color.Z, lightSource.Light2Color.Z, frames); 89 | } 90 | } 91 | } 92 | if (!hasLight1) 93 | { 94 | light1Vector.X += (_scene.Light1Vector.X - light1Vector.X) / 8f * frames; 95 | light1Vector.Y += (_scene.Light1Vector.Y - light1Vector.Y) / 8f * frames; 96 | light1Vector.Z += (_scene.Light1Vector.Z - light1Vector.Z) / 8f * frames; 97 | light1Color.X = UpdateChannel(light1Color.X, _scene.Light1Color.X, frames); 98 | light1Color.Y = UpdateChannel(light1Color.Y, _scene.Light1Color.Y, frames); 99 | light1Color.Z = UpdateChannel(light1Color.Z, _scene.Light1Color.Z, frames); 100 | } 101 | if (!hasLight2) 102 | { 103 | light2Vector.X += (_scene.Light2Vector.X - light2Vector.X) / 8f * frames; 104 | light2Vector.Y += (_scene.Light2Vector.Y - light2Vector.Y) / 8f * frames; 105 | light2Vector.Z += (_scene.Light2Vector.Z - light2Vector.Z) / 8f * frames; 106 | light2Color.X = UpdateChannel(light2Color.X, _scene.Light2Color.X, frames); 107 | light2Color.Y = UpdateChannel(light2Color.Y, _scene.Light2Color.Y, frames); 108 | light2Color.Z = UpdateChannel(light2Color.Z, _scene.Light2Color.Z, frames); 109 | } 110 | _light1Color = light1Color; 111 | _light1Vector = light1Vector.Normalized(); 112 | _light2Color = light2Color; 113 | _light2Vector = light2Vector.Normalized(); 114 | } 115 | 116 | protected override LightInfo GetLightInfo() 117 | { 118 | if (_useRoomLights) 119 | { 120 | return base.GetLightInfo(); 121 | } 122 | return new LightInfo(_light1Vector, _light1Color, _light2Vector, _light2Color); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/MphRead/Utility/Output.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace MphRead 8 | { 9 | public static class Output 10 | { 11 | public enum Operation 12 | { 13 | Write, 14 | Read, 15 | Clear 16 | } 17 | 18 | private struct QueueItem 19 | { 20 | public Operation Operation; 21 | public string? Message; 22 | 23 | public QueueItem(Operation operation, string? message) 24 | { 25 | Operation = operation; 26 | Message = message; 27 | } 28 | } 29 | 30 | private struct QueueInput 31 | { 32 | public int Count; 33 | public string Message; 34 | 35 | public QueueInput(int count, string message) 36 | { 37 | Count = count; 38 | Message = message; 39 | } 40 | } 41 | 42 | private static bool _initialized = false; 43 | private static readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); 44 | private static readonly SemaphoreSlim _batchLock = new SemaphoreSlim(1, 1); 45 | private static CancellationTokenSource _cts = null!; 46 | private static List _items = null!; 47 | private static List _input = null!; 48 | 49 | public static async Task Begin() 50 | { 51 | await _lock.WaitAsync(); 52 | if (!_initialized) 53 | { 54 | _initialized = true; 55 | _items = new List(); 56 | _input = new List(); 57 | _cts = new CancellationTokenSource(); 58 | _ = Task.Run(async () => 59 | { 60 | CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; 61 | await Run(_cts.Token); 62 | }); 63 | } 64 | _lock.Release(); 65 | } 66 | 67 | private static Guid? _batchGuid = null; 68 | 69 | public static async Task StartBatch() 70 | { 71 | await _batchLock.WaitAsync(); 72 | _batchGuid = Guid.NewGuid(); 73 | return _batchGuid.Value; 74 | } 75 | 76 | public static async Task EndBatch() 77 | { 78 | await _lock.WaitAsync(); 79 | _batchGuid = null; 80 | _batchLock.Release(); 81 | _lock.Release(); 82 | } 83 | 84 | public static async Task Write(Guid? guid = null) 85 | { 86 | await Write("", guid); 87 | } 88 | 89 | public static async Task Write(string message, Guid? guid = null) 90 | { 91 | await _lock.WaitAsync(); 92 | if (guid != _batchGuid) 93 | { 94 | await _batchLock.WaitAsync(); 95 | } 96 | _items.Add(new QueueItem(Operation.Write, message)); 97 | if (guid != _batchGuid) 98 | { 99 | _batchLock.Release(); 100 | } 101 | _lock.Release(); 102 | } 103 | 104 | public static async Task Clear(Guid? guid = null) 105 | { 106 | await _lock.WaitAsync(); 107 | if (guid != _batchGuid) 108 | { 109 | await _batchLock.WaitAsync(); 110 | } 111 | _items.Add(new QueueItem(Operation.Clear, message: null)); 112 | if (guid != _batchGuid) 113 | { 114 | _batchLock.Release(); 115 | } 116 | _lock.Release(); 117 | } 118 | 119 | public static async Task Read(string? message = null, Guid? guid = null) 120 | { 121 | await _lock.WaitAsync(); 122 | if (guid != _batchGuid) 123 | { 124 | await _batchLock.WaitAsync(); 125 | } 126 | _items.Add(new QueueItem(Operation.Read, message)); 127 | int count = _input.Count; 128 | if (guid != _batchGuid) 129 | { 130 | _batchLock.Release(); 131 | } 132 | _lock.Release(); 133 | return await DoRead(count); 134 | } 135 | 136 | private static async Task DoRead(int count) 137 | { 138 | while (true) 139 | { 140 | await _lock.WaitAsync(); 141 | if (_input.Count > 0 && _input[0].Count == count) 142 | { 143 | string input = _input[0].Message; 144 | _input.RemoveAt(0); 145 | _lock.Release(); 146 | return input; 147 | } 148 | _lock.Release(); 149 | await Task.Delay(100); 150 | } 151 | } 152 | 153 | private static async Task Run(CancellationToken token) 154 | { 155 | while (!token.IsCancellationRequested) 156 | { 157 | await _lock.WaitAsync(CancellationToken.None); 158 | while (_items.Count > 0) 159 | { 160 | QueueItem item = _items[0]; 161 | if (item.Operation == Operation.Write) 162 | { 163 | Console.WriteLine(item.Message); 164 | } 165 | else if (item.Operation == Operation.Clear) 166 | { 167 | Console.Clear(); 168 | } 169 | else if (item.Operation == Operation.Read) 170 | { 171 | if (item.Message != null) 172 | { 173 | Console.Write(item.Message); 174 | _input.Add(new QueueInput(_input.Count, Console.ReadLine() ?? "")); 175 | } 176 | } 177 | _items.RemoveAt(0); 178 | } 179 | _lock.Release(); 180 | await Task.Delay(100, CancellationToken.None); 181 | } 182 | } 183 | 184 | public static async Task End() 185 | { 186 | await _lock.WaitAsync(); 187 | if (_initialized) 188 | { 189 | _cts.Cancel(); 190 | } 191 | _lock.Release(); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/MphRead/Entities/ForceFieldEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using MphRead.Entities.Enemies; 4 | using MphRead.Formats; 5 | using MphRead.Sound; 6 | using OpenTK.Mathematics; 7 | 8 | namespace MphRead.Entities 9 | { 10 | public class ForceFieldEntity : EntityBase 11 | { 12 | private readonly ForceFieldEntityData _data; 13 | private Enemy49Entity? _lock; 14 | private readonly Vector3 _upVector; 15 | private readonly Vector3 _facingVector; 16 | private readonly Vector3 _rightVector; 17 | private readonly Vector4 _plane; 18 | private readonly float _width; 19 | private readonly float _height; 20 | private bool _active = false; // todo: start replace/removing the Active property on EntityBase 21 | 22 | public ForceFieldEntityData Data => _data; 23 | public Vector3 FieldUpVector => _upVector; 24 | public Vector3 FieldFacingVector => _facingVector; 25 | public Vector3 FieldRightVector => _rightVector; 26 | public Vector4 Plane => _plane; 27 | public float Width => _width; 28 | public float Height => _height; 29 | 30 | public new bool Active => _active; 31 | public Enemy49Entity? Lock => _lock; 32 | 33 | private static readonly IReadOnlyList _scanIds = new int[10] 34 | { 35 | 0, 294, 295, 291, 290, 292, 293, 296, 0, 267 36 | }; 37 | 38 | public ForceFieldEntity(ForceFieldEntityData data, string nodeName, Scene scene) 39 | : base(EntityType.ForceField, nodeName, scene) 40 | { 41 | _data = data; 42 | Id = data.Header.EntityId; 43 | _upVector = data.Header.UpVector.ToFloatVector(); 44 | _facingVector = data.Header.FacingVector.ToFloatVector(); 45 | _rightVector = Vector3.Cross(_upVector, _facingVector).Normalized(); 46 | _width = data.Width.FloatValue; 47 | _height = data.Height.FloatValue; 48 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 49 | Scale = new Vector3(_width, _height, 1.0f); 50 | Debug.Assert(scene.GameMode == GameMode.SinglePlayer); 51 | int state = GameState.StorySave.InitRoomState(_scene.RoomId, Id, active: _data.Active != 0); 52 | _active = state != 0; 53 | if (_active) 54 | { 55 | _scanId = _scanIds[(int)data.Type]; 56 | } 57 | else 58 | { 59 | Alpha = 0; 60 | } 61 | _plane = new Vector4(_facingVector, Vector3.Dot(_facingVector, Position)); 62 | Recolor = Metadata.DoorPalettes[(int)data.Type]; 63 | ModelInstance inst = SetUpModel("ForceField"); 64 | Read.GetModelInstance("ForceFieldLock"); 65 | inst.SetAnimation(0); 66 | } 67 | 68 | public override void Initialize() 69 | { 70 | base.Initialize(); 71 | if (_active && _data.Type != 9) 72 | { 73 | _lock = EnemySpawnEntity.SpawnEnemy(this, EnemyType.ForceFieldLock, NodeRef, _scene) as Enemy49Entity; 74 | if (_lock != null) 75 | { 76 | _scene.AddEntity(_lock); 77 | } 78 | } 79 | _scene.LoadEffect(77); // deathMech1 80 | } 81 | 82 | public override bool Process() 83 | { 84 | base.Process(); 85 | if (_active) 86 | { 87 | if (Alpha < 1) 88 | { 89 | Alpha += 1f / 31f / 2f; 90 | if (Alpha > 1) 91 | { 92 | Alpha = 1; 93 | } 94 | } 95 | } 96 | else if (Alpha > 0) 97 | { 98 | Alpha -= 1f / 31f / 2f; 99 | if (Alpha < 0) 100 | { 101 | Alpha = 0; 102 | } 103 | } 104 | return true; 105 | } 106 | 107 | public override void HandleMessage(MessageInfo info) 108 | { 109 | if (info.Message == Message.Unlock) 110 | { 111 | if (_lock != null) 112 | { 113 | _lock.SetHealth(0); 114 | } 115 | if (!_scene.Multiplayer) 116 | { 117 | if (CameraSequence.Current == null) 118 | { 119 | PlayerEntity.Main.ForceFieldSfxTimer = 2 / 30f; 120 | } 121 | else if (Sfx.ForceFieldSfxMute == 0 && _soundSource.CountPlayingSfx(SfxId.GEN_OFF) == 0) 122 | { 123 | _soundSource.PlayFreeSfx(SfxId.GEN_OFF); 124 | } 125 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 1); 126 | } 127 | _active = false; 128 | _scanId = 0; 129 | } 130 | else if (info.Message == Message.Lock) 131 | { 132 | if (!_active && !_scene.Multiplayer && CameraSequence.Current != null 133 | && _soundSource.CountPlayingSfx(SfxId.FORCEFIELD_APPEAR) == 0) 134 | { 135 | _soundSource.PlayFreeSfx(SfxId.FORCEFIELD_APPEAR); 136 | } 137 | _active = true; 138 | if (_data.Type == 9) 139 | { 140 | // bugfix?: this is a different result than when first created 141 | _scanId = 0; 142 | } 143 | else 144 | { 145 | _scanId = _scanIds[(int)_data.Type]; 146 | } 147 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 3); 148 | if (_lock == null && _data.Type != 9) 149 | { 150 | _lock = EnemySpawnEntity.SpawnEnemy(this, EnemyType.ForceFieldLock, NodeRef, _scene) as Enemy49Entity; 151 | if (_lock != null) 152 | { 153 | _scene.AddEntity(_lock); 154 | } 155 | } 156 | } 157 | } 158 | 159 | public override void GetDrawInfo() 160 | { 161 | if (Alpha > 0 && IsVisible(NodeRef)) 162 | { 163 | base.GetDrawInfo(); 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/MphRead/Testing/TestOverlay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace MphRead.Testing 7 | { 8 | public static class TestOverlay 9 | { 10 | public static void CompareGames(string game1, string game2) 11 | { 12 | string root1 = Paths.Combine(Path.GetDirectoryName(Paths.FileSystem) ?? "", game1); 13 | string root2 = Paths.Combine(Path.GetDirectoryName(Paths.FileSystem) ?? "", game2); 14 | var dirs1 = Directory.EnumerateDirectories(root1, "", SearchOption.AllDirectories) 15 | .Select(d => d.Replace(root1, "")).ToList(); 16 | var dirs2 = Directory.EnumerateDirectories(root2, "", SearchOption.AllDirectories) 17 | .Select(d => d.Replace(root2, "")).ToList(); 18 | var files1 = new Dictionary>(); 19 | var files2 = new Dictionary>(); 20 | foreach (string dir in dirs1) 21 | { 22 | files1.Add(dir, new List()); 23 | foreach (string file in Directory.EnumerateFiles(Paths.Combine(root1, dir))) 24 | { 25 | files1[dir].Add(Path.GetFileName(file)); 26 | } 27 | } 28 | foreach (string dir in dirs2) 29 | { 30 | files2.Add(dir, new List()); 31 | foreach (string file in Directory.EnumerateFiles(Paths.Combine(root2, dir))) 32 | { 33 | files2[dir].Add(Path.GetFileName(file)); 34 | } 35 | } 36 | var dir1not2 = dirs1.Where(d => !dirs2.Contains(d)).ToList(); 37 | if (dir1not2.Count > 0) 38 | { 39 | Console.WriteLine($"Directories in {game1} not in {game2}:"); 40 | foreach (string dir in dir1not2) 41 | { 42 | Console.WriteLine($"-- {dir}"); 43 | } 44 | Console.WriteLine(); 45 | } 46 | var dir2not1 = dirs2.Where(d => !dirs1.Contains(d)).ToList(); 47 | if (dir2not1.Count > 0) 48 | { 49 | Console.WriteLine($"Directories in {game2} not in {game1}:"); 50 | foreach (string dir in dir2not1) 51 | { 52 | Console.WriteLine($"-- {dir}"); 53 | } 54 | Console.WriteLine(); 55 | } 56 | foreach (string dir in dirs1.Where(d => dirs2.Contains(d))) 57 | { 58 | var file1not2 = files1[dir].Where(d => !files2[dir].Contains(d)).ToList(); 59 | var file2not1 = files2[dir].Where(d => !files1[dir].Contains(d)).ToList(); 60 | if (file1not2.Count > 0 || file2not1.Count > 0) 61 | { 62 | Console.WriteLine(dir); 63 | } 64 | if (file1not2.Count > 0) 65 | { 66 | Console.WriteLine($"Files in {game1} not in {game2}:"); 67 | foreach (string file in file1not2) 68 | { 69 | Console.WriteLine($"-- {file}"); 70 | } 71 | } 72 | if (file2not1.Count > 0) 73 | { 74 | Console.WriteLine($"Files in {game2} not in {game1}:"); 75 | foreach (string file in file2not1) 76 | { 77 | Console.WriteLine($"-- {file}"); 78 | } 79 | } 80 | if (file1not2.Count > 0 || file2not1.Count > 0) 81 | { 82 | Console.WriteLine(); 83 | } 84 | } 85 | foreach (string dir in dirs1.Where(d => dirs2.Contains(d))) 86 | { 87 | var changes = new List(); 88 | foreach (string file in files1[dir].Where(d => files2[dir].Contains(d))) 89 | { 90 | byte[] bytes1 = File.ReadAllBytes(Paths.Combine(root1, dir, file)); 91 | byte[] bytes2 = File.ReadAllBytes(Paths.Combine(root2, dir, file)); 92 | if (!Enumerable.SequenceEqual(bytes1, bytes2)) 93 | { 94 | changes.Add(file); 95 | } 96 | } 97 | if (changes.Count > 0) 98 | { 99 | Console.WriteLine(dir); 100 | Console.WriteLine("Changed files:"); 101 | foreach (string file in changes) 102 | { 103 | Console.WriteLine(file); 104 | } 105 | Console.WriteLine(); 106 | } 107 | } 108 | Nop(); 109 | } 110 | 111 | public static void Translate(int mask) 112 | { 113 | mask = 0x21; 114 | var active = new List(); 115 | for (int i = 0; i < 18; i++) 116 | { 117 | if ((mask & (1 << i)) != 0) 118 | { 119 | active.Add(OverlayMap[i]); 120 | } 121 | } 122 | Console.WriteLine(String.Join(", ", active.OrderBy(a => a))); 123 | Nop(); 124 | } 125 | 126 | private static void Nop() 127 | { 128 | } 129 | 130 | public static readonly IReadOnlyList OverlayMap = new List() 131 | { 132 | /* 0 */ 4, 133 | /* 1 */ 6, 134 | /* 2 */ 17, 135 | /* 3 */ 5, 136 | /* 4 */ 16, 137 | /* 5 */ 0, 138 | /* 6 */ 7, 139 | /* 7 */ 1, 140 | /* 8 */ 2, 141 | /* 9 */ 3, 142 | /* 10 */ 8, 143 | /* 11 */ 15, 144 | /* 12 */ 10, 145 | /* 13 */ 9, 146 | /* 14 */ 11, 147 | /* 15 */ 12, 148 | /* 16 */ 13, 149 | /* 17 */ 14 150 | }; 151 | } 152 | 153 | [Flags] 154 | public enum MphOverlay 155 | { 156 | None = 0x0, 157 | WiFiPlay = 0x1, 158 | DownloadPlay = 0x2, 159 | Bit02 = 0x4, // unused 160 | VoiceChat = 0x8, 161 | Bit04 = 0x10, // unused 162 | Frontend = 0x20, 163 | DownloadStation = 0x40, 164 | Movies = 0x80, 165 | Gameplay = 0x100, 166 | MpEntities = 0x200, 167 | SpEnt1Pause = 0x400, 168 | SpEntities2 = 0x800, 169 | Enemies = 0x1000, 170 | BotAi = 0x2000, 171 | Cretaphid = 0x4000, 172 | Gorea = 0x8000, 173 | Slench = 0x10000, 174 | Bit17 = 0x20000 // unused 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/32_GoreaSealSphere2.cs: -------------------------------------------------------------------------------- 1 | using MphRead.Formats; 2 | using MphRead.Formats.Culling; 3 | using OpenTK.Mathematics; 4 | 5 | namespace MphRead.Entities.Enemies 6 | { 7 | public class Enemy32Entity : GoreaEnemyEntityBase 8 | { 9 | private Enemy31Entity _gorea2 = null!; 10 | private Node _attachNode = null!; 11 | public Node AttachNode => _attachNode; 12 | 13 | private int _damage = 0; 14 | public int Damage { get => _damage; set => _damage = value; } 15 | private int _damageTimer = 0; 16 | public int DamageTimer => _damageTimer; 17 | private bool _visible = false; 18 | private bool _targetable = false; 19 | public bool Visible => _visible; 20 | public bool Targetable => _targetable; 21 | 22 | public Enemy32Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 23 | : base(data, nodeRef, scene) 24 | { 25 | } 26 | 27 | protected override void EnemyInitialize() 28 | { 29 | if (_owner is Enemy31Entity owner) 30 | { 31 | _gorea2 = owner; 32 | Flags &= ~EnemyFlags.Visible; 33 | Flags &= ~EnemyFlags.NoHomingNc; 34 | Flags &= ~EnemyFlags.NoHomingCo; 35 | Flags |= EnemyFlags.Invincible; 36 | Flags |= EnemyFlags.CollidePlayer; 37 | Flags |= EnemyFlags.CollideBeam; 38 | Flags |= EnemyFlags.NoMaxDistance; 39 | _state1 = _state2 = 255; 40 | HealthbarMessageId = 3; 41 | _attachNode = owner.GetModels()[0].Model.GetNodeByName("ChestBall1")!; 42 | SetTransform(owner.FacingVector, owner.UpVector, owner.Position); 43 | Position += _attachNode.Position; 44 | _prevPos = Position; 45 | _boundingRadius = 1; 46 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, _owner.Scale.X); 47 | _health = 65535; 48 | _healthMax = 840; 49 | Metadata.LoadEffectiveness(EnemyType.GoreaSealSphere2, BeamEffectiveness); 50 | } 51 | } 52 | 53 | protected override void EnemyProcess() 54 | { 55 | if (_gorea2.Flags.TestFlag(EnemyFlags.Visible)) 56 | { 57 | Matrix4 transform = GetNodeTransform(_gorea2, _attachNode); 58 | Position = transform.Row3.Xyz; 59 | } 60 | if (_damageTimer > 0) 61 | { 62 | _damageTimer--; 63 | } 64 | // note: the game updates the effectiveness here when damage taken is 720 or greater, 65 | // but the value doesn't seem to change (might have been intended to halve Omega Cannon?) 66 | _targetable = _visible && IsVisible(NodeRef); 67 | } 68 | 69 | public void UpdateVisibility() 70 | { 71 | CollisionResult discard = default; 72 | _visible = !CollisionDetection.CheckBetweenPoints(Position, PlayerEntity.Main.CameraInfo.Position, TestFlags.None, _scene, ref discard); 73 | } 74 | 75 | protected override bool EnemyTakeDamage(EntityBase? source) 76 | { 77 | bool ignoreDamage = false; 78 | bool isOmegaCannon = false; 79 | if (source?.Type == EntityType.BeamProjectile) 80 | { 81 | var beamSource = (BeamProjectileEntity)source; 82 | isOmegaCannon = beamSource.BeamKind == BeamType.OmegaCannon; 83 | } 84 | int damage = 65535 - _health; 85 | if (_damage >= 720 && (!isOmegaCannon || !_gorea2.GoreaFlags.TestFlag(Gorea2Flags.Bit9)) 86 | || source?.Type == EntityType.Bomb) 87 | { 88 | damage = 0; 89 | ignoreDamage = true; 90 | } 91 | if (isOmegaCannon) 92 | { 93 | _gorea2.GoreaFlags |= Gorea2Flags.Bit9; 94 | } 95 | if (Flags.TestFlag(EnemyFlags.Invincible) || _gorea2.Func214080C()) 96 | { 97 | damage = 0; 98 | ignoreDamage = true; 99 | } 100 | else 101 | { 102 | _damage += damage; 103 | _gorea2.UpdatePhase(); 104 | } 105 | _health = 65535; 106 | _gorea2.GoreaFlags &= ~Gorea2Flags.LaserActive; 107 | _soundSource.StopSfx(SfxId.GOREA2_ATTACK1B); 108 | if (_damage >= 840) 109 | { 110 | _gorea2.GoreaFlags |= Gorea2Flags.Bit11; 111 | } 112 | else if (damage != 0 && !Flags.TestFlag(EnemyFlags.Invincible)) 113 | { 114 | bool spawnEffect = false; 115 | if (damage >= 120) 116 | { 117 | spawnEffect = true; 118 | } 119 | else 120 | { 121 | // this returns 32, instead of 8, for 0 (which the game might also do?), but we clamp to 6 anyway 122 | int index = System.Numerics.BitOperations.TrailingZeroCount(_gorea2.Field244); 123 | if (index > 6) 124 | { 125 | index = 6; 126 | } 127 | int diff = _damage - 120 * index; 128 | for (int i = 1; i < 3; i++) 129 | { 130 | if (diff - damage < i * 40 && i * 40 <= diff) 131 | { 132 | spawnEffect = true; 133 | break; 134 | } 135 | } 136 | } 137 | if (spawnEffect) 138 | { 139 | _gorea2.GoreaFlags |= Gorea2Flags.Bit16; 140 | SpawnEffect(44, Position); // goreaShoulderHits 141 | _gorea2.GoreaFlags |= Gorea2Flags.Bit13; 142 | Flags |= EnemyFlags.Invincible; 143 | } 144 | _soundSource.PlaySfx(SfxId.GOREA2_DAMAGE1); 145 | _damageTimer = 10 * 2; // todo: FPS stuff 146 | Material material = _gorea2.GetModels()[0].Model.GetMaterialByName("ChestCore")!; 147 | material.Diffuse = new ColorRgb(31, 0, 0); 148 | } 149 | return ignoreDamage; 150 | } 151 | 152 | public void SetDead() 153 | { 154 | _scanId = 0; 155 | Flags &= ~EnemyFlags.CollidePlayer; 156 | Flags &= ~EnemyFlags.CollideBeam; 157 | Flags |= EnemyFlags.Invincible; 158 | Flags |= EnemyFlags.NoHomingNc; 159 | Flags |= EnemyFlags.NoHomingCo; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/MphRead/Utility/Parser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace MphRead.Utility 8 | { 9 | public static class Parser 10 | { 11 | public static IReadOnlyList ParseBytes(int size, int count, byte[] array) where T : struct 12 | { 13 | Debug.Assert(size == Marshal.SizeOf()); 14 | var results = new List(); 15 | var bytes = new ReadOnlySpan(array); 16 | Debug.Assert(bytes.Length == count * size); 17 | for (int i = 0; i < count; i++) 18 | { 19 | int start = i * size; 20 | int end = start + size; 21 | results.Add(Read.ReadStruct(bytes[start..end])); 22 | } 23 | return results; 24 | } 25 | 26 | public static void ParseFloat(ulong value) 27 | { 28 | double d = BitConverter.ToDouble(BitConverter.GetBytes(value), 0); 29 | Console.WriteLine($"{d} ({d / 4096f})"); 30 | } 31 | 32 | private enum ThingType 33 | { 34 | General, 35 | Boolean, 36 | Enum 37 | } 38 | 39 | private readonly struct Thing 40 | { 41 | public readonly string Name; 42 | public readonly ThingType Type; 43 | public readonly Type? EnumType; 44 | public readonly int BitFrom; 45 | public readonly int BitTo; 46 | 47 | public Thing(int bit, string name, ThingType type) 48 | { 49 | Name = name; 50 | Type = type; 51 | BitFrom = bit; 52 | BitTo = bit; 53 | EnumType = null; 54 | } 55 | 56 | public Thing(int bit, string name, Type enumType) 57 | { 58 | Name = name; 59 | Type = ThingType.Enum; 60 | BitFrom = bit; 61 | BitTo = bit; 62 | EnumType = enumType; 63 | } 64 | 65 | public Thing(int bitFrom, int bitTo, string name, ThingType type) 66 | { 67 | Name = name; 68 | Type = type; 69 | BitFrom = bitFrom; 70 | BitTo = bitTo; 71 | EnumType = null; 72 | } 73 | 74 | public Thing(int bitFrom, int bitTo, string name, Type enumType) 75 | { 76 | Name = name; 77 | Type = ThingType.Enum; 78 | BitFrom = bitFrom; 79 | BitTo = bitTo; 80 | EnumType = enumType; 81 | } 82 | 83 | public string Get(int value) 84 | { 85 | int mask = ~(~0 << (BitTo - BitFrom + 1)); 86 | value = (value >> BitFrom) & mask; 87 | string output = ""; 88 | if (Type == ThingType.Boolean) 89 | { 90 | output = value == 0 ? "No" : "Yes"; 91 | } 92 | else if (Type == ThingType.Enum) 93 | { 94 | output = Enum.ToObject(EnumType!, value).ToString() ?? "?"; 95 | } 96 | else 97 | { 98 | output = value.ToString(); 99 | } 100 | return $"{Name}: {output}"; 101 | } 102 | } 103 | 104 | private static readonly IReadOnlyDictionary> _things = new Dictionary>() 105 | { 106 | { 107 | "POLYGON_ATTR", 108 | new List() 109 | { 110 | new Thing(0, "Light 1", ThingType.Boolean), 111 | new Thing(1, "Light 2", ThingType.Boolean), 112 | new Thing(2, "Light 3", ThingType.Boolean), 113 | new Thing(3, "Light 4", ThingType.Boolean), 114 | new Thing(4, 5, "Polygon mode", typeof(PolygonMode)), 115 | new Thing(6, "Back face", ThingType.Boolean), 116 | new Thing(7, "Front face", ThingType.Boolean), 117 | new Thing(11, "Set new depth", ThingType.Boolean), 118 | new Thing(12, "Render far", ThingType.Boolean), 119 | new Thing(13, "Render 1-dot", ThingType.Boolean), 120 | new Thing(14, "Equal depth test", ThingType.Boolean), 121 | new Thing(15, "Enable fog", ThingType.Boolean), 122 | new Thing(16, 20, "Alpha", ThingType.General), 123 | new Thing(24, 29, "Polygon ID", ThingType.General) 124 | } 125 | } 126 | }; 127 | 128 | public static void MainLoop() 129 | { 130 | while (true) 131 | { 132 | Console.Clear(); 133 | Console.WriteLine($"1: POLYGON_ATTR{Environment.NewLine}x: quit"); 134 | string? type = null; 135 | string input = Console.ReadLine() ?? ""; 136 | if (input == "x" || input == "X") 137 | { 138 | break; 139 | } 140 | if (input == "1") 141 | { 142 | type = "POLYGON_ATTR"; 143 | } 144 | if (type != null) 145 | { 146 | string? output = null; 147 | while (true) 148 | { 149 | Console.Clear(); 150 | if (output != null) 151 | { 152 | Console.WriteLine(output); 153 | Console.WriteLine(); 154 | output = null; 155 | } 156 | Console.Write("Value: "); 157 | string value = Console.ReadLine() ?? ""; 158 | if (value == "x" || value == "X") 159 | { 160 | break; 161 | } 162 | if (value.Length <= 8 && Int32.TryParse(value, NumberStyles.HexNumber, provider: null, out int result)) 163 | { 164 | output = Convert.ToString(result, 2).PadLeft(32, '0'); 165 | foreach (Thing thing in _things[type]) 166 | { 167 | output += Environment.NewLine + thing.Get(result); 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/11_Shriekbat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using MphRead.Effects; 4 | using MphRead.Formats.Culling; 5 | using OpenTK.Mathematics; 6 | 7 | namespace MphRead.Entities.Enemies 8 | { 9 | public class Enemy11Entity : EnemyInstanceEntity 10 | { 11 | private readonly EnemySpawnEntity _spawner; 12 | private CollisionVolume _rangeVolume; 13 | private CollisionVolume _activeVolume; 14 | private Vector3 _targetPos; 15 | private int _moveTimer = 0; 16 | private EffectEntry? _effect = null; 17 | 18 | public Enemy11Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 19 | : base(data, nodeRef, scene) 20 | { 21 | var spawner = data.Spawner as EnemySpawnEntity; 22 | Debug.Assert(spawner != null); 23 | _spawner = spawner; 24 | _stateProcesses = new Action[5] 25 | { 26 | State0, State1, State2, State3, State4 27 | }; 28 | } 29 | 30 | protected override void EnemyInitialize() 31 | { 32 | _health = _healthMax = 12; 33 | Flags |= EnemyFlags.Visible; 34 | Flags |= EnemyFlags.OnRadar; 35 | Vector3 position = _spawner.Data.Header.Position.ToFloatVector(); 36 | SetTransform((PlayerEntity.Main.Position - position).Normalized(), Vector3.UnitY, position); 37 | _boundingRadius = 1; 38 | _hurtVolumeInit = new CollisionVolume(_spawner.Data.Fields.S02.Volume0); 39 | _rangeVolume = CollisionVolume.Move(_spawner.Data.Fields.S02.Volume1, position); 40 | _activeVolume = CollisionVolume.Move(_spawner.Data.Fields.S02.Volume2, position); 41 | _targetPos = position + _spawner.Data.Fields.S02.PathVector.ToFloatVector(); 42 | SetUpModel(Metadata.EnemyModelNames[11], animIndex: 4); 43 | } 44 | 45 | private void Die() 46 | { 47 | TakeDamage(100, source: null); 48 | if (_effect != null) 49 | { 50 | _scene.UnlinkEffectEntry(_effect); 51 | _effect = null; 52 | } 53 | } 54 | 55 | protected override void EnemyProcess() 56 | { 57 | Vector3 facing = (PlayerEntity.Main.Position - Position).Normalized(); 58 | SetTransform(facing, Vector3.UnitY, Position); 59 | if (_effect != null) 60 | { 61 | _effect.Transform(Position, Transform); 62 | } 63 | if (ContactDamagePlayer(20, knockback: false)) 64 | { 65 | Die(); 66 | } 67 | if (_state1 == 4 && HandleBlockingCollision(Position, _hurtVolume, updateSpeed: true)) 68 | { 69 | Die(); 70 | } 71 | CallStateProcess(); 72 | } 73 | 74 | // todo: really no need for these to be separate functions 75 | private void State0() 76 | { 77 | CallSubroutine(Metadata.Enemy11Subroutines, this); 78 | } 79 | 80 | private void State1() 81 | { 82 | State0(); 83 | } 84 | 85 | private void State2() 86 | { 87 | State0(); 88 | } 89 | 90 | private void State3() 91 | { 92 | State0(); 93 | } 94 | 95 | private void State4() 96 | { 97 | State0(); 98 | } 99 | 100 | // attacking 101 | private bool Behavior00() 102 | { 103 | return false; 104 | } 105 | 106 | // start attack (moving toward player) 107 | private bool Behavior01() 108 | { 109 | if (_moveTimer > 0) 110 | { 111 | _moveTimer--; 112 | return false; 113 | } 114 | Vector3 target = -PlayerEntity.Main.FacingVector + PlayerEntity.Main.Position; 115 | target.Y = PlayerEntity.Main.Position.Y + 0.5f; 116 | _speed = target - Position; 117 | float mag = _speed.Length; 118 | // _moveTimer is not used again after this 119 | _moveTimer = (int)(mag / 0.6f) + 1; 120 | _moveTimer *= 2; // todo: FPS stuff 121 | _speed *= 0.6f / mag; 122 | _speed /= 2; // todo: FPS stuff 123 | _soundSource.PlaySfx(SfxId.SHRIEKBAT_ATTACK); 124 | _models[0].SetAnimation(1); 125 | return true; 126 | } 127 | 128 | // pause before attack 129 | private bool Behavior02() 130 | { 131 | if (_moveTimer > 0) 132 | { 133 | _moveTimer--; 134 | return false; 135 | } 136 | _models[0].SetAnimation(3); 137 | _moveTimer = 20 * 2; // todo: FPS stuff 138 | _speed = Vector3.Zero; 139 | return true; 140 | } 141 | 142 | // start moving down to starting position for attack 143 | private bool Behavior03() 144 | { 145 | if (!_activeVolume.TestPoint(PlayerEntity.Main.Position)) 146 | { 147 | return false; 148 | } 149 | _effect = _scene.SpawnEffectGetEntry(29, Vector3.UnitX, Vector3.UnitY, Position); // shriekBatTrail 150 | _effect?.SetElementExtension(true); 151 | _speed = _targetPos - Position; 152 | float mag = _speed.Length; 153 | _moveTimer = (int)(mag / 0.3f) + 1; 154 | _moveTimer *= 2; // todo: FPS stuff 155 | _speed *= 0.3f / mag; 156 | _speed /= 2; // todo: FPS stuff 157 | _models[0].SetAnimation(2); 158 | _soundSource.PlaySfx(SfxId.SHRIEKBAT_PRE_ATTACK_SCR); 159 | return true; 160 | } 161 | 162 | // wait for player to be in range 163 | private bool Behavior04() 164 | { 165 | if (!_rangeVolume.TestPoint(PlayerEntity.Main.Position)) 166 | { 167 | return false; 168 | } 169 | _models[0].SetAnimation(0); 170 | return true; 171 | } 172 | 173 | public override void Destroy() 174 | { 175 | if (_effect != null) 176 | { 177 | _scene.UnlinkEffectEntry(_effect); 178 | _effect = null; 179 | } 180 | base.Destroy(); 181 | } 182 | 183 | #region Boilerplate 184 | 185 | public static bool Behavior00(Enemy11Entity enemy) 186 | { 187 | return enemy.Behavior00(); 188 | } 189 | 190 | public static bool Behavior01(Enemy11Entity enemy) 191 | { 192 | return enemy.Behavior01(); 193 | } 194 | 195 | public static bool Behavior02(Enemy11Entity enemy) 196 | { 197 | return enemy.Behavior02(); 198 | } 199 | 200 | public static bool Behavior03(Enemy11Entity enemy) 201 | { 202 | return enemy.Behavior03(); 203 | } 204 | 205 | public static bool Behavior04(Enemy11Entity enemy) 206 | { 207 | return enemy.Behavior04(); 208 | } 209 | 210 | #endregion 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/16_Blastcap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using MphRead.Formats.Culling; 4 | using OpenTK.Mathematics; 5 | 6 | namespace MphRead.Entities.Enemies 7 | { 8 | public class Enemy16Entity : EnemyInstanceEntity 9 | { 10 | private readonly EnemySpawnEntity _spawner; 11 | private ushort _agitateTimer = 0; 12 | private ushort _cloudTick = 0; 13 | private ushort _cloudTimer = 0; 14 | private bool _initialCloudHit = false; 15 | 16 | private const float _nearRadius = 8; 17 | private const float _cloudRadius = 2; 18 | 19 | public Enemy16Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 20 | : base(data, nodeRef, scene) 21 | { 22 | var spawner = data.Spawner as EnemySpawnEntity; 23 | Debug.Assert(spawner != null); 24 | _spawner = spawner; 25 | _stateProcesses = new Action[4] 26 | { 27 | State0, State1, State2, State3 28 | }; 29 | } 30 | 31 | protected override void EnemyInitialize() 32 | { 33 | EntityDataHeader header = _spawner.Data.Header; 34 | SetTransform(header.FacingVector.ToFloatVector(), Vector3.UnitY, header.Position.ToFloatVector()); 35 | _health = _healthMax = 12; 36 | Flags |= EnemyFlags.Visible; 37 | Flags |= EnemyFlags.OnRadar; 38 | _agitateTimer = 60 * 2; // todo: FPS stuff 39 | _boundingRadius = 1; 40 | _hurtVolumeInit = new CollisionVolume(_spawner.Data.Fields.S00.Volume0); 41 | SetUpModel(Metadata.EnemyModelNames[16], animIndex: 2); 42 | _cloudTimer = 150 * 2; // todo: FPS stuff 43 | } 44 | 45 | protected override void EnemyProcess() 46 | { 47 | CallStateProcess(); 48 | } 49 | 50 | protected override bool EnemyTakeDamage(EntityBase? source) 51 | { 52 | if (_health == 0) 53 | { 54 | _health = 1; 55 | _state2 = 3; 56 | _subId = _state2; 57 | Flags |= EnemyFlags.Invincible; 58 | Flags &= ~EnemyFlags.Visible; 59 | Flags &= ~EnemyFlags.CollidePlayer; 60 | Flags &= ~EnemyFlags.CollideBeam; 61 | _scene.SpawnEffect(4, Vector3.UnitX, Vector3.UnitY, Position); // blastCapBlow 62 | if (!_initialCloudHit) 63 | { 64 | float radii = PlayerEntity.Main.Volume.SphereRadius + _cloudRadius; 65 | if ((Position - PlayerEntity.Main.Volume.SpherePosition).LengthSquared < radii * radii) 66 | { 67 | _initialCloudHit = true; 68 | PlayerEntity.Main.TakeDamage(2, DamageFlags.NoDmgInvuln, null, this); 69 | } 70 | } 71 | } 72 | return false; 73 | } 74 | 75 | public void State0() 76 | { 77 | CallSubroutine(Metadata.Enemy16Subroutines, this); 78 | } 79 | 80 | public void State1() 81 | { 82 | State0(); 83 | } 84 | 85 | public void State2() 86 | { 87 | State0(); 88 | } 89 | 90 | public void State3() 91 | { 92 | _cloudTick++; 93 | State0(); 94 | } 95 | 96 | private bool Behavior00() 97 | { 98 | if (_cloudTick % (10 * 2) != 0) // todo: FPS stuff 99 | { 100 | return false; 101 | } 102 | float radii = PlayerEntity.Main.Volume.SphereRadius + _cloudRadius; 103 | if ((Position - PlayerEntity.Main.Volume.SpherePosition).LengthSquared < radii * radii) 104 | { 105 | _initialCloudHit = true; 106 | PlayerEntity.Main.TakeDamage(2, DamageFlags.None, null, this); 107 | return true; 108 | } 109 | return false; 110 | } 111 | 112 | private bool Behavior01() 113 | { 114 | if (_cloudTimer > 0) 115 | { 116 | _cloudTimer--; 117 | } 118 | else 119 | { 120 | _health = 0; 121 | } 122 | return false; 123 | } 124 | 125 | private bool Behavior02() 126 | { 127 | float radii = PlayerEntity.Main.Volume.SphereRadius + _nearRadius; 128 | if ((Position - PlayerEntity.Main.Volume.SpherePosition).LengthSquared < radii * radii) 129 | { 130 | return false; 131 | } 132 | _models[0].SetAnimation(2); 133 | return true; 134 | } 135 | 136 | private bool Behavior03() 137 | { 138 | if (!HitPlayers[PlayerEntity.Main.SlotIndex]) 139 | { 140 | return false; 141 | } 142 | _initialCloudHit = true; 143 | PlayerEntity.Main.TakeDamage(2, DamageFlags.None, null, this); 144 | TakeDamage(100, source: null); 145 | return true; 146 | } 147 | 148 | private bool Behavior04() 149 | { 150 | if (!_models[0].AnimInfo.Flags[0].TestFlag(AnimFlags.Ended)) 151 | { 152 | return false; 153 | } 154 | _models[0].SetAnimation(2); 155 | return true; 156 | } 157 | 158 | private bool Behavior05() 159 | { 160 | float radii = PlayerEntity.Main.Volume.SphereRadius + _nearRadius; 161 | if ((Position - PlayerEntity.Main.Volume.SpherePosition).LengthSquared < radii * radii) 162 | { 163 | _models[0].SetAnimation(1); 164 | return true; 165 | } 166 | return false; 167 | } 168 | 169 | private bool Behavior06() 170 | { 171 | if (_agitateTimer > 0) 172 | { 173 | _agitateTimer--; 174 | return false; 175 | } 176 | _soundSource.PlaySfx(SfxId.BLASTCAP_AGITATE); 177 | _agitateTimer = 60 * 2; // todo: FPS stuff 178 | _models[0].SetAnimation(0, AnimFlags.NoLoop); 179 | return true; 180 | } 181 | 182 | #region Boilerplate 183 | 184 | public static bool Behavior00(Enemy16Entity enemy) 185 | { 186 | return enemy.Behavior00(); 187 | } 188 | 189 | public static bool Behavior01(Enemy16Entity enemy) 190 | { 191 | return enemy.Behavior01(); 192 | } 193 | 194 | public static bool Behavior02(Enemy16Entity enemy) 195 | { 196 | return enemy.Behavior02(); 197 | } 198 | 199 | public static bool Behavior03(Enemy16Entity enemy) 200 | { 201 | return enemy.Behavior03(); 202 | } 203 | 204 | public static bool Behavior04(Enemy16Entity enemy) 205 | { 206 | return enemy.Behavior04(); 207 | } 208 | 209 | public static bool Behavior05(Enemy16Entity enemy) 210 | { 211 | return enemy.Behavior05(); 212 | } 213 | 214 | public static bool Behavior06(Enemy16Entity enemy) 215 | { 216 | return enemy.Behavior06(); 217 | } 218 | 219 | #endregion 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/MphRead/paths.txt 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | [Aa][Rr][Mm]/ 26 | [Aa][Rr][Mm]64/ 27 | bld/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | [Ll]og/ 31 | 32 | # Visual Studio 2015/2017 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # Visual Studio 2017 auto generated files 38 | Generated\ Files/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # Benchmark Results 54 | BenchmarkDotNet.Artifacts/ 55 | 56 | # .NET Core 57 | project.lock.json 58 | project.fragment.lock.json 59 | artifacts/ 60 | 61 | # StyleCop 62 | StyleCopReport.xml 63 | 64 | # Files built by Visual Studio 65 | *_i.c 66 | *_p.c 67 | *_h.h 68 | *.ilk 69 | *.meta 70 | *.obj 71 | *.iobj 72 | *.pch 73 | *.pdb 74 | *.ipdb 75 | *.pgc 76 | *.pgd 77 | *.rsp 78 | *.sbr 79 | *.tlb 80 | *.tli 81 | *.tlh 82 | *.tmp 83 | *.tmp_proj 84 | *_wpftmp.csproj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.svclog 91 | *.scc 92 | 93 | # Chutzpah Test files 94 | _Chutzpah* 95 | 96 | # Visual C++ cache files 97 | ipch/ 98 | *.aps 99 | *.ncb 100 | *.opendb 101 | *.opensdf 102 | *.sdf 103 | *.cachefile 104 | *.VC.db 105 | *.VC.VC.opendb 106 | 107 | # Visual Studio profiler 108 | *.psess 109 | *.vsp 110 | *.vspx 111 | *.sap 112 | 113 | # Visual Studio Trace Files 114 | *.e2e 115 | 116 | # TFS 2012 Local Workspace 117 | $tf/ 118 | 119 | # Guidance Automation Toolkit 120 | *.gpState 121 | 122 | # ReSharper is a .NET coding add-in 123 | _ReSharper*/ 124 | *.[Rr]e[Ss]harper 125 | *.DotSettings.user 126 | 127 | # JustCode is a .NET coding add-in 128 | .JustCode 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # The packages folder can be ignored because of Package Restore 188 | **/[Pp]ackages/* 189 | # except build/, which is used as an MSBuild target. 190 | !**/[Pp]ackages/build/ 191 | # Uncomment if necessary however generally it will be regenerated when needed 192 | #!**/[Pp]ackages/repositories.config 193 | # NuGet v3's project.json files produces more ignorable files 194 | *.nuget.props 195 | *.nuget.targets 196 | 197 | # Microsoft Azure Build Output 198 | csx/ 199 | *.build.csdef 200 | 201 | # Microsoft Azure Emulator 202 | ecf/ 203 | rcf/ 204 | 205 | # Windows Store app package directories and files 206 | AppPackages/ 207 | BundleArtifacts/ 208 | Package.StoreAssociation.xml 209 | _pkginfo.txt 210 | *.appx 211 | 212 | # Visual Studio cache files 213 | # files ending in .cache can be ignored 214 | *.[Cc]ache 215 | # but keep track of directories ending in .cache 216 | !?*.[Cc]ache/ 217 | 218 | # Others 219 | ClientBin/ 220 | ~$* 221 | *~ 222 | *.dbmdl 223 | *.dbproj.schemaview 224 | *.jfm 225 | *.pfx 226 | *.publishsettings 227 | orleans.codegen.cs 228 | 229 | # Including strong name files can present a security risk 230 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 231 | #*.snk 232 | 233 | # Since there are multiple workflows, uncomment next line to ignore bower_components 234 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 235 | #bower_components/ 236 | 237 | # RIA/Silverlight projects 238 | Generated_Code/ 239 | 240 | # Backup & report files from converting an old project file 241 | # to a newer Visual Studio version. Backup files are not needed, 242 | # because we have git ;-) 243 | _UpgradeReport_Files/ 244 | Backup*/ 245 | UpgradeLog*.XML 246 | UpgradeLog*.htm 247 | ServiceFabricBackup/ 248 | *.rptproj.bak 249 | 250 | # SQL Server files 251 | *.mdf 252 | *.ldf 253 | *.ndf 254 | 255 | # Business Intelligence projects 256 | *.rdl.data 257 | *.bim.layout 258 | *.bim_*.settings 259 | *.rptproj.rsuser 260 | *- Backup*.rdl 261 | 262 | # Microsoft Fakes 263 | FakesAssemblies/ 264 | 265 | # GhostDoc plugin setting file 266 | *.GhostDoc.xml 267 | 268 | # Node.js Tools for Visual Studio 269 | .ntvs_analysis.dat 270 | node_modules/ 271 | 272 | # Visual Studio 6 build log 273 | *.plg 274 | 275 | # Visual Studio 6 workspace options file 276 | *.opt 277 | 278 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 279 | *.vbw 280 | 281 | # Visual Studio LightSwitch build output 282 | **/*.HTMLClient/GeneratedArtifacts 283 | **/*.DesktopClient/GeneratedArtifacts 284 | **/*.DesktopClient/ModelManifest.xml 285 | **/*.Server/GeneratedArtifacts 286 | **/*.Server/ModelManifest.xml 287 | _Pvt_Extensions 288 | 289 | # Paket dependency manager 290 | .paket/paket.exe 291 | paket-files/ 292 | 293 | # FAKE - F# Make 294 | .fake/ 295 | 296 | # JetBrains Rider 297 | .idea/ 298 | *.sln.iml 299 | 300 | # CodeRush personal settings 301 | .cr/personal 302 | 303 | # Python Tools for Visual Studio (PTVS) 304 | __pycache__/ 305 | *.pyc 306 | 307 | # Cake - Uncomment if you are using it 308 | # tools/** 309 | # !tools/packages.config 310 | 311 | # Tabs Studio 312 | *.tss 313 | 314 | # Telerik's JustMock configuration file 315 | *.jmconfig 316 | 317 | # BizTalk build output 318 | *.btp.cs 319 | *.btm.cs 320 | *.odx.cs 321 | *.xsd.cs 322 | 323 | # OpenCover UI analysis results 324 | OpenCover/ 325 | 326 | # Azure Stream Analytics local run output 327 | ASALocalRun/ 328 | 329 | # MSBuild Binary and Structured Log 330 | *.binlog 331 | 332 | # NVidia Nsight GPU debugger configuration file 333 | *.nvuser 334 | 335 | # MFractors (Xamarin productivity tool) working folder 336 | .mfractor/ 337 | 338 | # Local History for Visual Studio 339 | .localhistory/ 340 | 341 | # BeatPulse healthcheck temp database 342 | healthchecksdb -------------------------------------------------------------------------------- /src/MphRead/Formats/Frontend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace MphRead.Formats 8 | { 9 | // size: 20 10 | public readonly struct FrontendHeader 11 | { 12 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] 13 | public readonly char[] Type; // MARM 14 | public readonly ushort Field4; 15 | public readonly byte Field6; 16 | public readonly byte Field7; 17 | public readonly uint Offfset1; // null-terminated list of MenuStruct1* 18 | public readonly uint Offfset2; // ntl of MenuStruct2* 19 | public readonly uint Offfset3; 20 | } 21 | 22 | // size: 32 23 | public readonly struct MenuStruct1 24 | { 25 | // 021E87F8 - offset 4 in use 26 | public readonly uint Field0; 27 | public readonly uint Offset1; // unused in metroidhunters.bin 28 | public readonly uint Offset2; // ntl of MenuStruct1A* 29 | public readonly uint Offset3; // ntl of MenuStruct1A1* 30 | public readonly uint Offset4; // ntl of MenuStruct1B* 31 | public readonly ushort Field14; // index into this struct's list of MenuStruct1A* 32 | public readonly ushort Field16; 33 | public readonly uint Field18; 34 | public readonly byte Index; // position in the list of MenuStruct1 35 | public readonly byte Field1D; 36 | public readonly byte Field1E; 37 | public readonly byte Flags; 38 | } 39 | 40 | // size: 60 41 | public readonly struct MenuStruct1A 42 | { 43 | public readonly uint Offset1; // unused in metroidhunters.bin 44 | public readonly uint Offset2; // ntl of MenuStruct1A1* 45 | public readonly uint Offset3; // ntl of MenuStruct1A5* 46 | public readonly uint Offset4; // ntl of MenuStruct1A2* 47 | public readonly uint Field10; // runtime single pointer to a MenuStruct1A2 48 | public readonly uint Field14; 49 | public readonly uint Field18; 50 | public readonly uint Field1C; // (?) runtime single pointer, maybe? 51 | public readonly uint Field20; 52 | public readonly uint Field24; 53 | public readonly uint Field28; 54 | public readonly uint Offset5; // MenuStruct1A6 -- only set for one MenuStruct1A in metroidhunters.bin (22140BC) 55 | public readonly uint Offset6; // pointer to list of ints, can be external 56 | public readonly byte Field34; 57 | public readonly byte Field35; 58 | public readonly ushort Field36; 59 | public readonly byte Flags; 60 | public readonly byte Field39; 61 | public readonly ushort Field3A; 62 | } 63 | 64 | // size: 4 65 | public readonly struct MenuStruct1A5 66 | { 67 | public readonly byte Field0; 68 | public readonly byte Field1; 69 | public readonly ushort Field2; // index into the list of MenuStruct1A* on the parent of MenuStruct1 of the parent MenuStruct1A 70 | } 71 | 72 | // size: 44 73 | public readonly struct MenuStruct1A6 74 | { 75 | public readonly int Field0; 76 | public readonly int Field4; 77 | public readonly int Field8; 78 | public readonly int FieldC; 79 | public readonly int Field10; 80 | public readonly int Field14; 81 | public readonly int Field18; 82 | public readonly int Field1C; 83 | public readonly int Field20; 84 | public readonly int Field24; 85 | public readonly int Field28; 86 | } 87 | 88 | // size: 24 89 | public readonly struct MenuStruct1A1 90 | { 91 | public readonly ushort Field0; 92 | public readonly byte Field2; 93 | public readonly byte Flags; 94 | public readonly uint Field4; 95 | public readonly uint Field8; 96 | public readonly uint FieldC; 97 | public readonly uint Offset1; // ntl of pointers to int pairs -- passed to call_pair_func_ptr 98 | public readonly ushort Field14; 99 | public readonly byte Field16; 100 | public readonly byte Field17; 101 | } 102 | 103 | // size: 8 104 | public readonly struct MenuStruct1A2 105 | { 106 | public readonly byte Field0; // if 1, Offset1 is converted to MenuStruct1A3* 107 | public readonly byte Field1; 108 | public readonly ushort Field2; 109 | public readonly uint Offset1; // else, this is a ushort index into the MenuStruct2* list (if not 0xFFFF) and another ushort value (flags?) 110 | } 111 | 112 | // size: 4 113 | public readonly struct MenuStruct1A3 114 | { 115 | public readonly uint Field0; 116 | public readonly uint Offset1; // MenuStruct1A4* 117 | } 118 | 119 | // size: 24 120 | public readonly struct MenuStruct1A4 121 | { 122 | public readonly int Field0; 123 | public readonly int Field4; 124 | public readonly int Field8; 125 | public readonly int FieldC; 126 | public readonly int Field10; 127 | public readonly byte Flags; 128 | public readonly byte Field15; 129 | public readonly ushort Field16; 130 | } 131 | 132 | // size: 32 133 | public readonly struct MenuStruct1B 134 | { 135 | public readonly uint Field0; 136 | public readonly MenuStruct1A1 Struct1A1; 137 | public readonly byte Flags; 138 | public readonly byte Field1D; 139 | public readonly ushort Field1E; 140 | } 141 | 142 | // size: 12 143 | public readonly struct MenuStruct2 144 | { 145 | public readonly uint Field0; 146 | public readonly uint Field4; // CModel* 147 | public readonly uint Offset1; // ntl of pairs of MenuStruct2A* -- first for model, second for animation 148 | } 149 | 150 | // size: 12 151 | public readonly struct MenuStruct2A 152 | { 153 | public readonly byte Field0; 154 | public readonly byte FilenameLength; // includes terminator and 0xBB padding to multiple of 4 bytes 155 | public readonly ushort Field2; 156 | public readonly uint Field4; 157 | public readonly uint FilenameOffset; // char* (becomes pointer to model/animation in download play version) 158 | } 159 | 160 | public static class Frontend 161 | { 162 | public static void Parse() 163 | { 164 | // todo: parse DP version (different header) 165 | //string path = @"D:\Cdrv\MPH\_FS\amhe1\frontend\single_metroidhunters.bin"; 166 | string path = @"D:\Cdrv\MPH\_FS\amhe1\frontend\metroidhunters.bin"; 167 | var bytes = new ReadOnlySpan(File.ReadAllBytes(path)); 168 | FrontendHeader header = Read.ReadStruct(bytes); 169 | Debug.Assert(header.Type.MarshalString() == "MARM"); 170 | var list1 = new List(); 171 | foreach (uint offset in Read.DoListNullEnd(bytes, header.Offfset1)) 172 | { 173 | MenuStruct1 item = Read.DoOffset(bytes, offset); 174 | Debug.Assert(item.Offset1 == 0); 175 | list1.Add(item); 176 | foreach (uint subOffset in Read.DoListNullEnd(bytes, item.Offset2)) 177 | { 178 | Console.WriteLine(subOffset); 179 | MenuStruct1A subItem = Read.DoOffset(bytes, subOffset); 180 | Debug.Assert(subItem.Offset1 == 0); 181 | } 182 | } 183 | Nop(); 184 | } 185 | 186 | private static void Nop() 187 | { 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/MphRead/Formats/NodeData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using OpenTK.Mathematics; 8 | 9 | namespace MphRead.Formats 10 | { 11 | public static class ReadNodeData 12 | { 13 | public static void TestAll() 14 | { 15 | foreach (string path in Directory.EnumerateFiles(Paths.Combine(Paths.FileSystem, @"levels\nodeData"))) 16 | { 17 | if (!path.EndsWith(@"levels\nodeData\unit2_Land_Node.bin")) // todo: version 4 18 | { 19 | ReadData(Paths.Combine(@"levels\nodeData", Path.GetFileName(path))); 20 | } 21 | } 22 | Nop(); 23 | } 24 | 25 | public static NodeData ReadData(string path) 26 | { 27 | var bytes = new ReadOnlySpan(File.ReadAllBytes(Paths.Combine(Paths.FileSystem, path))); 28 | ushort version = Read.SpanReadUshort(bytes, 0); 29 | if (version == 0) 30 | { 31 | // todo: this 32 | ReadFhNodeData(bytes); 33 | return null!; 34 | } 35 | // todo: version 4 (unit2_Land_Node.bin) 36 | if (version != 6) 37 | { 38 | throw new ProgramException($"Unexpected node data version {version}."); 39 | } 40 | NodeDataHeader header = Read.ReadStruct(bytes); 41 | int shortCount = header.SomeCount; 42 | if (shortCount == 0) 43 | { 44 | Debug.Assert(header.Offset1 == header.Offset0 + 2); 45 | shortCount = 1; 46 | } 47 | var types = new HashSet(); 48 | uint min = UInt32.MaxValue; 49 | IReadOnlyList shorts = Read.DoOffsets(bytes, header.Offset0, shortCount); 50 | var data = new List>>(); 51 | IReadOnlyList str1s = Read.DoOffsets(bytes, header.Offset1, header.Count); 52 | foreach (NodeDataStruct1 str1 in str1s) 53 | { 54 | var sub = new List>(); 55 | IReadOnlyList str2s = Read.DoOffsets(bytes, str1.Offset2, str1.Count); 56 | foreach (NodeDataStruct2 str2 in str2s) 57 | { 58 | IReadOnlyList str3s = Read.DoOffsets(bytes, str2.Offset3, str2.Count); 59 | foreach (NodeDataStruct3 str3 in str3s) 60 | { 61 | min = Math.Min(min, str3.Offset1); 62 | types.Add(str3.Field0); 63 | } 64 | sub.Add(str3s); 65 | } 66 | data.Add(sub); 67 | } 68 | int indexCount = (int)((bytes.Length - min) / 2); 69 | IReadOnlyList indices = Read.DoOffsets(bytes, min, indexCount); 70 | var cast = new List>>(); 71 | foreach (IReadOnlyList> sub in data) 72 | { 73 | var newSub = new List>(); 74 | foreach (IReadOnlyList str3s in sub) 75 | { 76 | var cast3s = new List(); 77 | foreach (NodeDataStruct3 str3 in str3s) 78 | { 79 | int index1 = (int)((str3.Offset1 - min) / 2); 80 | int index2 = (int)((str3.Offset2 - min) / 2); 81 | cast3s.Add(new NodeData3(str3, index1, index2)); 82 | } 83 | newSub.Add(cast3s); 84 | } 85 | cast.Add(newSub); 86 | } 87 | var nodeData = new NodeData(header, shorts, indices, cast); 88 | return nodeData; 89 | } 90 | 91 | private static void ReadFhNodeData(ReadOnlySpan bytes) 92 | { 93 | ushort count = Read.SpanReadUshort(bytes, 2); 94 | IReadOnlyList headers = Read.DoOffsets(bytes, 4, (uint)count); 95 | if (headers.Any(h => h.Field0 == 1)) 96 | { 97 | Debugger.Break(); 98 | } 99 | } 100 | 101 | private static void Nop() 102 | { 103 | } 104 | } 105 | 106 | public class NodeData 107 | { 108 | public NodeDataHeader Header { get; } 109 | public IReadOnlyList Shorts { get; } 110 | public IReadOnlyList Indices { get; } 111 | public IReadOnlyList>> Data { get; } 112 | 113 | public NodeData(NodeDataHeader header, IReadOnlyList shorts, IReadOnlyList indices, 114 | IReadOnlyList>> data) 115 | { 116 | Header = header; 117 | Shorts = shorts; 118 | Indices = indices; 119 | Data = data; 120 | } 121 | } 122 | 123 | public class NodeData3 124 | { 125 | public ushort Field0 { get; } 126 | public ushort Field2 { get; } 127 | public uint Field4 { get; } 128 | public Vector3 Position { get; } 129 | public uint Field14 { get; } 130 | public int Index1 { get; } 131 | public int Index2 { get; } 132 | 133 | public Matrix4 Transform { get; } 134 | public Vector4 Color { get; } 135 | 136 | private static readonly IReadOnlyList _nodeDataColors = new List() 137 | { 138 | new Vector4(1, 0, 0, 1), // 0 - red 139 | new Vector4(0, 1, 0, 1), // 1 - green 140 | new Vector4(0, 0, 1, 1), // 2 - blue 141 | new Vector4(0, 1, 1, 1), // 3 - cyan 142 | new Vector4(1, 0, 1, 1), // 4 - magenta 143 | new Vector4(1, 1, 0, 1), // 5 - yellow 144 | }; 145 | 146 | public NodeData3(NodeDataStruct3 raw, int index1, int index2) 147 | { 148 | Field0 = raw.Field0; 149 | Field2 = raw.Field2; 150 | Field4 = raw.Field4; 151 | Position = raw.Position.ToFloatVector(); 152 | Field14 = raw.Field14; 153 | Index1 = index1; 154 | Index2 = index2; 155 | Transform = Matrix4.CreateTranslation(Position); 156 | Color = _nodeDataColors[Field0]; 157 | } 158 | } 159 | 160 | // size: 14 161 | [StructLayout(LayoutKind.Sequential, Pack = 2)] 162 | public readonly struct NodeDataHeader 163 | { 164 | public readonly ushort Version; 165 | public readonly ushort Count; 166 | public readonly uint Offset0; 167 | public readonly uint Offset1; 168 | public readonly ushort SomeCount; 169 | } 170 | 171 | // size: 8 172 | public readonly struct NodeDataStruct1 173 | { 174 | public readonly uint Offset2; 175 | public readonly ushort Count; 176 | public readonly ushort Field6; // always 0x5C 177 | } 178 | 179 | // size: 8 180 | public readonly struct NodeDataStruct2 181 | { 182 | public readonly uint Offset3; 183 | public readonly ushort Count; 184 | public readonly ushort Field6; // always 0x5C 185 | } 186 | 187 | // size: 36 188 | public readonly struct NodeDataStruct3 189 | { 190 | public readonly ushort Field0; 191 | public readonly ushort Field2; 192 | public readonly uint Field4; 193 | public readonly Vector3Fx Position; 194 | public readonly uint Field14; 195 | public readonly uint Offset1; 196 | public readonly uint Offset2; 197 | public readonly uint Offset3; // always 0 198 | } 199 | 200 | // size: 24 201 | public readonly struct FhNodeData 202 | { 203 | public readonly ushort Field0; 204 | public readonly ushort Field2; 205 | public readonly uint Field4; 206 | public readonly uint Field8; 207 | public readonly uint FieldC; 208 | public readonly uint Field10; 209 | public readonly uint Field14; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/04_Petrasyl2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using MphRead.Formats.Culling; 4 | using OpenTK.Mathematics; 5 | 6 | namespace MphRead.Entities.Enemies 7 | { 8 | public class Enemy04Entity : EnemyInstanceEntity 9 | { 10 | private readonly EnemySpawnEntity _spawner; 11 | 12 | private Vector3 _initialPos; 13 | private Vector3 _field188; 14 | private Vector3 _field194; 15 | private float _weaveOffset = 0; 16 | private float _bobAngle = 0; 17 | private float _bobOffset = 0; 18 | private float _bobSpeed = 0; 19 | private float _weaveAngle = 0; 20 | private ushort _field170 = 0; 21 | 22 | public Enemy04Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 23 | : base(data, nodeRef, scene) 24 | { 25 | var spawner = data.Spawner as EnemySpawnEntity; 26 | Debug.Assert(spawner != null); 27 | _spawner = spawner; 28 | _stateProcesses = new Action[2] 29 | { 30 | State0, State1 31 | }; 32 | } 33 | 34 | protected override void EnemyInitialize() 35 | { 36 | _health = _healthMax = 8; 37 | Flags |= EnemyFlags.Visible; 38 | Flags |= EnemyFlags.NoHomingNc; 39 | Flags |= EnemyFlags.Invincible; 40 | Flags |= EnemyFlags.OnRadar; 41 | _boundingRadius = 0.5f; 42 | _hurtVolumeInit = new CollisionVolume(_spawner.Data.Fields.S04.Volume0); 43 | SetUpModel(Metadata.EnemyModelNames[4]); 44 | _models[0].SetAnimation(0, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node, AnimFlags.NoLoop); 45 | _models[0].SetAnimation(2, slot: 1, SetFlags.Texcoord); 46 | Vector3 facing = _spawner.Data.Header.FacingVector.ToFloatVector().Normalized(); 47 | Vector3 position = _spawner.Data.Fields.S04.Position.ToFloatVector() + _spawner.Data.Header.Position.ToFloatVector(); 48 | position = position.AddY(Fixed.ToFloat(5461)); 49 | SetTransform(facing, Vector3.UnitY, position); 50 | _initialPos = position; 51 | _weaveOffset = Fixed.ToFloat(_spawner.Data.Fields.S04.WeaveOffset); 52 | _field188 = facing; 53 | _field194 = facing; 54 | _bobOffset = Fixed.ToFloat(Rng.GetRandomInt2(0x1AAB) + 1365) / 2; // [0.1667, 1) 55 | _bobSpeed = Fixed.ToFloat(Rng.GetRandomInt2(0x6000)) + 1; // [1, 7) 56 | UpdateState(); 57 | } 58 | 59 | private void UpdateState() 60 | { 61 | if (_state2 == 0) 62 | { 63 | // finish teleporting in 64 | _models[0].SetAnimation(0, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node); 65 | Flags &= ~EnemyFlags.NoHomingNc; 66 | Flags &= ~EnemyFlags.Invincible; 67 | } 68 | else if (_state2 == 1) 69 | { 70 | // begin teleporting in 71 | _models[0].SetAnimation(5, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node, AnimFlags.NoLoop); 72 | _field170 = 30 * 2; // todo: FPS stuff 73 | _soundSource.PlaySfx(SfxId.MOCHTROID_TELEPORT_IN); 74 | } 75 | } 76 | 77 | private void UpdateMovement() 78 | { 79 | _soundSource.PlaySfx(SfxId.MOCHTROID_FLY, loop: true); 80 | _field188 = _spawner.Position - Position; 81 | _field188 = new Vector3(-_field188.Z, 0, _field188.X); 82 | if (_field188 != Vector3.Zero) 83 | { 84 | _field188 = _field188.Normalized(); 85 | } 86 | else 87 | { 88 | _field188 = FacingVector; 89 | } 90 | int animFrame = _models[0].AnimInfo.Frame[0]; 91 | if (_state1 != 0) 92 | { 93 | _weaveAngle += 1.5f / 2; // todo: FPS stuff 94 | } 95 | else 96 | { 97 | _weaveAngle += (1.5f * animFrame + (30 - animFrame)) / 30f / 2; // todo: FPS stuff 98 | } 99 | if (_weaveAngle >= 360) 100 | { 101 | _weaveAngle -= 360; 102 | } 103 | float angle = MathHelper.DegreesToRadians(_weaveAngle); 104 | float xzSin = MathF.Sin(angle); 105 | float xzCos = MathF.Cos(angle); 106 | if (_state1 != 0) 107 | { 108 | _speed.X = _initialPos.X + xzSin * _weaveOffset - Position.X; 109 | _speed.Z = _initialPos.Z + xzCos * _weaveOffset - Position.Z; 110 | } 111 | else 112 | { 113 | _speed.X = _initialPos.X + xzSin * (_weaveOffset * animFrame / 30) - Position.X; 114 | _speed.Z = _initialPos.Z + xzCos * (_weaveOffset * animFrame / 30) - Position.Z; 115 | } 116 | _bobAngle += _bobSpeed / 2; // todo: FPS stuff 117 | if (_bobAngle >= 360) 118 | { 119 | _bobAngle -= 360; 120 | } 121 | float ySin = MathF.Sin(MathHelper.DegreesToRadians(_bobAngle)); 122 | _speed.Y = _initialPos.Y + ySin * _bobOffset - Position.Y; 123 | _speed /= 2; // todo: FPS stuff 124 | Vector3 between = PlayerEntity.Main.Position - Position; 125 | if (between.LengthSquared >= 7 * 7) 126 | { 127 | _field194 = _field188.WithY(0).Normalized(); 128 | } 129 | else 130 | { 131 | _field194 = between.WithY(0).Normalized(); 132 | } 133 | Vector3 prevFacing = FacingVector; 134 | Vector3 newFacing = prevFacing; 135 | // todo: FPS stuff 136 | newFacing.X += (_field194.X - prevFacing.X) / 8 / 2; 137 | newFacing.Z += (_field194.Z - prevFacing.Z) / 8 / 2; 138 | if (newFacing.X == 0 && newFacing.Z == 0) 139 | { 140 | newFacing = prevFacing; 141 | } 142 | Debug.Assert(newFacing != Vector3.Zero); 143 | newFacing = newFacing.Normalized(); 144 | if (MathF.Abs(newFacing.X - prevFacing.X) < 1 / 4096f && MathF.Abs(newFacing.Z - prevFacing.Z) < 1 / 4096f) 145 | { 146 | newFacing.X += 0.125f / 2; 147 | newFacing.Z -= 0.125f / 2; 148 | if (newFacing.X == 0 && newFacing.Z == 0) 149 | { 150 | newFacing.X += 0.125f / 2; 151 | newFacing.Z -= 0.125f / 2; 152 | } 153 | newFacing = newFacing.Normalized(); 154 | } 155 | SetTransform(newFacing, UpVector, Position); 156 | } 157 | 158 | protected override void EnemyProcess() 159 | { 160 | CallStateProcess(); 161 | } 162 | 163 | private void State0() 164 | { 165 | UpdateMovement(); 166 | if (CallSubroutine(Metadata.Enemy04Subroutines, this)) 167 | { 168 | UpdateState(); 169 | } 170 | } 171 | 172 | private void State1() 173 | { 174 | UpdateMovement(); 175 | if (HitPlayers[PlayerEntity.Main.SlotIndex]) 176 | { 177 | PlayerEntity.Main.TakeDamage(12, DamageFlags.None, FacingVector, this); 178 | } 179 | AnimationInfo animInfo = _models[0].AnimInfo; 180 | if (animInfo.Index[0] != 0 && animInfo.Flags[0].TestFlag(AnimFlags.Ended)) 181 | { 182 | _models[0].SetAnimation(0, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node); 183 | } 184 | } 185 | 186 | private bool Behavior00() 187 | { 188 | return false; 189 | } 190 | 191 | private bool Behavior01() 192 | { 193 | if (_field170 == 0) 194 | { 195 | return true; 196 | } 197 | _field170--; 198 | return false; 199 | } 200 | 201 | #region Boilerplate 202 | 203 | public static bool Behavior00(Enemy04Entity enemy) 204 | { 205 | return enemy.Behavior00(); 206 | } 207 | 208 | public static bool Behavior01(Enemy04Entity enemy) 209 | { 210 | return enemy.Behavior01(); 211 | } 212 | 213 | #endregion 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/MphRead/Export/Images.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using MphRead.Hud; 10 | using OpenTK.Graphics.OpenGL; 11 | using SixLabors.ImageSharp; 12 | using SixLabors.ImageSharp.Formats.Png; 13 | using SixLabors.ImageSharp.PixelFormats; 14 | using SixLabors.ImageSharp.Processing; 15 | 16 | namespace MphRead.Export 17 | { 18 | public static class Images 19 | { 20 | private static Task? _task = null; 21 | private static readonly ConcurrentQueue<(Image, string)> _queue = new ConcurrentQueue<(Image, string)>(); 22 | private static readonly PngEncoder _encoderUncomp = new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }; 23 | private static readonly PngEncoder _encoderComp = new PngEncoder() { CompressionLevel = PngCompressionLevel.BestSpeed }; 24 | 25 | public static void Screenshot(int width, int height, string? name = null) 26 | { 27 | byte[] buffer = new byte[width * height * 4]; 28 | GL.ReadPixels(0, 0, width, height, PixelFormat.Rgba, PixelType.UnsignedByte, buffer); 29 | for (int i = 3; i < buffer.Length; i += 4) 30 | { 31 | buffer[i] = 255; 32 | } 33 | using var image = Image.LoadPixelData(buffer, width, height); 34 | image.Mutate(m => RotateFlipExtensions.RotateFlip(m, RotateMode.None, FlipMode.Vertical)); 35 | string path = Paths.Combine(Paths.Export, "_screenshots"); 36 | Directory.CreateDirectory(path); 37 | name ??= DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString(); 38 | image.SaveAsPng(Paths.Combine(path, $"{name}.png"), _encoderComp); 39 | } 40 | 41 | public static void Record(int width, int height, string name) 42 | { 43 | if (_task == null) 44 | { 45 | _task = Task.Run(async () => await ProcessQueue()); 46 | } 47 | byte[] buffer = ArrayPool.Shared.Rent(width * height * 4); 48 | GL.ReadPixels(0, 0, width, height, PixelFormat.Rgba, PixelType.UnsignedByte, buffer); 49 | for (int i = 3; i < width * height * 4; i += 4) 50 | { 51 | buffer[i] = 255; 52 | } 53 | var image = Image.LoadPixelData(buffer, width, height); 54 | ArrayPool.Shared.Return(buffer); 55 | _queue.Enqueue((image, name)); 56 | } 57 | 58 | private static async Task ProcessQueue() 59 | { 60 | while (true) 61 | { 62 | while (_queue.TryDequeue(out (Image Image, string Name) result)) 63 | { 64 | result.Image.Mutate(m => RotateFlipExtensions.RotateFlip(m, RotateMode.None, FlipMode.Vertical)); 65 | string path = Paths.Combine(Paths.Export, "_screenshots"); 66 | Directory.CreateDirectory(path); 67 | await result.Image.SaveAsPngAsync(Paths.Combine(path, $"{result.Name}.png"), _encoderUncomp); 68 | result.Image.Dispose(); 69 | } 70 | await Task.Delay(15); 71 | } 72 | } 73 | 74 | public static void ExportImages(Model model) 75 | { 76 | string exportPath = Paths.Combine(Paths.Export, model.Name); 77 | foreach (Recolor recolor in model.Recolors) 78 | { 79 | string colorPath = Paths.Combine(exportPath, recolor.Name); 80 | Directory.CreateDirectory(colorPath); 81 | var usedTextures = new HashSet(); 82 | int id = 0; 83 | var usedCombos = new HashSet<(int, int)>(); 84 | 85 | void DoTexture(int textureId, int paletteId) 86 | { 87 | if (textureId == -1 || usedCombos.Contains((textureId, paletteId))) 88 | { 89 | return; 90 | } 91 | Texture texture = recolor.Textures[textureId]; 92 | IReadOnlyList pixels = recolor.GetPixels(textureId, paletteId); 93 | if (texture.Width == 0 || texture.Height == 0 || pixels.Count == 0) 94 | { 95 | return; 96 | } 97 | Debug.Assert(texture.Width * texture.Height == pixels.Count); 98 | usedTextures.Add(textureId); 99 | usedCombos.Add((textureId, paletteId)); 100 | string filename = $"{textureId}-{paletteId}"; 101 | if (id > 0) 102 | { 103 | filename = $"anim__{id.ToString().PadLeft(3, '0')}"; 104 | } 105 | SaveTexture(colorPath, filename, texture.Width, texture.Height, pixels); 106 | } 107 | 108 | foreach (Material material in model.Materials.OrderBy(m => m.TextureId).ThenBy(m => m.PaletteId)) 109 | { 110 | DoTexture(material.TextureId, material.PaletteId); 111 | } 112 | id = 1; 113 | usedCombos.Clear(); 114 | foreach (TextureAnimationGroup group in model.AnimationGroups.Texture) 115 | { 116 | foreach (TextureAnimation animation in group.Animations.Values) 117 | { 118 | for (int i = animation.StartIndex; i < animation.StartIndex + animation.Count; i++) 119 | { 120 | DoTexture(group.TextureIds[i], group.PaletteIds[i]); 121 | id++; 122 | } 123 | } 124 | } 125 | if (usedTextures.Count != recolor.Textures.Count) 126 | { 127 | string unusedPath = Paths.Combine(colorPath, "unused"); 128 | Directory.CreateDirectory(unusedPath); 129 | for (int t = 0; t < recolor.Textures.Count; t++) 130 | { 131 | if (usedTextures.Contains(t)) 132 | { 133 | continue; 134 | } 135 | Texture texture = recolor.Textures[t]; 136 | for (int p = 0; p < recolor.Palettes.Count; p++) 137 | { 138 | IReadOnlyList textureData = recolor.TextureData[t]; 139 | IReadOnlyList palette = recolor.PaletteData[p]; 140 | if (textureData.Any(t => t.Data >= palette.Count)) 141 | { 142 | continue; 143 | } 144 | IReadOnlyList pixels = recolor.GetPixels(t, p); 145 | string filename = $"{t}-{p}"; 146 | SaveTexture(unusedPath, filename, texture.Width, texture.Height, pixels); 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | public static void ExportPalettes(Model model) 154 | { 155 | string exportPath = Paths.Combine(Paths.Export, model.Name); 156 | foreach (Recolor recolor in model.Recolors) 157 | { 158 | string palettePath = Paths.Combine(exportPath, recolor.Name, "palettes"); 159 | Directory.CreateDirectory(palettePath); 160 | for (int p = 0; p < recolor.Palettes.Count; p++) 161 | { 162 | IReadOnlyList pixels = recolor.GetPalettePixels(p); 163 | string filename = $"p{p}"; 164 | SaveTexture(palettePath, filename, 16, 16, pixels); 165 | } 166 | } 167 | } 168 | 169 | public static void SaveTexture(string directory, string filename, ushort width, ushort height, IReadOnlyList pixels) 170 | { 171 | string imagePath = Paths.Combine(directory, $"{filename}.png"); 172 | using var image = new Image(width, height); 173 | for (int p = 0; p < pixels.Count; p++) 174 | { 175 | ColorRgba pixel = pixels[p]; 176 | image[p % width, p / width] = new Rgba32(pixel.Red, pixel.Green, pixel.Blue, pixel.Alpha); 177 | } 178 | image.SaveAsPng(imagePath); 179 | } 180 | 181 | public static void ExportHudLayers() 182 | { 183 | HudInfo.TestLayers(exportScreens: true); 184 | } 185 | 186 | public static void ExportHudObjects() 187 | { 188 | HudInfo.TestObjects(null, 0, 0, 0, 0, export: true); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/MphRead/Entities/JumpPadEntity.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Mathematics; 2 | 3 | namespace MphRead.Entities 4 | { 5 | public class JumpPadEntity : EntityBase 6 | { 7 | private readonly JumpPadEntityData _data; 8 | private readonly Matrix4 _beamTransform; 9 | private readonly Vector3 _beamVector; 10 | private CollisionVolume _volume; 11 | private Vector3 _prevPos; 12 | 13 | private bool _invSetUp = false; 14 | private EntityBase? _parent = null; 15 | private Vector3 _invPos; 16 | 17 | private ushort _cooldownTimer = 0; 18 | 19 | public JumpPadEntity(JumpPadEntityData data, string nodeName, Scene scene) 20 | : base(EntityType.JumpPad, nodeName, scene) 21 | { 22 | _data = data; 23 | Id = data.Header.EntityId; 24 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 25 | _prevPos = Position; 26 | _volume = CollisionVolume.Move(_data.Volume, Position); 27 | string modelName = Metadata.JumpPads[(int)data.ModelId]; 28 | SetUpModel(modelName); 29 | ModelInstance beamInst = SetUpModel("JumpPad_Beam"); 30 | Vector3 beamVector = data.BeamVector.ToFloatVector().Normalized(); 31 | _beamTransform = GetTransformMatrix(beamVector, beamVector.X != 0 || beamVector.Z != 0 ? Vector3.UnitY : Vector3.UnitX); 32 | _beamTransform.Row3.Y = 0.25f; 33 | _beamVector = Matrix.Vec3MultMtx3(beamVector, Transform) * _data.Speed.FloatValue; 34 | if (_scene.GameMode == GameMode.SinglePlayer) 35 | { 36 | Active = GameState.StorySave.InitRoomState(_scene.RoomId, Id, active: data.Active != 0) != 0; 37 | } 38 | else 39 | { 40 | Active = data.Active != 0; 41 | } 42 | beamInst.Active = Active; 43 | } 44 | 45 | public override void Initialize() 46 | { 47 | base.Initialize(); 48 | if (_data.ParentId != -1) 49 | { 50 | if (_scene.TryGetEntity(_data.ParentId, out EntityBase? parent)) 51 | { 52 | _parent = parent; 53 | } 54 | } 55 | } 56 | 57 | public override bool GetTargetable() 58 | { 59 | return false; 60 | } 61 | 62 | public override bool Process() 63 | { 64 | if (_parent != null) 65 | { 66 | if (!_invSetUp) 67 | { 68 | _invPos = Matrix.Vec3MultMtx4(Position, _parent.CollisionTransform.Inverted()); 69 | _invSetUp = true; 70 | } 71 | Position = Matrix.Vec3MultMtx4(_invPos, _parent.CollisionTransform); 72 | } 73 | if (_prevPos != Position) 74 | { 75 | _volume = CollisionVolume.Move(_data.Volume, Position); 76 | _prevPos = Position; 77 | } 78 | if (Active && _cooldownTimer == 0) 79 | { 80 | for (int i = 0; i < _scene.Entities.Count; i++) 81 | { 82 | EntityBase entity = _scene.Entities[i]; 83 | if (entity.Type != EntityType.Player) 84 | { 85 | continue; 86 | } 87 | var player = (PlayerEntity)entity; 88 | if (player.Health == 0) 89 | { 90 | continue; 91 | } 92 | Vector3 position; 93 | if (player.IsAltForm) 94 | { 95 | if (!_data.TriggerFlags.TestFlag(TriggerFlags.PlayerAlt)) 96 | { 97 | continue; 98 | } 99 | position = player.Volume.SpherePosition; 100 | } 101 | else 102 | { 103 | if (!_data.TriggerFlags.TestFlag(TriggerFlags.PlayerBiped)) 104 | { 105 | continue; 106 | } 107 | position = player.Position; 108 | } 109 | if (_volume.TestPoint(position)) 110 | { 111 | player.ActivateJumpPad(this, _beamVector, _data.ControlLockTime); 112 | _cooldownTimer = (ushort)(_data.CooldownTime * 2); // todo: FPS stuff 113 | } 114 | } 115 | } 116 | if (_cooldownTimer > 0) 117 | { 118 | _cooldownTimer--; 119 | } 120 | return base.Process(); 121 | } 122 | 123 | public override void HandleMessage(MessageInfo info) 124 | { 125 | if (info.Message == Message.Activate) 126 | { 127 | Active = true; 128 | if (_scene.GameMode == GameMode.SinglePlayer) 129 | { 130 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 3); 131 | } 132 | } 133 | else if (info.Message == Message.SetActive) 134 | { 135 | if ((int)info.Param1 != 0) 136 | { 137 | Active = true; 138 | if (_scene.GameMode == GameMode.SinglePlayer) 139 | { 140 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 3); 141 | } 142 | } 143 | else 144 | { 145 | Active = false; 146 | if (_scene.GameMode == GameMode.SinglePlayer) 147 | { 148 | GameState.StorySave.SetRoomState(_scene.RoomId, Id, state: 1); 149 | } 150 | } 151 | } 152 | _models[1].Active = Active; 153 | } 154 | 155 | protected override Matrix4 GetModelTransform(ModelInstance inst, int index) 156 | { 157 | if (index == 1) 158 | { 159 | return Matrix4.CreateScale(inst.Model.Scale) * _beamTransform * _transform; 160 | } 161 | return base.GetModelTransform(inst, index); 162 | } 163 | 164 | public override void GetDrawInfo() 165 | { 166 | if (IsVisible(NodeRef)) 167 | { 168 | base.GetDrawInfo(); 169 | } 170 | } 171 | 172 | public override void GetDisplayVolumes() 173 | { 174 | if (_scene.ShowVolumes == VolumeDisplay.JumpPad) 175 | { 176 | AddVolumeItem(_volume, Vector3.UnitY); 177 | } 178 | } 179 | 180 | public override void SetActive(bool active) 181 | { 182 | base.SetActive(active); 183 | _models[1].Active = Active; 184 | } 185 | } 186 | 187 | public class FhJumpPadEntity : EntityBase 188 | { 189 | private readonly FhJumpPadEntityData _data; 190 | private readonly Matrix4 _beamTransform; 191 | 192 | private readonly CollisionVolume _volume; 193 | 194 | public FhJumpPadEntity(FhJumpPadEntityData data, Scene scene) : base(EntityType.JumpPad, scene) 195 | { 196 | _data = data; 197 | Id = data.Header.EntityId; 198 | SetTransform(data.Header.FacingVector, data.Header.UpVector, data.Header.Position); 199 | _volume = CollisionVolume.Move(_data.ActiveVolume, Position); 200 | string name = data.ModelId == 1 ? "balljump" : "jumppad_base"; 201 | SetUpModel(name, firstHunt: true); 202 | name = data.ModelId == 1 ? "balljump_ray" : "jumppad_ray"; 203 | SetUpModel(name, firstHunt: true); 204 | Vector3 beamVector = data.BeamVector.ToFloatVector().Normalized(); 205 | _beamTransform = GetTransformMatrix(beamVector, beamVector.X != 0 || beamVector.Z != 0 ? Vector3.UnitY : Vector3.UnitX); 206 | } 207 | 208 | protected override Matrix4 GetModelTransform(ModelInstance inst, int index) 209 | { 210 | if (index == 1) 211 | { 212 | // FH beam vectors are absolute, so don't include the parent rotation 213 | // todo: it would be nicer to just compute beamTransform as relative on creation 214 | Matrix4 transform = Matrix4.CreateScale(inst.Model.Scale) * _beamTransform; 215 | transform.Row3.Xyz = Position.AddY(0.25f); 216 | return transform; 217 | } 218 | return base.GetModelTransform(inst, index); 219 | } 220 | 221 | public override void GetDisplayVolumes() 222 | { 223 | if (_scene.ShowVolumes == VolumeDisplay.JumpPad) 224 | { 225 | AddVolumeItem(_volume, Vector3.UnitY); 226 | } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/01_Zoomer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using MphRead.Formats; 4 | using MphRead.Formats.Culling; 5 | using OpenTK.Mathematics; 6 | 7 | namespace MphRead.Entities.Enemies 8 | { 9 | public class Enemy01Entity : EnemyInstanceEntity 10 | { 11 | private readonly EnemySpawnEntity _spawner; 12 | private EnemySpawnEntityData SpawnData => _spawner.Data; 13 | private EnemySpawnFields00 SpawnFields => _spawner.Data.Fields.S00; 14 | private Vector3 _direction; 15 | private float _angleInc = 0; 16 | private float _curAngle = 0; 17 | private float _maxAngle = 0; 18 | private float _angleCos = 0; 19 | private Vector3 _intendedDir; 20 | // sktodo: implement vector visualization with line loops and use it to classify these fields 21 | private Vector3 _field1A0; 22 | private Vector3 _field1AC; 23 | private int _volumeCheckDelay = 0; 24 | private bool _seekingVolume = false; 25 | private CollisionVolume _homeVolume; 26 | 27 | public Enemy01Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 28 | : base(data, nodeRef, scene) 29 | { 30 | // technically this enemy has one state function and one behavior, but they're no-ops 31 | var spawner = data.Spawner as EnemySpawnEntity; 32 | Debug.Assert(spawner != null); 33 | _spawner = spawner; 34 | } 35 | 36 | protected override void EnemyInitialize() 37 | { 38 | var facing = new Vector3(Rng.GetRandomInt2(4096) / 4096f, 0, Rng.GetRandomInt2(4096) / 4096f); 39 | if (facing.X == 0 && facing.Z == 0) 40 | { 41 | facing = _spawner.Transform.Row2.Xyz; 42 | } 43 | facing = facing.Normalized(); // the game doesn't do this 44 | var up = new Vector3(0, _spawner.Transform.Row1.Y, 0); 45 | SetTransform(facing, up, _spawner.Position); 46 | Flags |= EnemyFlags.Visible; 47 | Flags |= EnemyFlags.OnRadar; 48 | _health = _healthMax = 12; 49 | _boundingRadius = 0.25f; 50 | _hurtVolumeInit = new CollisionVolume(SpawnFields.Volume0); 51 | SetUpModel(Metadata.EnemyModelNames[1]); 52 | _field1A0 = _field1AC = up; 53 | _angleInc = Fixed.ToFloat(Rng.GetRandomInt2(0x3000)) + 3; 54 | _angleInc /= 2; // todo: FPS stuff 55 | _maxAngle = Fixed.ToFloat(Rng.GetRandomInt2(0)) + 40; 56 | _angleCos = MathF.Cos(MathHelper.DegreesToRadians(_angleInc)); 57 | _homeVolume = CollisionVolume.Move(SpawnFields.Volume1, _spawner.Data.Header.Position.ToFloatVector()); 58 | _direction = Vector3.Cross(facing, up).Normalized(); 59 | } 60 | 61 | protected override void EnemyProcess() 62 | { 63 | // todo?: owner ent col (although it's unused) 64 | ModelInstance model = _models[0]; 65 | if (model.AnimInfo.Flags[0].TestFlag(AnimFlags.Ended) && model.AnimInfo.Index[0] != 0) 66 | { 67 | model.SetAnimation(0); 68 | } 69 | _soundSource.PlaySfx(SfxId.ZOOMER_IDLE_LOOP, loop: true); 70 | if (!_seekingVolume) 71 | { 72 | if (_volumeCheckDelay > 0) 73 | { 74 | _volumeCheckDelay--; 75 | } 76 | else if (!_homeVolume.TestPoint(Position)) 77 | { 78 | _volumeCheckDelay = 16 * 2; // todo: FPS stuff 79 | Vector3 facing = (Position - SpawnData.Header.Position.ToFloatVector()).WithY(0); 80 | if (facing.X == 0 && facing.Z == 0) 81 | { 82 | facing = FacingVector.WithY(0); 83 | if (facing.X == 0 && facing.Z == 0) 84 | { 85 | facing = SpawnData.Header.FacingVector.ToFloatVector().WithY(0); 86 | } 87 | } 88 | _intendedDir = Vector3.Cross(UpVector, facing).Normalized(); 89 | if (Vector3.Dot(_intendedDir, _direction) < Fixed.ToFloat(-4091)) 90 | { 91 | _direction = RotateVector(_direction, UpVector, 1 / 2f); // todo: FPS stuff 92 | } 93 | _seekingVolume = true; 94 | } 95 | } 96 | Vector3 testPos = Position + UpVector * _boundingRadius; 97 | var results = new CollisionResult[8]; 98 | int colCount = CollisionDetection.CheckInRadius(testPos, _boundingRadius, limit: 8, 99 | getSimpleNormal: true, TestFlags.None, _scene, results); 100 | if (colCount > 0) 101 | { 102 | Vector3 facing = FacingVector; 103 | Vector3 vec = Vector3.Zero; 104 | for (int i = 0; i < colCount; i++) 105 | { 106 | CollisionResult result = results[i]; 107 | float dot = Vector3.Dot(testPos, result.Plane.Xyz) - result.Plane.W; 108 | float radMinusDot = _boundingRadius - dot; 109 | if (radMinusDot > 0 && radMinusDot < _boundingRadius && result.Field0 == 0 && Vector3.Dot(result.Plane.Xyz, _speed) < 0) 110 | { 111 | // sktodo: convert this to float math 112 | int rmd = (int)(radMinusDot * 4096); 113 | float DoThing(float value) 114 | { 115 | int n = (int)(value * 4096); 116 | int v20 = n * rmd; 117 | int v21 = (int)((ulong)(n * (long)rmd) >> 32); 118 | int v22; 119 | if (v21 < 0) 120 | { 121 | v22 = -((-v20 >> 12) | (-1048576 * (v21 + (v20 != 0 ? 1 : 0)))); 122 | } 123 | else 124 | { 125 | v22 = (v20 >> 12) | (v21 << 20); 126 | } 127 | return v22 / 4096f; 128 | } 129 | var b = new Vector3(DoThing(result.Plane.X), DoThing(result.Plane.Y), DoThing(result.Plane.Z)); 130 | testPos += b; 131 | } 132 | float facingDot = Vector3.Dot(result.Plane.Xyz, facing); 133 | if (Vector3.Dot(_field1A0, result.Plane.Xyz) < Fixed.ToFloat(4094) 134 | && (dot < _boundingRadius - Fixed.ToFloat(408) && facingDot >= Fixed.ToFloat(-143) 135 | || dot >= _boundingRadius - Fixed.ToFloat(408) && facingDot <= Fixed.ToFloat(143))) 136 | { 137 | vec += result.Plane.Xyz; 138 | } 139 | } 140 | Vector3 position = testPos - UpVector * _boundingRadius; 141 | if (vec != Vector3.Zero) 142 | { 143 | vec = vec.Normalized(); 144 | _field1AC = vec; 145 | } 146 | Vector3 upVec = UpVector + (_field1AC - UpVector) * (Fixed.ToFloat(819) / 2); // todo: FPS stuff 147 | upVec = upVec.Normalized(); 148 | Vector3 facingVec = Vector3.Cross(upVec, _direction).Normalized(); 149 | SetTransform(facingVec, upVec, position); 150 | } 151 | _speed = UpVector * (Fixed.ToFloat(-245) / 2); // todo: FPS stuff 152 | if (_seekingVolume) 153 | { 154 | _direction += (_intendedDir - _direction) * (Fixed.ToFloat(819) / 2); // todo: FPS stuff 155 | _direction = _direction.Normalized(); 156 | if (Vector3.Dot(_intendedDir, _direction) > _angleCos) 157 | { 158 | _direction = _intendedDir; 159 | _seekingVolume = false; 160 | _curAngle = 0; 161 | } 162 | } 163 | else if (colCount > 0) 164 | { 165 | float dot = Vector3.Dot(_field1AC, UpVector); 166 | if (dot >= Fixed.ToFloat(3712)) 167 | { 168 | _speed += FacingVector * (Fixed.ToFloat(204) / 2); // todo: FPS stuff 169 | if (dot >= Fixed.ToFloat(4095)) 170 | { 171 | _field1A0 = _field1AC; 172 | _curAngle += _angleInc; 173 | if (_curAngle > _maxAngle || _curAngle < -_maxAngle) 174 | { 175 | _angleInc *= -1; 176 | } 177 | Vector3 direction = RotateVector(FacingVector, UpVector, _angleInc); 178 | _direction = Vector3.Cross(direction, UpVector).Normalized(); 179 | } 180 | } 181 | } 182 | ContactDamagePlayer(15, knockback: true); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/MphRead/Utility/Archive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | 9 | namespace MphRead.Archive 10 | { 11 | // size: 32 12 | public readonly struct ArchiveHeader 13 | { 14 | // this is actaully 7 characters and a terminator, so we can use a string 15 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)] 16 | public readonly string MagicString; 17 | public readonly uint FileCount; 18 | public readonly uint TotalSize; 19 | [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U4, SizeConst = 4)] 20 | public readonly uint[] Padding; 21 | 22 | public ArchiveHeader(string magicString, uint fileCount, uint totalSize) 23 | { 24 | MagicString = magicString; 25 | FileCount = fileCount; 26 | TotalSize = totalSize; 27 | Padding = new uint[] { 0, 0, 0, 0 }; 28 | } 29 | 30 | public ArchiveHeader SwapBytes() 31 | { 32 | return new ArchiveHeader(MagicString, FileCount.SwapBytes(), TotalSize.SwapBytes()); 33 | } 34 | } 35 | 36 | // size: 64 37 | public readonly struct FileHeader 38 | { 39 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] 40 | public readonly char[] Filename; 41 | public readonly uint Offset; 42 | public readonly uint PaddedFileSize; 43 | public readonly uint TargetFileSize; 44 | [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U4, SizeConst = 5)] 45 | public readonly uint[] Padding; 46 | 47 | public FileHeader(char[] filename, uint offset, uint paddedFileSize, uint targetFileSize) 48 | { 49 | Filename = filename; 50 | Offset = offset; 51 | PaddedFileSize = paddedFileSize; 52 | TargetFileSize = targetFileSize; 53 | Padding = new uint[] { 0, 0, 0, 0, 0 }; 54 | } 55 | 56 | public FileHeader(string filename, uint offset, uint paddedFileSize, uint targetFileSize) 57 | { 58 | Filename = filename.PadRight(32, '\0').ToCharArray(); 59 | Offset = offset; 60 | PaddedFileSize = paddedFileSize; 61 | TargetFileSize = targetFileSize; 62 | Padding = new uint[] { 0, 0, 0, 0, 0 }; 63 | } 64 | 65 | public FileHeader SwapBytes() 66 | { 67 | return new FileHeader(Filename, Offset.SwapBytes(), PaddedFileSize.SwapBytes(), TargetFileSize.SwapBytes()); 68 | } 69 | } 70 | 71 | public static class ArchiveSizes 72 | { 73 | public static readonly int ArchiveHeader = Marshal.SizeOf(typeof(ArchiveHeader)); 74 | public static readonly int FileHeader = Marshal.SizeOf(typeof(FileHeader)); 75 | } 76 | 77 | public static class Archiver 78 | { 79 | public static string MagicString { get; } = "SNDFILE"; 80 | 81 | public static int Extract(string path, string? destination = null) 82 | { 83 | if (destination == null) 84 | { 85 | destination = Path.GetDirectoryName(path); 86 | } 87 | var bytes = new ReadOnlySpan(File.ReadAllBytes(path)); 88 | if (bytes.Length < ArchiveSizes.ArchiveHeader) 89 | { 90 | ThrowRead(); 91 | } 92 | ArchiveHeader header = Read.ReadStruct(bytes[0..ArchiveSizes.ArchiveHeader]); 93 | header = header.SwapBytes(); 94 | if (header.MagicString != MagicString || header.TotalSize != bytes.Length) 95 | { 96 | ThrowRead(); 97 | } 98 | uint pointer = (uint)(ArchiveSizes.ArchiveHeader + ArchiveSizes.FileHeader * header.FileCount); // only for validation 99 | var files = new List(); 100 | foreach (FileHeader swap in Read.DoOffsets(bytes, (uint)ArchiveSizes.ArchiveHeader, (int)header.FileCount)) 101 | { 102 | FileHeader file = swap.SwapBytes(); 103 | string filename = file.Filename.MarshalString(); 104 | if (filename == "" || file.PaddedFileSize == 0 || file.TargetFileSize == 0 105 | || file.Offset > header.TotalSize || file.Offset < ArchiveSizes.ArchiveHeader 106 | || file.PaddedFileSize > header.TotalSize || file.TargetFileSize > header.TotalSize 107 | || file.PaddedFileSize < file.TargetFileSize || NearestMultiple(file.TargetFileSize, 32) != file.PaddedFileSize 108 | || pointer != file.Offset) 109 | { 110 | ThrowRead(); 111 | } 112 | pointer += file.PaddedFileSize; 113 | files.Add(file); 114 | } 115 | if (files.Count == 0 || files[^1].Offset + files[^1].PaddedFileSize != header.TotalSize) 116 | { 117 | ThrowRead(); 118 | } 119 | int filesWritten = 0; 120 | foreach (FileHeader file in files) 121 | { 122 | string filename = file.Filename.MarshalString(); 123 | int start = (int)file.Offset; 124 | int end = start + (int)file.TargetFileSize; 125 | string output = Paths.Combine(destination!, filename); 126 | File.WriteAllBytes(output, bytes[start..end].ToArray()); 127 | filesWritten++; 128 | } 129 | return filesWritten; 130 | } 131 | 132 | public static void Archive(string destinationPath, IEnumerable filePaths) 133 | { 134 | if (filePaths == null || !filePaths.Any()) 135 | { 136 | ThrowWrite(); 137 | } 138 | var files = new List(); 139 | var entries = new List(); 140 | uint pointer = (uint)(ArchiveSizes.ArchiveHeader + ArchiveSizes.FileHeader * filePaths.Count()); 141 | foreach (string filePath in filePaths) 142 | { 143 | string filename = Path.GetFileName(filePath); 144 | if (filename.Length > 32) 145 | { 146 | ThrowWrite(); 147 | } 148 | byte[] file = File.ReadAllBytes(filePath); 149 | files.Add(file); 150 | var entry = new FileHeader( 151 | filename, 152 | pointer, 153 | NearestMultiple((uint)file.Length, 32), 154 | (uint)file.Length 155 | ); 156 | entries.Add(entry); 157 | pointer += entry.PaddedFileSize; 158 | } 159 | var header = new ArchiveHeader(MagicString, (uint)filePaths.Count(), pointer); 160 | using var writer = new BinaryWriter(File.Open(destinationPath, FileMode.Create)); 161 | writer.Write($"{MagicString}\0".ToCharArray()); 162 | writer.Write(header.FileCount.SwapBytes()); 163 | writer.Write(header.TotalSize.SwapBytes()); 164 | for (int i = 0; i < 16; i++) 165 | { 166 | writer.Write('\0'); 167 | } 168 | foreach (FileHeader entry in entries) 169 | { 170 | writer.Write(entry.Filename); 171 | writer.Write(entry.Offset.SwapBytes()); 172 | writer.Write(entry.PaddedFileSize.SwapBytes()); 173 | writer.Write(entry.TargetFileSize.SwapBytes()); 174 | for (int i = 0; i < 20; i++) 175 | { 176 | writer.Write('\0'); 177 | } 178 | } 179 | for (int i = 0; i < files.Count; i++) 180 | { 181 | writer.Write(files[i]); 182 | uint padding = entries[i].PaddedFileSize - entries[i].TargetFileSize; 183 | for (uint j = 0; j < padding; j++) 184 | { 185 | writer.Write((byte)0); 186 | } 187 | } 188 | Debug.Assert(writer.BaseStream.Length == pointer); 189 | } 190 | 191 | [DoesNotReturn] 192 | private static void ThrowRead() 193 | { 194 | throw new InvalidOperationException("Could not read archive."); 195 | } 196 | 197 | [DoesNotReturn] 198 | private static void ThrowWrite() 199 | { 200 | throw new InvalidOperationException("Could not write archive."); 201 | } 202 | 203 | private static uint NearestMultiple(uint value, uint of) 204 | { 205 | if (value <= of) 206 | { 207 | return value; 208 | } 209 | while (value % of != 0) 210 | { 211 | value += 1; 212 | } 213 | return value; 214 | } 215 | 216 | public static uint SwapBytes(this uint value) 217 | { 218 | byte[] bytes = BitConverter.GetBytes(value); 219 | Array.Reverse(bytes); 220 | return BitConverter.ToUInt32(bytes); 221 | } 222 | 223 | private static void Nop() { } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/49_ForceFieldLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using MphRead.Formats.Culling; 4 | using OpenTK.Mathematics; 5 | 6 | namespace MphRead.Entities.Enemies 7 | { 8 | public class Enemy49Entity : EnemyInstanceEntity 9 | { 10 | private Vector3 _vec1; 11 | private Vector3 _vec2; 12 | private Vector3 _fieldPosition; 13 | private Vector3 _targetPosition; 14 | private readonly ForceFieldEntity _forceField; 15 | private byte _shotFrames = 0; 16 | private EquipInfo? _equipInfo; 17 | private int _ammo = -1; 18 | private Vector3 _ownSpeed; // todo: revisit this? 19 | 20 | // todo?: technically this has a custom draw function, but I don't think we need it 21 | // (unless it's possible to observe the damage flash) 22 | public Enemy49Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 23 | : base(data, nodeRef, scene) 24 | { 25 | var spawner = data.Spawner as ForceFieldEntity; 26 | Debug.Assert(spawner != null); 27 | _forceField = spawner; 28 | } 29 | 30 | protected override void EnemyInitialize() 31 | { 32 | Vector3 position = _forceField.Data.Header.Position.ToFloatVector(); 33 | _fieldPosition = position; 34 | _vec1 = _forceField.Data.Header.UpVector.ToFloatVector(); 35 | _vec2 = _forceField.Data.Header.FacingVector.ToFloatVector(); 36 | position += _vec2 * Fixed.ToFloat(409); 37 | SetTransform(_vec2, _vec1, position); 38 | Flags |= EnemyFlags.NoMaxDistance; 39 | Flags |= EnemyFlags.Visible; 40 | Flags |= EnemyFlags.NoBombDamage; 41 | _health = _healthMax = 1; 42 | _boundingRadius = 0.5f; 43 | _hurtVolumeInit = new CollisionVolume(Vector3.Zero, _boundingRadius); 44 | ClearEffectiveness(); 45 | switch (_forceField.Data.Type) 46 | { 47 | case 0: 48 | SetEffectiveness(BeamType.PowerBeam, Effectiveness.Normal); 49 | break; 50 | case 1: 51 | SetEffectiveness(BeamType.VoltDriver, Effectiveness.Normal); 52 | break; 53 | case 2: 54 | SetEffectiveness(BeamType.Missile, Effectiveness.Normal); 55 | break; 56 | case 3: 57 | SetEffectiveness(BeamType.Battlehammer, Effectiveness.Normal); 58 | break; 59 | case 4: 60 | SetEffectiveness(BeamType.Imperialist, Effectiveness.Normal); 61 | break; 62 | case 5: 63 | SetEffectiveness(BeamType.Judicator, Effectiveness.Normal); 64 | break; 65 | case 6: 66 | SetEffectiveness(BeamType.Magmaul, Effectiveness.Normal); 67 | break; 68 | case 7: 69 | SetEffectiveness(BeamType.ShockCoil, Effectiveness.Normal); 70 | break; 71 | case 8: 72 | Flags &= ~EnemyFlags.NoBombDamage; 73 | break; 74 | } 75 | SetUpModel("ForceFieldLock"); 76 | Recolor = _forceField.Recolor; 77 | _equipInfo = new EquipInfo(Weapons.Weapons1P[(int)_forceField.Data.Type], _beams); 78 | _equipInfo.GetAmmo = () => _ammo; 79 | _equipInfo.SetAmmo = (newAmmo) => _ammo = newAmmo; 80 | } 81 | 82 | private void ClearEffectiveness() 83 | { 84 | for (int i = 0; i < BeamEffectiveness.Length; i++) 85 | { 86 | BeamEffectiveness[i] = Effectiveness.Zero; 87 | } 88 | } 89 | 90 | private void SetEffectiveness(BeamType type, Effectiveness effectiveness) 91 | { 92 | int index = (int)type; 93 | Debug.Assert(index < BeamEffectiveness.Length); 94 | BeamEffectiveness[index] = effectiveness; 95 | } 96 | 97 | protected override void EnemyProcess() 98 | { 99 | // this is called twice per tick, so the animation plays twice as fast 100 | if (Active) 101 | { 102 | for (int i = 0; i < _models.Count; i++) 103 | { 104 | UpdateAnimFrames(_models[i]); 105 | } 106 | } 107 | if (Vector3.Dot(PlayerEntity.Main.CameraInfo.Position - _fieldPosition, _vec2) < 0) 108 | { 109 | _vec2 *= -1; 110 | Vector3 position = _fieldPosition + _vec2 * Fixed.ToFloat(409); 111 | SetTransform(_vec2, _vec1, position); 112 | _prevPos = Position; 113 | } 114 | if (_models[0].AnimInfo.Flags[0].TestFlag(AnimFlags.Ended)) 115 | { 116 | if (_shotFrames > 0) 117 | { 118 | _shotFrames--; 119 | Debug.Assert(_equipInfo != null); 120 | Vector3 spawnDir = (_targetPosition - Position).Normalized(); 121 | Vector3 spawnPos = Position + spawnDir * 0.1f; 122 | BeamProjectileEntity.Spawn(this, _equipInfo, spawnPos, spawnDir, BeamSpawnFlags.None, NodeRef, _scene); 123 | } 124 | if (_shotFrames == 0) 125 | { 126 | _models[0].SetAnimation(0); 127 | } 128 | } 129 | float width = _forceField.Width - 0.3f; 130 | float height = _forceField.Height - 0.3f; 131 | Vector3 between = Position - _fieldPosition; 132 | float rightPct = Vector3.Dot(between, _forceField.FieldRightVector) / width; 133 | float upPct = Vector3.Dot(between, _forceField.FieldUpVector) / height; 134 | // percentage of the lock's distance toward the "bounding oval" 135 | float pct = rightPct * rightPct + upPct * upPct; 136 | if (pct >= 1) 137 | { 138 | float dot1 = Vector3.Dot(between, _forceField.FieldFacingVector); 139 | between = (between - _forceField.FieldFacingVector * dot1).Normalized(); 140 | float dot2 = Vector3.Dot(_ownSpeed, between) * 2; 141 | _ownSpeed -= between * dot2; 142 | float inv = 1 / MathF.Sqrt(pct); 143 | float rf = rightPct * inv * width; 144 | float uf = upPct * inv * height; 145 | Position = new Vector3( 146 | _fieldPosition.X + _forceField.FieldRightVector.X * rf + _forceField.FieldUpVector.X * uf, 147 | _fieldPosition.Y + _forceField.FieldRightVector.Y * rf + _forceField.FieldUpVector.Y * uf, 148 | _fieldPosition.Z + _forceField.FieldRightVector.Z * rf + _forceField.FieldUpVector.Z * uf 149 | ); 150 | } 151 | float magSqr = _ownSpeed.X * _ownSpeed.X + _ownSpeed.Y * _ownSpeed.Y + _ownSpeed.Z * _ownSpeed.Z; 152 | if (magSqr <= 0.0004f) 153 | { 154 | if (_shotFrames == 0) 155 | { 156 | if (_models[0].AnimInfo.Index[0] == 1) 157 | { 158 | if (_models[0].AnimInfo.Frame[0] >= 10) 159 | { 160 | float randRight = Rng.GetRandomInt2(0x666) / 4096f - 0.2f; 161 | float randUp = Rng.GetRandomInt2(0x666) / 4096f - 0.2f; 162 | _ownSpeed = new Vector3( 163 | _forceField.FieldUpVector.X * randUp + _forceField.FieldRightVector.X * randRight, 164 | _forceField.FieldUpVector.Y * randUp + _forceField.FieldRightVector.Y * randRight, 165 | _forceField.FieldUpVector.Z * randUp + _forceField.FieldRightVector.Z * randRight 166 | ); 167 | } 168 | } 169 | else 170 | { 171 | _models[0].SetAnimation(1, AnimFlags.NoLoop); 172 | } 173 | } 174 | } 175 | else if (_scene.FrameCount % 2 == 0) // todo: FPS stuff 176 | { 177 | _ownSpeed *= Fixed.ToFloat(3973); 178 | } 179 | _speed = _ownSpeed / 2; // todo: FPS stuff 180 | } 181 | 182 | protected override bool EnemyTakeDamage(EntityBase? source) 183 | { 184 | if (_health > 0) 185 | { 186 | if (source?.Type == EntityType.BeamProjectile) 187 | { 188 | LockHit(source); 189 | } 190 | } 191 | else 192 | { 193 | _scene.SendMessage(Message.Unlock, this, _owner, 0, 0); 194 | } 195 | return false; 196 | } 197 | 198 | public void LockHit(EntityBase source) 199 | { 200 | var beam = (BeamProjectileEntity)source; 201 | if (_shotFrames == 0 && GetEffectiveness(beam.Beam) == Effectiveness.Zero && beam.Owner == PlayerEntity.Main) 202 | { 203 | _shotFrames = _forceField.Data.Type == 7 ? (byte)(30 * 2) : (byte)1; // todo: FPS stuff 204 | beam.Owner.GetPosition(out _targetPosition); 205 | _models[0].SetAnimation(2, AnimFlags.NoLoop); 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/MphRead/Entities/Enemies/03_Petrasyl1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using MphRead.Formats.Culling; 4 | using OpenTK.Mathematics; 5 | 6 | namespace MphRead.Entities.Enemies 7 | { 8 | public class Enemy03Entity : EnemyInstanceEntity 9 | { 10 | private readonly EnemySpawnEntity _spawner; 11 | private float _idleRangeX = 0; 12 | private float _idleRangeZ = 0; 13 | private Vector3 _initialPos; 14 | private Vector3 _idleLimits; 15 | private Vector3 _field194; 16 | private Vector3 _field1A0; 17 | private float _bobAngle = 0; 18 | private float _bobOffset = 0; 19 | private float _bobSpeed = 0; 20 | private ushort _field170 = 0; 21 | private ushort _field172 = 0; 22 | private bool _teleportInAtInitial = true; 23 | private int _field18C = 0; // counter/steps in idle Z range 24 | 25 | public Enemy03Entity(EnemyInstanceEntityData data, NodeRef nodeRef, Scene scene) 26 | : base(data, nodeRef, scene) 27 | { 28 | var spawner = data.Spawner as EnemySpawnEntity; 29 | Debug.Assert(spawner != null); 30 | _spawner = spawner; 31 | _stateProcesses = new Action[3] 32 | { 33 | State00, State01, State02 34 | }; 35 | } 36 | 37 | protected override void EnemyInitialize() 38 | { 39 | _health = _healthMax = 8; 40 | Flags |= EnemyFlags.Visible; 41 | Flags |= EnemyFlags.NoHomingNc; 42 | Flags |= EnemyFlags.Invincible; 43 | Flags |= EnemyFlags.OnRadar; 44 | _boundingRadius = 0.5f; 45 | _hurtVolumeInit = new CollisionVolume(_spawner.Data.Fields.S03.Volume0); 46 | SetUpModel(Metadata.EnemyModelNames[3]); 47 | _models[0].SetAnimation(9, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node, AnimFlags.NoLoop); 48 | _models[0].SetAnimation(2, slot: 1, SetFlags.Texcoord); 49 | _idleRangeX = _spawner.Data.Fields.S03.IdleRange.X.FloatValue; 50 | _idleRangeZ = _spawner.Data.Fields.S03.IdleRange.Z.FloatValue; 51 | Vector3 facing = _spawner.Data.Fields.S03.Facing.ToFloatVector().WithY(0).Normalized(); 52 | Vector3 position = _spawner.Data.Fields.S03.Position.ToFloatVector() + _spawner.Data.Header.Position.ToFloatVector(); 53 | position = position.AddY(Fixed.ToFloat(5461)); 54 | _initialPos = position; 55 | _idleLimits = new Vector3( 56 | position.X + facing.X * _idleRangeZ - facing.Z * _idleRangeX, 57 | position.Y, 58 | position.Z + facing.Z * _idleRangeZ + facing.X * _idleRangeX 59 | ); 60 | facing.X *= -1; 61 | facing.Z *= -1; 62 | SetTransform(facing, Vector3.UnitY, position); 63 | _field194 = facing; 64 | _field1A0 = facing; 65 | _bobOffset = Fixed.ToFloat(Rng.GetRandomInt2(0x1800) + 2048) / 2; // [0.25, 1) 66 | _bobSpeed = Fixed.ToFloat(Rng.GetRandomInt2(0x6000)) + 1; // [1, 7) 67 | _field170 = _field172 = 20 * 2; // todo: FPS stuff 68 | UpdateState(); 69 | } 70 | 71 | private void UpdateState() 72 | { 73 | if (_state2 == 0) 74 | { 75 | // begin teleporting in 76 | _models[0].SetAnimation(3, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node, AnimFlags.NoLoop); 77 | if (_teleportInAtInitial) 78 | { 79 | Position = new Vector3(_initialPos.X, Position.Y, _initialPos.Z); 80 | _teleportInAtInitial = false; 81 | } 82 | else 83 | { 84 | Position = new Vector3(_idleLimits.X, Position.Y, _idleLimits.Z); 85 | _teleportInAtInitial = true; 86 | } 87 | _field194 = (-_field194).Normalized(); 88 | SetTransform(_field194, UpVector, Position); 89 | _field170 = 20 * 2; // todo: FPS stuff 90 | _soundSource.PlaySfx(SfxId.MOCHTROID_TELEPORT_IN); 91 | } 92 | else if (_state2 == 1) 93 | { 94 | // finish teleporting in 95 | _models[0].SetAnimation(0, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node); 96 | Flags &= ~EnemyFlags.NoHomingNc; 97 | Flags &= ~EnemyFlags.Invincible; 98 | _field18C = (int)(_idleRangeZ / 0.7f) * 2; // todo: FPS stuff 99 | _speed = (_field194 * 0.7f).WithY(0); 100 | _speed /= 2; // todo: FPS stuff 101 | } 102 | else if (_state2 == 2) 103 | { 104 | // teleport out 105 | _models[0].SetAnimation(4, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node, AnimFlags.NoLoop); 106 | Flags |= EnemyFlags.NoHomingNc; 107 | Flags |= EnemyFlags.Invincible; 108 | _speed = Vector3.Zero; 109 | _field172 = 20 * 2; // todo: FPS stuff 110 | _soundSource.PlaySfx(SfxId.MOCHTROID_TELEPORT_OUT); 111 | } 112 | } 113 | 114 | protected override void EnemyProcess() 115 | { 116 | CallStateProcess(); 117 | } 118 | 119 | private void State00() 120 | { 121 | if (CallSubroutine(Metadata.Enemy03Subroutines, this)) 122 | { 123 | UpdateState(); 124 | } 125 | } 126 | 127 | private void State01() 128 | { 129 | _soundSource.PlaySfx(SfxId.MOCHTROID_FLY, loop: true); 130 | _bobAngle += _bobSpeed / 2; // todo: FPS stuff 131 | if (_bobAngle >= 360) 132 | { 133 | _bobAngle -= 360; 134 | } 135 | float sin = MathF.Sin(MathHelper.DegreesToRadians(_bobAngle)); 136 | _speed.Y = _initialPos.Y + sin * _bobOffset - Position.Y; 137 | _speed.Y /= 2; // todo: FPS stuff 138 | if (HitPlayers[PlayerEntity.Main.SlotIndex]) 139 | { 140 | PlayerEntity.Main.TakeDamage(12, DamageFlags.None, FacingVector, this); 141 | } 142 | Vector3 between = PlayerEntity.Main.Position - Position; 143 | if (between.LengthSquared >= 7 * 7) 144 | { 145 | _field1A0 = _field194.WithY(0).Normalized(); 146 | } 147 | else 148 | { 149 | _field1A0 = between.WithY(0).Normalized(); 150 | } 151 | Vector3 prevFacing = FacingVector; 152 | Vector3 newFacing = prevFacing; 153 | // todo: FPS stuff 154 | newFacing.X += (_field1A0.X - prevFacing.X) / 8 / 2; 155 | newFacing.Z += (_field1A0.Z - prevFacing.Z) / 8 / 2; 156 | if (newFacing.X == 0 && newFacing.Z == 0) 157 | { 158 | newFacing = prevFacing; 159 | } 160 | Debug.Assert(newFacing != Vector3.Zero); 161 | newFacing = newFacing.Normalized(); 162 | if (MathF.Abs(newFacing.X - prevFacing.X) < 1 / 4096f && MathF.Abs(newFacing.Z - prevFacing.Z) < 1 / 4096f) 163 | { 164 | newFacing.X += 0.125f / 2; 165 | newFacing.Z -= 0.125f / 2; 166 | if (newFacing.X == 0 && newFacing.Z == 0) 167 | { 168 | newFacing.X += 0.125f / 2; 169 | newFacing.Z -= 0.125f / 2; 170 | } 171 | newFacing = newFacing.Normalized(); 172 | } 173 | SetTransform(newFacing, UpVector, Position); 174 | AnimationInfo animInfo = _models[0].AnimInfo; 175 | if (animInfo.Index[0] != 0 && animInfo.Flags[0].TestFlag(AnimFlags.Ended)) 176 | { 177 | _models[0].SetAnimation(0, slot: 0, SetFlags.Texture | SetFlags.Material | SetFlags.Node); 178 | } 179 | if (CallSubroutine(Metadata.Enemy03Subroutines, this)) 180 | { 181 | _soundSource.StopSfx(SfxId.MOCHTROID_FLY); 182 | UpdateState(); 183 | } 184 | } 185 | 186 | private void State02() 187 | { 188 | State00(); 189 | } 190 | 191 | private bool Behavior00() 192 | { 193 | if (_field172 == 0) 194 | { 195 | return true; 196 | } 197 | _field172--; 198 | return false; 199 | } 200 | 201 | private bool Behavior01() 202 | { 203 | if (_field18C == 0) 204 | { 205 | return true; 206 | } 207 | _field18C--; 208 | return false; 209 | } 210 | 211 | private bool Behavior02() 212 | { 213 | if (_field170 == 0) 214 | { 215 | return true; 216 | } 217 | _field170--; 218 | return false; 219 | } 220 | 221 | #region Boilerplate 222 | 223 | public static bool Behavior00(Enemy03Entity enemy) 224 | { 225 | return enemy.Behavior00(); 226 | } 227 | 228 | public static bool Behavior01(Enemy03Entity enemy) 229 | { 230 | return enemy.Behavior01(); 231 | } 232 | 233 | public static bool Behavior02(Enemy03Entity enemy) 234 | { 235 | return enemy.Behavior02(); 236 | } 237 | 238 | #endregion 239 | } 240 | } 241 | --------------------------------------------------------------------------------