mirror of
https://github.com/ficed/Braver.git
synced 2024-11-23 13:19:43 +00:00
Go
This commit is contained in:
commit
29a1ec213f
43
F7.sln
Normal file
43
F7.sln
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32630.192
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "F7", "F7\F7.csproj", "{7FCF50A6-C53B-4626-B20E-1FB3E1C09E8B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficedula.FF7", "Ficedula.FF7\Ficedula.FF7.csproj", "{1F444A1B-B218-4BF5-AFD4-D6B006193C28}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F7Cmd", "F7Cmd\F7Cmd.csproj", "{F2533F12-3A9C-471C-82BC-428F1A32C2E4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficedula.FF7.Exporters", "..\Ficedula.FF7.Exporters\Ficedula.FF7.Exporters.csproj", "{C83E0777-8288-4121-840A-6C9AC95F7A3D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7FCF50A6-C53B-4626-B20E-1FB3E1C09E8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7FCF50A6-C53B-4626-B20E-1FB3E1C09E8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7FCF50A6-C53B-4626-B20E-1FB3E1C09E8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7FCF50A6-C53B-4626-B20E-1FB3E1C09E8B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1F444A1B-B218-4BF5-AFD4-D6B006193C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1F444A1B-B218-4BF5-AFD4-D6B006193C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1F444A1B-B218-4BF5-AFD4-D6B006193C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1F444A1B-B218-4BF5-AFD4-D6B006193C28}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2533F12-3A9C-471C-82BC-428F1A32C2E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2533F12-3A9C-471C-82BC-428F1A32C2E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2533F12-3A9C-471C-82BC-428F1A32C2E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2533F12-3A9C-471C-82BC-428F1A32C2E4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C83E0777-8288-4121-840A-6C9AC95F7A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C83E0777-8288-4121-840A-6C9AC95F7A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C83E0777-8288-4121-840A-6C9AC95F7A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C83E0777-8288-4121-840A-6C9AC95F7A3D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B6F98DEC-D512-4691-9537-4BE2C9B9D69B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
15
F7/Content/Content.mgcb
Normal file
15
F7/Content/Content.mgcb
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
#----------------------------- Global Properties ----------------------------#
|
||||
|
||||
/outputDir:bin/$(Platform)
|
||||
/intermediateDir:obj/$(Platform)
|
||||
/platform:Windows
|
||||
/config:
|
||||
/profile:Reach
|
||||
/compress:False
|
||||
|
||||
#-------------------------------- References --------------------------------#
|
||||
|
||||
|
||||
#---------------------------------- Content ---------------------------------#
|
||||
|
26
F7/F7.csproj
Normal file
26
F7/F7.csproj
Normal file
@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<TieredCompilation>false</TieredCompilation>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<TrimmerRootAssembly Include="Microsoft.Xna.Framework.Content.ContentTypeReader" Visible="false" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MonoGame.Framework.WindowsDX" Version="3.8.0.1641" />
|
||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.0.1641" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MonoGameContentReference Include="Content\Content.mgcb" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ficedula.FF7\Ficedula.FF7.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
72
F7/FGame.cs
Normal file
72
F7/FGame.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
|
||||
public abstract class DataSource {
|
||||
public abstract Stream TryOpen(string file);
|
||||
}
|
||||
|
||||
public class FGame {
|
||||
|
||||
private class LGPDataSource : DataSource {
|
||||
private Ficedula.FF7.LGPFile _lgp;
|
||||
|
||||
public LGPDataSource(Ficedula.FF7.LGPFile lgp) {
|
||||
_lgp = lgp;
|
||||
}
|
||||
public override Stream TryOpen(string file) => _lgp.TryOpen(file);
|
||||
}
|
||||
|
||||
private class FileDataSource : DataSource {
|
||||
private string _root;
|
||||
|
||||
public FileDataSource(string root) {
|
||||
_root = root;
|
||||
}
|
||||
public override Stream TryOpen(string file) {
|
||||
string fn = Path.Combine(_root, file);
|
||||
if (File.Exists(fn))
|
||||
return new FileStream(fn, FileMode.Open, FileAccess.Read);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public VMM Memory { get; } = new();
|
||||
public SaveData SaveData { get; private set; }
|
||||
|
||||
private Dictionary<string, List<DataSource>> _data = new Dictionary<string, List<DataSource>>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
public FGame(string data, string bdata) {
|
||||
_data["field"] = new List<DataSource> {
|
||||
new LGPDataSource(new Ficedula.FF7.LGPFile(Path.Combine(data, "field", "flevel.lgp"))),
|
||||
new LGPDataSource(new Ficedula.FF7.LGPFile(Path.Combine(data, "field", "char.lgp"))),
|
||||
};
|
||||
foreach(string dir in Directory.GetDirectories(bdata)) {
|
||||
string category = Path.GetFileName(dir);
|
||||
if (!_data.TryGetValue(category, out var L))
|
||||
L = _data[category] = new();
|
||||
L.Add(new FileDataSource(dir));
|
||||
}
|
||||
}
|
||||
|
||||
public void NewGame() {
|
||||
using (var s = Open("save", "newgame.xml"))
|
||||
SaveData = Serialisation.Deserialise<SaveData>(s);
|
||||
SaveData.Loaded();
|
||||
}
|
||||
|
||||
public Stream Open(string category, string file) {
|
||||
foreach(var source in _data[category]) {
|
||||
var s = source.TryOpen(file);
|
||||
if (s != null)
|
||||
return s;
|
||||
}
|
||||
throw new F7Exception($"Could not open {category}/{file}");
|
||||
}
|
||||
}
|
||||
}
|
181
F7/Field/Background.cs
Normal file
181
F7/Field/Background.cs
Normal file
@ -0,0 +1,181 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7.Field {
|
||||
public class Background {
|
||||
|
||||
public int ScrollX { get; set; }
|
||||
public int ScrollY { get; set; }
|
||||
|
||||
private enum BlendType {
|
||||
Blend,
|
||||
Additive,
|
||||
Subtractive,
|
||||
QuarterAdd,
|
||||
None = 0xff,
|
||||
}
|
||||
|
||||
private class TexLayer {
|
||||
public Texture2D Tex;
|
||||
public VertexPositionTexture[] Verts;
|
||||
public IEnumerable<Ficedula.FF7.Field.Sprite> Sprites;
|
||||
public List<uint[]> Data;
|
||||
public BlendType Blend;
|
||||
public int Parameter;
|
||||
public int Mask;
|
||||
public int OffsetX, OffsetY;
|
||||
}
|
||||
|
||||
private List<TexLayer> _layers = new();
|
||||
private Ficedula.FF7.Field.Background _bg;
|
||||
private GraphicsDevice _graphics;
|
||||
private BasicEffect _effect;
|
||||
private Dictionary<int, int> _parameters = new();
|
||||
|
||||
private void Draw(IEnumerable<Ficedula.FF7.Field.Sprite> sprites, List<uint[]> data, int offsetX, int offsetY, bool clear) {
|
||||
foreach(var tile in sprites) {
|
||||
int destX = tile.DestX + offsetX, destY = tile.DestY + offsetY;
|
||||
var src = _bg.Pages[tile.TextureID].Data;
|
||||
var pal = _bg.Palettes[tile.PaletteID].Colours;
|
||||
foreach (int y in Enumerable.Range(0, 16)) {
|
||||
foreach (int x in Enumerable.Range(0, 16)) {
|
||||
byte p = src[tile.SrcY + y][tile.SrcX + x];
|
||||
uint c = pal[p];
|
||||
if (((c >> 24) != 0) || clear)
|
||||
data[destY + y][destX + x] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Background(GraphicsDevice graphics, Ficedula.FF7.Field.Background bg) {
|
||||
_bg = bg;
|
||||
_graphics = graphics;
|
||||
_effect = new BasicEffect(graphics) {
|
||||
TextureEnabled = true,
|
||||
LightingEnabled = false,
|
||||
VertexColorEnabled = false,
|
||||
FogEnabled = false,
|
||||
};
|
||||
|
||||
foreach (var layer in bg.Layers.Where(L => L.Any())) {
|
||||
var groups = layer
|
||||
.GroupBy(s => s.SortKey)
|
||||
.OrderByDescending(group => group.Key);
|
||||
|
||||
foreach (var group in groups) {
|
||||
|
||||
int minX = group.Min(s => s.DestX),
|
||||
minY = group.Min(s => s.DestY),
|
||||
maxX = group.Max(s => s.DestX + 16),
|
||||
maxY = group.Max(s => s.DestY + 16);
|
||||
|
||||
int texWidth = Util.MakePowerOfTwo(maxX - minX),
|
||||
texHeight = Util.MakePowerOfTwo(maxY - minY);
|
||||
float maxS = 1f * (maxX - minX) / texWidth,
|
||||
maxT = 1f * (maxY - minY) / texHeight;
|
||||
|
||||
TexLayer tl = new TexLayer {
|
||||
Tex = new Texture2D(graphics, texWidth, texHeight, false, SurfaceFormat.Color),
|
||||
OffsetX = -minX,
|
||||
OffsetY = -minY,
|
||||
Blend = (BlendType)group.First().TypeTrans,
|
||||
Sprites = group.OrderBy(t => t.ID).ToArray(),
|
||||
Data = Enumerable.Range(0, texHeight)
|
||||
.Select(_ => new uint[texWidth])
|
||||
.ToList(),
|
||||
Parameter = group.First().Param,
|
||||
Mask = group.First().State,
|
||||
Verts = new[] {
|
||||
new VertexPositionTexture {
|
||||
Position = new Vector3(minX, -minY, 0),
|
||||
TextureCoordinate = new Vector2(0, 0)
|
||||
},
|
||||
new VertexPositionTexture {
|
||||
Position = new Vector3(maxX, -minY, 0),
|
||||
TextureCoordinate = new Vector2(maxS, 0)
|
||||
},
|
||||
new VertexPositionTexture {
|
||||
Position = new Vector3(maxX, -maxY, 0),
|
||||
TextureCoordinate = new Vector2(maxS, maxT)
|
||||
},
|
||||
|
||||
new VertexPositionTexture {
|
||||
Position = new Vector3(minX, -minY, 0),
|
||||
TextureCoordinate = new Vector2(0, 0)
|
||||
},
|
||||
new VertexPositionTexture {
|
||||
Position = new Vector3(maxX, -maxY, 0),
|
||||
TextureCoordinate = new Vector2(maxS, maxT)
|
||||
},
|
||||
new VertexPositionTexture {
|
||||
Position = new Vector3(minX, -maxY, 0),
|
||||
TextureCoordinate = new Vector2(0, maxT)
|
||||
},
|
||||
}
|
||||
};
|
||||
_layers.Add(tl);
|
||||
Draw(tl.Sprites, tl.Data, -minX, -minY, false);
|
||||
foreach (int y in Enumerable.Range(0, tl.Tex.Height))
|
||||
tl.Tex.SetData(0, new Rectangle(0, y, tl.Tex.Width, 1), tl.Data[y], 0, tl.Tex.Width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetParameter(int parm, int value) {
|
||||
_parameters[parm] = value;
|
||||
}
|
||||
public void ModifyParameter(int parm, Func<int, int> modify) {
|
||||
int value;
|
||||
_parameters.TryGetValue(parm, out value);
|
||||
_parameters[parm] = modify(value);
|
||||
}
|
||||
|
||||
public void Step() {
|
||||
}
|
||||
|
||||
public void Render(Viewer viewer) {
|
||||
_graphics.BlendState = BlendState.AlphaBlend; //TODO!!!
|
||||
_graphics.DepthStencilState = DepthStencilState.None; //TODO!!!
|
||||
|
||||
_effect.Projection = viewer.Projection;
|
||||
_effect.View = viewer.View;
|
||||
|
||||
int L = 0;
|
||||
|
||||
_graphics.SamplerStates[0] = SamplerState.PointClamp;
|
||||
|
||||
foreach (var layer in _layers) {
|
||||
if ((layer.Mask != 0) && (_parameters[layer.Parameter] & layer.Mask) == 0)
|
||||
continue;
|
||||
|
||||
switch (layer.Blend) {
|
||||
case BlendType.None:
|
||||
case BlendType.Blend:
|
||||
_graphics.BlendState = BlendState.AlphaBlend;
|
||||
break;
|
||||
case BlendType.Additive:
|
||||
_graphics.BlendState = BlendState.Additive;
|
||||
break;
|
||||
default: //TODO NO
|
||||
_graphics.BlendState = BlendState.Opaque;
|
||||
break;
|
||||
}
|
||||
|
||||
_effect.World = Matrix.CreateTranslation(ScrollX, ScrollY, L++ * 0.01f)
|
||||
* Matrix.CreateScale(2);
|
||||
_effect.Texture = layer.Tex;
|
||||
|
||||
foreach (var pass in _effect.CurrentTechnique.Passes) {
|
||||
pass.Apply();
|
||||
_graphics.DrawUserPrimitives(PrimitiveType.TriangleList, layer.Verts, 0, layer.Verts.Length / 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
F7/Field/Entity.cs
Normal file
60
F7/Field/Entity.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7.Field {
|
||||
[Flags]
|
||||
public enum EntityFlags {
|
||||
None = 0,
|
||||
CanTalk = 0x1,
|
||||
CanCollide = 0x2,
|
||||
}
|
||||
|
||||
public class Entity {
|
||||
private Ficedula.FF7.Field.Entity _entity;
|
||||
private Fiber[] _priorities;
|
||||
|
||||
public string Name => _entity.Name;
|
||||
public FieldModel Model { get; set; }
|
||||
public Character Character { get; set; }
|
||||
public EntityFlags Flags { get; set; }
|
||||
public float TalkDistance { get; set; }
|
||||
public float CollideDistance { get; set; }
|
||||
public float MoveSpeed { get; set; }
|
||||
public int WalkmeshTri { get; set; }
|
||||
|
||||
public Entity(Ficedula.FF7.Field.Entity entity, FieldScreen screen) {
|
||||
_entity = entity;
|
||||
_priorities = Enumerable.Range(0, 8)
|
||||
.Select(_ => new Fiber(this, screen, screen.Dialog.ScriptBytecode))
|
||||
.ToArray();
|
||||
Flags = EntityFlags.CanTalk | EntityFlags.CanCollide;
|
||||
MoveSpeed = 512;
|
||||
}
|
||||
|
||||
public bool Call(int priority, int script, Action onComplete) {
|
||||
if (_priorities[priority].Active)
|
||||
return false;
|
||||
|
||||
_priorities[priority].OnStop = onComplete;
|
||||
_priorities[priority].Start(_entity.Scripts[script]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Run(bool isInit = false) {
|
||||
int priority = 7;
|
||||
foreach (var fiber in _priorities.Reverse()) {
|
||||
if (fiber.InProgress && fiber.Active) {
|
||||
System.Diagnostics.Debug.WriteLine($"Entity {Name} running script from IP {fiber.IP} priority {priority}");
|
||||
fiber.Run(isInit);
|
||||
if (isInit) fiber.Resume();
|
||||
break;
|
||||
}
|
||||
priority--;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
75
F7/Field/FieldDebug.cs
Normal file
75
F7/Field/FieldDebug.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7.Field {
|
||||
public class FieldDebug {
|
||||
|
||||
private VertexBuffer _vertexBuffer;
|
||||
private int _walkMeshTris;
|
||||
private BasicEffect _effect;
|
||||
private GraphicsDevice _graphics;
|
||||
|
||||
public FieldDebug(GraphicsDevice graphics, Ficedula.FF7.Field.FieldFile field) {
|
||||
_graphics = graphics;
|
||||
|
||||
List<VertexPositionColor> verts = new();
|
||||
|
||||
foreach(var tri in field.GetWalkmesh().Triangles) {
|
||||
verts.Add(new VertexPositionColor {
|
||||
Position = new Vector3(tri.V0.X, tri.V0.Y, tri.V0.Z),
|
||||
Color = Color.Red.WithAlpha(0x80),
|
||||
});
|
||||
verts.Add(new VertexPositionColor {
|
||||
Position = new Vector3(tri.V2.X, tri.V2.Y, tri.V2.Z),
|
||||
Color = Color.Green.WithAlpha(0x80),
|
||||
});
|
||||
verts.Add(new VertexPositionColor {
|
||||
Position = new Vector3(tri.V1.X, tri.V1.Y, tri.V1.Z),
|
||||
Color = Color.Blue.WithAlpha(0x80),
|
||||
});
|
||||
_walkMeshTris++;
|
||||
}
|
||||
|
||||
var minWM = new Vector3(
|
||||
verts.Select(v => v.Position.X).Min(),
|
||||
verts.Select(v => v.Position.Y).Min(),
|
||||
verts.Select(v => v.Position.Z).Min()
|
||||
);
|
||||
var maxWM = new Vector3(
|
||||
verts.Select(v => v.Position.X).Max(),
|
||||
verts.Select(v => v.Position.Y).Max(),
|
||||
verts.Select(v => v.Position.Z).Max()
|
||||
);
|
||||
System.Diagnostics.Debug.WriteLine($"Walkmesh min bounds {minWM} max {maxWM}");
|
||||
|
||||
_vertexBuffer = new VertexBuffer(graphics, typeof(VertexPositionColor), verts.Count, BufferUsage.WriteOnly);
|
||||
_vertexBuffer.SetData(verts.ToArray());
|
||||
|
||||
_effect = new BasicEffect(graphics) {
|
||||
FogEnabled = false,
|
||||
LightingEnabled = false,
|
||||
TextureEnabled = false,
|
||||
VertexColorEnabled = true,
|
||||
};
|
||||
}
|
||||
|
||||
public void Render(Viewer viewer) {
|
||||
//_graphics.RasterizerState = RasterizerState.CullNone;
|
||||
_graphics.SetVertexBuffer(_vertexBuffer);
|
||||
|
||||
_effect.View = viewer.View;
|
||||
_effect.Projection = viewer.Projection;
|
||||
_effect.World = Matrix.Identity;
|
||||
|
||||
foreach(var pass in _effect.CurrentTechnique.Passes) {
|
||||
pass.Apply();
|
||||
_graphics.DrawPrimitives(PrimitiveType.TriangleList, 0, _walkMeshTris);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
215
F7/Field/FieldModel.cs
Normal file
215
F7/Field/FieldModel.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7.Field {
|
||||
|
||||
public class AnimationState {
|
||||
public float AnimationSpeed { get; set; }
|
||||
public int Animation { get; set; }
|
||||
public int Frame { get; set; }
|
||||
public Action AnimationComplete { get; set; }
|
||||
public bool AnimationLoop { get; set; }
|
||||
}
|
||||
|
||||
public class FieldModel {
|
||||
public Vector3 Rotation { get; set; }
|
||||
public Vector3 Translation { get; set; }
|
||||
public float Scale { get; set; } = 1f;
|
||||
public bool Visible { get; set; } = true;
|
||||
public float GlobalAnimationSpeed { get; set; } = 1f;
|
||||
public AnimationState AnimationState { get; set; }
|
||||
|
||||
private class RenderNode {
|
||||
public int VertOffset, IndexOffset, TriCount;
|
||||
public Texture2D Texture;
|
||||
}
|
||||
|
||||
private Dictionary<Ficedula.FF7.PFileChunk, RenderNode> _nodes = new();
|
||||
private BasicEffect _texEffect, _colEffect;
|
||||
private VertexBuffer _vertexBuffer;
|
||||
private IndexBuffer _indexBuffer;
|
||||
private Ficedula.FF7.Field.HRCModel _hrcModel;
|
||||
private GraphicsDevice _graphics;
|
||||
private List<Ficedula.FF7.Field.FieldAnim> _animations = new();
|
||||
|
||||
//TODO dedupe textures
|
||||
public FieldModel(GraphicsDevice graphics, FGame g, string hrc, IEnumerable<string> animations) {
|
||||
_graphics = graphics;
|
||||
_hrcModel = new Ficedula.FF7.Field.HRCModel(s => g.Open("field", s), hrc);
|
||||
|
||||
List<VertexPositionNormalColorTexture> verts = new();
|
||||
List<int> indices = new();
|
||||
|
||||
void DescendBuild(Ficedula.FF7.Field.HRCModel.Bone bone) {
|
||||
|
||||
foreach (var poly in bone.Polygons) {
|
||||
var textures = poly.Textures
|
||||
.Select(t => graphics.LoadTex(t, 0))
|
||||
.ToArray();
|
||||
foreach (var chunk in poly.PFile.Chunks) {
|
||||
_nodes[chunk] = new RenderNode {
|
||||
IndexOffset = indices.Count,
|
||||
Texture = chunk.Texture == null ? null : textures[chunk.Texture.Value],
|
||||
TriCount = chunk.Indices.Count / 3,
|
||||
VertOffset = verts.Count,
|
||||
};
|
||||
indices.AddRange(chunk.Indices);
|
||||
verts.AddRange(
|
||||
chunk.Verts
|
||||
.Select(v => new VertexPositionNormalColorTexture {
|
||||
Position = v.Position.ToX(),
|
||||
Normal = v.Normal.ToX(),
|
||||
Color = new Color(v.Colour),
|
||||
TexCoord = v.TexCoord.ToX(),
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in bone.Children)
|
||||
DescendBuild(child);
|
||||
}
|
||||
DescendBuild(_hrcModel.Root);
|
||||
|
||||
_texEffect = new BasicEffect(graphics) {
|
||||
TextureEnabled = true,
|
||||
VertexColorEnabled = true,
|
||||
LightingEnabled = false,
|
||||
};
|
||||
_colEffect = new BasicEffect(graphics) {
|
||||
TextureEnabled = false,
|
||||
VertexColorEnabled = true,
|
||||
LightingEnabled = false,
|
||||
};
|
||||
|
||||
_vertexBuffer = new VertexBuffer(graphics, typeof(VertexPositionNormalColorTexture), verts.Count, BufferUsage.WriteOnly);
|
||||
_vertexBuffer.SetData(verts.ToArray());
|
||||
_indexBuffer = new IndexBuffer(graphics, typeof(int), indices.Count, BufferUsage.WriteOnly);
|
||||
_indexBuffer.SetData(indices.ToArray());
|
||||
|
||||
_animations = animations
|
||||
.Select(a => new Ficedula.FF7.Field.FieldAnim(g.Open("field", a)))
|
||||
.ToList();
|
||||
|
||||
PlayAnimation(0, false, 1f, null);
|
||||
|
||||
Vector3 minBounds = Vector3.Zero, maxBounds = Vector3.Zero;
|
||||
Descend(_hrcModel.Root, Matrix.Identity,
|
||||
(chunk, m) => {
|
||||
var transformed = chunk.Verts
|
||||
.Select(v => Vector3.Transform(v.Position.ToX(), m));
|
||||
minBounds = new Vector3(
|
||||
Math.Min(minBounds.X, transformed.Min(v => v.X)),
|
||||
Math.Min(minBounds.Y, transformed.Min(v => v.Y)),
|
||||
Math.Min(minBounds.Z, transformed.Min(v => v.Z))
|
||||
);
|
||||
maxBounds = new Vector3(
|
||||
Math.Max(minBounds.X, transformed.Max(v => v.X)),
|
||||
Math.Max(minBounds.Y, transformed.Max(v => v.Y)),
|
||||
Math.Max(minBounds.Z, transformed.Max(v => v.Z))
|
||||
);
|
||||
}
|
||||
);
|
||||
System.Diagnostics.Debug.WriteLine($"Model {hrc} with min bounds {minBounds}, max {maxBounds}");
|
||||
}
|
||||
|
||||
private void Descend(Ficedula.FF7.Field.HRCModel.Bone bone, Matrix m, Action<Ficedula.FF7.PFileChunk, Matrix> onChunk) {
|
||||
var frame = _animations[AnimationState.Animation].Frames[AnimationState.Frame];
|
||||
Matrix child = m;
|
||||
if (bone.Index >= 0) {
|
||||
var rotation = frame.Bones[bone.Index];
|
||||
child =
|
||||
Matrix.CreateRotationZ(rotation.Z * (float)Math.PI / 180)
|
||||
* Matrix.CreateRotationX(rotation.X * (float)Math.PI / 180)
|
||||
* Matrix.CreateRotationY(rotation.Y * (float)Math.PI / 180)
|
||||
* child
|
||||
;
|
||||
} else {
|
||||
child =
|
||||
Matrix.CreateTranslation(frame.Translation.ToX()) //TODO check ordering here
|
||||
* Matrix.CreateRotationZ(frame.Rotation.Z * (float)Math.PI / 180)
|
||||
* Matrix.CreateRotationX(frame.Rotation.X * (float)Math.PI / 180)
|
||||
* Matrix.CreateRotationY(frame.Rotation.Y * (float)Math.PI / 180)
|
||||
* child
|
||||
;
|
||||
}
|
||||
|
||||
if (bone.Polygons.Any()) {
|
||||
foreach (var poly in bone.Polygons) {
|
||||
foreach (var chunk in poly.PFile.Chunks) {
|
||||
onChunk(chunk, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child = Matrix.CreateTranslation(0, 0, -bone.Length) * child;
|
||||
|
||||
foreach (var cb in bone.Children)
|
||||
Descend(cb, child, onChunk);
|
||||
}
|
||||
|
||||
public void Render(Viewer viewer) {
|
||||
_texEffect.View = viewer.View;
|
||||
_texEffect.Projection = viewer.Projection;
|
||||
_colEffect.View = viewer.View;
|
||||
_colEffect.Projection = viewer.Projection;
|
||||
|
||||
_graphics.Indices = _indexBuffer;
|
||||
_graphics.SetVertexBuffer(_vertexBuffer);
|
||||
|
||||
Descend(
|
||||
_hrcModel.Root,
|
||||
Matrix.CreateRotationZ(Rotation.Z * (float)Math.PI / 180)
|
||||
* Matrix.CreateRotationX(Rotation.X * (float)Math.PI / 180)
|
||||
* Matrix.CreateRotationY(Rotation.Y * (float)Math.PI / 180)
|
||||
* Matrix.CreateScale(Scale, -Scale, Scale)
|
||||
* Matrix.CreateTranslation(Translation)
|
||||
* Matrix.CreateRotationX(90 * (float)Math.PI / 180)
|
||||
,
|
||||
(chunk, m) => {
|
||||
_texEffect.World = _colEffect.World = m;
|
||||
var rn = _nodes[chunk];
|
||||
_texEffect.Texture = rn.Texture;
|
||||
|
||||
var effect = rn.Texture == null ? _colEffect : _texEffect;
|
||||
foreach (var pass in effect.CurrentTechnique.Passes) {
|
||||
pass.Apply();
|
||||
_graphics.DrawIndexedPrimitives(
|
||||
PrimitiveType.TriangleList, rn.VertOffset, rn.IndexOffset, rn.TriCount
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private float _animCountdown;
|
||||
public void FrameStep() {
|
||||
_animCountdown -= AnimationState.AnimationSpeed * GlobalAnimationSpeed * 0.25f;
|
||||
if (_animCountdown <= 0) {
|
||||
AnimationState.AnimationComplete?.Invoke();
|
||||
_animCountdown = 1;
|
||||
if (AnimationState.AnimationLoop)
|
||||
AnimationState.Frame = (AnimationState.Frame + 1) % _animations[AnimationState.Animation].Frames.Count;
|
||||
else
|
||||
AnimationState.Frame = Math.Min(AnimationState.Frame + 1, _animations[AnimationState.Animation].Frames.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayAnimation(int animation, bool loop, float speed, Action onComplete) {
|
||||
AnimationState = new AnimationState {
|
||||
Animation = animation,
|
||||
AnimationLoop = loop,
|
||||
AnimationSpeed = speed,
|
||||
AnimationComplete = onComplete,
|
||||
};
|
||||
_animCountdown = 1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
117
F7/Field/FieldScreen.cs
Normal file
117
F7/Field/FieldScreen.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7.Field {
|
||||
public class FieldScreen : Screen {
|
||||
|
||||
private View3D _view3D;
|
||||
private Viewer _view2D;
|
||||
private FieldDebug _debug;
|
||||
|
||||
private bool _debugMode = false;
|
||||
|
||||
public Background Background { get; }
|
||||
public List<Entity> Entities { get; }
|
||||
public FieldModel Player { get; set; }
|
||||
public List<FieldModel> FieldModels { get; }
|
||||
public Ficedula.FF7.Field.DialogEvent Dialog { get; }
|
||||
|
||||
public FieldScreen(string file, FGame g, GraphicsDevice graphics) : base(g, graphics) {
|
||||
using (var s = g.Open("field", file)) {
|
||||
var field = new Ficedula.FF7.Field.FieldFile(s);
|
||||
Background = new Background(graphics, field.GetBackground());
|
||||
Dialog = field.GetDialogEvent();
|
||||
|
||||
Entities = Dialog.Entities
|
||||
.Select(e => new Entity(e, this))
|
||||
.ToList();
|
||||
|
||||
FieldModels = field.GetModels()
|
||||
.Models
|
||||
.Select(m => new FieldModel(
|
||||
graphics, g, m.HRC,
|
||||
m.Animations.Select(s => System.IO.Path.ChangeExtension(s, ".a"))
|
||||
) {
|
||||
//Scale = float.Parse(m.Scale)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var cam = field.GetCameraMatrices().First();
|
||||
_view3D = new OrthoView3D {
|
||||
CameraPosition = new Vector3(cam.CameraPosition.X, cam.CameraPosition.Z, cam.CameraPosition.Y),
|
||||
CameraForwards = new Vector3(cam.Forwards.X, cam.Forwards.Z, cam.Forwards.Y),
|
||||
CameraUp = new Vector3(cam.Up.X, cam.Up.Z, cam.Up.Y),
|
||||
Width = cam.Zoom,
|
||||
Height = cam.Zoom * 720f / 1280f,
|
||||
};
|
||||
|
||||
_debug = new FieldDebug(graphics, field);
|
||||
}
|
||||
|
||||
g.Memory.ResetScratch();
|
||||
|
||||
foreach (var entity in Entities) {
|
||||
entity.Call(0, 0, null);
|
||||
entity.Run(true);
|
||||
}
|
||||
|
||||
_view2D = new View2D {
|
||||
Width = 1280,
|
||||
Height = 720
|
||||
};
|
||||
}
|
||||
|
||||
private int _nextModelIndex = 0;
|
||||
public int GetNextModelIndex() {
|
||||
return _nextModelIndex++;
|
||||
}
|
||||
|
||||
public override void Render() {
|
||||
Background.Render(_view2D);
|
||||
_debug.Render(_view3D);
|
||||
foreach (var entity in Entities)
|
||||
entity.Model?.Render(_view3D);
|
||||
}
|
||||
|
||||
int frame = 0;
|
||||
public override void Step(GameTime elapsed) {
|
||||
if ((frame++ % 4) == 0) {
|
||||
foreach (var entity in Entities) {
|
||||
entity.Run();
|
||||
entity.Model?.FrameStep();
|
||||
}
|
||||
}
|
||||
Background.Step();
|
||||
}
|
||||
|
||||
public override void ProcessInput(InputState input) {
|
||||
base.ProcessInput(input);
|
||||
if (input.IsJustDown(InputKey.Start))
|
||||
_debugMode = !_debugMode;
|
||||
|
||||
if (_debugMode) {
|
||||
var forwards = new Vector3(_view3D.CameraForwards.X, _view3D.CameraForwards.Y, 0);
|
||||
forwards.Normalize();
|
||||
var right = new Vector3(forwards.Y, forwards.X, 0);
|
||||
|
||||
if (input.IsDown(InputKey.Up)) {
|
||||
_view3D.CameraPosition += forwards;
|
||||
}
|
||||
if (input.IsDown(InputKey.Down)) {
|
||||
_view3D.CameraPosition -= forwards;
|
||||
}
|
||||
if (input.IsDown(InputKey.Left)) {
|
||||
_view3D.CameraPosition += right;
|
||||
}
|
||||
if (input.IsDown(InputKey.Right)) {
|
||||
_view3D.CameraPosition -= right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
716
F7/Field/VM.cs
Normal file
716
F7/Field/VM.cs
Normal file
@ -0,0 +1,716 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7.Field {
|
||||
public enum OpCode {
|
||||
RET,
|
||||
REQ,
|
||||
REQSW,
|
||||
REQEW,
|
||||
PREQ,
|
||||
PRQSW,
|
||||
PRQEW,
|
||||
RETTO,
|
||||
JOIN,
|
||||
SPLIT,
|
||||
SPTYE,
|
||||
GTPYE,
|
||||
__unused0,
|
||||
__unused1,
|
||||
DSKCG,
|
||||
SPECIAL,
|
||||
JMPF,
|
||||
JMPFL,
|
||||
JMPB,
|
||||
JMPBL,
|
||||
IFUB,
|
||||
IFUBL,
|
||||
IFSW,
|
||||
IFSWL,
|
||||
IFUW,
|
||||
IFUWL,
|
||||
__unused2,
|
||||
__unused3,
|
||||
__unused4,
|
||||
__unused5,
|
||||
__unused6,
|
||||
__unused7,
|
||||
MINIGAME,
|
||||
TUTOR,
|
||||
BTMD2,
|
||||
BTRLD,
|
||||
WAIT,
|
||||
NFADE,
|
||||
BLINK,
|
||||
BGMOVIE,
|
||||
KAWAI,
|
||||
KAWIW,
|
||||
PMOVA,
|
||||
SLIP,
|
||||
BGPDH,
|
||||
BGSCR,
|
||||
WCLS,
|
||||
WSIZW,
|
||||
IFKEY,
|
||||
IFKEYON,
|
||||
IFKEYOFF,
|
||||
UC,
|
||||
PDIRA,
|
||||
PTURA,
|
||||
WSPCL,
|
||||
WNUMB,
|
||||
STTIM,
|
||||
GOLDu,
|
||||
GOLDd,
|
||||
CHGLD,
|
||||
HMPMAX1,
|
||||
HMPMAX2,
|
||||
MHMMX,
|
||||
HMPMAX3,
|
||||
MESSAGE,
|
||||
MPARA,
|
||||
MPRA2,
|
||||
MPNAM,
|
||||
__unused8,
|
||||
MPu,
|
||||
__unused9,
|
||||
MPd,
|
||||
ASK,
|
||||
MENU,
|
||||
MENU2,
|
||||
BTLTB,
|
||||
__unused10,
|
||||
HPu,
|
||||
__unused11,
|
||||
HPd,
|
||||
WINDOW,
|
||||
WMOVE,
|
||||
WMODE,
|
||||
WREST,
|
||||
WCLSE,
|
||||
WROW,
|
||||
GWCOL,
|
||||
SWCOL,
|
||||
STITM,
|
||||
DLITM,
|
||||
CKITM,
|
||||
SMTRA,
|
||||
DMTRA,
|
||||
CMTRA,
|
||||
SHAKE,
|
||||
NOP,
|
||||
MAPJUMP,
|
||||
SCRLO,
|
||||
SCRLC,
|
||||
SCRLA,
|
||||
SCR2D,
|
||||
SCRCC,
|
||||
SCR2DC,
|
||||
SCRLW,
|
||||
SCR2DL,
|
||||
MPDSP,
|
||||
VWOFT,
|
||||
FADE,
|
||||
FADEW,
|
||||
IDLCK,
|
||||
LSTMP,
|
||||
SCRLP,
|
||||
BATTLE,
|
||||
BTLON,
|
||||
BTLMD,
|
||||
PGTDR,
|
||||
GETPC,
|
||||
PXYZI,
|
||||
PLUS_,
|
||||
PLUS2_,
|
||||
MINUS_,
|
||||
MINUS2_,
|
||||
INC_,
|
||||
INC2_,
|
||||
DEC_,
|
||||
DEC2_,
|
||||
TLKON,
|
||||
RDMSD,
|
||||
SETBYTE,
|
||||
SETWORD,
|
||||
BITON,
|
||||
BITOFF,
|
||||
BITXOR,
|
||||
PLUS,
|
||||
PLUS2,
|
||||
MINUS,
|
||||
MINUS2,
|
||||
MUL,
|
||||
MUL2,
|
||||
DIV,
|
||||
DIV2,
|
||||
MOD,
|
||||
MOD2,
|
||||
AND,
|
||||
AND2,
|
||||
OR,
|
||||
OR2,
|
||||
XOR,
|
||||
XOR2,
|
||||
INC,
|
||||
INC2,
|
||||
DEC,
|
||||
DEC2,
|
||||
RANDOM,
|
||||
LBYTE,
|
||||
HBYTE,
|
||||
_2BYTE,
|
||||
SETX,
|
||||
GETX,
|
||||
SEARCHX,
|
||||
PC,
|
||||
CHAR,
|
||||
DFANM,
|
||||
ANIME1,
|
||||
VISI,
|
||||
XYZI,
|
||||
XYI,
|
||||
XYZ,
|
||||
MOVE,
|
||||
CMOVE,
|
||||
MOVA,
|
||||
TURA,
|
||||
ANIMW,
|
||||
FMOVE,
|
||||
ANIME2,
|
||||
ANIM_1,
|
||||
CANIM1,
|
||||
CANM_1,
|
||||
MSPED,
|
||||
DIR,
|
||||
TURNGEN,
|
||||
TURN,
|
||||
DIRA,
|
||||
GETDIR,
|
||||
GETAXY,
|
||||
GETAI,
|
||||
ANIM_2,
|
||||
CANIM2,
|
||||
CANM_2,
|
||||
ASPED,
|
||||
__unused12,
|
||||
CC,
|
||||
JUMP,
|
||||
AXYZI,
|
||||
LADER,
|
||||
OFST,
|
||||
OFSTW,
|
||||
TALKR,
|
||||
SLIDR,
|
||||
SOLID,
|
||||
PRTYP,
|
||||
PRTYM,
|
||||
PRTYE,
|
||||
IFPRTYQ,
|
||||
IFMEMBQ,
|
||||
MMBud,
|
||||
MMBLK,
|
||||
MMBUK,
|
||||
LINE,
|
||||
LINON,
|
||||
MPJPO,
|
||||
SLINE,
|
||||
SIN,
|
||||
COS,
|
||||
TLKR2,
|
||||
SLDR2,
|
||||
PMJMP,
|
||||
PMJMP2,
|
||||
AKAO2,
|
||||
FCFIX,
|
||||
CCANM,
|
||||
ANIMB,
|
||||
TURNW,
|
||||
MPPAL,
|
||||
BGON,
|
||||
BGOFF,
|
||||
BGROL,
|
||||
BGROL2,
|
||||
BGCLR,
|
||||
STPAL,
|
||||
LDPAL,
|
||||
CPPAL,
|
||||
RTPAL,
|
||||
ADPAL,
|
||||
MPPAL2,
|
||||
STPLS,
|
||||
LDPLS,
|
||||
CPPAL2,
|
||||
RTPAL2,
|
||||
ADPAL2,
|
||||
MUSIC,
|
||||
SOUND,
|
||||
AKAO,
|
||||
MUSVT,
|
||||
MUSVM,
|
||||
MULCK,
|
||||
BMUSC,
|
||||
CHMPH,
|
||||
PMVIE,
|
||||
MOVIE,
|
||||
MVIEF,
|
||||
MVCAM,
|
||||
FMUSC,
|
||||
CMUSC,
|
||||
CHMST,
|
||||
GAMEOVER,
|
||||
}
|
||||
|
||||
public enum OpResult {
|
||||
Continue,
|
||||
Restart,
|
||||
}
|
||||
|
||||
public delegate OpResult OpExecute(Fiber f, Entity e, FieldScreen s);
|
||||
|
||||
public class Fiber {
|
||||
private byte[] _script;
|
||||
private FieldScreen _screen;
|
||||
private Entity _entity;
|
||||
private int _ip;
|
||||
private bool _inInit;
|
||||
public bool Active { get; private set; }
|
||||
public bool InProgress => _ip >= 0;
|
||||
public Action OnStop { get; set; }
|
||||
public int IP => _ip;
|
||||
|
||||
public int OpcodeAttempts { get; private set; }
|
||||
|
||||
public byte ReadU8() {
|
||||
return _script[_ip++];
|
||||
}
|
||||
public ushort ReadU16() {
|
||||
ushort us = _script[_ip++];
|
||||
us |= (ushort)(_script[_ip++] << 8);
|
||||
return us;
|
||||
}
|
||||
public short ReadS16() {
|
||||
short s = _script[_ip++];
|
||||
s |= (short)(_script[_ip++] << 8);
|
||||
return s;
|
||||
}
|
||||
|
||||
public Fiber(Entity e, FieldScreen s, byte[] scriptBytecode) {
|
||||
_entity = e;
|
||||
Active = false;
|
||||
_screen = s;
|
||||
_ip = -1;
|
||||
_script = scriptBytecode;
|
||||
}
|
||||
|
||||
public void Start(int ip) {
|
||||
_ip = ip;
|
||||
Active = true;
|
||||
OpcodeAttempts = 0;
|
||||
}
|
||||
|
||||
public void Pause() {
|
||||
Active = false;
|
||||
}
|
||||
public void Resume() {
|
||||
Active = true;
|
||||
}
|
||||
|
||||
public void Stop() {
|
||||
Active = false;
|
||||
if (!_inInit) _ip = -1;
|
||||
OnStop?.Invoke();
|
||||
}
|
||||
|
||||
public void Jump(int ip) {
|
||||
_ip = ip;
|
||||
}
|
||||
|
||||
public void Run(bool isInit = false) {
|
||||
_inInit = isInit;
|
||||
while (Active) {
|
||||
int opIP = _ip;
|
||||
OpCode op = (OpCode)ReadU8();
|
||||
switch (VM.Execute(op, this, _entity, _screen)) {
|
||||
case OpResult.Continue:
|
||||
OpcodeAttempts = 0;
|
||||
break;
|
||||
case OpResult.Restart:
|
||||
OpcodeAttempts++;
|
||||
_ip = opIP;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class VM {
|
||||
|
||||
public static bool ErrorOnUnknown { get; set; }
|
||||
|
||||
private static OpExecute[] _executors = new OpExecute[256];
|
||||
|
||||
private static void Register(Type t) {
|
||||
foreach (var method in t.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) {
|
||||
OpCode op;
|
||||
if (Enum.TryParse(method.Name, out op))
|
||||
_executors[(int)op] = (OpExecute)method.CreateDelegate(typeof(OpExecute));
|
||||
}
|
||||
}
|
||||
|
||||
static VM() {
|
||||
Register(typeof(Flow));
|
||||
Register(typeof(BackgroundPal));
|
||||
Register(typeof(Audio));
|
||||
Register(typeof(WindowMenu));
|
||||
Register(typeof(FieldModels));
|
||||
Register(typeof(Maths));
|
||||
}
|
||||
|
||||
public static OpResult Execute(OpCode op, Fiber f, Entity e, FieldScreen s) {
|
||||
if (_executors[(int)op] == null) {
|
||||
if (ErrorOnUnknown)
|
||||
throw new F7Exception($"Cannot execute opcode {op}");
|
||||
f.Stop();
|
||||
f.Jump(f.IP - 1); //So if we retry, the opcode is actually retried [and will fail again] rather than trying the next operand byte as an opcode
|
||||
System.Diagnostics.Debug.WriteLine($"Aborting script due to unrecognised opcode {op}");
|
||||
return OpResult.Continue;
|
||||
} else
|
||||
return _executors[(int)op](f, e, s);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Flow {
|
||||
|
||||
public static OpResult RET(Fiber f, Entity e, FieldScreen s) {
|
||||
f.Stop();
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult WAIT(Fiber f, Entity e, FieldScreen s) {
|
||||
ushort frames = f.ReadU16();
|
||||
if (frames < f.OpcodeAttempts)
|
||||
return OpResult.Continue;
|
||||
else
|
||||
return OpResult.Restart;
|
||||
}
|
||||
|
||||
public static OpResult JMPF(Fiber f, Entity e, FieldScreen s) {
|
||||
int newIP = f.IP + f.ReadU8();
|
||||
f.Jump(newIP);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult JMPFL(Fiber f, Entity e, FieldScreen s) {
|
||||
int newIP = f.IP + f.ReadU16();
|
||||
f.Jump(newIP);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult JMPB(Fiber f, Entity e, FieldScreen s) {
|
||||
int newIP = f.IP - 1 - f.ReadU8();
|
||||
f.Jump(newIP);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult JMPBL(Fiber f, Entity e, FieldScreen s) {
|
||||
int newIP = f.IP - 1 - f.ReadU16();
|
||||
f.Jump(newIP);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
private static OpResult IfImpl(Fiber f, FieldScreen s, byte banks, int iVal1, int iVal2, byte comparison, int newIP) {
|
||||
int val1 = s.Game.Memory.Read(banks >> 4, iVal1), val2 = s.Game.Memory.Read(banks & 0xf, iVal2);
|
||||
bool match;
|
||||
switch (comparison) {
|
||||
case 0:
|
||||
match = val1 == val2; break;
|
||||
case 1:
|
||||
match = val1 != val2; break;
|
||||
case 2:
|
||||
match = val1 > val2; break;
|
||||
case 3:
|
||||
match = val1 < val2; break;
|
||||
case 4:
|
||||
match = val1 >= val2; break;
|
||||
case 5:
|
||||
match = val1 <= val2; break;
|
||||
case 6:
|
||||
match = (val1 & val2) != 0; break;
|
||||
case 7:
|
||||
match = (val1 ^ val2) != 0; break;
|
||||
case 8:
|
||||
match = (val1 | val2) != 0; break;
|
||||
case 9:
|
||||
match = (val2 & (1 << val2)) != 0; break;
|
||||
case 0xA:
|
||||
match = ~(val2 & (1 << val2)) != 0; break;
|
||||
default:
|
||||
throw new F7Exception($"Unrecognised comparison {comparison}");
|
||||
}
|
||||
|
||||
if (match)
|
||||
f.Jump(newIP);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult IFUB(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8(), bVal1 = f.ReadU8(), bVal2 = f.ReadU8(), comparison = f.ReadU8();
|
||||
int newIP = f.IP + f.ReadU8();
|
||||
return IfImpl(f, s, banks, bVal1, bVal2, comparison, newIP);
|
||||
}
|
||||
public static OpResult IFUBL(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8(), bVal1 = f.ReadU8(), bVal2 = f.ReadU8(), comparison = f.ReadU8();
|
||||
int newIP = f.IP + f.ReadU16();
|
||||
return IfImpl(f, s, banks, bVal1, bVal2, comparison, newIP);
|
||||
}
|
||||
public static OpResult IFSW(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8();
|
||||
short sVal1 = (short)f.ReadU16(), sVal2 = (short)f.ReadU16();
|
||||
byte comparison = f.ReadU8();
|
||||
int newIP = f.IP + f.ReadU8();
|
||||
return IfImpl(f, s, banks, sVal1, sVal2, comparison, newIP);
|
||||
}
|
||||
public static OpResult IFSWL(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8();
|
||||
short sVal1 = (short)f.ReadU16(), sVal2 = (short)f.ReadU16();
|
||||
byte comparison = f.ReadU8();
|
||||
int newIP = f.IP + f.ReadU16();
|
||||
return IfImpl(f, s, banks, sVal1, sVal2, comparison, newIP);
|
||||
}
|
||||
public static OpResult IFUW(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8();
|
||||
ushort sVal1 = f.ReadU16(), sVal2 = f.ReadU16();
|
||||
byte comparison = f.ReadU8();
|
||||
int newIP = f.IP + f.ReadU8();
|
||||
return IfImpl(f, s, banks, sVal1, sVal2, comparison, newIP);
|
||||
}
|
||||
public static OpResult IFUWL(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8();
|
||||
ushort sVal1 = f.ReadU16(), sVal2 = f.ReadU16();
|
||||
byte comparison = f.ReadU8();
|
||||
int newIP = f.IP + f.ReadU16();
|
||||
return IfImpl(f, s, banks, sVal1, sVal2, comparison, newIP);
|
||||
}
|
||||
|
||||
public static OpResult NOP(Fiber f, Entity e, FieldScreen s) {
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult REQ(Fiber f, Entity e, FieldScreen s) {
|
||||
int entity = f.ReadU8(), parm = f.ReadU8();
|
||||
s.Entities[entity].Call(parm >> 5, parm & 0x1f, null);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult REQSW(Fiber f, Entity e, FieldScreen s) {
|
||||
int entity = f.ReadU8(), parm = f.ReadU8();
|
||||
if (s.Entities[entity].Call(parm >> 5, parm & 0x1f, null))
|
||||
return OpResult.Continue;
|
||||
else
|
||||
return OpResult.Restart;
|
||||
}
|
||||
public static OpResult REQEW(Fiber f, Entity e, FieldScreen s) {
|
||||
int entity = f.ReadU8(), parm = f.ReadU8();
|
||||
if (s.Entities[entity].Call(parm >> 5, parm & 0x1f, f.Resume)) {
|
||||
f.Pause();
|
||||
return OpResult.Continue;
|
||||
} else
|
||||
return OpResult.Restart;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class BackgroundPal {
|
||||
public static OpResult BGON(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8(), bArea = f.ReadU8(), bLayer = f.ReadU8();
|
||||
int area = s.Game.Memory.Read(banks & 0xf, bArea);
|
||||
int layer = s.Game.Memory.Read(banks >> 4, bLayer);
|
||||
s.Background.ModifyParameter(area, i => i | (1 << layer));
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult BGOFF(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8(), bArea = f.ReadU8(), bLayer = f.ReadU8();
|
||||
int area = s.Game.Memory.Read(banks & 0xf, bArea);
|
||||
int layer = s.Game.Memory.Read(banks >> 4, bLayer);
|
||||
s.Background.ModifyParameter(area, i => i & ~(1 << layer));
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult BGCLR(Fiber f, Entity e, FieldScreen s) {
|
||||
byte bank = f.ReadU8(), bArea = f.ReadU8();
|
||||
int area = s.Game.Memory.Read(bank, bArea);
|
||||
s.Background.SetParameter(area, 0);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FieldModels {
|
||||
public static OpResult CC(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
s.Player = s.Entities[parm].Model; //TODO: also center screen etc.
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult CHAR(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
int modelIndex = s.GetNextModelIndex();
|
||||
if (parm != modelIndex)
|
||||
System.Diagnostics.Debug.WriteLine($"CHAR opcode - parameter {parm} did not match auto-assign ID {modelIndex}");
|
||||
e.Model = s.FieldModels[modelIndex];
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult PC(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
e.Character = s.Game.SaveData.Characters[parm];
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult DIR(Fiber f, Entity e, FieldScreen s) {
|
||||
byte bank = f.ReadU8(), parm = f.ReadU8();
|
||||
float rotation = 360f * s.Game.Memory.Read(bank, parm) / 255f;
|
||||
e.Model.Rotation = new Vector3(0, 0, rotation);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult XYZI(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks1 = f.ReadU8(), banks2 = f.ReadU8();
|
||||
short px = f.ReadS16(), py = f.ReadS16(), pz = f.ReadS16();
|
||||
ushort ptri = f.ReadU16();
|
||||
|
||||
int x = s.Game.Memory.Read(banks1 >> 4, px),
|
||||
y = s.Game.Memory.Read(banks1 & 0xf, py),
|
||||
z = s.Game.Memory.Read(banks2 >> 4, pz),
|
||||
tri = s.Game.Memory.Read(banks2 & 0xf, ptri);
|
||||
|
||||
e.WalkmeshTri = tri;
|
||||
e.Model.Translation = new Vector3(x, y, z);
|
||||
System.Diagnostics.Debug.WriteLine($"VM:XYZI moving {e.Name} to {e.Model.Translation} wmtri {tri}");
|
||||
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult TLKON(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
if (parm != 0)
|
||||
e.Flags |= EntityFlags.CanTalk;
|
||||
else
|
||||
e.Flags &= ~EntityFlags.CanTalk;
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult SOLID(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
if (parm != 0)
|
||||
e.Flags |= EntityFlags.CanCollide;
|
||||
else
|
||||
e.Flags &= ~EntityFlags.CanCollide;
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult VISI(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
e.Model.Visible = parm != 0;
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult ANIME1(Fiber f, Entity e, FieldScreen s) {
|
||||
byte anim = f.ReadU8(), speed = f.ReadU8();
|
||||
f.Pause();
|
||||
var state = e.Model.AnimationState;
|
||||
Action onComplete = () => {
|
||||
f.Resume();
|
||||
e.Model.AnimationState = state;
|
||||
};
|
||||
e.Model.PlayAnimation(anim, true, 1f / speed, onComplete); //TODO is this speed even vaguely correct?
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult ANIM_2(Fiber f, Entity e, FieldScreen s) {
|
||||
byte anim = f.ReadU8(), speed = f.ReadU8();
|
||||
f.Pause();
|
||||
e.Model.PlayAnimation(anim, true, 1f / speed, f.Resume); //TODO is this speed even vaguely correct?
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult DFANM(Fiber f, Entity e, FieldScreen s) {
|
||||
byte anim = f.ReadU8(), speed = f.ReadU8();
|
||||
e.Model.PlayAnimation(anim, true, 1f / speed, null); //TODO is this speed even vaguely correct?
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult ASPED(Fiber f, Entity e, FieldScreen s) {
|
||||
byte bank = f.ReadU8();
|
||||
ushort parm = f.ReadU16();
|
||||
int speed = s.Game.Memory.Read(bank, parm);
|
||||
e.Model.GlobalAnimationSpeed = speed / 16f; //TODO is this even vaguely close
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult MSPED(Fiber f, Entity e, FieldScreen s) {
|
||||
byte bank = f.ReadU8();
|
||||
ushort parm = f.ReadU16();
|
||||
e.MoveSpeed = s.Game.Memory.Read(bank, parm);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult TALKR(Fiber f, Entity e, FieldScreen s) {
|
||||
byte bank = f.ReadU8(), parm = f.ReadU8();
|
||||
e.TalkDistance = s.Game.Memory.Read(bank, parm);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
public static OpResult ANIMW(Fiber f, Entity e, FieldScreen s) {
|
||||
f.Pause();
|
||||
e.Model.AnimationState.AnimationComplete += f.Resume;
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
public static OpResult SLIDR(Fiber f, Entity e, FieldScreen s) {
|
||||
byte bank = f.ReadU8(), parm = f.ReadU8();
|
||||
e.CollideDistance = s.Game.Memory.Read(bank, parm);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class WindowMenu {
|
||||
public static OpResult MPNAM(Fiber f, Entity e, FieldScreen s) {
|
||||
byte parm = f.ReadU8();
|
||||
string name = s.Dialog.Dialogs[parm];
|
||||
s.Game.SaveData.Location = name;
|
||||
return OpResult.Continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static class Audio {
|
||||
|
||||
//TODO store in external file?
|
||||
private static readonly string[] _trackNames = new[] {
|
||||
"ERROR", "ERROR", "oa", "ob", "dun2", "guitar2", "fanfare", "makoro", "bat",
|
||||
"fiddle", "kurai", "chu", "ketc", "earis", "ta", "tb", "sato",
|
||||
"parade", "comical", "yume", "mati", "sido", "siera", "walz", "corneo",
|
||||
"horror", "canyon", "red", "seto", "ayasi", "sinra", "sinraslo", "dokubo",
|
||||
"bokujo", "tm", "tifa", "costa", "rocket", "earislo", "chase", "rukei",
|
||||
"cephiros", "barret", "corel", "boo", "elec", "rhythm", "fan2", "hiku",
|
||||
"cannon", "date", "cintro", "cinco", "chu2", "yufi", "aseri", "gold1",
|
||||
"mura1", "yado", "over2", "crwin", "crlost", "odds", "geki", "junon",
|
||||
"tender", "wind", "vincent", "bee", "jukai", "sadbar", "aseri2", "kita",
|
||||
"sid2", "sadsid", "iseki", "hen", "utai", "snow", "yufi2", "mekyu",
|
||||
"condor", "lb2", "gun", "weapon", "pj", "sea", "ld", "lb1",
|
||||
"sensui", "ro", "jyro", "nointro", "riku", "si", "mogu", "pre",
|
||||
"fin", "heart", "roll"
|
||||
};
|
||||
|
||||
public static OpResult MUSIC(Fiber f, Entity e, FieldScreen s) {
|
||||
byte track = f.ReadU8();
|
||||
//TODO!!!!
|
||||
//s.Game.Audio.PlayMusic(_trackNames[s.Script.MusicIDs.ElementAt(track)], false);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Maths {
|
||||
public static OpResult SETBYTE(Fiber f, Entity e, FieldScreen s) {
|
||||
byte banks = f.ReadU8(), dest = f.ReadU8(), src = f.ReadU8();
|
||||
int value = s.Game.Memory.Read(banks & 0xf, src);
|
||||
s.Game.Memory.Write(banks >> 4, dest, (byte)value);
|
||||
return OpResult.Continue;
|
||||
}
|
||||
}
|
||||
}
|
89
F7/Game1.cs
Normal file
89
F7/Game1.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace F7 {
|
||||
public class Game1 : Game {
|
||||
private GraphicsDeviceManager _graphics;
|
||||
private SpriteBatch _spriteBatch;
|
||||
|
||||
public Game1() {
|
||||
_graphics = new GraphicsDeviceManager(this);
|
||||
_graphics.PreferredBackBufferWidth = 1280 * 2;
|
||||
_graphics.PreferredBackBufferHeight = 720 * 2;
|
||||
_graphics.GraphicsProfile = GraphicsProfile.HiDef;
|
||||
Content.RootDirectory = "Content";
|
||||
IsMouseVisible = true;
|
||||
}
|
||||
|
||||
protected override void Initialize() {
|
||||
// TODO: Add your initialization logic here
|
||||
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
private Screen _screen;
|
||||
private FGame _g;
|
||||
|
||||
protected override void LoadContent() {
|
||||
_spriteBatch = new SpriteBatch(GraphicsDevice);
|
||||
|
||||
_g = new FGame(@"C:\games\ff7\data", @"C:\Users\ficed\Projects\F7\data");
|
||||
_g.NewGame();
|
||||
//_screen = new TestScreen(_g, GraphicsDevice);
|
||||
_screen = new Field.FieldScreen("mrkt2", _g, GraphicsDevice);
|
||||
}
|
||||
|
||||
private static Dictionary<Keys, InputKey> _keyMap = new Dictionary<Keys, InputKey> {
|
||||
[Keys.W] = InputKey.Up,
|
||||
[Keys.S] = InputKey.Down,
|
||||
[Keys.A] = InputKey.Left,
|
||||
[Keys.D] = InputKey.Right,
|
||||
[Keys.Enter] = InputKey.OK,
|
||||
[Keys.Space] = InputKey.Cancel,
|
||||
[Keys.F1] = InputKey.Start,
|
||||
[Keys.F2] = InputKey.Select,
|
||||
};
|
||||
|
||||
private InputState _input = new();
|
||||
|
||||
protected override void Update(GameTime gameTime) {
|
||||
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
|
||||
Exit();
|
||||
|
||||
var keyState = Keyboard.GetState();
|
||||
foreach(var key in _keyMap.Keys) {
|
||||
var ik = _keyMap[key];
|
||||
bool down = keyState.IsKeyDown(key);
|
||||
if (down) {
|
||||
if (_input.DownFor[ik] > 0)
|
||||
_input.DownFor[ik]++;
|
||||
else
|
||||
_input.DownFor[ik] = 1;
|
||||
} else {
|
||||
if (_input.DownFor[ik] > 0)
|
||||
_input.DownFor[ik] = -1;
|
||||
else
|
||||
_input.DownFor[ik] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_screen.ProcessInput(_input);
|
||||
|
||||
// TODO: Add your update logic here
|
||||
|
||||
base.Update(gameTime);
|
||||
|
||||
_screen.Step(gameTime);
|
||||
}
|
||||
|
||||
protected override void Draw(GameTime gameTime) {
|
||||
GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1f, 0);
|
||||
|
||||
base.Draw(gameTime);
|
||||
|
||||
_screen.Render();
|
||||
}
|
||||
}
|
||||
}
|
BIN
F7/Icon.ico
Normal file
BIN
F7/Icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 KiB |
34
F7/Input.cs
Normal file
34
F7/Input.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
public enum InputKey {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
OK,
|
||||
Cancel,
|
||||
Start,
|
||||
Select,
|
||||
}
|
||||
|
||||
public class InputState {
|
||||
public Dictionary<InputKey, int> DownFor { get; } = new();
|
||||
|
||||
public Vector2 Stick1 { get; set; }
|
||||
|
||||
public bool IsDown(InputKey k) => DownFor[k] > 0;
|
||||
public bool IsJustDown(InputKey k) => DownFor[k] == 1;
|
||||
public bool IsJustReleased(InputKey k) => DownFor[k] == -1;
|
||||
|
||||
public InputState() {
|
||||
foreach (InputKey k in Enum.GetValues<InputKey>())
|
||||
DownFor[k] = 0;
|
||||
}
|
||||
}
|
||||
}
|
14
F7/Program.cs
Normal file
14
F7/Program.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace F7
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
using (var game = new Game1())
|
||||
game.Run();
|
||||
}
|
||||
}
|
||||
}
|
117
F7/SaveData.cs
Normal file
117
F7/SaveData.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace F7 {
|
||||
[Flags]
|
||||
public enum CharFlags {
|
||||
None = 0,
|
||||
Sadness = 0x1,
|
||||
Fury = 0x2,
|
||||
BackRow = 0x4,
|
||||
|
||||
Party1 = 0x10,
|
||||
Party2 = 0x20,
|
||||
Party3 = 0x40,
|
||||
|
||||
Available = 0x100,
|
||||
|
||||
ANY_PARTY_SLOT = Party1 | Party2 | Party3,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum LimitBreaks {
|
||||
None = 0, //Not really valid
|
||||
Limit1_1 = 0x1,
|
||||
Limit1_2 = 0x2,
|
||||
Limit2_1 = 0x4,
|
||||
Limit2_2 = 0x8,
|
||||
Limit3_1 = 0x10,
|
||||
Limit3_2 = 0x20,
|
||||
Limit4_1 = 0x40,
|
||||
Limit4_2 = 0x80,
|
||||
}
|
||||
|
||||
public class Character {
|
||||
public int CharIndex { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Strength { get; set; }
|
||||
public int Vitality { get; set; }
|
||||
public int Magic { get; set; }
|
||||
public int Spirit { get; set; }
|
||||
public int Dexterity { get; set; }
|
||||
public int Luck { get; set; }
|
||||
public int StrBonus { get; set; }
|
||||
public int VitBonus { get; set; }
|
||||
public int MagBonus { get; set; }
|
||||
public int SprBonus { get; set; }
|
||||
public int DexBonus { get; set; }
|
||||
public int LckBonus { get; set; }
|
||||
|
||||
public int LimitLevel { get; set; }
|
||||
public int LimitBar { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public int EquipWeapon { get; set; }
|
||||
public int EquipArmour { get; set; }
|
||||
public int EquipAccessory { get; set; }
|
||||
|
||||
public CharFlags Flags { get; set; }
|
||||
public LimitBreaks LimitBreaks { get; set; }
|
||||
public int NumKills { get; set; }
|
||||
public int UsedLimit1_1 { get; set; }
|
||||
public int UsedLimit2_1 { get; set; }
|
||||
public int UsedLimit3_1 { get; set; }
|
||||
|
||||
public int CurrentHP { get; set; }
|
||||
public int BaseHP { get; set; }
|
||||
public int MaxHP { get; set; }
|
||||
public int CurrentMP { get; set; }
|
||||
public int BaseMP { get; set; }
|
||||
public int MaxMP { get; set; }
|
||||
public int XP { get; set; }
|
||||
public int XPTNL { get; set; }
|
||||
|
||||
public List<int> WeaponMateria { get; set; }
|
||||
public List<int> ArmourMateria { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
public float LevelProgress => 0.7f; //TODO!!!
|
||||
|
||||
[XmlIgnore]
|
||||
public bool IsBackRow => Flags.HasFlag(CharFlags.BackRow);
|
||||
}
|
||||
|
||||
public class SaveData {
|
||||
|
||||
private IEnumerable<Character> CharactersInParty() {
|
||||
var p1 = Characters.Where(c => c != null && c.Flags.HasFlag(CharFlags.Party1)).FirstOrDefault();
|
||||
if (p1 != null) yield return p1;
|
||||
var p2 = Characters.Where(c => c != null && c.Flags.HasFlag(CharFlags.Party2)).FirstOrDefault();
|
||||
if (p2 != null) yield return p2;
|
||||
var p3 = Characters.Where(c => c != null && c.Flags.HasFlag(CharFlags.Party3)).FirstOrDefault();
|
||||
if (p3 != null) yield return p3;
|
||||
}
|
||||
|
||||
public List<Character> Characters { get; set; }
|
||||
|
||||
public string Location { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
public Character[] Party => CharactersInParty().ToArray(); //Array so we can interop with Lua
|
||||
|
||||
public void Loaded() {
|
||||
Characters.Sort((c1, c2) => c1.CharIndex.CompareTo(c2.CharIndex));
|
||||
foreach (int i in Enumerable.Range(0, 8)) {
|
||||
while (Characters.Count <= i)
|
||||
Characters.Add(null);
|
||||
while ((Characters[i] != null) && (Characters[i].CharIndex > i))
|
||||
Characters.Insert(i, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
F7/Screen.cs
Normal file
26
F7/Screen.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
public abstract class Screen {
|
||||
|
||||
public FGame Game { get; }
|
||||
|
||||
protected GraphicsDevice _graphics;
|
||||
|
||||
protected Screen(FGame g, GraphicsDevice graphics) {
|
||||
Game = g;
|
||||
_graphics = graphics;
|
||||
}
|
||||
|
||||
public abstract void Step(GameTime elapsed);
|
||||
public abstract void Render();
|
||||
|
||||
public virtual void ProcessInput(InputState input) { }
|
||||
}
|
||||
}
|
37
F7/TestScreen.cs
Normal file
37
F7/TestScreen.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
internal class TestScreen : Screen {
|
||||
|
||||
private Field.FieldModel _model;
|
||||
private Viewer _viewer;
|
||||
|
||||
public TestScreen(FGame g, GraphicsDevice graphics) : base(g, graphics) {
|
||||
|
||||
graphics.BlendState = BlendState.AlphaBlend;
|
||||
|
||||
_model = new Field.FieldModel(graphics, g, "AUFF.hrc", new[] { "AVBF.a", "AVCA.a" });
|
||||
_model.PlayAnimation(1, true, 1f, null);
|
||||
|
||||
_viewer = new PerspView3D {
|
||||
CameraPosition = new Vector3(0, -50f, 10f),
|
||||
CameraForwards = new Vector3(0, 50f, -5f),
|
||||
CameraUp = Vector3.UnitZ,
|
||||
};
|
||||
}
|
||||
|
||||
public override void Render() {
|
||||
_model.Render(_viewer);
|
||||
}
|
||||
|
||||
public override void Step(GameTime elapsed) {
|
||||
_model.FrameStep();
|
||||
}
|
||||
}
|
||||
}
|
73
F7/Util.cs
Normal file
73
F7/Util.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
public static class Util {
|
||||
public static int MakePowerOfTwo(int i) {
|
||||
int n = 1;
|
||||
while (n < i)
|
||||
n <<= 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
public static Texture2D LoadTex(this GraphicsDevice graphics, Ficedula.FF7.TexFile tex, int palette) {
|
||||
var texture = new Texture2D(graphics, tex.Width, tex.Height, false, SurfaceFormat.Color); //TODO MIPMAPS!
|
||||
texture.SetData(
|
||||
tex.ApplyPalette(palette)
|
||||
.SelectMany(row => row)
|
||||
.ToArray()
|
||||
);
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static Vector2 ToX(this System.Numerics.Vector2 v) {
|
||||
return new Vector2(v.X, v.Y);
|
||||
}
|
||||
public static Vector3 ToX(this System.Numerics.Vector3 v) {
|
||||
return new Vector3(v.X, v.Y, v.Z);
|
||||
}
|
||||
|
||||
public static Color WithAlpha(this Color c, byte alpha) {
|
||||
c.A = alpha;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
public class F7Exception : Exception {
|
||||
public F7Exception(string msg) : base(msg) { }
|
||||
}
|
||||
|
||||
public static class Serialisation {
|
||||
public static void Serialise(object o, System.IO.Stream s) {
|
||||
new System.Xml.Serialization.XmlSerializer(o.GetType()).Serialize(s, o);
|
||||
}
|
||||
|
||||
public static T Deserialise<T>(System.IO.Stream s) {
|
||||
return (T)(new System.Xml.Serialization.XmlSerializer(typeof(T)).Deserialize(s));
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct VertexPositionNormalColorTexture : IVertexType {
|
||||
public Vector3 Position;
|
||||
public Vector3 Normal;
|
||||
public Color Color;
|
||||
public Vector2 TexCoord;
|
||||
|
||||
public static VertexDeclaration VertexDeclaration = new VertexDeclaration
|
||||
(
|
||||
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
|
||||
new VertexElement(12, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0),
|
||||
new VertexElement(24, VertexElementFormat.Color, VertexElementUsage.Color, 0),
|
||||
new VertexElement(28, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0)
|
||||
);
|
||||
VertexDeclaration IVertexType.VertexDeclaration { get { return VertexDeclaration; } }
|
||||
}
|
||||
|
||||
}
|
106
F7/VMM.cs
Normal file
106
F7/VMM.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
public class VMM {
|
||||
|
||||
private byte[][] _banks;
|
||||
private byte[] _scratch;
|
||||
|
||||
public VMM() {
|
||||
_banks = Enumerable.Range(0, 5)
|
||||
.Select(_ => new byte[256])
|
||||
.ToArray();
|
||||
_scratch = new byte[256];
|
||||
}
|
||||
|
||||
public void ResetScratch() {
|
||||
_scratch = new byte[256];
|
||||
}
|
||||
|
||||
public void Write(int bank, int offset, byte value) {
|
||||
switch (bank) {
|
||||
case 0:
|
||||
throw new F7Exception("Can't write to literal bank 0");
|
||||
case 1:
|
||||
_banks[0][offset] = value;
|
||||
break;
|
||||
case 2:
|
||||
_banks[0][offset * 2] = value;
|
||||
break;
|
||||
case 3:
|
||||
_banks[1][offset] = value;
|
||||
break;
|
||||
case 4:
|
||||
_banks[1][offset * 2] = value;
|
||||
break;
|
||||
case 0xB:
|
||||
_banks[2][offset] = value;
|
||||
break;
|
||||
case 0xC:
|
||||
_banks[2][offset * 2] = value;
|
||||
break;
|
||||
case 0xD:
|
||||
_banks[3][offset] = value;
|
||||
break;
|
||||
case 0xE:
|
||||
_banks[3][offset * 2] = value;
|
||||
break;
|
||||
case 0xF:
|
||||
_banks[4][offset] = value;
|
||||
break;
|
||||
case 7:
|
||||
_banks[4][offset * 2] = value;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
_scratch[offset] = value;
|
||||
break;
|
||||
case 6:
|
||||
_scratch[offset * 2] = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new F7Exception($"Unknown memory bank {bank}/{offset}");
|
||||
}
|
||||
}
|
||||
|
||||
public int Read(int bank, int offset) {
|
||||
switch (bank) {
|
||||
case 0:
|
||||
return offset;
|
||||
case 1:
|
||||
return _banks[0][offset];
|
||||
case 2:
|
||||
return _banks[0][offset * 2] | (_banks[0][offset * 2 + 1] << 8);
|
||||
case 3:
|
||||
return _banks[1][offset];
|
||||
case 4:
|
||||
return _banks[1][offset * 2] | (_banks[1][offset * 2 + 1] << 8);
|
||||
case 0xB:
|
||||
return _banks[2][offset];
|
||||
case 0xC:
|
||||
return _banks[2][offset * 2] | (_banks[2][offset * 2 + 1] << 8);
|
||||
case 0xD:
|
||||
return _banks[3][offset];
|
||||
case 0xE:
|
||||
return _banks[3][offset * 2] | (_banks[3][offset * 2 + 1] << 8);
|
||||
case 0xF:
|
||||
return _banks[4][offset];
|
||||
case 7:
|
||||
return _banks[4][offset * 2] | (_banks[4][offset * 2 + 1] << 8);
|
||||
|
||||
case 5:
|
||||
return _scratch[offset];
|
||||
case 6:
|
||||
return _scratch[offset * 2] | (_scratch[offset * 2 + 1] << 8);
|
||||
|
||||
default:
|
||||
throw new F7Exception($"Unknown memory bank {bank}/{offset}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
F7/Viewer.cs
Normal file
57
F7/Viewer.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace F7 {
|
||||
public abstract class Viewer {
|
||||
public abstract Matrix Projection { get; }
|
||||
public abstract Matrix View { get; }
|
||||
}
|
||||
|
||||
public class View2D : Viewer {
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public int CenterX { get; set; }
|
||||
public int CenterY { get; set; }
|
||||
|
||||
public override Matrix Projection => Matrix.CreateOrthographicOffCenter(
|
||||
CenterX - Width / 2, CenterX + Width / 2,
|
||||
CenterY - Height / 2, CenterY + Height / 2,
|
||||
-1, 1
|
||||
);
|
||||
public override Matrix View => Matrix.Identity;
|
||||
}
|
||||
|
||||
public abstract class View3D : Viewer {
|
||||
public float AspectRatio { get; set; } = 1280f / 720f;
|
||||
public float ZNear { get; set; } = 10f;
|
||||
public float ZFar { get; set; } = 10000f;
|
||||
public Vector3 CameraPosition { get; set; }
|
||||
public Vector3 CameraUp { get; set; }
|
||||
public Vector3 CameraForwards { get; set; }
|
||||
|
||||
public override Matrix View => Matrix.CreateLookAt(
|
||||
CameraPosition, CameraPosition + CameraPosition, CameraUp
|
||||
);
|
||||
}
|
||||
|
||||
public class OrthoView3D : View3D {
|
||||
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
|
||||
public override Matrix Projection => Matrix.CreateOrthographicOffCenter(
|
||||
-Width/2, Width/2, -Height/2, Height/2, ZNear, ZFar
|
||||
);
|
||||
}
|
||||
|
||||
public class PerspView3D : View3D {
|
||||
|
||||
public override Matrix Projection => Matrix.CreatePerspectiveFieldOfView(
|
||||
90f * (float)Math.PI / 180, AspectRatio, ZNear, ZFar
|
||||
);
|
||||
}
|
||||
}
|
43
F7/app.manifest
Normal file
43
F7/app.manifest
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="F7"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on and is
|
||||
is designed to work with. Uncomment the appropriate elements and Windows will
|
||||
automatically selected the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
</assembly>
|
15
F7Cmd/F7Cmd.csproj
Normal file
15
F7Cmd/F7Cmd.csproj
Normal file
@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Ficedula.FF7.Exporters\Ficedula.FF7.Exporters.csproj" />
|
||||
<ProjectReference Include="..\Ficedula.FF7\Ficedula.FF7.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
40
F7Cmd/Program.cs
Normal file
40
F7Cmd/Program.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
using Ficedula.FF7.Exporters;
|
||||
|
||||
Console.WriteLine("F7Cmd");
|
||||
|
||||
if (args.Length < 2) return;
|
||||
|
||||
if (args[0].Equals("LGP", StringComparison.OrdinalIgnoreCase)) {
|
||||
using(var lgp = new Ficedula.FF7.LGPFile(args[1])) {
|
||||
Console.WriteLine($"LGP file {args[1]}");
|
||||
foreach(string file in lgp.Filenames) {
|
||||
using(var data = lgp.Open(file)) {
|
||||
Console.WriteLine($" {file} size {data.Length}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args[0].Equals("Field", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
using(var lgp = new Ficedula.FF7.LGPFile(args[1])) {
|
||||
using(var ffile = lgp.Open(args[2])) {
|
||||
var field = new Ficedula.FF7.Field.FieldFile(ffile);
|
||||
var palettes = field.GetPalettes();
|
||||
var walkmesh = field.GetWalkmesh();
|
||||
var etables = field.GetEncounterTables();
|
||||
var cameras = field.GetCameraMatrices();
|
||||
var tg = field.GetTriggersAndGateways();
|
||||
var background = field.GetBackground();
|
||||
foreach(var layer in background.Export()) {
|
||||
File.WriteAllBytes(
|
||||
@$"C:\temp\layer{layer.Layer}_{layer.Key}.png",
|
||||
layer.Bitmap.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100).ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
var de = field.GetDialogEvent();
|
||||
var models = field.GetModels();
|
||||
}
|
||||
}
|
||||
}
|
8
F7Cmd/Properties/launchSettings.json
Normal file
8
F7Cmd/Properties/launchSettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"F7Cmd": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "Field C:\\games\\FF7\\data\\field\\flevel.lgp mrkt2"
|
||||
}
|
||||
}
|
||||
}
|
9
Ficedula.FF7/Ficedula.FF7.csproj
Normal file
9
Ficedula.FF7/Ficedula.FF7.csproj
Normal file
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
285
Ficedula.FF7/Field/Background.cs
Normal file
285
Ficedula.FF7/Field/Background.cs
Normal file
@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7.Field {
|
||||
public enum BlendType {
|
||||
Blend,
|
||||
Additive,
|
||||
Subtractive,
|
||||
QuarterAdd,
|
||||
None = 0xff,
|
||||
}
|
||||
|
||||
public class TileGroup {
|
||||
public BlendType Blend { get; set; }
|
||||
public int Parameter { get; set; }
|
||||
public int Mask { get; set; }
|
||||
|
||||
public int Initial { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public struct Sprite {
|
||||
|
||||
public short DestX, DestY, ZZ2a, ZZ2b, SrcX, SrcY;
|
||||
public short SrcX2, SrcY2, Width, Height, PaletteID, ID;
|
||||
public byte Param, State, Blending, ZZ3, TypeTrans, ZZ4;
|
||||
public short TextureID, TextureID2, Depth;
|
||||
public int IDBig;
|
||||
|
||||
public int SortKey => (TypeTrans << 16) | (Param << 8) | State;
|
||||
|
||||
public Sprite(Stream source, int layer) {
|
||||
DestX = source.ReadI16();
|
||||
DestY = source.ReadI16();
|
||||
ZZ2a = source.ReadI16();
|
||||
ZZ2b = source.ReadI16();
|
||||
SrcX = source.ReadI16();
|
||||
SrcY = source.ReadI16();
|
||||
SrcX2 = source.ReadI16();
|
||||
SrcY2 = source.ReadI16();
|
||||
Width = source.ReadI16();
|
||||
Height = source.ReadI16();
|
||||
PaletteID = source.ReadI16();
|
||||
ID = source.ReadI16();
|
||||
Param = (byte)source.ReadByte();
|
||||
State = (byte)source.ReadByte();
|
||||
Blending = (byte)source.ReadByte();
|
||||
ZZ3 = (byte)source.ReadByte();
|
||||
TypeTrans = (byte)source.ReadByte();
|
||||
ZZ4 = (byte)source.ReadByte();
|
||||
TextureID = source.ReadI16();
|
||||
TextureID2 = source.ReadI16();
|
||||
Depth = source.ReadI16();
|
||||
IDBig = source.ReadI32();
|
||||
|
||||
FixUp(layer);
|
||||
|
||||
source.Seek(12, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
public void FixUp(int layerID) {
|
||||
if (layerID > 0 && TextureID2 > 0) {
|
||||
SrcX = SrcX2; SrcY = SrcY2; TextureID = TextureID2;
|
||||
}
|
||||
if (layerID == 0) {
|
||||
Param = State = Blending = 0;
|
||||
}
|
||||
if (Blending == 0) TypeTrans = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
public class BackgroundPalette {
|
||||
public uint[] Colours;
|
||||
}
|
||||
|
||||
public class TexturePage {
|
||||
public List<byte[]> Data;
|
||||
}
|
||||
|
||||
public class Background {
|
||||
private List<TileGroup> _groups = new();
|
||||
|
||||
public Dictionary<int, TexturePage> Pages { get; }
|
||||
|
||||
public List<BackgroundPalette> Palettes { get; }
|
||||
|
||||
public short Width { get; private set; }
|
||||
public short Height { get; private set; }
|
||||
public IEnumerable<Sprite> Layer1 { get; private set; }
|
||||
public IEnumerable<Sprite> Layer2 { get; private set; }
|
||||
public IEnumerable<Sprite> Layer3 { get; private set; }
|
||||
public IEnumerable<Sprite> Layer4 { get; private set; }
|
||||
public IEnumerable<Sprite> AllSprites => Layer1.Concat(Layer2).Concat(Layer3).Concat(Layer4);
|
||||
public IEnumerable<IEnumerable<Sprite>> Layers => new[] { Layer1, Layer2, Layer3, Layer4 };
|
||||
|
||||
private static uint ExpandColour(ushort c) {
|
||||
uint r = (uint)(255f * (c & 0x1f) / 31f);
|
||||
uint g = (uint)(255f * ((c >> 5) & 0x1f) / 31f);
|
||||
uint b = (uint)(255f * ((c >> 10) & 0x1f) / 31f);
|
||||
//uint a = (c & 0x8000) != 0 ? (uint)0xff : 0;
|
||||
uint a = 0xff; //TODO
|
||||
return (a << 24) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
|
||||
public Background(Stream source, Palettes palettes) {
|
||||
byte[] palFlags = new byte[16];
|
||||
|
||||
Palettes = palettes
|
||||
.PaletteData
|
||||
.Select(cols => new BackgroundPalette {
|
||||
Colours = cols.Select(us => ExpandColour(us)).ToArray()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
int bgOffset = 0;
|
||||
source.Position = bgOffset + 0x28;
|
||||
Width = source.ReadI16();
|
||||
Height = source.ReadI16();
|
||||
short numSprites = source.ReadI16();
|
||||
|
||||
source.Position = bgOffset + 12;
|
||||
|
||||
source.Read(palFlags, 0, 16);
|
||||
|
||||
|
||||
foreach (int i in Enumerable.Range(0, Palettes.Count)) {
|
||||
if (palFlags[i] != 0)
|
||||
Palettes[i].Colours[0] = 0;
|
||||
#if DUMP_FIELD
|
||||
ImageLoader.DumpImage(palettes[i], $@"C:\temp\palette{i}.png");
|
||||
#endif
|
||||
}
|
||||
|
||||
source.Position = bgOffset + 0x34;
|
||||
Sprite[] layer1 = Enumerable.Range(0, numSprites)
|
||||
.Select(_ => new Sprite(source, 0))
|
||||
.ToArray();
|
||||
|
||||
Sprite[] layer2, layer3, layer4;
|
||||
layer2 = layer3 = layer4 = new Sprite[0];
|
||||
|
||||
int numSprites2 = 0, numSprites3 = 0, numSprites4 = 0;
|
||||
|
||||
source.Position = bgOffset + 0x34 + 52 * numSprites;
|
||||
|
||||
if (source.ReadByte() != 0) {
|
||||
source.Seek(4, SeekOrigin.Current);
|
||||
numSprites2 = source.ReadI16();
|
||||
if (numSprites2 > 0) {
|
||||
source.Seek(20, SeekOrigin.Current);
|
||||
layer2 = Enumerable.Range(0, numSprites2)
|
||||
.Select(_ => new Sprite(source, 1))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
if (source.ReadByte() != 0) {
|
||||
source.Seek(4, SeekOrigin.Current);
|
||||
numSprites3 = source.ReadI16();
|
||||
if (numSprites3 > 0) {
|
||||
source.Seek(14, SeekOrigin.Current);
|
||||
layer3 = Enumerable.Range(0, numSprites3)
|
||||
.Select(_ => new Sprite(source, 2))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
if (source.ReadByte() != 0) {
|
||||
source.Seek(4, SeekOrigin.Current);
|
||||
numSprites4 = source.ReadI16();
|
||||
if (numSprites4 > 0) {
|
||||
source.Seek(14, SeekOrigin.Current);
|
||||
layer4 = Enumerable.Range(0, numSprites3)
|
||||
.Select(_ => new Sprite(source, 3))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
Layer1 = layer1;
|
||||
Layer2 = layer2;
|
||||
Layer3 = layer3;
|
||||
Layer4 = layer4;
|
||||
|
||||
byte[] pageData = new byte[256 * 256];
|
||||
//int numPages = allSprites.Max(s => s.TextureID) + 1;
|
||||
|
||||
Pages = new();
|
||||
|
||||
source.Seek(7, SeekOrigin.Current);
|
||||
|
||||
Dictionary<int, byte[]> pages = new Dictionary<int, byte[]>();
|
||||
|
||||
foreach (int i in Enumerable.Range(0, 42)) {
|
||||
bool exists = source.ReadI16() != 0;
|
||||
if (exists) {
|
||||
short size = source.ReadI16(), depth = source.ReadI16();
|
||||
System.Diagnostics.Debug.Assert(depth == 1);
|
||||
source.Read(pageData, 0, pageData.Length);
|
||||
|
||||
TexturePage page = new TexturePage {
|
||||
Data = new List<byte[]>()
|
||||
};
|
||||
foreach (int y in Enumerable.Range(0, 256)) {
|
||||
byte[] pixels = new byte[256];
|
||||
Buffer.BlockCopy(pageData, 256 * y, pixels, 0, 256);
|
||||
page.Data.Add(pixels);
|
||||
}
|
||||
/*
|
||||
foreach (int dy in Enumerable.Range(0, 256))
|
||||
foreach (int dx in Enumerable.Range(0, 256))
|
||||
pageData[dx + (dy << 8)] = (byte)(dy & 0xf);
|
||||
*/
|
||||
|
||||
#if DUMP_FIELD
|
||||
ImageLoader.DumpImage(img, $@"C:\temp\page{i}.png");
|
||||
#endif
|
||||
Pages[i] = page;
|
||||
|
||||
pages[i] = pageData.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DUMP_FIELD
|
||||
int offsetX = -allSprites.Min(s => s.DestX),
|
||||
offsetY = -allSprites.Min(s => s.DestY);
|
||||
|
||||
int L = 0;
|
||||
foreach (var layer in new[] { layer1, layer2 }) {
|
||||
foreach (var tiles in layer.GroupBy(s => s.Blending)) {
|
||||
List<int[]> render = Enumerable.Range(0, height)
|
||||
.Select(_ => new int[width])
|
||||
.ToList();
|
||||
foreach (var tile in tiles.OrderBy(t => t.ID)) {
|
||||
int destX = tile.DestX + offsetX, destY = tile.DestY + offsetY;
|
||||
var src = pages[tile.TextureID];
|
||||
var pal = paletteData[tile.PaletteID];
|
||||
foreach (int y in Enumerable.Range(0, 16)) {
|
||||
foreach (int x in Enumerable.Range(0, 16)) {
|
||||
byte p = src[tile.SrcX + x + (tile.SrcY + y) * 256];
|
||||
// if (p != 0)
|
||||
render[destY + y][destX + x] = pal[p];
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageLoader.DumpImage(render, $@"C:\temp\layer{L}_{tiles.Key}.png");
|
||||
}
|
||||
L++;
|
||||
}
|
||||
#endif
|
||||
|
||||
var groups = AllSprites
|
||||
// .Where(s => s.DestX == 0 && s.DestY == 0)
|
||||
// .Skip(1)
|
||||
.GroupBy(s => s.SortKey)
|
||||
.OrderByDescending(group => group.Key)
|
||||
;
|
||||
/*
|
||||
foreach(var spr in groups.First())
|
||||
foreach (int y in Enumerable.Range(0, 16))
|
||||
foreach (int x in Enumerable.Range(0, 16)) {
|
||||
int index = pages[spr.TextureID][spr.SrcX + x + (spr.SrcY + y) * 256];
|
||||
int colour = paletteData[spr.PaletteID][index];
|
||||
System.Diagnostics.Debug.WriteLine($"X{x} Y{y} Data {index} Colour {colour:x8}");
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
foreach (var group in groups) {
|
||||
TileGroup tg = new TileGroup {
|
||||
Blend = (BlendType)group.First().TypeTrans,
|
||||
Parameter = group.First().Param,
|
||||
Mask = group.First().State,
|
||||
Initial = AddTiles(group),
|
||||
Count = group.Count() * 6,
|
||||
};
|
||||
_groups.Add(tg);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
98
Ficedula.FF7/Field/DialogEvent.cs
Normal file
98
Ficedula.FF7/Field/DialogEvent.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7.Field {
|
||||
|
||||
public class Entity {
|
||||
public string Name { get; }
|
||||
public List<int> Scripts { get; }
|
||||
|
||||
public Entity(string name, IEnumerable<int> scripts) {
|
||||
Name = name;
|
||||
Scripts = scripts.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class DialogEvent {
|
||||
|
||||
public string Creator { get; }
|
||||
public string Name { get; }
|
||||
public short Scale { get; }
|
||||
public List<Entity> Entities { get; }
|
||||
public List<string> Dialogs { get; }
|
||||
|
||||
public byte[] ScriptBytecode { get; }
|
||||
|
||||
public DialogEvent(Stream source) {
|
||||
source.Position = 0;
|
||||
Debug.Assert(source.ReadI16() == 0x0502);
|
||||
|
||||
string ReadName() {
|
||||
byte[] buffer = new byte[8];
|
||||
source.Read(buffer, 0, buffer.Length);
|
||||
return Encoding.UTF8.GetString(buffer).Trim('\0');
|
||||
}
|
||||
|
||||
byte nEntities = source.ReadU8(), nModels = source.ReadU8();
|
||||
ushort strOffset = source.ReadU16(), nAkaoOffsets = source.ReadU16();
|
||||
Scale = source.ReadI16();
|
||||
source.Seek(6, SeekOrigin.Current);
|
||||
|
||||
Creator = ReadName();
|
||||
Name = ReadName();
|
||||
|
||||
string[] entNames = Enumerable.Range(0, nEntities)
|
||||
.Select(_ => ReadName())
|
||||
.ToArray();
|
||||
|
||||
int[] akaoOffsets = Enumerable.Range(0, nAkaoOffsets)
|
||||
.Select(_ => source.ReadI32())
|
||||
.ToArray();
|
||||
|
||||
ushort[][] scripts = Enumerable.Range(0, nEntities)
|
||||
.Select(_ =>
|
||||
Enumerable.Range(0, 32)
|
||||
.Select(_ => source.ReadU16())
|
||||
.ToArray()
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
|
||||
ScriptBytecode = new byte[strOffset - scripts[0][0]];
|
||||
source.Position = scripts[0][0];
|
||||
source.Read(ScriptBytecode, 0, ScriptBytecode.Length);
|
||||
|
||||
Entities = new();
|
||||
foreach(int e in Enumerable.Range(0, nEntities)) {
|
||||
Entities.Add(new Entity(entNames[e], scripts[e].Select(us => us - scripts[0][0])));
|
||||
}
|
||||
|
||||
|
||||
source.Position = strOffset;
|
||||
ushort numDialog = source.ReadU16();
|
||||
ushort[] dlgOffsets = Enumerable.Range(0, numDialog)
|
||||
.Select(_ => source.ReadU16())
|
||||
.ToArray();
|
||||
|
||||
Dialogs = Enumerable.Range(0, numDialog)
|
||||
.Select(d => {
|
||||
source.Position = strOffset + dlgOffsets[d];
|
||||
List<byte> chars = new();
|
||||
byte c;
|
||||
while (true) {
|
||||
c = source.ReadU8();
|
||||
if (c == 0xff) break;
|
||||
chars.Add(c);
|
||||
}
|
||||
return Text.Convert(chars.ToArray(), 0, chars.Count);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
//TODO: AKAO
|
||||
}
|
||||
}
|
||||
}
|
313
Ficedula.FF7/Field/FieldFile.cs
Normal file
313
Ficedula.FF7/Field/FieldFile.cs
Normal file
@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ficedula.FF7.Field {
|
||||
|
||||
public struct FieldBounds {
|
||||
public short Left { get; private set; }
|
||||
public short Bottom { get; private set; }
|
||||
public short Right { get; private set; }
|
||||
public short Top { get; private set; }
|
||||
|
||||
public FieldBounds(Stream s) {
|
||||
Left = s.ReadI16();
|
||||
Bottom = s.ReadI16();
|
||||
Right = s.ReadI16();
|
||||
Top = s.ReadI16();
|
||||
}
|
||||
|
||||
public override string ToString() => $"Left: {Left} Top: {Top} Right: {Right} Bottom: {Bottom}";
|
||||
}
|
||||
|
||||
public class Gateway {
|
||||
public FieldVertex V0 { get; private set; }
|
||||
public FieldVertex V1 { get; private set; }
|
||||
public FieldVertex DestinationVertex { get; private set; }
|
||||
public short DestinationFieldID { get; private set; }
|
||||
public bool ShowArrow { get; set; }
|
||||
|
||||
public Gateway(Stream s) {
|
||||
V0 = new FieldVertex(s, false);
|
||||
V1 = new FieldVertex(s, false);
|
||||
DestinationVertex = new FieldVertex(s, false);
|
||||
DestinationFieldID = s.ReadI16();
|
||||
s.ReadI32();
|
||||
}
|
||||
}
|
||||
|
||||
public enum TriggerBehaviour : byte {
|
||||
|
||||
}
|
||||
public class Trigger {
|
||||
public FieldVertex V0 { get; private set; }
|
||||
public FieldVertex V1 { get; private set; }
|
||||
public byte BackgroundID { get; private set; }
|
||||
public byte BackgroundState { get; private set; }
|
||||
public TriggerBehaviour Behaviour { get; private set; }
|
||||
public byte SoundID { get; private set; }
|
||||
|
||||
public Trigger(Stream s) {
|
||||
V0 = new FieldVertex(s, false);
|
||||
V1 = new FieldVertex(s, false);
|
||||
BackgroundID = s.ReadU8();
|
||||
BackgroundState = s.ReadU8();
|
||||
Behaviour = (TriggerBehaviour)s.ReadU8();
|
||||
SoundID = s.ReadU8();
|
||||
}
|
||||
}
|
||||
|
||||
public enum ArrowType : int {
|
||||
Disabled = 0,
|
||||
Red = 1,
|
||||
Green = 2,
|
||||
}
|
||||
|
||||
public class Arrow {
|
||||
public Vector3 Position { get; private set; }
|
||||
public ArrowType Type { get; private set; }
|
||||
|
||||
public Arrow(Stream s) {
|
||||
Position = new Vector3(s.ReadI32(), s.ReadI32(), s.ReadI32());
|
||||
Type = (ArrowType)s.ReadI32();
|
||||
}
|
||||
}
|
||||
|
||||
public class TriggersAndGateways {
|
||||
public string Name { get; private set; }
|
||||
public byte ControlDirection { get; private set; }
|
||||
public short CameraFocus { get; private set; }
|
||||
public FieldBounds CameraRange { get; private set; }
|
||||
public short? BG3AnimWidth { get; private set; }
|
||||
public short? BG3AnimHeight { get; private set; }
|
||||
public short? BG4AnimWidth { get; private set; }
|
||||
public short? BG4AnimHeight { get; private set; }
|
||||
|
||||
public List<Gateway> Gateways { get; }
|
||||
public List<Trigger> Triggers { get; }
|
||||
public List<Arrow> Arrows { get; }
|
||||
|
||||
public TriggersAndGateways(Stream s) {
|
||||
s.Position = 0;
|
||||
byte[] bname = new byte[9];
|
||||
s.Read(bname, 0, 9);
|
||||
Name = Encoding.ASCII.GetString(bname).Trim();
|
||||
ControlDirection = s.ReadU8();
|
||||
CameraFocus = s.ReadI16();
|
||||
CameraRange = new FieldBounds(s);
|
||||
s.ReadI32();
|
||||
BG3AnimWidth = Util.ValueOrNull(s.ReadI16(), (short)1024);
|
||||
BG3AnimHeight = Util.ValueOrNull(s.ReadI16(), (short)1024);
|
||||
BG4AnimWidth = Util.ValueOrNull(s.ReadI16(), (short)1024);
|
||||
BG4AnimHeight = Util.ValueOrNull(s.ReadI16(), (short)1024);
|
||||
s.Seek(24, SeekOrigin.Current); //....
|
||||
Gateways = Enumerable.Range(0, 12)
|
||||
.Select(_ => new Gateway(s))
|
||||
.ToList();
|
||||
Triggers = Enumerable.Range(0, 12)
|
||||
.Select(_ => new Trigger(s))
|
||||
.ToList();
|
||||
foreach (var gateway in Gateways)
|
||||
gateway.ShowArrow = s.ReadU8() != 0;
|
||||
Arrows = Enumerable.Range(0, 12)
|
||||
.Select(_ => new Arrow(s))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class CameraMatrix {
|
||||
public Vector3 Forwards { get; private set; }
|
||||
public Vector3 Up { get; private set; }
|
||||
public Vector3 Right { get; private set; }
|
||||
public Vector3 CameraPosition { get; private set; }
|
||||
public short Zoom { get; private set; }
|
||||
|
||||
public CameraMatrix(Stream s) {
|
||||
Vector3 DecodeVec() {
|
||||
return new Vector3(
|
||||
s.ReadI16() / 4096f,
|
||||
s.ReadI16() / -4096f,
|
||||
s.ReadI16() / -4096f
|
||||
);
|
||||
}
|
||||
|
||||
Right = DecodeVec();
|
||||
Up = DecodeVec();
|
||||
Forwards = DecodeVec();
|
||||
|
||||
s.ReadI16();
|
||||
|
||||
int ox = s.ReadI32(), oy = s.ReadI32(), oz = s.ReadI32();
|
||||
|
||||
CameraPosition = -(ox * Right + oy * Up + oz * Forwards);
|
||||
|
||||
s.ReadI32();
|
||||
|
||||
Zoom = s.ReadI16();
|
||||
}
|
||||
}
|
||||
|
||||
public struct FieldVertex {
|
||||
public short X { get; set; }
|
||||
public short Y { get; set; }
|
||||
public short Z { get; set; }
|
||||
|
||||
public FieldVertex(Stream s, bool withPadding) {
|
||||
X = s.ReadI16();
|
||||
Y = s.ReadI16();
|
||||
Z = s.ReadI16();
|
||||
if (withPadding)
|
||||
s.ReadI16();
|
||||
}
|
||||
|
||||
public override string ToString() => $"X:{X} Y:{Y} Z:{Z}";
|
||||
}
|
||||
|
||||
public class WalkmeshTriangle {
|
||||
public FieldVertex V0 { get; set; }
|
||||
public FieldVertex V1 { get; set; }
|
||||
public FieldVertex V2 { get; set; }
|
||||
public short? V01Tri { get; set; }
|
||||
public short? V12Tri { get; set; }
|
||||
public short? V20Tri { get; set; }
|
||||
}
|
||||
|
||||
public class Walkmesh {
|
||||
public List<WalkmeshTriangle> Triangles { get; private set; }
|
||||
|
||||
public Walkmesh(Stream source) {
|
||||
source.Position = 0;
|
||||
int count = source.ReadI32();
|
||||
Triangles = Enumerable.Range(0, count)
|
||||
.Select(_ => new WalkmeshTriangle {
|
||||
V0 = new FieldVertex(source, true),
|
||||
V1 = new FieldVertex(source, true),
|
||||
V2 = new FieldVertex(source, true)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach(int i in Enumerable.Range(0, count)) {
|
||||
Triangles[i].V01Tri = Util.ValueOrNull(source.ReadI16(), (short)-1);
|
||||
Triangles[i].V12Tri = Util.ValueOrNull(source.ReadI16(), (short)-1);
|
||||
Triangles[i].V20Tri = Util.ValueOrNull(source.ReadI16(), (short)-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Encounter {
|
||||
public byte Frequency { get; set; }
|
||||
public ushort EncounterID { get; set; }
|
||||
|
||||
public Encounter(ushort value) {
|
||||
Frequency = (byte)(value >> 10);
|
||||
EncounterID = (ushort)(value & 0x3ff);
|
||||
}
|
||||
}
|
||||
|
||||
public class EncounterTable {
|
||||
public bool Enabled { get; set; }
|
||||
public byte Rate { get; set; }
|
||||
public List<Encounter> StandardEncounters { get; }
|
||||
public Encounter BackAttack1 { get; set; }
|
||||
public Encounter BackAttack2 { get; set; }
|
||||
public Encounter SideAttack { get; set; }
|
||||
public Encounter BothSidesAttack { get; set; }
|
||||
|
||||
public IEnumerable<Encounter> AllEncounters => StandardEncounters
|
||||
.Concat(new[] { BackAttack1, BackAttack2, SideAttack, BothSidesAttack });
|
||||
|
||||
public EncounterTable(Stream s) {
|
||||
Enabled = s.ReadByte() != 0;
|
||||
Rate = (byte)s.ReadByte();
|
||||
StandardEncounters = Enumerable.Range(0, 6)
|
||||
.Select(_ => new Encounter(s.ReadU16()))
|
||||
.ToList();
|
||||
BackAttack1 = new Encounter(s.ReadU16());
|
||||
BackAttack2 = new Encounter(s.ReadU16());
|
||||
SideAttack = new Encounter(s.ReadU16());
|
||||
BothSidesAttack = new Encounter(s.ReadU16());
|
||||
|
||||
s.ReadU16();
|
||||
}
|
||||
}
|
||||
|
||||
public class Palettes {
|
||||
public short PalX { get; private set; }
|
||||
public short PalY { get; private set; }
|
||||
public List<ushort[]> PaletteData { get; private set; }
|
||||
|
||||
public Palettes(Stream source) {
|
||||
source.Position = 4;
|
||||
PalX = source.ReadI16();
|
||||
PalY = source.ReadI16();
|
||||
int colours = source.ReadI16(), palcount = source.ReadI16();
|
||||
PaletteData = new List<ushort[]>();
|
||||
PaletteData = Enumerable.Range(0, palcount)
|
||||
.Select(_ => {
|
||||
ushort[] data = new ushort[256];
|
||||
foreach (int i in Enumerable.Range(0, 256)) {
|
||||
ushort colour = source.ReadU16();
|
||||
if (colour != 0)
|
||||
data[i] = colour;
|
||||
else
|
||||
data[i] = data[0];
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class FieldFile {
|
||||
|
||||
private List<Stream> _sections;
|
||||
|
||||
public FieldFile(Stream source) {
|
||||
using(var data = Lzss.Decode(source, true)) {
|
||||
data.Position = 2;
|
||||
if (data.ReadI32() != 9)
|
||||
throw new FFException($"Invalid field file number of section (should be 9)");
|
||||
|
||||
var offsets = Enumerable.Range(0, 9)
|
||||
.Select(_ => data.ReadI32())
|
||||
.ToArray();
|
||||
|
||||
_sections = offsets
|
||||
.Select(offset => {
|
||||
data.Position = offset;
|
||||
int size = data.ReadI32();
|
||||
byte[] section = new byte[size];
|
||||
data.Read(section, 0, section.Length);
|
||||
return (Stream)new MemoryStream(section);
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public Palettes GetPalettes() => new Palettes(_sections[3]);
|
||||
public Walkmesh GetWalkmesh() => new Walkmesh(_sections[4]);
|
||||
public TriggersAndGateways GetTriggersAndGateways() => new TriggersAndGateways(_sections[7]);
|
||||
public Background GetBackground() => new Background(_sections[8], GetPalettes());
|
||||
public DialogEvent GetDialogEvent() => new DialogEvent(_sections[0]);
|
||||
public FieldModels GetModels() => new FieldModels(_sections[2]);
|
||||
|
||||
public IEnumerable<EncounterTable> GetEncounterTables() {
|
||||
_sections[6].Position = 0;
|
||||
var table0 = new EncounterTable(_sections[6]);
|
||||
var table1 = new EncounterTable(_sections[6]);
|
||||
return new[] { table0, table1 };
|
||||
}
|
||||
|
||||
public IEnumerable<CameraMatrix> GetCameraMatrices() {
|
||||
_sections[1].Position = 0;
|
||||
List<CameraMatrix> matrices = new();
|
||||
while (_sections[1].Position < _sections[1].Length)
|
||||
matrices.Add(new CameraMatrix(_sections[1]));
|
||||
return matrices.AsReadOnly();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
110
Ficedula.FF7/Field/HRCModel.cs
Normal file
110
Ficedula.FF7/Field/HRCModel.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7.Field {
|
||||
public class HRCModel {
|
||||
|
||||
public List<Bone> Bones { get; } = new();
|
||||
public Bone Root { get; }
|
||||
|
||||
public class BonePolygon {
|
||||
public PFile PFile { get; }
|
||||
public List<TexFile> Textures { get; }
|
||||
|
||||
public BonePolygon(PFile pFile, List<TexFile> textures) {
|
||||
PFile = pFile;
|
||||
Textures = textures;
|
||||
}
|
||||
}
|
||||
|
||||
public class Bone {
|
||||
public List<Bone> Children { get; } = new();
|
||||
public float Length { get; }
|
||||
public List<BonePolygon> Polygons { get; } = new();
|
||||
public int Index { get; }
|
||||
|
||||
public Bone(float length, int index) {
|
||||
Length = length;
|
||||
Index = index;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public HRCModel(Func<string, Stream> dataProvider, string hrcFile) {
|
||||
using (var hrc = dataProvider(hrcFile)) {
|
||||
var lines = hrc.ReadAllLines().ToArray();
|
||||
int numBones = int.Parse(lines[2].Split(null).Last());
|
||||
Dictionary<string, Bone> bones = new Dictionary<string, Bone>(StringComparer.InvariantCultureIgnoreCase);
|
||||
Root = new Bone(0, -1);
|
||||
bones.Add("root", Root);
|
||||
|
||||
foreach (int b in Enumerable.Range(0, numBones)) {
|
||||
Bone bone = new Bone(float.Parse(lines[6 + 5 * b]), b);
|
||||
foreach (string rsd in lines[7 + 5 * b].Split(null).Skip(1)) {
|
||||
if (string.IsNullOrWhiteSpace(rsd)) continue;
|
||||
var rsdLines = dataProvider(rsd + ".RSD").ReadAllLines().ToArray();
|
||||
string pFile = rsdLines
|
||||
.First(s => s.StartsWith("PLY=", StringComparison.InvariantCultureIgnoreCase))
|
||||
.Substring(4)
|
||||
.Replace(".PLY", ".P");
|
||||
int numTex = int.Parse(rsdLines
|
||||
.First(s => s.StartsWith("NTEX="))
|
||||
.Substring(5)
|
||||
);
|
||||
BonePolygon bp = new BonePolygon(
|
||||
new PFile(dataProvider(pFile)),
|
||||
Enumerable.Range(0, numTex)
|
||||
.Select(n => rsdLines.First(s => s.StartsWith($"TEX[{n}]=")).Substring(7).Replace(".TIM", ".TEX"))
|
||||
.Select(t => new TexFile(dataProvider(t)))
|
||||
.ToList()
|
||||
);
|
||||
bone.Polygons.Add(bp);
|
||||
}
|
||||
bones.Add(lines[4 + 5 * b], bone);
|
||||
bones[lines[5 + 5 * b]].Children.Add(bone);
|
||||
Bones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HRCModel(LGPFile lgp, string hrcFile) : this(lgp.Open, hrcFile) { }
|
||||
|
||||
}
|
||||
|
||||
public class FieldAnim {
|
||||
|
||||
public int BoneCount { get; private set; }
|
||||
public List<Frame> Frames { get; private set; }
|
||||
|
||||
public class Frame {
|
||||
public Vector3 Translation { get; }
|
||||
public Vector3 Rotation { get; }
|
||||
public List<Vector3> Bones { get; }
|
||||
|
||||
internal Frame(Stream s, int boneCount) {
|
||||
Rotation = new Vector3(s.ReadF32(), s.ReadF32(), s.ReadF32());
|
||||
Translation = new Vector3(s.ReadF32(), s.ReadF32(), s.ReadF32());
|
||||
Bones = Enumerable.Range(0, boneCount)
|
||||
.Select(_ => new Vector3(s.ReadF32(), s.ReadF32(), s.ReadF32()))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public FieldAnim(Stream s) {
|
||||
Debug.Assert(s.ReadI32() == 1);
|
||||
int frameCount = s.ReadI32();
|
||||
BoneCount = s.ReadI32();
|
||||
int rotationOrder = s.ReadI32();
|
||||
Debug.Assert(rotationOrder == 0x020001);
|
||||
s.Seek(20, SeekOrigin.Current);
|
||||
Frames = Enumerable.Range(0, frameCount)
|
||||
.Select(_ => new Frame(s, BoneCount))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
76
Ficedula.FF7/Field/ModelLoader.cs
Normal file
76
Ficedula.FF7/Field/ModelLoader.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7.Field {
|
||||
|
||||
public class LoadModel {
|
||||
public string Name { get; }
|
||||
public string HRC { get; }
|
||||
public string Scale { get; }
|
||||
public uint Light1Color { get; }
|
||||
public FieldVertex Light1Pos { get; }
|
||||
public uint Light2Color { get; }
|
||||
public FieldVertex Light2Pos { get; }
|
||||
public uint Light3Color { get; }
|
||||
public FieldVertex Light3Pos { get; }
|
||||
public uint GlobalLightColor { get; }
|
||||
public List<string> Animations { get; }
|
||||
|
||||
public LoadModel(Stream s) {
|
||||
|
||||
string GetStr(ushort? size) {
|
||||
size ??= s.ReadU16();
|
||||
byte[] buffer = new byte[size.Value];
|
||||
s.Read(buffer, 0, buffer.Length);
|
||||
return Encoding.ASCII.GetString(buffer).Trim('\0');
|
||||
}
|
||||
|
||||
Name = GetStr(null);
|
||||
s.ReadU16(); //???
|
||||
HRC = GetStr(8);
|
||||
Scale = GetStr(4);
|
||||
|
||||
ushort animCount = s.ReadU16();
|
||||
Light1Color = s.ReadU32() & 0xffffff;
|
||||
s.Seek(-1, SeekOrigin.Current);
|
||||
Light1Pos = new FieldVertex(s, false);
|
||||
|
||||
Light2Color = s.ReadU32() & 0xffffff;
|
||||
s.Seek(-1, SeekOrigin.Current);
|
||||
Light2Pos = new FieldVertex(s, false);
|
||||
|
||||
Light3Color = s.ReadU32() & 0xffffff;
|
||||
s.Seek(-1, SeekOrigin.Current);
|
||||
Light3Pos = new FieldVertex(s, false);
|
||||
|
||||
GlobalLightColor = s.ReadU32() & 0xffffff;
|
||||
s.Seek(-1, SeekOrigin.Current);
|
||||
|
||||
Animations = Enumerable.Range(0, animCount)
|
||||
.Select(_ => {
|
||||
string anim = GetStr(null);
|
||||
s.ReadU16();
|
||||
return anim;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class FieldModels {
|
||||
public short ModelScale { get; }
|
||||
public List<LoadModel> Models { get; }
|
||||
|
||||
public FieldModels(Stream source) {
|
||||
source.Position = 0;
|
||||
source.ReadI16();
|
||||
int count = source.ReadI16();
|
||||
ModelScale = source.ReadI16();
|
||||
Models = Enumerable.Range(0, count)
|
||||
.Select(_ => new LoadModel(source))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
92
Ficedula.FF7/LGP.cs
Normal file
92
Ficedula.FF7/LGP.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7 {
|
||||
public class LGPFile : IDisposable {
|
||||
|
||||
private class Entry {
|
||||
public int Offset { get; init; }
|
||||
public string Name { get; init; }
|
||||
public bool ExtendedName { get; init; }
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string FullPath {
|
||||
get {
|
||||
if (Path == null)
|
||||
return Name;
|
||||
else
|
||||
return Path + "/" + Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream _source;
|
||||
private Dictionary<string, Entry> _entries;
|
||||
|
||||
public IEnumerable<string> Filenames => _entries.Select(e => e.Value.FullPath);
|
||||
|
||||
public LGPFile(string filename) : this(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) {
|
||||
}
|
||||
|
||||
public LGPFile(Stream source) {
|
||||
_source = source;
|
||||
_source.Position = 0;
|
||||
if (_source.ReadI16() != 0)
|
||||
throw new FFException("Invalid LGP file: bad header(0)");
|
||||
if (!_source.ReadAscii(10).Equals("SQUARESOFT"))
|
||||
throw new FFException("Invalid LGP file: bad header(1)");
|
||||
|
||||
int numFiles = _source.ReadI32();
|
||||
List<Entry> tempEntries = new List<Entry>();
|
||||
foreach (int i in Enumerable.Range(0, numFiles)) {
|
||||
string name = _source.ReadAscii(20).Trim('\0', ' ');
|
||||
int offset = _source.ReadI32();
|
||||
_source.ReadByte();
|
||||
bool extended = _source.ReadI16() != 0;
|
||||
tempEntries.Add(new Entry {
|
||||
Name = name,
|
||||
Offset = offset,
|
||||
ExtendedName = extended,
|
||||
});
|
||||
}
|
||||
if (tempEntries.Any(e => e.ExtendedName)) {
|
||||
_source.Seek(3600, System.IO.SeekOrigin.Current);
|
||||
foreach (int _ in Enumerable.Range(0, _source.ReadI16())) {
|
||||
foreach (int __ in Enumerable.Range(0, _source.ReadI16())) {
|
||||
string path = _source.ReadAscii(128);
|
||||
int entry = _source.ReadI16();
|
||||
tempEntries[entry].Path = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
_entries = tempEntries
|
||||
.ToDictionary(e => e.FullPath, e => e, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public Stream? TryOpen(string name) {
|
||||
if (_entries.TryGetValue(name, out Entry? e)) {
|
||||
_source.Position = e.Offset + 20;
|
||||
int length = _source.ReadI32();
|
||||
//TODO: don't always load into memory, support passthrough reading from source?
|
||||
byte[] buffer = new byte[length];
|
||||
_source.Read(buffer, 0, length);
|
||||
var mem = new MemoryStream(buffer);
|
||||
return mem;
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
public Stream Open(string name) {
|
||||
var s = TryOpen(name);
|
||||
if (s == null)
|
||||
throw new FFException($"LGP file {name} not found");
|
||||
return s;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_source.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
200
Ficedula.FF7/Lzss.cs
Normal file
200
Ficedula.FF7/Lzss.cs
Normal file
@ -0,0 +1,200 @@
|
||||
namespace Ficedula.FF7 {
|
||||
public static class Lzss {
|
||||
private const int N = 4096;
|
||||
private const int F = 18;
|
||||
private const int THRESHOLD = 2;
|
||||
private const int NIL = N;
|
||||
|
||||
public static void Encode(Stream input, Stream output) {
|
||||
new EncodeContext().Encode(input, output);
|
||||
}
|
||||
public static void Decode(Stream input, Stream output) {
|
||||
new EncodeContext().Decode(input, output);
|
||||
}
|
||||
public static MemoryStream Decode(Stream input, bool withLengthHeader) {
|
||||
var ms = new MemoryStream();
|
||||
if (withLengthHeader)
|
||||
ms.Capacity = input.ReadI32();
|
||||
Decode(input, ms);
|
||||
ms.Position = 0;
|
||||
return ms;
|
||||
}
|
||||
|
||||
private class EncodeContext {
|
||||
public byte[] buffer = new byte[N + F];
|
||||
public int MatchPos, MatchLen;
|
||||
public int[] Lson = new int[N + 1];
|
||||
public int[] Rson = new int[N + 257];
|
||||
public int[] Dad = new int[N + 1];
|
||||
|
||||
public void InitTree() {
|
||||
for (int i = N + 1; i <= N + 256; i++) Rson[i] = NIL;
|
||||
for (int i = 0; i < N; i++) Dad[i] = NIL;
|
||||
}
|
||||
|
||||
public void InsertNode(int r) {
|
||||
int i, p, cmp;
|
||||
int key = r;
|
||||
cmp = 1;
|
||||
p = N + 1 + buffer[key];
|
||||
Rson[r] = Lson[r] = NIL;
|
||||
MatchLen = 0;
|
||||
|
||||
while (true) {
|
||||
if (cmp >= 0) {
|
||||
if (Rson[p] != NIL)
|
||||
p = Rson[p];
|
||||
else {
|
||||
Rson[p] = r;
|
||||
Dad[r] = p;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (Lson[p] != NIL)
|
||||
p = Lson[p];
|
||||
else {
|
||||
Lson[p] = r;
|
||||
Dad[r] = p;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (i = 1; i < F; i++)
|
||||
if ((cmp = buffer[key + i] - buffer[p + i]) != 0) break;
|
||||
if (i > MatchLen) {
|
||||
MatchPos = p;
|
||||
if ((MatchLen = i) >= F) break;
|
||||
}
|
||||
}
|
||||
Dad[r] = Dad[p]; Lson[r] = Lson[p]; Rson[r] = Rson[p];
|
||||
Dad[Lson[p]] = r; Dad[Rson[p]] = r;
|
||||
if (Rson[Dad[p]] == p)
|
||||
Rson[Dad[p]] = r;
|
||||
else
|
||||
Lson[Dad[p]] = r;
|
||||
Dad[p] = NIL;
|
||||
}
|
||||
|
||||
public void DeleteNode(int p) {
|
||||
int q;
|
||||
if (Dad[p] == NIL) return;
|
||||
if (Rson[p] == NIL)
|
||||
q = Lson[p];
|
||||
else if (Lson[p] == NIL)
|
||||
q = Rson[p];
|
||||
else {
|
||||
q = Lson[p];
|
||||
if (Rson[q] != NIL) {
|
||||
do {
|
||||
q = Rson[q];
|
||||
} while (Rson[q] != NIL);
|
||||
Rson[Dad[q]] = Lson[q]; Dad[Lson[q]] = Dad[q];
|
||||
Lson[q] = Lson[p]; Dad[Lson[p]] = q;
|
||||
}
|
||||
Rson[q] = Rson[p]; Dad[Rson[p]] = q;
|
||||
}
|
||||
Dad[q] = Dad[p];
|
||||
if (Rson[Dad[p]] == p)
|
||||
Rson[Dad[p]] = q;
|
||||
else
|
||||
Lson[Dad[p]] = q;
|
||||
Dad[p] = NIL;
|
||||
}
|
||||
|
||||
|
||||
public void Encode(Stream input, Stream output) /* was Encode(void) */
|
||||
{
|
||||
int i, c, len, r, s, last_match_length, code_buf_ptr;
|
||||
byte[] code_buf = new byte[17];
|
||||
byte mask;
|
||||
InitTree(); /* initialize trees */
|
||||
code_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and
|
||||
code_buf[0] works as eight flags, "1" representing that the unit
|
||||
is an unencoded letter (1 byte), "0" a position-and-length pair
|
||||
(2 bytes). Thus, eight units require at most 16 bytes of code. */
|
||||
code_buf_ptr = mask = 1;
|
||||
s = 0; r = N - F;
|
||||
for (i = s; i < r; i++) buffer[i] = 0;
|
||||
for (len = 0; len < F && (c = input.ReadByte()) != -1; len++)
|
||||
buffer[r + len] = (byte)c; /* Read F bytes into the last F bytes of
|
||||
the buffer */
|
||||
if (len == 0) return; /* text of size zero */
|
||||
for (i = 1; i <= F; i++) InsertNode(r - i); /* Insert the F strings,
|
||||
each of which begins with one or more 'space' characters. Note
|
||||
the order in which these strings are inserted. This way,
|
||||
degenerate trees will be less likely to occur. */
|
||||
InsertNode(r); /* Finally, insert the whole string just read. The
|
||||
global variables match_length and match_position are set. */
|
||||
do {
|
||||
if (MatchLen > len) MatchLen = len; /* match_length
|
||||
may be spuriously long near the end of text. */
|
||||
if (MatchLen <= THRESHOLD) {
|
||||
MatchLen = 1; /* Not long enough match. Send one byte. */
|
||||
code_buf[0] |= mask; /* 'send one byte' flag */
|
||||
code_buf[code_buf_ptr++] = buffer[r]; /* Send uncoded. */
|
||||
} else {
|
||||
code_buf[code_buf_ptr++] = (byte)MatchPos;
|
||||
code_buf[code_buf_ptr++] = (byte)(((MatchPos >> 4) & 0xf0) | (MatchLen - (THRESHOLD + 1))); /* Send position and
|
||||
length pair. Note match_length > THRESHOLD. */
|
||||
}
|
||||
if ((mask <<= 1) == 0) { /* Shift mask left one bit. */
|
||||
for (i = 0; i < code_buf_ptr; i++) /* Send at most 8 units of */
|
||||
output.WriteByte(code_buf[i]); /* code together */
|
||||
code_buf[0] = 0; code_buf_ptr = mask = 1;
|
||||
}
|
||||
last_match_length = MatchLen;
|
||||
for (i = 0; i < last_match_length &&
|
||||
(c = input.ReadByte()) != -1; i++) {
|
||||
DeleteNode(s); /* Delete old strings and */
|
||||
buffer[s] = (byte)c; /* read new bytes */
|
||||
if (s < F - 1) buffer[s + N] = (byte)c; /* If the position is
|
||||
near the end of buffer, extend the buffer to make
|
||||
string comparison easier. */
|
||||
s = (s + 1) & (N - 1); r = (r + 1) & (N - 1);
|
||||
/* Since this is a ring buffer, increment the position
|
||||
modulo N. */
|
||||
InsertNode(r); /* Register the string in text_buf[r..r+F-1] */
|
||||
}
|
||||
while (i++ < last_match_length) { /* After the end of text, */
|
||||
DeleteNode(s); /* no need to read, but */
|
||||
s = (s + 1) & (N - 1); r = (r + 1) & (N - 1);
|
||||
if ((--len) != 0) InsertNode(r); /* buffer may not be empty. */
|
||||
}
|
||||
} while (len > 0); /* until length of string to be processed is zero */
|
||||
if (code_buf_ptr > 1) { /* Send remaining code. */
|
||||
for (i = 0; i < code_buf_ptr; i++) output.WriteByte(code_buf[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
public void Decode(Stream input, Stream output) /* was Decode(void)
|
||||
Just the reverse of Encode(). */
|
||||
{
|
||||
int i, j, k, r, c;
|
||||
int flags;
|
||||
|
||||
for (i = 0; i < N - F; i++) buffer[i] = 0;
|
||||
r = N - F; flags = 0;
|
||||
for (; ; ) {
|
||||
if (((flags >>= 1) & 256) == 0) {
|
||||
if ((c = input.ReadByte()) == -1) break;
|
||||
flags = c | 0xff00; /* uses higher byte cleverly */
|
||||
} /* to count eight */
|
||||
if ((flags & 1) != 0) {
|
||||
if ((c = input.ReadByte()) == -1) break;
|
||||
output.WriteByte((byte)c); buffer[r++] = (byte)c; r &= (N - 1);
|
||||
} else {
|
||||
if ((i = input.ReadByte()) == -1) break;
|
||||
if ((j = input.ReadByte()) == -1) break;
|
||||
i |= ((j & 0xf0) << 4); j = (j & 0x0f) + THRESHOLD;
|
||||
for (k = 0; k <= j; k++) {
|
||||
c = buffer[(i + k) & (N - 1)];
|
||||
output.WriteByte((byte)c); buffer[r++] = (byte)c; r &= (N - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
Ficedula.FF7/PFile.cs
Normal file
176
Ficedula.FF7/PFile.cs
Normal file
@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7 {
|
||||
|
||||
public class PFileVert {
|
||||
public Vector3 Position { get; set; }
|
||||
public Vector3 Normal { get; set; }
|
||||
public uint Colour { get; set; }
|
||||
public Vector2 TexCoord { get; set; }
|
||||
}
|
||||
|
||||
public class PFileChunk {
|
||||
public int? Texture { get; }
|
||||
public List<PFileVert> Verts { get; }
|
||||
public List<int> Indices { get; }
|
||||
|
||||
public PFileChunk(int? texture, List<PFileVert> verts, List<int> indices) {
|
||||
Texture = texture;
|
||||
Verts = verts;
|
||||
Indices = indices;
|
||||
}
|
||||
}
|
||||
|
||||
public class PFile {
|
||||
|
||||
public List<PFileChunk> Chunks { get; } = new();
|
||||
|
||||
private class Polygon {
|
||||
public ushort U0,
|
||||
V0, V1, V2,
|
||||
N0, N1, N2,
|
||||
E0, E1, E2,
|
||||
U1, U2;
|
||||
|
||||
public Polygon(System.IO.Stream s) {
|
||||
U0 = s.ReadU16();
|
||||
V0 = s.ReadU16();
|
||||
V1 = s.ReadU16();
|
||||
V2 = s.ReadU16();
|
||||
N0 = s.ReadU16();
|
||||
N1 = s.ReadU16();
|
||||
N2 = s.ReadU16();
|
||||
E0 = s.ReadU16();
|
||||
E1 = s.ReadU16();
|
||||
E2 = s.ReadU16();
|
||||
U1 = s.ReadU16();
|
||||
U2 = s.ReadU16();
|
||||
}
|
||||
}
|
||||
|
||||
private class Group {
|
||||
public int PrimitiveType, PolygonStartIndex, NumPolygons,
|
||||
VerticesStartIndex, NumVertices, EdgeStartIndex,
|
||||
NumEdges, U1, U2, U3, U4,
|
||||
TexCoordStartIndex, AreTexturesUsed, TextureNumber;
|
||||
|
||||
public Group(System.IO.Stream s) {
|
||||
PrimitiveType = s.ReadI32();
|
||||
PolygonStartIndex = s.ReadI32();
|
||||
NumPolygons = s.ReadI32();
|
||||
VerticesStartIndex = s.ReadI32();
|
||||
NumVertices = s.ReadI32();
|
||||
EdgeStartIndex = s.ReadI32();
|
||||
NumEdges = s.ReadI32();
|
||||
U1 = s.ReadI32();
|
||||
U2 = s.ReadI32();
|
||||
U3 = s.ReadI32();
|
||||
U4 = s.ReadI32();
|
||||
TexCoordStartIndex = s.ReadI32();
|
||||
AreTexturesUsed = s.ReadI32();
|
||||
TextureNumber = s.ReadI32();
|
||||
}
|
||||
}
|
||||
|
||||
public PFile(Stream s) {
|
||||
s.Position = 12;
|
||||
int numVertices = s.ReadI32(),
|
||||
numNormals = s.ReadI32(),
|
||||
Dummy1 = s.ReadI32(),
|
||||
numTexCoords = s.ReadI32(),
|
||||
numVertexColours = s.ReadI32(),
|
||||
numEdges = s.ReadI32(),
|
||||
numPolys = s.ReadI32(),
|
||||
numUnknown2 = s.ReadI32(),
|
||||
numUnknown3 = s.ReadI32(),
|
||||
numHundreds = s.ReadI32(),
|
||||
numGroups = s.ReadI32(),
|
||||
numBoundingBoxes = s.ReadI32();
|
||||
s.Seek(17 * 4, System.IO.SeekOrigin.Current);
|
||||
|
||||
Vector3[] pVerts = Enumerable.Range(0, numVertices)
|
||||
.Select(_ => new Vector3(s.ReadF32(), s.ReadF32(), s.ReadF32()))
|
||||
.ToArray();
|
||||
|
||||
Vector3[] pNormals = Enumerable.Range(0, numNormals)
|
||||
.Select(_ => new Vector3(s.ReadF32(), s.ReadF32(), s.ReadF32()))
|
||||
.ToArray();
|
||||
|
||||
s.Seek(12 * Dummy1, System.IO.SeekOrigin.Current);
|
||||
|
||||
Vector2[] pTexCoord = Enumerable.Range(0, numTexCoords)
|
||||
.Select(_ => new Vector2(s.ReadF32(), s.ReadF32()))
|
||||
.ToArray();
|
||||
|
||||
uint[] pVertColours = Enumerable.Range(0, numVertexColours)
|
||||
.Select(_ => Util.BSwap(s.ReadU32()))
|
||||
.ToArray();
|
||||
|
||||
uint[] pPolyColours = Enumerable.Range(0, numPolys)
|
||||
.Select(_ => s.ReadU32())
|
||||
.ToArray();
|
||||
|
||||
s.Seek(4 * numEdges, System.IO.SeekOrigin.Current);
|
||||
|
||||
Polygon[] pPolygons = Enumerable.Range(0, numPolys)
|
||||
.Select(_ => new Polygon(s))
|
||||
.ToArray();
|
||||
|
||||
s.Seek(24 * numUnknown2, System.IO.SeekOrigin.Current);
|
||||
|
||||
s.Seek(3 * numUnknown3, System.IO.SeekOrigin.Current);
|
||||
|
||||
s.Seek(100 * numHundreds, System.IO.SeekOrigin.Current);
|
||||
|
||||
Group[] pGroups = Enumerable.Range(0, numGroups)
|
||||
.Select(_ => new Group(s))
|
||||
.ToArray();
|
||||
|
||||
List<Vector3> verts = new List<Vector3>(),
|
||||
normals = new List<Vector3>(),
|
||||
texcoords = new List<Vector3>();
|
||||
List<uint> colours = new List<uint>();
|
||||
|
||||
foreach (var group in pGroups) {
|
||||
Dictionary<(int, int), int> vertMap = new();
|
||||
List<PFileVert> gverts = new();
|
||||
List<int> gindices = new();
|
||||
|
||||
void Emit(int v, int n) {
|
||||
if (vertMap.TryGetValue((v, n), out int i))
|
||||
gindices.Add(i);
|
||||
else {
|
||||
vertMap[(v, n)] = gverts.Count;
|
||||
gindices.Add(gverts.Count);
|
||||
var pv = new PFileVert {
|
||||
Position = pVerts[group.VerticesStartIndex + v],
|
||||
Normal = pNormals[n],
|
||||
Colour = pVertColours[group.VerticesStartIndex + v]
|
||||
};
|
||||
if (group.AreTexturesUsed != 0)
|
||||
pv.TexCoord = pTexCoord[group.TexCoordStartIndex + v];
|
||||
gverts.Add(pv);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (int iPoly in Enumerable.Range(group.PolygonStartIndex, group.NumPolygons)) {
|
||||
var poly = pPolygons[iPoly];
|
||||
Emit(poly.V0, poly.N0);
|
||||
Emit(poly.V2, poly.N2);
|
||||
Emit(poly.V1, poly.N1);
|
||||
}
|
||||
|
||||
Chunks.Add(new PFileChunk(
|
||||
group.AreTexturesUsed != 0 ? group.TextureNumber : null,
|
||||
gverts,
|
||||
gindices
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
Ficedula.FF7/Streams.cs
Normal file
102
Ficedula.FF7/Streams.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7 {
|
||||
public static class Streams {
|
||||
|
||||
public static int ReadI32(this Stream s) {
|
||||
byte[] buffer = new byte[4];
|
||||
s.Read(buffer, 0, 4);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
public static uint ReadU32(this Stream s) {
|
||||
byte[] buffer = new byte[4];
|
||||
s.Read(buffer, 0, 4);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
|
||||
public static void WriteI32(this Stream s, int i) {
|
||||
byte[] buffer = BitConverter.GetBytes(i);
|
||||
s.Write(buffer, 0, 4);
|
||||
}
|
||||
|
||||
public static short ReadI16(this Stream s) {
|
||||
byte[] buffer = new byte[16];
|
||||
s.Read(buffer, 0, 2);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
public static ushort ReadU16(this Stream s) {
|
||||
byte[] buffer = new byte[16];
|
||||
s.Read(buffer, 0, 2);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
|
||||
public static byte ReadU8(this Stream s) {
|
||||
return (byte)s.ReadByte();
|
||||
}
|
||||
|
||||
public static void WriteI16(this Stream s, short i) {
|
||||
byte[] buffer = BitConverter.GetBytes(i);
|
||||
s.Write(buffer, 0, 2);
|
||||
}
|
||||
|
||||
public static string ReadS(this Stream s) {
|
||||
byte[] data = new byte[s.ReadI32()];
|
||||
s.Read(data, 0, data.Length);
|
||||
return Encoding.Unicode.GetString(data);
|
||||
}
|
||||
public static string ReadAscii(this Stream s, int len) {
|
||||
byte[] data = new byte[len];
|
||||
s.Read(data, 0, data.Length);
|
||||
return Encoding.ASCII.GetString(data).Trim();
|
||||
}
|
||||
|
||||
public static void WriteS(this Stream s, string str) {
|
||||
byte[] data = Encoding.Unicode.GetBytes(str);
|
||||
s.WriteI32(data.Length);
|
||||
s.Write(data, 0, data.Length);
|
||||
}
|
||||
|
||||
public static void WriteF(this Stream s, double f) {
|
||||
byte[] data = BitConverter.GetBytes(f);
|
||||
s.Write(data, 0, 8);
|
||||
}
|
||||
|
||||
public static double ReadF64(this Stream s) {
|
||||
byte[] data = new byte[8];
|
||||
s.Read(data, 0, 8);
|
||||
return BitConverter.ToDouble(data, 0);
|
||||
}
|
||||
public static float ReadF32(this Stream s) {
|
||||
byte[] data = new byte[4];
|
||||
s.Read(data, 0, 4);
|
||||
return BitConverter.ToSingle(data, 0);
|
||||
}
|
||||
|
||||
public static void WriteG(this Stream s, Guid g) {
|
||||
s.Write(g.ToByteArray(), 0, 16);
|
||||
}
|
||||
|
||||
public static Guid ReadG(this Stream s) {
|
||||
byte[] data = new byte[16];
|
||||
s.Read(data, 0, 16);
|
||||
return new Guid(data);
|
||||
}
|
||||
|
||||
public static string ReadAllText(this Stream s) {
|
||||
using (var sr = new StreamReader(s))
|
||||
return sr.ReadToEnd();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ReadAllLines(this Stream s) {
|
||||
using (var sr = new StreamReader(s)) {
|
||||
string? line;
|
||||
while ((line = sr.ReadLine()) != null)
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
Ficedula.FF7/TexFile.cs
Normal file
54
Ficedula.FF7/TexFile.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7 {
|
||||
public class TexFile {
|
||||
|
||||
public List<uint[]> Palettes { get; } = new();
|
||||
public List<byte[]> Pixels { get; }
|
||||
|
||||
public int Width => Pixels[0].Length;
|
||||
public int Height => Pixels.Count;
|
||||
|
||||
public TexFile(Stream source) {
|
||||
source.Position = 0x30;
|
||||
int numPalettes = source.ReadI32();
|
||||
int colours = source.ReadI32();
|
||||
|
||||
source.Position = 0x3C;
|
||||
int width = source.ReadI32();
|
||||
int height = source.ReadI32();
|
||||
|
||||
source.Position = 0x58;
|
||||
int paletteSize = source.ReadI32() * 4;
|
||||
|
||||
foreach(int p in Enumerable.Range(0, numPalettes)) {
|
||||
source.Position = 0xEC + colours * 4 * p;
|
||||
Palettes.Add(
|
||||
Enumerable.Range(0, colours)
|
||||
.Select(_ => Util.BSwap(source.ReadU32()))
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
source.Position = 0xEC + paletteSize;
|
||||
Pixels = Enumerable.Range(0, height)
|
||||
.Select(_ =>
|
||||
Enumerable.Range(0, width)
|
||||
.Select(__ => source.ReadU8())
|
||||
.ToArray()
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<uint[]> ApplyPalette(int which) {
|
||||
var palette = Palettes[which];
|
||||
return Pixels
|
||||
.Select(row => row.Select(b => palette[b]).ToArray())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
64
Ficedula.FF7/Text.cs
Normal file
64
Ficedula.FF7/Text.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7 {
|
||||
public static class Text {
|
||||
private static char[] _translate = new[] {
|
||||
' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
|
||||
'@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_',
|
||||
'`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', ' ', '}', '~', ' ',
|
||||
'¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'¬', '¬', '“', '”', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
' ', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬',
|
||||
'\xE000', '\t', ',', '\xE001', '\xE002', '¬', '¬', '\xE003', '\xE004', '\xE005', '\xE006', '\xE007', '\xE008', '\xE009', '\xE00A', '\xE00B',
|
||||
'\xE00C', '\xE00D', '\xE00E', '\xE00F', '\xE010', '\xE011', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '¬', '\xE012', '\xE013',
|
||||
};
|
||||
|
||||
public static string Convert(byte[] input, int offset, int length) {
|
||||
char[] c = Enumerable.Range(offset, length)
|
||||
.Select(i => _translate[input[i]])
|
||||
.ToArray();
|
||||
return new string(c);
|
||||
}
|
||||
|
||||
public static string Expand(string input, string[] charNames, string[] partyNames) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in input) {
|
||||
switch (c) {
|
||||
case '\xE001':
|
||||
sb.Append(".\""); break;
|
||||
case '\xE002':
|
||||
sb.Append("...\""); break;
|
||||
case '\xE006':
|
||||
case '\xE007':
|
||||
case '\xE008':
|
||||
case '\xE009':
|
||||
case '\xE00A':
|
||||
case '\xE00B':
|
||||
case '\xE00C':
|
||||
case '\xE00D':
|
||||
case '\xE00E':
|
||||
sb.Append(charNames[c - 0xE006]); break;
|
||||
case '\xE00F':
|
||||
case '\xE010':
|
||||
case '\xE011':
|
||||
sb.Append(partyNames[c - 0xE00F]); break;
|
||||
default:
|
||||
sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
22
Ficedula.FF7/Util.cs
Normal file
22
Ficedula.FF7/Util.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ficedula.FF7 {
|
||||
public class FFException : Exception {
|
||||
public FFException(string msg) : base(msg) { }
|
||||
}
|
||||
|
||||
public static class Util {
|
||||
public static T? ValueOrNull<T>(T value, T nullValue) where T : struct {
|
||||
return value.Equals(nullValue) ? null : value;
|
||||
}
|
||||
|
||||
public static uint BSwap(uint i) {
|
||||
return (i & 0xff00ff00) | ((i & 0xff) << 16) | ((i >> 16) & 0xff);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
83
data/save/newgame.xml
Normal file
83
data/save/newgame.xml
Normal file
@ -0,0 +1,83 @@
|
||||
<SaveData>
|
||||
<Characters>
|
||||
<Character>
|
||||
<CharIndex>0</CharIndex>
|
||||
<Level>8</Level>
|
||||
<Strength>40</Strength>
|
||||
<Vitality>42</Vitality>
|
||||
<Magic>38</Magic>
|
||||
<Spirit>35</Spirit>
|
||||
<Dexterity>40</Dexterity>
|
||||
<Luck>30</Luck>
|
||||
<StrBonus>0</StrBonus>
|
||||
<VitBonus>0</VitBonus>
|
||||
<MagBonus>0</MagBonus>
|
||||
<SprBonus>0</SprBonus>
|
||||
<DexBonus>0</DexBonus>
|
||||
<LckBonus>0</LckBonus>
|
||||
<LimitLevel>1</LimitLevel>
|
||||
<LimitBar>128</LimitBar>
|
||||
<Name>Cloud</Name>
|
||||
<EquipWeapon>0</EquipWeapon>
|
||||
<EquipArmour>0</EquipArmour>
|
||||
<EquipAccessor>0</EquipAccessor>
|
||||
<Flags>Available Party1</Flags>
|
||||
<LimitBreaks>Limit1_1</LimitBreaks>
|
||||
<NumKills>10</NumKills>
|
||||
<UsedLimit1_1>4</UsedLimit1_1>
|
||||
<UsedLimit2_1>0</UsedLimit2_1>
|
||||
<UsedLimit3_1>0</UsedLimit3_1>
|
||||
<CurrentHP>310</CurrentHP>
|
||||
<BaseHP>340</BaseHP>
|
||||
<MaxHP>335</MaxHP>
|
||||
<CurrentMP>30</CurrentMP>
|
||||
<BaseMP>55</BaseMP>
|
||||
<MaxMP>60</MaxMP>
|
||||
<XP>8000</XP>
|
||||
<XPTNL>500</XPTNL>
|
||||
<WeaponMateria>0</WeaponMateria>
|
||||
<WeaponMateria>1</WeaponMateria>
|
||||
<ArmourMateria>2</ArmourMateria>
|
||||
</Character>
|
||||
<Character>
|
||||
<CharIndex>3</CharIndex>
|
||||
<Level>8</Level>
|
||||
<Strength>35</Strength>
|
||||
<Vitality>40</Vitality>
|
||||
<Magic>48</Magic>
|
||||
<Spirit>45</Spirit>
|
||||
<Dexterity>40</Dexterity>
|
||||
<Luck>38</Luck>
|
||||
<StrBonus>0</StrBonus>
|
||||
<VitBonus>0</VitBonus>
|
||||
<MagBonus>0</MagBonus>
|
||||
<SprBonus>0</SprBonus>
|
||||
<DexBonus>0</DexBonus>
|
||||
<LckBonus>0</LckBonus>
|
||||
<LimitLevel>1</LimitLevel>
|
||||
<LimitBar>20</LimitBar>
|
||||
<Name>Aeris</Name>
|
||||
<EquipWeapon>0</EquipWeapon>
|
||||
<EquipArmour>0</EquipArmour>
|
||||
<EquipAccessor>0</EquipAccessor>
|
||||
<Flags>Available Party2 BackRow</Flags>
|
||||
<LimitBreaks>Limit1_1</LimitBreaks>
|
||||
<NumKills>5</NumKills>
|
||||
<UsedLimit1_1>3</UsedLimit1_1>
|
||||
<UsedLimit2_1>0</UsedLimit2_1>
|
||||
<UsedLimit3_1>0</UsedLimit3_1>
|
||||
<CurrentHP>190</CurrentHP>
|
||||
<BaseHP>320</BaseHP>
|
||||
<MaxHP>315</MaxHP>
|
||||
<CurrentMP>40</CurrentMP>
|
||||
<BaseMP>60</BaseMP>
|
||||
<MaxMP>65</MaxMP>
|
||||
<XP>8000</XP>
|
||||
<XPTNL>500</XPTNL>
|
||||
<WeaponMateria>0</WeaponMateria>
|
||||
<WeaponMateria>1</WeaponMateria>
|
||||
<ArmourMateria>2</ArmourMateria>
|
||||
</Character>
|
||||
</Characters>
|
||||
<Location>Train Station</Location>
|
||||
</SaveData>
|
Loading…
Reference in New Issue
Block a user