This commit is contained in:
ficedula 2022-07-20 19:46:44 +01:00
commit 29a1ec213f
39 changed files with 3867 additions and 0 deletions

4
.hgignore Normal file
View File

@ -0,0 +1,4 @@
syntax: glob
.vs/*
*/obj/*
*/bin/*

43
F7.sln Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

34
F7/Input.cs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}
}
}

View File

@ -0,0 +1,8 @@
{
"profiles": {
"F7Cmd": {
"commandName": "Project",
"commandLineArgs": "Field C:\\games\\FF7\\data\\field\\flevel.lgp mrkt2"
}
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View 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);
}
*/
}
}
}

View 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
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>