├── 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 |
--------------------------------------------------------------------------------